docsPackagesvie — Turn-Taking Engine

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

EntryHolds
vie/corePure code: signals to events, actions and evidence, the tuner, calibration, probe interpretation. No DOM, timers or clock
vie/runtimecreateVieController (real clock, timers, audio ring, judge), the device profile store, the browser probe, notice text
vie/replayRecorded signals run through the core with a fake clock
vieRe-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

FieldNotes
profileA 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
actionsstartRecognizer, ship, amend, cue, and optional continuation and applyParam
onEvent, onEvidence, onTunerMove, onTelemetryHooks
now, judgeMaxSamples, judgeMinSamples, logOptional

VieController

MethodDoes
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.

  • Signal types: power, vad.start, vad.end, rec.start, rec.end, rec.update, judge.verdict, judge.error, verdict.result, merge, agent.speaking, tick, stop.
  • Action kinds: 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:

ParamDefaultTuned
powerThreshold0.01No
prerollMs0Yes, 0 to 400
tailMs200Bounded 150 to 1500, not tuned since rules v2
settleMs400Yes, 100 to 1200
threshold0.15Yes, 0.05 to 0.6 (Smart Turn)
breathMs300No
continuationMs1000No
holdSilenceMs3000No
judgeTimeoutMs1400No

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.

ExportDoes
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, pushAlertsWhich tuner changes are worth announcing (TuningAlert)
formatTuningNotice, formatTuningAlert, tuningChangedPayload, tuningAlertPayloadText 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 into gates, alerts and a signature.
  • 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