9.4 KiB
9.4 KiB
实时会议识别改造报告
1. 背景
当前 Qwen-Asr 的实时会议链路,已经暴露出 3 类系统性问题:
- 长句在
max_duration或静音点附近容易被截坏,出现残句、半句、尾词漂移。 partial与final segment责任混杂,静音、串音、背景音、测试语种会直接污染最终结果。- 说话人识别仍以“句级单 embedding + 会话聚类”为主,面对插话、背景音、儿童声音、英语/泰语测试时容易抖动。
这些问题并不是单个阈值导致的,而是实时链路的职责分层不清晰。
2. 外部方案调研结论
本次调研覆盖了:
- vLLM / Qwen3-ASR 官方实时文档
diartpyannote.audio- NVIDIA NeMo
Streaming Sortformer - 在线 speaker diarization / label matching 论文
- streaming ASR partial stability / endpointing 论文
2.1 vLLM/Qwen3-ASR 的边界
vLLM 的 Qwen3-ASR realtime 更像“流式推理后端”,它负责:
- 累积音频
- 到固定块长后执行流式推理
- 提供
flush/finish
它不负责:
- 会议场景 endpointing
- speaker diarization
- partial 稳定化
- 说话人标签稳定跟踪
结论:
vLLM 只能做实时 ASR 的第一层,不是完整会议链路的解决方案。
2.2 开源实时 speaker diarization 的主流范式
A. diart 范式
特征:
- rolling buffer
- overlap-aware segmentation
- incremental clustering
- cannot-link constraints
- 低延迟滚动更新
优点:
- 工程上相对容易接入
- 很适合作为现有系统的 speaker 旁路
B. Streaming Sortformer 范式
特征:
- 真正 streaming diarization
- chunk-based processing
- speaker cache / AOSC
- 跨 chunk 标签稳定
优点:
- 更像现代工业方案
- 标签稳定性更强
缺点:
- 接入成本高于
diart
2.3 论文结论
在线 diarization 的关键问题不是“分离”,而是“标签一致性”
主流论文强调:
- 在线 diarization 必须解决 label matching
- 否则
Speaker01/02/03会在不同 chunk 间乱跳
streaming ASR 的关键问题不是“能不能出字”,而是“partial 稳定性”
论文结论:
- partial 会被不断修正
- partial 不应直接当 final 使用
- 需要单独设计稳定策略
endpointing 对长句质量至关重要
论文与开源实现都说明:
- 只靠静音阈值不够
- 最好使用 VAD/SAD 或模型辅助 endpointing
max_duration只能是兜底机制,不应成为主要切句方式
3. 当前项目存在的核心架构问题
3.1 partial 和 final 混线
当前链路中:
partial的输出直接影响最终句子定稿- 静音前后、噪声和串音有机会直接污染最终句子
这会带来:
- 静音幻觉
- 末尾漂移
- 错误残句
3.2 断句主要靠“能量阈值 + 最长时长”
当前主逻辑仍然偏向:
self._has_voice(audio)做粗能量判定silence_samples达阈值就截断12s max_duration强行保底
这会导致:
- 长句被硬切
- 断句点不自然
- 下一段拿到的是残尾巴
3.3 speaker 仍然是“句级单 embedding”
当前 speaker 的主要依据是:
- 当前句或其裁剪后的音频
- 提一个 embedding
- 与会话内 slot 聚类
这对干净单人句子有效,但对会议场景不够:
- 一句里可能混多人
- 背景音会污染 embedding
- 不同 chunk 之间缺少真正的 diarization cache
3.4 registry match 介入过早
当前一旦句级聚类通过阈值,就可能直接映射实名。
这会带来:
- 错误聚类被直接升级为错实名
- 后续纠正空间变小
4. 推荐的目标架构
建议将实时会议链路拆成 4 层:
Layer A: Streaming ASR Partial
职责:
- 面向 UI 输出中间文本
- 只作为“参考内容”
- 不入库
- 不参与 speaker
- 不做实名映射
要求:
- 可以允许回退、修正
- 需要稳定策略,但不要求和 final 完全一致
Layer B: Endpointing / Final Segment Flush
职责:
- 决定“什么时候一句真正结束”
- 产出 final segment
推荐方式:
pending_buffer- 周期性 VAD/SAD 检查
- 只刷出“已完成”的语音片段
- 最后一段继续留 buffer,等待更多上下文
说明:
max_duration仍保留- 但只作为异常保底
Layer C: Online Speaker Tracking
职责:
- 产出稳定
Speaker01/02/03/... - 不直接输出实名
推荐路线:
- 低成本版:
rolling window diarization + incremental clustering - 强化版:
streaming diarization + speaker cache
必要能力:
- label matching
- speaker cache
- cluster centroid 更新
- recent speaker continuity
Layer D: Speaker Registry Match
职责:
- 把稳定的 slot 映射成实名
原则:
- 先有稳定
Speaker01/02/03 - 再做 registry match
- 不要在每个短句上直接实名匹配
5. 对当前代码库的具体落地建议
5.1 保留 Qwen3ASREngine 作为流式 ASR 后端
文件:
app/services/asr/qwen3_engine.py
建议:
- 继续让它只负责
init_streaming_state / streaming_transcribe / finish_streaming_transcribe - 不再让它承担句边界和 speaker 逻辑
5.2 重构 qwen3_websocket_asr.py
文件:
app/services/qwen3_websocket_asr.py
建议把它拆成以下内部组件:
-
PartialStreamController- 负责把有声 chunk 喂给流式 ASR
- 维护 partial 状态
-
RealtimeEndpointController- 维护
pending_buffer - 周期性 VAD flush
- 产出 finalized audio span
- 维护
-
RealtimeSpeakerController- 对 finalized span 做 speaker tracking
- 维护
Speaker01/02/...
-
RegistryMatchController- 在 slot 稳定后映射实名
5.3 speaker tracker 升级方向
当前文件:
app/services/realtime_speaker_tracker.py
建议保留它,但升级为:
- slot centroid
- slot history
- recent speaker cache
- explicit label matching step
- “未知 slot” 与 “实名 slot” 分层
不建议继续只用:
assign(embedding)的一次性聚类结果
5.4 引入真正的 VAD completed-segment flush
当前项目最应该借鉴老项目的部分就是:
pending_bufferflush_completed_segments- 只把已完成语音刷成 final
建议新建模块,例如:
app/services/realtime_endpointing.py
它负责:
- 累积 PCM
- 每隔固定步长跑 VAD
- 判断哪些片段已完成
- 返回
finalized_audio与remaining_audio
5.5 max_duration 的正确角色
建议保留,但只作为:
- buffer 过长
- 长时间不出句
- VAD 失效
时的保底。
不建议让它继续承担:
- 常规切句
- 主要 final 机制
6. 推荐改造路线
阶段 1:先把文本链路拆干净
目标:
partial只显示final segment只由 endpointing 决定
工作项:
- 引入
pending_buffer + VAD flush - 保留现有 websocket 接口字段不变
max_duration降级为兜底机制
阶段 2:speaker 从句级 embedding 升级为在线 tracking
目标:
- 稳定
Speaker01/02/03
工作项:
- 引入 rolling window diarization 或 speaker turn 检测
- 增加 label matching
- 增加 speaker cache
阶段 3:实名映射后移
目标:
- 先稳定 slot
- 再实名
工作项:
- registry match 不再逐句触发
- 改成基于 slot centroid 或稳定窗口触发
阶段 4:partial 稳定策略
目标:
- UI 不再频繁抖动
工作项:
- prefix commit
- suffix freeze
- partial stability score
7. 不建议继续做的事
以下方向收益很低,且会继续增加系统复杂度:
- 继续堆更多
if suspicious - 继续调
speaker_threshold - 继续靠
max_duration修长句 - 继续用句级单 embedding 解决多人会议 speaker
- 继续让
partial直接影响 final
8. 结论
当前项目最需要的不是继续补条件分支,而是把实时会议能力拆成明确的四层:
streaming partialendpointing/final flushonline diarization / speaker trackingregistry match
其中:
vLLM属于第 1 层- 不是第 2、3、4 层的替代品
如果后续继续在当前单文件链路上修修补补,复杂度会继续升高,稳定性仍然不可控。
如果按本报告做分层重构,问题会从“靠猜修 bug”变成“按职责逐层验证”。
9. 参考资料
- vLLM realtime Qwen3-ASR model docs
https://docs.vllm.ai/en/v0.20.1/api/vllm/model_executor/models/qwen3_asr_realtime/ - vLLM Qwen3-ASR recipe
https://docs.vllm.ai/projects/recipes/en/latest/Qwen/Qwen3-ASR.html - diart
https://github.com/juanmc2005/diart - pyannote.audio
https://github.com/pyannote/pyannote-audio - NVIDIA NeMo speaker diarization docs
https://docs.nvidia.com/nemo-framework/user-guide/latest/nemotoolkit/asr/speaker_diarization/models.html - NVIDIA Streaming Sortformer blog
https://developer.nvidia.com/blog/identify-speakers-in-meetings-calls-and-voice-apps-in-real-time-with-nvidia-streaming-sortformer/ - Low-Latency Online Speaker Diarization with Graph-Based Label Generation
https://arxiv.org/abs/2111.13803 - TURN-TO-DIARIZE: Online Speaker Diarization Constrained by Transformer Transducer Speaker Turn Detection
https://resourcecenter.ieee.org/conferences/icassp-2022/spsicassp22vid1565 - Analyzing the Quality and Stability of a Streaming End-to-End On-Device Speech Recognizer
https://arxiv.org/abs/2006.01416 - Improving endpoint detection in end-to-end streaming ASR for conversational speech
https://arxiv.org/abs/2505.17070