The README still listed the settings UI as "todo" and its quick start told users to download models into the working directory; phase 3 still described the caption window's close button as "quit" and used the wrong binary name. Bring the docs up to date and fill the gaps: a GUI guide (first run, settings tabs, tray, download, verify), a full CLI option reference marking the test-only flags, a troubleshooting page, the model registry's size/sha256 fields plus a licence/size table, the RTF measurement caveat (llama-cli 0.19 vs the in-process 0.56), and a note that the streaming-ONNX backend has its own code path. phase0/phase1 experiment docs are marked as historical. 文档与现状对齐:README 的 Phase 4 从「待做」改为完成、快速开始不再让人把模型下到当前 目录;phase3 的 ✕ 说明改为「隐藏到托盘」、用法改用 live-caption;新增 GUI/CLI/排错三份 文档;asr-backends 补上注册表的 size/sha256 字段与模型许可/体积表;phase0 补上 RTF 口径 说明(llama-cli 的 0.19 与程序内的 0.56);phase2 补上「流式 ONNX 走另一条分支」的说明。
6.1 KiB
6.1 KiB
Phase 3:Qt 悬浮字幕窗
状态:完成。悬浮窗可用,已接入实时采集 + 流式转写。 (窗口之外的交互 —— 托盘、设置对话框、应用内下载 —— 属于 Phase 4,见 docs/gui.md。本文记录 Phase 3 当时的形态,并在末尾列出后续变化。)
已实现
- 悬浮窗:无边框、半透明深色圆角、始终置顶、不接受焦点(
Qt::FramelessWindowHint | WindowStaysOnTopHint | Tool | WindowDoesNotAcceptFocus),可拖动。 - 折叠 / 展开:折叠两行、展开八行,底部箭头切换(折叠态
⌄、展开态⌃)。 行数可在设置里改(Phase 4)。 - 语言标签:左上角圆角 pill,显示识别到的语言(英语 / 中文 …);点击发
languageClicked—— Phase 4 起就是用它打开设置对话框。 - 稳定/不稳定区分:已稳定文本白色,不稳定尾部灰色 —— 与 CLI 版一致,也是 Chrome 那种"字长出来"的观感。 灰色尾部可以用设置里的「用灰色显示尚未稳定的尾部」关掉(Phase 4)。
- 背景:黑色 + 可调不透明度(不是全透明;纯黑底才能保证任何桌面背景下文字都可读)。
默认 84%,范围限制 20~100,设置界面里是可拖的滑块(Phase 4),
--opacity/setPanelOpacity()也可调。 - 关闭:右上角
✕。Phase 4 起它的行为是隐藏到托盘(否则关掉就没法再打开了); 只有在--no-tray或托盘不可用时才真正退出。 - 实时链路:采集默认输出的 monitor → 流式转写 → 信号推给窗口,全部在主线程外的工作线程里跑。
实时分段
Phase 2 的结论是段不能无限长(解码器对 KV 的注意力随总长增长)。Phase 3 用静音检测决定段边界:
- 能量 VAD(噪声底用"快降慢升"的最小值跟踪,见 Phase 1 的教训);
- 检测到语音才开始把音频送进引擎,并保留 250ms 前导避免吃掉词头;
- 静音期间完全不送音频:早期版本会把静音也喂进模型,模型对静音产生了幻觉(英文语音前冒出一个
嗯。); - 静音超过 0.8s 就收束这一段(提交文本、清空 KV),下一段重新开始。
--stream-segment(默认 30s)仍作为兜底上界,设置界面对应「高级 → 单段最长时长」。
代码结构
src/
├─ captionwindow.{h,cpp} # 悬浮窗
├─ livetranscriber.{h,cpp} # 采集 + VAD + 分段 + 流式引擎(QObject)
├─ streamingasr.{h,cpp} # 流式策略(前缀回滚、切片、分段)
├─ qwen3asr.{h,cpp} # 进程内推理(libllama + libmtmd)
├─ audio.{h,cpp} # WAV 读取 + monitor 采集 + 重采样
└─ transcript.{h,cpp} # 输出解析与语言名映射
目录后来重排过:
captionwindow/livetranscriber现在在src/ui/, 其余在src/core/,模型管理独立成src/models/。见 README 的代码结构。
过程中踩的坑
- 静音幻觉:第一版把采集到的静音也喂给了模型,模型对静音产生幻觉,英文语音前面冒出一个
嗯。。改为静音期间完全不送音频。 - 自定义 QWidget 子类的样式表背景不生效:
CaptionWindow { background-color: rgba(0,0,0,215) }这条规则在真实渲染时根本没画,面板全透明,只有文字和按钮浮在桌面上。原因是 Qt 不会为自定义QWidget子类绘制样式表背景(需要WA_StyledBackground,或自己画)。改成在paintEvent()里用QPainter::drawRoundedRect画底板 —— 这也是WA_TranslucentBackground顶层窗口的推荐做法。 - 验证方法骗了自己:
QWidget::grab()的产物当时是合成到深灰底色上看的,「黑色 + 84%」和「全透明」在深色背景上几乎看不出区别,于是把第 2 条盖住了。改成直接检查像素 alpha(面板中央应为(0,0,0,≈214)、圆角外应为(0,0,0,0)),并合成到亮色背景上复核。 - DPI 警告不关本程序的事:
setHighDpiScaleFactorRoundingPolicy must be called before creating the QGuiApplication instance连 5 行的空白 Qt 程序也会打印 —— 是 Deepin 会话环境设了QT_SCALE_FACTOR_ROUNDING_POLICY导致的。
用法
# 悬浮窗程序(图形界面,Phase 4 起带托盘与设置)
./build/live-caption
# 调整背景不透明度(20-100)—— 这是 GUI 程序的选项
./build/live-caption --opacity 60
# 无窗口、纯命令行实时(便于在无显示环境调试)
./build/live-caption-cli --asr-backend sherpa --asr-model zipformer-zh-2025 --cli-live
# 自截图(验证渲染,不依赖外部截图工具);会依次截折叠态与展开态
./build/live-caption --asr-backend sherpa --asr-model zipformer-zh-2025 \
--no-tray --screenshot out.png --screenshot-delay 12
自截图产物带 alpha 通道;要判断不透明度是否正确,直接看像素 alpha,不要靠肉眼看合成效果。 所有选项见 docs/cli.md。
Phase 3 之后的变化(Phase 4)
- ✕ 从「退出」改成「隐藏到托盘」,并加了托盘图标与菜单;
- 语言标签点击 → 打开设置对话框(模型下载/选择、语言、显示、采集设备、下载源与代理、校验);
- 背景不透明度、字号、行数、灰色尾部、窗口位置全部进了设置界面并持久化(
QSettings); - 采集设备可选(原来固定跟随启动时的默认输出,切到 HDMI 不会跟着变);
- 细节见 docs/gui.md。
已知限制 / 后续
- X11 下置顶与定位都正常;Wayland 下需要 layer-shell,属于 Phase 5(见 docs/troubleshooting.md)。
- 界面尚未做 HiDPI 之外的适配(当前主屏宽度 72%、最大 900 逻辑像素)。



