Files
live-caption/tests/tst_livepipeline.cpp
T
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

128 lines
3.9 KiB
C++
Raw 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 <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"