前一篇写了 silero-vad · 一个 2 MB 的神经 VAD。这一篇是另一个开源 VAD ------ 阿里达摩院 FunASR 里的 fsmn-vad · 参数更小(0.4M)· 离线整段输入模式 下本文三条 case 上耗时约为 silero 的 40-45%(约 2.3× 速度)· 但判决口径不太一样。同一段音频、同一套后处理、silero 和 fsmn 输出可能完全相反 ------ 这篇就讲这个"完全相反"从哪来。
一、fsmn-vad 是什么
- 代码仓库 https://github.com/modelscope/FunASR · MIT license · 阿里达摩院
- 模型权重 ModelScope
iic/speech_fsmn_vad_zh-cn-16k-common-pytorch·iic是达摩院院内代号 · license 以模型页为准 - 参数量 0.4M(ModelScope 官方 tag 口径)· 本地下的
model.pt约 1.72 MB · 比 silero-vad (TorchScript 2.3 MB) 更小 - 架构 FSMN(Feedforward Sequential Memory Network)· 记忆块 + 无循环依赖 · 逐 chunk 前向、GPU/CPU 并行度都比 LSTM 高
- 采样率 16 kHz 单声道输入
- 接口 一次跑整段 · 输出 speech 段的
[start_ms, end_ms]列表(本文示例走整段输入 · FunASR 也支持流式/online 用法 · 本文不展开) - 多语种 官方 tag
zh/en· 训练语料原文 "industrial corpora"(未公开具体小时数)
技术脉络:Shiliang Zhang 等 2015 年在 iFlytek 提出 FSMN 架构(https://arxiv.org/abs/1512.08301)· 2018 年到阿里做出 Deep-FSMN 加跨层 skip connection(https://arxiv.org/abs/1803.05030)· 2023 年达摩院把 fsmn-vad 作为 FunASR toolkit 的组件正式开源(https://arxiv.org/abs/2305.11013 · INTERSPEECH 2023)。
不做什么:跟 silero 一样 · 不做说话人区分 · 不识别文字内容 · 不做语义级 utterance 判决。它做的是 speech/non-speech 分段------把音频切成"这段是有效语音、那段不是"的时间边界。
二、三行 API 上手
pip install funasr
Python 侧最短闭环(调用示意 · 未实跑):
from funasr import AutoModel
model = AutoModel(model="fsmn-vad") # 首次运行自动从 ModelScope 拉权重
res = model.generate(input="audio.wav")
# [{'key': 'audio', 'value': [[start_ms, end_ms], ...]}]
注意跟 silero 的差异:
- 模型按 16 kHz 单声道工作 ·
model.generate(input=wav_path)走文件路径时 FunASR loader 可能会自动读入并预处理 · 生产侧仍建议显式转 16 kHz 后再传;silero 支持 8/16 kHz 双分支 - 返回是段级
(start_ms, end_ms)列表 · silero 返回逐 chunk 的P(speech)或时间戳、决策粒度不同 - 没有 silero 式
threshold概率阈值暴露 · FunASR 里 VAD 还有max_end_silence_time/speech_noise_thres等参数可调(走vad_kwargs)· 但不等价于直接调 P(speech) - 依赖较重 ·
pip install funasr会拉 30+ 个包(含 kaldiio / torchaudio / hydra 等)· silero 依赖非常轻
三、架构 · memory block 无递归
FSMN 论文(arXiv:1512.08301)Figure 1 是最好的架构参考:

(a) 整体结构:input → 若干 hidden layer + 每层挂一个 memory block(红色 ˜h_t^ℓ)· memory 加权后回注下一层。(b) memory block 内部:hidden 序列 h_t 经过 z^-1 延迟线(tapped-delay)· 每个抽头 × 系数 a_i · 求和输出 ˜h_t ------ 本质是一个 FIR filter 。
核心机制:
时间步 t 的 memory 输出 ˜h_t = a_0 · h(t) + a_1 · h(t-1) + a_2 · h(t-2) + ... + a_N · h(t-N)
← 通用 FSMN 也可对称加"未来 M 步" ·
但在线/流式 VAD 一般把右上下文限制得很短或取消 →
- 每个时间步 · 每层 hidden 表示
h_t会看过去 N 步(可选加未来 M 步)的 h · 加权求和作为记忆信号 · 塞进下一层 - 没有递归依赖 · chunk 内所有时间步可以并行 forward · LSTM 的 state 传递被消除
- 类似 TCN(temporal convolutional network)· 但早了 TCN 3 年
Deep-FSMN 的改进 (2018 年阿里版 · arXiv:1803.05030):memory block 之间加跨层 skip connection(下图红色路径)· 让梯度能贯穿更深的 FSMN 堆叠:

Deep-FSMN 架构 · memory block ˜p 之间的红色路径 = skip connection · 支持 20+ 层堆叠。
实测速度 (本文三条 case · 单条 CPU 单线程 · 离线整段输入模式 ):fsmn 耗时约为 silero 的 40-45% (约 2.3× 速度)· 跟架构预期一致 ------ 无递归 = 更好并行;更大样本可保守估计 1.5-2×。流式模式下两家速度对比会翻转 · 见 §七。
四、复用 silero-vad 那篇的无语音后处理
跟 silero-vad 那篇讲过一套后处理 · 把 speech 段反推为 no-speech · 合并短间隔 · 过滤 ≥ 2 s。这一篇直接复用 、只把内部的 VAD 换成 fsmn(get_speech_timestamps 换成 model.generate):
import soundfile as sf
from funasr import AutoModel
SR = 16000
MERGE_GAP_S = 0.1
MIN_NO_SPEECH_DUR = 2.0
model = AutoModel(model="fsmn-vad")
def detect_no_speech_fsmn(wav_path: str):
info = sf.info(wav_path)
audio_end = info.frames / info.samplerate # 只用于知道总时长
out = model.generate(input=wav_path) # FunASR loader 读文件 · 生产侧建议显式转 16k
speech = [(s/1000, e/1000) for s, e in out[0]["value"]]
if not speech:
no_speech = [(0.0, audio_end)]
else:
no_speech, prev = [], 0.0
for s, e in speech:
if s > prev: no_speech.append((prev, s))
prev = max(prev, e)
if prev < audio_end: no_speech.append((prev, audio_end))
no_speech.sort()
merged = []
for s, e in no_speech:
if merged and s - merged[-1][1] <= MERGE_GAP_S:
merged[-1] = (merged[-1][0], max(merged[-1][1], e))
else:
merged.append((s, e))
return [(s, e) for s, e in merged if (e - s) >= MIN_NO_SPEECH_DUR]
跟 silero 版几乎一模一样 · 只是 get_speech_timestamps(...) 换成 model.generate(input=...) · threshold 等参数不再暴露。
五、三条 case · 看判决口径差异
三条真实 TTS 合成音频 · 分别代表两家判决一致 、fsmn 追回 silero 的漏检 、fsmn 也有短板 三种场景。
5.1 case1 · 两家都识别 badcase · silero 划分更精
音频 23.50 s · label 里两段无语音 10.70-12.90 和 14.40-23.50。
下方五行 track 自上而下为 label speech / silero-vad speech / fsmn-vad speech / silero 检出 no-speech / fsmn 检出 no-speech;空白 = 该模型无检出。波形上叠 silero speech(绿)+ 后处理命中段(silero 橙 / fsmn 红)作为速览。

silero 把前段连贯讲话拆成 5 小段 · 后处理合并短 gap · 精确命中 label 两段无语音。fsmn 更"连续判决"------ 把前段合成一大段 speech(0.72-11.10)· speech 边界向 label 静音 10.70-12.90 的两端各侵入 0.4 s · 反推出的 no-speech 只有约 1.48 s(11.10-12.58)· 被 ≥ 2 s 阈值过滤掉。最终只剩 14.68-23.50 一段。
两家最终结果:都识别出至少一段 ≥ 2 s 的无语音 · 都会把这条音频报为 badcase 。区别在精度:silero 划分更细、fsmn 判决更"整块"。
5.2 case2 · fsmn 追回 silero 的漏检
音频 8.61 s · label 全程有语音(低电平语音)。
波形显示整段电平极低(峰值 ±0.10 · RMS ≈ 0.01)· silero 前 3.40 s 没触发进 speech · 后处理反推假阳性;fsmn 直接一段 speech 吃到底、无假阳性。下方五行 track 布局同 case1:label speech / silero-vad speech / fsmn-vad speech / silero 检出 / fsmn 检出(空白 = 无检出)。

silero 用 threshold=0.3 也没把前段判成 speech · 反推出 (0.00, 3.40) 3.40 s 无语音假阳性。fsmn 从头到尾一段 speech · 反推空 · 命中 label。
可能原因(推测 · 非官方消融):可能与 fsmn 的训练语料、前端归一化或内部阈值有关;本文没有官方消融 · 不能判断 fsmn 为什么在这条样本上保住了低电平语音。
5.3 case3 · fsmn 也有短板 · 带底噪的静音段被判 speech
音频 18.61 s · label 前段 0.00-2.00 是合成前的头部静音(含合成器底噪 hum)· 主体是英文 TTS 语音。
波形前 2 s 有明显低电平底噪(峰值 ±0.02)· silero 判前段无语音 · 命中 label;fsmn 对头部底噪后的入声边界更早触发(1.67 s 起判 speech)· 反推 no-speech 只剩 1.67 s · 被 ≥ 2 s 过滤掉、漏检。下方五行 track 布局同 case1:label / silero-vad speech / fsmn-vad speech / silero 检出 / fsmn 检出(空白 = 无检出)。

在这条样本上 · silero 没把低电平 hum 当成 speech · 更接近 "人声存在" 口径而不是能量门限口径 · 于是 (0, 2) 被判 no-speech · 命中 。fsmn 从 1.67 s 起就判 speech · 反推留下 (0, 1.67) 一段 no-speech · 但只有 1.67 s · 被 MIN_NO_SPEECH_DUR=2.0 过滤掉 · 最终输出为空 · 漏检 · 边界问题 + 后处理阈值组合导致 · 不是模型完全没检出底噪。
同一个"敏感"特点 · case2 是优点、case3 是短板 :低电平语音 fsmn 能救、低电平底噪 fsmn 也当 speech。silero 反过来 ------ 对低电平底噪不敏感(好)· 对低电平语音也不敏感(坏)。
六、一句话 · 两家的判决口径
在本文这三条 case 的观察下 · 可以用一个粗糙的类比总结两家的倾向(教学类比 · 非严格分类学定义):
- silero-vad · 偏"人声检测器"(voice detector)· 在这三条样本上更像在保守保留 human-voice 段 · 底噪 / 音乐 / 环境声一般不算 speech · 边界最窄
- fsmn-vad · 偏"活动检测器"(activity detector)· speech 段更连续 · 对低电平信号(含底噪)更容易保留 · 边界中等
这不是靠一个 threshold 能对齐的差异 ------ silero 有 threshold / fsmn 有 max_end_silence_time 等参数可微调 · 但两家的判决边界不在同一维度上。仅凭这三条 case · 无法把差异归因到单一变量 ------ 训练数据、特征前端、归一化、内部阈值和后处理都会共同起作用 · 本文不做消融归因。
选型口诀(本文三条 case 的经验 · 落地前建议用自己业务的一批 case 验证):
- 判"有没有人在说话 "(TTS 数据清洗 · ASR 前置 · 空白 badcase 检测)→ silero-vad
- 判"通话线路 / 麦克风是否在录音 "(电话客服 · 智能音箱唤醒后的持续通话 · 车载语音 turn-taking)→ fsmn-vad
- 判"电平低段"(音频后期归一化 · 拼接检测)→ 两家都不合适 · 用能量法
七、流式与并发
两家都原生支持流式 · API 有别 · 本文用 case1 音频(23.5 s)实测流式模式(下面只比较流式 API · 不代表离线吞吐):
silero-vad · VADIterator(32 ms chunks · 每 chunk 512 samples @ 16 kHz · 核心调用片段 · 省略读 wav / 分块函数):
from silero_vad import load_silero_vad, VADIterator
model = load_silero_vad()
vad_iter = VADIterator(model, threshold=0.3, sampling_rate=16000,
min_silence_duration_ms=300, speech_pad_ms=0)
for chunk in stream_chunks(wav, chunk_size=512):
r = vad_iter(torch.from_numpy(chunk), return_seconds=True)
if r: print(r) # {'start': 1.0} / {'end': 4.4} 逐事件
vad_iter.reset_states()
fsmn-vad · cache-based streaming(200 ms chunks · 官方 demo 用法 · 核心调用片段):
from funasr import AutoModel
model = AutoModel(model="fsmn-vad")
stride = int(0.2 * 16000) # 200 ms · 3200 samples
cache = {}
total = (len(wav) + stride - 1) // stride
for i in range(total):
chunk = wav[i*stride : (i+1)*stride]
res = model.generate(input=chunk, cache=cache,
is_final=(i == total - 1), chunk_size=200)
if res[0]["value"]:
print(res[0]["value"]) # [[720, -1]] / [[-1, 11100]]
输出格式 :[[start_ms, end_ms]] 是完整 speech 段;-1 是事件哨兵 ------[[720, -1]] 表示 speech 起点检测到但结束还没来 · [[-1, 11100]] 表示前面某段的结束 · 调用侧需自己把 start / end 事件配对成完整段。
实测速度(case1 · 23.5 s · CPU 单线程 · 流式模式):
| chunk size | 每 chunk 平均处理时间 | 单位音频 RTF | |
|---|---|---|---|
| silero VADIterator | 32 ms | 0.154 ms | 0.00481(约 208× 实时) |
| fsmn streaming | 200 ms | 1.211 ms | 0.00606(约 165× 实时) |
流式模式下 silero 单位耗时反而更低------每 chunk 32 ms 单次前向轻;fsmn 200 ms chunk 单次前向重但摊薄同长度音频后仍略慢。这跟离线模式下 fsmn 快 2.3× 相反------离线批处理 fsmn 的并行优势明显 · 流式逐 chunk 就不明显了。
输出一致性:两家流式跟离线输出高度对齐 · 差异在 100 ms 内(silero 5 段 vs 5 段 · 边界 ±0.1 s;fsmn 2 段 vs 2 段 · 完全一致)。
并发建议 : - 流式跨请求 cache 必须按请求隔离 (silero 的 _states / fsmn 的 cache dict)· 混用会串音污染 - 同一个模型实例是否可跨线程并发安全复用 · FunASR / silero 都没给出官方保证 · 需要按具体封装压测验证;保守做法是 model pool / 单 worker 串行 / batch 组织 - 实际吞吐还要看 batch 组织 / FunASR 封装 / 硬件 · 上线前按目标 QPS 压测
八、写在最后
silero 和 fsmn 都是优秀的开源轻量 VAD · 参数几 MB 内 · CPU 单线程 100× 以上实时 · 生产可用。但它们不是可互换的 ------训练语料、特征前端、归一化、内部阈值和后处理共同影响判决边界差异 · 会直接反映到后处理的输出上、可能让"是否 badcase"的最终结论完全相反。上线前 · 用少量代表性 case(本文这样的三条)走一遍 · 看输出对不对齐业务口径 · 比拍脑袋选一个再回头调参靠谱。