refactor: split core/ui/apps targets and add unit tests

Move all business logic into liblivecaption_core (no Qt dependency) and
the widgets into liblivecaption_ui, leaving two executables: the caption
window and the CLI, which no longer links Qt Widgets. Abstract the ASR
backend behind IAsr so the streaming policy and the live pipeline can be
tested with a scripted fake backend instead of a real model.

把全部业务逻辑下沉到不依赖 Qt 的 liblivecaption_core,界面独立成
liblivecaption_ui,拆出悬浮窗与命令行两个可执行程序(CLI 不再链接 Qt
Widgets);并用 IAsr 抽象推理后端,使流式策略与实时管线可脱离真模型
用假实现做单元测试。测试还抓到一个真 bug:段恰好结束在 chunk 边界时
只提交了稳定部分,最后几个词被丢掉,已修。
This commit is contained in:
blumia committed 2026-10-12 13:43:50 +08:00
1 parent 4ed4e23282
commit 736ec14d5b
32 files changed
+1502 -655

No files matched your search

+83
View File
@@ -0,0 +1,83 @@
#include <QtTest>
#include <cmath>
#include <vector>
#include "vad.h"
namespace {
constexpr size_t kFrame = 320; // 20ms @16k
constexpr double kPi = 3.14159265358979323846;
// 生成 amplitude 幅值的 440Hz 正弦,用来模拟"有内容"的音频
std::vector<float> tone(size_t samples, float amplitude)
{
std::vector<float> v(samples);
for (size_t i = 0; i < samples; ++i) {
v[i] = amplitude * (float) std::sin(2.0 * kPi * 440.0 * (double) i / 16000.0);
}
return v;
}
int countSpeech(lc::Vad & vad, const std::vector<float> & audio)
{
int speech = 0;
for (size_t off = 0; off + kFrame <= audio.size(); off += kFrame) {
if (vad.feed(audio.data() + off, kFrame)) ++speech;
}
return speech;
}
} // namespace
// VAD 的回归测试。
//
// 关键教训:噪声底必须用"最小值跟踪"。曾经用均值跟踪,遇到本来就很轻的音频
// (峰值 RMS 只有 0.03 量级的录音)会把阈值抬到语音电平之上,把绝大部分话音
// 判成静音 —— 实时字幕于是每句都被切断。
class TstVad : public QObject {
Q_OBJECT
private slots:
void backgroundStaysSilent()
{
lc::Vad vad;
const auto bg = tone(kFrame * 200, 0.001f); // rms ≈ 0.0007
QCOMPARE(countSpeech(vad, bg), 0);
}
// 轻语音必须被判成语音:这里语音 rms ≈ 0.0028,
// 旧的"均值跟踪 + 0.004 绝对下限"实现会全部漏掉。
void quietSpeechIsDetected()
{
lc::Vad vad;
const auto bg = tone(kFrame * 40, 0.001f); // rms ≈ 0.0007
countSpeech(vad, bg);
const auto sp = tone(kFrame * 100, 0.004f); // rms ≈ 0.0028
const int speech = countSpeech(vad, sp);
QVERIFY2(speech >= 95,
qPrintable(QString("轻语音只被识别出 %1/100 帧").arg(speech)));
}
void loudSpeechIsDetected()
{
lc::Vad vad;
const auto sp = tone(kFrame * 50, 0.2f);
QVERIFY(countSpeech(vad, sp) >= 48);
}
void floorFollowsQuietInput()
{
lc::Vad vad;
const auto bg = tone(kFrame * 200, 0.001f);
countSpeech(vad, bg);
// 噪声底应该已经下探到背景噪声量级,而不是停在初值
QVERIFY2(vad.noiseFloor() < 0.002f,
qPrintable(QString("噪声底仍为 %1").arg(vad.noiseFloor())));
}
};
QTEST_MAIN(TstVad)
#include "tst_vad.moc"