tardigrade-doc commited on
Commit
085466e
·
verified ·
1 Parent(s): de7ff47

Upload folder using huggingface_hub

Browse files
Files changed (5) hide show
  1. README.md +16 -3
  2. cmvn.bin +3 -0
  3. cmvn.json +1 -0
  4. infer_onnx.py +441 -0
  5. model.onnx +3 -0
README.md CHANGED
@@ -1,3 +1,16 @@
1
- ---
2
- license: apache-2.0
3
- ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ tags:
4
+ - voice-activity-detection
5
+ - onnx
6
+ - streaming
7
+ - firered-vad
8
+ - dfsmn
9
+ language:
10
+ - en
11
+ - zh
12
+ base_model: FireRedTeam/FireRedVAD
13
+ pipeline_tag: voice-activity-detection
14
+ ---
15
+
16
+ # FireRedVAD VAD (ONNX), do not contain vad_streaming & aed
cmvn.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b020eb6a57b01993c7aa032fbb0e33d257359ef1bdcb4b66e3dc360f11b42d4e
3
+ size 644
cmvn.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"dim": 80, "means": [10.422951698303223, 10.86209774017334, 11.764544486999512, 12.490164756774902, 13.259830474853516, 13.895943641662598, 14.364940643310547, 14.593948364257812, 14.749723434448242, 14.668314933776855, 14.730796813964844, 14.775052070617676, 14.989051818847656, 15.17800521850586, 15.253520011901855, 15.32863712310791, 15.33401870727539, 15.288641929626465, 15.427661895751953, 15.24626636505127, 15.092574119567871, 15.290421485900879, 15.075750350952148, 15.186773300170898, 15.08867359161377, 15.170797348022461, 15.070178031921387, 15.150794982910156, 15.108532905578613, 15.115345001220703, 15.141280174255371, 15.131832122802734, 15.145195960998535, 15.191518783569336, 15.235478401184082, 15.30636978149414, 15.373021125793457, 15.416394233703613, 15.459856986999512, 15.391432762145996, 15.463576316833496, 15.39966106414795, 15.462907791137695, 15.441629409790039, 15.484969139099121, 15.552401542663574, 15.638092041015625, 15.705489158630371, 15.767008781433105, 15.855123519897461, 15.867269515991211, 15.8915376663208, 15.923145294189453, 15.97838306427002, 16.014801025390625, 16.048675537109375, 16.082029342651367, 16.096799850463867, 16.09373664855957, 16.072479248046875, 16.075510025024414, 16.02227020263672, 15.976761817932129, 15.89786434173584, 15.81274127960205, 15.71120548248291, 15.604198455810547, 15.553519248962402, 15.510252952575684, 15.460023880004883, 15.415684700012207, 15.376028060913086, 15.328349113464355, 15.295371055603027, 15.185470581054688, 15.017045021057129, 14.90507984161377, 14.623806953430176, 14.138093948364258, 13.313870429992676], "inverse_std_variances": [0.24949808418750763, 0.23563234508037567, 0.23145152628421783, 0.23322339355945587, 0.23182660341262817, 0.2285335659980774, 0.22434869408607483, 0.21898920834064484, 0.21832437813282013, 0.2208259254693985, 0.22296735644340515, 0.2228841632604599, 0.22234810888767242, 0.22100642323493958, 0.21994201838970184, 0.22005443274974823, 0.2207009196281433, 0.22150810062885284, 0.22236667573451996, 0.22305291891098022, 0.22335341572761536, 0.22438906133174896, 0.22547701001167297, 0.22690075635910034, 0.22823023796081543, 0.22931471467018127, 0.2304672747850418, 0.23083552718162537, 0.23143382370471954, 0.23220659792423248, 0.23257988691329956, 0.23361970484256744, 0.23437240719795227, 0.23508252203464508, 0.23578079044818878, 0.23589199781417847, 0.2360209822654724, 0.23663799464702606, 0.23749876022338867, 0.2379845231771469, 0.2389937788248062, 0.23974815011024475, 0.24030835926532745, 0.24097692966461182, 0.24143248796463013, 0.24135465919971466, 0.24079938232898712, 0.24047406017780304, 0.23995524644851685, 0.23952287435531616, 0.2394808828830719, 0.2393650859594345, 0.2392933964729309, 0.23902198672294617, 0.23857873678207397, 0.23814702033996582, 0.23804621398448944, 0.23824194073677063, 0.238600954413414, 0.23915407061576843, 0.23922540247440338, 0.23938308656215668, 0.23973360657691956, 0.2396056205034256, 0.2402850240468979, 0.24061813950538635, 0.2406792938709259, 0.24096201360225677, 0.24043606221675873, 0.2402152717113495, 0.23972514271736145, 0.23871998488903046, 0.2374413162469864, 0.23619508743286133, 0.23337280750274658, 0.22680233418941498, 0.22577503323554993, 0.22503846883773804, 0.22631137073040009, 0.2289949357509613]}
infer_onnx.py ADDED
@@ -0,0 +1,441 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ FireRedVAD ONNX inference — no PyTorch / kaldi_native_fbank required.
4
+
5
+ Dependencies: numpy, scipy, soundfile, onnxruntime
6
+
7
+ Usage:
8
+
9
+ python infer_onnx.py assets/hello_zh.wav --model_dir /path/to/FireRedVAD_onnx
10
+ python infer_onnx.py assets/hello_zh.wav
11
+ python infer_onnx.py assets/hello_zh.wav --model_dir /path/to/FireRedVAD_onnx
12
+ python infer_onnx.py assets/hello_en.wav --speech_threshold 0.4 --min_speech_frame 20
13
+ """
14
+
15
+ import argparse
16
+ import json
17
+ import math
18
+ import os
19
+ from collections import deque
20
+
21
+ import numpy as np
22
+ import soundfile as sf
23
+ import onnxruntime as ort
24
+
25
+
26
+ # ---------------------------------------------------------------------------
27
+ # Constants (matches fireredvad/core/constants.py)
28
+ # ---------------------------------------------------------------------------
29
+ SAMPLE_RATE = 16000
30
+ FRAME_LENGTH_MS = 25
31
+ FRAME_SHIFT_MS = 10
32
+ FRAME_LENGTH_S = 0.025
33
+ FRAME_SHIFT_S = 0.010
34
+
35
+
36
+ # ---------------------------------------------------------------------------
37
+ # Kaldi-compatible Fbank (replaces kaldi_native_fbank dependency)
38
+ # ---------------------------------------------------------------------------
39
+
40
+ def _mel_to_hz(mel):
41
+ return 700.0 * (np.exp(mel / 1127.0) - 1.0)
42
+
43
+ def _hz_to_mel(hz):
44
+ return 1127.0 * np.log(1.0 + hz / 700.0)
45
+
46
+ def _build_mel_filterbank(n_fft: int, n_mels: int = 80,
47
+ f_min: float = 0.0, f_max: float = 8000.0,
48
+ sample_rate: int = 16000) -> np.ndarray:
49
+ """
50
+ Build Kaldi-style mel filterbank matrix.
51
+ Returns shape (n_mels, n_fft // 2 + 1) — real spectrum bins.
52
+ Kaldi uses triangular filters defined on the mel scale, NOT on the FFT bin scale.
53
+ """
54
+ n_freqs = n_fft // 2 + 1
55
+ freq_bins = np.linspace(0, sample_rate / 2, n_freqs) # Hz for each FFT bin
56
+
57
+ mel_min = _hz_to_mel(f_min)
58
+ mel_max = _hz_to_mel(f_max)
59
+ # n_mels + 2 points: left edge, n_mels centers, right edge
60
+ mel_points = np.linspace(mel_min, mel_max, n_mels + 2)
61
+ hz_points = _mel_to_hz(mel_points)
62
+
63
+ # For each mel band, compute the triangular weight for each FFT bin
64
+ filters = np.zeros((n_mels, n_freqs), dtype=np.float32)
65
+ for m in range(1, n_mels + 1):
66
+ left = hz_points[m - 1]
67
+ center = hz_points[m]
68
+ right = hz_points[m + 1]
69
+ for k, f in enumerate(freq_bins):
70
+ if left <= f <= center:
71
+ filters[m - 1, k] = (f - left) / (center - left)
72
+ elif center < f <= right:
73
+ filters[m - 1, k] = (right - f) / (right - center)
74
+ return filters
75
+
76
+
77
+ def _extract_kaldi_fbank(wav_int16: np.ndarray,
78
+ sample_rate: int = 16000,
79
+ num_mel_bins: int = 80,
80
+ frame_length_ms: float = 25.0,
81
+ frame_shift_ms: float = 10.0) -> np.ndarray:
82
+ """
83
+ Compute log Mel filterbank features identical to kaldi_native_fbank with:
84
+ samp_freq=16000, frame_length_ms=25, frame_shift_ms=10,
85
+ dither=0, snip_edges=True, num_mel_bins=80
86
+
87
+ Input : int16 PCM array (1-D)
88
+ Output: (T, 80) float32 array — log fbank energies
89
+ """
90
+ assert wav_int16.dtype == np.int16 and wav_int16.ndim == 1
91
+
92
+ frame_len = int(round(sample_rate * frame_length_ms / 1000)) # 400
93
+ frame_shift = int(round(sample_rate * frame_shift_ms / 1000)) # 160
94
+ n_fft = 1 << (frame_len - 1).bit_length() # next power of 2 >= 400 → 512
95
+
96
+ # Hann window (Kaldi uses the same)
97
+ window = np.hanning(frame_len).astype(np.float32)
98
+
99
+ # Convert to float, Kaldi: raw int16 → float32 (NOT divided by 32768 at fbank stage)
100
+ # Actually Kaldi processes the waveform as-is (int16 values as float).
101
+ wav_f = wav_int16.astype(np.float32)
102
+
103
+ # Pre-emphasis (Kaldi default: 0.97)
104
+ wav_preemph = np.append(wav_f[0], wav_f[1:] - 0.97 * wav_f[:-1])
105
+
106
+ # Framing (snip_edges=True: only frames that fit completely)
107
+ n_frames = 1 + (len(wav_preemph) - frame_len) // frame_shift
108
+ if n_frames <= 0:
109
+ return np.zeros((0, num_mel_bins), dtype=np.float32)
110
+
111
+ frames = np.stack([
112
+ wav_preemph[i * frame_shift: i * frame_shift + frame_len] * window
113
+ for i in range(n_frames)
114
+ ]) # (T, frame_len)
115
+
116
+ # FFT → power spectrum
117
+ spec = np.fft.rfft(frames, n=n_fft) # (T, n_fft//2+1) complex
118
+ power = (spec.real ** 2 + spec.imag ** 2).astype(np.float32) # (T, n_fft//2+1)
119
+
120
+ # Mel filterbank
121
+ mel_fb = _build_mel_filterbank(n_fft, num_mel_bins,
122
+ f_min=0.0, f_max=sample_rate / 2,
123
+ sample_rate=sample_rate) # (80, n_fft//2+1)
124
+ mel_energy = power @ mel_fb.T # (T, 80)
125
+
126
+ # Log compression — floor at 1.0 (Kaldi: log_energy_floor = FLT_MIN, effectively 0)
127
+ mel_energy = np.maximum(mel_energy, 1.0)
128
+ log_mel = np.log(mel_energy).astype(np.float32)
129
+
130
+ return log_mel # (T, 80)
131
+
132
+
133
+ # ---------------------------------------------------------------------------
134
+ # CMVN (matches fireredvad/core/audio_feat.py CMVN.__call__)
135
+ # ---------------------------------------------------------------------------
136
+
137
+ class CMVN:
138
+ def __init__(self, cmvn_json_path: str):
139
+ with open(cmvn_json_path) as f:
140
+ d = json.load(f)
141
+ self.means = np.array(d["means"], dtype=np.float32)
142
+ self.inv_std = np.array(d["inverse_std_variances"], dtype=np.float32)
143
+
144
+ def __call__(self, fbank: np.ndarray) -> np.ndarray:
145
+ return (fbank - self.means) * self.inv_std
146
+
147
+
148
+ # ---------------------------------------------------------------------------
149
+ # VAD postprocessor (pure Python, mirrors vad_postprocessor.py)
150
+ # ---------------------------------------------------------------------------
151
+
152
+ class VadPostprocessor:
153
+ def __init__(self, smooth_window_size=5, prob_threshold=0.4,
154
+ min_speech_frame=20, max_speech_frame=2000,
155
+ min_silence_frame=20, merge_silence_frame=0,
156
+ extend_speech_frame=0):
157
+ self.smooth_window_size = max(1, smooth_window_size)
158
+ self.prob_threshold = prob_threshold
159
+ self.min_speech_frame = min_speech_frame
160
+ self.max_speech_frame = max_speech_frame
161
+ self.min_silence_frame = min_silence_frame
162
+ self.merge_silence_frame = merge_silence_frame
163
+ self.extend_speech_frame = extend_speech_frame
164
+
165
+ def process(self, raw_probs):
166
+ if not raw_probs:
167
+ return []
168
+ smoothed = self._smooth(raw_probs)
169
+ binary = (np.asarray(smoothed) >= self.prob_threshold).astype(int).tolist()
170
+ decisions = self._state_machine(binary)
171
+ decisions = self._fix_start(decisions)
172
+ decisions = self._merge_silence(decisions)
173
+ decisions = self._extend_speech(decisions)
174
+ decisions = self._split_long(decisions, raw_probs)
175
+ return decisions
176
+
177
+ def decisions_to_segments(self, decisions, wav_dur=None):
178
+ segments = []
179
+ speech_start = None
180
+ for t, d in enumerate(decisions):
181
+ if d == 1 and speech_start is None:
182
+ speech_start = t
183
+ elif d == 0 and speech_start is not None:
184
+ segments.append((speech_start * FRAME_SHIFT_S, t * FRAME_SHIFT_S))
185
+ speech_start = None
186
+ if speech_start is not None:
187
+ end = len(decisions) * FRAME_SHIFT_S + FRAME_LENGTH_S
188
+ if wav_dur is not None:
189
+ end = min(end, wav_dur)
190
+ segments.append((speech_start * FRAME_SHIFT_S, end))
191
+ return [(round(s, 3), round(e, 3)) for s, e in segments]
192
+
193
+ def _smooth(self, probs):
194
+ if self.smooth_window_size <= 1:
195
+ return probs
196
+ probs_np = np.array(probs)
197
+ kernel = np.ones(self.smooth_window_size) / self.smooth_window_size
198
+ smoothed = np.convolve(probs_np, kernel, mode='full')[:len(probs)]
199
+ for i in range(min(self.smooth_window_size - 1, len(probs))):
200
+ smoothed[i] = np.mean(probs_np[:i + 1])
201
+ return smoothed
202
+
203
+ def _state_machine(self, binary):
204
+ SILENCE, POSSIBLE_SPEECH, SPEECH, POSSIBLE_SILENCE = 0, 1, 2, 3
205
+ decisions = [0] * len(binary)
206
+ state = SILENCE
207
+ speech_start = silence_start = -1
208
+ for t, is_speech in enumerate(binary):
209
+ if state == SILENCE:
210
+ if is_speech:
211
+ state = POSSIBLE_SPEECH; speech_start = t
212
+ elif state == POSSIBLE_SPEECH:
213
+ if is_speech:
214
+ if t - speech_start >= self.min_speech_frame:
215
+ state = SPEECH
216
+ decisions[speech_start:t] = [1] * (t - speech_start)
217
+ else:
218
+ state = SILENCE; speech_start = -1
219
+ elif state == SPEECH:
220
+ if not is_speech:
221
+ state = POSSIBLE_SILENCE; silence_start = t
222
+ elif state == POSSIBLE_SILENCE:
223
+ if not is_speech:
224
+ if t - silence_start >= self.min_silence_frame:
225
+ state = SILENCE; speech_start = -1
226
+ else:
227
+ state = SPEECH; silence_start = -1
228
+ decisions[t] = 1 if state in (SPEECH, POSSIBLE_SILENCE) else 0
229
+ return decisions
230
+
231
+ def _fix_start(self, decisions):
232
+ new = decisions.copy()
233
+ for t, d in enumerate(decisions):
234
+ if t > 0 and decisions[t - 1] == 0 and d == 1:
235
+ start = max(0, t - self.smooth_window_size)
236
+ new[start:t] = [1] * (t - start)
237
+ return new
238
+
239
+ def _merge_silence(self, decisions):
240
+ if self.merge_silence_frame <= 0:
241
+ return decisions
242
+ new = decisions.copy()
243
+ silence_start = None
244
+ for t, d in enumerate(decisions):
245
+ if t > 0 and decisions[t - 1] == 1 and d == 0 and silence_start is None:
246
+ silence_start = t
247
+ elif t > 0 and decisions[t - 1] == 0 and d == 1 and silence_start is not None:
248
+ if t - silence_start < self.merge_silence_frame:
249
+ new[silence_start:t] = [1] * (t - silence_start)
250
+ silence_start = None
251
+ return new
252
+
253
+ def _extend_speech(self, decisions):
254
+ if self.extend_speech_frame <= 0:
255
+ return decisions
256
+ d = np.array(decisions)
257
+ k = np.ones(2 * self.extend_speech_frame + 1)
258
+ return (np.convolve(d, k, mode='same') > 0).astype(int).tolist()
259
+
260
+ def _split_long(self, decisions, probs):
261
+ new = decisions.copy()
262
+ segments = self.decisions_to_segments(decisions)
263
+ for s_s, e_s in segments:
264
+ sf_ = int(s_s / FRAME_SHIFT_S)
265
+ ef_ = int(e_s / FRAME_SHIFT_S)
266
+ if ef_ - sf_ > self.max_speech_frame:
267
+ seg_probs = probs[sf_:ef_]
268
+ for split in self._find_splits(seg_probs):
269
+ new[sf_ + split] = 0
270
+ return new
271
+
272
+ def _find_splits(self, probs):
273
+ splits, L, start = [], len(probs), 0
274
+ while start < L:
275
+ if (L - start) <= self.max_speech_frame:
276
+ break
277
+ ws = int(start + self.max_speech_frame / 2)
278
+ we = int(start + self.max_speech_frame)
279
+ splits.append(ws + int(np.argmin(probs[ws:we])))
280
+ start = splits[-1] + 1
281
+ return splits
282
+
283
+
284
+ # ---------------------------------------------------------------------------
285
+ # Main inference class
286
+ # ---------------------------------------------------------------------------
287
+
288
+ class FireRedVadOnnx:
289
+ """
290
+ End-to-end FireRedVAD inference using ONNX Runtime.
291
+ No PyTorch or kaldi_native_fbank required.
292
+ """
293
+
294
+ def __init__(self, model_dir: str,
295
+ smooth_window_size: int = 5,
296
+ speech_threshold: float = 0.4,
297
+ min_speech_frame: int = 20,
298
+ max_speech_frame: int = 2000,
299
+ min_silence_frame: int = 20,
300
+ merge_silence_frame: int = 0,
301
+ extend_speech_frame: int = 0,
302
+ chunk_max_frame: int = 30000,
303
+ use_coreml: bool = False):
304
+
305
+ # CMVN params
306
+ # Support both naming conventions: cmvn.json and cmvn_params.json
307
+ for name in ("cmvn.json", "cmvn_params.json"):
308
+ p = os.path.join(model_dir, name)
309
+ if os.path.exists(p):
310
+ self.cmvn = CMVN(p)
311
+ break
312
+ else:
313
+ raise FileNotFoundError(f"No cmvn JSON found in {model_dir}")
314
+
315
+ # ONNX session
316
+ model_path = os.path.join(model_dir, "model.onnx")
317
+ providers = ["CPUExecutionProvider"]
318
+ if use_coreml:
319
+ providers = ["CoreMLExecutionProvider"] + providers
320
+ opts = ort.SessionOptions()
321
+ opts.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
322
+ self.session = ort.InferenceSession(model_path, sess_options=opts,
323
+ providers=providers)
324
+
325
+ # Fbank config (fixed to match training)
326
+ self.num_mel_bins = 80
327
+ self.chunk_max_frame = chunk_max_frame
328
+
329
+ # Postprocessor
330
+ self.postprocessor = VadPostprocessor(
331
+ smooth_window_size=smooth_window_size,
332
+ prob_threshold=speech_threshold,
333
+ min_speech_frame=min_speech_frame,
334
+ max_speech_frame=max_speech_frame,
335
+ min_silence_frame=min_silence_frame,
336
+ merge_silence_frame=merge_silence_frame,
337
+ extend_speech_frame=extend_speech_frame)
338
+
339
+ def _load_audio(self, audio_path: str):
340
+ wav, sr = sf.read(audio_path, dtype="int16")
341
+ assert sr == SAMPLE_RATE, f"Expected 16kHz, got {sr}Hz. Convert with ffmpeg first."
342
+ assert wav.ndim == 1, "Expected mono audio."
343
+ return wav
344
+
345
+ def _extract_features(self, wav_int16: np.ndarray) -> np.ndarray:
346
+ """Returns (T, 80) CMVN-normalized log-fbank."""
347
+ fbank = _extract_kaldi_fbank(wav_int16, SAMPLE_RATE, self.num_mel_bins)
348
+ return self.cmvn(fbank)
349
+
350
+ def _run_model(self, feat: np.ndarray) -> np.ndarray:
351
+ """
352
+ feat: (T, 80)
353
+ Returns probs: (T,) float32
354
+ """
355
+ T = feat.shape[0]
356
+ all_probs = []
357
+
358
+ for chunk_start in range(0, T, self.chunk_max_frame):
359
+ chunk = feat[chunk_start: chunk_start + self.chunk_max_frame]
360
+ inp = chunk[np.newaxis, :, :].astype(np.float32) # (1, t, 80)
361
+
362
+ outputs = self.session.run(["output"], {"input": inp})
363
+ probs_chunk = outputs[0][0, :, 0] # (t,)
364
+ all_probs.append(probs_chunk)
365
+
366
+ return np.concatenate(all_probs) # (T,)
367
+
368
+ def detect(self, audio_path: str):
369
+ """
370
+ Run VAD on a 16kHz mono WAV file.
371
+
372
+ Returns:
373
+ result : dict with keys 'dur', 'timestamps', 'wav_path'
374
+ probs : (T,) float32 array of raw per-frame speech probabilities
375
+ """
376
+ wav = self._load_audio(audio_path)
377
+ dur = len(wav) / SAMPLE_RATE
378
+
379
+ feat = self._extract_features(wav) # (T, 80)
380
+ probs = self._run_model(feat) # (T,)
381
+
382
+ decisions = self.postprocessor.process(probs.tolist())
383
+ segments = self.postprocessor.decisions_to_segments(decisions, dur)
384
+
385
+ result = {
386
+ "dur": round(dur, 3),
387
+ "timestamps": segments,
388
+ "wav_path": audio_path,
389
+ }
390
+ return result, probs
391
+
392
+
393
+ # ---------------------------------------------------------------------------
394
+ # CLI
395
+ # ---------------------------------------------------------------------------
396
+
397
+ def main():
398
+ parser = argparse.ArgumentParser(description="FireRedVAD ONNX inference")
399
+ parser.add_argument("wav_path", help="Path to 16kHz mono WAV file")
400
+ parser.add_argument("--model_dir",
401
+ default="/path/to/FireRedVAD_onnx",
402
+ help="Directory containing model.onnx and cmvn.json")
403
+ parser.add_argument("--smooth_window_size", type=int, default=5)
404
+ parser.add_argument("--speech_threshold", type=float, default=0.4)
405
+ parser.add_argument("--min_speech_frame", type=int, default=20)
406
+ parser.add_argument("--max_speech_frame", type=int, default=2000)
407
+ parser.add_argument("--min_silence_frame", type=int, default=20)
408
+ parser.add_argument("--merge_silence_frame", type=int, default=0)
409
+ parser.add_argument("--extend_speech_frame", type=int, default=0)
410
+ parser.add_argument("--use_coreml", action="store_true",
411
+ help="Use CoreML execution provider (Apple Silicon)")
412
+ args = parser.parse_args()
413
+
414
+ vad = FireRedVadOnnx(
415
+ model_dir=args.model_dir,
416
+ smooth_window_size=args.smooth_window_size,
417
+ speech_threshold=args.speech_threshold,
418
+ min_speech_frame=args.min_speech_frame,
419
+ max_speech_frame=args.max_speech_frame,
420
+ min_silence_frame=args.min_silence_frame,
421
+ merge_silence_frame=args.merge_silence_frame,
422
+ extend_speech_frame=args.extend_speech_frame,
423
+ use_coreml=args.use_coreml,
424
+ )
425
+
426
+ import time
427
+ start = time.time()
428
+ result, probs = vad.detect(args.wav_path)
429
+
430
+ du = time.time() - start
431
+ print(f"vad : {du}s")
432
+ print(f"Duration : {result['dur']:.3f}s")
433
+ print(f"Segments : {len(result['timestamps'])}")
434
+ for i, (s, e) in enumerate(result["timestamps"]):
435
+ print(f" [{i+1:3d}] {s:.3f}s -- {e:.3f}s ({e-s:.3f}s)")
436
+
437
+
438
+ if __name__ == "__main__":
439
+ main()
440
+
441
+ # python infer_onnx.py assets/hello_zh.wav --model_dir /path/to/FireRedVAD_onnx
model.onnx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:517e9c6207618407da41fc274b1e3f09e8cde531db42f039a52be93b29a49151
3
+ size 2461278