413 lines
9.4 KiB
Markdown
413 lines
9.4 KiB
Markdown
# 实时会议识别改造报告
|
||
|
||
## 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>
|