# Phase 2:原生流式解码 状态:**完成**。准确率与离线转写一致,单步计算稳定在 2s 预算内。 ## 结论 | 指标 | 结果 | |---|---| | 准确率 | 流式输出与离线转写逐字一致(中/英各自验证) | | 单步计算 | 最慢 1.83s(预算 2.0s,提交粒度 2s) | | 整体 RTF | 约 0.66 | | 实现 | 纯 C++,libllama + libmtmd,无 Python | ## 算法 官方文档说「streaming 只支持 vLLM 后端」,容易误解为依赖 vLLM 特性。实际上流式只是一套**解码策略**(`qwen_asr/inference/qwen3_asr.py`,`antirez/qwen-asr` 的 MODEL.md 有逐行记录): 1. 音频按 2s 累积; 2. 把音频(连同已有的历史)过编码器; 3. 解码器 prompt 里带上**上一轮文本减去最后 `unfixed_token_num` 个 token** 作为 assistant 前缀,从那里继续生成; 4. 前 `unfixed_chunk_num` 个 chunk 不带前缀(冷启动); 5. 输出 = 前缀 + 新生成部分。 参数取代码默认值:`chunk_size_sec=2.0`、`unfixed_token_num=5`、`unfixed_chunk_num=2`。 ## 关键发现:编码必须按 8 秒切片对齐 这是决定准确率的核心。编码器(AuT)按 `n_window_infer = 800` 帧(8s @100Hz)做窗口注意力,并按 100 帧一组做独立卷积。**把音频切得太碎去编码会明显掉准确率**: | 编码粒度 | 结果 | |---|---| | 2s | ❌ `dinner, or dinner,` / `boiled bruised potatoes` / `thick, thick` | | 4s | ⚠️ 一处错(`browned bruised`、`thick, black`) | | **8s** | ✅ 与离线逐字一致 | 所以实现上**按 8s 对齐切片**:排满 8s 的切片固化进 KV 永久复用;尾部不满 8s 的部分每步重编码(临时的,下一步丢弃)。 ## 实现:增量 KV 复用 与「每步重编码整个窗口」(早期做法)不同: - 新音频只编码一次; - 已解码的音频留在 KV cache 里,不再重编码、也不再重新 prefill; - 每步只做:`llama_memory_seq_rm` 回退到音频末尾 → 追加新音频(`mtmd_helper_eval_chunk_single`,它内部处理 M-RoPE 与分批)→ 解码音频之后的 prompt 文本与前缀 → 贪心生成。 因此单步成本与音频总长无关,只与**当前段的长度**有关。 对比(62.6s 英文音频,chunk=2s): | 做法 | 墙钟 | RTF | 最慢单步 | |---|---|---|---| | 每步重编码整个窗口,window=4s | 32.9s | 0.53 | 1.39s(但准确率在窗口滑动时抖动) | | 每步重编码整个窗口,window=16s | 89.0s | 1.42 | 3.93s ❌ | | **增量 KV 复用 + 8s 切片** | 41.2s | 0.66 | **1.89s ✅** | ## 单段时长上限 解码器对 KV 的注意力随总长增长,所以**段不能无限长**: | 单段上限 | RTF | 最慢单步 | 超预算步数 | |---|---|---|---| | 20s | 0.66 | 1.89s | 0/32 ✅ | | 30s | 0.63 | 2.23s | 1/32 ⚠️ | | 不限 | 1.04 | 3.62s | 15/32 ❌ | 真实字幕场景里句子之间本来就有静音,应该由**静音检测**分段(Phase 3 接入实时链路时做)。当前用 `--stream-segment`(默认 30s)作为兜底上界,超过即提交并重置上下文,历史正文会保留并继续拼接。 ## 过程中修掉的问题 1. **拼接错**:`Result::text` 只是新生成的部分,忘了接回前缀 —— 看起来像"算法不工作",其实只是显示拼接错。 2. **缝隙缺空格**:`parseAsrOutput` 的 `trim()` 吃掉了续写开头那个空格(`potatoesand`)。改用未 trim 的 `raw` 拼接。 3. **分段后重复**:多段拼接时 `parseAsrOutput` 用 `rfind` 定位,只保留最后一个语言头之后的内容,导致前面的正文被丢掉。改为逐段剥离头部再拼接正文。 ## 用法 ```bash # 流式转写(增量 + 8s 切片 + 2s 提交) ./build/live-caption-cli --model MODEL --mmproj MMPROJ \ --stream-file audio_16k_mono.wav [--stream-chunk 2] [--stream-segment 30] # 对比用:旧的"重编码整个窗口"实现 ./build/live-caption-cli ... --stream-mode window --stream-window 4 ``` 相关参数:`--stream-chunk`(提交粒度)、`--stream-window`(仅 window 模式)、`--stream-segment`(单段上限)、`--unfixed-tokens`、`--unfixed-chunks`。 ## 遗留 - 实时链路接入时用**静音检测**代替固定 `--stream-segment` 分段。 - 目前每段结束都会重置语言检测;跨段语言切换的表现待观察。 - 边界标点偶有差异(分段处 `—`/`,` 之类),不影响内容。