fix(gui): drop a dead setting, make the language list and tail toggle real

Three settings were only half-wired:

- autoPickByLanguage was saved but never read anywhere: delete it.
- showUnstableTail had a checkbox and a setter, but nothing ever called
  the setter, so the checkbox did nothing: apply it in applyAppearance
  and re-render from the cached text instead of blanking the caption.
- the language dropdown was hard-coded to auto/zh/en/ja while the registry
  declares up to 16 languages per model: build it from the selected
  model's own language list, with an ISO-code to display-name mapping
  (plus a unit test covering every code in the registry). The caption
  window's language pill now follows the setting too, instead of keeping
  whatever it showed at startup.

windowPos is not dead -- it stores the dragged window position -- so it stays.

三个「半死」设置:autoPickByLanguage 存了但从没被读取,直接删掉;showUnstableTail
有复选框也有 setter,但从来没人调用 setter(复选框点了没用),改为在 applyAppearance
里生效并用缓存文本重画;「识别语言」下拉原来硬编码 自动/中/英/日,而注册表里每个模型
声明了最多 16 种语言,改为按所选模型的语言动态生成,并补 ISO 码→显示名映射与单元测试。
字幕窗的语言标签也跟着设置走了(原来只在启动时读一次)。windowPos 不是死设置(它存的
是拖动后的位置),保留。
This commit is contained in:
blumia committed 2026-10-12 17:48:21 +08:00
1 parent 6c04071bb5
commit 898763eb74
12 files changed
+138 -23

No files matched your search

