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 验证文档。
74 lines
3.5 KiB
Markdown
74 lines
3.5 KiB
Markdown
# 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))。
|