CoolFace
Modelpublic

Scicom-intl/semantic-vad-eot-whisper-small

sourceHugging Faceapache-2.0updated 16d agoView on Hugging Face
0likes
Model Card

Semantic VAD — Whisper-small end-of-turn detector (audio only)

The whisper-small sibling of `Scicom-intl/semantic-vad-eot-whisper-tiny` and `…-whisper-base`: same recipe, same input contract, same data, an 88 M-parameter encoder. Given the last 8 seconds of a caller's 16 kHz audio it returns p(end of turn) — finished speaking vs paused mid-sentence — with no transcript.

88 M parameters · int8 ONNX 95 MB · the most accurate of the three (validation AUC 0.894 vs 0.877 base / 0.859 tiny; offline 0.86 / 0.88 / 0.99 at 0 / 0.2 / 0.6 s into a pause). CPU cost on one thread is ≈ 175–200 ms per prediction, 78 ms at four threads and 50 ms at eight (measured on a busy 164-core box), so it is the choice for nodes with cores to spare or for a GPU-served, batched endpoint (STT-API's RemoteEoT backend posts the PCM to a URL and is the hook for that); the tiny model remains the pick for a single CPU thread per agent.

Serving cost (int8 ONNX, measured 2026-09-08): 190–220 ms per prediction on one CPU thread standalone and 200 / 245 ms p50 / p90 inside a LiveKit agent process (≈ 3 % of a core per call; a 164-core node sustains ≈ 290 predictions/s with 64 dedicated single-thread processes at p90 244 ms). It keeps an agent worker thread busy for ~0.2 s per prediction, so run it with four threads, in a sidecar, or behind the GPU API rather than inside a dense agent process; latency does not depend on the audio length sent (fixed 8 s window).

Results

Compared with other open detectors (eot-bench, private telephony)

Same eot-bench harness for every model (100 ms causal grid over every pause ≥ 0.1 s, threshold × action_delay × timeout policy sweep, scalar metrics scored 0.2 s into each pause), random private telephony test turns, both language tags pooled. Third-party models run through eot-bench's own adapters with the language gate widened to Malay; the text detector on transcripts from our Whisper STT (segment timestamps interpolated to words, ~30 % of these short turns have no transcript); ultraVAD without the assistant context it was designed for (this set has none); LiveKit's cloud Turn Detector v1 streamed the 300-turn set once with the data owner's approval (292 of 300 turns scored, 8 failed the gateway handshake; LiveKit Cloud caps this project at ~5 streaming turns per minute, so the run was paced).

The 300 benchmark turns (300 eot / 191 hold spans) — every detector, LiveKit cloud v1 included:

modelcutoff @ 300 mscutoff @ 600 mslatency @ 5 % cutofflatency @ 10 % cutoffAUC
Scicom Semantic VAD (enterprise model, GPU-served, private)38.6 %20.0 %1 444 ms1 034 ms0.87
Semantic-VAD whisper-small v648.6 %22.1 %1 381 ms1 099 ms0.86
Semantic-VAD whisper-base v645.0 %23.6 %1 592 ms1 260 ms0.84
Semantic-VAD whisper-tiny v657.9 %28.6 %1 685 ms1 332 ms0.78
LiveKit turn-detector v1-mini (audio-only, local)55.7 %28.6 %1 633 ms1 292 ms0.76
LiveKit Turn Detector v1 (cloud, audio)66.7 %27.8 %1 568 ms1 348 ms0.70
ultraVAD (no text context)65.7 %32.1 %1 880 ms1 384 ms0.65
smart-turn v3.273.6 %31.4 %1 863 ms1 367 ms0.64
smart-turn v277.1 %32.1 %2 000 ms1 420 ms0.64
LiveKit text turn-detector v0.4.1-intl (on STT transcripts)1 952 ms1 769 ms0.45
VAD baseline (silence timer)77.9 %32.1 %1 900 ms1 510 ms

LiveKit's cloud v1 lands between ultraVAD and the silence timer on this Malay-heavy telephony audio (no Malay, no telephony in its training); its local v1-mini does better. Scicom Semantic VAD is the enterprise member of this family, served from a GPU with dynamic batching, and is not public.

Pareto frontier, 300 turns

Best false-cutoff rate at a 300 / 600 ms latency budget, 300 turns

Best mean latency at a 5 / 10 % false-cutoff budget, 300 turns

1 000 random test turns (1 010 eot / 569 hold spans) — the larger sample; the cloud detector was not run here:

modelcutoff @ 300 mscutoff @ 600 mslatency @ 5 % cutofflatency @ 10 % cutoffAUC
Scicom Semantic VAD (enterprise model, GPU-served, private)43.8 %24.5 %1 663 ms1 215 ms0.87
Semantic-VAD whisper-small v6 (this model)45.2 %24.5 %1 839 ms1 226 ms0.86
Semantic-VAD whisper-base v6 (repo)47.3 %25.4 %1 812 ms1 280 ms0.85
Semantic-VAD whisper-tiny v6 (repo)52.2 %30.8 %2 042 ms1 503 ms0.81
LiveKit turn-detector v1-mini (audio-only, livekit-local-inference)63.6 %35.7 %2 156 ms1 720 ms0.74
ultraVAD (fixie-ai/ultraVAD, 0.7 B, no text context)71.6 %39.6 %2 212 ms1 784 ms0.65
smart-turn v3.2 (pipecat-ai/smart-turn-v3)73.7 %36.6 %2 296 ms1 860 ms0.65
LiveKit text turn-detector v0.4.1-intl (on STT transcripts)2 381 ms1 894 ms0.45
smart-turn v2 (pipecat-ai/smart-turn-v2, 95 M wav2vec2)74.1 %39.6 %2 500 ms2 000 ms0.62
VAD baseline (silence timer)78.1 %43.6 %2 250 ms1 770 ms

Pareto frontier, 1 000 turns

Best false-cutoff rate at a 300 / 600 ms latency budget, 1 000 turns

Best mean latency at a 5 / 10 % false-cutoff budget, 1 000 turns

Operating points across sets and language tags

In the pipeline and at fixed cut points

In a real LiveKit Agents 1.8 pipeline (Silero VAD → turn detector → endpointing, no STT, 300 recorded telephony turns, LiveKit defaults: VAD silence 0.55 s, min_delay 0.5 s, max_delay 3.0 s):

turn detectorlatency p50 / p90turns cut offfinished turns on the fast pathAUC (eot vs hold)
VAD only0.63 / 0.71 s14.3 %
smart-turn-v3, threshold 0.50.65 / 3.04 s10.0 %82 %0.74
tiny variant, threshold 0.50.64 / 0.74 s10.0 %95 %0.84
base variant, threshold 0.30.64 / 0.74 s9.7 %96 %0.88
this model, threshold 0.50.64 / 0.76 s10.3 %94 %0.89
this model, threshold 0.30.64 / 0.73 s10.7 %97 %0.89

The cleanest separation of the family: mean p(eot) 0.83 on finished turns against 0.30 on mid-turn pauses at the moment LiveKit asks (tiny 0.81 / 0.42), so 0.5 is the natural threshold and 0.3 trades one more cut-off in 300 turns for a 97 % fast path. Per call in the pipeline's single CPU thread it took ≈200 ms on a heavily loaded box, inside LiveKit's 1 s prediction budget every time.

Offline, at fixed cut points relative to the start of each pause (AUC, same 300 turns, every pause):

cut relative to pause start−0.4 s−0.2 s0.0 s+0.2 s+0.6 s
smart-turn-v30.600.620.630.650.69
tiny variant (int8)0.720.780.800.810.97
base variant (int8)0.770.820.850.870.98
this model (int8)0.770.830.860.880.99

Score smoothness along a pause is in line with the family (local std 0.034 over 200 ms, threshold flips 1.0 % per 20 ms step; smart-turn-v3 0.124 / 9.8 %).

Under LiveKit's [eot-bench](https://github.com/livekit/eot-bench) harness (100 ms causal grid over every pause ≥ 0.1 s, threshold × action_delay × timeout policy sweep; same adapter for all audio models, scored 0.2 s into each pause for the scalar metrics):

setmodelcutoff @ 300 ms budgetcutoff @ 600 mslatency @ 5 % cutofflatency @ 10 % cutoffAUC
telephony test, 1 000 turns, English (510 eot / 260 hold spans)this model47.3 %25.0 %1 722 ms1 261 ms0.84
tiny variant50.8 %30.0 %2 039 ms1 529 ms0.80
smart-turn-v369.6 %35.4 %2 269 ms1 756 ms0.66
VAD baseline77.3 %41.9 %2 020 ms1 610 ms
telephony test, 1 000 turns, Malay (485 / 169)this model49.1 %25.4 %1 903 ms1 494 ms0.86
tiny variant55.0 %32.5 %2 019 ms1 423 ms0.81
smart-turn-v378.1 %39.1 %2 635 ms2 116 ms0.63
VAD baseline79.3 %46.2 %2 540 ms2 060 ms
telephony, the 300 benchmark turns, English (188 / 105)this model45.7 %21.9 %1 530 ms1 151 ms0.83
tiny variant58.1 %27.6 %1 636 ms1 198 ms0.77
smart-turn-v374.3 %30.5 %1 649 ms1 164 ms0.64
VAD baseline77.1 %31.4 %1 800 ms1 510 ms
telephony, the 300 benchmark turns, Malay (112 / 35)this model48.6 %25.7 %1 761 ms1 238 ms0.85
tiny variant57.1 %31.4 %1 843 ms1 482 ms0.78
smart-turn-v368.6 %34.3 %2 357 ms1 603 ms0.63
VAD baseline80.0 %34.3 %2 410 ms1 830 ms

Where the tiny model only ties the VAD timer on latency at a 5 % cutoff budget, this one is ahead of it on every operating point of every subset, and ahead of the tiny model everywhere except latency at 10 % on the Malay 1 000-turn set. The harness asks within the first 100–300 ms of every pause, before an audio model has silence evidence; the extra encoder capacity buys the most exactly there (AUC 0.85 vs 0.80 at the pause start). In the LiveKit pipeline, which asks after the VAD's 0.4–0.55 s of silence, the two are one cut-off turn apart.

Files

filewhat
onnx/model.int8.onnxMatMul-only dynamic int8, 95 MB; max abs Δp vs PyTorch 0.10, mean 0.02
onnx/model.fp32.onnxfp32 export, 350 MB; max abs Δp vs PyTorch 1e-6
onnx/export_report.jsonsizes, parity vs PyTorch, latency at export time
encoder/fine-tuned Whisper-small encoder, HF format (config.json, model.safetensors, bf16, 169 MB)
eot_head.pt{"state_dict": LayerNorm→Linear(768,256)→GELU→Linear(256,1), "pooling": "last5"}
eot_window.json / preprocessor_config.jsonthe input contract: 8 s window, 80 mel bins, 16 kHz, no mel normalisation, mean of the last 5 encoder frames
training_summary.jsonbest step, validation AUC history

Input: input_features [batch, 80, 800] float32 — Whisper log-mel of the last 8 s of audio, left-padded with zeros when shorter, do_normalize=False. Output: probability [batch, 1], already through the sigmoid.

Usage

Identical to the tiny model — substitute the repo id. In short (ONNX, no torch):

python
import numpy as np, onnxruntime as ort
from huggingface_hub import hf_hub_download
from transformers import WhisperFeatureExtractor

REPO, SR, WINDOW = "Scicom-intl/semantic-vad-eot-whisper-small", 16000, 8 * 16000
opts = ort.SessionOptions(); opts.intra_op_num_threads = 1
sess = ort.InferenceSession(hf_hub_download(REPO, "onnx/model.int8.onnx"), opts, providers=["CPUExecutionProvider"])
fe = WhisperFeatureExtractor(feature_size=80, sampling_rate=SR, chunk_length=8)

def p_end_of_turn(pcm: np.ndarray) -> float:
    """pcm: float32 in [-1, 1] at 16 kHz, the caller's audio up to *now* (any length)."""
    pcm = np.asarray(pcm, dtype=np.float32)
    if pcm.size and np.abs(pcm).max() > 1.5:   # int16-scale samples -> unit float
        pcm = pcm / 32768.0
    pcm = pcm[-WINDOW:] if len(pcm) >= WINDOW else np.pad(pcm, (WINDOW - len(pcm), 0))
    feats = fe([pcm], sampling_rate=SR, return_tensors="np", padding="max_length", max_length=WINDOW,
               truncation=True, do_normalize=False)["input_features"].astype(np.float32)
    return float(sess.run(None, {"input_features": feats})[0].reshape(-1)[0])

For LiveKit Agents use it as the backend of STT-API's SemanticVAD through a three-line predict(pcm) -> p(eot) backend around the ONNX snippet, exactly as on the tiny model's card.

Use p ≥ 0.5 (measured operating point above; 0.3 for a higher fast-path share). The PyTorch loading snippet (a WhisperEncoder subclass that accepts the 8 s window + the 3-layer head) is on the tiny model's card and works unchanged with this repo id (d_model 768).

License

Apache-2.0 (the Whisper encoder it fine-tunes is Apache-2.0).