Three settings were only half-wired: - autoPickByLanguage was saved but never read anywhere: delete it. - showUnstableTail had a checkbox and a setter, but nothing ever called the setter, so the checkbox did nothing: apply it in applyAppearance and re-render from the cached text instead of blanking the caption. - the language dropdown was hard-coded to auto/zh/en/ja while the registry declares up to 16 languages per model: build it from the selected model's own language list, with an ISO-code to display-name mapping (plus a unit test covering every code in the registry). The caption window's language pill now follows the setting too, instead of keeping whatever it showed at startup. windowPos is not dead -- it stores the dragged window position -- so it stays. 三个「半死」设置:autoPickByLanguage 存了但从没被读取,直接删掉;showUnstableTail 有复选框也有 setter,但从来没人调用 setter(复选框点了没用),改为在 applyAppearance 里生效并用缓存文本重画;「识别语言」下拉原来硬编码 自动/中/英/日,而注册表里每个模型 声明了最多 16 种语言,改为按所选模型的语言动态生成,并补 ISO 码→显示名映射与单元测试。 字幕窗的语言标签也跟着设置走了(原来只在启动时读一次)。windowPos 不是死设置(它存的 是拖动后的位置),保留。
5.1 KiB
5.1 KiB
测试
分层
| 目标 | 依赖 | 说明 |
|---|---|---|
livecaption_core |
无 Qt | 全部业务逻辑;单元测试只依赖它 |
livecaption_models |
Qt6::Core / Network | 注册表、安装检查、sha256 校验、应用内下载 |
livecaption_ui |
Qt6::Widgets | 悬浮窗、设置对话框、Qt 适配层 |
live-caption |
ui + models | 悬浮窗程序 |
live-caption-cli |
core + models + Qt6::Core | 命令行程序(不链接 Qt Widgets) |
tst_* |
core + Qt6::Test | 单元测试 |
运行
cmake -S . -B build && cmake --build build -j
ctest --test-dir build --output-on-failure # 也可以单独跑 build/tests/tst_xxx
单元测试不需要模型文件,也不需要显示环境,秒级完成。目前 4 套:
| 测试 | 覆盖 |
|---|---|
tst_streamingasr |
流式策略:拼接、分段、已稳定/不稳定尾部、冷启动前缀 |
tst_livepipeline |
实时管线:静音过滤、不规则 chunk 不丢样本、静音分段 |
tst_vad |
能量 VAD(轻话音的检测) |
tst_transcript |
输出解析与语言名映射 |
为什么能这样测
关键是把推理后端抽象成 IAsr(src/core/iasr.h),测试里换成 tests/fakeasr.h 的 FakeAsr:
- 可以脚本化每一次
streamAppend返回什么文本; - 记录每次拿到的前缀、音频样本数、以及
streamReset的调用次数; dropLastTokens用"空格分词"模拟词表,并保留原有格式。
另外 LivePipeline 刻意不带 Qt 依赖、并且把「送音频」拆成同步的 process(),所以测试可以直接喂音频,不必碰 PulseAudio。
覆盖的回归用例
这些用例对应的都是开发过程中实际踩到的坑:
| 测试 | 对应的坑 |
|---|---|
tst_streamingasr.keepsJoiningSpace |
用 trim() 过的文本拼接,导致续写缺空格(potatoesand) |
tst_streamingasr.segmentsAccumulateWithoutLosingEarlierText |
多段拼接时只保留最后一个语言头之后的内容,前面正文丢失 |
tst_streamingasr.committedDropsUnstableTail |
已提交 / 不稳定尾部的划分 |
tst_streamingasr.flushCommitsEverything |
流结束时尾部被丢掉 |
tst_streamingasr.coldStartHasNoPrefix |
冷启动阶段不应回喂前缀 |
tst_livepipeline.silenceIsNotForwarded |
静音被送进模型导致幻觉(英文语音前冒出 嗯。) |
tst_livepipeline.noSamplesDroppedAcrossIrregularChunks |
工作线程丢样本(余数没接上),造成假静音与截断 |
tst_livepipeline.silenceEndsSegment |
静音收束分段 |
tst_vad.quietSpeechIsDetected |
噪声底用均值跟踪,把很轻的话音全判成静音 |
tst_transcript.* |
输出解析与语言名映射 |
tst_transcript.isoCodeDisplayName / registryLanguagesAllHaveDisplayNames |
设置里的语言提示用的是 ISO 码(zh/uk…),漏了显示名的话「识别语言」下拉会直接露出 uk 这种码 |
写测试的过程中还抓到一个真 bug:段恰好结束在 chunk 边界(没有待处理音频)时,flush() 只提交"已稳定"部分,最后几个词会被丢掉。已修(用完整文本 lastRawFull_ 提交)。
界面 / 集成测试钩子
界面与真实音频链路不做单元测试,但留有可脚本化的入口(全部选项见 cli.md):
# 设置对话框各条退出路径的冒烟测试(当初定位「点确定就退出」的崩溃就是靠它)
for how in reject close accept ok apply; do
./build/live-caption --asr-model zipformer-zh-2025 --no-tray \
--settings-autoclose $how --quit-after 8
echo "$how → 退出码 $?"
done
# 界面渲染自截图(折叠态 + 展开态),不依赖外部截图工具
./build/live-caption --asr-model zipformer-zh-2025 --no-tray \
--screenshot /tmp/win.png --screenshot-delay 6
./build/live-caption --screenshot-settings /tmp/settings.png
# 模型完整性与采集设备(不需要显示环境)
./build/live-caption-cli --verify-model zipformer-zh-2025
./build/live-caption-cli --list-devices
崩溃时用 coredumpctl list / coredumpctl info <PID> 看调用栈。
尚未覆盖
以下几类需要真模型或真实音频设备,属于集成测试,没放进单元测试:
- 8s 编码切片对齐:这是准确率的决定性因素(切 2s 会明显掉准确率),但必须用真模型跑才能验证。
- 增量 KV 复用的单步延迟:需要真模型才能测。
- 采集链路(PulseAudio/PipeWire 的 monitor 源、设备枚举)。
- GUI 渲染的像素级断言(目前只有人工看截图)。
这些目前靠 experiments/ 下的脚本与实机验证(见 docs/phase0..3)。
后续可加
- 给需要模型的集成测试打 CTest label(如
model),默认跳过,用环境变量指模型路径时才跑。 - GUI 的渲染回归:已有的
--screenshot自截图,可以断言面板像素 alpha((0,0,0,214)),避免再次出现"样式表背景对自定义 QWidget 不生效 → 明明想半透明结果全透明"这类问题。 - 给
ModelStore::verify加一个用临时文件构造损坏模型的单元测试(现在只在集成层面手工验证过)。