Add a C++/Qt6/CMake CLI prototype that captures the default sink monitor through libpulse and transcribes it in-process with Qwen3-ASR via libllama/libmtmd. Add phase 0/1 verification docs. 新增实时字幕命令行原型:libpulse 采集默认输出 monitor,进程内用 libllama/libmtmd 跑 Qwen3-ASR 推理。附阶段 0/1 验证文档。
3.3 KiB
3.3 KiB
Phase 2 计划:原生流式解码
目标:把「窗口重转写 + 事后取最长公共前缀」换成模型官方的流式解码 —— 前缀回滚(prefix rollback),消除 Phase 1 里「窗口在词中间被切断时模型自己编结尾」的问题。
实现方式:在 libllama / libmtmd 上自己实现(只保留一个推理运行时,复用已有的 CMake、模型管理、采集与解析代码)。
参考实现:antirez/qwen-asr(纯 C,已实现 --stream 的前缀回滚与滑窗),其 MODEL.md 逐行记录了官方 Python 实现 qwen_asr/inference/qwen3_asr.py 的流式算法,用作对齐参数的对照。
为什么不需要 vLLM
官方文档写「streaming 只支持 vLLM 后端」,容易误解为依赖 vLLM 特性。实际上流式只是一套解码策略:
- 音频按 2s 累积;
- 每次触发时把已累积的全部音频重新过一遍编码器(O(n²),被 2s 节奏限制住);
- 解码器 prompt 里带上**上一轮转写文本(去掉最后 5 个 token)**作为 assistant 前缀,从那里继续生成;
- 前
unfixed_chunk_num个 chunk 不带前缀(冷启动); - 输出 = 前缀 + 新生成部分。
参数(官方代码默认值)
| 参数 | 值 | 说明 |
|---|---|---|
chunk_size_sec |
2.0 s | 音频累积粒度 |
unfixed_token_num |
5 | 前缀回滚:丢掉最后 5 个 token 再续写 |
unfixed_chunk_num |
2 | 冷启动不开前缀;论文写 4,取保守值 |
| 滑窗上限 | ~16 s | 丢弃「文本已提交」对应的音频,把成本从 O(n²) 压到 O(n·窗口) |
与 Phase 1 原型的差距
| 已有 | 缺 |
|---|---|
| 2s 节奏的窗口重转写 | 把「上一轮文本 − 最后 5 token」拼进 assistant 前缀 |
| mtmd tokenize + eval 的完整路径 | 按 token 而非字符回滚(需跟踪生成的 token 序列) |
| 语言识别、输出解析、稳定前缀显示 | 滑窗:丢弃已提交文本对应的音频 |
| monitor 采集 + 重采样 | 用「已提交 token 数」定义稳定边界,替换事后取交集 |
注意:Phase 1 的「字符最长公共前缀」是错的替代品,官方的定义是「丢最后 5 个 token」。
顺带简化:去掉 VAD
官方明确不使用 VAD(模型自己处理静音;只有非流式长音频才用「在切点 ±5s 内找 100ms 最低能量点」切段)。流式模式下可以整个删掉能量 VAD,少一个需要调参的组件。
Spike(先验证,再改产品代码)
- 效果:同一段音频,对比「离线转写」与「带前缀回喂的流式」输出,确认流式结果连贯、不再编结尾。
- 延迟:实测 2s 节奏下的单步耗时,重点看窗口涨到 16s 时是否仍跟得上(每步预算 < 2s)。
- 以上两条通过后,才把它落进
Qwen3Asr,并补一个流式模式的实机测试脚本。
风险
- 成本:每步重编码累积音频,窗口越大越慢;若 16s 窗口跟不上,需改为「复用 KV cache + 只编码新 chunk」,并用
llama_memory_seq_rm做回滚。 - 前缀注入方式:需要确认把文本前缀拼在 chat template 的 assistant 头之后,mtmd 能否正确切分(预期:
[text]<media>[text]三段,前缀属于第三段)。 - 吞吐:前缀越长,每步 prefill 越贵;滑窗是必须的,不是优化项。