test/docs/realtime_meeting_refactor_r...

9.4 KiB
Raw Blame History

实时会议识别改造报告

1. 背景

当前 Qwen-Asr 的实时会议链路,已经暴露出 3 类系统性问题:

  1. 长句在 max_duration 或静音点附近容易被截坏,出现残句、半句、尾词漂移。
  2. partial 与 final segment 责任混杂,静音、串音、背景音、测试语种会直接污染最终结果。
  3. 说话人识别仍以“句级单 embedding + 会话聚类”为主,面对插话、背景音、儿童声音、英语/泰语测试时容易抖动。

这些问题并不是单个阈值导致的,而是实时链路的职责分层不清晰。

2. 外部方案调研结论

本次调研覆盖了:

  • vLLM / Qwen3-ASR 官方实时文档
  • diart
  • pyannote.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

建议把它拆成以下内部组件:

  1. PartialStreamController

    • 负责把有声 chunk 喂给流式 ASR
    • 维护 partial 状态
  2. RealtimeEndpointController

    • 维护 pending_buffer
    • 周期性 VAD flush
    • 产出 finalized audio span
  3. RealtimeSpeakerController

    • 对 finalized span 做 speaker tracking
    • 维护 Speaker01/02/...
  4. 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_buffer
  • flush_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. 结论

当前项目最需要的不是继续补条件分支,而是把实时会议能力拆成明确的四层:

  1. streaming partial
  2. endpointing/final flush
  3. online diarization / speaker tracking
  4. registry match

其中:

  • vLLM 属于第 1 层
  • 不是第 2、3、4 层的替代品

如果后续继续在当前单文件链路上修修补补,复杂度会继续升高,稳定性仍然不可控。
如果按本报告做分层重构,问题会从“靠猜修 bug”变成“按职责逐层验证”。

9. 参考资料