# 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](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: ```powershell cd D:\github-project\ASR\Qwen-Asr\demo python scripts\download_models.py python scripts\serve.py ``` 另一个终端启动辅助模型服务: ```powershell cd D:\github-project\ASR\Qwen-Asr\demo pip install -r requirements-auxiliary.txt python scripts\auxiliary_server.py ``` 另开一个终端启动 WebSocket 页面: ```powershell 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`。可通过环境变量切换到远程服务: ```powershell $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 中的旧辅助服务进程。 ## 页面操作 1. 选择 Mic 或 File。 2. 点击开始,浏览器通过 `/ws` 建立本项目 WebSocket。 3. 页面展示腾讯 Demo 风格的气泡;麦克风或 PCM/WAV 文件按流式方式输入,partial 会在讲话过程中实时刷新。 4. VAD 检测到静音后提交当前 turn,先返回 pending,再异步更新说话人。 5. 停止会发送 `stop`,服务端完成当前 turn 和 speaker 队列后再发送 `end`。 6. `abort` 只取消会话,不提交当前片段。 ## WebSocket 消息 客户端首条消息: ```json { "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 } ``` 随后持续发送 16kHz、单声道、PCM16 二进制音频;文件模式仅支持 PCM/WAV,结束发送 `{"type":"eof"}`,停止发送 `{"type":"stop"}`,取消发送 `{"type":"abort"}`。每个已完成 turn 会向辅助服务 发送一次 `/v1/speaker/resolve`,只包含当前 turn 音频和 session_id,不会重复上传整段会话。 服务端会发送 `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 静音切段,更适合段落模式。 ## 目录 - `server.py`:本地 HTTP 页面和 WebSocket 会话编排。 - `model_service.py`:独立 VLLM OpenAI 音频接口适配层。 - `auxiliary_service.py`:独立辅助模型 HTTP 接口适配层。 - `speaker_assembler.py`:raw segment、speaker evidence 和 display block 状态机。 - `static/`:麦克风/文件测试页面。 - `tests/`:只测试本项目状态合并和音频转换逻辑。