from __future__ import annotations
import argparse
import json
import sys
from dataclasses import asdict
from pathlib import Path
import numpy as np
import soundfile as sf
from fireredvad import FireRedStreamVad, FireRedStreamVadConfig
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--model-dir", required=True, help="Path to FireRedVAD/Stream-VAD")
parser.add_argument("--wav", required=True, help="Path to 16 kHz mono WAV")
parser.add_argument("--out", required=True, help="Output JSON path")
parser.add_argument("--smooth-window-size", type=int, default=5)
parser.add_argument("--speech-threshold", type=float, default=0.5)
parser.add_argument("--pad-start-frame", type=int, default=5)
parser.add_argument("--min-speech-frame", type=int, default=8)
parser.add_argument("--max-speech-frame", type=int, default=2000)
parser.add_argument("--min-silence-frame", type=int, default=20)
args = parser.parse_args()
cfg = FireRedStreamVadConfig(
use_gpu=False,
smooth_window_size=args.smooth_window_size,
speech_threshold=args.speech_threshold,
pad_start_frame=args.pad_start_frame,
min_speech_frame=args.min_speech_frame,
max_speech_frame=args.max_speech_frame,
min_silence_frame=args.min_silence_frame,
)
vad = FireRedStreamVad.from_pretrained(args.model_dir, cfg)
orig_process = vad.postprocessor.process_one_frame
def process_one_frame_unrounded(raw_prob: float):
result = orig_process(raw_prob)
result._unrounded_raw_prob = float(raw_prob)
return result
vad.postprocessor.process_one_frame = process_one_frame_unrounded
wav, sr = sf.read(args.wav, dtype="int16")
if sr != 16000:
print(f"error: expected 16 kHz, got {sr}", file=sys.stderr)
return 1
if wav.ndim != 1:
print(f"error: expected mono, got shape {wav.shape}", file=sys.stderr)
return 1
results = vad.detect_chunk(wav)
from collections import deque
smooth_size = max(1, args.smooth_window_size)
smooth_window: deque[float] = deque()
smooth_sum: float = 0.0
def compute_smoothed(raw: float) -> float:
nonlocal smooth_sum
if smooth_size <= 1:
return raw
smooth_window.append(raw)
smooth_sum += raw
if len(smooth_window) > smooth_size:
smooth_sum -= smooth_window.popleft()
return smooth_sum / len(smooth_window)
frames = []
for r in results:
frame_index = int(r.frame_idx) - 1 speech_start_frame = (
int(r.speech_start_frame) - 1 if r.speech_start_frame > 0 else None
)
speech_end_frame = (
int(r.speech_end_frame) - 1 if r.speech_end_frame > 0 else None
)
raw_prob_unrounded = float(getattr(r, "_unrounded_raw_prob", r.raw_prob))
smoothed_unrounded = compute_smoothed(raw_prob_unrounded)
frames.append(
{
"frame_index": frame_index,
"raw_prob": raw_prob_unrounded,
"smoothed_prob": smoothed_unrounded,
"is_speech": bool(r.is_speech),
"is_speech_start": bool(r.is_speech_start),
"is_speech_end": bool(r.is_speech_end),
"speech_start_frame": speech_start_frame,
"speech_end_frame": speech_end_frame,
}
)
output = {
"wav_path": str(Path(args.wav).resolve()),
"sample_rate": sr,
"n_samples": int(wav.shape[0]),
"duration_s": round(wav.shape[0] / sr, 3),
"n_frames": len(frames),
"config": {
"smooth_window_size": args.smooth_window_size,
"speech_threshold": args.speech_threshold,
"pad_start_frame": args.pad_start_frame,
"min_speech_frame": args.min_speech_frame,
"max_speech_frame": args.max_speech_frame,
"min_silence_frame": args.min_silence_frame,
},
"frames": frames,
}
with open(args.out, "w") as f:
json.dump(output, f, indent=None, separators=(",", ":"))
print(
f"python: {args.wav} -> {args.out} "
f"({len(frames)} frames, "
f"{sum(1 for f in frames if f['is_speech_start'])} starts, "
f"{sum(1 for f in frames if f['is_speech_end'])} ends)"
)
return 0
if __name__ == "__main__":
raise SystemExit(main())