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,73 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
|
||||
#include <QCommandLineOption>
|
||||
#include <QCommandLineParser>
|
||||
|
||||
#include "qwen3asr.h"
|
||||
#include "streamingasr.h"
|
||||
|
||||
// 两个可执行程序(GUI / CLI)共用的命令行选项定义与解析。
|
||||
namespace lc::app {
|
||||
|
||||
inline void addModelOptions(QCommandLineParser & p)
|
||||
{
|
||||
p.addOption(QCommandLineOption("model", "主模型 GGUF 路径(架构须为 qwen3vl)", "path"));
|
||||
p.addOption(QCommandLineOption("mmproj", "mmproj GGUF 路径(clip + qwen3a)", "path"));
|
||||
p.addOption(QCommandLineOption("threads", "推理线程数", "n", "8"));
|
||||
p.addOption(QCommandLineOption("max-tokens", "单段最多生成 token 数", "n", "256"));
|
||||
p.addOption(QCommandLineOption("verbose", "输出 llama.cpp/mtmd 的详细日志"));
|
||||
}
|
||||
|
||||
inline void addStreamOptions(QCommandLineParser & p)
|
||||
{
|
||||
p.addOption(QCommandLineOption("stream-chunk", "流式 chunk 秒数", "sec", "2.0"));
|
||||
p.addOption(QCommandLineOption("stream-window", "滑窗上限秒数(仅 window 模式)", "sec", "16"));
|
||||
p.addOption(QCommandLineOption("stream-segment", "单段最长秒数(0=不限)", "sec", "30"));
|
||||
p.addOption(QCommandLineOption("unfixed-tokens", "前缀回滚丢弃的 token 数", "n", "5"));
|
||||
p.addOption(QCommandLineOption("unfixed-chunks", "冷启动不带前缀的 chunk 数", "n", "2"));
|
||||
p.addOption(QCommandLineOption("stream-mode", "流式后端: incremental 或 window", "mode",
|
||||
"incremental"));
|
||||
}
|
||||
|
||||
inline AsrOptions asrOptions(const QCommandLineParser & p)
|
||||
{
|
||||
AsrOptions o;
|
||||
o.modelPath = p.value("model").toStdString();
|
||||
o.mmprojPath = p.value("mmproj").toStdString();
|
||||
o.threads = p.value("threads").toInt();
|
||||
o.maxTokens = p.value("max-tokens").toInt();
|
||||
o.verbose = p.isSet("verbose");
|
||||
return o;
|
||||
}
|
||||
|
||||
inline StreamOptions streamOptions(const QCommandLineParser & p)
|
||||
{
|
||||
StreamOptions o;
|
||||
o.chunkSec = p.value("stream-chunk").toDouble();
|
||||
o.windowSec = p.value("stream-window").toDouble();
|
||||
o.maxUtteranceSec = p.value("stream-segment").toDouble();
|
||||
o.unfixedTokenNum = p.value("unfixed-tokens").toInt();
|
||||
o.unfixedChunkNum = p.value("unfixed-chunks").toInt();
|
||||
o.maxTokensPerStep = p.value("max-tokens").toInt();
|
||||
o.incremental = p.value("stream-mode") != QLatin1String("window");
|
||||
return o;
|
||||
}
|
||||
|
||||
inline bool loadModel(Qwen3Asr & asr, const AsrOptions & opt)
|
||||
{
|
||||
if (opt.modelPath.empty() || opt.mmprojPath.empty()) {
|
||||
std::fprintf(stderr, "必须提供 --model 和 --mmproj\n");
|
||||
return false;
|
||||
}
|
||||
std::string err;
|
||||
if (!asr.load(opt, &err)) {
|
||||
std::fprintf(stderr, "加载失败: %s\n", err.c_str());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace lc::app
|
||||
Reference in new issue
Block a user