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 边界时 只提交了稳定部分,最后几个词被丢掉,已修。
128 lines
3.9 KiB
C++
128 lines
3.9 KiB
C++
#include <QtTest>
|
||
|
||
#include <algorithm>
|
||
#include <cmath>
|
||
#include <vector>
|
||
|
||
#include "fakeasr.h"
|
||
#include "livepipeline.h"
|
||
|
||
namespace {
|
||
|
||
constexpr double kPi = 3.14159265358979323846;
|
||
|
||
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;
|
||
}
|
||
|
||
size_t totalSamples(const std::vector<size_t> & v)
|
||
{
|
||
size_t n = 0;
|
||
for (size_t s : v) n += s;
|
||
return n;
|
||
}
|
||
|
||
lc::LivePipelineOptions options()
|
||
{
|
||
lc::LivePipelineOptions o;
|
||
o.stream.chunkSec = 0.5; // 0.5s 一块,测试里能快些触发
|
||
return o;
|
||
}
|
||
|
||
} // namespace
|
||
|
||
// 实时管线的回归测试(用 FakeAsr + 直接喂音频,不碰 PulseAudio)。
|
||
class TstLivePipeline : public QObject {
|
||
Q_OBJECT
|
||
|
||
private slots:
|
||
// 静音不能送进模型。
|
||
// 曾经把采集到的静音也喂了进去,模型对静音产生幻觉(英文语音前冒出一个"嗯。")。
|
||
void silenceIsNotForwarded()
|
||
{
|
||
FakeAsr fake;
|
||
lc::LivePipeline pipeline(fake, options());
|
||
|
||
const auto silence = tone(16000 * 3, 0.001f); // 3s 背景噪声
|
||
pipeline.process(silence.data(), silence.size());
|
||
|
||
QCOMPARE(fake.samplesSeen.size(), size_t(0));
|
||
}
|
||
|
||
// 不丢样本。
|
||
// 曾经每次只处理 floor(可用/320) 帧并丢掉余数,每轮丢约 160 个样本,
|
||
// 造成假静音和句子被截断。
|
||
void noSamplesDroppedAcrossIrregularChunks()
|
||
{
|
||
FakeAsr fake;
|
||
lc::LivePipeline pipeline(fake, options());
|
||
|
||
const auto speech = tone(16000 * 10, 0.1f); // 10s 语音
|
||
const size_t sizes[] = {1000, 7, 3333, 999, 12345, 1};
|
||
|
||
size_t off = 0;
|
||
int i = 0;
|
||
while (off < speech.size()) {
|
||
const size_t n = std::min(sizes[i++ % 6], speech.size() - off);
|
||
pipeline.process(speech.data() + off, n);
|
||
off += n;
|
||
}
|
||
|
||
const size_t forwarded = totalSamples(fake.samplesSeen);
|
||
// 允许剩下的不足一块留在引擎里(< 0.5s),其余都必须送到
|
||
QVERIFY2(forwarded >= 16000 * 9,
|
||
qPrintable(QString("只送到了 %1 个样本(共 %2)")
|
||
.arg(forwarded).arg(speech.size())));
|
||
}
|
||
|
||
// 静音够久要收束这一段(提交文本并重置上下文)
|
||
void silenceEndsSegment()
|
||
{
|
||
FakeAsr fake;
|
||
fake.scripted = {"a b c d e f"};
|
||
|
||
lc::LivePipeline pipeline(fake, options());
|
||
QString lastCommitted;
|
||
pipeline.setCaptionCallback(
|
||
[&lastCommitted](const std::string & committed, const std::string &,
|
||
const std::string &) {
|
||
lastCommitted = QString::fromStdString(committed);
|
||
});
|
||
|
||
const auto speech = tone(16000 * 1, 0.1f);
|
||
pipeline.process(speech.data(), speech.size());
|
||
const int resetsAfterSpeech = fake.resets;
|
||
|
||
const auto silence = tone(16000 * 2, 0.001f);
|
||
pipeline.process(silence.data(), silence.size());
|
||
|
||
QVERIFY2(fake.resets > resetsAfterSpeech, "静音后没有收束这一段");
|
||
QVERIFY(!pipeline.inSegment());
|
||
}
|
||
|
||
// 语音之后继续有语音:不应该被切断成很多段
|
||
void continuousSpeechStaysInOneSegment()
|
||
{
|
||
FakeAsr fake;
|
||
lc::LivePipeline pipeline(fake, options());
|
||
const int resetsAfterConstruct = fake.resets; // 构造 StreamingAsr 时会 reset 一次
|
||
|
||
const auto speech = tone(16000 * 4, 0.1f);
|
||
for (size_t off = 0; off < speech.size(); off += 4000) {
|
||
const size_t n = std::min(size_t(4000), speech.size() - off);
|
||
pipeline.process(speech.data() + off, n);
|
||
}
|
||
|
||
QVERIFY(pipeline.inSegment());
|
||
QCOMPARE(fake.resets, resetsAfterConstruct);
|
||
}
|
||
};
|
||
|
||
QTEST_MAIN(TstLivePipeline)
|
||
#include "tst_livepipeline.moc"
|