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,127 @@
|
||||
#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"
|
||||
Reference in new issue
Block a user