外观
stream-vad-proxy
前置 VAD 流式代理(Rust)。透明串接在 FreeSWITCH mod_audio_fork 与上游 ASR 之间:在音频 进入上游之前本地跑 Silero VAD,在用户开嗓的那一帧就向下游注入真实 VAD 事件帧,为本身不 提供语音起始事件的流式识别 API 补齐「开嗓即打断」能力,同时按语音段闸门只把有效语音转发 给上游。
- 要解决的问题:部分厂商流式识别协议既不下发语音起始事件,也不提供服务端断句,调用方只能 等文本吐出后感知开嗓,并且无法直接获得稳定的语音段边界。
- 交付方式:下游 WS 保持
mod_audio_fork与apps/callflow-esl所需的调用面契约(握手、 metadata、PCM16 音频、零长 flush 以及transcription/error/disconnect事件),接入只需把audioFork.wsUrl指向本代理;这不表示代理与任一 ASR 服务的全部能力、字段来源或断句机制相同。 - 边界:代理不做 ASR、不自造文本——
text/isFinal永远来自上游,代理只覆盖四个 VAD 布尔位。配置采用 Profile 模式(upstream.defaultProfile+upstream.profiles):当前专门驱动 科大讯飞私有云实时识别(provider: "iflytek-iat",HTTP JSON-RPC,含 16k → 8k 抗混叠降采样)。 本代理专为自身既无语音起始事件、也无服务端断句的三方流式识别 API 补齐 VAD 与切段——阿里云 / Azure 自带服务端断句,仍走各自的 TS 语音网关;详见上游适配器。
专题导航
目录结构
text
onnx-platform/stream-vad-proxy/
├── src/
│ ├── main.rs # 启动、加载配置、模型预检、监听
│ ├── config.rs # config.toml 解析、路径解析、Profile 提取与启动自检
│ ├── downstream.rs # 服务端:/health + WS /audio、子协议协商、名额控制
│ ├── session.rs # 单连接会话:VAD 边沿、闸门、pre-roll、讯飞会话生命周期与排空
│ ├── upstream.rs # 上游连接分派与通用抽象
│ ├── upstream/
│ │ ├── iflytek.rs # 科大讯飞 IAT 适配器(HTTP JSON-RPC)
│ │ └── resample.rs # 16k -> 8k 抗混叠 FIR 抽取降采样
│ ├── vad.rs # Silero VAD 封装:512 整窗喂入与边沿判定
│ ├── protocol.rs # 下游帧结构与上游帧改写
│ ├── recording.rs # 入站音频落盘(诊断用)
│ └── logging.rs # 按天滚动日志
├── config.toml # 外置配置(Profile 模式)
├── build.ps1 / build.sh # Windows / Linux 构建入口
├── run-server.mjs # 根 `bun run dev:vad` 的启动器
├── target/ # Cargo 构建产物与缓存目录,不提交
└── dist/ # 自包含交付目录(如 dist/win_x64/),不提交VAD 模型从 ../models/silero-vad/silero_vad.onnx 解析;sherpa-onnx SDK 来自 ../sherpa-onnx/。Cargo 构建遵循 Rust 官方标准,输出至 target/; 最终运行包由打包脚本统一装配到 dist/<platform>/。
构建
需要 Rust stable(rust-toolchain.toml 已固定 channel 与 rustfmt / clippy 组件, rustup 会自动装齐)。Rust 侧链接的是仓库自带的 sherpa-onnx 预编译 SDK: sherpa-onnx crate 以 features = ["shared"] 动态链接,构建脚本会把 sherpa-onnx 动态库自动拷入交付目录。
Windows x64:
powershell
pwsh ./build.ps1Linux x64:
bash
bash ./build.sh # 或根目录 bun run vad:build:linux两个脚本都做同一件事:cargo build --release → 重建 dist/<platform>/ → 拷入可执行文件、 sherpa-onnx 运行库、config.toml 与 silero-vad/silero_vad.onnx,产出自包含的可运行交付目录。
三处容易踩的点:
- 必须用
pwsh(PowerShell 7)跑build.ps1。仓库.ps1约定不带 BOM,Windows PowerShell 5.1 会把无 BOM 的 UTF-8 中文按 ANSI 解读并直接抛ParserError。 target/是构建缓存,dist/<platform>/是交付目录。Cargo 构建使用官方原生的target/目录,支持安全的cargo clean;最终可运行的自包含交付包统一装配至dist/<platform>/,彻底解耦编译器中间件与发布资产。- 首次构建需要能访问 SDK。
onnx-platform/sherpa-onnx/<platform>/lib存在时直接链接本地 SDK;不存在时sherpa-onnx-sys会联网拉取对应平台的预编译库,离线环境请自行准备目录并用SHERPA_ONNX_LIB_DIR指定。
运行
仓库根目录:
powershell
bun run dev:vad也可进入交付目录直接运行:
powershell
cd dist\win_x64
.\stream-vad-proxy.exe配置路径解析顺序:argv[1] → STREAM_VAD_PROXY_CONFIG_PATH → 进程 CWD 下 ./config.toml。 run-server.mjs 以交付目录为 CWD 启动,因此配置与模型都从交付目录解析。启动时会做一次 VAD 模型预检(加载 + 一窗推理),模型缺失或参数非法时直接退出,不静默降级。
本服务不属于根 bun run dev 默认启动组,与两个云语音网关一样按需启动。
配置
配置文件采用清晰的 Profile 模式:
toml
[upstream]
defaultProfile = "iflytek"
[upstream.profiles.iflytek]
provider = "iflytek-iat"
url = ""
appid = ""
svc = "iat"
type = "1"
busid = "stream-vad-proxy"
authNeed = false
sampleRate = 8000
chunkBytes = 800
maxChunkBytes = 8000
requestTimeoutMs = 10000
connectTimeoutMs = 3000
grsPollIntervalMs = 20
grsMaxPolls = 100
drainIdleMs = 1500
drainTimeoutMs = 8000启动前至少校准两项:
upstream.profiles.<profile>凭据:url与appid优先通过环境变量注入,仓库内config.toml留空:IFLYTEK_IAT_URL:讯飞私有云 IAT HTTP 端点(形如http://host:port/iat/);IFLYTEK_IAT_APPID:厂商分配的应用 ID。
vad.preRollMs:默认500ms,开嗓前预缓冲音频时长,在上升沿时一次性补给上游,确保不吃首字。
vad 段默认值逐项对齐 onnx-platform/sherpa-asr-server/config.toml 的实际运行值 (threshold: 0.35、minSilenceDuration: 0.35、minSpeechDuration: 0.15),不是 0.5 / 0.5 / 0.25。完整字段表与路径解析见配置参考。
健康检查
powershell
Invoke-WebRequest http://127.0.0.1:10093/health响应包含 activeSessions / maxSessions / wsPath / profile / provider / upstream / upstreamConnected。profile 是当前激活的 Profile 名称(默认 "iflytek"),provider 是当前上游适配器实现("iflytek-iat"),upstream 是实际后端端点。 upstreamConnected 反映当前是否有会话持有上游连接(代理按连接建立上游链路,没有通话时 为 false,这不是故障)。健康检查通过只代表模型已加载与配置合法,不代表上游可达。
最小验证
直接复用 ASR 服务的测试客户端当作 mod_audio_fork 替身(先起代理,再推流测试):
powershell
bun run onnx-platform\sherpa-asr-server\request-ws.js --file <audio.wav> --port 10093验证空文本 VAD 起始帧出现在首个非空 partial 之前——这个时间差就是本代理交付的提速量。--silence(3 秒零 PCM) 必须不产生任何厂商请求与 final 帧,这是误打断的回归防线。完整排障步骤见 集成与排障。
性能实测与容量校准
Windows 11 / x64,numThreads: 1、provider: "cpu"、单路:
| 指标 | 实测值 |
|---|---|
每会话 VAD 创建(加载 silero_vad.onnx,643KB) | 约 83~97ms |
| 单窗推理(512 采样 = 32ms 音频) | 约 113µs |
| 实时率(RTF) | 约 0.36% |
| VAD 起始帧与 C++ 参考实现的偏差(同一段音频) | 约 10ms 内 |
| 闸门拦下的静音音频占比(含 5 句人声的 20.34s 素材) | 约 32% |
由此校准并发:单路推理只占 0.36% 的 CPU 时间,瓶颈不是推理而是每会话建链时那次模型加载 (约 90ms,已放在阻塞线程池,不卡异步 worker)。concurrency.maxSessions 默认 256 是上限 保护而非实测容量;按呼叫建立速率估算,每秒新建 N 路会话大约需要 N × 0.09 个核用于加载, 稳态识别再按每路 0.36% 叠加。生产上先按并发通话数的 1.5 倍设置,再用 /health 的 activeSessions 观察真实峰值。
注意事项
- 本地 VAD 切段是三方非 VAD 引擎唯一的断句来源。代理在静音期自动拦截音频(带 pre-roll 预缓冲), 语音段结束时下发零长帧以驱动讯飞定稿。
- 任务中启动的服务必须在结束前停止。