Flag suspected agent-channel gaps in voice-agent call recordings: mid-turn dead air and dropouts the agent caused itself, read straight from stereo audio (agent left, caller right). Works with any voice agent.
Voice agents sometimes break their own turn. The transcript looks perfect, but the delivered audio stutters or goes silent mid-sentence — "Thank you for calling," then three seconds of dead air, then "my name is Adam, how can I help you?" The text pipeline was fine; the audio wasn't.
hiccup catches that class of problem from the recording alone. It reads a stereo call (agent on one channel, caller on the other), finds stretches where the agent's audio drops out mid-turn, and rules out the ones the caller actually caused.
The core rule: a suspected gap is a low-energy stretch on the agent channel, bounded by agent speech on both sides, that the caller's own speech doesn't explain. Every result carries a confidence, and the whole file is marked unreliable when the two channels aren't cleanly separated. Audio alone can't prove a gap is a bug, so hiccup surfaces symptoms instead of handing out a pass/fail score.
Energy-based (adaptive per-channel floor), no ML.
- Python 3.10+
numpyandsoundfile(installed automatically)- ffmpeg — optional, only needed for formats
soundfilecan't decode on its own. WAV always works; most mp3/flac/ogg files work without it. - The dashboard adds
fastapianduvicorn, pulled in by the[dashboard]extra.
pip install -e . # library + CLI
pip install -e ".[dashboard]" # also install the annotation dashboard(Published to an index later; for now install from a clone.)
As a library:
from hiccup import analyze
result = analyze("call.wav") # one file -> FileResult
results = analyze(["a.wav", "b.mp3"]) # many -> list[FileResult]
if not result.reliable:
print("channels not cleanly separated:", result.separation.reasons)
for g in result.gaps:
print(g.type, g.start_s, g.duration_ms, g.confidence, g.caller_context)From the command line:
hiccup call.wav # human-readable table
hiccup calls/*.wav --json # machine-readable JSON for many files
hiccup call.wav --agent-channel 1 # agent is the right channel instead of the leftDashboard (needs the [dashboard] extra) — point it at a folder of recordings and open the browser. Click a file, or drop in a new one with + analyze audio to upload and analyze it on the spot. You get the waveform with each gap highlighted, click-to-seek playback, and confirm/reject buttons that save your verdict to a <recording>.hiccup.json sidecar:
hiccup serve calls/ # http://127.0.0.1:8000
hiccup serve calls/ --port 9000Combining separate tracks — hiccup needs the agent and caller on separate channels. If your recorder exports them as two isolated mono files (e.g. LiveKit per-participant egress), stitch them into the stereo file hiccup expects. Differing sample rates and lengths are handled for you:
hiccup combine agent.wav caller.wav -o call.wav # writes L=agent, R=caller
hiccup call.wavA single mixed/dual-mono recording (both channels carrying the same audio) can't be analyzed — hiccup flags it as unreliable rather than guessing, because separating two speakers from one mixed stream is out of scope for v1.
| Type | What it means |
|---|---|
dead_air |
A mid-turn silence longer than ~800ms while the caller is quiet. The high-confidence case. |
micro_dropout |
A short near-silent hole (packet-loss-like) inside agent speech. Low confidence by default. |
clustered_short_gaps |
Several micro-dropouts bunched together. Reported honestly as clustered gaps, not as linguistic stutter. |
The caller channel decides whether a silence counts. If the caller started talking at or before the gap, it's a barge-in and gets suppressed. If the caller only spoke up after the dead air, hiccup flags the silence up to that point and labels it reacted_after.
The pipeline is a short chain of small pieces:
load (decode stereo) → channel_qa (is the separation trustworthy?) → activity (per-channel speech mask, adaptive noise floor + hysteresis) → detect (the core rule + the caller gate) → report.
If channel_qa decides the two channels are a duplicated mono mix or bleed into each other, the file's results are marked reliable = False — a "blind" analysis never gets to read as clean.
analyze() returns FileResult(path, sample_rate, separation, gaps, summary, reliable). Each gap is Gap(type, start_s, end_s, duration_ms, confidence, caller_context). confidence is 0–1, folding in gap duration and channel-separation quality; caller_context is one of silent, reacted_after, or barge_in_suppressed.
- Mid-turn silences between 300 and 800ms aren't flagged. They're too short for the dead-air threshold (~800ms) and not near-silent enough to look like a packet-loss hole, so they fall between the
dead_airandmicro_dropoutbuckets. That's deliberate: it keeps natural pauses from tripping the detector. - Bleed below the
channel_qathreshold can fool the caller gate. When a bit of agent audio leaks into the caller channel, the gate can read it as caller speech and suppress a real gap. That's the price of an audio-only approach, so use isolated or dual-channel recordings when you can.
