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 用;不依赖任何命令行参数) // 按注册表里的模型直接构造后端(GUI 用;不依赖任何命令行参数)
inline std::unique_ptr<IAsr> makeAsrForModel(const models::ModelInfo & m, inline std::unique_ptr<IAsr> makeAsrForModel(const models::ModelInfo & m,
const QString & language, const QString & language,
int threads, bool verbose, int threads, int maxTokens, bool verbose,
std::string * err) std::string * err)
{ {
QStringList missing; QStringList missing;
@@ -125,6 +125,7 @@ inline std::unique_ptr<IAsr> makeAsrForModel(const models::ModelInfo & m,
o.modelPath = m.filePath(QStringLiteral("model")).toStdString(); o.modelPath = m.filePath(QStringLiteral("model")).toStdString();
o.mmprojPath = m.filePath(QStringLiteral("mmproj")).toStdString(); o.mmprojPath = m.filePath(QStringLiteral("mmproj")).toStdString();
o.threads = threads; o.threads = threads;
if (maxTokens > 0) o.maxTokens = maxTokens;
o.verbose = verbose; o.verbose = verbose;
auto a = std::make_unique<Qwen3Asr>(); auto a = std::make_unique<Qwen3Asr>();
if (!a->load(o, err)) return nullptr; if (!a->load(o, err)) return nullptr;
+59 -12
View File
@@ -13,7 +13,9 @@
#include <QPainter> #include <QPainter>
#include <QPixmap> #include <QPixmap>
#include <QPushButton> #include <QPushButton>
#include <QStringList>
#include <QSystemTrayIcon> #include <QSystemTrayIcon>
#include <QTabWidget>
#include <QTimer> #include <QTimer>
#include "appcli.h" #include "appcli.h"
@@ -66,6 +68,25 @@ bool hasUsableModel()
return m && ModelStore::isInstalled(*m); 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 } // namespace
int main(int argc, char ** argv) 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("screenshot-delay", "截图前等待秒数", "sec", "6"));
parser.addOption(QCommandLineOption("no-tray", "不创建托盘图标(测试用)")); parser.addOption(QCommandLineOption("no-tray", "不创建托盘图标(测试用)"));
parser.addOption(QCommandLineOption("screenshot-settings", "打开设置对话框截图后退出", "path")); 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("settings-autoclose", "复现用:自动操作设置对话框(reject|close|accept|ok|apply)", "how"));
parser.addOption(QCommandLineOption("quit-after", "运行若干秒后优雅退出(测试退出路径用)", "sec")); parser.addOption(QCommandLineOption("quit-after", "运行若干秒后优雅退出(测试退出路径用)", "sec"));
parser.process(app); 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("asr-model")) cfg.modelId = parser.value("asr-model");
if (parser.isSet("language")) cfg.language = parser.value("language"); if (parser.isSet("language")) cfg.language = parser.value("language");
if (parser.isSet("opacity")) cfg.opacityPercent = parser.value("opacity").toInt(); 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")) { if (parser.isSet("screenshot-settings")) {
const QString path = parser.value("screenshot-settings"); const QString path = parser.value("screenshot-settings");
lc::SettingsDialog dlg(!hasUsableModel()); 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(); dlg.show();
QTimer::singleShot(1200, &dlg, [&dlg, path] { QTimer::singleShot(1200, &dlg, [&dlg, path] {
dlg.grab().save(path); dlg.grab().save(path);
@@ -173,14 +211,24 @@ int main(int argc, char ** argv)
return false; return false;
} }
std::string err; std::string err;
asr = lc::app::makeAsrForModel(*m, cfg.language, cfg.threads, asr = lc::app::makeAsrForModel(*m, cfg.language, cfg.threads, cfg.maxTokens,
parser.isSet("verbose"), &err); cfg.verbose || parser.isSet("verbose"), &err);
if (!asr) { if (!asr) {
qCritical("加载模型失败: %s", err.c_str()); qCritical("加载模型失败: %s", err.c_str());
return false; return false;
} }
// 流式参数:只有 llama 后端(Qwen3-ASR)用得到大部分参数,
// 流式 ONNX 只关心「更新粒度」。
lc::StreamOptions sop; 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) { if (asr->kind() == lc::AsrKind::NativeStreaming) {
sop.chunkSec = (cfg.chunkSec > 0.0) ? cfg.chunkSec : 0.32; sop.chunkSec = (cfg.chunkSec > 0.0) ? cfg.chunkSec : 0.32;
} else if (cfg.chunkSec > 0.0) { } else if (cfg.chunkSec > 0.0) {
@@ -207,28 +255,27 @@ int main(int argc, char ** argv)
qWarning("采集失败: %s", qUtf8Printable(m)); qWarning("采集失败: %s", qUtf8Printable(m));
}); });
// 后端相关项(模型/语言/采集设备)变化才需要重启;外观类改动即时生效 // 后端相关项(模型/语言/采集设备/流式参数)变化才需要重启;外观类改动即时生效
const auto backendKey = [&cfg] { QString activeKey = backendKeyOf(cfg);
return cfg.modelId + QLatin1Char('|') + cfg.language + QLatin1Char('|') + cfg.captureDevice;
};
QString activeKey = backendKey();
const auto openSettings = [&] { const auto openSettings = [&] {
lc::SettingsDialog dlg(false); lc::SettingsDialog dlg(false);
QObject::connect(&dlg, &lc::SettingsDialog::settingsApplied, &window, [&] { QObject::connect(&dlg, &lc::SettingsDialog::settingsApplied, &window, [&] {
lc::models::ModelStore::setRootOverride(cfg.modelDir);
applyAppearance(window, cfg); // 透明度/字号等立刻可见 applyAppearance(window, cfg); // 透明度/字号等立刻可见
if (backendKey() != activeKey) { if (backendKeyOf(cfg) != activeKey) {
if (startTranscriber()) activeKey = backendKey(); if (startTranscriber()) activeKey = backendKeyOf(cfg);
} }
}); });
if (dlg.exec() == QDialog::Accepted) { if (dlg.exec() == QDialog::Accepted) {
lc::models::ModelStore::setRootOverride(cfg.modelDir);
applyAppearance(window, cfg); applyAppearance(window, cfg);
if (backendKey() != activeKey) { if (backendKeyOf(cfg) != activeKey) {
if (!startTranscriber()) { if (!startTranscriber()) {
QMessageBox::warning(&window, QObject::tr("启动失败"), QMessageBox::warning(&window, QObject::tr("启动失败"),
QObject::tr("模型加载失败,请检查设置。")); QObject::tr("模型加载失败,请检查设置。"));
} else { } else {
activeKey = backendKey(); activeKey = backendKeyOf(cfg);
} }
} }
} }
+2
View File
@@ -28,6 +28,7 @@
参数的选择与实测见 [docs/phase2-streaming.md](phase2-streaming.md)。 参数的选择与实测见 [docs/phase2-streaming.md](phase2-streaming.md)。
**这些参数只影响 `llama` 后端**(Qwen3-ASR);`sherpa` 的流式模型只用到「提交粒度」一个参数。 **这些参数只影响 `llama` 后端**(Qwen3-ASR);`sherpa` 的流式模型只用到「提交粒度」一个参数。
同一组参数在 GUI 的「高级 → 流式」里也有(选了流式 ONNX 模型时那一组会置灰)。
| 选项 | 默认 | 说明 | | 选项 | 默认 | 说明 |
|---|---|---| |---|---|---|
@@ -84,6 +85,7 @@
| `--screenshot <path>` | — | 启动后自动截图并退出;同时截折叠态与展开态(`-expanded.png`) | | `--screenshot <path>` | — | 启动后自动截图并退出;同时截折叠态与展开态(`-expanded.png`) |
| `--screenshot-delay <sec>` | `6` | 截图前等待秒数 | | `--screenshot-delay <sec>` | `6` | 截图前等待秒数 |
| `--screenshot-settings <path>` | — | 打开设置对话框、截图后退出 | | `--screenshot-settings <path>` | — | 打开设置对话框、截图后退出 |
| `--settings-tab <tab>` | — | 截图设置对话框时先切到某个页签(`0`–`2` 或 `模型`/`显示`/`高级`) |
| `--settings-autoclose <how>` | — | 自动操作设置对话框:`reject` / `close` / `accept` / `ok` / `apply`(`ok`/`apply` 是真的去点按钮) | | `--settings-autoclose <how>` | — | 自动操作设置对话框:`reject` / `close` / `accept` / `ok` / `apply`(`ok`/`apply` 是真的去点按钮) |
| `--quit-after <sec>` | — | 运行若干秒后优雅退出(测退出路径) | | `--quit-after <sec>` | — | 运行若干秒后优雅退出(测退出路径) |
+29 -3
View File
@@ -47,8 +47,9 @@
| 取消 | 放弃本次改动(已经用「应用」生效过的改动不会被回滚) | | 取消 | 放弃本次改动(已经用「应用」生效过的改动不会被回滚) |
| 确定 | 应用并关闭 | | 确定 | 应用并关闭 |
**哪些改动要重启识别后端?** 只有**模型 / 识别语言 / 采集设备**这三项变化时才会重建后端并重启转写; **哪些改动要重启识别后端?** 只有**模型 / 识别语言 / 采集设备**,以及高级页里的**流式参数、
透明度、字号、行数、灰色尾部这类外观改动是即时生效的,不会打断正在进行的转写。 线程数、模型目录**变化时才会重建后端并重启转写;透明度、字号、行数、灰色尾部这类外观改动是
即时生效的,不会打断正在进行的转写。
### 模型页 ### 模型页
@@ -80,10 +81,31 @@
### 高级页 ### 高级页
**通用**
| 项 | 说明 | | 项 | 说明 |
|---|---| |---|---|
| 更新粒度(秒) | 累积多少秒音频跑一步;「自动」= 由后端决定(流式模型 0.32s) | | 更新粒度(秒) | 累积多少秒音频跑一步;「自动」= 由后端决定(流式模型 0.32s) |
| 线程数 | 推理线程数;**只影响 Qwen3-ASR**,流式模型固定单线程(多线程反而更慢) |
**流式(只影响 Qwen3-ASR)** —— 这一组对应 llama 后端的流式参数,改动会重启识别后端。
选了流式 ONNX 模型(zipformer / X-ASR / Nemotron)时整组会**置灰**,因为那些模型只用「更新粒度」。
| 项 | 对应命令行 | 说明 |
|---|---|---|
| 策略 | `--stream-mode` | `incremental`(增量复用,1~2 秒就出字,推荐)/ `append`(追加式,成本平坦但需要大 chunk)/ `window`(早期的对比实现) |
| 滑窗上限(秒) | `--stream-window` | 仅 window 模式;「不限」= 不滑窗 |
| 单段最长时长(秒) | `--stream-segment` | 超过就提交并重置上下文;「不限」= 不设上限 |
| 前缀回滚 token 数 | `--unfixed-tokens` | 回滚丢弃最后 N 个 token —— 它们会显示成灰色的不稳定尾部 |
| 冷启动 chunk 数 | `--unfixed-chunks` | 前 N 个 chunk 不带前缀,避免开头重复 |
| 单段最多生成 token | `--max-tokens` | 单段生成的 token 上限 |
| 线程数 | `--threads` | 只影响 Qwen3-ASR;流式模型固定单线程(多线程反而更慢) |
**调试**
| 项 | 对应命令行 | 说明 |
|---|---|---|
| 输出详细日志 | `--verbose` | 打印 llama.cpp / mtmd 的详细日志(排查加载/推理问题时用) |
| 模型目录 | `--model-dir` | 留空 = 默认目录(占位符会显示实际路径);一般不需要改 |
## 配置文件 ## 配置文件
@@ -101,6 +123,10 @@ windowX=179 windowY=725 ; 拖动后记录的位置
mirrorBase=https://hf-mirror.com mirrorBase=https://hf-mirror.com
proxyMode=0 ; 0=不使用 1=跟随系统 2=手动 proxyMode=0 ; 0=不使用 1=跟随系统 2=手动
chunkSec=0 threads=8 ; 高级 chunkSec=0 threads=8 ; 高级
streamMode=incremental ; 高级:流式参数(只影响 Qwen3-ASR)
streamWindowSec=16 streamSegmentSec=30
unfixedTokens=5 unfixedChunks=2 maxTokens=256
verbose=false modelDir= ; 高级:调试
``` ```
删掉这个文件(或其中某个键)就回到默认值。模型文件在 删掉这个文件(或其中某个键)就回到默认值。模型文件在
+17
View File
@@ -42,6 +42,15 @@ void AppSettings::load()
chunkSec = s.value(QStringLiteral("chunkSec"), 0.0).toDouble(); chunkSec = s.value(QStringLiteral("chunkSec"), 0.0).toDouble();
threads = s.value(QStringLiteral("threads"), 8).toInt(); threads = s.value(QStringLiteral("threads"), 8).toInt();
streamMode = s.value(QStringLiteral("streamMode"), QStringLiteral("incremental")).toString();
streamWindowSec = s.value(QStringLiteral("streamWindowSec"), 16.0).toDouble();
streamSegmentSec = s.value(QStringLiteral("streamSegmentSec"), 30.0).toDouble();
unfixedTokens = s.value(QStringLiteral("unfixedTokens"), 5).toInt();
unfixedChunks = s.value(QStringLiteral("unfixedChunks"), 2).toInt();
maxTokens = s.value(QStringLiteral("maxTokens"), 256).toInt();
verbose = s.value(QStringLiteral("verbose"), false).toBool();
modelDir = s.value(QStringLiteral("modelDir")).toString();
s.endGroup(); s.endGroup();
} }
@@ -74,6 +83,14 @@ void AppSettings::save()
s.setValue(QStringLiteral("chunkSec"), chunkSec); s.setValue(QStringLiteral("chunkSec"), chunkSec);
s.setValue(QStringLiteral("threads"), threads); s.setValue(QStringLiteral("threads"), threads);
s.setValue(QStringLiteral("streamMode"), streamMode);
s.setValue(QStringLiteral("streamWindowSec"), streamWindowSec);
s.setValue(QStringLiteral("streamSegmentSec"), streamSegmentSec);
s.setValue(QStringLiteral("unfixedTokens"), unfixedTokens);
s.setValue(QStringLiteral("unfixedChunks"), unfixedChunks);
s.setValue(QStringLiteral("maxTokens"), maxTokens);
s.setValue(QStringLiteral("verbose"), verbose);
s.setValue(QStringLiteral("modelDir"), modelDir);
s.endGroup(); s.endGroup();
s.sync(); s.sync();
} }
+12
View File
@@ -42,6 +42,18 @@ public:
double chunkSec = 0.0; // 0 = 由后端决定 double chunkSec = 0.0; // 0 = 由后端决定
int threads = 8; int threads = 8;
// 高级:流式参数(只影响 llama 后端的 Qwen3-ASR;流式 ONNX 只用 chunkSec)
QString streamMode = QStringLiteral("incremental"); // incremental | append | window
double streamWindowSec = 16.0; // 仅 window 模式:滑窗上限;0 = 不限
double streamSegmentSec = 30.0; // 单段最长时长;0 = 不限
int unfixedTokens = 5; // 前缀回滚丢弃的 token 数(算作不稳定尾部)
int unfixedChunks = 2; // 冷启动不带前缀的 chunk 数
int maxTokens = 256; // 单段最多生成 token 数
// 高级:调试
bool verbose = false; // 输出 llama.cpp / mtmd 的详细日志
QString modelDir; // 空 = 默认模型存储目录
void load(); void load();
void save(); void save();
+115 -9
View File
@@ -77,6 +77,7 @@ SettingsDialog::SettingsDialog(bool firstRun, QWidget * parent)
refreshModelTable(); refreshModelTable();
refreshActiveCombo(); refreshActiveCombo();
updateStreamGroupEnabled();
} }
SettingsDialog::~SettingsDialog() = default; SettingsDialog::~SettingsDialog() = default;
@@ -208,6 +209,7 @@ QWidget * SettingsDialog::buildModelPage()
AppSettings::instance().modelId = activeCombo_->itemData(idx).toString(); AppSettings::instance().modelId = activeCombo_->itemData(idx).toString();
AppSettings::instance().save(); AppSettings::instance().save();
} }
updateStreamGroupEnabled();
}); });
connect(downloader_, &ModelDownloader::status, this, [this](const QString & m) { connect(downloader_, &ModelDownloader::status, this, [this](const QString & m) {
@@ -282,30 +284,125 @@ QWidget * SettingsDialog::buildDisplayPage()
QWidget * SettingsDialog::buildAdvancedPage() QWidget * SettingsDialog::buildAdvancedPage()
{ {
auto * page = new QWidget(this); auto * page = new QWidget(this);
auto * form = new QFormLayout(page); auto * root = new QVBoxLayout(page);
AppSettings & cfg = AppSettings::instance(); AppSettings & cfg = AppSettings::instance();
// ---- 通用 ----
auto * generalForm = new QFormLayout;
chunkSpin_ = new QDoubleSpinBox(page); chunkSpin_ = new QDoubleSpinBox(page);
chunkSpin_->setRange(0.0, 5.0); chunkSpin_->setRange(0.0, 5.0);
chunkSpin_->setSingleStep(0.1); chunkSpin_->setSingleStep(0.1);
chunkSpin_->setDecimals(2); chunkSpin_->setDecimals(2);
chunkSpin_->setSpecialValueText(tr("自动")); chunkSpin_->setSpecialValueText(tr("自动"));
chunkSpin_->setValue(cfg.chunkSec); chunkSpin_->setValue(cfg.chunkSec);
form->addRow(tr("更新粒度(秒)"), chunkSpin_); chunkSpin_->setToolTip(tr("累积多少秒音频跑一步。流式模型默认 0.32 秒。"));
generalForm->addRow(tr("更新粒度(秒)"), chunkSpin_);
threadsSpin_ = new QSpinBox(page); root->addLayout(generalForm);
// ---- 流式:只影响 llama 后端的 Qwen3-ASR ----
streamGroup_ = new QGroupBox(tr("流式(只影响 Qwen3-ASR)"), page);
auto * streamForm = new QFormLayout(streamGroup_);
streamModeCombo_ = new QComboBox(streamGroup_);
streamModeCombo_->addItem(tr("incremental — 增量复用,1~2 秒就出字(推荐)"),
QStringLiteral("incremental"));
streamModeCombo_->addItem(tr("append — 追加式,成本平坦但需要大 chunk"),
QStringLiteral("append"));
streamModeCombo_->addItem(tr("window — 每次重编码整个窗口(早期的对比实现)"),
QStringLiteral("window"));
{
const int i = streamModeCombo_->findData(cfg.streamMode);
if (i >= 0) streamModeCombo_->setCurrentIndex(i);
}
streamForm->addRow(tr("策略"), streamModeCombo_);
windowSpin_ = new QDoubleSpinBox(streamGroup_);
windowSpin_->setRange(0.0, 120.0);
windowSpin_->setDecimals(1);
windowSpin_->setSpecialValueText(tr("不限"));
windowSpin_->setValue(cfg.streamWindowSec);
windowSpin_->setToolTip(tr("仅 window 模式使用:滑窗上限。"));
streamForm->addRow(tr("滑窗上限(秒)"), windowSpin_);
segmentSpin_ = new QDoubleSpinBox(streamGroup_);
segmentSpin_->setRange(0.0, 300.0);
segmentSpin_->setDecimals(1);
segmentSpin_->setSpecialValueText(tr("不限"));
segmentSpin_->setValue(cfg.streamSegmentSec);
segmentSpin_->setToolTip(tr("超过就提交并重置上下文。真实场景由静音检测分段,这是兜底上界。"));
streamForm->addRow(tr("单段最长时长(秒)"), segmentSpin_);
unfixedTokenSpin_ = new QSpinBox(streamGroup_);
unfixedTokenSpin_->setRange(0, 64);
unfixedTokenSpin_->setValue(cfg.unfixedTokens);
unfixedTokenSpin_->setToolTip(tr("回滚丢弃最后 N 个 token —— 它们会显示成灰色的不稳定尾部。"));
streamForm->addRow(tr("前缀回滚 token 数"), unfixedTokenSpin_);
unfixedChunkSpin_ = new QSpinBox(streamGroup_);
unfixedChunkSpin_->setRange(0, 32);
unfixedChunkSpin_->setValue(cfg.unfixedChunks);
unfixedChunkSpin_->setToolTip(tr("前 N 个 chunk 不带前缀(冷启动),用于避免开头重复。"));
streamForm->addRow(tr("冷启动 chunk 数"), unfixedChunkSpin_);
maxTokenSpin_ = new QSpinBox(streamGroup_);
maxTokenSpin_->setRange(16, 4096);
maxTokenSpin_->setValue(cfg.maxTokens);
maxTokenSpin_->setToolTip(tr("单段最多生成的 token 数。"));
streamForm->addRow(tr("单段最多生成 token"), maxTokenSpin_);
threadsSpin_ = new QSpinBox(streamGroup_);
threadsSpin_->setRange(1, 32); threadsSpin_->setRange(1, 32);
threadsSpin_->setValue(cfg.threads); threadsSpin_->setValue(cfg.threads);
form->addRow(tr("线程数"), threadsSpin_); threadsSpin_->setToolTip(tr("只影响 Qwen3-ASR;流式模型固定单线程,因为这类模型多线程反而更慢。"));
streamForm->addRow(tr("线程数"), threadsSpin_);
auto * note = new QLabel( streamNote_ = new QLabel(streamGroup_);
tr("说明:流式模型(zipformer / X-ASR / Nemotron)会自动使用单线程," streamNote_->setWordWrap(true);
"因为这类模型多线程反而更慢;线程数只影响 Qwen3-ASR。"), page); streamForm->addRow(QString(), streamNote_);
note->setWordWrap(true);
form->addRow(QString(), note); root->addWidget(streamGroup_);
// ---- 调试 ----
auto * debugForm = new QFormLayout;
verboseCheck_ = new QCheckBox(tr("输出详细日志(llama.cpp / mtmd)"), page);
verboseCheck_->setChecked(cfg.verbose);
debugForm->addRow(QString(), verboseCheck_);
modelDirEdit_ = new QLineEdit(page);
modelDirEdit_->setText(cfg.modelDir);
modelDirEdit_->setPlaceholderText(ModelStore::root());
modelDirEdit_->setToolTip(tr("留空 = 默认目录(占位符所示)。一般不需要改。"));
debugForm->addRow(tr("模型目录"), modelDirEdit_);
root->addLayout(debugForm);
root->addStretch(1);
updateStreamGroupEnabled();
return page; return page;
} }
// 流式那一组参数只对 llama 后端(Qwen3-ASR)有意义;
// 选了流式 ONNX 模型时置灰,免得改了没效果。
void SettingsDialog::updateStreamGroupEnabled()
{
if (!streamGroup_) return;
const QString id = (activeCombo_ && activeCombo_->currentIndex() >= 0)
? activeCombo_->currentData().toString()
: QString();
const ModelInfo * m = id.isEmpty() ? nullptr : ModelStore::find(id);
const bool llamaBackend = m ? (m->backend == models::Backend::LlamaCpp) : true;
streamGroup_->setEnabled(llamaBackend);
streamNote_->setText(llamaBackend
? tr("这些参数对应命令行里的 --stream-mode / --stream-window / --stream-segment / "
"--unfixed-tokens / --unfixed-chunks / --max-tokens / --threads。改动会重启识别后端。")
: tr("当前模型是流式 ONNX(zipformer / X-ASR / Nemotron),这些参数不适用;"
"它只用上面那个「更新粒度」。"));
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
void SettingsDialog::refreshModelTable() void SettingsDialog::refreshModelTable()
{ {
@@ -480,6 +577,15 @@ void SettingsDialog::applySettings()
cfg.chunkSec = chunkSpin_->value(); cfg.chunkSec = chunkSpin_->value();
cfg.threads = threadsSpin_->value(); cfg.threads = threadsSpin_->value();
cfg.streamMode = streamModeCombo_->currentData().toString();
cfg.streamWindowSec = windowSpin_->value();
cfg.streamSegmentSec = segmentSpin_->value();
cfg.unfixedTokens = unfixedTokenSpin_->value();
cfg.unfixedChunks = unfixedChunkSpin_->value();
cfg.maxTokens = maxTokenSpin_->value();
cfg.verbose = verboseCheck_->isChecked();
cfg.modelDir = modelDirEdit_->text().trimmed();
if (activeCombo_->currentIndex() >= 0) { if (activeCombo_->currentIndex() >= 0) {
cfg.modelId = activeCombo_->currentData().toString(); cfg.modelId = activeCombo_->currentData().toString();
} }
+16
View File
@@ -5,6 +5,7 @@
class QComboBox; class QComboBox;
class QCheckBox; class QCheckBox;
class QDoubleSpinBox; class QDoubleSpinBox;
class QGroupBox;
class QLabel; class QLabel;
class QLineEdit; class QLineEdit;
class QProgressBar; class QProgressBar;
@@ -37,6 +38,7 @@ private:
void refreshModelTable(); void refreshModelTable();
void refreshActiveCombo(); void refreshActiveCombo();
void updateStreamGroupEnabled(); // 流式参数只在 llama 后端下可用
void onDownload(); void onDownload();
void onVerify(); void onVerify();
void onDelete(); void onDelete();
@@ -73,6 +75,20 @@ private:
QDoubleSpinBox * chunkSpin_ = nullptr; QDoubleSpinBox * chunkSpin_ = nullptr;
QSpinBox * threadsSpin_ = nullptr; QSpinBox * threadsSpin_ = nullptr;
// 高级:流式参数(只影响 llama 后端)
QGroupBox * streamGroup_ = nullptr;
QLabel * streamNote_ = nullptr;
QComboBox * streamModeCombo_ = nullptr;
QDoubleSpinBox * windowSpin_ = nullptr;
QDoubleSpinBox * segmentSpin_ = nullptr;
QSpinBox * unfixedTokenSpin_ = nullptr;
QSpinBox * unfixedChunkSpin_ = nullptr;
QSpinBox * maxTokenSpin_ = nullptr;
// 高级:调试
QCheckBox * verboseCheck_ = nullptr;
QLineEdit * modelDirEdit_ = nullptr;
models::ModelDownloader * downloader_ = nullptr; models::ModelDownloader * downloader_ = nullptr;
bool firstRun_ = false; bool firstRun_ = false;
}; };