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 以便给单个
页签截图。
This commit is contained in:
blumia committed 2026-10-12 17:36:43 +08:00
1 parent d234b9658d
commit 6c04071bb5
8 files changed
+252 -25

No files matched your search

+2 -1
View File
@@ -109,7 +109,7 @@ inline void addBackendOptions(QCommandLineParser & p)
// 按注册表里的模型直接构造后端(GUI 用;不依赖任何命令行参数)
inline std::unique_ptr<IAsr> makeAsrForModel(const models::ModelInfo & m,
const QString & language,
int threads, bool verbose,
int threads, int maxTokens, bool verbose,
std::string * err)
{
QStringList missing;
@@ -125,6 +125,7 @@ inline std::unique_ptr<IAsr> makeAsrForModel(const models::ModelInfo & m,
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;
+59 -12
View File
@@ -13,7 +13,9 @@
#include <QPainter>
#include <QPixmap>
#include <QPushButton>
#include <QStringList>
#include <QSystemTrayIcon>
#include <QTabWidget>
#include <QTimer>
#include "appcli.h"
@@ -66,6 +68,25 @@ bool hasUsableModel()
return m && ModelStore::isInstalled(*m);
}
// 后端相关设置的「指纹」:这些值变了就必须重建后端(外观类改动不需要)。
QString backendKeyOf(const lc::AppSettings & c)
{
return QStringList{ c.modelId,
c.language,
c.captureDevice,
c.modelDir,
c.streamMode,
QString::number(c.chunkSec),
QString::number(c.streamWindowSec),
QString::number(c.streamSegmentSec),
QString::number(c.unfixedTokens),
QString::number(c.unfixedChunks),
QString::number(c.maxTokens),
QString::number(c.threads),
QString::number(c.verbose ? 1 : 0) }
.join(QLatin1Char('|'));
}
} // namespace
int main(int argc, char ** argv)
@@ -95,6 +116,7 @@ int main(int argc, char ** argv)
parser.addOption(QCommandLineOption("screenshot-delay", "截图前等待秒数", "sec", "6"));
parser.addOption(QCommandLineOption("no-tray", "不创建托盘图标(测试用)"));
parser.addOption(QCommandLineOption("screenshot-settings", "打开设置对话框截图后退出", "path"));
parser.addOption(QCommandLineOption("settings-tab", "截图设置对话框时先切到某个页签(0-2 或 模型/显示/高级)", "tab"));
parser.addOption(QCommandLineOption("settings-autoclose", "复现用:自动操作设置对话框(reject|close|accept|ok|apply)", "how"));
parser.addOption(QCommandLineOption("quit-after", "运行若干秒后优雅退出(测试退出路径用)", "sec"));
parser.process(app);
@@ -102,12 +124,28 @@ int main(int argc, char ** argv)
if (parser.isSet("asr-model")) cfg.modelId = parser.value("asr-model");
if (parser.isSet("language")) cfg.language = parser.value("language");
if (parser.isSet("opacity")) cfg.opacityPercent = parser.value("opacity").toInt();
if (parser.isSet("model-dir")) lc::models::ModelStore::setRootOverride(parser.value("model-dir"));
if (parser.isSet("model-dir")) cfg.modelDir = parser.value("model-dir");
if (!cfg.modelDir.isEmpty()) lc::models::ModelStore::setRootOverride(cfg.modelDir);
// 仅用于验证渲染:打开设置对话框截图后退出
if (parser.isSet("screenshot-settings")) {
const QString path = parser.value("screenshot-settings");
lc::SettingsDialog dlg(!hasUsableModel());
// 可选:先切页签,便于给某个页单独截图
const QString tab = parser.value("settings-tab");
if (!tab.isEmpty()) {
if (auto * tw = dlg.findChild<QTabWidget *>()) {
bool numeric = false;
const int n = tab.toInt(&numeric);
if (numeric) {
tw->setCurrentIndex(qBound(0, n, tw->count() - 1));
} else {
for (int i = 0; i < tw->count(); ++i) {
if (tw->tabText(i) == tab) { tw->setCurrentIndex(i); break; }
}
}
}
}
dlg.show();
QTimer::singleShot(1200, &dlg, [&dlg, path] {
dlg.grab().save(path);
@@ -173,14 +211,24 @@ int main(int argc, char ** argv)
return false;
}
std::string err;
asr = lc::app::makeAsrForModel(*m, cfg.language, cfg.threads,
parser.isSet("verbose"), &err);
asr = lc::app::makeAsrForModel(*m, cfg.language, cfg.threads, cfg.maxTokens,
cfg.verbose || parser.isSet("verbose"), &err);
if (!asr) {
qCritical("加载模型失败: %s", err.c_str());
return false;
}
// 流式参数:只有 llama 后端(Qwen3-ASR)用得到大部分参数,
// 流式 ONNX 只关心「更新粒度」。
lc::StreamOptions sop;
sop.windowSec = cfg.streamWindowSec;
sop.maxUtteranceSec = cfg.streamSegmentSec;
sop.unfixedTokenNum = cfg.unfixedTokens;
sop.unfixedChunkNum = cfg.unfixedChunks;
sop.maxTokensPerStep = cfg.maxTokens;
sop.incremental = (cfg.streamMode != QLatin1String("append")
&& cfg.streamMode != QLatin1String("window"));
sop.appendOnly = (cfg.streamMode == QLatin1String("append"));
if (asr->kind() == lc::AsrKind::NativeStreaming) {
sop.chunkSec = (cfg.chunkSec > 0.0) ? cfg.chunkSec : 0.32;
} else if (cfg.chunkSec > 0.0) {
@@ -207,28 +255,27 @@ int main(int argc, char ** argv)
qWarning("采集失败: %s", qUtf8Printable(m));
});
// 后端相关项(模型/语言/采集设备)变化才需要重启;外观类改动即时生效
const auto backendKey = [&cfg] {
return cfg.modelId + QLatin1Char('|') + cfg.language + QLatin1Char('|') + cfg.captureDevice;
};
QString activeKey = backendKey();
// 后端相关项(模型/语言/采集设备/流式参数)变化才需要重启;外观类改动即时生效
QString activeKey = backendKeyOf(cfg);
const auto openSettings = [&] {
lc::SettingsDialog dlg(false);
QObject::connect(&dlg, &lc::SettingsDialog::settingsApplied, &window, [&] {
lc::models::ModelStore::setRootOverride(cfg.modelDir);
applyAppearance(window, cfg); // 透明度/字号等立刻可见
if (backendKey() != activeKey) {
if (startTranscriber()) activeKey = backendKey();
if (backendKeyOf(cfg) != activeKey) {
if (startTranscriber()) activeKey = backendKeyOf(cfg);
}
});
if (dlg.exec() == QDialog::Accepted) {
lc::models::ModelStore::setRootOverride(cfg.modelDir);
applyAppearance(window, cfg);
if (backendKey() != activeKey) {
if (backendKeyOf(cfg) != activeKey) {
if (!startTranscriber()) {
QMessageBox::warning(&window, QObject::tr("启动失败"),
QObject::tr("模型加载失败,请检查设置。"));
} else {
activeKey = backendKey();
activeKey = backendKeyOf(cfg);
}
}
}