test/docs/realtime_meeting_refactor_r...

413 lines
9.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 实时会议识别改造报告
## 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. 参考资料
- 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>