English | 简体中文
原生 Rust 实现的工业级 VAD / AED / Stream-VAD,权重全部编译进二进制,单文件可执行,零运行时依赖。也可作为 Rust crate 嵌入。底层 API 与官方 Python FireRedVad / FireRedAed / FireRedStreamVad 1:1 对齐(方法名、参数、JSON schema 全部一致),输出在 event.wav 上与官方逐帧匹配。
| 模式 | 输入 | 输出 | 用途 |
|---|---|---|---|
| VAD | 任意 wav | [{start,end}] 语音段 |
切长音频给 ASR 喂 |
| AED | 任意 wav | {speech:[..], singing:[..], music:[..]} |
内容分类、过滤非语音 |
| Stream-VAD | wav 或 stdin 实时 | {dur, timestamps, [frames]} |
实时字幕、直播 |
Intel Core Ultra 7 265K(20 核)上,7835 秒音频推理耗时 3.96 秒,RTFx ≈ 1978×。相比原版 Python(PyTorch)快 10–100 倍,且无需 GPU。
测试方法:5 次连续推理取中位,脚本见
tmp/bench_rtfx.ps1。
与上游同源(FireRedTeam/FireRedVAD),测试集 FLEURS-VAD-102:
| Metric | FireRedVAD | Silero | TEN | FunASR | WebRTC |
|---|---|---|---|---|---|
| AUC-ROC ↑ | 99.60 | 97.99 | 97.81 | - | - |
| F1 ↑ | 97.57 | 95.95 | 95.19 | 90.91 | 52.30 |
| 误报率 ↓ | 2.69 | 9.41 | 15.47 | 44.03 | 2.83 |
- 单文件可执行 — 三个模型(VAD + AED + Stream-VAD)权重全部编译进二进制,复制到任何 Windows x64 机器双击就跑
- CPU 加速 —
gemm运行期探测 AVX-512/AVX2/AVX/SSE/NEON,自动选最快微内核 - 并行推理 —
rayon切块并行,多核线性扩展 - 格式宽容 — 多通道自动混音,任意采样率重采样到 16kHz,PCM16/float32 都吃
- 动态 VAD — 可选的动态静音阈值,避免把完整句子切碎
- API 1:1 对齐 Python — 改用 Rust 不用改业务逻辑;
from_pretrained/detect/set_mode/results_to_timestamps全在
下载二进制(从 Releases):
.\fireredvad.exe vad D:\path\to\audio.wav
.\fireredvad.exe aed D:\path\to\audio.wav
.\fireredvad.exe stream-vad D:\path\to\audio.wav从源码构建:
cargo build --release
# → target/release/fireredvad.exe三个子命令,互不影响,按需选用:
输出 [start, end] 语音段。
.\fireredvad.exe vad audio.wav{
"dur": 22.016,
"timestamps": [[0.09, 18.48], [18.49, 22.016]],
"wav_path": "audio.wav",
"sample_rate_in": 16000,
"sample_rate_model": 16000
}输出 speech / singing / music 三类时间戳。
.\fireredvad.exe aed audio.wav{
"dur": 22.016,
"event2timestamps": {
"speech": [[0.40, 3.56], [3.66, 9.08], [9.27, 9.77], [10.78, 21.76]],
"singing": [[1.79, 19.96], [19.97, 22.016]],
"music": [[0.09, 12.32], [12.33, 22.016]]
},
"event2ratio": {"speech": 0.848, "singing": 0.905, "music": 0.991},
"wav_path": "audio.wav"
}输出同 vad,但内部按帧串行处理,模拟"逐帧推理"的实时场景。
.\fireredvad.exe stream-vad audio.wavstdin 持续吃 16kHz int16 LE PCM 数据(任意 chunk 大小),stdout 每帧立刻吐一行 JSON(NDJSON),最后一行是汇总:
# 从麦克风管道送入
mic --format=s16le --rate=16000 --channels=1 | fireredvad.exe stream-vad --stdinNDJSON 格式(每帧一行 + 最后一行 _summary: true):
{"frame_idx":1,"is_speech":true,"raw_prob":0.86,"smoothed_prob":0.81,"is_speech_start":true,...}
{"frame_idx":2,"is_speech":true,"raw_prob":0.91,"smoothed_prob":0.86,...}
...
{"_summary":true,"dur":22.016,"timestamps":[[0.09,20.13],[20.14,21.99]],"n_frames":2200}100Hz 帧率(每 10ms 一帧),与 stream-vad <wav> 离线模拟逐帧一致。
[dependencies]
fireredvad = "0.1"底层 API 与官方 Python FireRedVad / FireRedAed / FireRedStreamVad 1:1 对齐:
use fireredvad::{Vad, VadConfig, Aed, AedConfig, StreamVad, StreamVadConfig};
// ---- VAD: binary speech/silence ----
let vad = Vad::from_pretrained()?; // Python: FireRedVad.from_pretrained(model_dir, config)
let out = vad.detect_wav("audio.wav", &VadConfig::default())?;
println!("{} segments", out.timestamps.len());
// ---- AED: speech / singing / music ----
let aed = Aed::from_pretrained()?; // Python: FireRedAed.from_pretrained(model_dir, config)
let out = aed.detect_wav("audio.wav")?;
for (ev, segs) in &out.event2timestamps {
println!("{ev}: {} segs", segs.len());
}
// ---- Stream-VAD: real-time streaming ----
let mut sv = StreamVad::from_pretrained()?;
sv.set_mode(0); // Python: FireRedStreamVad.set_mode(0..3)
// Per-frame (one frame = 400 samples at 16 kHz = 25 ms):
let r = sv.detect_frame(&[0.0f32; 400])?; // Python: detect_frame(audio_frame)
if r.is_speech_start { /* speech begins at r.speech_start_frame */ }
// Per-chunk (variable length; processed at 100 Hz = 10 ms shift):
let r = sv.detect_chunk(&pcm_chunk)?; // Python: detect_chunk(audio_chunk)
// Whole file (offline streaming simulation):
let (frames, out) = sv.detect_full("audio.wav")?; // Python: detect_full(audio)
// Convert per-frame results to (start, end) in seconds:
let timestamps = StreamVad::results_to_timestamps(&frames);Python (fireredvad 原版) |
Rust crate |
|---|---|
FireRedVad.from_pretrained(model_dir, config) |
Vad::from_pretrained() / Vad::new() |
vad.detect(audio) |
vad.detect_wav(path) / vad.detect_pcm(samples) |
FireRedAed.from_pretrained(model_dir, config) |
Aed::from_pretrained() / Aed::new() / Aed::with_config(cfg) |
aed.detect(audio) |
aed.detect_wav(path) / aed.detect_pcm(samples) |
FireRedStreamVad.from_pretrained(model_dir, config) |
StreamVad::from_pretrained() / StreamVad::new() / StreamVad::with_config(cfg) |
sv.reset() |
sv.reset() |
sv.set_mode(0..3) |
sv.set_mode(mode: usize) |
sv.detect_frame(audio_frame) |
sv.detect_frame(samples: &[f32]) |
sv.detect_chunk(audio_chunk) |
sv.detect_chunk(samples: &[f32]) |
sv.detect_full(audio) |
sv.detect_full(path) (returns (Vec<FrameResult>, Output)) |
FireRedStreamVad.results_to_timestamps(results) |
StreamVad::results_to_timestamps(results) |
from_pretrained当前不接受 path 参数——权重全部编译进二进制,无外部模型依赖。如果未来需要外部加载,会加一个from_path(model_dir)不破坏 API。
| 参数 | 默认值 | 说明 |
|---|---|---|
<wav> |
(必填) | 输入 WAV 路径 |
--speech-threshold |
0.4 |
语音概率阈值 |
--smooth-window-size |
5 |
滑动窗口平滑(帧) |
--min-speech-frame |
20 |
确认语音的最少连续帧 |
--max-speech-frame |
2000 |
单段最大长度,超过强制切分 |
--min-silence-frame |
20 |
确认静音的最少连续帧 |
--merge-silence-frame |
0 |
短于该帧数的静音缝合并 |
--extend-speech-frame |
0 |
语音段前后延展帧数 |
--chunk-max-frame |
30000 |
并行分块的最大帧数 |
--dynamic-vad |
off |
启用动态静音阈值 |
| 参数 | 默认值 | 说明 |
|---|---|---|
<wav> |
(必填) | 输入 WAV 路径 |
--speech-threshold |
0.4 |
语音阈值 |
--singing-threshold |
0.5 |
唱歌阈值 |
--music-threshold |
0.5 |
音乐阈值 |
--smooth-window-size |
5 |
同上 |
--min-event-frame |
20 |
任一事件确认的最少连续帧 |
--max-event-frame |
2000 |
同上 |
--min-silence-frame |
20 |
同上 |
--merge-silence-frame |
0 |
同上 |
--extend-speech-frame |
0 |
同上 |
--chunk-max-frame |
30000 |
同上 |
| 参数 | 默认值 | 说明 |
|---|---|---|
<wav> |
(可选) | 整文件模拟(默认) |
--stdin |
off |
真·实时流式模式(覆盖 <wav>) |
--pad-trailing |
off |
--stdin 模式下,把尾部不足一帧的部分补零再推理 |
--speech-threshold |
0.3 |
语音概率阈值 |
--smooth-window-size |
5 |
滑动窗口 |
--pad-start-frame |
5 |
语音开始处向前回看帧数 |
--min-speech-frame |
8 |
确认语音最少帧 |
--max-speech-frame |
2000 |
单段最大长度 |
--min-silence-frame |
20 |
确认静音最少帧 |
| mode | 名称 | threshold | min_speech | min_silence |
|---|---|---|---|---|
| 0 | VERY_PERMISSIVE | 0.3 | 8 | 20 |
| 1 | PERMISSIVE | 0.5 | 10 | 15 |
| 2 | AGGRESSIVE | 0.7 | 15 | 10 |
| 3 | VERY_AGGRESSIVE | 0.9 | 20 | 5 |
vad 子命令的 --dynamic-vad 让切分阈值随当前段的累积时长收紧——段越长,越短的停顿就足以切分(适用于不切碎长句):
| 当前段已说 | 需静音超过 | 才切 |
|---|---|---|
| ≤ 5 s | 2000 ms | 2 秒 |
| ≤ 10 s | 1500 ms | 1.5 秒 |
| ≤ 15 s | 1000 ms | 1 秒 |
| ≤ 30 s | 800 ms | |
| ≤ 45 s | 400 ms | |
| > 45 s | 100 ms | 一停就切 |
.\fireredvad.exe vad audio.wav --dynamic-vad
- 与
--min-silence-frame互斥,开启后固定阈值被忽略--max-speech-frame仍作为兜底硬切- 表源自 FunASR
fsmn_vad_streaming/dynamic_vad.py
VAD 是宽松的"speech"(会吞掉 singing),AED 的 speech 类更严。如果要"喂 ASR 的纯净人话",推荐:
// 方案 A:AED 直接替代 VAD
let aed_out = aed.detect_wav("audio.wav")?;
let speech_segments = aed_out.event2timestamps["speech"];
// 方案 B:VAD 粗切 + AED 过滤掉 singing 重叠段(见 examples/pipeline_vad_aed.rs)WAV → 混音 mono → 重采样 16kHz → 80维 fbank → CMVN → DFSMN 编码器 → sigmoid → 状态机后处理 → 段
| 模块 | 作用 |
|---|---|
audio.rs |
解码 PCM16/float32 WAV,混音,重采样 |
fbank.rs |
80维 log-Mel 滤波器组 + Povey 窗 |
cmvn.rs |
CMVN 均值/方差归一化 |
model.rs |
Deep FSMN 编码器(gemm 矩阵乘),含流式 cache |
postprocess.rs |
平滑、阈值、状态机、切段;含流式变体 |
aed.rs |
AED 高层封装(3 路并行后处理) |
stream_vad.rs |
Stream-VAD 高层封装(流式 cache) |
cli.rs |
三个子命令 + stdin 模式 |
model/ 下三个模型权重源自 FireRedTeam/FireRedASR2S 的 pretrained_models/FireRedVAD/{VAD,AED,Stream-VAD}/,Apache-2.0 协议再分发。模型通过 include_bytes! 在编译期嵌入二进制,不依赖任何外部文件。