Home/Docs/How it works

How it works

Hotato scores the audio timing of turn-taking with an energy-based voice-activity detector: deterministic, every threshold an exposed parameter, every frame inspectable.

It measures when there was speech-level energy, not what was said, or who said it.

Read this before trusting a number: energy is not intent

A frame is “active” when its short-time energy crosses a threshold: a cough, a slammed door, or line noise reads active exactly like a word.

Nothing here recovers meaning, only when there was speech-level energy.

No speaker identification, no diarization, no transcription, no emotion or intent detection, and no claim about any vendor’s internal accuracy.

The pipeline, in short

Everything runs offline, standard library, no network; the same audio and config give the same output, every time, on any machine.

  1. Per-frame RMS. Each channel is cut into 20 ms windows stepped by 10 ms; the linear root-mean-square amplitude is computed per window. 10 ms is the time resolution.
  2. RMS → dBFS. Each value becomes decibels relative to full scale, with a floor to avoid log(0).
  3. Per-channel energy VAD. The quietest 10% of frames estimates the noise floor; the threshold is noise_floor + 15 dB, but never below a −60 dBFS absolute gate. A dynamic-margin guard keeps the threshold below speech on a rarely-silent channel. A 150 ms hangover keeps one utterance whole across brief gaps.
  4. Caller onset. Supplied as a label (--onset) or taken as the start of the caller’s earliest sustained active run. The label is preferred, because on mixed audio the earliest energy is not always the caller.

What it reports

SignalMeaning
did_yieldDid the agent go quiet, and stay quiet, for a real interruption, with the caller holding the floor nearby? The proximity check stops an agent that merely finished its own sentence from scoring as a barge-in.
seconds_to_yieldHow long the yield took, from caller onset to the quiet point. null if it never yielded in the search window.
talk_over_secSeconds where caller and agent were both active before the yield.
endpointingOn the same two tracks: a premature start (the agent begins before the caller finishes) or a response gap (silence after the caller’s turn end). Both are null when not derivable, never fabricated.

Why it is reproducible

  • Deterministic given (audio, config): no randomness, no network, no hidden state.
  • Every threshold that drives the active decision is a named, overridable parameter, echoed back in the output.
  • --dump-frames writes, per frame: t_sec, per-channel dBFS, active flags, and the constant thresholds and noise floors, so you can re-derive any reported number by hand.
bash
# every number on the table, re-derivable from the frame dump
uvx hotato run --stereo call.wav --dump-frames frames.json

Two channels is the high-validity mode

With caller and agent on separate channels, overlap is ground-truthable: both tracks active at once, by construction.

Mono is accepted but degraded: separation is then no better than the VAD, so mono requires an onset label.

Hotato won’t report caller/agent separation from one mixed channel as authoritative.

Validity is measured, not claimed

The bundled fixtures are synthetic, rendered from exact declared segment boundaries, so the scorer’s own measurement error is computable.

Report that: timing error in milliseconds per scenario, and a four-cell confusion matrix for did_yield (correct yields, missed yields, correct holds, phantom yields).

Never blended into a single accuracy percentage: a missed interruption and a phantom yield are different failures with different fixes.

One number would hide the very distinction the tool exists to surface.

Full method

Every default, formula, and a worked hand-derivation live in METHODOLOGY.md in the repo. Disagree with a number? The frames are on the table and every threshold is yours to change. Read METHODOLOGY.md →