TTS 音频入库前常规检查:"这条合成里有没有一大段静音"。传统做法是短时能量 + 阈值------低音量语音直接漏、带噪音频满屏假阳性。silero-vad 神经模型替换掉能量法之后,鲁棒性拉起来了,模型本身还只有 2 MB · 在桌面级单线程 CPU 上跑到 350× 以上实时(本文两条样本的实测口径 · 含推理与后处理 · 不含模型加载)。
一、silero-vad 是什么
- 仓库 https://github.com/snakers4/silero-vad · MIT license · Silero Team
- 模型体积 TorchScript JIT 2.3 MB · ONNX 2.3 MB · half 精度 ONNX 1.3 MB
- 架构 4 层 SileroVadBlock(Conv1d + ReLU)+ LSTMCell decoder + Sigmoid 输出 P(speech) · 每 chunk 输出一个概率 · 依赖历史 state
- 采样率 16 kHz 和 8 kHz 双分支 · chunk 大小 16k 用 512 样本(32 ms)· 8k 用 256 样本
- 接口 一次跑整段音频 · 也可逐 chunk 流式喂 state · 不依赖未来 chunk · 实时使用等一个 32 ms chunk 的输入到齐即可
- 多语种 训练语料覆盖 6000+ 语言变体(官方 README 口径) · 跨语种退化小 · 是它对比 WebRTC VAD 的最大优势
不做什么:不做说话人区分 · 不做识别文字内容 · 不做端点检测(EPD · 判断一句话是否说完、能否触发对话轮次结束)· 判断口径是"这段时间里有没有 human voice 发声"。
二、三行 API 上手
pip install silero-vad
Python 侧最短闭环(调用示意 · 未实跑):
from silero_vad import load_silero_vad, read_audio, get_speech_timestamps
model = load_silero_vad() # TorchScript JIT 加载
wav = read_audio("audio.wav", sampling_rate=16000) # 16 kHz mono
segs = get_speech_timestamps(wav, model, sampling_rate=16000, return_seconds=True)
print(segs)
# [{'start': 1.0, 'end': 4.5}, {'start': 5.3, 'end': 6.2}, ...]
切 ONNX Runtime 只改一个 flag(其他 API 完全一致):
model = load_silero_vad(onnx=True) # 内部走 onnxruntime · CPU 推理
接入常见 pipeline :faster-whisper 内置 vad_filter=True 参数直接调它 · WhisperX 也用它做 pre-VAD · pyannote 常把它当前置轻量筛子。
三、TorchScript vs ONNX · CPU 单线程性能
单线程 CPU(torch.set_num_threads(1) + OMP_NUM_THREADS=1)· warmup 3 次 · 计时 5 次取均值 · 只计 get_speech_timestamps 与后处理 · 不计模型加载和音频读取。硬件口径:桌面级单线程 CPU · 换硬件需重跑。

