mirror of
https://github.com/suitenumerique/meet.git
synced 2026-07-27 20:29:09 +00:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6b9ffc0c23 | |||
| 7665de1d0c | |||
| d8ed15ec25 | |||
| 98fba525ad | |||
| b87dfc5f15 | |||
| 067cabfa92 | |||
| 4a291690b4 | |||
| 33a6d2f47f | |||
| 27fe0d8eff | |||
| 8f6ffc7553 | |||
| 87dd0816f9 | |||
| 969435fc23 | |||
| 698aef8e72 |
@@ -21,4 +21,4 @@ COPY --from=builder /install /usr/local
|
|||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
CMD ["python", "multi-user-transcriber.py", "start"]
|
CMD ["python", "multi_user_transcriber.py", "start"]
|
||||||
|
|||||||
@@ -0,0 +1,391 @@
|
|||||||
|
"""Metadata agent that extracts metadata from active room."""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
from dataclasses import asdict, dataclass
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from io import BytesIO
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from livekit import api, rtc
|
||||||
|
from livekit.agents import (
|
||||||
|
Agent,
|
||||||
|
AgentSession,
|
||||||
|
AutoSubscribe,
|
||||||
|
JobContext,
|
||||||
|
JobProcess,
|
||||||
|
JobRequest,
|
||||||
|
RoomInputOptions,
|
||||||
|
RoomIO,
|
||||||
|
RoomOutputOptions,
|
||||||
|
WorkerOptions,
|
||||||
|
WorkerPermissions,
|
||||||
|
cli,
|
||||||
|
utils,
|
||||||
|
)
|
||||||
|
from livekit.plugins import silero
|
||||||
|
from minio import Minio
|
||||||
|
from minio.error import S3Error
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
logger = logging.getLogger("metadata-extractor")
|
||||||
|
|
||||||
|
AGENT_NAME = os.getenv("ROOM_METADATA_EXTRACTOR_AGENT_NAME", "metadata-extractor")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MetadataEvent:
|
||||||
|
"""Wip."""
|
||||||
|
|
||||||
|
participant_id: str
|
||||||
|
type: str
|
||||||
|
timestamp: datetime
|
||||||
|
data: Optional[str] = None
|
||||||
|
|
||||||
|
def serialize(self) -> dict:
|
||||||
|
"""Return a JSON-serializable dictionary representation of the event."""
|
||||||
|
data = asdict(self)
|
||||||
|
data["timestamp"] = self.timestamp.isoformat()
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
class VADAgent(Agent):
|
||||||
|
"""Agent that monitors voice activity for a specific participant."""
|
||||||
|
|
||||||
|
def __init__(self, participant_identity: str, events: List):
|
||||||
|
"""Wip."""
|
||||||
|
super().__init__(
|
||||||
|
instructions="not-needed",
|
||||||
|
)
|
||||||
|
self.participant_identity = participant_identity
|
||||||
|
self.events = events
|
||||||
|
|
||||||
|
async def on_enter(self) -> None:
|
||||||
|
"""Initialize VAD monitoring for this participant."""
|
||||||
|
|
||||||
|
@self.session.on("user_state_changed")
|
||||||
|
def on_user_state(event):
|
||||||
|
timestamp = datetime.now(timezone.utc)
|
||||||
|
|
||||||
|
if event.new_state == "speaking":
|
||||||
|
event = MetadataEvent(
|
||||||
|
participant_id=self.participant_identity,
|
||||||
|
type="speech_start",
|
||||||
|
timestamp=timestamp,
|
||||||
|
)
|
||||||
|
self.events.append(event)
|
||||||
|
|
||||||
|
elif event.old_state == "speaking":
|
||||||
|
event = MetadataEvent(
|
||||||
|
participant_id=self.participant_identity,
|
||||||
|
type="speech_end",
|
||||||
|
timestamp=timestamp,
|
||||||
|
)
|
||||||
|
self.events.append(event)
|
||||||
|
|
||||||
|
|
||||||
|
class MetadataAgent:
|
||||||
|
"""Monitor and manage real-time metadata extraction from meeting rooms.
|
||||||
|
|
||||||
|
Oversees VAD (Voice Activity Detection) and participant metadata streams
|
||||||
|
to track and analyze real-time events, coordinating data collection across
|
||||||
|
participants for insights like speaking activity and engagement.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, ctx: JobContext, recording_id: str):
|
||||||
|
"""Initialize metadata agent."""
|
||||||
|
self.minio_client = Minio(
|
||||||
|
endpoint=os.getenv("AWS_S3_ENDPOINT_URL"),
|
||||||
|
access_key=os.getenv("AWS_S3_ACCESS_KEY_ID"),
|
||||||
|
secret_key=os.getenv("AWS_S3_SECRET_ACCESS_KEY"),
|
||||||
|
secure=os.getenv("AWS_S3_SECURE_ACCESS", "False").lower() == "true",
|
||||||
|
)
|
||||||
|
|
||||||
|
# todo - raise error if none
|
||||||
|
self.bucket_name = os.getenv("AWS_STORAGE_BUCKET_NAME")
|
||||||
|
|
||||||
|
self.ctx = ctx
|
||||||
|
self._sessions: dict[str, AgentSession] = {}
|
||||||
|
self._tasks: set[asyncio.Task] = set()
|
||||||
|
|
||||||
|
self.output_filename = (
|
||||||
|
f"{os.getenv('AWS_S3_OUTPUT_FOLDER', 'metadata')}/{recording_id}-metadata.json"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Storage for events
|
||||||
|
self.events = []
|
||||||
|
self.participants = {}
|
||||||
|
|
||||||
|
logger.info("MetadataAgent initialized")
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
"""Start listening for participant connection events."""
|
||||||
|
self.ctx.room.on("participant_connected", self.on_participant_connected)
|
||||||
|
self.ctx.room.on("participant_disconnected", self.on_participant_disconnected)
|
||||||
|
self.ctx.room.on("participant_name_changed", self.on_participant_name_changed)
|
||||||
|
|
||||||
|
self.ctx.room.register_text_stream_handler("lk.chat", self.handle_chat_stream)
|
||||||
|
|
||||||
|
logger.info("Started listening for participant events")
|
||||||
|
|
||||||
|
async def on_chat_message_received(
|
||||||
|
self, reader: rtc.TextStreamReader, participant_identity: str
|
||||||
|
):
|
||||||
|
"""Wip."""
|
||||||
|
full_text = await reader.read_all()
|
||||||
|
logger.info(
|
||||||
|
"Received chat message from %s: '%s'", participant_identity, full_text
|
||||||
|
)
|
||||||
|
|
||||||
|
self.events.append(
|
||||||
|
MetadataEvent(
|
||||||
|
participant_id=participant_identity,
|
||||||
|
type="chat_received",
|
||||||
|
timestamp=datetime.now(timezone.utc),
|
||||||
|
data=full_text,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def handle_chat_stream(self, reader, participant_identity):
|
||||||
|
"""Wip."""
|
||||||
|
task = asyncio.create_task(
|
||||||
|
self.on_chat_message_received(reader, participant_identity)
|
||||||
|
)
|
||||||
|
self._tasks.add(task)
|
||||||
|
task.add_done_callback(lambda _: self._tasks.remove(task))
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
"""Wip."""
|
||||||
|
logger.info("Persisting processed metadata output to disk…")
|
||||||
|
|
||||||
|
participants = []
|
||||||
|
for k, v in self.participants.items():
|
||||||
|
participants.append({"participantId": k, "name": v})
|
||||||
|
|
||||||
|
sorted_event = sorted(self.events, key=lambda e: e.timestamp)
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"events": [event.serialize() for event in sorted_event],
|
||||||
|
"participants": participants,
|
||||||
|
}
|
||||||
|
|
||||||
|
data = json.dumps(payload, indent=2).encode("utf-8")
|
||||||
|
stream = BytesIO(data)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.minio_client.put_object(
|
||||||
|
self.bucket_name,
|
||||||
|
self.output_filename,
|
||||||
|
stream,
|
||||||
|
length=len(data),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
logger.info(
|
||||||
|
"Uploaded speaker meeting metadata",
|
||||||
|
)
|
||||||
|
except S3Error:
|
||||||
|
logger.exception(
|
||||||
|
"Failed to upload meeting metadata",
|
||||||
|
)
|
||||||
|
|
||||||
|
async def aclose(self):
|
||||||
|
"""Close all sessions and cleanup resources."""
|
||||||
|
logger.info("Closing all VAD monitoring sessions…")
|
||||||
|
|
||||||
|
await utils.aio.cancel_and_wait(*self._tasks)
|
||||||
|
|
||||||
|
await asyncio.gather(
|
||||||
|
*[self._close_session(session) for session in self._sessions.values()],
|
||||||
|
return_exceptions=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.ctx.room.off("participant_connected", self.on_participant_connected)
|
||||||
|
self.ctx.room.off("participant_disconnected", self.on_participant_disconnected)
|
||||||
|
self.ctx.room.off("participant_name_changed", self.on_participant_name_changed)
|
||||||
|
|
||||||
|
logger.info("All VAD sessions closed")
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
def on_participant_connected(self, participant: rtc.RemoteParticipant):
|
||||||
|
"""Handle new participant connection by starting VAD monitoring."""
|
||||||
|
if participant.identity in self._sessions:
|
||||||
|
logger.debug("Session already exists for %s", participant.identity)
|
||||||
|
return
|
||||||
|
|
||||||
|
self.events.append(
|
||||||
|
MetadataEvent(
|
||||||
|
participant_id=participant.identity,
|
||||||
|
type="participant_connected",
|
||||||
|
timestamp=datetime.now(timezone.utc),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.participants[participant.identity] = participant.name
|
||||||
|
|
||||||
|
logger.info("New participant connected: %s", participant.identity)
|
||||||
|
task = asyncio.create_task(self._start_session(participant))
|
||||||
|
self._tasks.add(task)
|
||||||
|
|
||||||
|
def on_task_done(task: asyncio.Task):
|
||||||
|
try:
|
||||||
|
self._sessions[participant.identity] = task.result()
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Failed to start session for %s", participant.identity)
|
||||||
|
finally:
|
||||||
|
self._tasks.discard(task)
|
||||||
|
|
||||||
|
task.add_done_callback(on_task_done)
|
||||||
|
|
||||||
|
def on_participant_disconnected(self, participant: rtc.RemoteParticipant):
|
||||||
|
"""Handle participant disconnection by closing VAD monitoring."""
|
||||||
|
self.events.append(
|
||||||
|
MetadataEvent(
|
||||||
|
participant_id=participant.identity,
|
||||||
|
type="participant_disconnected",
|
||||||
|
timestamp=datetime.now(timezone.utc),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
session = self._sessions.pop(participant.identity, None)
|
||||||
|
if session is None:
|
||||||
|
logger.debug("No session found for %s", participant.identity)
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info("Participant disconnected: %s", participant.identity)
|
||||||
|
task = asyncio.create_task(self._close_session(session))
|
||||||
|
self._tasks.add(task)
|
||||||
|
|
||||||
|
def on_close_done(_):
|
||||||
|
self._tasks.discard(task)
|
||||||
|
logger.info(
|
||||||
|
"VAD session closed for %s (remaining sessions: %d)",
|
||||||
|
participant.identity,
|
||||||
|
len(self._sessions),
|
||||||
|
)
|
||||||
|
|
||||||
|
task.add_done_callback(on_close_done)
|
||||||
|
|
||||||
|
def on_participant_name_changed(self, participant: rtc.RemoteParticipant):
|
||||||
|
"""Wip."""
|
||||||
|
logger.info("Participant's name changed: %s", participant.identity)
|
||||||
|
self.participants[participant.identity] = participant.name
|
||||||
|
|
||||||
|
async def _start_session(self, participant: rtc.RemoteParticipant) -> AgentSession:
|
||||||
|
"""Create and start VAD monitoring session for participant."""
|
||||||
|
if participant.identity in self._sessions:
|
||||||
|
return self._sessions[participant.identity]
|
||||||
|
|
||||||
|
# Create session with VAD only - no STT, LLM, or TTS
|
||||||
|
session = AgentSession(
|
||||||
|
vad=self.ctx.proc.userdata["vad"],
|
||||||
|
turn_detection="vad",
|
||||||
|
user_away_timeout=30.0,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set up room IO to receive audio from this specific participant
|
||||||
|
room_io = RoomIO(
|
||||||
|
agent_session=session,
|
||||||
|
room=self.ctx.room,
|
||||||
|
participant=participant,
|
||||||
|
input_options=RoomInputOptions(
|
||||||
|
audio_enabled=True,
|
||||||
|
text_enabled=False,
|
||||||
|
),
|
||||||
|
output_options=RoomOutputOptions(
|
||||||
|
audio_enabled=False,
|
||||||
|
transcription_enabled=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
await room_io.start()
|
||||||
|
await session.start(
|
||||||
|
agent=VADAgent(
|
||||||
|
participant_identity=participant.identity, events=self.events
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return session
|
||||||
|
|
||||||
|
async def _close_session(self, session: AgentSession) -> None:
|
||||||
|
"""Close and cleanup VAD monitoring session."""
|
||||||
|
try:
|
||||||
|
await session.drain()
|
||||||
|
await session.aclose()
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Error closing session")
|
||||||
|
|
||||||
|
|
||||||
|
async def entrypoint(ctx: JobContext):
|
||||||
|
"""Initialize and run the multi-user VAD monitor."""
|
||||||
|
logger.info("Starting metadata agent in room: %s", ctx.room.name)
|
||||||
|
recording_id = ctx.job.metadata
|
||||||
|
vad_monitor = MetadataAgent(ctx, recording_id)
|
||||||
|
vad_monitor.start()
|
||||||
|
|
||||||
|
# Connect to room and subscribe to audio only
|
||||||
|
await ctx.connect(auto_subscribe=AutoSubscribe.AUDIO_ONLY)
|
||||||
|
|
||||||
|
existing_participants = list(ctx.room.remote_participants.values())
|
||||||
|
for participant in existing_participants:
|
||||||
|
vad_monitor.on_participant_connected(participant)
|
||||||
|
|
||||||
|
async def cleanup():
|
||||||
|
logger.info("Shutting down VAD monitor...")
|
||||||
|
await vad_monitor.aclose()
|
||||||
|
|
||||||
|
ctx.add_shutdown_callback(cleanup)
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_job_request(job_req: JobRequest) -> None:
|
||||||
|
"""Accept or reject the job request based on agent presence in the room."""
|
||||||
|
room_name = job_req.room.name
|
||||||
|
recording_id = job_req.job.metadata
|
||||||
|
agent_identity = f"{AGENT_NAME}-{room_name}"
|
||||||
|
|
||||||
|
async with api.LiveKitAPI() as lk:
|
||||||
|
try:
|
||||||
|
resp = await lk.room.list_participants(
|
||||||
|
list=api.ListParticipantsRequest(room=room_name)
|
||||||
|
)
|
||||||
|
already_present = any(
|
||||||
|
p.kind == rtc.ParticipantKind.PARTICIPANT_KIND_AGENT
|
||||||
|
and p.identity == agent_identity
|
||||||
|
for p in resp.participants
|
||||||
|
)
|
||||||
|
if already_present:
|
||||||
|
logger.info("Agent already in the room '%s' — reject", room_name)
|
||||||
|
await job_req.reject()
|
||||||
|
else:
|
||||||
|
logger.info(
|
||||||
|
"Accept job for '%s' — identity=%s", room_name, agent_identity
|
||||||
|
)
|
||||||
|
await job_req.accept(identity=agent_identity, metadata=recording_id)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Error treating the job for '%s'", room_name)
|
||||||
|
await job_req.reject()
|
||||||
|
|
||||||
|
|
||||||
|
def prewarm(proc: JobProcess):
|
||||||
|
"""Preload voice activity detection model."""
|
||||||
|
proc.userdata["vad"] = silero.VAD.load()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
cli.run_app(
|
||||||
|
WorkerOptions(
|
||||||
|
entrypoint_fnc=entrypoint,
|
||||||
|
prewarm_fnc=prewarm,
|
||||||
|
request_fnc=handle_job_request,
|
||||||
|
agent_name=AGENT_NAME,
|
||||||
|
permissions=WorkerPermissions(
|
||||||
|
can_publish=False,
|
||||||
|
can_publish_data=False,
|
||||||
|
can_subscribe=True,
|
||||||
|
hidden=True,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -7,7 +7,8 @@ dependencies = [
|
|||||||
"livekit-agents==1.2.6",
|
"livekit-agents==1.2.6",
|
||||||
"livekit-plugins-deepgram==1.2.6",
|
"livekit-plugins-deepgram==1.2.6",
|
||||||
"livekit-plugins-silero==1.2.6",
|
"livekit-plugins-silero==1.2.6",
|
||||||
"python-dotenv==1.1.1"
|
"python-dotenv==1.1.1",
|
||||||
|
"minio==7.2.15"
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
|
|||||||
@@ -31,6 +31,10 @@ from core.recording.event.exceptions import (
|
|||||||
)
|
)
|
||||||
from core.recording.event.notification import notification_service
|
from core.recording.event.notification import notification_service
|
||||||
from core.recording.event.parsers import get_parser
|
from core.recording.event.parsers import get_parser
|
||||||
|
from core.recording.services.metadata_extractor import (
|
||||||
|
MetadataExtractorException,
|
||||||
|
MetadataExtractorService,
|
||||||
|
)
|
||||||
from core.recording.worker.exceptions import (
|
from core.recording.worker.exceptions import (
|
||||||
RecordingStartError,
|
RecordingStartError,
|
||||||
RecordingStopError,
|
RecordingStopError,
|
||||||
@@ -328,6 +332,15 @@ class RoomViewSet(
|
|||||||
status=drf_status.HTTP_500_INTERNAL_SERVER_ERROR,
|
status=drf_status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
settings.ROOM_METADATA_EXTRACTOR_ENABLED
|
||||||
|
and recording.mode == models.RecordingModeChoices.TRANSCRIPT
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
MetadataExtractorService().start(recording)
|
||||||
|
except MetadataExtractorException:
|
||||||
|
pass
|
||||||
|
|
||||||
return drf_response.Response(
|
return drf_response.Response(
|
||||||
{"message": f"Recording successfully started for room {room.slug}"},
|
{"message": f"Recording successfully started for room {room.slug}"},
|
||||||
status=drf_status.HTTP_201_CREATED,
|
status=drf_status.HTTP_201_CREATED,
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
"""Wip."""
|
||||||
|
|
||||||
|
from logging import getLogger
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
from asgiref.sync import async_to_sync
|
||||||
|
from livekit.protocol.agent_dispatch import (
|
||||||
|
CreateAgentDispatchRequest,
|
||||||
|
)
|
||||||
|
|
||||||
|
from core import utils
|
||||||
|
|
||||||
|
logger = getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class MetadataExtractorException(Exception):
|
||||||
|
"""Wip."""
|
||||||
|
|
||||||
|
|
||||||
|
class MetadataExtractorService:
|
||||||
|
"""Wip."""
|
||||||
|
|
||||||
|
@async_to_sync
|
||||||
|
async def start(self, recording):
|
||||||
|
"""Wip."""
|
||||||
|
|
||||||
|
lkapi = utils.create_livekit_client()
|
||||||
|
room_id = str(recording.room.id)
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = await lkapi.agent_dispatch.create_dispatch(
|
||||||
|
CreateAgentDispatchRequest(
|
||||||
|
agent_name=settings.ROOM_METADATA_EXTRACTOR_AGENT_NAME,
|
||||||
|
room=room_id,
|
||||||
|
metadata=str(recording.id),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(
|
||||||
|
"Failed to create metadata extractor agent for room %s", room_id
|
||||||
|
)
|
||||||
|
raise MetadataExtractorException(
|
||||||
|
"Failed to create metadata extractor agent"
|
||||||
|
) from e
|
||||||
|
finally:
|
||||||
|
await lkapi.aclose()
|
||||||
|
|
||||||
|
dispatch_id = getattr(response, "id", None)
|
||||||
|
|
||||||
|
if not dispatch_id:
|
||||||
|
logger.error("LiveKit response missing dispatch ID for room %s", room_id)
|
||||||
|
raise MetadataExtractorException(
|
||||||
|
f"LiveKit did not return a dispatch_id for room {room_id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return dispatch_id
|
||||||
|
|
||||||
|
@async_to_sync
|
||||||
|
async def stop(self, recording):
|
||||||
|
"""Wip."""
|
||||||
|
|
||||||
|
room_name = str(recording.room.id)
|
||||||
|
lkapi = utils.create_livekit_client()
|
||||||
|
|
||||||
|
try:
|
||||||
|
dispatches = await lkapi.agent_dispatch.list_dispatch(room_name=room_name)
|
||||||
|
|
||||||
|
dispatch_id = next(
|
||||||
|
(
|
||||||
|
d.id
|
||||||
|
for d in dispatches
|
||||||
|
if d.agent_name == settings.ROOM_METADATA_EXTRACTOR_AGENT_NAME
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not dispatch_id:
|
||||||
|
logger.warning(
|
||||||
|
"No metadata extractor agent found for room %s", room_name
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
await lkapi.agent_dispatch.delete_dispatch(
|
||||||
|
dispatch_id=str(dispatch_id), room_name=room_name
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(
|
||||||
|
"Failed to stop metadata extractor agent dispatch for room %s",
|
||||||
|
room_name,
|
||||||
|
)
|
||||||
|
raise MetadataExtractorException(
|
||||||
|
f"Failed to stop metadata metadata extractor agent for room {room_name}"
|
||||||
|
) from e
|
||||||
|
finally:
|
||||||
|
await lkapi.aclose()
|
||||||
@@ -11,6 +11,10 @@ from django.conf import settings
|
|||||||
from livekit import api
|
from livekit import api
|
||||||
|
|
||||||
from core import models
|
from core import models
|
||||||
|
from core.recording.services.metadata_extractor import (
|
||||||
|
MetadataExtractorException,
|
||||||
|
MetadataExtractorService,
|
||||||
|
)
|
||||||
from core.recording.services.recording_events import (
|
from core.recording.services.recording_events import (
|
||||||
RecordingEventsError,
|
RecordingEventsError,
|
||||||
RecordingEventsService,
|
RecordingEventsService,
|
||||||
@@ -126,7 +130,7 @@ class LiveKitEventsService:
|
|||||||
"""Handle 'egress_ended' event."""
|
"""Handle 'egress_ended' event."""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
recording = models.Recording.objects.get(
|
recording = models.Recording.objects.select_related("room").get(
|
||||||
worker_id=data.egress_info.egress_id
|
worker_id=data.egress_info.egress_id
|
||||||
)
|
)
|
||||||
except models.Recording.DoesNotExist as err:
|
except models.Recording.DoesNotExist as err:
|
||||||
@@ -134,6 +138,15 @@ class LiveKitEventsService:
|
|||||||
f"Recording with worker ID {data.egress_info.egress_id} does not exist"
|
f"Recording with worker ID {data.egress_info.egress_id} does not exist"
|
||||||
) from err
|
) from err
|
||||||
|
|
||||||
|
if (
|
||||||
|
settings.ROOM_METADATA_EXTRACTOR_ENABLED
|
||||||
|
and recording.mode == models.RecordingModeChoices.TRANSCRIPT
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
MetadataExtractorService().stop(recording)
|
||||||
|
except MetadataExtractorException:
|
||||||
|
pass
|
||||||
|
|
||||||
if (
|
if (
|
||||||
data.egress_info.status == api.EgressStatus.EGRESS_LIMIT_REACHED
|
data.egress_info.status == api.EgressStatus.EGRESS_LIMIT_REACHED
|
||||||
and recording.status == models.RecordingStatusChoices.ACTIVE
|
and recording.status == models.RecordingStatusChoices.ACTIVE
|
||||||
|
|||||||
@@ -668,6 +668,16 @@ class Base(Configuration):
|
|||||||
environ_prefix=None,
|
environ_prefix=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Metadata Extractor settings
|
||||||
|
ROOM_METADATA_EXTRACTOR_ENABLED = values.BooleanValue(
|
||||||
|
False, environ_name="ROOM_METADATA_EXTRACTOR_ENABLED", environ_prefix=None
|
||||||
|
)
|
||||||
|
ROOM_METADATA_EXTRACTOR_AGENT_NAME = values.Value(
|
||||||
|
"metadata-extractor",
|
||||||
|
environ_name="ROOM_METADATA_EXTRACTOR_AGENT_NAME",
|
||||||
|
environ_prefix=None,
|
||||||
|
)
|
||||||
|
|
||||||
# External Applications
|
# External Applications
|
||||||
APPLICATION_CLIENT_ID_LENGTH = values.PositiveIntegerValue(
|
APPLICATION_CLIENT_ID_LENGTH = values.PositiveIntegerValue(
|
||||||
40,
|
40,
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ backend:
|
|||||||
ROOM_TELEPHONY_PHONE_NUMBER: '+33901020304'
|
ROOM_TELEPHONY_PHONE_NUMBER: '+33901020304'
|
||||||
SSL_CERT_FILE: /usr/local/lib/python3.13/site-packages/certifi/cacert.pem
|
SSL_CERT_FILE: /usr/local/lib/python3.13/site-packages/certifi/cacert.pem
|
||||||
ROOM_SUBTITLE_ENABLED: True
|
ROOM_SUBTITLE_ENABLED: True
|
||||||
|
ROOM_METADATA_EXTRACTOR_ENABLED: True
|
||||||
|
|
||||||
|
|
||||||
migrate:
|
migrate:
|
||||||
@@ -262,12 +263,23 @@ agents:
|
|||||||
LIVEKIT_API_KEY: {{ $key }}
|
LIVEKIT_API_KEY: {{ $key }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
AWS_S3_ENDPOINT_URL: minio.meet.svc.cluster.local:9000
|
||||||
|
AWS_S3_ACCESS_KEY_ID: meet
|
||||||
|
AWS_S3_SECRET_ACCESS_KEY: password
|
||||||
|
AWS_S3_SECURE_ACCESS: False
|
||||||
|
AWS_STORAGE_BUCKET_NAME: meet-media-storage
|
||||||
|
AWS_S3_OUTPUT_FOLDER: recordings
|
||||||
|
|
||||||
image:
|
image:
|
||||||
repository: localhost:5001/meet-agents
|
repository: localhost:5001/meet-agents
|
||||||
pullPolicy: Always
|
pullPolicy: Always
|
||||||
tag: "latest"
|
tag: "latest"
|
||||||
|
|
||||||
|
command:
|
||||||
|
- "python"
|
||||||
|
- "metadata_extractor.py"
|
||||||
|
- "start"
|
||||||
|
|
||||||
# Extra volume mounts to manage our local custom CA and avoid to disable ssl
|
# Extra volume mounts to manage our local custom CA and avoid to disable ssl
|
||||||
extraVolumeMounts:
|
extraVolumeMounts:
|
||||||
- name: certs
|
- name: certs
|
||||||
|
|||||||
Reference in New Issue
Block a user