Voice engine
In a voice session, SmartChats has to decide when you have finished speaking and your words should go to the agent. The part that makes this decision is vie, the voice interface engine (packages/vie). It is the default engine. The older pipeline stays available as voiceEngine: 'legacy'.
This page explains how vie works, what you can change, and where to look when a turn ends too early or too late. The full design is in the vie spec.
What the engine does
vie reads four kinds of signal:
- Audio power: is the room loud enough to start the speech recognizer?
- The speech detector (VAD): when speech starts and stops.
- The browser speech recognizer: the text, updated word by word.
- A judge: does the text so far sound finished?
From these it produces a fixed chain of events for each turn:
listen.wanted → speech.ended → transcript.settled → turn.sent → turn.ended → turn.shipped → (turn.amended)| Event | Meaning | Decided by |
|---|---|---|
listen.wanted | Sound is loud enough to start the recognizer | Audio power vs powerThreshold |
speech.ended | You paused | The speech detector, after tailMs of silence |
transcript.settled | The words have caught up | No word-changing update for settleMs after speech.ended |
turn.sent | The text left for the agent | At the settle for the server judge, at COMPLETE for the others |
turn.ended / turn.shipped | You are done and the turn is final | The judge |
turn.amended | Words arrived for a turn already sent | The recognizer delivering late |
Around that chain, the turn lifecycle adds four more events:
| Event | Meaning |
|---|---|
turn.held | The judge said INCOMPLETE. The turn waits for more words. |
turn.merged | A held turn shipped with the new words added to it, as one message. |
turn.forced | A held turn shipped as it was, after silence or a commit keyword. |
turn.reopened | New speech reopened a turn that had already been sent. |
Only one thing is ever undone. If speech starts again while vie waits for the settle, speech.ended is cancelled and the turn carries on as one.
The turn states look like this:
listening ─ vad.end ─► ended ─ settleMs quiet ─► judging ─ COMPLETE ───► shipped ─ reply done ─► idle
▲ │ │
│ vad.start ────┘ (the one retraction) ├─ INCOMPLETE ─► held ─ silence / keyword ─► shipped (as is)
│ │ │
└──────────── continuation ◄────────────────────┴─ abort ─► held ─┘ new words ─► merged into the turnThe three judges
The speech detector and the settle only know that you paused. The judge decides whether the pause ended your thought. Choose one in Settings under “Who decides you’re done talking” (tivi setting vieJudge).
| Judge | Setting | How it decides | Use it when |
|---|---|---|---|
| Server verdict | server (default) | A small LLM call on the server reads your words before the main reply | You have a key for the verdict model. This is the most accurate option. |
| Smart Turn | smart_turn | An audio model scores the end of your speech in the browser | You want no extra server call. It struggles on some voices (see the spec, section 7). |
| None | none | Silence alone ends the turn | You speak in short, complete commands, or you want the lowest latency. |
Every judge goes through the same judging state. They differ only in how they answer:
- Server: vie sends the text with a verdict request at the settle (
turn.sent). The verdict arrives before any reply text, usually 0.3 to 0.5 s later. If it never arrives, the turn is confirmed after 3 s. - Smart Turn: vie scores the audio. A probability at or above
thresholdis COMPLETE. PastjudgeTimeoutMs(1.4 s) the turn ships anyway. - None: COMPLETE at once.
Words that go to a tool waiting for input (dictation, voice memo) always use no judge, because they make no model call for the server to judge.
The server turn verdict
The server judge is one small LLM call made before the main reply. It answers COMPLETE (the reply proceeds) or INCOMPLETE (the turn is held). The code is in packages/llm-service/src/streaming/verdict.ts, and TURN_VERDICT.md has the measurements.
The app asks for a verdict only when all of these hold:
- the call comes from a running voice session
- the voice engine is vie and the judge is
server - a verdict model is set (
verdictModel, defaultdeepseek-v4.1-flash)
Judge v2
The app sends judge: 'v2'. Under judge v2:
- The score is P(COMPLETE), the model’s own probability that its one-word answer is COMPLETE. It is read from the first output token’s probabilities, not written by the model.
- The threshold defaults to 0.5. The server holds the turn when the score is below it.
- The input is the last 2 conversational turns, quoted as a transcript (“User: … / Assistant: …”) in one message. The judge reads the exchange as a third party. This stops a reply like “go ahead, I’m listening” from reading as an invitation to keep talking.
Scores pile up at both ends. Fewer than 3% fall between 0.1 and 0.9, so the threshold matters little in practice. There is no tuner for it.
Failures never cut you off. A verdict that fails, cannot be parsed or passes its cap (verdictCapMs, default 800 ms) counts as COMPLETE. The only failure you can hear is a false hold: the app waiting for more after you finished.
Holds and merges
When the verdict is INCOMPLETE:
- The server ends the response without making the main model call.
- The app plays the keep-going cue. This is a short tone, or a spoken “ok” / “mhm” when acks are on (
enableAcks). - Whatever you say next is merged with the held text and sent as one message (
turn.merged). - If you say nothing for
holdSilenceMs(3 s), or say a commit keyword (“done”, “finished”, “listo”, “terminado”), the held text ships as it is (turn.forced). A forced turn carries no verdict.
Providers
Judge v2 needs a provider that returns token probabilities. Today that is Together, so the default verdict model needs a Together key.
- With a Together key, v2 decides from P(COMPLETE).
- With a verdict model from another provider, v2 decides from the sampled word and the threshold has no effect.
- With no key for the verdict model, the local server skips the verdict and answers the turn, as if the verdict had said COMPLETE.
See Providers & API keys for how to set the key.
Parameters
All timing lives in packages/vie/src/core/params.ts.
| Parameter | Default | Bounds | How it is set |
|---|---|---|---|
powerThreshold | 0.01 | Manual | |
tailMs | 200 ms | 150 to 1500 | Fixed. The speech detector’s debounce. Never tuned, can be set by hand. |
settleMs | 400 ms | 100 to 1200 | Adapts both ways, every turn |
prerollMs | 0 ms | 0 to 400 | Ratchets up when a first word is clipped |
threshold | 0.15 | 0.05 to 0.6 | Smart Turn’s COMPLETE threshold. Ratchets down on a wrong hold. |
breathMs | 300 ms | Manual | |
continuationMs | 1000 ms | Manual. Speech within this of a ship belongs to that turn. | |
holdSilenceMs | 3000 ms | Fixed. A held turn ships after this much silence. | |
judgeTimeoutMs | 1400 ms | Fixed. Smart Turn only. |
How tuning works
vie learns its timing on each device from what happens in real turns:
- Settle eases down and jumps up. On every settle,
settleMsmoves 25% of the way toward the largest gap between word updates in that settle window. These moves are quiet. When a word lands late after a ship,settleMsjumps straight to that word’s gap. On a clean device settle drifts down toward the recognizer’s real gaps, jumps when a word arrives late, then eases down again. - The tail is fixed at 200 ms. Deciding whether a pause ended your thought is the settle’s and the judge’s job.
- Pre-roll and the Smart Turn threshold ratchet. Each moves at most one step per turn, and only in its safe direction: pre-roll up after a clipped first word, the threshold down after a wrong hold. They do not come back down on their own.
- Pins. A pinned parameter never moves. Moving a slider in Settings pins it.
- Per device. Parameters, pins, the evidence ledger and the tuner history are stored in the browser’s localStorage under
vie.device.v1. A phone and a laptop each learn their own values.
Calibration pins settle. Running calibration on /voicelab pins settleMs at the end of its descent, which stops it adapting. Unpin it if you want the per-turn easing back.
Continuations, late words and amends
Two things can happen after a turn has settled. vie tells them apart by whether the speech detector heard new speech.
Late words. A word update that arrives after the settle with no new speech start is the recognizer finishing what it already heard. It belongs to the same utterance, in any phase:
- While the judge runs, the words are added to the turn. For the server judge the request is sent again with the full text.
- While the turn is held, they are added to the held text.
- After the ship, the turn is amended (
turn.amended). Before the reply’s first token, the app cancels the reply and sends one complete message in place of the old one. After that, the words are handed to the agent as late words.
A recognizer revision is not late words. If the recognizer only repeats the end of what was already sent (for example by dropping a word), the sent text is kept and only the new words after it count.
Continuations. New speech after the settle, a speech start followed by its first word, is a continuation. vie acts at the first word, not the bare speech start, because coughs and background talk often start the detector without producing words.
| When the first word arrives | What happens |
|---|---|
| The assistant is speaking | Barge-in: the audio stops at once, then cancel and add (below) |
| The judge or model is running, nothing heard yet, no tool has run | Abort and hold. The run is stopped, the agent never sees it, and the held text plus the new words go as one message. |
| A tool already ran in this turn | Cancel and add. The new words go as their own message after a cancel note, so a side effect is not repeated. |
| A tool is waiting for input | The words go to the tool |
| The reply is finished | A normal new turn |
Settings you can change
These are tivi settings (packages/tivi/src/lib/settings.ts), set in the Settings panel:
| Setting | Default | What it does |
|---|---|---|
voiceEngine | vie | legacy switches back to the old pipeline |
vieJudge | server | server, smart_turn or none |
verdictModel | deepseek-v4.1-flash | The server verdict model |
verdictEffort | none | Reasoning effort for the verdict call. none turns thinking off. |
verdictCapMs | 800 | Longest the server waits for the verdict before going ahead as COMPLETE |
enableAcks | false | Speak “ok” / “mhm” on a hold in place of the tone |
The engine choice and the verdict settings are only in the Settings panel. The agent can change the rest by voice. Ask it, for example, “turn on backchannel acks” or “pin the settle time at 500 ms”. Its tools:
set_voice_settingandreset_voice_settingsfor settings such as acks, pre-roll and playback speedget_voice_tuningfor current parameters, pins, recent tuner moves and device alertspin_voice_setting,unpin_voice_setting,reset_voice_tuningandset_auto_tunefor vie’s learned parametersreport_speech_cutoffwhen a transcript lost its first word (this raises pre-roll). A lost last word is recorded but does not move settle.
/voicelab shows the engine’s layers live and runs calibration.
Telemetry
vie writes these events to insights:
| Event | What to look at |
|---|---|
voice_turn | One row per turn: params.settle_ms, params.tail_ms, settle_max_gap_ms, settle_update_offsets_ms |
vie_event | Each event in the chain. transcript.settled carries max_gap_ms, turn.amended carries gap_ms, turn.reopened carries phase and held_by |
vie_evidence | Each evidence row, including verdict.wrong_hold (a hold ended by silence or a keyword) and verdict.cut_off (speech soon after a COMPLETE), with the score and threshold |
voice_tuning_changed | Each parameter move. quiet: true marks routine settle easing |
turn_verdict | Each server verdict as vie received it |
smart_turn_decision | Each Smart Turn score |
A healthy device shows settle easing down, an occasional jump on a late word, and a low rate of late words. Each late word cancels a reply in progress.
See also
- vie spec: states, tuning rules, invariants and open items
- TURN_VERDICT.md: the server verdict, judge v2 and the replay script for measuring a prompt or model change
- Providers & API keys
- tivi