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 边界时 只提交了稳定部分,最后几个词被丢掉,已修。
84 lines
2.4 KiB
C++
84 lines
2.4 KiB
C++
#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"
|