Field Notes
N 44° 56′Eval Design/Field Notes

VAD stopped the voice agent 640 milliseconds earlier

Strict local VAD vs strict transcript triggering, measured acoustically on a common clock. Plus the messy engineering work required to trust the results.

Ryan Schuetz/Jul 2026/9 min readN 44° 56′

The 31st live call of this project was the first one I could trust, and it was missing a packet.

One frame out of 1,244 never arrived. Instead of interpolating through it and pretending the timeline was continuous, the measurement harness declared a 191 ms void in the greeting audio and carried it forward as a known defect. Manual review confirmed the void ended 1.12 seconds before the stop boundary and touched nothing the measurement depended on.

That capture became the template for everything after it: if this benchmark was going to claim a latency number, every defect had to be on the record.

Capture review screen for run p2-96ddebf9e4d6eb36: a two-channel waveform timeline on one clock showing received agent audio and the transmitted caller fixture, a red voided loss interval marked in the greeting audio, and four boundary inset waveforms for the stop, fixture emission, separating silence, and response onset.
The capture review for that call: both channels on one monotonic clock, with the 191 ms voided loss interval (red) carried on the record.
N 01°The problem with the two-repo fight

The problem with the two-repo fight

The last time I measured this, I tested a transcript-triggered voice agent built on Telnyx Call Control, offleash. It had a 1.3 second interruption floor.

You might expect this post to pit that old transcript repo against a new frame-based repo, onset, winner takes the headline.

That is not what this is.

A head-to-head between two different architectures is a whole-system observation, not an experiment. If the old repo loses by 900 ms, is it because of the transcription surface? The TTS path? The media ownership? The provider events? A comparison cannot tell you.

That two-repo comparison was formally preregistered, demoted to secondary for exactly this reason, and gated on offleash being rerunnable through the same acoustic boundary, which it was not. So the last post’s 1328 ms stays historical context, and it should not be compared with any number in this post, including this post’s transcript condition: different agent, different measurement boundary, different transport.

Instead, the primary comparison flips exactly one thing inside a single media-stream agent, onset:

  • Condition A: a local VAD decision on the raw audio frames is the sole interruption-eligible trigger.
  • Condition B: the arrival of the first eligible transcript is the sole eligible trigger.

Shared between the two conditions: the runtime, the call and media topology, STT connection, TTS path, playback queue, caller fixture, and analysis rules. One variable moves. The result is actually attributable.

N 02°The obvious measurement was invalid

The obvious measurement was invalid

The original plan was to measure the interruption at the provider’s media-stream surfaces. Twenty-six bounded live attempts proved this was a bad idea. Four behaviors controlled the outcome:

  • A bidirectional stream did not expose a usable provider-outbound track for the same leg.
  • Inbound-direction surfaces on the agent leg mirrored the agent’s own injected audio rather than providing an isolated caller reference.
  • Receive-only streams inherited the leg’s media context and did not honor the codec override the plan needed.
  • A Call Control-answered harness endpoint did not transmit RTP, which starved the delivery-gated design.

The provider surfaces were not broken. They behaved consistently; they could not satisfy this measurement topology, which needed an isolated view of what a caller actually hears. To get that, the measurement had to move to the caller’s side of the wire.

N 03°The rescue: a common acoustic boundary

The rescue: a common acoustic boundary

I replaced the provider-side harness with an external SIP media endpoint acting as the caller.

This harness places the call, transmits the synthetic caller fixture ("I need a table for two") 1,000 ms into the agent’s greeting, records every frame of returned agent audio, and timestamps everything on a single monotonic clock.

This is critical: the headline metric runs from the harness-side emitted-stimulus boundary to the harness-side returned-audio stop boundary. No cross-process clock math. No webhook timestamps. No provider-reported durations.

One eligibility rule matters for reading the tables below. The fixture is timed to land mid-utterance, but the agent’s greeting has natural pauses. If the fixture happened to land on one, there was no active speech to interrupt, so the trial was marked ineligible. Ineligible trials stayed in the dataset and the denominator; they were never rerun.

N 04°The detector calibration loop

The detector calibration loop

Detecting when the agent "stopped talking" requires a silence detector. To be clear about roles: this detector is part of the measurement, deciding when returned audio actually stopped. It is not the agent’s VAD, which is the interruption trigger under test in Condition A. I used a finite grid over labeled audio spans to find the simplest passing threshold and shortest passing hold.

Here is what it took before I believed the detector:

  1. 01Campaign 1: 300 ms hold

    Passed the initial labels, but qualification caught a defect: the detector labeled a natural pause in the agent’s speech as a stop. Old response audio resumed immediately after. Campaign discarded in full.

  2. 02Campaign 2: 400 ms hold

    The recalibrated grid caught three more natural-pause false stops, plus a classifier omission for terminal failures. Campaign discarded in full.

  3. 03Final campaign: 600 ms hold

    The final detector passed qualification entirely. Zero false stops, zero stale-audio resumptions, and zero lost turns among eligible trials.

Both recalibrations were driven by pooled natural-pause evidence, never by between-condition latency results, so the detector could not drift toward a preferred answer. No latency from a discarded campaign ever entered the final analysis.

