Live STT — Real-Time Speech-to-Text
Audiences: developer, internal
⚠️ DEPRECATED (2026-06). The
ddx-live-sttFastAPI service documented here has been moved out of this monorepo todudoxx-omni-intelligence. It survives in-tree only until the client migration completes (see rootCLAUDE.md"Deferred (NOT archived, next-session wipe)" +ddx-reminders/). The live on-prem STT for voice agents is now the CUDA Parakeet backend (NeMo Parakeet-TDT,stt.home.dudoxx.com/stt.forge.dudoxx.com), consumed by Vivoxx viaddx-vivoxx/src/providers/stt/dudoxx-stt.tsand by ddx-api via theDUDOXX_STTengine. Do NOT wire NEW consumers toddx-live-stt. Historical description below.
Self-hosted, on-prem real-time transcription service that streams clinical-grade Whisper output to TUCAN voice agents, FlowAgent scenarios, and Vivoxx — so every utterance the patient or clinician makes hits a Mastra agent in <500 ms with no audio leaving Dudoxx infrastructure.
Business Purpose
Voice-driven workflows (TUCAN Operator, FlowAgent voice forms, Vivoxx avatars) all need low-latency transcription that is HIPAA-compliant by construction — meaning the audio stream MUST NOT leave the customer's VPC. ddx-live-stt is the on-prem speech recognition tier that replaces Deepgram Cloud for tenants under data-residency mandates and provides a deterministic fallback when the cloud STT path degrades. It is licensed as part of the Dudoxx Voice Pack and is the first piece of the "no PHI to OpenAI / no audio to Google" story we sell to hospital procurement.
Audiences
- Investor: Removes a recurring per-minute STT bill (Deepgram cloud) and unblocks deals where the customer refuses to send audio to US-hosted SaaS. Margin lever + sales unblocker.
- Clinical buyer (doctor/nurse/receptionist): Voice assistant works during internet outages and in EU/CH/DE deployments where cross-border audio is legally constrained.
- Developer/partner: Single FastAPI service exposing
WS /ws/transcribe(DDX STT Protocol v2). Same WebSocket protocol whether the backend is faster-whisper on CUDA, MLX on Apple Silicon, or a future engine plug-in. - Internal (ops/support): Pre-loads a default model at boot, has
/health+/metricsroutes, and degrades gracefully (model fails to load → service stays up, requests get a structured error).
Architecture
AudioSession + Pipeline stages
| Stage | Detail |
|---|---|
VadStage | Silero |
AsrStage | DdxTranscriptionEngine — LocalAgreement-2 strategy, backends: FasterWhisperBackend (CUDA / CPU), MlxWhisperBackend (Apple Silicon) |
DiarizationStage | sortformer | pyannote |
PunctuationStage | — |
Layered NLAH split: transport (api/) is isolated from inference (engine/); the pipeline (pipeline/) chains stages with shared PipelineContext. Vendored Whisper code lives under engine/vendored/ so the service has zero git-runtime dependency on upstream openai-whisper — critical for air-gapped customer deployments.
Tech Stack & Choices
- Framework: FastAPI + Uvicorn (
ddx-live-stt/pyproject.toml:14-30) — async WebSocket support, Pydantic v2 schemas, lifespan hooks for model pre-load. - Models:
large-v3-turboby default (ddx-live-stt/src/ddx_live_stt/settings.py:50-53), with the full Whisper catalog (tiny→large-v3) selectable per session. - Inference backends:
faster-whisper(CTranslate2 — CUDA + CPU int8) andmlx-whisper(Apple Silicon native). Auto-detected (ddx-live-stt/src/ddx_live_stt/settings.py:54-65). - Streaming strategy: LocalAgreement-2 (
engine/ddx_engine.py:43-50) — two consecutive Whisper passes must agree before a segment is finalised. Keeps the inference buffer ~2-5 s instead of re-transcribing every 15 s. Alternativesimul-streaming(AlignAtt) selectable viaengine_policy. - VAD: Silero ONNX (
audio/vad_model.py) — short-circuits silent frames before they hit Whisper. - Diarization: NVIDIA Sortformer (default) or pyannote / diart (
schemas/config.py+pipeline/diarization.py). - Why not call OpenAI / Deepgram Cloud directly? Audio is PHI. The whole reason this service exists is that the cloud route is legally unusable in our EU/CH targets.
Data Flow
- Client opens
WS /ws/transcribe?language=en&model=large-v3-turboand authenticates withDDX_LIVE_STT_API_KEY(header or query param — seeapi/auth.py). - Server responds with
SessionCreated(schemas/ws.py) — includessession_id, model name, sample rate. - Client streams 16 kHz mono PCM as binary WS frames; control messages (
{"type":"flush"},{"type":"stop"},{"type":"config", ...}) come as text frames. - A receiver task enqueues frames; a separate processor loop drains them, runs VAD → ASR → diarization → punctuation, and emits:
VadSpeechStart/VadSpeechEndTranscriptionInterim(each pass)TranscriptionFinal(LocalAgreement-2 confirmed; with optionalwords: WordTimestamp[])ErrorMessageon engine faultSessionEndedon close
- Consumers —
ddx-livekit-flowagent-ts/src/index.tsfor FlowAgent voice,ddx-api/src/voice-video/livekit/services/pipeline/voice-stream.service.tsfor TUCAN Operator — readTranscriptionFinal.textand forward it to the Mastra agent / workflow step. No SSE involvement: the WS is point-to-point between the consumer and STT.
Implicated Code
MANDATORY: ≥3 file:line citations.
ddx-live-stt/src/ddx_live_stt/app.py:25— FastAPI app factory; lifespan pre-loads default engine (app.py:54-67).ddx-live-stt/src/ddx_live_stt/app.py:133-141— router mount: test page, health, config, models, metrics,ws_router(WebSocket), admin.ddx-live-stt/src/ddx_live_stt/api/ws_transcribe.py:1-44— DDX Live STT Protocol v2 WebSocket implementation; producer-consumer split keeps the event loop responsive while MLX inference blocks.ddx-live-stt/src/ddx_live_stt/engine/ddx_engine.py:43-50—DdxTranscriptionEngine(LocalAgreement-2 online Whisper).ddx-live-stt/src/ddx_live_stt/engine/backends/faster_whisper_backend.py:1— CTranslate2-backed engine (CUDA / CPU int8).ddx-live-stt/src/ddx_live_stt/engine/backends/mlx_backend.py:1— Apple Silicon MLX backend (vendoredsimul_whisper).ddx-live-stt/src/ddx_live_stt/settings.py:35— default port 6350.ddx-live-stt/src/ddx_live_stt/settings.py:46-49—engine_policyswitch (local-agreement-2|simul-streaming).ddx-live-stt/src/ddx_live_stt/settings.py:50-65— model + backend + device + compute selection.
Operational Notes
- Port: 6350 (
settings.py:35). NOT 6360 — there is no in-tree FunASR engine today; the W4 brief that referenced it was speculative. If a FunASR backend is added later it would slot in underengine/backends/. - Model pre-load: lifespan loads the default engine at startup and logs load time (
app.py:58-67). A load failure does NOT kill the service — it serves errors per session until a working model is reachable. - Env vars:
STT_PORT,MODEL_NAME,BACKEND(auto/mlx-whisper/faster-whisper),DEVICE(auto/cuda/mps/cpu),COMPUTE_TYPE,API_KEY,DEFAULT_LANGUAGE. Seeddx-live-stt/src/ddx_live_stt/settings.py:23-79. - Auth: empty
api_keydisables auth (dev mode); production deployments MUST set it (settings.py:42). - Pitfall — backend mismatch on workstations: developer Macs auto-pick MLX; CI/CD on Linux picks faster-whisper. Word-timestamp formats differ slightly; always test through the WS contract, not against the backend directly.
- Pitfall — buffer trimming:
buffer_trimming_secdefault 15 s (settings.py:77-79). Lower for chatty workflows; raising it improves accuracy on long monologues but linearly increases re-transcription cost. - Defects: any STT-side regressions go into
DEFECTS/witharea: voice/sttand a copy of the failingTranscriptionFinalsequence. - REMINDER: STT is the only voice service that touches raw audio. Audit access carefully —
api_keyis the only thing standing between an attacker and the patient's microphone.
Related Topics
- Live TTS — the synthesis side of the same voice pipeline
- Voxial LLM — what consumes STT finals and decides what to say next
- FlowAgent Scenarios — Mastra workflow steps suspend/resume on
TranscriptionFinal - LiveKit FlowAgent TS Bridge — the Node process that brokers LiveKit ↔ STT ↔ Mastra
- Vivoxx Voice — POC voice agent that also consumes this STT WS
- LiveKit Infra (W1) — the SFU that delivers audio to STT