读图 :横轴两条 case(23.50 s 长语音 · 8.61 s 短语音)· 纵轴单次跑完全长音频耗时。左蓝 TorchScript JIT · 右橙 ONNX Runtime · 数字上方标 ms + RTF。
- ONNX 在这两条样本上比 JIT 快约 20%
- 两条 case 都跑到 350× 以上实时(在桌面级单线程 CPU 口径下 · 换硬件需要重跑)
- 单条音频推理下 ONNX 提速主要来自算子融合和更精简的运行时开销 · 端侧交付 / 部署栈偏 ONNX Runtime 时更方便 · QPS 和内存收益按服务形态另压测确认
两个 runtime 在本次实测里出的 speech 段完全相同 · 差异只在耗时。TorchScript 走 pytorch 是好写好调 · ONNX 走 onnxruntime 是端侧部署的常见选择。
四、无语音检测 · silero-vad 之上的常用后处理
silero-vad 直接输出的是 speech 段。业务上关心的往往是反过来的问题:"TTS 合成音里有没有一大段无语音"------超过 2 s 的空白就当 badcase 打回。围绕这个目标 · 生产实践里常见的一套配置和后处理链路(本文两条 case 就是跑这段函数的实测结果):
推荐配置 + 后处理(生产实践中收敛下来的一组):
import torch, io
import librosa
from silero_vad import load_silero_vad, get_speech_timestamps
SR = 16000
VAD_THRESHOLD = 0.3
MIN_SPEECH_MS = 100
MIN_SILENCE_INTERNAL_MS = 300
MERGE_GAP_S = 0.1
MIN_NO_SPEECH_DUR = 2.0
model = load_silero_vad()
def detect_no_speech(wav_bytes: bytes):
y, sr = librosa.load(io.BytesIO(wav_bytes), sr=SR, mono=True)
audio_end = len(y) / sr
speech = get_speech_timestamps(
torch.from_numpy(y), model,
sampling_rate=sr,
threshold=VAD_THRESHOLD,
min_speech_duration_ms=MIN_SPEECH_MS,
min_silence_duration_ms=MIN_SILENCE_INTERNAL_MS,
return_seconds=True,
)
speech_segs = [(float(s["start"]), float(s["end"])) for s in speech]
if not speech_segs:
no_speech = [(0.0, audio_end)]
else:
no_speech, prev = [], 0.0
for s, e in speech_segs:
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]
实测输出(这段函数跑本文两条 case · 完整波形图在 §五):
- case1(含长静音的语音片段 · 23.50 s)→
[(10.70, 12.90), (14.40, 23.50)]· 与人工 label 两段无语音完全对齐 - case2(低电平全程语音 · 8.61 s)→
[(0.00, 3.40)]· 假阳性 · 详见 §5.2
几处配置的取向:
threshold=0.3· 比默认 0.5 低 · 对 TTS 合成里常见的轻声、气声、尾音更宽容 · 代价是背景音乐 / 底噪也可能被识成 speechmin_silence_duration_ms=300· silero-vad 内部参数 · 意思是一句话内部的呼吸停顿 ≤ 300 ms 不切段 · 避免把连贯句子切成多段短 speech 后反推出一堆假的短静音MERGE_GAP_S=0.1· 相邻无语音段之间只隔 100 ms 的一小段 speech · 直接合并 · 通常是 silero-vad 误检的短脉冲MIN_NO_SPEECH_DUR=2.0· 业务阈值:合成音里出现 ≥ 2 s 的空白才算异常 · 更短的都当作正常语气停顿
调参顺序 (出问题时按这个次序动):漏检语音 → 先看响度归一化 / 再降 threshold;碎片短静音多 → 加 min_silence_duration_ms 或 MERGE_GAP_S;长空白误报多 → 改 MIN_NO_SPEECH_DUR 或加豁免规则。
并发 :流式调用不要跨请求共享 model state(一个实例的 LSTM 状态会被别的线程污染)· 整段批处理可以用 queue.Queue 做 model pool 隔离多个实例。POOL_SIZE 的吞吐提升需按部署环境压测确认 · 不要预设一个固定倍数。
五、两个 case 复盘
两条真实 TTS 合成音频跑一遍上面的后处理链路 · 一条精确命中 · 一条误报。
5.1 case1 · 精确命中
音频 23.50 s · label 里两段真实静音 10.70-12.90 和 14.40-23.50 · 后处理输出的两段无语音跟 label 完全对齐。
图分四层:主图(波形 + silero-vad 绿色 speech + 最终无语音橙色)· label 条(人工标注 speech)· silero-vad 条 · 后处理输出条。三条 track 上下对齐即命中一致。

silero-vad 把前半段连贯说话拆成 5 小段 · min_silence_internal=300ms + merge_gap=100ms 把段间气口吞回来 · 短静音过滤后只剩两段长静音------正好压在 label 空档里。
5.2 case2 · 误报
音频 8.61 s · label 全程有语音 · 后处理却输出 (0.00, 3.40) 3.40 s 无语音。
同样四层结构 · 底部橙条压在 label 蓝条上 = 假阳性。

看波形前 3.40 s 明显有能量(幅度 ±0.05)· 但整段电平偏低 · silero-vad 用 threshold=0.3 也没把它判成 speech · 后处理链路无法挽回。
tradeoff :silero-vad 触发 speech 主要看 chunk 级 P(speech) >= threshold;已进入 speech 后 · 结束还受 neg_threshold(默认 threshold - 0.15)和 min_silence_duration_ms 控制。case2 前 3.40 s 的问题是始终没触发进入 speech ------ 对电平整体压低的语音(TTS 尾部渐弱 · 远场录音 · 咬字轻的感叹词)不敏感。生产上常见做法:
- 先做峰值 / RMS 响度归一化再判 ------本例前段电平低 · 把检测副本(不覆盖原始音频)归一化到 -3 dBFS 后再跑 silero-vad · 在这类样本上通常有效 · 具体是否命中需按批 case 验证
- 降 threshold 到 0.15/0.20 能救一部分 · 但会把底噪 / 前奏也判成 speech · 换成另一种假阳性
- 报出来的 badcase 走二次人工抽检 · 允许一定假阳性率换召回
- 对已知场景(TTS 尾部渐弱)单独维护豁免规则(例如尾部 3 s 内的无语音段不报)
六、写在最后
silero-vad 装机简单、CPU 单线程实时倍数高、多语种鲁棒 · 是 TTS 前处理 / ASR 前置 VAD 的一个轻量 baseline。但它不是万能的 ------case2 揭示的低电平语音漏检是它稳定的弱点之一。生产落地要把上面这套后处理链路当成配方而不是黑盒 · 阈值和过滤时长按场景调 · 出问题时能定位到是神经模型漏检 还是后处理链路合并/过滤逻辑不当 。ASR 前置场景可复用同一套 pipeline · 但 threshold / min_silence / 误报成本口径都要重新校。