TAI BUI
Back to curriculum
BuildPython

Build a Voice Assistant Pipeline — The Phase 6 Capstone

Lessons 01 through 11, wired into one pipeline: a voice assistant that listens, reasons, and talks back. By 2026 this is solved engineering, not open research. The integration details are what decide whether it actually ships.

TIME: ~120 minutesPREREQUISITES: Phase 6 · 04, 05, 06, 07, 11; Phase 11 · 09 (Function Calling); Phase 14 · 01 (Agent Loop)

Learning Objectives

  • Wire the seven pipeline components (capture, VAD, streaming STT, tool-calling LLM, streaming TTS, playback, interruption handling) into one working voice assistant loop.
  • Diagnose and fix the three integration failure modes that only surface once components are wired together: first-word clip, mid-response interrupt confusion, and silence hallucination.
  • Implement VAD-gated turn capture with a pre-roll buffer, LLM tool calling, and barge-in interruption handling in Python.
  • Weigh 2026 production reference stacks against latency and licensing, and apply the pipeline's privacy and security pitfalls (PII retention, untrusted transcripts and tool results, wake-word gating) before shipping.

The Problem

Build an end-to-end assistant:

  1. Captures mic input (16 kHz mono).
  2. Detects start/end of user speech.
  3. Transcribes streaming.
  4. Passes transcript to an LLM that can call tools (timer, weather, calendar).
  5. Streams LLM text to a TTS.
  6. Plays audio back to the user.
  7. Stops if the user interrupts mid-response.

Latency target: first TTS audio byte within 800 ms of the user finishing their utterance, on a laptop CPU. Quality target: no missed words, no hallucinated subtitles on silence, no voice-cloning leakage, no successful prompt injection.

Warm-up0/3
1. A single-threaded async program calls a blocking function that takes two seconds. What happens to its other tasks during those two seconds?Warm-up
Timeline with three lanes. The only thread runs a blocking call from 0 s to 2 s. The lanes for task B and task C show a dashed box with a question mark over the same two seconds.
Typed schematic of the stem's situation.
2. Why do chat products usually show a model's reply as it is generated instead of after the whole reply is finished?Warm-up
A chat window with a user question and a partly written reply ending in a cursor. A question mark sits beside the reply.
Typed illustration; the exchange is made up.
3. For each short slice of microphone audio, what does a voice activity detector decide?Warm-up
Six small waveform panels labelled slice 1 to slice 6. Slices 1, 2 and 6 are nearly flat, slices 3 to 5 have large swings. A question mark under each slice stands for the detector's output.
Synthetic waveforms for illustration.
Check: The Problem0/1
1. What latency target does the lesson set for the first TTS audio byte after the user finishes speaking?Define
A time axis with a user speaks block ending at a marker user finishes, and a later marker first TTS audio byte. An arrow between the two markers carries a question mark.
Typed schematic; positions are not to scale.

The Concept

Voice assistant pipeline: mic → VAD → STT → LLM+tools → TTS → speaker

The seven components

  1. Audio capture. The mic feeds 16 kHz mono audio in 20 ms chunks: sounddevice in Python, or native AudioUnit, ALSA, or WASAPI in production.
  2. VAD (Lesson 11). Silero VAD at a 0.5 threshold, 250 ms minimum speech, 500 ms silence hangover. It signals "start" and "end" for each turn.
  3. Streaming STT (Lessons 4-5). Whisper-streaming, Parakeet-TDT, or Deepgram Nova-3 over API, producing partial and final transcripts as the audio arrives.
  4. LLM with tool calling. GPT-4o, Claude 3.5, or Gemini 2.5 Flash, given a JSON schema for tools and streaming tokens as it reasons.
  5. Streaming TTS (Lesson 7). Kokoro-82M for the fastest open option, Cartesia Sonic if you want commercial quality. Start TTS after the first 20 LLM tokens, not the full response.
  6. Playback. Audio goes out to the speaker, Opus-encoded when the network is thin.
  7. Interruption handler. If VAD fires while TTS is playing, stop playback, cancel the LLM call, and restart STT.

