Turn-taking regression tests for voice agents. Catch barge-in, talk-over, and slow-yield regressions from real call recordings. Offline, deterministic, MIT.
Project description
hotato
Find interruption bugs in your voice agent before users do.
Hotato scores turn-taking from a call recording, on your machine, so call audio stays with you.
It catches the three failures callers feel most: the agent talks over a real interruption, stops
for a backchannel ("mhm"), or is slow to yield. Every failing event returns three measured
signals (did_yield, seconds_to_yield, talk_over_sec) and a fix class that names the knob to turn.
See it fail a bad agent
uvx hotato demo
The demo battery is intentionally bad and fully synthetic: it exists to show what a catch looks like.
Score your own call
uvx hotato doctor --stereo your_call.wav # two-channel WAV: caller ch0, agent ch1
doctor scores the call, writes the visual HTML report, and opens it. Or pull a recording straight from your stack:
- Vapi:
uvx hotato capture --stack vapi --call-id <id>withVAPI_API_KEYset. - Twilio:
uvx hotato capture --stack twilio --recording-sid RE...withTWILIO_ACCOUNT_SID/TWILIO_AUTH_TOKENset. - Retell:
uvx hotato capture --stack retell --call-id <id>withRETELL_API_KEYset; fetches the call's multichannel recording. - LiveKit / Pipecat:
uvx hotato setup --stack livekit(orpipecat) scaffolds recording in your infra, thenhotato capturescores the files.
Scope, stated once: Hotato scores separated caller/agent tracks, as one two-channel WAV or two aligned mono WAVs. Per-stack details and verification dates: adapters/README.md, docs/ADAPTER-STATUS.md.
What Hotato measures
Three objective timing signals per event, measured frame by frame and reproducible with --dump-frames:
| Signal | What it answers |
|---|---|
did_yield |
did the agent stop talking when the caller took the floor? |
seconds_to_yield |
how long did that take? |
talk_over_sec |
how many seconds it kept talking over the caller first |
Every failing event carries exactly one fix, from a taxonomy of two classes. Pass --stack livekit, pipecat, vapi, or generic to get the knob in your stack's vocabulary:
| Failure | fix_class |
The knob it names |
|---|---|---|
| Missed a real interruption | config |
interruption sensitivity: LiveKit turn_handling interruption.min_duration / interruption.min_words, Pipecat VADParams(start_secs, stop_secs, confidence), Vapi stopSpeakingPlan.numWords |
| Slow yield | config |
endpointing latency: LiveKit endpointing.min_delay / endpointing.max_delay, Pipecat SpeechTimeoutUserTurnStopStrategy(user_speech_timeout=...), Vapi stopSpeakingPlan.backoffSeconds |
| Excess talk-over | config |
overlap debounce: LiveKit interruption.min_duration, Pipecat VADParams(stop_secs), Vapi stopSpeakingPlan.voiceSeconds |
| Yielded to its own echo | config |
audio routing: echo cancellation and separate caller/agent channels |
| Yielded to a backchannel | engagement-control |
a vendor-neutral pointer. No single timing threshold separates a backchannel from a one-word interruption. Where your stack provides an interruption/backchannel classifier, use it; the general case calls for a learned engagement-control / addressee-detection layer |
Each config knob ships with a direction and its honest trade-off. When a battery fails on both axes at once (missed a real interruption AND false-triggered on a backchannel), a battery-level funnel pointer fires: that pattern is the signal a discriminating layer is needed, since one threshold trades the two cases against each other.
CI
uvx hotato run --suite barge-in --format json # text is the human default; json is the machine envelope
Exit codes: 0 all pass (or --no-fail), 1 a regression, 2 usage or IO error, or a single recording that is not scorable (silent caller, or agent silent at onset: the event reports scorable: false with the reason, never a fake verdict). Two ready-made gates: copy .github/workflows/hotato.yml for a PR check with a sticky results comment (docs/CI.md), or add --hotato-suite to your existing pytest run; the plugin auto-registers on install and fails the session on a regression (docs/PYTEST.md).
What you get
doctor: score a recording (or the bundled self-test), write the visual report, open it.report: self-contained HTML with per-event SVG timelines, a per-frame inspector, print CSS for PDF, and--base base.jsonregression deltas.team: aggregate a directory of runs into pass rate over time plus mean/median/p90 talk-over and time-to-yield.export: research-grade CSVs (events.csv,frames.csv,envelope.json), columns documented in-file.benchmark: score your captured battery per stack, then compare result files side by side.- Pytest plugin: a
hotato_scorefixture plus a session gate (pytest --hotato-suite). - MCP server: one tool,
voice_eval_run; passreport_pathto also get the HTML report.uvx --from "hotato[mcp]" hotato-mcp - Tiered corpus suites: 112 deterministic scenarios across silver and gold tiers, plus defect suites that must fail.
Optional neural cross-check (verified, non-reference)
pip install 'hotato[neural]', then hotato run --stereo call.wav --backend neural re-runs the same turn-taking timing math over a Silero VAD speech track. The ONNX weights ship inside the package and inference runs offline on CPU. Verified properties:
- Same contract. The neural track comes back in the identical result shape as the energy reference, on the same hop grid, end to end.
- Deterministic. Repeated runs on the same audio are byte-identical.
- The energy reference is untouched. Installing the extra changes no energy number, and
--suitealways scores with energy (a note says so if you pass--backend neuralthere). - Built for real recordings. The bundled fixtures are synthetic shaped noise rendered for the energy reference, and a speech-trained model marks no speech in them, so the cross-check is informative on your own calls, where both tracks carry real speech.
- Clear errors. Without the extra,
--backend neuralexits with an explicit error, never a silent energy fallback. Silero accepts 8000 Hz, 16000 Hz, and integer multiples of 16000 Hz; other rates get an actionable resample message.
Full method and the measured cross-check properties: METHODOLOGY.md.
Install
uvx hotato runs every command with zero install. To add it to a project:
pip install hotato # core: stdlib-only, zero dependencies
pip install 'hotato[neural]' # optional Silero VAD cross-check
pip install 'hotato[livekit]' # LiveKit live capture
pip install 'hotato[pipecat]' # Pipecat live capture
More
- Reports and analytics:
docs/REPORTS.md· Pytest gate:docs/PYTEST.md· Suites:docs/SUITES.md· CI recipes:docs/CI.md - Turn a bad call into a regression test: hotato.dev/docs/regression-loop.html
- Python API:
docs/API.md· Stack benchmarks:docs/BENCHMARK-STACKS.md - Adapters:
adapters/README.md· Status and verification dates:docs/ADAPTER-STATUS.md - Why this exists:
docs/WHY.md· Method:METHODOLOGY.md· Security:SECURITY.md - Contributing: the highest-value PR is a real, labelled call fixture. Start at
docs/SUBMITTING.md.
Why "hotato": good turn-taking is a game of hot potato. Take your turn, then pass it, fast and clean. MIT licensed (LICENSE); the open core stays open.
mcp-name: io.github.attenlabs/hotato
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file hotato-0.2.2.tar.gz.
File metadata
- Download URL: hotato-0.2.2.tar.gz
- Upload date:
- Size: 7.1 MB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.9 {"installer":{"name":"uv","version":"0.11.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c8a362d4521d3d5ba2b9c8d0241699e3210d17c7052a15c5d88d5452a64a4eb2
|
|
| MD5 |
f37949619312422a1ef117474d976284
|
|
| BLAKE2b-256 |
19f8237bc75013c4f270bb2df2c225236c26d83529fba9bc3a170a0fa8ecbea2
|
File details
Details for the file hotato-0.2.2-py3-none-any.whl.
File metadata
- Download URL: hotato-0.2.2-py3-none-any.whl
- Upload date:
- Size: 2.9 MB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.9 {"installer":{"name":"uv","version":"0.11.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
82e6f1a04261e12481a6abb7cc76f166d93bf83f61049b8587ac3a0ad00b9b4d
|
|
| MD5 |
c282cf6e26051fd1f0bdc0ec8706ea3e
|
|
| BLAKE2b-256 |
1b9d349590c5f287dd2b779aca557f8d57959bfa346b36a4e496c1ac2b28c147
|