#include #include #include #include "fakeasr.h" #include "streamingasr.h" namespace { std::vector chunk(size_t samples, float value = 0.1f) { return std::vector(samples, value); } QString q(const std::string & s) { return QString::fromStdString(s); } } // namespace // 流式策略的回归测试(用 FakeAsr,不加载真模型)。 class TstStreamingAsr : public QObject { Q_OBJECT private: static lc::StreamOptions baseOptions() { lc::StreamOptions o; o.chunkSec = 1.0; // 1s 一块,方便构造 o.unfixedTokenNum = 1; // 回滚 1 个 token,断言更直观 o.unfixedChunkNum = 0; // 默认就回喂前缀 o.incremental = true; return o; } private slots: // 冷启动阶段(前 unfixedChunkNum 块)不回喂前缀 void coldStartHasNoPrefix() { FakeAsr fake; fake.scripted = {"a b c d", "e f", "g h"}; lc::StreamOptions o = baseOptions(); o.unfixedChunkNum = 2; lc::StreamingAsr stream(fake, o); const auto cb = [](const lc::StreamingAsr::Step &) {}; const auto c = chunk(16000); stream.feed(c.data(), c.size(), cb); stream.feed(c.data(), c.size(), cb); stream.feed(c.data(), c.size(), cb); QCOMPARE(fake.prefixesSeen.size(), size_t(3)); QCOMPARE(q(fake.prefixesSeen[0]), QString("")); QCOMPARE(q(fake.prefixesSeen[1]), QString("")); QVERIFY(!fake.prefixesSeen[2].empty()); } // 续写开头那个空格不能被吃掉。 // 曾经用 trim 过的文本去拼接,导致 "potatoesand" 这种缺空格。 void keepsJoiningSpace() { FakeAsr fake; fake.scripted = {"language EnglishHe hoped there", " would be stew"}; lc::StreamingAsr stream(fake, baseOptions()); std::vector steps; const auto cb = [&steps](const lc::StreamingAsr::Step & s) { steps.push_back(s); }; const auto c = chunk(16000); stream.feed(c.data(), c.size(), cb); stream.feed(c.data(), c.size(), cb); QCOMPARE(steps.size(), size_t(2)); QCOMPARE(q(steps[0].full), QString("He hoped there")); QVERIFY2(q(steps[1].full) == QString("He hoped would be stew"), qPrintable(QString("实际为 \"%1\"").arg(q(steps[1].full)))); } // 已提交文本 = 完整文本去掉最后 unfixedTokenNum 个 token; // 尾部单独作为不稳定部分给出。 void committedDropsUnstableTail() { FakeAsr fake; fake.scripted = {"language EnglishHe hoped there", " would be stew"}; lc::StreamingAsr stream(fake, baseOptions()); std::vector steps; const auto cb = [&steps](const lc::StreamingAsr::Step & s) { steps.push_back(s); }; const auto c = chunk(16000); stream.feed(c.data(), c.size(), cb); stream.feed(c.data(), c.size(), cb); QCOMPARE(q(steps[1].committed), QString("He hoped would be")); QCOMPARE(q(steps[1].tail), QString(" stew")); // 已提交 + 尾部 = 完整文本 QCOMPARE(q(steps[1].committed + steps[1].tail), q(steps[1].full)); } // 流结束时要全部提交,不能把尾部丢掉 void flushCommitsEverything() { FakeAsr fake; fake.scripted = {"language Englishone two three"}; lc::StreamOptions o = baseOptions(); o.chunkSec = 100.0; // 特意设大:让音频留在 pending 里,由 flush 处理 lc::StreamingAsr stream(fake, o); std::vector steps; const auto cb = [&steps](const lc::StreamingAsr::Step & s) { steps.push_back(s); }; const auto c = chunk(16000); stream.feed(c.data(), c.size(), cb); QCOMPARE(steps.size(), size_t(0)); // 还没凑够一块 stream.flush(cb); QCOMPARE(steps.size(), size_t(1)); QCOMPARE(q(steps[0].committed), QString("one two three")); QVERIFY(steps[0].tail.empty()); } // 多段:每段各自的 language 头都要剥掉,前面几段的正文不能丢。 // 曾经用整段拼接后再解析,rfind 只保留最后一个头之后的内容,前面的正文全没了。 void segmentsAccumulateWithoutLosingEarlierText() { FakeAsr fake; fake.scripted = {"language Chineseni hao shi jie", " language English hello there world"}; lc::StreamOptions o = baseOptions(); o.chunkSec = 1.0; lc::StreamingAsr stream(fake, o); const auto cb = [](const lc::StreamingAsr::Step &) {}; const auto c = chunk(16000); stream.feed(c.data(), c.size(), cb); // 第 1 段 stream.flush(cb); // 收束第 1 段 stream.feed(c.data(), c.size(), cb); // 第 2 段 stream.flush(cb); const QString all = q(stream.committedText()); QVERIFY2(all.contains("ni hao"), qPrintable(all)); QVERIFY2(all.contains("hello there world"), qPrintable(all)); } // 追加式:每块音频作为新一轮追加,文本立即定稿,不回喂任何前缀 void appendModeCommitsEachChunkImmediately() { FakeAsr fake; fake.scripted = {"language Englishone two", " language English three four"}; lc::StreamOptions o = baseOptions(); o.appendOnly = true; lc::StreamingAsr stream(fake, o); std::vector steps; const auto cb = [&steps](const lc::StreamingAsr::Step & s) { steps.push_back(s); }; const auto c = chunk(16000); stream.feed(c.data(), c.size(), cb); stream.feed(c.data(), c.size(), cb); QCOMPARE(fake.turnsSeen.size(), size_t(2)); QVERIFY2(fake.prefixesSeen.empty(), "追加式不应该回喂前缀"); QCOMPARE(steps.size(), size_t(2)); QCOMPARE(q(steps[0].committed), QString("one two")); QVERIFY(steps[0].tail.empty()); // 追加式没有"不稳定尾部" QVERIFY(q(steps[1].committed).contains("three four")); } // 同一次 feed 里跨多块也要正确切分 void splitsMultipleChunksInOneFeed() { FakeAsr fake; fake.scripted = {"a b c", "d e f", "g h i"}; lc::StreamOptions o = baseOptions(); lc::StreamingAsr stream(fake, o); int steps = 0; const auto cb = [&steps](const lc::StreamingAsr::Step &) { ++steps; }; const auto c = chunk(16000 * 3); stream.feed(c.data(), c.size(), cb); QCOMPARE(steps, 3); QCOMPARE(fake.samplesSeen.size(), size_t(3)); for (size_t n : fake.samplesSeen) QCOMPARE(n, size_t(16000)); } }; QTEST_MAIN(TstStreamingAsr) #include "tst_streamingasr.moc"