The 600 ms logic: the 600 ms value is a confirmation interval. The detector reports the stop boundary at the beginning of the silent run, not 600 ms after it. If a trial measures a 400 ms latency, it still required 600 ms of future silence to confirm that the 400 ms mark was the actual stop.

N 05°The final results

The final results

For the final run, I scheduled 40 attempts per condition in a balanced-block order under a frozen seed. Failed and ineligible attempts were never replaced. In voice systems, reliability metrics are as critical as speed metrics. Keeping those rows in the dataset costs nothing and buys everything: a benchmark that quietly retries its failures is reporting its retry policy, not its system.

MetricStrict local VADStrict transcript
Attempted4040
Eligible3533
Successful3228
Success / attempted80.0%70.0%
Median latency400.7 ms1,040.9 ms
p90 latency501.5 ms1,690.7 ms
The predeclared median difference: 640.2 ms earlier for strict local VAD.

The trials were independent and unpaired, and the preregistered analysis reports bootstrap intervals for each condition’s median, 361.5 to 440.0 ms for VAD and 900.6 to 1211.5 ms for transcript, rather than a hypothesis test. I am not claiming statistical significance. I am showing two medians whose intervals sit far apart, with all 80 trials, including every failure, in a committed per-trial CSV.

Look at the spread, not just the medians. The VAD condition is tight, with a worst successful trial at 720 ms. The transcript condition is wide, with a p90 near 1.7 seconds. A transcript-triggered agent does not have a barge-in latency; it has a barge-in distribution, and the tail is what callers remember.

N 06°Architectural reality and product takeaways

Architectural reality and product takeaways

The application code itself was not the bottleneck in either condition. Once the trigger fired, the internal bookkeeping was sub-millisecond:

Agent-local metricVAD medianTranscript median
Trigger to interruption request0.506 ms0.495 ms
Request to media-epoch invalidation0.330 ms0.561 ms
Request to clear start0.357 ms0.585 ms
Clear send duration0.036 ms0.046 ms

Measurement warning: do not subtract these internal agent timestamps from the external SIP harness clock. They live in different processes. They merely prove the application acts immediately once allowed.

The gap exists because of when the agent is allowed to decide. The VAD condition acts as soon as sustained caller energy shows up in the frames. The transcript condition has to wait for speech to become text on a remote server and travel back.

The scope is deliberately narrow: this harness, this fixture, this VAD configuration, this implementation. It is not a claim that every VAD beats every STT engine, and it says nothing about other thresholds, voices, providers, or physical devices with speakerphone echo. The full limitations list is in the repo’s benchmark plan.

The harness is part of the release. It is a SIP caller that measures a dialable voice agent acoustically, from the caller’s side of the wire, and the agent under test is untouched. It dials a number, injects a timed fixture, records every returned frame, and timestamps everything on one clock. It currently drives onset, and nothing in the measurement path knows what it is calling. The spec, the qualification protocol, and the per-trial manifests are all in the repo.

The product takeaway: for raw speed, the earlier acoustic signal wins clearly. But a transcript loop still buys you something.

A transcript is late, but interpreted. A VAD decision is early, but dumb.

VAD mostly says that speech-like energy exists. It does not know whether the caller is saying "stop," giving an agreeable "mm-hmm," talking to someone else in the room, or just dropping a pan on the floor.

Waiting for words costs you about 640 ms in this implementation. Whether every sudden sound should cancel your agent remains your product decision.

This kind of loop analysis is the core of how we evaluate production voice agents at ProofNorth.

N 07°What I am measuring next

What I am measuring next

The engine question from the last post is still open: interim-forwarding behavior for non-Google STT engines through Call Control transcription webhooks was inconsistent in early testing, and it needs a real sample size before anything gets published. That is the next field note.

N 08°Methods appendix

Methods appendix

Fixture "I need a table for two," injected 1,000 ms after confirmed active agent playback; frozen greeting natural-end reference 3,200 ms. Wire audio PCMU 8 kHz mono; analysis PCM16 16 kHz mono; 20 ms frames. Detector: RMS, minus 42 dBFS activity and silence thresholds, 100 ms arm, 600 ms hold, selected by a preregistered rule over a condition-independent grid and validated in a separate qualification population. Final collection seed 2026071306; analysis seed 2026071305; 10,000 bootstrap resamples per condition median; no replacement policy throughout.

  • Preregistration, comparison hierarchy, gates, failure taxonomy, and limitations: BENCHMARK_PLAN.md in the onset repo.
  • Final analysis: bench/PHASE3_REPORT.md.
  • Provider-surface attempts and the SIP amendment: bench/PHASE2_REPORT.md.
N 09°Links

Links

Ryan Schuetz

Founder · ProofNorth

Founder and CTO of Automatdo, an active voice AI platform for contact centers. Previously CTO of HelloGym for over a decade. Architect of a Fortune 50 pharmaceutical COVID-19 response contact center. He still writes production code daily.

Read on LinkedIn →
N 44°56′ Next Step

Move from demo to production.

Two-week AI Readiness Audit. Fixed scope, fixed fee, written deliverables your team owns.

Read the Method