跳到正文

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.ps1

Linux 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 预缓冲), 语音段结束时下发零长帧以驱动讯飞定稿。
  • 任务中启动的服务必须在结束前停止。

文档与代码在同一仓库维护,现有 Markdown 是唯一内容源。