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:
1 parent
d234b9658d
commit
6c04071bb5
8 files changed
+252
-25
No files matched your search
@@ -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
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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= ; 高级:调试
|
||||||
```
|
```
|
||||||
|
|
||||||
删掉这个文件(或其中某个键)就回到默认值。模型文件在
|
删掉这个文件(或其中某个键)就回到默认值。模型文件在
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in new issue
Block a user