| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240 |
- from __future__ import annotations
- import asyncio
- import logging
- from typing import Any
- from .config import Settings
- from .media import ThreeCXMediaClient
- from .media_session import MediaSession
- from .whisper import WhisperWorker
- logger = logging.getLogger(__name__)
- class MediaManager:
- def __init__(self, settings: Settings):
- self.settings = settings
- self.media = ThreeCXMediaClient(settings)
- self.whisper = WhisperWorker(settings)
- self.session = MediaSession(
- settings,
- self.media,
- self.whisper,
- on_transcript=self._on_transcript,
- )
- self._running = False
- self._poll_task: asyncio.Task | None = None
- self._active: dict[str, asyncio.Task] = {}
- self._completed: set[str] = set()
- # 3CX hat aktuell 8 SC.
- # Maximal 8 MediaSessions dürfen gleichzeitig laufen.
- self._session_semaphore = asyncio.Semaphore(8)
- async def start(self) -> None:
- if self._running:
- return
- self._running = True
- logger.info(
- "MediaManager startet für RoutePoint %s",
- self.settings.threecx_media_routepoint_dn,
- )
- logger.info("Lade Whisper-Modell ...")
- await asyncio.to_thread(
- self.whisper.start
- )
- logger.info("Whisper-Modell geladen")
- self._poll_task = asyncio.create_task(
- self._poll_loop()
- )
- async def stop(self) -> None:
- self._running = False
- if self._poll_task:
- self._poll_task.cancel()
- try:
- await self._poll_task
- except asyncio.CancelledError:
- pass
- self._poll_task = None
- tasks = list(self._active.values())
- if tasks:
- await asyncio.gather(
- *tasks,
- return_exceptions=True,
- )
- self._active.clear()
- self._completed.clear()
- logger.info("MediaManager gestoppt")
- async def _poll_loop(self) -> None:
- while self._running:
- try:
- await self._poll_once()
- except asyncio.CancelledError:
- raise
- except Exception:
- logger.exception(
- "Fehler im MediaManager-Poll"
- )
- await asyncio.sleep(1)
- async def _poll_once(self) -> None:
- participants = await self.media.participants()
- current_keys: set[str] = set()
- for participant in participants:
- key = self._participant_key(participant)
- current_keys.add(key)
- if not self._is_processable(participant):
- continue
- if key in self._active:
- continue
- if key in self._completed:
- continue
- logger.info(
- "Neuer Media-Participant: "
- "call=%s leg=%s participant=%s",
- participant.get("callid"),
- participant.get("legid"),
- participant.get("id"),
- )
- task = asyncio.create_task(
- self._run_session(
- participant,
- key,
- )
- )
- self._active[key] = task
- task.add_done_callback(
- lambda t, k=key:
- self._session_done(k, t)
- )
- self._completed.intersection_update(
- current_keys
- )
- @staticmethod
- def _is_processable(
- participant: dict[str, Any],
- ) -> bool:
- status = str(
- participant.get("status", "")
- ).lower()
- return (
- status == "connected"
- and participant.get("id") is not None
- and participant.get("callid") is not None
- )
- @staticmethod
- def _participant_key(
- participant: dict[str, Any],
- ) -> str:
- return (
- f"{participant.get('callid')}:"
- f"{participant.get('legid')}:"
- f"{participant.get('id')}"
- )
- async def _run_session(
- self,
- participant: dict[str, Any],
- key: str,
- ) -> None:
- try:
- async with self._session_semaphore:
- result = await self.session.process_participant(
- participant,
- )
- logger.info(
- "MediaSession abgeschlossen: "
- "call=%s participant=%s "
- "audio=%s bytes segments=%s",
- result["callid"],
- result["participant_id"],
- result["audio_bytes"],
- len(result["segments"]),
- )
- self._completed.add(key)
- except Exception:
- logger.exception(
- "MediaSession fehlgeschlagen: %s",
- key,
- )
- async def _on_transcript(
- self,
- transcript: dict[str, Any],
- ) -> None:
- logger.info(
- "TRANSCRIPT "
- "call=%s leg=%s participant=%s "
- "segment=%s text=%r",
- transcript.get("callid"),
- transcript.get("legid"),
- transcript.get("participant_id"),
- transcript.get("segment"),
- transcript.get("text"),
- )
- def _session_done(
- self,
- key: str,
- task: asyncio.Task,
- ) -> None:
- self._active.pop(key, None)
- try:
- task.result()
- except asyncio.CancelledError:
- pass
- except Exception:
- logger.exception(
- "MediaSession Task beendet mit Fehler: %s",
- key,
- )
|