Files
blumia 6c04071bb5 feat(gui): expose the streaming and debug options in the advanced page
These were command-line only. Add them to the settings dialog so the whole
feature set is reachable without arguments: stream mode, window limit,
segment limit, prefix-rollback token count, cold-start chunk count, max
tokens per step, thread count, verbose logging and the model directory. The
streaming group is grouped and greyed out for streaming-ONNX models, which
only use the commit granularity. The backend fingerprint now includes them,
so changing one restarts the recogniser; appearance changes still apply live.

Also add a test-only --settings-tab so a specific page can be screenshotted.

设置「高级」页补上原来只有命令行才有的选项:流式策略/滑窗上限/单段最长/前缀回滚
token 数/冷启动 chunk 数/单段最多生成 token/线程数/详细日志/模型目录。流式那一组
单独成组,选了流式 ONNX 模型时置灰(它们只用「更新粒度」)。这些值纳入「后端指纹」,
改动会重启识别后端;外观类改动仍然即时生效。另加测试用 --settings-tab 以便给单个
页签截图。
2026-10-12 17:36:43 +08:00

215 lines
8.7 KiB
C++
Raw Permalink 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.
#pragma once
#include <cstdio>
#include <memory>
#include <string>
#include <QCommandLineOption>
#include <QCommandLineParser>
#include "iasr.h"
#include "modelstore.h"
#include "streamingasr.h"
#include "qwen3asr.h"
#if defined(LC_HAVE_SHERPA)
#include "sherpaasr.h"
#endif
// 两个可执行程序(GUI / CLI)共用的命令行选项定义与解析。
namespace lc::app {
// 构造命令行选项。
//
// Qt 自己不会把默认值印进 --help(构造函数里传的默认值只作为 value() 的兜底),
// 所以这里在给了默认值时把「(默认 X)」拼到说明后面。
inline QCommandLineOption makeOption(const QString & name, const QString & description,
const QString & valueName = QString(),
const QString & defaultValue = QString())
{
QString desc = description;
if (!defaultValue.isEmpty()) {
desc += QStringLiteral("(默认 %1)").arg(defaultValue);
}
if (valueName.isEmpty()) return QCommandLineOption(name, desc);
return QCommandLineOption(name, desc, valueName, defaultValue);
}
inline void addModelOptions(QCommandLineParser & p)
{
p.addOption(makeOption("model", "主模型 GGUF 路径(架构须为 qwen3vl)", "path"));
p.addOption(makeOption("mmproj", "mmproj GGUF 路径(clip + qwen3a)", "path"));
p.addOption(makeOption("threads", "推理线程数", "n", "8"));
p.addOption(makeOption("max-tokens", "单段最多生成 token 数", "n", "256"));
p.addOption(makeOption("verbose", "输出 llama.cpp/mtmd 的详细日志"));
}
inline void addStreamOptions(QCommandLineParser & p)
{
p.addOption(makeOption("stream-chunk", "流式 chunk / 提交粒度,单位秒", "sec", "2.0"));
p.addOption(makeOption("stream-window", "滑窗上限,单位秒;仅 window 模式,0=不滑窗", "sec", "16"));
p.addOption(makeOption("stream-segment", "单段最长时长,单位秒;0=不限", "sec", "30"));
p.addOption(makeOption("unfixed-tokens", "前缀回滚丢弃的 token 数", "n", "5"));
p.addOption(makeOption("unfixed-chunks", "冷启动不带前缀的 chunk 数", "n", "2"));
p.addOption(makeOption("stream-mode", "流式后端: incremental / append / 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("incremental");
o.appendOnly = p.value("stream-mode") == QLatin1String("append");
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;
}
// ---- 推理后端选择 ----
//
// llama 后端:Qwen3-ASR 的 GGUF 模型(--model / --mmproj)
// sherpa 后端:流式 ONNX 模型,按注册表 id 从模型存储目录取(--asr-model <id>)
inline void addBackendOptions(QCommandLineParser & p)
{
p.addOption(makeOption("asr-backend", "推理后端: llama(Qwen3-ASR GGUF)或 sherpa(流式 ONNX)",
"kind", "llama"));
p.addOption(makeOption("asr-model", "模型注册表里的 id(sherpa 后端用它定位文件)", "id"));
p.addOption(makeOption("model-dir", "覆盖模型存储目录(默认 ~/.local/share/live-caption/models)", "dir"));
p.addOption(makeOption("language", "语言提示,多语言模型用(zh / en / ja …);留空=自动", "lang"));
}
// 按注册表里的模型直接构造后端(GUI 用;不依赖任何命令行参数)
inline std::unique_ptr<IAsr> makeAsrForModel(const models::ModelInfo & m,
const QString & language,
int threads, int maxTokens, bool verbose,
std::string * err)
{
QStringList missing;
if (!models::ModelStore::isInstalled(m, &missing)) {
if (err) {
*err = "模型未安装,缺失: " + missing.join(QStringLiteral(", ")).toStdString();
}
return nullptr;
}
if (m.backend == models::Backend::LlamaCpp) {
AsrOptions o;
o.modelPath = m.filePath(QStringLiteral("model")).toStdString();
o.mmprojPath = m.filePath(QStringLiteral("mmproj")).toStdString();
o.threads = threads;
if (maxTokens > 0) o.maxTokens = maxTokens;
o.verbose = verbose;
auto a = std::make_unique<Qwen3Asr>();
if (!a->load(o, err)) return nullptr;
return a;
}
#if defined(LC_HAVE_SHERPA)
SherpaAsr::Options o;
o.encoder = m.filePath(QStringLiteral("encoder")).toStdString();
o.decoder = m.filePath(QStringLiteral("decoder")).toStdString();
o.joiner = m.filePath(QStringLiteral("joiner")).toStdString();
o.tokens = m.filePath(QStringLiteral("tokens")).toStdString();
o.threads = 1; // 这类模型单线程最快
if (m.languages.size() > 1 && !language.isEmpty()) o.language = language.toStdString();
auto a = std::make_unique<SherpaAsr>();
if (!a->load(o, err)) return nullptr;
return a;
#else
if (err) *err = "编译时未找到 sherpa-onnx";
return nullptr;
#endif
}
// 按命令行选择并加载后端;失败时返回 nullptr 并把原因写进 err
inline std::unique_ptr<IAsr> makeAsr(const QCommandLineParser & p,
const AsrOptions & asrOpt, std::string * err)
{
const QString dir = p.value("model-dir");
if (!dir.isEmpty()) models::ModelStore::setRootOverride(dir);
const QString backend = p.value("asr-backend");
const QString modelId = p.value("asr-model");
const QString lang = p.value("language");
const models::ModelInfo * info =
modelId.isEmpty() ? nullptr : models::ModelStore::find(modelId);
if (!modelId.isEmpty() && !info) {
if (err) *err = "注册表里没有模型: " + modelId.toStdString();
return nullptr;
}
const bool useSherpa = (backend == QLatin1String("sherpa"))
|| (info && info->backend == models::Backend::SherpaOnline);
if (useSherpa) {
#if defined(LC_HAVE_SHERPA)
if (!info) {
if (err) *err = "sherpa 后端需要 --asr-model <id>(见 data/models.json)";
return nullptr;
}
QStringList missing;
if (!models::ModelStore::isInstalled(*info, &missing)) {
if (err) {
*err = "模型未安装,缺失: " + missing.join(QStringLiteral(", ")).toStdString()
+ "\n 可运行: scripts/fetch-models.sh " + info->id.toStdString();
}
return nullptr;
}
SherpaAsr::Options o;
o.encoder = info->filePath(QStringLiteral("encoder")).toStdString();
o.decoder = info->filePath(QStringLiteral("decoder")).toStdString();
o.joiner = info->filePath(QStringLiteral("joiner")).toStdString();
o.tokens = info->filePath(QStringLiteral("tokens")).toStdString();
o.threads = 1; // 这类模型单线程最快(多线程反而被同步开销拖慢)
if (info->languages.size() > 1 && !lang.isEmpty()) {
o.language = lang.toStdString(); // 多语言模型(Nemotron)需要语言提示
}
auto a = std::make_unique<SherpaAsr>();
if (!a->load(o, err)) return nullptr;
return a;
#else
if (err) *err = "编译时未找到 sherpa-onnx(-DSHERPA_ONNX_ROOT=...)";
return nullptr;
#endif
}
auto a = std::make_unique<Qwen3Asr>();
if (asrOpt.modelPath.empty() || asrOpt.mmprojPath.empty()) {
if (err) *err = "llama 后端需要 --model 和 --mmproj(或改用 --asr-backend sherpa --asr-model <id>)";
return nullptr;
}
if (!a->load(asrOpt, err)) return nullptr;
return a;
}
} // namespace lc::app