|
| 1 | +# Voice pipeline fluidity audit and optimization |
| 2 | + |
| 3 | +This note records a focused audit of the shipped voice dialogue pipeline (Epics #491 and #1556) |
| 4 | +prompted by user reports that the human↔assistant dialogue did not feel fluid (latency, choppiness, |
| 5 | +unconvincing quality), the fixes applied, and the prioritized follow-ups. |
| 6 | + |
| 7 | +## Method |
| 8 | + |
| 9 | +The pipeline hot path was audited statically across the Model Gateway voice adapters, the BFF voice |
| 10 | +handlers, and the browser dialogue/transport/playback hooks. In addition, the **live** Azure Foundry |
| 11 | +voice deployments were exercised directly to obtain real latency numbers and to verify provider |
| 12 | +request/response schemas (no mocks). Audio quality (timbre/naturalness) is a property of the |
| 13 | +configured provider model and is out of scope for pipeline changes; perceived fluidity (start |
| 14 | +latency, choppiness, turn-taking, robustness) is what this work targets. |
| 15 | + |
| 16 | +## Live baseline (real Azure Foundry calls) |
| 17 | + |
| 18 | +| Path | Observation | |
| 19 | +| ---------------------------------------- | --------------------------------------------------------------------------------------------------- | |
| 20 | +| TTS `keiko-tts`, short reply, mp3 | ttfb ~0.78s, total ~1.4s, ~72KB | |
| 21 | +| TTS `keiko-tts`, short reply, opus | ttfb ~0.90s, total ~1.1s, ~18KB | |
| 22 | +| TTS `keiko-tts`, long reply, mp3 | total ~3.0s, ~303KB | |
| 23 | +| TTS `keiko-tts`, long reply, opus | total ~1.95s, ~75KB | |
| 24 | +| STT `keiko-stt` | ~1.2s round-trip; rejects audio without a recognized file extension | |
| 25 | +| Realtime `keiko-realtime` session create | HTTP 200 ~0.3s; **default session ran the provider demo persona**; voice `nova` rejected (HTTP 400) | |
| 26 | + |
| 27 | +## Fixed in this change |
| 28 | + |
| 29 | +### Realtime dialogue (Wave A) — `fix(voice): make realtime dialogue audible and grounded` |
| 30 | + |
| 31 | +- **Silent assistant (critical):** the negotiated remote audio track was never attached to an output |
| 32 | + sink (`useRealtimeVoice` wired `onConnectionStateChange` but never `onRemoteTrack`). The remote |
| 33 | + stream is now attached to an autoplaying audio sink. |
| 34 | +- **Ungrounded persona (critical):** the realtime session was created with no `instructions`, so it |
| 35 | + ran the provider default demo persona ("helpful, witty, friendly AI … talk quickly … knowledge |
| 36 | + cutoff 2023-10"). It now uses the same system persona as the text chat, set via the GA nested |
| 37 | + `audio.{input,output}` session schema (verified against the live endpoint; the older top-level |
| 38 | + shape returns HTTP 500). |
| 39 | +- **Voice + transcription:** a realtime-valid output voice and `input_audio_transcription` + |
| 40 | + `server_vad` turn detection are now configured. |
| 41 | +- **Invalid-voice guard:** `resolveRealtimeVoice` maps a TTS-only voice id (e.g. `nova`, rejected by |
| 42 | + the realtime model with HTTP 400) to a safe default so a misconfigured persona mapping cannot break |
| 43 | + session creation. |
| 44 | +- **Transient ICE `disconnected`** no longer tears the session down immediately; a bounded grace |
| 45 | + window allows recovery. |
| 46 | +- **Full-duplex capture:** `getUserMedia` now requests echo cancellation / noise suppression / AGC / |
| 47 | + mono, preventing the assistant echoing into the microphone. |
| 48 | +- **Negotiation timeout** clamped to 8s (was the generic 30s provider timeout). |
| 49 | + |
| 50 | +### Turn-based speech latency (Wave B) — `perf(voice): request opus for interactive assistant speech` |
| 51 | + |
| 52 | +- The assistant speak path now requests **opus (audio/ogg)** instead of mp3: measured ~25–35% faster |
| 53 | + end-to-end and ~4x smaller for the same utterance, lowering both the synth-to-first-audio wait and |
| 54 | + the base64 inflation of the JSON envelope. opus output is a valid, browser-playable Ogg/Opus |
| 55 | + container, and the MIME stays inside the existing server allowlist. |
| 56 | + |
| 57 | +## Prioritized follow-ups (identified, deliberately deferred) |
| 58 | + |
| 59 | +These were confirmed real but are deferred to keep this change contained, well-tested, and within the |
| 60 | +shipped subsystem's invariants. They are ordered by perceived-fluidity value. |
| 61 | + |
| 62 | +1. **Turn-based VAD end-of-turn + voice-activated barge-in.** The shipped dialogue mode (STT+TTS) |
| 63 | + ends a user turn only when the user manually taps the mic again, and barge-in is button-driven. |
| 64 | + A WebAudio `AnalyserNode` on the dictation `MediaStream` (trailing-silence end-of-turn + energy- |
| 65 | + onset barge-in) would make turns feel natural. Deferred because it requires exposing the shared |
| 66 | + dictation recorder's stream and WebAudio test infrastructure (regression surface on composer |
| 67 | + dictation, #495). _Files: `useVoiceDialogueSession.ts`, `voice-dialogue-session.ts`, |
| 68 | + `dictation-recorder.ts`._ Note: realtime mode already gets natural turn-taking via `server_vad`. |
| 69 | +2. **Streaming assistant-speech playout (MSE/Web Audio).** Start playback on the first audio chunk |
| 70 | + instead of buffering the whole clip. With opus the whole-clip wait is already ~1.1s versus a |
| 71 | + ~0.9s time-to-first-audio floor, so the remaining benefit is ~0.2s; it is a larger architectural |
| 72 | + change to the audible path and warrants its own change with perceptual sign-off. _Files: |
| 73 | + `useAssistantSpeech.ts`, `voice-handlers.ts` (reuse the existing SSE seam), `lib/api.ts`._ |
| 74 | +3. **Control-plane WS heartbeat + client negotiate() timeout.** Ping/pong liveness and a browser-side |
| 75 | + handshake timeout so a half-open connection cannot stall on "negotiating". _Files: |
| 76 | + `voice-realtime.ts`, `voice-realtime-client.ts`._ |
| 77 | +4. **Per-user persona selection for realtime.** Plumb the selected `male`/`female`/`neutral` persona |
| 78 | + through the control protocol so realtime honors it (today it uses the configured neutral default). |
| 79 | +5. **Live interrupt offset.** Source the barge-in offset from the live media position rather than the |
| 80 | + previous interrupt's stored offset. _File: `useVoiceDialogueSession.ts`._ |
| 81 | +6. **STT interim/verbose_json.** Request `verbose_json` for real duration (the current `json` path |
| 82 | + parses `confidence`/`duration` that are never returned) and surface interim transcripts. _File: |
| 83 | + `speech-to-text-adapter.ts`._ |
| 84 | + |
| 85 | +## Invariants preserved |
| 86 | + |
| 87 | +Model Gateway boundary intact; no new runtime media dependencies; no raw audio persisted; the browser |
| 88 | +never receives the provider key; spoken Keiko derives from the same persona as written Keiko; voice |
| 89 | +remains capability-gated and fully optional. |
0 commit comments