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:
1 parent
4ed4e23282
commit
736ec14d5b
32 files changed
+1502
-655
No files matched your search
@@ -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"
|
||||
Reference in new issue
Block a user