Real-Time Voice Assistants
VAD, turn detection, barge-in, latency budgets and full-duplex speech for a voice agent that answers in time.
- Concepts and terms
- Tasks and commands
- Annotated code
- Diagrams
- Defaults and numbers
- Troubleshooting
- Questions and tips
Cascaded voice pipeline#
From Lesson 06.12, Lesson 19.03
Latency budget per turn#
| Setting | Default | Note |
|---|---|---|
| Mic to buffer | 20 ms | One frame |
| VAD | 10 ms | Silero runs under 1 ms per frame |
| ASR (streaming) | 150 ms | Partial transcripts |
| LLM (first token) | 100 ms | Stream the tokens |
| TTS (first chunk) | 100 ms | Start before the full reply |
| Render to speaker | 20 ms | One frame |
| Total | ~400 ms | Above 500 ms feels robotic |
| First TTS audio, laptop CPU | 800 ms | Phase 6 capstone target |
From Lesson 06.11, Lesson 06.12
Silero VAD segments#
from silero_vad import load_silero_vad, get_speech_timestampsvad = load_silero_vad()audio = torch.tensor(waveform_16k, dtype=torch.float32)segments = get_speech_timestamps( audio, vad, sampling_rate=16000, threshold=0.5,0.3 is sensitive min_speech_duration_ms=250,drops coughs min_silence_duration_ms=500,silence hangover speech_pad_ms=300,pre-roll pad)for s in segments: print(f"{s['start']/16000:.2f}s - {s['end']/16000:.2f}s")From Lesson 06.14
Barge-in and the flush trick#
tts_task = asyncio.create_task(tts_loop())while True: chunk = await mic.get() if vad(chunk):user speaks during TTS tts_task.cancel()stop the reply await speaker.stop() await new_turn()restart STT breakdef flush_on_end(stt_client, audio_buffer): stt_client.send_audio(audio_buffer) stt_client.send_flush()forces output now return stt_client.recv_transcript(timeout_ms=150)From Lesson 06.12, Lesson 06.14
Troubleshooting#
| Symptom | Cause | Fix |
|---|---|---|
| First word is missing | VAD starts a beat too late | Threshold 0.3 and a pre-roll |
| Bot talks over the user | LLM keeps going after interrupt | Wire VAD to cancel the LLM |
| "Thanks for watching" on silence | Whisper on silent frames | Always VAD-gate |
| ASR hears the bot voice | No echo cancellation | WebRTC AEC3 |
| Agent cuts user off mid-sentence | Hangover too short | 500-800 ms and a turn detector |
| Slow first reply | TTS warm-up on first request | Warm it with a dummy run |
| Audible vocoder artifacts | Sub-200 ms TTS chunks | 320 ms chunks |
| Glitches under load | GIL starves the audio thread | C-callback audio library |
From Lesson 06.12, Lesson 06.11, Lesson 06.14
Glossary#
- AEC
- echo cancellation. Subtracts the speaker to mic path.
- TPR @ 5% FPR
- VAD benchmark point. Silero 87.7%, WebRTC VAD 50.0%.
- MultimodalAgent
- LiveKit class. Audio in, audio out, no text in the middle.
- VoicePipelineAgent
- LiveKit class. STT, LLM, TTS with text-level control.
From Lesson 06.11, Lesson 06.14, Lesson 14.22
Capstone scheduler, one turn#
VAD and turn settings#
| Setting | Default | Note |
|---|---|---|
| Silero threshold | 0.5 | 0.3 is sensitive. Fewer first-word clips |
| Minimum speech | 250 ms | Rejects coughs and chair noise |
| Silence hangover | 500-800 ms | Too short cuts the user off |
| Pre-roll buffer | 300-500 ms | Keeps the first word |
| Energy gate | -40 dBFS | Fires on any noise above it |
| Frame size | 20 ms | 320 samples at 16 kHz |
| Ring buffer | 32,000 samples | 2 s at 16 kHz |
| Turn commit (capstone) | 0.6 | Plus 500ms of VAD silence |
From Lesson 06.14, Lesson 06.11, Lesson 19.03
Run the lessons#
| Command | What it does |
|---|---|
python3 code/ | Runs the simulator of a lesson |
python main.py | Capstone scheduler, clean and barge-in calls |
pip install silero-vad | Real VAD in place of the stub |
npm install | Installs the TypeScript client |
npm start | Two offline sessions and a ws probe, exits 0 |
npm start -- --serve | Keeps the ws server up |
npm test | node --test runner via tsx |
npm run typecheck | Runs tsc --noEmit |
From 06.11 code/main.py, Lesson 06.12, 19.03 code/main.py, 19.03 code/ts/README.md
Core terms#
- Ring buffer
- fixed circular buffer. No allocations in the hot path.
- Jitter buffer
- reorders late packets. Too small gives gaps. Too large adds latency.
- Pre-roll
- audio kept before VAD fires. It prevents the first-word clip.
- Silence hangover
- the wait after VAD returns to 0, before end of turn.
- Semantic endpoint
- a classifier that reads the words, not only the silence.
- Flush trick
- flush the STT at end of speech. 500 ms of look-ahead ends in ~125 ms.
- Filler
- a short phrase when a tool takes more than 300ms.
- DOWNSTREAM
- Pipecat frames from source to sink, audio in and speech out.
- UPSTREAM
- Pipecat control frames, such as cancel, metrics and barge-in.
Full-duplex Moshi, one 80 ms step#
Q&A#
- How fast do humans take turns?
- About 230 ms. Above 1500 ms feels broken.
- Pipeline or Moshi?
- Use a pipeline for tools. Moshi is not trained for tool calling.
- Why no flush with Whisper streaming?
- It is block-based and always waits for chunks.
- What is end-to-end premium?
- 450 to 600 ms. 800 to 1200 ms is common.
- What is the capstone bar?
- First-audio-out p50 under 800ms, false-cutoff under 3%.
- What does barge-in need in Pipecat?
- UPSTREAM cancel frames back to TTS and LLM.
From Lesson 06.11, Lesson 06.14, Lesson 06.15, Lesson 14.22, Lesson 19.03