Files
live-caption/docs/phase3-caption-window.md
T
blumia 4ed4e23282 feat(ui): add floating caption window with live transcription
Add a frameless always-on-top caption window wired to the live capture
pipeline. Silence-based segmentation gates the audio so the model never
sees silence, and the panel background is painted in paintEvent with a
configurable opacity instead of a style sheet.

新增悬浮字幕窗(无边框、置顶、可折叠/展开),接入实时采集与流式转写。
分段由静音检测驱动,静音期间不送音频,避免模型对静音产生幻觉。底板
改为在 paintEvent 中绘制,不透明度可调。
2026-10-12 13:32:16 +08:00

4.8 KiB
Raw Blame History

Phase 3:Qt 悬浮字幕窗

状态:完成。悬浮窗可用,已接入实时采集 + 流式转写。

折叠态(英文)

展开态(英文)

折叠态(中文)

不透明度 50%

已实现

  • 悬浮窗:无边框、半透明深色圆角、始终置顶、不接受焦点(Qt::FramelessWindowHint | WindowStaysOnTopHint | Tool | WindowDoesNotAcceptFocus),可拖动。
  • 折叠 / 展开:折叠两行、展开八行,底部箭头切换(折叠态 ⌄、展开态 ⌃)。
  • 语言标签:左上角圆角 pill,显示识别到的语言(英语 / 中文 …);点击会发 languageClicked 信号,设置界面留到 Phase 4。
  • 稳定/不稳定区分:已稳定文本白色,不稳定尾部灰色 —— 与 CLI 版一致,也是 Chrome 那种"字长出来"的观感。
  • 背景:黑色 + 可调不透明度(不是全透明;纯黑底才能保证任何桌面背景下文字都可读)。默认 84%,范围限制 20~100,--opacity 或 setPanelOpacity() 可调,Phase 4 接到设置里即可。
  • 关闭:右上角 ✕ 退出。
  • 实时链路:采集默认输出的 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}      # 流式策略(前缀回滚、8s 切片、分段)
├─ qwen3asr.{h,cpp}          # 进程内推理(libllama + libmtmd)
├─ audio.{h,cpp}             # WAV 读取 + monitor 采集 + 重采样
└─ transcript.{h,cpp}        # 输出解析与语言名映射

过程中踩的坑

  1. 静音幻觉:第一版把采集到的静音也喂给了模型,模型对静音产生幻觉,英文语音前面冒出一个 嗯。。改为静音期间完全不送音频。
  2. 自定义 QWidget 子类的样式表背景不生效:CaptionWindow { background-color: rgba(0,0,0,215) } 这条规则在真实渲染时根本没画,面板全透明,只有文字和按钮浮在桌面上。原因是 Qt 不会为自定义 QWidget 子类绘制样式表背景(需要 WA_StyledBackground,或自己画)。改成在 paintEvent() 里用 QPainter::drawRoundedRect 画底板 —— 这也是 WA_TranslucentBackground 顶层窗口的推荐做法。
  3. 验证方法骗了自己:QWidget::grab() 的产物当时是合成到深灰底色上看的,「黑色 + 84%」和「全透明」在深色背景上几乎看不出区别,于是把第 2 条盖住了。改成直接检查像素 alpha(面板中央应为 (0,0,0,≈214)、圆角外应为 (0,0,0,0)),并合成到亮色背景上复核。
  4. DPI 警告不关本程序的事:setHighDpiScaleFactorRoundingPolicy must be called before creating the QGuiApplication instance 连 5 行的空白 Qt 程序也会打印 —— 是 Deepin 会话环境设了 QT_SCALE_FACTOR_ROUNDING_POLICY 导致的。

用法

# 默认就是悬浮窗模式
./build/live-caption-cli --model MODEL --mmproj MMPROJ

# 调整背景不透明度(20-100)
./build/live-caption-cli --model MODEL --mmproj MMPROJ --opacity 60

# 无窗口、纯命令行实时(便于在无显示环境调试)
./build/live-caption-cli --model MODEL --mmproj MMPROJ --cli-live

# 自截图(验证渲染,不依赖外部截图工具);会依次截折叠态与展开态
./build/live-caption-cli --model MODEL --mmproj MMPROJ \
    --screenshot out.png --screenshot-delay 12

自截图产物带 alpha 通道;要判断不透明度是否正确,直接看像素 alpha,不要靠肉眼看合成效果。

已知限制 / 后续

  • X11 下置顶与定位都正常;Wayland 下需要 layer-shell,属于 Phase 5。
  • 语言标签点击暂无设置界面(Phase 4)。
  • 采集跟随的是启动时的默认输出设备;设备切换(例如切到 HDMI)暂未处理。
  • 界面尚未做 HiDPI 之外的适配(当前主屏宽度 72%、最大 900 逻辑像素)。