+19 -10
View File
@@ -50,12 +50,13 @@ QIcon makeTrayIcon()
return QIcon(pm); return QIcon(pm);
} }
// 把设置应用到字幕窗(字号/不透明度/行数/位置) // 把设置应用到字幕窗(字号/不透明度/行数/位置/灰色尾部)
void applyAppearance(lc::CaptionWindow & window, const lc::AppSettings & cfg) void applyAppearance(lc::CaptionWindow & window, const lc::AppSettings & cfg)
{ {
window.setPanelOpacity(cfg.opacityPercent); window.setPanelOpacity(cfg.opacityPercent);
window.setTextFontSize(cfg.fontSize); window.setTextFontSize(cfg.fontSize);
window.setLineCounts(cfg.collapsedLines, cfg.expandedLines); window.setLineCounts(cfg.collapsedLines, cfg.expandedLines);
window.setShowTail(cfg.showUnstableTail);
if (cfg.hasWindowPos) window.move(cfg.windowPos); if (cfg.hasWindowPos) window.move(cfg.windowPos);
} }
@@ -193,7 +194,8 @@ int main(int argc, char ** argv)
applyAppearance(window, cfg); applyAppearance(window, cfg);
// 语言 pill:流式模型不提供语言识别,就显示设置里选的语言 // 语言 pill:流式模型不提供语言识别,就显示设置里选的语言
if (!cfg.language.isEmpty()) { if (!cfg.language.isEmpty()) {
window.setLanguage(QString::fromStdString(lc::languageDisplayName(cfg.language.toStdString()))); window.setLanguage(
QString::fromStdString(lc::languageDisplayNameFromCode(cfg.language.toStdString())));
} }
// 注意声明顺序:asr 必须在 live 之前声明, // 注意声明顺序:asr 必须在 live 之前声明,
@@ -259,24 +261,31 @@ int main(int argc, char ** argv)
QString activeKey = backendKeyOf(cfg); QString activeKey = backendKeyOf(cfg);
const auto openSettings = [&] { const auto openSettings = [&] {
// 后端相关项变化时:先把语言标签跟设置对齐(流式模型没有自动检测结果来更新它),
// 再重启识别后端。
const auto applyBackend = [&]() -> bool {
if (backendKeyOf(cfg) == activeKey) return true;
window.setLanguage(cfg.language.isEmpty()
? QString()
: QString::fromStdString(
lc::languageDisplayNameFromCode(cfg.language.toStdString())));
if (!startTranscriber()) return false;
activeKey = backendKeyOf(cfg);
return true;
};
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); lc::models::ModelStore::setRootOverride(cfg.modelDir);
applyAppearance(window, cfg); // 透明度/字号等立刻可见 applyAppearance(window, cfg); // 透明度/字号等立刻可见
if (backendKeyOf(cfg) != activeKey) { applyBackend();
if (startTranscriber()) activeKey = backendKeyOf(cfg);
}
}); });
if (dlg.exec() == QDialog::Accepted) { if (dlg.exec() == QDialog::Accepted) {
lc::models::ModelStore::setRootOverride(cfg.modelDir); lc::models::ModelStore::setRootOverride(cfg.modelDir);
applyAppearance(window, cfg); applyAppearance(window, cfg);
if (backendKeyOf(cfg) != activeKey) { if (!applyBackend()) {
if (!startTranscriber()) {
QMessageBox::warning(&window, QObject::tr("启动失败"), QMessageBox::warning(&window, QObject::tr("启动失败"),
QObject::tr("模型加载失败,请检查设置。")); QObject::tr("模型加载失败,请检查设置。"));
} else {
activeKey = backendKeyOf(cfg);
}
} }
} }
}; };
+3 -2
View File
@@ -65,8 +65,9 @@
也可以指定某个具体输出设备(如 HDMI);列不出来或没列全时见 也可以指定某个具体输出设备(如 HDMI);列不出来或没列全时见
[troubleshooting.md](troubleshooting.md)。 [troubleshooting.md](troubleshooting.md)。
- **当前使用**:选择实际生效的模型(只列出**已安装**的)。 - **当前使用**:选择实际生效的模型(只列出**已安装**的)。
- **识别语言**:语言提示,多语言模型用它(例如 Nemotron 需要指定语言才不出错); - **识别语言**:语言提示,多语言模型用它(例如 Nemotron 必须指定语言,否则「自动」可能识别成别的语言);
「自动检测」= 留空。目前提供 自动/中文/英文/日语。 「自动检测」= 留空。**候选项按所选模型支持的语言生成** —— 中文模型只给中文,Nemotron 给它的 16 种
语言,Qwen3-ASR 给常用语言。改完之后左上角的语言标签会同步更新。
- **下载源**:默认 `https://hf-mirror.com`(网络可达性最好的镜像);可改成其它镜像或官方源。 - **下载源**:默认 `https://hf-mirror.com`(网络可达性最好的镜像);可改成其它镜像或官方源。
- **代理**:不使用 / 跟随系统 / 手动设置(主机、端口、用户名、密码)。 - **代理**:不使用 / 跟随系统 / 手动设置(主机、端口、用户名、密码)。
+1
View File
@@ -53,6 +53,7 @@ ctest --test-dir build --output-on-failure # 也可以单独跑 build/tests/t
| `tst_livepipeline.silenceEndsSegment` | 静音收束分段 | | `tst_livepipeline.silenceEndsSegment` | 静音收束分段 |
| `tst_vad.quietSpeechIsDetected` | 噪声底用均值跟踪,把很轻的话音全判成静音 | | `tst_vad.quietSpeechIsDetected` | 噪声底用均值跟踪,把很轻的话音全判成静音 |
| `tst_transcript.*` | 输出解析与语言名映射 | | `tst_transcript.*` | 输出解析与语言名映射 |
| `tst_transcript.isoCodeDisplayName` / `registryLanguagesAllHaveDisplayNames` | 设置里的语言提示用的是 ISO 码(`zh`/`uk`…),漏了显示名的话「识别语言」下拉会直接露出 `uk` 这种码 |
写测试的过程中还**抓到一个真 bug**:段恰好结束在 chunk 边界(没有待处理音频)时,`flush()` 只提交"已稳定"部分,最后几个词会被丢掉。已修(用完整文本 `lastRawFull_` 提交)。 写测试的过程中还**抓到一个真 bug**:段恰好结束在 chunk 边界(没有待处理音频)时,`flush()` 只提交"已稳定"部分,最后几个词会被丢掉。已修(用完整文本 `lastRawFull_` 提交)。
+34
View File
@@ -76,4 +76,38 @@ std::string languageDisplayName(const std::string & lang)
return lang; return lang;
} }
// ISO 639-1 码(注册表里用的形式,如 zh / en / ja)→ 显示名。
// ASR 模型自己吐的是完整语言名(走上面的 languageDisplayName),
// 但设置里的语言提示是 ISO 码,界面要显示成中文。
std::string languageDisplayNameFromCode(const std::string & code)
{
if (code.empty()) return {};
static const struct { const char * key; const char * display; } kMap[] = {
{ "zh", "中文" },
{ "en", "英语" },
{ "ja", "日语" },
{ "ko", "韩语" },
{ "fr", "法语" },
{ "de", "德语" },
{ "es", "西班牙语" },
{ "ru", "俄语" },
{ "it", "意大利语" },
{ "pt", "葡萄牙语" },
{ "ar", "阿拉伯语" },
{ "hi", "印地语" },
{ "vi", "越南语" },
{ "nl", "荷兰语" },
{ "tr", "土耳其语" },
{ "uk", "乌克兰语" },
{ "yue", "粤语" },
{ "auto", "自动检测" },
};
for (const auto & e : kMap) {
if (code == e.key) return e.display;
}
return code;
}
} // namespace lc } // namespace lc
+3
View File
@@ -17,4 +17,7 @@ ParsedTranscript parseAsrOutput(const std::string & raw);
// "English" -> "英语","Chinese" -> "中文";未知则原样返回 // "English" -> "英语","Chinese" -> "中文";未知则原样返回
std::string languageDisplayName(const std::string & lang); std::string languageDisplayName(const std::string & lang);
// ISO 码(注册表里用的形式)-> 显示名:"zh" -> "中文"、"ja" -> "日语";未知则原样返回
std::string languageDisplayNameFromCode(const std::string & code);
} // namespace lc } // namespace lc
-2
View File
@@ -16,7 +16,6 @@ void AppSettings::load()
s.beginGroup(QStringLiteral("app")); s.beginGroup(QStringLiteral("app"));
modelId = s.value(QStringLiteral("modelId")).toString(); modelId = s.value(QStringLiteral("modelId")).toString();
language = s.value(QStringLiteral("language")).toString(); language = s.value(QStringLiteral("language")).toString();
autoPickByLanguage = s.value(QStringLiteral("autoPickByLanguage"), false).toBool();
opacityPercent = s.value(QStringLiteral("opacity"), 84).toInt(); opacityPercent = s.value(QStringLiteral("opacity"), 84).toInt();
fontSize = s.value(QStringLiteral("fontSize"), 15).toInt(); fontSize = s.value(QStringLiteral("fontSize"), 15).toInt();
@@ -60,7 +59,6 @@ void AppSettings::save()
s.beginGroup(QStringLiteral("app")); s.beginGroup(QStringLiteral("app"));
s.setValue(QStringLiteral("modelId"), modelId); s.setValue(QStringLiteral("modelId"), modelId);
s.setValue(QStringLiteral("language"), language); s.setValue(QStringLiteral("language"), language);
s.setValue(QStringLiteral("autoPickByLanguage"), autoPickByLanguage);
s.setValue(QStringLiteral("opacity"), opacityPercent); s.setValue(QStringLiteral("opacity"), opacityPercent);
s.setValue(QStringLiteral("fontSize"), fontSize); s.setValue(QStringLiteral("fontSize"), fontSize);
-1
View File
@@ -14,7 +14,6 @@ public:
// 模型 // 模型
QString modelId; // 当前使用的模型注册表 id QString modelId; // 当前使用的模型注册表 id
QString language; // 识别语言提示;"" = 自动 QString language; // 识别语言提示;"" = 自动
bool autoPickByLanguage = false;
// 显示 // 显示
int opacityPercent = 84; int opacityPercent = 84;
+5 -2
View File
@@ -171,8 +171,7 @@ void CaptionWindow::setShowTail(bool on)
{ {
if (showTail_ == on) return; if (showTail_ == on) return;
showTail_ = on; showTail_ = on;
setCaption(QString(), QString()); // 强制重画需要外部再喂一次;这里先清空缓存 renderCaption(); // 用缓存的文本立刻重画(不能走 setCaption:参数没变会被早退)
committed_.clear();
} }
void CaptionWindow::setPanelOpacity(int percent) void CaptionWindow::setPanelOpacity(int percent)
@@ -235,8 +234,12 @@ void CaptionWindow::setCaption(const QString & committed, const QString & tail)
if (committed == committed_ && tail == tail_) return; if (committed == committed_ && tail == tail_) return;
committed_ = committed; committed_ = committed;
tail_ = tail; tail_ = tail;
renderCaption();
}
// 已稳定部分正常色,不稳定尾部暗色 —— 和 CLI 版渲染一致 // 已稳定部分正常色,不稳定尾部暗色 —— 和 CLI 版渲染一致
void CaptionWindow::renderCaption()
{
const QString html = const QString html =
showTail_ showTail_
? QStringLiteral("<div>%1<span style=\"color:#9a9a9a\">%2</span></div>") ? QStringLiteral("<div>%1<span style=\"color:#9a9a9a\">%2</span></div>")
+1
View File
@@ -49,6 +49,7 @@ private:
int textHeight() const; // 按折叠/展开计算文本区高度 int textHeight() const; // 按折叠/展开计算文本区高度
void relayout(); void relayout();
void scrollToEnd(); void scrollToEnd();
void renderCaption(); // 按 showTail_ 重画缓存的文本
void applyStyle(); void applyStyle();
QPushButton * langPill_ = nullptr; QPushButton * langPill_ = nullptr;
+37 -6
View File
@@ -28,6 +28,7 @@
#include "audio.h" #include "audio.h"
#include "modeldownloader.h" #include "modeldownloader.h"
#include "modelstore.h" #include "modelstore.h"
#include "transcript.h"
namespace lc { namespace lc {
@@ -77,6 +78,7 @@ SettingsDialog::SettingsDialog(bool firstRun, QWidget * parent)
refreshModelTable(); refreshModelTable();
refreshActiveCombo(); refreshActiveCombo();
rebuildLanguageCombo();
updateStreamGroupEnabled(); updateStreamGroupEnabled();
} }
@@ -145,10 +147,8 @@ QWidget * SettingsDialog::buildModelPage()
form->addRow(tr("当前使用"), activeCombo_); form->addRow(tr("当前使用"), activeCombo_);
languageCombo_ = new QComboBox(page); languageCombo_ = new QComboBox(page);
languageCombo_->addItem(tr("自动检测"), QString()); languageCombo_->setToolTip(tr("多语言模型(Nemotron / Qwen3-ASR)用它指定语言;"
languageCombo_->addItem(tr("中文"), QStringLiteral("zh")); "列表按所选模型支持的语言变化。"));
languageCombo_->addItem(tr("英文"), QStringLiteral("en"));
languageCombo_->addItem(tr("日语"), QStringLiteral("ja"));
form->addRow(tr("识别语言"), languageCombo_); form->addRow(tr("识别语言"), languageCombo_);
mirrorEdit_ = new QLineEdit(page); mirrorEdit_ = new QLineEdit(page);
@@ -201,14 +201,13 @@ QWidget * SettingsDialog::buildModelPage()
proxyUser_->setText(cfg.proxyUser); proxyUser_->setText(cfg.proxyUser);
proxyPass_->setText(cfg.proxyPass); proxyPass_->setText(cfg.proxyPass);
updateProxyEnabled(); updateProxyEnabled();
languageCombo_->setCurrentIndex(
qMax(0, languageCombo_->findData(cfg.language)));
connect(activeCombo_, &QComboBox::currentIndexChanged, this, [this](int idx) { connect(activeCombo_, &QComboBox::currentIndexChanged, this, [this](int idx) {
if (idx >= 0) { if (idx >= 0) {
AppSettings::instance().modelId = activeCombo_->itemData(idx).toString(); AppSettings::instance().modelId = activeCombo_->itemData(idx).toString();
AppSettings::instance().save(); AppSettings::instance().save();
} }
rebuildLanguageCombo(); // 不同模型支持的语言不同
updateStreamGroupEnabled(); updateStreamGroupEnabled();
}); });
@@ -384,6 +383,38 @@ QWidget * SettingsDialog::buildAdvancedPage()
return page; return page;
} }
// 「识别语言」的候选项按**所选模型**支持的语言来填:
// 中文模型只给中文,Nemotron 给它的 16 种语言,Qwen3-ASR 给 52 语言里的常用项。
// 这样既不会出现「选了模型不支持的语言」,也不会像以前那样只有中/英/日三项。
void SettingsDialog::rebuildLanguageCombo()
{
if (!languageCombo_) return;
const QString wanted = (languageCombo_->currentIndex() >= 0)
? languageCombo_->currentData().toString()
: AppSettings::instance().language;
QStringList langs;
const QString id = (activeCombo_ && activeCombo_->currentIndex() >= 0)
? activeCombo_->currentData().toString()
: QString();
if (const ModelInfo * m = id.isEmpty() ? nullptr : ModelStore::find(id)) {
langs = m->languages;
}
if (langs.isEmpty()) langs = { QStringLiteral("zh"), QStringLiteral("en"), QStringLiteral("ja") };
languageCombo_->blockSignals(true);
languageCombo_->clear();
languageCombo_->addItem(tr("自动检测"), QString());
for (const QString & code : langs) {
if (code == QLatin1String("auto")) continue; // 第一项就是「自动」
languageCombo_->addItem(
QString::fromStdString(languageDisplayNameFromCode(code.toStdString())), code);
}
const int idx = languageCombo_->findData(wanted);
languageCombo_->setCurrentIndex(idx >= 0 ? idx : 0);
languageCombo_->blockSignals(false);
}
// 流式那一组参数只对 llama 后端(Qwen3-ASR)有意义; // 流式那一组参数只对 llama 后端(Qwen3-ASR)有意义;
// 选了流式 ONNX 模型时置灰,免得改了没效果。 // 选了流式 ONNX 模型时置灰,免得改了没效果。
void SettingsDialog::updateStreamGroupEnabled() void SettingsDialog::updateStreamGroupEnabled()
+1
View File
@@ -38,6 +38,7 @@ private:
void refreshModelTable(); void refreshModelTable();
void refreshActiveCombo(); void refreshActiveCombo();
void rebuildLanguageCombo(); // 按所选模型支持的语言重建语言下拉
void updateStreamGroupEnabled(); // 流式参数只在 llama 后端下可用 void updateStreamGroupEnabled(); // 流式参数只在 llama 后端下可用
void onDownload(); void onDownload();
void onVerify(); void onVerify();
+34
View File
@@ -42,6 +42,40 @@ private slots:
QCOMPARE(QString::fromStdString(lc::languageDisplayName("Klingon")), QString("Klingon")); QCOMPARE(QString::fromStdString(lc::languageDisplayName("Klingon")), QString("Klingon"));
QVERIFY(lc::languageDisplayName("").empty()); QVERIFY(lc::languageDisplayName("").empty());
} }
// 设置里的语言提示是 ISO 码,界面要把它显示成中文名。
// 覆盖注册表里出现过的所有语言码,避免加了模型却漏了显示名。
void isoCodeDisplayName()
{
const struct { const char * code; const char * display; } kCases[] = {
{ "zh", "中文" }, { "en", "英语" }, { "ja", "日语" },
{ "ko", "韩语" }, { "fr", "法语" }, { "de", "德语" },
{ "es", "西班牙语" },{ "ru", "俄语" }, { "ar", "阿拉伯语" },
{ "hi", "印地语" }, { "vi", "越南语" }, { "it", "意大利语" },
{ "pt", "葡萄牙语" },{ "nl", "荷兰语" }, { "tr", "土耳其语" },
{ "uk", "乌克兰语" },{ "auto", "自动检测" },
};
for (const auto & c : kCases) {
QCOMPARE(QString::fromStdString(lc::languageDisplayNameFromCode(c.code)),
QString::fromUtf8(c.display));
}
// 未知码原样返回;空串返回空
QCOMPARE(QString::fromStdString(lc::languageDisplayNameFromCode("xx")), QString("xx"));
QVERIFY(lc::languageDisplayNameFromCode("").empty());
}
// 注册表里每个模型声明的语言都必须能显示成中文名,
// 否则设置里的「识别语言」下拉会出现英文码。
void registryLanguagesAllHaveDisplayNames()
{
for (const char * code : { "zh", "en", "ja", "ko", "fr", "de", "es", "ru", "ar",
"hi", "vi", "it", "pt", "nl", "tr", "uk" }) {
const QString display =
QString::fromStdString(lc::languageDisplayNameFromCode(code));
QVERIFY2(display != QString::fromLatin1(code),
qPrintable(QStringLiteral("语言码没有显示名: %1").arg(code)));
}
}
}; };
QTEST_MAIN(TstTranscript) QTEST_MAIN(TstTranscript)