适用工程:
mnn-whisper当前版本重点:Whisper + MNN C++ 推理、decoder KV cache、实时预览 single-pass 优化、视频字幕叠加演示。
1. 项目概述
本工程将 HuggingFace Whisper 模型导出为 MNN 可执行模型,并在 C++ 端完成音频提取、Whisper log-mel 前处理、encoder 推理、decoder 解码、tokenizer 解码和视频实时字幕叠加。
当前优化目标:
- 支持 Whisper decoder KV cache,减少自回归解码阶段的重复计算。
- 支持实时预览
single-pass模式,避免一个外层窗口内再次切块导致重复 ASR。 - 输出实时性能日志,包括
cost_ms、rtf、mode、lag_sec。 - 保留旧版
chunked模式,方便在长窗口或离线批处理场景下回退。
2. 目录与关键文件
text
mnn-whisper/
├── include/
│ ├── asr.hpp # ASR 类接口
│ ├── asrconfig.hpp # 配置读取与模型路径接口
│ ├── tokenizer.hpp # Tokenizer 接口
├── src/
│ ├── asr.cpp # Whisper 前处理、encoder/decoder 推理、解码逻辑
│ ├── tokenizer.cpp # Tokenizer 实现
│ └── asr_demo.cpp # 视频/音频实时演示与字幕叠加
├── asrexport.py # Whisper -> ONNX/MNN 导出脚本
├── export_model.sh # 模型导出命令封装
├── run.sh # 运行示例
└── model_whisper/
├── config.json
├── asr_config.json
├── tokenizer.txt
├── encoder.mnn
├── decoder.mnn
├── decoder_prefill.mnn # KV cache prefill decoder
└── decoder_cache.mnn # KV cache incremental decoder
3. 当前能力
3.1 Whisper MNN 推理流程
text
PCM16 音频
↓
Whisper log-mel frontend
↓
encoder.mnn
↓
decoder / decoder_prefill / decoder_cache
↓
token ids
↓
tokenizer decode
↓
text
3.2 decoder KV cache
当前支持三类 decoder:
| 模型 | 作用 |
|---|---|
decoder.mnn |
旧版 full-history decoder,作为 fallback |
decoder_prefill.mnn |
首次输入完整 prompt,输出 logits + present KV |
decoder_cache.mnn |
后续每步只输入上一个 token + position + past KV,输出 logits + 更新后的 KV |
运行时如果 KV cache 模型不存在或执行失败,会自动回退到 decoder.mnn。
3.3 实时 single-pass 模式
旧逻辑:
text
外层 window=3 秒
↓
内部再按 asr_chunk=1.4 / asr_overlap=0.35 切成多段
↓
每段都完整跑一次 ASR
这会导致 3 秒窗口实际触发多次完整 Whisper 推理,性能开销明显增加。
新逻辑:
text
外层 window=3 秒
↓
single-pass 一次完整 ASR
↓
输出字幕
日志中会显示:
text
mode=single
4. 模型导出
4.1 默认导出 KV cache 版本
bash
bash export_model.sh
等价于:
bash
python asrexport.py \
--path ./whisper-tiny \
--language auto \
--task transcribe \
--no_timestamps
导出完成后,model_whisper/ 下应包含:
text
encoder.mnn
decoder.mnn
decoder_prefill.mnn
decoder_cache.mnn
config.json
asr_config.json
tokenizer.txt
4.2 禁用 KV cache 导出
如果需要回退旧版 decoder 导出:
bash
python asrexport.py \
--path ./whisper-tiny \
--language auto \
--task transcribe \
--no_timestamps \
--disable_kv_cache
5. 编译工程
建议每次替换核心源码后 clean build:
bash
cd /home/panguofeng/project/mnn-whisper
rm -rf build
mkdir build
cd build
cmake ..
make -j$(nproc)
编译产物:
text
build/asr_demo
6. 推荐实时运行命令
6.1 实时预览推荐配置
bash
./build/asr_demo model_whisper/config.json ../mnn-asr/36401905752-1-192.mp4 \
/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc \
--show \
--window=3 \
--stride=0.6 \
--ahead=0.8 \
--hold=0.35 \
--speed=1.0 \
--recent-chunks=3 \
--chunks-per-line=2 \
--subtitle-width=24 \
--rt-single-pass \
--log-level=quiet
6.2 CPU 压力较大时
降低刷新频率:
bash
--stride=0.8
或:
bash
--stride=1.0
6.3 字幕断句较多时
适当加长窗口:
bash
--window=4 --stride=0.8 --ahead=1.0
只要日志中的 rtf < 1.0,整体就可以跟上实时播放。
7. 参数说明
| 参数 | 推荐值 | 说明 |
|---|---|---|
--window |
3 |
ASR 外层实时滑窗长度,单位秒 |
--stride |
0.6 |
ASR 刷新间隔,越小刷新越频繁,CPU 压力越大 |
--ahead |
0.8 |
离线视频模式下预读未来音频,用于抵消识别耗时 |
--hold |
0.35 |
语句结束后字幕保留时间 |
--speed |
1.0 |
播放速度,1.0 为原速 |
--recent-chunks |
3 |
预览中保留最近几段识别结果 |
--chunks-per-line |
2 |
每行拼接几个 chunk |
--subtitle-width |
24 |
字幕换行显示宽度 |
--rt-single-pass |
开启 | 实时窗口只跑一次 ASR,推荐 |
--rt-chunked |
关闭 | 回退旧版窗口内二次切块逻辑 |
--log-level |
quiet |
日志级别,实时预览推荐 quiet |
8. 性能日志解释
示例:
text
[RT-ASR] req_end=87.7 window=3 cost_ms=2003.31 rtf=0.667769 mode=single lag_sec=-1.13687e-13
字段含义:
| 字段 | 含义 |
|---|---|
req_end |
当前请求窗口的音频结束时间,单位秒 |
window |
当前 ASR 窗口长度,单位秒 |
cost_ms |
本次 ASR 耗时,单位毫秒 |
rtf |
Real-Time Factor,cost_ms / (window * 1000) |
mode |
single 表示 single-pass;chunked 表示窗口内二次切块 |
lag_sec |
相对播放时间的延迟估计 |
判断标准:
text
rtf < 1.0 表示可以实时
rtf = 1.0 表示刚好实时
rtf > 1.0 表示识别速度跟不上播放
当前 single-pass 实测日志:
text
[RT-ASR] req_end=70.9 window=3 cost_ms=2369.93 rtf=0.789976 mode=single lag_sec=...
[RT-ASR] req_end=73.3 window=3 cost_ms=2807.13 rtf=0.935711 mode=single lag_sec=...
[RT-ASR] req_end=76.3 window=3 cost_ms=1496.48 rtf=0.498827 mode=single lag_sec=...
说明当前 3 秒窗口已经能实时跑。
9. KV cache 与 single-pass 的关系
9.1 KV cache 优化什么
decoder KV cache 优化的是 Whisper decoder 自回归生成阶段。
旧方式:
text
step 0: 输入完整 tokens[0]
step 1: 输入完整 tokens[0:1]
step 2: 输入完整 tokens[0:2]
...
step N: 输入完整 tokens[0:N]
KV cache 方式:
text
prefill: 输入完整 prompt,生成初始 KV
step 1: 只输入上一个 token + past KV
step 2: 只输入上一个 token + past KV
...
它可以减少 decoder 重复计算。
9.2 single-pass 优化什么
single-pass 优化的是实时 demo 外层调度逻辑,避免一个 3 秒窗口被内部再次切成多段,导致多次完整 ASR。
旧实时逻辑的问题:
text
window=3
asr_chunk=1.4
asr_overlap=0.35
会导致一次 3 秒窗口里跑多次完整 Whisper。
single-pass 后:
text
window=3
只跑一次 ASR
所以 single-pass 对端到端实时性能提升更明显。
10. 常见问题
10.1 日志没有 rtf 和 mode=single
说明运行的仍是旧版二进制,或源码没有覆盖成功。
正确日志应包含:
text
rtf=...
mode=single
处理方式:
bash
cd /home/panguofeng/project/mnn-whisper
rm -rf build
mkdir build
cd build
cmake ..
make -j$(nproc)
然后重新运行:
bash
./build/asr_demo ...
10.2 出现 mode=chunked
说明启用了旧版窗口内二次切块模式。
检查命令中是否包含:
bash
--rt-chunked
实时预览推荐使用:
bash
--rt-single-pass
10.3 3 秒窗口仍然超过 3 秒
先确认:
- 是否为
mode=single - 是否
rtf > 1.0 - 是否 CPU 负载过高
- 是否 stride 太小
可尝试:
bash
--stride=0.8
或:
bash
--stride=1.0
10.4 KV cache 模型加载失败
检查 model_whisper/ 下是否存在:
text
decoder_prefill.mnn
decoder_cache.mnn
如果不存在,重新导出:
bash
bash export_model.sh
如果 MNN 转换后 tensor name 被改写,可能需要根据实际 MNN 输入输出名调整 Module::load() 的 input/output names。
11. 下一步优化方向
当前优化状态:
text
已完成:decoder KV cache
已完成:实时 single-pass,RTF 已低于 1
待优化:dynamic log-mel + dynamic encoder
待优化:streaming encoder / encoder cache
11.1 dynamic log-mel + dynamic encoder
当前 Whisper frontend 仍然容易固定到 30 秒特征长度:
text
[1, 80, 3000]
这意味着即使输入只有 3 秒,也可能按 30 秒路径处理。
下一步可以改成:
text
3 秒输入 → 约 300 帧 mel
4 秒输入 → 约 400 帧 mel
同时导出 dynamic time-axis encoder,进一步降低 encoder 计算量。
11.2 streaming encoder
更进一步可以实现真正流式 ASR:
text
音频 ring buffer
↓
增量 log-mel
↓
encoder cache / streaming encoder
↓
decoder KV cache
↓
稳定增量字幕
这会比当前滑窗 single-pass 更接近真正的低延迟流式识别。
12. 推荐默认配置
当前推荐实时预览配置:
bash
./build/asr_demo model_whisper/config.json ../mnn-asr/36401905752-1-192.mp4 \
/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc \
--show \
--window=3 \
--stride=0.6 \
--ahead=0.8 \
--hold=0.35 \
--speed=1.0 \
--recent-chunks=3 \
--chunks-per-line=2 \
--subtitle-width=24 \
--rt-single-pass \
--log-level=quiet
判断是否达标:
text
mode=single
rtf < 1.0
cost_ms < window * 1000
满足以上条件即可认为实时预览可用。