- 人工智能
- 语音
【免费下载链接】silero-vad
Silero VAD: pre-trained enterprise-grade Voice Activity Detector
导读
本文围绕 Silero VAD 仓库中 examples/openvino 目录的配套文档展开,讲解如何把仓库内随附的silero_vad.onnx转换成可直接在 OpenVINO 中加载、编译与推理的模型。你将掌握:ONNX 前端为何无法加载原模型的根本原因(If 子图内的 Conv/STFT 静态秩推断失败)、convert.py如何通过"经验性条件求值 + 死代码消除 + Identity 折叠 + 静态形状修复"完成等价变换、如何通过verify.py做逐位级验证,以及推理时为什么必须把 CPU 插件精度锁定为 f32、I/O 协议如何与仓库 OnnxWrapper 保持一致。
问题背景:stock 模型为什么无法加载
仓库随附的原始模型位于 src/silero_vad/data/silero_vad.onnx(文件约 2.32 MB,与文档描述的 2.27 MB 一致)。它由 TorchScript 追踪导出,无法直接被 OpenVINO 的read_model加载,报错如下:
OpConversionFailure: Model wasn't fully converted. -- Conv-16: While validating ONNX node '<Node(Conv): If_0_then_branch__Inline_0__/decoder/decoder/2/Conv_output_0>': Check 'data.get_partial_shape().rank().is_static()' failed The input data tensor's rank has to be known (static)根因链条可以拆成三步:
- Conv 算子藏在 If 子图里:STFT 前端特征计算被封装为
Conv算子,且这些Conv位于If节点的then_branch/else_branch子图中。OpenVINO 的 ONNX 前端在转换时无法为子图内的这些Conv推断出静态的输入张量秩(rank仍为动态)。 - If 数量多且嵌套:图中有一个顶层
If用于在 8 kHz 与 16 kHz 两条路径之间切换,另外每个分支下还残留 7 个由 TorchScript 追踪留下的嵌套If节点。因此仅仅把采样率常量折叠掉(即直接写死sr)是不够的——其余嵌套 If 依然会让前端转换失败。 - 所有条件都与音频内容无关:这些 If 的条件只依赖采样率和张量形状(
sr取 8000 还是 16000),不依赖实际音频数据。这意味着对任一固定配置(固定采样率、固定 batch),所有分支都可以在离线阶段一次性解析掉——这正是convert.py转换思路成立的前提。
convert.py:四步离线等价变换
convert.py 的核心流程(convert函数)如下:加载模型 → 循环内联所有 If → 死代码消除 → 折叠 Identity → 固定静态形状 → 类型推断与校验 → 保存(可选导出 IR)。下面结合源码逐段拆解。
1. 经验性求值并内联每个 If
eval_cond(convert.py)用一个"探针"技巧求 If 条件的实际取值:深度拷贝当前模型,把条件张量追加为 graph 的额外输出,再用 onnxruntime 在 CPU 上运行一次,从输出中取出条件值并断言它是标量布尔。
inline_if(convert.py)随后把选中的分支整体替换掉 If 节点:
- 把分支内部的节点、initializer 统一重命名,加
F{step}::前缀避免与外层作用域名字冲突(deep_rename会递归进入嵌套子图,并按 ONNX 作用域规则跳过被遮蔽的名字); - 分支输出若来自内部,则重命名直接映射到 If 节点的对应输出;若分支输出是外层捕获的张量,则补一个
Identity节点做桥接; - 分支的 initializer 拷贝进外层 graph。
convert的主循环(convert.py)反复扫描If节点,逐个求值、内联,直到图中不再存在If,最后断言图中也不存在Loop、Scan这类控制流算子,保证产出一个纯数据流图。
2. 死代码消除(DCE)
dce(convert.py)从 graph 输出反向做可达性分析:只保留能连到输出节点的节点,并递归收集其输入及子图中从外层捕获的输入;随后同步清理不可达的 initializer 和 graph input。这一步的意义不止于"瘦身"——未选中的采样率分支(例如--sr 8000时整个 16 kHz 分支)连同其全部权重会被物理移除,这正是文档所说文件从 2.27 MB 降到 1.23 MB 的原因。
3. 折叠 Identity 节点
eliminate_identity(convert.py)反复遍历并移除 Identity 节点,把它的输入/输出引用在消费者与生产者之间重连(保留"输出到输出"别名这类必要场景)。文档明确指出其必要性:较新版本的 OpenVINO 在序列化 IR 时会把这类节点写成opset16::Identity,而较旧的 CPU 插件"能解析但不能执行",转换脚本在测试中真实踩过这个坑——这也是 README 反复强调"IR 要用目标 OpenVINO 版本本地重新生成"的原因。
4. 固定静态形状
内联与清理完成后,脚本把input、state、output、stateN四个 I/O 的形状从动态维度(dim_param)改写为固定值(convert.py),删除残留的value_info,再调用shape_inference.infer_shapes做全图形状推断,最后onnx.checker.check_model校验并用onnx.save写出干净模型。默认输出文件名为silero_vad_16k.onnx(--sr 8000时则为silero_vad_8k.onnx),见main中的默认值逻辑(convert.py)。
命令行用法
文档给出的完整命令如下,注意输出文件名与采样率选择:
pip install numpy onnx onnxruntime openvino python convert.py # writes silero_vad_16k.onnx python convert.py --sr 8000 # writes silero_vad_8k.onnx python convert.py --save-ir # also exports OpenVINO IR python verify.py silero_vad_16k.onnx ../../tests/data/test.wav "../c++/aepyx.wav" python verify.py silero_vad_8k.onnx --sr 8000 "../c++/aepyx_8k.wav"补充说明(均与脚本实现一致):
convert.py默认输入是仓库自带的 silero_vad.onnx(REPO_MODEL定义于 convert.py,通过parents[2]定位到仓库根);也可传[input.onnx] [output.onnx]位置参数指定自定义路径;--sr仅接受 8000 / 16000 两个取值(convert.py),对应保留的分支与输入长度:16 kHz 时input为[1, 576],8 kHz 时[1, 288](见 convert.py 的 feeds 构造);--save-ir需要已安装 openvino,会在输出旁生成同名.xml+.bin,且compress_to_fp16=False保持 f32 权重(convert.py);- 依赖方面:
convert.py只要求 numpy、onnx、onnxruntime;openvino 仅在--save-ir时才需要(见文件头部 docstring,convert.py)。
必须设置推理精度为 f32
文档给出了一条容易被忽略的硬性建议:在支持 bf16 的 CPU(具备 AMX 或 AVX512 BF16 指令集,多见于服务器级 Xeon)上,OpenVINO CPU 插件默认会静默启用 bf16 推理。对 Silero VAD 这种带循环状态的小模型,这不是"可以接受的小误差":
- 每个 chunk 的数值误差会通过循环状态(
state,形状[2, 1, 128])逐步累积放大,直到语音分段结果发生变化; - 文档实测:真实音频上默认精度与 stock 模型的最大绝对差可达
3e-1,而 f32 仅为2e-6——相差约 5 个数量级。
因此推理时务必显式指定精度,Python 端写法:
compiled = core.compile_model(model, "CPU", {"INFERENCE_PRECISION_HINT": "f32"})C++ 端对应ov::hint::inference_precision(ov::element::f32)。verify.py的OvConverted类也遵循这一约定(verify.py),并在输出中打印当前 OpenVINO 版本与 "inference precision forced to f32"。模型本身很小,f32 带来的开销可以忽略。
转换后模型的 I/O 契约与推理协议
转换后模型已移除sr输入(采样率分支被固定),I/O 如下:
| 张量 | 方向 | 形状(16 kHz) | 形状(8 kHz) | 类型 |
|---|---|---|---|---|
input | 输入 | f32[1, 576]= 64 上下文 + 512 新样本 | f32[1, 288]= 32 上下文 + 256 新样本 | f32 |
state | 输入 | [2, 1, 128] | [2, 1, 128] | f32 |
output | 输出 | [1, 1] | [1, 1] | f32(语音概率) |
stateN | 输出 | [2, 1, 128] | [2, 1, 128] | f32 |
推理循环协议与仓库内 OnnxWrapper 完全一致(对照OnnxWrapper.__call__,utils_vad.py):
- 每个 chunk 为 512 个新样本(8 kHz 时 256 个),拼上上一轮的上下文组成
input; - 把本次输出的
stateN链回下一次的state; - 从本次
input尾部截取最后 64(8 kHz 为 32)个样本作为下一轮的上下文; - 新数据流开始时,
state与ctx都清零。
文档给出的最小推理示例:
import numpy as np import openvino as ov req = ov.Core().compile_model("silero_vad_16k.onnx", "CPU", {"INFERENCE_PRECISION_HINT": "f32"}).create_infer_request() state = np.zeros((2, 1, 128), np.float32) ctx = np.zeros((1, 64), np.float32) for chunk in stream_of_512_sample_chunks: x = np.concatenate([ctx, chunk[None]], axis=1) res = req.infer({"input": x, "state": state}) prob, state, ctx = res["output"][0, 0], res["stateN"], x[:, -64:]verify.py:逐位级验证与分段一致性检查
verify.py 把转换后的模型与 stock 模型在"链式状态"下做端到端对比,任何不符都会以非零退出码结束(verify.py),其判定标准为:
- onnxruntime 中转换模型 vs stock 必须逐位相等(max abs diff 恰好为 0.0);
- OpenVINO 中 max abs diff 必须小于
1e-4; - 真实 wav 上的语音分段结果必须与 stock 完全一致。
对比引擎有三个(verify.py):OrtStock(stock 模型,保留sr输入)、OrtConverted(转换模型、onnxruntime 后端)、OvConverted(转换模型、OpenVINO 后端、强制 f32)。它们共享相同 reset/chunk 拼接逻辑,其中上下文大小 64/32 与 chunk 大小 512/256 的映射和推理协议一一对应。
测试音频有两类:
- 合成音频(
synthetic_audio,verify.py):一段约 22 秒、包含静音、50/60/120/180 Hz 工频谐波、高斯噪声、4 Hz 包络 + 220/710/2400 Hz 类共振峰载波、100→1000 Hz 扫频、强噪声与尾部静音的拼接信号,随机种子固定为 42。文档强调对比是纯数值性的,不需要像真实语音; - 真实 wav:任意 16 bit 单声道 PCM 文件,采样率必须匹配(
load_wav会断言帧率、声道数与位深,verify.py)。
分段判定函数segments(verify.py)以概率 0.5 为阈值、最短 8 个 chunk 为一段,统计起始/结束索引并逐段比对。
文档给出的实测验证结论(可复现依据即上述脚本与仓库内测试数据):
- 在 onnxruntime 中,转换模型对两种采样率均逐位一致(max abs diff 0.0),说明该变换对所选采样率是数学意义上的恒等变换;
- OpenVINO + f32 下,16 kHz 对 tests/data/test.wav 与 examples/c++/aepyx.wav(合计 229 秒、7161 个 chunk)max abs diff 为
2.3e-06,29/29 与 65/65 个分段全部一致; - 8 kHz 对 examples/c++/aepyx_8k.wav max abs diff 为
4.5e-06,79/79 个分段一致; - 上述结果在 Linux 与 Windows、两个 OpenVINO 版本上复现;stock 模型 sha256 为
1a153a22f4509e292a94e67d6f9b85e8deb25b4988682b7e174c65279d8788e3(可直接对 src/silero_vad/data/silero_vad.onnx 校验)。
使用限制(Caveats)
- 单采样率、固定 batch:每个转换后的模型只保留一个采样率分支(
sr输入被移除),batch 固定为 1。文档明确指出部分被内联的 guard 可能依赖 batch,因此 batch=1 已被穷尽验证,更大的 batch 未验证; - IR 对版本敏感:IR(xml + bin)与生成它的 OpenVINO 版本绑定。部署时应使用目标 OpenVINO 版本重新生成 IR,或直接
read_model加载清洗后的 ONNX; - wav 文件格式:verify 只接受 16 bit 单声道 PCM,采样率须与
--sr匹配(verify.py)。
常见问题(FAQ)要点
每个 OpenVINO 大版本都要重新生成模型吗?不需要。唯一版本敏感的产物是 IR;convert.py产出的清洗后 ONNX 是普通 ONNX 文件,OpenVINO 直接read_model即可加载,跨 OV 版本的行为与 stock 模型跨 onnxruntime 版本一致。这也是仓库选择发布转换脚本而非二进制产物、并建议"如要发布 IR 就用目标版本本地生成"的原因。
IR 向前还是向后兼容?IR 向前兼容、不向后兼容:旧版本生成的 IR 可被新运行时加载(IR 格式自 2022.1 起稳定),但新版本生成的 IR 可能无法在旧运行时上使用——opset16::Identity无法被旧 CPU 插件执行正是实例,也是convert.py折叠 Identity、README 建议本地重生成 IR 的由来。
是否依赖具体 CPU 型号?文件本身与硬件无关,编译在加载时针对目标进行;唯一的 CPU 相关行为是默认推理精度——bf16 能力的 CPU 上插件默认 bf16,错误会经循环状态累积到分段改变;按 README 设置INFERENCE_PRECISION_HINT=f32后,各 CPU 上与 onnxruntime 的误差均约 1e-6。
只能跑在 Intel CPU 上吗?不是。CPU 插件按指令集特性而非厂商分派:AMD x86-64 同样可运行(Intel 是验证与调优的主要对象),ARM64(含 Apple Silicon)也官方支持。Intel 独占的是加速器插件(集成 GPU 与 NPU),本示例未涉及。
与仓库主代码的关系
本示例的推理协议与仓库主库的 OnnxWrapper 完全同构——上下文拼接、状态链回、清零时机与num_samples/context_size的 512/64、256/32 映射均可在 utils_vad.py 中找到对应实现;verify.py中的segments分段逻辑也与仓库get_speech_timestamps的阈值化思想一致。因此,在 OpenVINO 上运行转换模型时,可以沿用仓库 tests/test_basic.py 中read_audio+ 分块推理的使用范式,仅在推理后端上替换为 OpenVINO 的 InferRequest。若需进一步了解模型结构本身,可参考 src/silero_vad/model.py 与 hubconf.py。
- 人工智能
- 语音
【免费下载链接】silero-vad
Silero VAD: pre-trained enterprise-grade Voice Activity Detector
相关推荐
DeOldify模型转换终极指南:ONNX Runtime与OpenVINO部署实践
DeOldify模型转换终极指南:ONNX Runtime与OpenVINO部署实践 DeOldify作为基于深度学习的图像和视频着色修复工具,在实际应用中往往
人工智能深度学习计算机视觉图像处理Jellium Desktop vs Plex Desktop:哪个才是最佳媒体中心客户端?
Jellium Desktop vs Plex Desktop:哪个才是最佳媒体中心客户端? 当你在寻找理想的媒体中心客户端时,Jellium Desktop和
桌面应用音视频Silero VAD模型终极指南:从PyTorch到ONNX完整转换与部署
Silero VAD模型终极指南:从PyTorch到ONNX完整转换与部署 语音活动检测(VAD)作为现代语音应用的核心组件,在实时通信、语音识别预处理和智能设
人工智能语音
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考