Skill: Transcription Analysis
Use this skill for tasks involving transcription providers, caption streaming, or the audio pipeline. Reference:
portal/transcription/, TRANSCRIPTION_MAP.md.
Audio Pipeline (end-to-end)
Interpreter browser
└── getUserMedia (mic, 16-bit PCM via WebRTC/WHIP)
└── MediaMTX (RTSP port 8554, path: {event_slug}/{language_code})
└── ffmpeg (spawned by worker.py)
-rtsp_transport tcp -i rtsp://mediamtx:8554/{path}
-f s16le -acodec pcm_s16le -ar 16000 -ac 1 -
└── TranscriptionProvider.run_stream()
└── 3-second PCM chunks
└── process_chunk() → text
└── CaptionAggregator
└── broadcast_transcription()
├── /ws/booth/{booth_id} (interpreters)
└── /ws/captions/{booth_id} (listeners)
Worker State
active_workers: dict[str, dict]— keyed bybooth_id; value has{task, provider, stderr_task}active_processes: dict[str, asyncio.subprocess.Process]— the ffmpeg process per booth- Both protected by
active_workers_lock: asyncio.Lock MAX_TOTAL_WORKERS = 10— process-global hard limit
Adding a New Provider (checklist)
- Create
portal/transcription/providers/{name}.py - Implement class inheriting
TranscriptionProviderfromproviders/base.py - Implement
process_chunk(chunk, language_code, model_variant, config, booth_state) -> str - For streaming (WebSocket-based): override
run_stream(process, language_code, model_variant, config, broadcast_callback, booth_id)— seedeepgram.pyandopenai.pyas references - Add
ProviderEnum.{NAME} = "{name}"inconstants.py - Add
{name}: {allowed_models_set}toALLOWED_MODELS - Add to
PROVIDERSdict inworker.py - If API key needed: add encrypted column to
Eventmodel (migration 008 pattern) + updateget_api_keykey_map inbase.py - Update
admin_event_api_settings_postinportal/routers/admin/settings.pyto handle new key - Update
templates/admin/api_settings.html
CaptionAggregator Usage
Providers call aggregator methods, not broadcast_callback directly:
aggregator = CaptionAggregator(broadcast_callback)
await aggregator.handle_partial(booth_id, partial_text) # streaming partial
await aggregator.handle_final(booth_id, final_text) # completed utterance
await aggregator.handle_chunk(booth_id, whisper_chunk) # Whisper-style chunks
await aggregator.handle_clear(booth_id) # silence endpoint
Forced finalization: if an utterance exceeds 50 words or 15 seconds, it auto-finalizes regardless of provider signal. This prevents endlessly growing partials.
Local Provider Details
- Model loading:
get_model(model_size)— thread-safe lazy loading. Model is loaded once and reused. - Overlap buffer: 1 second (32 000 bytes) of previous chunk is prepended to current chunk → 4-second effective window per inference call.
- Thread pool:
asyncio.to_thread(self._run_inference, ...)— each chunk is inferred in a thread. - VAD filter:
vad_filter=Truein faster-whisper — reduces empty segment noise. - Model eviction:
eviction_loopchecks every 15 min; evicts model if no active booths and last used > 1 hour ago.
OpenAI Provider Details
whisper-1: uses RESTPOST /v1/audio/transcriptions(WAV conversion viapcm_to_wav)gpt-4o-realtime-*: useswss://api.openai.com/v1/realtimeWebSocket; streams raw PCM- Retry:
tenacitywith exponential backoff (2–10s, 3 attempts) on 429/5xx - HTTP client:
portal.transcription.shared_http_client— created inlifespaninfastapi_app.py
Deepgram Provider Details
- Connects to
wss://api.deepgram.com/v1/listen?model=nova-2&...&interim_results=true - Runs concurrent
sender(pipes PCM) andreceiver(gets transcripts) tasks - Handles
KeepAlive(every 5s timeout to prevent WS close) speech_final=Trueoris_final=True→handle_final; otherwise →handle_partial
NVIDIA Provider Details
- Uses Riva gRPC (requires
nvidia-riva-clientoptional dependency:uv add eventyay-interpretation-portal[nvidia]) - Models:
parakeet-rnnt,parakeet-ctc - Requires
NVIDIA_FUNCTION_IDsetting inportal/config.py
Transcription Trigger (Admin → Worker)
- Admin sets
transcription_enabled=True,provider,modelvia booth detail form. POST /admin/.../booths/{id}/transcription-settingshandler:- Validates provider + model combination.
- Verifies API key exists if non-local provider.
- Saves settings to DB.
- If booth is currently live (has
active_interpreter_id): stops old worker, starts new worker.
- On interpreter Go Live: worker is started if
transcription_enabled=Trueon the DBBooth.- Look for this logic in
portal/routers/api.py(search forstart_transcription_worker).
- Look for this logic in
Debugging Transcription
- Check
active_workersdict for the booth_id. - Check ffmpeg process is alive:
active_processes[booth_id].returncode is None. - Check MediaMTX RTSP is reachable:
rtsp://mediamtx:8554/{event_slug}/{language_code}. - Check API key:
get_api_key(event, ProviderEnum.X)— returnsNoneif not set. - Check
MAX_TOTAL_WORKERSnot exceeded. - Check
event.transcription_api_enabledisTruefor external providers.