Three failure modes you'll hit

  1. First-word clip. VAD starts a beat too late, so the user's "hey" never makes it into the transcript. Start the threshold at 0.3, not 0.5.
  2. Mid-response interrupt confusion. The LLM keeps generating after the user interrupts, so the assistant talks over them. Wire VAD to cancel the LLM directly.
  3. Silence hallucination. Whisper outputs "Thanks for watching" on silent warm-up frames. Always VAD-gate before transcription runs.

2026 production reference stacks

StackLatencyLicenseNotes
LiveKit + Deepgram + GPT-4o + Cartesia350-500 mscommercial APIIndustry default 2026
Pipecat + Whisper-streaming + GPT-4o + Kokoro500-800 msmostly openDIY-friendly
Moshi (full-duplex)200-300 msCC-BY 4.0Single-model; different architecture, lesson 15
Vapi / Retell (managed)300-500 mscommercialFastest to launch; limited customization
Whisper.cpp + llama.cpp + Kokoro-ONNXofflineopenPrivacy / edge
Check: The Concept0/18
1. How many components does the lesson break the voice assistant pipeline into?Define
A box labelled mic on the left, a dashed box labelled the pipeline with a question mark in the middle, and a box labelled speaker on the right. Below, an example heard sentence and spoken reply.
Example sentences are the lesson's code/main.py transcript and reply.
2. What is a turn in the voice assistant pipeline?Define
Two lanes on a time axis. The user mic lane has two blue utterance blocks. The assistant speaker lane has two orange reply blocks, each after an utterance.
Typed schematic; block lengths are arbitrary.
3. What is barge-in?Define
Two lanes. The assistant speaker lane has an orange reply block that continues as a dashed box with a question mark. The user mic lane has a blue user speaks block starting partway through the reply.
Typed schematic.
4. What is end-pointing?Define
A row of 24 small squares: two dark, ten green, twelve dark. Below the row, a key and a question mark asking when the system decides the user is done.
Illustrative chunk pattern.
5. What is a tool call in this pipeline?Define
A transcript box, an LLM box and a spoken reply box in a row, with the heard sentence set a timer for five minutes and the reply Sure, setting a 5 minute timer. Below, two tool signatures get_weather and set_timer, and a question mark about what happens in the LLM stage.
Sentences from the lesson's code/main.py; tool list from Step 4.
6. What is the role of the VAD component in the pipeline?Define
A row of boxes: mic, VAD, a dashed box labelled rest of the pipeline, and speaker. Under the VAD box a question mark follows the words sends to the next stage.
Typed schematic.
7. What VAD start threshold does the lesson recommend to fix the 'first-word clip' failure mode, and why?Explain
A curve of VAD speech probability rising from near 0 to near 1 after a dashed marker for speech begins. The axis runs from 0 to 1. A line below asks which threshold value makes the VAD start the turn.
Illustrative curve, not measured.
8. When does the pipeline start streaming TTS relative to the LLM's output?Explain
A lane of small token boxes ending in an ellipsis, and a lane below for TTS audio drawn as an empty dashed box with the text starts after: a question mark.
Typed schematic; the token boxes are not counted.
9. Which stack in the 2026 reference table is a single full-duplex model rather than a chain of separate components?Explain
Shape A: audio in, part 1, part 2, part 3, audio out in a chain. Shape B: audio in, one model, audio out. A note says the one model listens and speaks at the same time, and a line asks which stack is Shape B.
Typed schematic of the two shapes.
10. Why must VAD gate audio before it reaches the transcription model?Explain
A mic frames lane drawn as a flat line, an STT model box with an arrow to the label transcript and a question mark. A note says nobody is speaking.
Typed schematic.
11. Why does the lesson wire VAD to cancel the LLM directly instead of only stopping playback?Explain
Three lanes. The LLM stream has token blocks up to the interrupt and then a dashed question-mark box. The speaker lane stops at the interrupt marked stopped. The user mic lane starts at the interrupt.
Typed schematic.
12. Why do the three failure modes only show up once the components are wired together?Explain
Three cards: first word missing, talks over the user, and text from nothing, each with a one-line description.
The three failure modes as described in the lesson.
13. A user pauses 300 ms between two sentences while the 500 ms silence hangover is in place. What happens?Apply
A user mic lane with two blue blocks, sentence 1 and sentence 2, separated by a gap marked 300 ms pause. A note gives the silence hangover as 500 ms.
Typed schematic.
14. Your pipeline logs a transcript reading 'Thanks for watching' during a few seconds of dead silence at startup. What's going on, and what fixes it?Analyze
A log panel with three lines: mic opened, a few seconds of dead silence, and a transcript reading Thanks for watching. A question mark asks what is going on.
Typed example; the log times are made up.
15. The user starts talking over the assistant, but the assistant keeps going for another second before it stops. Which failure mode is this?Analyze
Two lanes. The assistant speaker lane has a long speaking block that ends about a second after the user mic lane's user speaks block begins; an arrow between the two marks the overlap.
Typed schematic; the overlap is the stem's one second.
16. A clinic kiosk has no network connection and strict privacy rules. Which reference stack fits best?Design
A requirements card: network none, privacy rules strict, device a kiosk in the clinic.
Typed summary of the question's requirements.
17. A startup must demo a voice assistant this week and can live with limited customization. Which approach fits?Design
A requirements card: deadline is a demo this week, customization limited is acceptable.
Typed summary of the question's requirements.
18. Every component in your pipeline passes its own tests, yet users report clipped first words and talk-over. Where should you work first?Design
Four boxes VAD, STT, LLM and TTS each marked tests pass, and two user reports: it cuts off my first word, and it keeps talking when I interrupt.
Typed illustration; the reports mirror the question's symptoms.

