fsmn-vad:来自 FunASR 的开源 VAD 实践指南

前一篇写了 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.9014.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(本文这样的三条)走一遍 · 看输出对不对齐业务口径 · 比拍脑袋选一个再回头调参靠谱。

相关推荐
IvanCodes2 小时前
GitHub 本周热门开源项目:Agent Infra与端侧 AI|8.17–8.23
开源·github
fthux3 小时前
装修怕增项、合同看不懂?我做了 RenoPit,帮普通业主提前发现装修坑
人工智能·ai·开源·github·open source·renopit
河南三丰环保设备有限公司3 小时前
哪个机构教炼油设备技术教得好
开源
六年码农4 小时前
2026最新开源 屏译ScreenTranslator 0.4.4 开源 全屏实时翻译教程
运维·人工智能·flutter·开源
小马9265 小时前
智能体时代的两块基石:DeepSeek Harness 开源与“认知基础设施“数据库
数据库·人工智能·开源·agent
元岳数字人小元5 小时前
极简运维体系,数字人一体机适配中小场景升级
运维·人工智能·开源·人机交互·交互·源代码管理
Niuguangshuo6 小时前
服务端降噪三家:FRCRN, FullSubNet+, MP-SENet
开源·降噪
dong_junshuai6 小时前
每天一个开源项目#74 OpenViking:3万星Agent上下文数据库
开源·github·agent
小弥儿6 小时前
GitHub今日热榜 | 2026-08-20:Agent 上下文数据库接棒
数据库·学习·开源·github