huseinzolkepliscicom commited on
Commit
bbdc3c1
·
verified ·
1 Parent(s): 70298ba

Upload PyTorch encoder + head and ONNX (fp32, int8) exports

Browse files
README.md ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ base_model: openai/whisper-base
4
+ datasets:
5
+ - Scicom-intl/semantic-vad-eot-emgs
6
+ language:
7
+ - ms
8
+ - en
9
+ pipeline_tag: audio-classification
10
+ library_name: transformers
11
+ tags:
12
+ - end-of-turn-detection
13
+ - turn-detection
14
+ - semantic-vad
15
+ - endpointing
16
+ - voice-agent
17
+ - livekit
18
+ - onnx
19
+ - whisper
20
+ - telephony
21
+ ---
22
+
23
+ # Semantic VAD — Whisper-base end-of-turn detector (audio only)
24
+
25
+ The whisper-base sibling of
26
+ [`Scicom-intl/semantic-vad-eot-whisper-tiny`](https://huggingface.co/Scicom-intl/semantic-vad-eot-whisper-tiny):
27
+ same recipe, same input contract, same data, a 2.5× larger encoder. Given the last 8 seconds of a caller's
28
+ 16 kHz audio it returns `p(end of turn)` — finished speaking vs paused mid-sentence — with no transcript.
29
+
30
+ **20 M parameters · int8 ONNX 24 MB · roughly twice the compute of the tiny model** (int8 69 ms vs 34 ms per
31
+ prediction measured back to back on the same busy CPU; the tiny model runs ≈30 ms idle on one thread). It
32
+ ranks turns better than the tiny model (AUC 0.88 vs 0.84 in the pipeline, 0.85 / 0.87 / 0.98 vs
33
+ 0.80 / 0.81 / 0.97 offline at 0 / 0.2 / 0.6 s into a pause) — worth it when CPU is not the constraint or when
34
+ you want a stricter threshold; otherwise use the tiny model, which we recommend for production.
35
+
36
+ ## Results
37
+
38
+ **In a real LiveKit Agents 1.8 pipeline** (Silero VAD → turn detector → endpointing, no STT, 300 recorded
39
+ telephony turns, LiveKit defaults: VAD silence 0.55 s, `min_delay` 0.5 s, `max_delay` 3.0 s):
40
+
41
+ | turn detector | latency p50 / p90 | turns cut off | finished turns on the fast path | AUC (eot vs hold) |
42
+ |---|---:|---:|---:|---:|
43
+ | VAD only | 0.63 / 0.71 s | 14.3 % | – | – |
44
+ | smart-turn-v3, threshold 0.5 | 0.65 / 3.04 s | 10.0 % | 82 % | 0.74 |
45
+ | tiny variant, threshold 0.5 | 0.64 / 0.74 s | 10.0 % | 95 % | 0.84 |
46
+ | **this model, threshold 0.3** | **0.64 / 0.74 s** | **9.7 %** | **96 %** | **0.88** |
47
+ | this model, threshold 0.5 | 0.65 / 2.93 s | 9.3 % | 90 % | 0.88 |
48
+
49
+ The int8 export's scores sit a little lower than the tiny model's (recall at 0.5 is 0.87 vs 0.92 at the pause
50
+ start), so **0.3 is this model's equivalent of the tiny model's 0.5**; at 0.5 it is stricter — one more cut-off
51
+ avoided, but 10 % of finished turns wait for `max_delay`. One cut-off turn in 300 separates it from the tiny
52
+ model at matched fast-path share, which is within noise.
53
+
54
+ **Offline, at fixed cut points relative to the start of each pause** (AUC, same 300 turns, every pause):
55
+
56
+ | cut relative to pause start | −0.4 s | −0.2 s | 0.0 s | +0.2 s | +0.6 s |
57
+ |---|---:|---:|---:|---:|---:|
58
+ | smart-turn-v3 | 0.60 | 0.62 | 0.63 | 0.65 | 0.69 |
59
+ | tiny variant (int8) | 0.72 | 0.78 | 0.80 | 0.81 | 0.97 |
60
+ | **this model (int8)** | **0.77** | **0.82** | **0.85** | **0.87** | **0.98** |
61
+
62
+ Score smoothness along a pause matches the tiny model (local std 0.044 over 200 ms, threshold flips 1.7 %
63
+ per 20 ms step; smart-turn-v3 0.124 / 9.8 %).
64
+
65
+ ## Files
66
+
67
+ | file | what |
68
+ |---|---|
69
+ | `onnx/model.int8.onnx` | MatMul-only dynamic int8, 24 MB |
70
+ | `onnx/model.fp32.onnx` | fp32 export, 81 MB; max abs Δp vs PyTorch 1e-6 |
71
+ | `onnx/export_report.json` | sizes, parity vs PyTorch, latency at export time |
72
+ | `encoder/` | fine-tuned Whisper-base encoder, HF format (`config.json`, `model.safetensors`, bf16) |
73
+ | `eot_head.pt` | `{"state_dict": LayerNorm→Linear(512,256)→GELU→Linear(256,1), "pooling": "last5"}` |
74
+ | `eot_window.json` / `preprocessor_config.json` | the input contract: 8 s window, 80 mel bins, 16 kHz, no mel normalisation, mean of the last 5 encoder frames |
75
+ | `training_summary.json` | best step, validation AUC history |
76
+
77
+ Input: `input_features` `[batch, 80, 800]` float32 — Whisper log-mel of the **last 8 s of audio, left-padded
78
+ with zeros when shorter**, `do_normalize=False`. Output: `probability` `[batch, 1]`, already through the
79
+ sigmoid.
80
+
81
+ ## Usage
82
+
83
+ Identical to the tiny model — substitute the repo id. In short (ONNX, no torch):
84
+
85
+ ```python
86
+ import numpy as np, onnxruntime as ort
87
+ from huggingface_hub import hf_hub_download
88
+ from transformers import WhisperFeatureExtractor
89
+
90
+ REPO, SR, WINDOW = "Scicom-intl/semantic-vad-eot-whisper-base", 16000, 8 * 16000
91
+ opts = ort.SessionOptions(); opts.intra_op_num_threads = 1
92
+ sess = ort.InferenceSession(hf_hub_download(REPO, "onnx/model.int8.onnx"), opts, providers=["CPUExecutionProvider"])
93
+ fe = WhisperFeatureExtractor(feature_size=80, sampling_rate=SR, chunk_length=8)
94
+
95
+ def p_end_of_turn(pcm: np.ndarray) -> float:
96
+ """pcm: float32 in [-1, 1] at 16 kHz, the caller's audio up to *now* (any length)."""
97
+ pcm = np.asarray(pcm, dtype=np.float32)
98
+ if pcm.size and np.abs(pcm).max() > 1.5: # int16-scale samples -> unit float
99
+ pcm = pcm / 32768.0
100
+ pcm = pcm[-WINDOW:] if len(pcm) >= WINDOW else np.pad(pcm, (WINDOW - len(pcm), 0))
101
+ feats = fe([pcm], sampling_rate=SR, return_tensors="np", padding="max_length", max_length=WINDOW,
102
+ truncation=True, do_normalize=False)["input_features"].astype(np.float32)
103
+ return float(sess.run(None, {"input_features": feats})[0].reshape(-1)[0])
104
+ ```
105
+
106
+ Use `p ≥ 0.3` as "the turn is over" for the operating point in the table above. The PyTorch loading
107
+ snippet (a `WhisperEncoder` subclass that accepts the 8 s window + the 3-layer head) is on the tiny model's
108
+ card and works unchanged with this repo id (`d_model` 512).
109
+
110
+ ## Training
111
+
112
+ Same as the tiny model: `Scicom-intl/semantic-vad-eot-emgs` (private call-centre telephony, Malay/English,
113
+ customer + agent channels), all train + validation files, early stopping (patience 3) on a fixed 4 000-cut
114
+ sample of the test split — the pipeline benchmark's 300 turns come from that split too, so the numbers are
115
+ in-distribution. Six cut offsets per pause drawn uniformly in [−0.4, +1.2] s (never a fixed grid — it gets
116
+ memorised), 8 s left-padded window, mean of the last 5 encoder frames → `EoTHead`. `openai/whisper-base`
117
+ encoder (6 layers, d 512) fully fine-tuned in bf16, batch 128, AdamW lr 5e-5 constant after warm-up.
118
+ Early-stopped at step 32 000 (validation AUC 0.877 on uniform cuts; tiny: 0.859 at step 8 000). ONNX via
119
+ `torch.onnx.export` at the fixed 800-frame input, MatMul-only dynamic int8.
120
+
121
+ ## Limitations
122
+
123
+ As for the tiny model: telephony Malay/English only (matches, does not beat, the VAD baseline on Malay read
124
+ speech); expects the raw phone channel (noise cancellation in front of it hurt); feed unit-scale float audio;
125
+ query it after a short VAD silence, not on every frame; a 300-turn sample stands behind the pipeline numbers.
126
+
127
+ ## License
128
+
129
+ Apache-2.0 (the Whisper encoder it fine-tunes is Apache-2.0). The training data is not released.
encoder/config.json ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "activation_dropout": 0.0,
3
+ "activation_function": "gelu",
4
+ "apply_spec_augment": false,
5
+ "architectures": [
6
+ "VariableLengthWhisperEncoder"
7
+ ],
8
+ "attention_dropout": 0.0,
9
+ "begin_suppress_tokens": [
10
+ 220,
11
+ 50257
12
+ ],
13
+ "bos_token_id": 50257,
14
+ "classifier_proj_size": 256,
15
+ "d_model": 512,
16
+ "decoder_attention_heads": 8,
17
+ "decoder_ffn_dim": 2048,
18
+ "decoder_layerdrop": 0.0,
19
+ "decoder_layers": 6,
20
+ "decoder_start_token_id": 50258,
21
+ "dropout": 0.0,
22
+ "dtype": "bfloat16",
23
+ "encoder_attention_heads": 8,
24
+ "encoder_ffn_dim": 2048,
25
+ "encoder_layerdrop": 0.0,
26
+ "encoder_layers": 6,
27
+ "eos_token_id": 50257,
28
+ "forced_decoder_ids": [
29
+ [
30
+ 1,
31
+ 50259
32
+ ],
33
+ [
34
+ 2,
35
+ 50359
36
+ ],
37
+ [
38
+ 3,
39
+ 50363
40
+ ]
41
+ ],
42
+ "init_std": 0.02,
43
+ "is_encoder_decoder": true,
44
+ "mask_feature_length": 10,
45
+ "mask_feature_min_masks": 0,
46
+ "mask_feature_prob": 0.0,
47
+ "mask_time_length": 10,
48
+ "mask_time_min_masks": 2,
49
+ "mask_time_prob": 0.05,
50
+ "max_source_positions": 1500,
51
+ "max_target_positions": 448,
52
+ "median_filter_width": 7,
53
+ "model_type": "whisper",
54
+ "num_mel_bins": 80,
55
+ "pad_token_id": 50257,
56
+ "scale_embedding": false,
57
+ "suppress_tokens": [
58
+ 1,
59
+ 2,
60
+ 7,
61
+ 8,
62
+ 9,
63
+ 10,
64
+ 14,
65
+ 25,
66
+ 26,
67
+ 27,
68
+ 28,
69
+ 29,
70
+ 31,
71
+ 58,
72
+ 59,
73
+ 60,
74
+ 61,
75
+ 62,
76
+ 63,
77
+ 90,
78
+ 91,
79
+ 92,
80
+ 93,
81
+ 359,
82
+ 503,
83
+ 522,
84
+ 542,
85
+ 873,
86
+ 893,
87
+ 902,
88
+ 918,
89
+ 922,
90
+ 931,
91
+ 1350,
92
+ 1853,
93
+ 1982,
94
+ 2460,
95
+ 2627,
96
+ 3246,
97
+ 3253,
98
+ 3268,
99
+ 3536,
100
+ 3846,
101
+ 3961,
102
+ 4183,
103
+ 4667,
104
+ 6585,
105
+ 6647,
106
+ 7273,
107
+ 9061,
108
+ 9383,
109
+ 10428,
110
+ 10929,
111
+ 11938,
112
+ 12033,
113
+ 12331,
114
+ 12562,
115
+ 13793,
116
+ 14157,
117
+ 14635,
118
+ 15265,
119
+ 15618,
120
+ 16553,
121
+ 16604,
122
+ 18362,
123
+ 18956,
124
+ 20075,
125
+ 21675,
126
+ 22520,
127
+ 26130,
128
+ 26161,
129
+ 26435,
130
+ 28279,
131
+ 29464,
132
+ 31650,
133
+ 32302,
134
+ 32470,
135
+ 36865,
136
+ 42863,
137
+ 47425,
138
+ 49870,
139
+ 50254,
140
+ 50258,
141
+ 50358,
142
+ 50359,
143
+ 50360,
144
+ 50361,
145
+ 50362
146
+ ],
147
+ "tie_word_embeddings": true,
148
+ "transformers_version": "5.12.0",
149
+ "use_cache": true,
150
+ "use_weighted_layer_sum": false,
151
+ "vocab_size": 51865
152
+ }
encoder/model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2bc40ad0cae34e26ebd291598a2c887c20e2240ae4a2563985c3372565843611
3
+ size 41190688
eot_head.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5dbe75d29b53b8f1a1025df5a6c587be457e1f8bf8f1aa64519b4ab2d959a38c
3
+ size 533601
eot_window.json ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ {
2
+ "window_seconds": 8.0,
3
+ "do_normalize": false,
4
+ "num_mel_bins": 80,
5
+ "sample_rate": 16000,
6
+ "pooling": "last5"
7
+ }
onnx/eot_window.json ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ {
2
+ "window_seconds": 8.0,
3
+ "do_normalize": false,
4
+ "num_mel_bins": 80,
5
+ "sample_rate": 16000,
6
+ "pooling": "last5"
7
+ }
onnx/export_report.json ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "window": {
3
+ "window_seconds": 8.0,
4
+ "do_normalize": false,
5
+ "num_mel_bins": 80,
6
+ "sample_rate": 16000,
7
+ "pooling": "last5"
8
+ },
9
+ "params": 20160001,
10
+ "fp32": {
11
+ "bytes": 80718310,
12
+ "max_abs_dp_vs_torch": 1.3113021850585938e-06,
13
+ "latency": {
14
+ "p50_ms": 163.6397559195757,
15
+ "p90_ms": 165.72624072432518,
16
+ "min_ms": 146.37318346649408
17
+ }
18
+ },
19
+ "int8": {
20
+ "bytes": 23776856,
21
+ "max_abs_dp_vs_torch": 0.14714109897613525,
22
+ "mean_abs_dp_vs_torch": 0.047445375472307205,
23
+ "latency": {
24
+ "p50_ms": 68.82935343310237,
25
+ "p90_ms": 69.49121318757534,
26
+ "min_ms": 57.50598106533289
27
+ }
28
+ }
29
+ }
onnx/model.fp32.onnx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ae327622a3d579a27aa2c51780c9349261920e12018b1642825ece1185876c7a
3
+ size 80718310
onnx/model.int8.onnx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a480ce7730609685f4481d95de374b932c3578aeef86f826acb4225c6dc2369d
3
+ size 23776856
preprocessor_config.json ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "chunk_length": 8,
3
+ "dither": 0.0,
4
+ "feature_extractor_type": "WhisperFeatureExtractor",
5
+ "feature_size": 80,
6
+ "hop_length": 160,
7
+ "n_fft": 400,
8
+ "n_samples": 128000,
9
+ "nb_max_frames": 800,
10
+ "padding_side": "right",
11
+ "padding_value": 0.0,
12
+ "return_attention_mask": false,
13
+ "sampling_rate": 16000
14
+ }
training_summary.json ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "best_step": 32000,
3
+ "best_eval_auc": 0.8770214562847013,
4
+ "steps_trained": 38000,
5
+ "eval_history": [
6
+ {
7
+ "step": 2000,
8
+ "eval_auc": 0.8451234196267309,
9
+ "eval_loss": 0.05243109539151192
10
+ },
11
+ {
12
+ "step": 4000,
13
+ "eval_auc": 0.8548836878720985,
14
+ "eval_loss": 0.05047003552317619
15
+ },
16
+ {
17
+ "step": 6000,
18
+ "eval_auc": 0.8605236972372734,
19
+ "eval_loss": 0.050362538546323776
20
+ },
21
+ {
22
+ "step": 8000,
23
+ "eval_auc": 0.864192420897719,
24
+ "eval_loss": 0.04977281764149666
25
+ },
26
+ {
27
+ "step": 10000,
28
+ "eval_auc": 0.867650010034116,
29
+ "eval_loss": 0.049336887896060944
30
+ },
31
+ {
32
+ "step": 12000,
33
+ "eval_auc": 0.8667155829821394,
34
+ "eval_loss": 0.048195865005254745
35
+ },
36
+ {
37
+ "step": 14000,
38
+ "eval_auc": 0.8678381497090106,
39
+ "eval_loss": 0.0470057912170887
40
+ },
41
+ {
42
+ "step": 16000,
43
+ "eval_auc": 0.8705327279416684,
44
+ "eval_loss": 0.047456044703722
45
+ },
46
+ {
47
+ "step": 18000,
48
+ "eval_auc": 0.8703926684059134,
49
+ "eval_loss": 0.0463123619556427
50
+ },
51
+ {
52
+ "step": 20000,
53
+ "eval_auc": 0.8699223192186769,
54
+ "eval_loss": 0.04807543754577637
55
+ },
56
+ {
57
+ "step": 22000,
58
+ "eval_auc": 0.8728593885878654,
59
+ "eval_loss": 0.046615827828645706
60
+ },
61
+ {
62
+ "step": 24000,
63
+ "eval_auc": 0.8696860994046425,
64
+ "eval_loss": 0.0480119027197361
65
+ },
66
+ {
67
+ "step": 26000,
68
+ "eval_auc": 0.8674555990367249,
69
+ "eval_loss": 0.04942657798528671
70
+ },
71
+ {
72
+ "step": 28000,
73
+ "eval_auc": 0.8743436015787008,
74
+ "eval_loss": 0.04626317694783211
75
+ },
76
+ {
77
+ "step": 30000,
78
+ "eval_auc": 0.8736662987490802,
79
+ "eval_loss": 0.046077266335487366
80
+ },
81
+ {
82
+ "step": 32000,
83
+ "eval_auc": 0.8770214562847013,
84
+ "eval_loss": 0.04583379626274109
85
+ },
86
+ {
87
+ "step": 34000,
88
+ "eval_auc": 0.8757483778179143,
89
+ "eval_loss": 0.04697026312351227
90
+ },
91
+ {
92
+ "step": 36000,
93
+ "eval_auc": 0.8766556291390728,
94
+ "eval_loss": 0.04517030343413353
95
+ },
96
+ {
97
+ "step": 38000,
98
+ "eval_auc": 0.8752487624590273,
99
+ "eval_loss": 0.04783298820257187
100
+ }
101
+ ],
102
+ "early_stopping_patience": 3
103
+ }