7.6 KiB
Realtime ASR WebSocket Optimization Demo
这是一个独立的实时 ASR WebSocket 验证项目,放在模型部署项目 demo 下,但不导入、不启动、也不调用仓库根目录的原始 app/。
验证目标
- 浏览器麦克风或按实时速度发送的 PCM/WAV 音频通过本项目 WebSocket 发送。
- 同一
sentence_id的 partial、final 和后续更新覆盖同一条 raw segment。 - raw segment 与前端 display block 分离,避免把物理切段直接等同于展示换行。
- 小于 1.6 秒且带有
short_attach/embedding_attach策略的实名结果降级为 pending。 - 不把 embedding 字段写入 demo 状态池。
- 只有相邻且身份可信的 segment 才合并;A→B→A 保持时间顺序。
- 记录 partial 首次延迟、final 延迟、partial 修订次数和服务端返回时间范围。
- 每个实时 turn 单独提交声纹特征,由辅助服务维护本 WebSocket session 的在线聚类中心。
本次修复、诊断状态和部署验收步骤见 FIXES.md。ASR 继续使用已部署的独立 vLLM;已有 vLLM 服务时无需重复启动或下载模型。
当前 VLLM 端点提供的是同步 OpenAI 音频转写接口,没有暴露原生
create_stream/feed_stream/finish_stream。因此本项目仍然是真实 WebSocket
音频流:麦克风 PCM 到达后立即进入 VAD,按窗口调用 VLLM 生成 partial;它不会
等整段音频结束。native_partial_supported=false 只表示模型 HTTP 接口本身
不是原生 ASR stream,不伪造不存在的能力。
启动
需要启动两个模型服务和一个 WebSocket 页面:VLLM 只负责 ASR,辅助服务负责 VAD、CAM++ 声纹模型及在线聚类,WebSocket 只做音频流编排,不导入原项目代码。
先在 demo 目录下载 ASR 和辅助模型,并启动 VLLM:
cd D:\github-project\ASR\Qwen-Asr\demo
python scripts\download_models.py
python scripts\serve.py
另一个终端启动辅助模型服务:
cd D:\github-project\ASR\Qwen-Asr\demo
pip install -r requirements-auxiliary.txt
python scripts\auxiliary_server.py
另开一个终端启动 WebSocket 页面:
cd D:\github-project\ASR\Qwen-Asr\demo\realtime_asr_optimization_demo
python -m venv .venv
.\.venv\Scripts\Activate.ps1
pip install -r requirements.txt
python server.py --no-browser
页面服务默认监听 0.0.0.0:8082,端口在 server.py 顶部的 WEB_PORT 内部变量中维护。
VLLM 默认地址为 http://127.0.0.1:9950/v1,辅助服务默认地址为
http://127.0.0.1:8010。可通过环境变量切换到远程服务:
$env:MODEL_SERVICE_URL = 'http://127.0.0.1:9950/v1'
$env:AUXILIARY_SERVICE_URL = 'http://127.0.0.1:8010'
python server.py --no-browser
如果服务器端口已通过 VS Code Remote/端口转发映射到本机,保持上述两个
127.0.0.1 地址即可:本地 WebSocket 只负责编排,ASR 和 VAD/CAM++ 推理仍在
服务器 GPU 服务中完成。先访问 http://127.0.0.1:9950/v1/models 与
http://127.0.0.1:8010/health,分别确认 VLLM 模型和辅助模型服务可达且 ready=true。
服务器部署时使用 --no-browser,然后在客户端浏览器访问 http://服务器IP:8082。如需让启动日志显示服务器域名或 IP,可设置 WEB_DISPLAY_HOST;它只影响提示文本,不改变监听地址。
辅助服务启动后可用 http://服务器IP:8010/health 检查模型状态。WebSocket
收到聚类服务错误时仍会继续输出 ASR,但对应片段会显示“未知说话人”;详细的
speaker_reason 可将鼠标悬停在标签上查看,事件日志仍会显示 speaker_warning,
便于区分“模型未归类”和“ASR 失败”。
声纹服务的实时路径必须通过 ModelScope pipeline 的公开接口提取 embedding:
pipeline([wav_path], output_emb=True)。不能绕过 pipeline 预处理后直接调用
pipeline.model,否则采样率、声道和 waveform 预处理不会执行,部分 ModelScope
版本会直接抛异常,WebSocket 仍会继续输出 ASR 并把说话人保留为 pending。
更新辅助服务代码后需要重启 python scripts/auxiliary_server.py,仅重启页面
服务不会替换已经驻留在 GPU 中的旧辅助服务进程。
页面操作
- 选择 Mic 或 File。
- 点击开始,浏览器通过
/ws建立本项目 WebSocket。 - 页面展示腾讯 Demo 风格的气泡;麦克风或 PCM/WAV 文件按流式方式输入,partial 会在讲话过程中实时刷新。
- VAD 检测到静音、或活跃 turn 内声纹窗口比对检出换人时提交当前 turn,先返回 pending,再异步更新说话人。
- 停止会发送
stop,服务端完成当前 turn 和 speaker 队列后再发送end。 abort只取消会话,不提交当前片段。
WebSocket 消息
客户端首条消息:
{
"type": "start",
"source": "mic",
"model_service_url": "http://127.0.0.1:9950/v1",
"model": "Qwen/Qwen3-ASR-0.6B",
"speaker_diarization": 1,
"sentence_strategy": 0,
"enable_native_partial_stream": true,
"partial_interval_ms": 1200,
"max_segment_sec": 12,
"display_merge": true,
"speaker_window_ms": 1000,
"speaker_switch_similarity": 0.5
}
speaker_window_ms 与 speaker_switch_similarity 可省略,默认读取环境变量
SPEAKER_TURN_WINDOW_MS(1000)和 SPEAKER_SWITCH_SIMILARITY(0.5)。
随后持续发送 16kHz、单声道、PCM16 二进制音频;文件模式仅支持 PCM/WAV,结束发送 {"type":"eof"},停止发送
{"type":"stop"},取消发送 {"type":"abort"}。每个已完成 turn 会向辅助服务
发送一次 /v1/speaker/resolve,只包含当前 turn 音频和 session_id,不会重复上传整段会话。
换人强制切段产生的边界段会附带 speaker_verified=0:辅助服务仍返回匹配标签,
但不更新簇质心、不建立新簇。活跃 turn 内另有窗口比对请求 /v1/speaker/embedding
(multipart 音频,返回 {"ok": true, "embedding": [...]}),只出向量、零聚类副作用。
辅助服务 speaker_protocol_version 为 3;旧版辅助服务缺少窗端点时 WebSocket 侧
探测一次即自动停用切换感知切段并发送 speaker_warning,不影响 ASR。
服务端会发送 start、sentences、display_state、metrics、speaker_warning、draining、end 和 error。
页面用带 revision 的 display_state 渲染,以 block_id 标识展示块;sentences 保留原始片段及诊断状态。
停止后必须等待 end,其中包含完整 sentences 和 display_blocks,不能提前关闭连接。
sentences 中 sentence_type=0 是 partial,sentence_type=1 是 final;同一个
sentence_id 必须覆盖更新而不是追加。sentence_strategy=0 使用约 800ms
静音切句,sentence_strategy=1 使用约 1400ms 静音切段,更适合段落模式。
静音阈值之外还有说话人切换感知切段:活跃 turn 内每积累约 1 秒有声窗口就与
辅助服务比对一次声纹,与当前 turn 参考向量余弦低于阈值时在“停顿≥200ms 后的
首个起音”处强制切开,头段 final 的 commit_reason 为 speaker_change。这覆盖
上一位尾句与下一位间隔不足静音阈值(常见 200~600ms 换话停顿)被合成一句的问题。
目录
server.py:本地 HTTP 页面和 WebSocket 会话编排。model_service.py:独立 VLLM OpenAI 音频接口适配层。auxiliary_service.py:独立辅助模型 HTTP 接口适配层。speaker_assembler.py:raw segment、speaker evidence 和 display block 状态机。static/:麦克风/文件测试页面。tests/:只测试本项目状态合并和音频转换逻辑。