Files
blumia 736ec14d5b 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 边界时
只提交了稳定部分,最后几个词被丢掉,已修。
2026-10-12 13:43:50 +08:00

84 lines
2.4 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#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"