服务定位
sherpa-asr-online-server 是一个 C++ WebSocket 流式 ASR 服务,使用 sherpa-onnx 的流式 zipformer + silero-vad。协议沿用 mod_audio_fork 子协议约定,因此可被 FreeSWITCH mod_audio_fork 直接当作 ASR 后端,也可被任意 WebSocket 客户端调用。
特性:
- HTTP
GET /health健康检查 - WebSocket
/audio,子协议audio.drachtio.org - 客户端先发一帧 metadata 文本,再持续推 16kHz / mono / PCM16 二进制音频
- 服务端按 partial / final 返回
type=transcription的 JSON 文本帧 - 客户端发送 0 长度二进制帧或关闭连接时,服务自动 flush 最后的识别结果
- 固定数量 I/O worker 线程 + select 事件循环
- 启动时预热
sessionPoolSize个 ASR session
构建与运行
1 | cd onnx-platform\sherpa-asr-online-server |
构建目录保留在 build\win_x64\,**可运行的发布产物安装到 target\win_x64\**(仅含可执行文件、config.json 与运行时 DLL)。bun run dev / bun run dev:asr 即从这里启动它。
启动:
1 | cd .\target\win_x64 |
从
target\win_x64\启动时优先改该目录下的config.json;重新构建不会自动覆盖已有的target\win_x64\config.json。
健康检查:
1 | curl http://127.0.0.1:10096/health |
返回:
1 | { |
WebSocket 协议
握手
1 | GET /audio HTTP/1.1 |
校验:
- 必须
Sec-WebSocket-Version: 13 - 必须
Upgrade: websocket+Connection: upgrade - 若
wsSubprotocol非空(默认audio.drachtio.org),请求必须带对应 token - 受
maxSessions限流,超出返回 503
握手成功后服务端立即为连接绑定一个预热的 ASR session(recognizer + VAD 已就绪),日志 ws-<n> websocket session started on worker=<i>。
客户端 → 服务端
1. metadata 文本帧(第一帧)
任意 JSON 文本,服务端会尝试解析 uuid | channel_uuid | call_uuid | sessionId | session_id 任一字段作为日志和录音文件名的 channelUuid。mod_audio_fork 默认发:
1 | { "uuid": "fs-channel-uuid", "callerId": "10086", "direction": "inbound" } |
后续 text frame 默认丢弃,logging.debugTextFrames=true 时打印。
2. 二进制音频帧
16kHz / 单声道 / PCM16 little-endian 原始采样。
- 帧大小受
socket.maxFrameBytes限制(默认 4MB),超过即触发 1002 协议错误关闭 - 任意时刻发 0 长度二进制帧 = “flush”:服务端立刻 flush 当前 segment 并发出 final
- 服务端不做重采样,上游非 16kHz 会导致 partial 全错
3. 控制帧
0x8Close:服务端先 flush final,再回1000 byeclose0x9Ping:原样回 Pong0xAPong:忽略- 其他 opcode:发
error后回1003 unsupported opcode并关闭 - 不支持分片帧(FIN=0 或 opcode=0x0),收到回
1002 protocol error
服务端 → 客户端
服务端通过文本帧下发 JSON 事件,按 type 分三类。
type=transcription
每次 partial 推送 / final 段完成 / 客户端 flush 都可能发一条:
1 | { |
| 字段 | 说明 |
|---|---|
text |
当前识别文本(partial 或 final) |
isFinal |
true 表示本句已结算,下一段从 utteranceIndex+1 开始 |
utteranceIndex |
同一连接内的语句序号,从 1 起 |
speechStarted/Active/SegmentDetected/SegmentCompleted |
VAD 状态 |
endpointDetected |
sherpa 三规则触发 endpoint |
serverSessionId |
服务端会话 ID(ws-<n>) |
channelUuid |
从 metadata 解析到的通道 UUID |
finalReason |
isFinal=true 时填:endpoint / segment-completed / final / flush |
flushReason |
flush 触发时填:client-close / zero-length-binary / socket-read-end |
partial 节流:同一句话内,partial 最快每 partialMinIntervalMs ms 下发一次;文本未变化的事件不重发。
type=error
1 | { "type": "error", "data": { "message": "ASR accept failed: <reason>" } } |
error 通常紧随 close:
- ASR
AcceptPcm16失败 → 1011 内部错误 - 协议错误(帧过大 / 不支持的 opcode)→ 1002 / 1003
type=disconnect
服务端主动断开前,可能先发:
1 | { "type": "disconnect", "data": { "reason": "<message>" } } |
连接生命周期
1 | client server |
关闭路径:
| 触发 | 行为 |
|---|---|
| client 发 close | flush final → 回 1000 bye → 关闭 |
| client TCP 断开 | flush final(flushReason=socket-read-end)→ 释放 session |
| 服务端流式异常 | 发 error → 关闭码 1011 |
| 协议错误 | 发 error + disconnect → 关闭码 1002 / 1003 |
关键配置
config.json 按域分组:server / concurrency / socket / logging /recording / asr / vad(与仓库 onnx-platform/sherpa-asr-online-server/README.md 一致)。
server — 监听
| 字段 | 默认 | 说明 |
|---|---|---|
server.host |
127.0.0.1 |
实际监听地址 |
server.port |
10096 |
实际监听端口 |
server.healthPath |
/health |
HTTP 健康检查路径 |
server.wsPath |
/audio |
WebSocket 升级路径 |
server.wsSubprotocol |
audio.drachtio.org |
强制子协议;空则不强制 |
concurrency — 并发
| 字段 | 默认 | 说明 |
|---|---|---|
concurrency.maxSessions |
16 |
并发会话上限;超出返回 503 |
concurrency.ioWorkers |
CPU 核心数 | I/O worker 线程数(受 FD_SETSIZE 约束) |
concurrency.sessionPoolSize |
16 |
预热的 ASR session 数;超过 maxSessions 会被裁剪 |
concurrency.numThreads |
1 |
ONNX 推理线程数 |
concurrency.acceptBacklog |
SOMAXCONN |
listen(backlog) |
concurrency.workerPollTimeoutMs |
10 |
工作线程单轮 select 超时;夹紧到 [1, 1000] |
socket — 套接字缓冲
| 字段 | 默认 | 说明 |
|---|---|---|
socket.tcpNoDelay |
true |
启用 TCP_NODELAY |
socket.maxFrameBytes |
4194304 |
单个 WebSocket 帧最大字节数;超过即 1002 协议错误 |
socket.readBufferBytes |
65536 |
每连接 recv 缓冲 |
socket.writeBufferBytes |
65536 |
每连接 send 缓冲(预留) |
socket.recvBufferBytes |
262144 |
SO_RCVBUF(0 表示不调整) |
socket.sendBufferBytes |
262144 |
SO_SNDBUF(0 表示不调整) |
logging — 日志
| 字段 | 默认 | 说明 |
|---|---|---|
logging.dir |
logs |
日志目录;相对路径按运行目录解析,按天滚动 |
logging.debugTextFrames |
false |
打印每条 metadata 文本帧 |
logging.debugAudioFrames |
false |
每个二进制帧附识别状态摘要(高吞吐慎开) |
logging.debugRecognitionState |
false |
每次识别状态变化落日志(定位 final 缺失) |
recording — 录音
| 字段 | 默认 | 说明 |
|---|---|---|
recording.enabled |
false |
启用上行音频录制 |
recording.dir |
recordings |
录制目录(相对路径按配置文件所在目录解析) |
asr — 识别
ASR 模型采用 asr.activeModel + asr.models profile 表结构:在 asr.models 下
声明多个模型 profile,用 asr.activeModel 指定当前加载哪一个。切换模型只改asr.activeModel,无需改动各 profile 的文件路径。默认配置声明两个 profile:中文
int8 流式 zipformer(默认)与中英双语。
| 字段 | 默认 | 说明 |
|---|---|---|
asr.activeModel |
sherpa-onnx-streaming-zipformer-zh-int8-2025-06-30 |
当前加载的模型 profile id,必须存在于 asr.models |
asr.models |
(profile 表) | 模型 profile 表,每项含 modelDir / encoder / decoder / joiner / tokens / bpeVocab? / modelType / modelingUnit |
asr.provider |
cpu |
ONNX 后端 |
asr.sampleRate |
16000 |
仅支持 16kHz |
asr.debug |
false |
sherpa-onnx 内部 debug |
asr.sendPartialResults |
true |
是否下发 partial |
asr.hotwordsScore |
1.5 |
热词权重;对所有 hotword profile 生效 |
asr.defaultHotwordProfile |
空 | metadata 未指定 hotwordProfile 时使用的 profile ID;为空则不启用 hotwords |
asr.hotwordProfiles |
{} |
静态 hotword profile 集合;连接建立后按 metadata 选择 |
asr.partialMinIntervalMs |
300 |
同一句 partial 下发节流间隔 |
asr.decodeBatchMs |
80 |
批量 decode 节奏 |
asr.models 每个 profile 的 modelDir 相对仓库 onnx-platform/models/ 解析,绝对路径
原样使用;profile 内的模型文件路径相对 modelDir 解析。
vad — 语音活动检测(强制启用,无开关)
| 字段 | 默认 | 说明 |
|---|---|---|
vad.model |
silero_vad.onnx |
silero-vad onnx;相对 onnx-platform/models/ 解析,绝对路径原样使用 |
vad.bufferSizeSeconds |
30.0 |
silero-vad ring buffer |
vad.threshold |
0.5 |
VAD 置信度阈值 |
vad.minSilenceDuration |
0.5 |
触发段尾的最小静音时长 |
vad.minSpeechDuration |
0.25 |
视为有效语音的最小时长 |
vad.windowSize |
512 |
VAD 窗大小(采样点) |
vad.maxSpeechDuration |
20.0 |
单段最大语音时长,超过强制切段 |
VAD 强制启用,没有
enableVad开关;endpoint 检测走 sherpa 默认,不在配置中暴露enableEndpoint/endpointRule*字段。旧版扁平字段asrModelName/asrEncoder/asrDecoder/asrJoiner/asrTokens/asrBpeVocab/asrModelType/asrModelingUnit/vadModel/listenHost/listenPort/recordAudioEnabled/recordAudioDir/enableVad/enableEndpoint/endpointRule*已移除,迁移时改写为上述分组结构。完整字段见仓库onnx-platform/sherpa-asr-online-server/README.md。
吞吐调优
concurrency.ioWorkers:每个 worker 受FD_SETSIZE约束(Windows 默认 1024,本进程编译期固定 1024,单 worker ~992 连接)concurrency.sessionPoolSize:建议接近或等于concurrency.maxSessions,避免连接抖动反复冷启动 recognizer / VADconcurrency.numThreads:ONNX 推理线程asr.partialMinIntervalMs/asr.decodeBatchMs:影响 partial 实时性 vs CPU 占用recording.enabled:默认关闭,只在排查问题时打开
与 FreeSWITCH 集成
通过 uuid_audio_fork:
1 | fs_cli -x "uuid_audio_fork <uuid> start ws://127.0.0.1:10096/audio mono 16k callflow_asr {}" |
callflow-esl 默认配置:
1 | { |
业务通过 ctx.hear({...}) 或 ctx.callHear(...) 触发,runtime 内部负责 audio_fork 启停与 transcription 事件订阅。
排查识别问题
开启上行音频录制
1 | { |
每个 WebSocket 会话会把 PCM16 二进制保存为 .wav:
1 | asr-<YYYYMMDD-HHMMSS-mmm>-<serverSessionId>[-<channelUuid>].wav |
放回 sherpa-demo/demo_online_asr.exe 复现,确认是模型问题还是网络问题。
调试日志
| 开关 | 用途 |
|---|---|
logging.debugTextFrames |
每条 metadata / 文本帧落日志 |
logging.debugAudioFrames |
每个二进制帧附识别状态摘要(高吞吐慎开) |
logging.debugRecognitionState |
每次识别状态变化落日志(定位 final 缺失) |
测试客户端
仓库内 request-asr.js 用原生 net + crypto 实现最小 WebSocket 客户端,按本协议发送 models/sherpa-onnx-sense-voice/test_wavs/zh_vad.wav 的 PCM16:
1 | node .\request-asr.js |
打印每条 JSON 事件。
局限
注意
- 仅支持 16kHz / mono / PCM16 上行,不做自动重采样
- 不支持 WebSocket 帧分片
- 模型加载耗时 1~3 秒/个,预热池规模影响启动时间
- 无鉴权、无限流(除
maxSessions),建议仅在内网 / 可信网络运行 - 服务无 graceful shutdown:Ctrl+C 直接终止 accept 与 worker
参考
- 协议详细描述:仓库
onnx-platform/sherpa-asr-online-server/README.md - mod_audio_fork 子协议:见 mod_audio_fork