Build It

Step 1: mic capture with chunking (pseudocode)

python
import sounddevice as sd
 
def mic_stream(chunk_ms=20, sr=16000):
    q = queue.Queue()
    def cb(indata, frames, time, status):
        q.put(indata.copy().flatten())
    with sd.InputStream(channels=1, samplerate=sr, blocksize=int(sr * chunk_ms/1000), callback=cb):
        while True:
            yield q.get()

Step 2: VAD-gated turn capture

Lowering the VAD threshold only closes part of the first-word-clip gap. You also need a pre-roll buffer: keep the last 200-400 ms of audio around at all times, so when VAD finally fires, the captured turn still includes the syllable it almost missed.

python
def capture_turn(stream, vad, pre_roll_ms=300, silence_ms=500):
    buf, pre, triggered = [], collections.deque(maxlen=pre_roll_ms // 20), False
    silent = 0
    for chunk in stream:
        pre.append(chunk)
        if vad(chunk):
            if not triggered:
                buf = list(pre)
                triggered = True
            buf.append(chunk)
            silent = 0
        elif triggered:
            silent += 20
            buf.append(chunk)
            if silent >= silence_ms:
                return b"".join(buf)

Step 3: streaming STT → LLM → TTS

python
async def turn(audio_bytes):
    transcript = await stt.transcribe(audio_bytes)
    async for token in llm.stream(transcript):
        async for audio in tts.stream(token):
            await speaker.play(audio)

Step 4: tool calling inside the LLM loop

python
tools = [
    {"name": "get_weather", "parameters": {"location": "string"}},
    {"name": "set_timer", "parameters": {"seconds": "int"}},
]
 
async for chunk in llm.stream(user_text, tools=tools):
    if chunk.type == "tool_call":
        result = dispatch(chunk.name, chunk.args)
        continue_streaming(result)
    if chunk.type == "text":
        await tts.stream(chunk.text)

Step 5: interruption handling

python
tts_task = asyncio.create_task(tts_loop())
while True:
    chunk = await mic.get()
    if vad(chunk):
        tts_task.cancel()
        await speaker.stop()
        await new_turn()
        break
Check: Build It0/10
1. What is pre-roll?Define
A lane with a speech block containing hey, set a timer. A dashed marker shows where speech begins and a solid orange marker, a little later, shows where the VAD fires. The transcript reads 'ey, set a timer. A line at the bottom asks what pre-roll is.
Typed illustration of the lesson's first-word-clip failure; positions are not to scale.
2. Why is lowering the VAD threshold alone not enough to fix first-word clip?Explain
A VAD speech probability curve rising from near 0 to near 1 after a dashed marker for speech begins. The title says the threshold was lowered from 0.5 to 0.3.
Illustrative curve, not measured.
3. In capture_turn with pre_roll_ms=300 and 20 ms chunks, how many chunks can the pre-roll deque hold?Apply
Two lines of the capture_turn definition with the deque maxlen shown as a question mark. Below, a bracket marks 300 ms of audio, a single small box marks one 20 ms chunk, and a question mark asks how many chunks fit.
Code lines copied from the lesson's Step 2 with the unknown replaced by a question mark.
4. With silence_ms=500 and 20 ms chunks, how many consecutive non-speech chunks after speech make capture_turn return the turn?Apply
A row of squares, six green then eight dark, followed by an ellipsis, with the capture_turn call signature and a question mark asking how many consecutive non-speech chunks until it returns.
Typed schematic; squares are not counted for the answer.
5. In the Step 4 loop, a chunk arrives with type 'text'. What does the code do with it?Apply
The Step 4 loop with both branch bodies replaced by ellipses. Below it, an incoming chunk of type text with the text Sure, and a question mark about what the code does with it.
Loop header and conditions copied from the lesson; branch bodies hidden. The chunk is a typed example.
6. Given the Step 4 tools list, the user says 'set a 5 minute timer.' Which call should the LLM emit?Apply
The lesson's two-tool list (get_weather with a string location, set_timer with an int seconds), the user's request set a 5 minute timer, an arrow into an LLM box, and a question mark for the call it emits.
Tool list copied from Step 4.
7. After the user's last word, VAD returns False for 12 consecutive 20 ms chunks (silence_ms=500). What does capture_turn do?Apply
A row of 22 squares, ten green then twelve dark. Text says ten speech chunks then twelve non-speech chunks, silence_ms is 500, the stream is still open, and a question mark asks what capture_turn does next.
Typed schematic of the stem's situation.
8. What does the interruption-handling code in Build It do when VAD fires mid-response?Apply
A TTS playback lane with a tts_task playing block, a lane of mic chunk squares where the tenth square is green and labelled vad(chunk) is true, a one-line sketch of the Step 5 loop, and a question mark.
Typed schematic; the loop line is abridged from Step 5.
9. Mid-turn, the LLM's stream emits a chunk of type 'tool_call' instead of text. Per the Step 4 code, what happens to it?Apply
The Step 4 loop with both branch bodies hidden, and an incoming chunk of type tool_call naming set_timer with seconds 300, followed by a question mark.
Conditions copied from the lesson; branch bodies hidden. The chunk values follow code/main.py.
10. You lowered the VAD threshold to 0.3, yet users still occasionally lose a leading syllable. Which addition fits best?Analyze
The probability curve with a green 0.3 line and a vertical line where it crosses. A red bar before the crossing is labelled lost and the audio after it is labelled audio sent to STT.
Illustrative curve; the crossing point is computed from it.

Use It

code/main.py runs a full simulation that wires all seven components together with stub models, so you can see the pipeline's shape before you touch real hardware. To make it real, swap the stubs for:

  • silero-vad (pip install silero-vad)
  • deepgram-sdk or openai-whisper
  • openai (gpt-4o) or anthropic
  • kokoro or cartesia
  • sounddevice for I/O

Pitfalls

  • Logging PII forever. Full-turn audio counts as PII in most jurisdictions. Retain it for 30 days, encrypted at rest, and no longer.
  • No barge-in. Users will interrupt you. Your assistant has to stop talking when they do.
  • TTS that blocks. Synchronous TTS blocks the event loop. Use async, or move it to a separate thread.
  • No tool-call error handling. Tools fail. Feed the error back to the LLM, let it retry once, then degrade gracefully.
  • Overzealous hallucination filters. Filter too hard and the assistant just repeats "I can't help with that." Filter too little and it says anything. Calibrate against a held-out set.
  • No wake-word option. Always-listening is a privacy liability. Add a wake-word gate; Porcupine or openWakeWord both work.
  • Untrusted transcripts and tool results. A prompt-injected instruction can ride in on a transcribed webpage or a tool's API response just as easily as on typed text. Treat every transcript and tool result as untrusted input: only the user's own speech authorizes an action.
  • Cloned-voice leakage. If your TTS voice is a clone of a real person, don't let arbitrary user text drive it for someone else's audio. Anti-spoofing and watermarking (Lesson 16) are the real fix, not a policy promise.
Check: Pitfalls0/10
1. What is a wake word?Define
A single audio lane with three phrases in order: dinner at eight in grey, hey assistant in orange, what's the weather in blue. A question mark with an arrow points at hey assistant.
Typed illustration of spoken phrases.
2. What retention policy does the lesson prescribe for full-turn audio, given that it counts as PII in most jurisdictions?Explain
A card describing stored full-turn audio, classified as PII in most jurisdictions, with a question mark for how long to keep it.
Typed card.
3. What does the pitfalls section say about synchronous TTS that blocks the event loop?Explain
One event loop lane with a VAD block, a long orange block labelled TTS call synchronous, then another VAD block.
Typed schematic; block lengths are arbitrary.
4. What tradeoff does the pitfalls section describe for hallucination filters, and what's the recommended fix?Explain
A horizontal slider labelled loose on the left and strict on the right with the knob left of centre (position arbitrary), and a question mark asking what tradeoff the setting carries and what the fix is.
Typed schematic.
5. Why does the pitfalls section recommend adding a wake-word gate like Porcupine or openWakeWord?Explain
Three boxes: mic, a highlighted wake-word gate, and a dashed rest of the pipeline. A line below names Porcupine and openWakeWord and asks what adding the gate buys.
Typed schematic.
6. The set_timer tool throws an exception mid-turn. Per the lesson's pitfalls, what should happen next?Apply
A row of boxes user request, LLM, set_timer, with set_timer highlighted and a red cross beside it, a note that set_timer raised an exception and a question mark asking what happens next.
Typed schematic.
7. After heavy filtering is added, the assistant answers nearly every request with 'I can't help with that.' What is the most likely cause?Analyze
A chat with three user requests: weather, a five minute timer, and calendar. Each gets the reply I can't help with that.
Typed illustration of the stem's symptom; requests are made up.
8. The assistant plays a long reply and ignores the user saying 'stop' until it finishes. The TTS call is synchronous. What is the likely cause?Analyze
A speaker lane with a long reply playing block across the whole width and a user mic lane with a short stop block early in the reply. A note says the TTS call is synchronous.
Typed schematic.
9. A webpage your assistant reads aloud contains hidden text instructing it to delete files. What does the lesson say to do with tool results and transcripts like this?Analyze
A mock webpage card with a visible heading and two visible lines and one dimmed hidden line telling the assistant to delete the user's files. An arrow leads to an assistant box, and a question mark asks how the text should be treated.
Typed illustration; the page content is made up.
10. You are shipping an always-on living-room speaker that reads webpages aloud and can run tools. Which combination best limits risk?Design
A card describing a speaker: listening always on, reads aloud web pages, can run tools.
Typed summary of the question's setup.

Ship It

Save this as outputs/skill-voice-assistant-architect.md. Feed it a budget, a scale target, a language, and a compliance constraint, and it produces a full stack spec.

Author's Note

The seven components in this lesson each work fine in isolation. Every demo you'll find online proves that. What breaks in production is the seam between two of them: VAD firing a half-beat late and clipping the first word, or the LLM still streaming tokens after the user has already started talking over it. I've shipped assistants where the single biggest quality win of the whole project wasn't a better model, it was moving the VAD threshold from 0.5 down to 0.3. Tune the seams before you tune the components.

Exercises

  1. Easy. Run code/main.py. It simulates one full turn end-to-end with stub modules and prints per-stage latency.
  2. Medium. Replace the STT stub with a real Whisper model on a pre-recorded .wav. Measure WER and end-to-end latency.
  3. Hard. Add tool calling: implement get_weather (any API) and set_timer. Route the LLM through the tools and verify that when the user says "set a 5 minute timer" the right function fires and the spoken reply confirms it.

Key Terms

TermWhat people sayWhat it actually means
TurnA user + assistant round-tripOne VAD-bounded user speech + one LLM-TTS response.
Barge-inInterruptionUser speaks while assistant talks; assistant stops.
Wake word"Hey assistant"Short keyword detector; Porcupine, Snowboy, openWakeWord.
End-pointingTurn endingVAD + min-silence decision that user has finished.
Pre-rollPre-speech bufferKeep 200-400 ms of audio before VAD fires to avoid first-word clip.
Tool callFunction invocationLLM emits JSON; runtime dispatches; result feeds back in-loop.

Further Reading

Code

Ships With