pVAD v1 β speaker-conditioned frame VAD for telephony barge-in
A 171k-parameter streaming model that labels every 10 ms of an 8 kHz caller audio stream as silence, the enrolled caller, or someone else (a bystander, a TV, the agent's own echo). It is conditioned on a 192-d speaker embedding of the caller taken from the first seconds of the call, so a voice-agent's barge-in logic can ignore other voices in the room without waiting for a transcript.
Trained by Telenow AI on its own Indian PSTN call recordings (Hindi, English and other Indian languages) with synthetic bystander / TV / echo mixtures. No audio is released with this model.
Results (held-out recordings, synthetic mixtures, 1.5 s windows)
| window | cosine speaker gate (baseline) | pVAD v1 |
|---|---|---|
| caller alone β accepted as caller | 22 % | 96 % |
| caller + bystander overlapping β accepted | 11 % | 81 % |
| bystander only β accepted (false accept) | 0 % (+19 % unsure) | 6 % |
| window EER | 0.22 | 0.091 |
Frame accuracy 0.87, speech-frame AUC 0.97, target recall 0.84 at 5 % false accept. One 10 ms frame costs ~0.08 ms single-threaded in onnxruntime.
Files
| file | what |
|---|---|
pvad_stream_v1.onnx |
streaming graph β one 10 ms frame per call, carries its own state (use this in production) |
pvad_full_v1.onnx |
whole-sequence graph feats [1,T,40], emb [1,192], h0 [2,1,96] β logits [1,T,3] (offline scoring) |
pvad_v1.pt |
PyTorch state_dict of the model below |
pvad_norm.json |
feature mean / sd (already baked into both ONNX graphs; here for retraining) |
config.json |
the full I/O and feature contract |
example_stream.py |
reference streaming inference with onnxruntime |
train_log.txt |
the training run's validation curve |
Model
fbank(40) β Linear(40β64) + ReLU
β causal Conv1d(64, k=5) + ReLU
β causal Conv1d(64, k=5, dil=2) + ReLU
β FiLM(emb 192 β scale, shift) (x Β· (1+Ξ³) + Ξ²)
β GRU(64β96, 2 layers)
β Linear(96β3) [silence, caller, other]
Normalisation (feats β mu) / sd with the stored global statistics is the
first op inside both graphs.
Input features
Kaldi-style log-mel filterbank, 8 kHz, 40 bins, 25 ms Hamming window,
10 ms hop, no dither, DC removal, 0.97 pre-emphasis, 256-point FFT, energies
floored at float epsilon then ln, no mean subtraction, on int16-scale
samples (i.e. waveform Γ 32768). Equivalent to:
torchaudio.compliance.kaldi.fbank(x_int16_scale[None], num_mel_bins=40,
frame_length=25, frame_shift=10, dither=0.0, energy_floor=0.0,
sample_frequency=8000, window_type="hamming", use_energy=False, snip_edges=True)
Conditioning (enrollment) embedding
A 192-d, L2-normalised WeSpeaker ECAPA-TDNN512-LM embedding
(voxceleb_ECAPA512_LM.onnx, CC-BY-4.0) of 1.5β3 s of the caller's clean
speech: audio upsampled 8 β 16 kHz, 80-bin Kaldi fbank (25/10 ms, Hamming, no
dither), per-utterance mean subtraction, feats [1,T,80] β embs [1,192]. The
model was trained with enrollments of 1.5β3 s (first turn) and 3 s (later in
the call), 50/50. Any other speaker encoder requires retraining.
Streaming contract (pvad_stream_v1.onnx)
| tensor | shape | meaning |
|---|---|---|
feats |
[1,1,40] |
the new 10 ms frame |
hctx |
[1,4,64] |
last 4 post-ReLU input-hidden frames (zero-init) |
cctx |
[1,8,64] |
last 8 conv-1 outputs (zero-init) |
emb |
[1,192] |
enrollment embedding |
h0 |
[2,1,96] |
GRU state (zero-init) |
β logits |
[1,1,3] |
silence / caller / other |
β hn |
[2,1,96] |
next GRU state |
β h1 |
[1,1,64] |
push into hctx (drop the oldest) |
β c1 |
[1,1,64] |
push into cctx (drop the oldest) |
The two rings reproduce the training graph's causal zero padding exactly: streaming output equals the whole-sequence graph to ~2e-6 from the first frame.
Intended use and limits
- Built for barge-in gating in a full-duplex voice agent on telephony audio: "is this speech the caller's?" before STT has returned. It says who is speaking, not whom they address.
- Evaluated on synthetic bystanders (other real callers, room-filtered and mixed at 3β22 dB below the caller, TV at 12β25 dB below, agent echo); real-room recordings were not part of the evaluation.
- Trained on 8 kHz Indian-language telephony; wideband or non-telephony audio is out of distribution.
- Needs an enrollment; until the caller has spoken ~1.5 s there is nothing to condition on β pass through until then.
- Two people legitimately sharing a call must be handled by policy on top (e.g. admit a non-caller voice after N speech runs inside a window).
- Downloads last month
- 9