Files
live-caption/experiments/phase1/README.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

74 lines
3.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Phase 1:命令行原型
目标:把 `docs/phase0-verification.md` 验证过的链路写成一个真正的 C++/Qt/CMake 项目骨架,并在命令行跑通「采集系统输出 → 实时转写 → 打印」。
UI 悬浮窗留到 Phase 2。
## 目录
```
src/
├─ main.cpp # CLI 入口、VAD、滑窗策略、稳定前缀提交
├─ qwen3asr.{h,cpp}# 进程内 Qwen3-ASR 封装(libllama + libmtmd)
├─ audio.{h,cpp} # WAV 读取 + 默认输出 monitor 采集 + libsamplerate 重采样
└─ transcript.{h,cpp} # 解析 `language X<asr_text>正文`,语言显示名映射
```
## 构建
```bash
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j
```
依赖:Qt6 Core、libpulse、libsamplerate、系统 llama.cpp(`libllama0`/`libllama-dev`,见 Phase 0 报告里的链接注意事项)。
## 运行
```bash
M=/tmp/opencode/models/Qwen3-ASR-0.6B-Q8_0.gguf
MM=/tmp/opencode/models/mmproj-Qwen3-ASR-0.6B-Q8_0.gguf
# 转写单个文件(不需要采集)
./build/live-caption-cli --model $M --mmproj $MM --file test_16k.wav
# 实时字幕(采集默认输出设备的 monitor)
./build/live-caption-cli --model $M --mmproj $MM --threads 8
# 需要看 llama.cpp 日志时加 --verbose
```
实时输出约定:`~` 是预览行(其中稳定前缀为正常色、不稳定尾部为暗色),`=` 是提交行。
## 实测结果
文件模式(16k 单声道):
| 音频 | 结果 |
|---|---|
| LibriSpeech 英文 10.4s | `He hoped there would be stew for dinner—turnips and carrots and bruised potatoes and fat mutton pieces—to be ladled out in thick peppered flour-fatted sauce.` ✅ |
| AISHELL 中文 3.35s | `不排除购房者可以在互联网上下单。` ✅ |
实时模式(播放上述音频到默认输出,经 monitor 采集转写):
- 英文/中文均实时正确,语言标签正确显示「英语」「中文」。
- 每次推理耗时(0.6B Q8_0,8 线程):
| 窗口时长 | 计算耗时 | RTF |
|---|---|---|
| 0.8 s | ≈0.30 s | 0.42 |
| 2.5 s | ≈0.55 s | 0.22 |
| 5.9 s | ≈1.06 s | 0.18 |
| 20 s | ≈3.59 s | 0.18 |
短窗口 RTF 差是因为有固定开销(prompt 处理 + 音频编码 + 分词),窗口越长越划算。
## 过程中发现并修复的两个 bug
1. **丢样本**:工作线程每轮只处理 `floor(可用样本/320)` 帧并丢弃余数,每次唤醒丢约 160 个样本(~20% 音频),造成假静音和句子被截断。修复:跨批次保留残帧。
2. **VAD 噪声底跟错**:原先用均值跟踪噪声底,遇到本身就很轻的音频(AISHELL 峰值 RMS 仅 0.028、中位 0.006)会一路抬升阈值,把 77% 的话音判成静音。修复:改为「快降慢升」的最小值跟踪,阈值 = `max(floor*3, 0.0015)`。
## 已知问题(留给后续)
- **强制切窗会诱发模型编结尾**:窗口在词中间被切断(到达 max window 或静音点)时,模型会给尾部补一个看似合理的结尾(例如 `...mutton pieces too.`)。已在显示层用「稳定前缀提交」缓解(只把跨多次转写一致的前缀当已定文本),但**根治要靠模型原生的流式解码**:按官方论文的做法,用动态注意力窗口 + 只固定到倒数第 4 个 chunk,尾部 token 不提交。
- 目前每段独立转写、每次清空 KV cache,因此段与段之间没有上下文。原生流式(复用 KV cache、滑动删除已提交部分)能同时降低延迟和提升连贯性,是 Phase 2 的核心工作(见 [../../docs/phase2-streaming.md](../../docs/phase2-streaming.md))。