Files
live-caption/docs/phase2-streaming.md
T
blumia 18dbc9c8d3 feat: add real-time caption CLI prototype with Qwen3-ASR
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 验证文档。
2026-10-12 11:40:59 +08:00

3.3 KiB
Raw Blame History

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 特性。实际上流式只是一套解码策略:

  1. 音频按 2s 累积;
  2. 每次触发时把已累积的全部音频重新过一遍编码器(O(n²),被 2s 节奏限制住);
  3. 解码器 prompt 里带上**上一轮转写文本(去掉最后 5 个 token)**作为 assistant 前缀,从那里继续生成;
  4. 前 unfixed_chunk_num 个 chunk 不带前缀(冷启动);
  5. 输出 = 前缀 + 新生成部分。

参数(官方代码默认值)

参数 值 说明
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(先验证,再改产品代码)

  1. 效果:同一段音频,对比「离线转写」与「带前缀回喂的流式」输出,确认流式结果连贯、不再编结尾。
  2. 延迟:实测 2s 节奏下的单步耗时,重点看窗口涨到 16s 时是否仍跟得上(每步预算 < 2s)。
  3. 以上两条通过后,才把它落进 Qwen3Asr,并补一个流式模式的实机测试脚本。

风险

  • 成本:每步重编码累积音频,窗口越大越慢;若 16s 窗口跟不上,需改为「复用 KV cache + 只编码新 chunk」,并用 llama_memory_seq_rm 做回滚。
  • 前缀注入方式:需要确认把文本前缀拼在 chat template 的 assistant 头之后,mtmd 能否正确切分(预期:[text]<media>[text] 三段,前缀属于第三段)。
  • 吞吐:前缀越长,每步 prefill 越贵;滑窗是必须的,不是优化项。