vie
vie (Voice Interface Engine) is the turn-taking engine behind SmartChats
voice: it decides when a spoken turn is over and ships the text. It is the
default engine in tivi (voiceEngine: 'vie').
This page is the API reference. What the engine does turn by turn, the judges and the tuning rules are described in Voice engine.
Layout
| Entry | Holds |
|---|---|
vie/core | Pure code: signals to events, actions and evidence, the tuner, calibration, probe interpretation. No DOM, timers or clock |
vie/runtime | createVieController (real clock, timers, audio ring, judge), the device profile store, the browser probe, notice text |
vie/replay | Recorded signals run through the core with a fake clock |
vie | Re-exports all three |
The package has no runtime dependencies.
Controller
import { createVieController, getProfileStore } from 'vie';
const vie = createVieController({
profile: getProfileStore(),
smartTurn: () => false,
judgeSource: () => 'server',
speechThreshold: () => 0.5,
judge: () => null,
actions: {
startRecognizer: (trigger) => recognizer.start(),
ship: (text, { turn, via, p }) => sendToAgent(text),
amend: (text, meta) => replaceMessage(meta.sentText, text),
cue: (p, turn, source) => playAck(),
},
});
vie.onVadStart();
vie.onUpdate(key, text, isFinal);
vie.onVadEnd(null);ControllerDeps
| Field | Notes |
|---|---|
profile | A ProfileStore. Params, pins and the evidence ledger are read from and saved to it |
smartTurn | () => boolean. Whether the browser Smart Turn judge is on |
judgeSource | () => 'smart_turn' | 'server' | 'none'. Default: smartTurn() ? 'smart_turn' : 'none' |
speechThreshold | () => number. VAD probability that counts a frame as speech |
judge | () => JudgeLike | null. JudgeLike has predict(audio, signal?) resolving to { probability, latency_ms } |
gates | () => TunerGates. Which tuners the device allows |
actions | startRecognizer, ship, amend, cue, and optional continuation and applyParam |
onEvent, onEvidence, onTunerMove, onTelemetry | Hooks |
now, judgeMaxSamples, judgeMinSamples, log | Optional |
VieController
| Method | Does |
|---|---|
onPower(level), onRecStart(trigger, prerollActiveMs), onRecEnd(), onVadStart(), onVadEnd(tailElapsedMs), onUpdate(key, text, isFinal) | Signals in |
pushFrame(samples, prob) | Feeds the audio ring the judge reads |
setAgentSpeaking(speaking) | Suppresses listening while the agent talks |
onVerdict(result) | The server verdict for the last ship (complete, source, model, verdictMs, optional score, threshold, judge) |
pendingVerdictTurn() | The turn whose verdict is pending, or null |
mergeContinuation(turn) | After the app aborts the previous run, send it merged with turn |
reportCutoff(where, heard) | Records an agent-reported cut-off as evidence |
applyParam(key, value) | Applies a pinned or calibrated value to the running session |
stop() | Drops everything pending |
view(), params(), log(), metrics(), ringSeconds(), lastJudgeInput() | Read state |
Engine
createEngine({ params, smartTurn, judgeSource? }) returns an Engine with
step(signal): EngineOutput and view(). Each step returns events,
actions, evidence and wakeAt (when the engine next needs a tick). The
controller drives it. Replay and tests drive it directly.
Signaltypes:power,vad.start,vad.end,rec.start,rec.end,rec.update,judge.verdict,judge.error,verdict.result,merge,agent.speaking,tick,stop.Actionkinds:start_recognizer,run_judge,cancel_judge,ship,amend,cue,discard,continuation.EventName:probe.done,listen.wanted,speech.ended,transcript.settled,turn.ended,turn.shipped,turn.sent,turn.reopened,turn.amended,turn.held,turn.merged,turn.forced.
Parameters
Params and DEFAULT_PARAMS:
| Param | Default | Tuned |
|---|---|---|
powerThreshold | 0.01 | No |
prerollMs | 0 | Yes, 0 to 400 |
tailMs | 200 | Bounded 150 to 1500, not tuned since rules v2 |
settleMs | 400 | Yes, 100 to 1200 |
threshold | 0.15 | Yes, 0.05 to 0.6 (Smart Turn) |
breathMs | 300 | No |
continuationMs | 1000 | No |
holdSilenceMs | 3000 | No |
judgeTimeoutMs | 1400 | No |
TUNED_KEYS, BOUNDS, CONSERVATIVE, SAFE_DIRECTION, clampParam,
TUNING_RULES_VERSION (2) and Pins are exported with them.
Evidence and tuner
Each evidence row (EvidenceRow) judges one event: name, ms, turn,
judges, measure, the params in force and source ('engine',
'agent' or 'calibration'). Names: onset.margin, onset.slack,
tail.early, settle.late_words, settle.slack, settle.gap,
judge.wrong_hold, ship.followed_by_speech, verdict.wrong_hold,
verdict.cut_off.
| Export | Does |
|---|---|
freshLedger(), addRow(ledger, row), noteTurn(ledger) | The per-device evidence ledger (capped at LEDGER_CAP, 300 rows) |
rate(ledger, name, window?) | Share of recent turns with that evidence |
watchMetrics(ledger, window?) | Summary metrics over the last DISPLAY_WINDOW (20) turns |
applyEvidence(row, params, pins, gates, state) | The tuner. Returns { params, state, move, skipped? } |
freshTunerState() | Empty tuner state |
moveAlert, turnAlerts, pushAlerts | Which tuner changes are worth announcing (TuningAlert) |
formatTuningNotice, formatTuningAlert, tuningChangedPayload, tuningAlertPayload | Text and telemetry payloads for those |
A TunerMove has param, from, to, reason, evidence, ms and turn.
Device profile
DeviceProfile holds params, pins, autoTune, the evidence ledger,
tuner state, the last probe and calibration result, and dismissed
alerts. It is stored under PROFILE_KEY (vie.device.v1).
import { getProfileStore, createProfileStore } from 'vie';
const profile = getProfileStore(); // shared store on localStorage
profile.setParam('settleMs', 600, true); // set and pin
profile.unpin('settleMs');
profile.resetTuning();ProfileStore methods: get, update, subscribe, getSnapshot,
setParam(key, value, pin?), pin, unpin, resetTuning(seed?).
createProfileStore(store, seed?, now?) takes any KeyValueStore (or
null for memory only).
Probe and calibration
collectCapabilities(inputs?)reads what the browser supports (recognizer, track start, AudioWorklet, WASM SIMD and threads, platform).interpretProbe(caps, at)turns that intogates,alertsand asignature.- Calibration:
startCalibration(params, gates, at, script?),observeItem(state, params, observation),summarize(state),judgeItem,DEFAULT_SCRIPT.
Replay
import { replay, fromSessionExport } from 'vie';
const fixture = fromSessionExport(sessionExport);
const result = replay(fixture, { params: { settleMs: 600 } });
result.score; // { exact, lostWordsAtShip, recoveredByAmend, shipAfterEndP50Ms, holds, ... }replay(fixture, opts?) runs a Fixture (signals plus truth, the text
per turn) with optional params, smartTurn and a judge callback, and
returns turns, events, actions, evidence and score. drive(engine, fixture, opts?)
does the same with an engine you built. fromSessionExport builds a fixture
from a session export. Interim word boundaries are approximate.
See also
- Voice engine for the behaviour
- tivi, which hosts the controller
- llm-service for the server verdict
- Source:
packages/vie/src/index.ts