diff --git a/apps/gui/main.cpp b/apps/gui/main.cpp index 90f0f7f..2c906df 100644 --- a/apps/gui/main.cpp +++ b/apps/gui/main.cpp @@ -50,12 +50,13 @@ QIcon makeTrayIcon() return QIcon(pm); } -// 把设置应用到字幕窗(字号/不透明度/行数/位置) +// 把设置应用到字幕窗(字号/不透明度/行数/位置/灰色尾部) void applyAppearance(lc::CaptionWindow & window, const lc::AppSettings & cfg) { window.setPanelOpacity(cfg.opacityPercent); window.setTextFontSize(cfg.fontSize); window.setLineCounts(cfg.collapsedLines, cfg.expandedLines); + window.setShowTail(cfg.showUnstableTail); if (cfg.hasWindowPos) window.move(cfg.windowPos); } @@ -193,7 +194,8 @@ int main(int argc, char ** argv) applyAppearance(window, cfg); // 语言 pill:流式模型不提供语言识别,就显示设置里选的语言 if (!cfg.language.isEmpty()) { - window.setLanguage(QString::fromStdString(lc::languageDisplayName(cfg.language.toStdString()))); + window.setLanguage( + QString::fromStdString(lc::languageDisplayNameFromCode(cfg.language.toStdString()))); } // 注意声明顺序:asr 必须在 live 之前声明, @@ -259,24 +261,31 @@ int main(int argc, char ** argv) QString activeKey = backendKeyOf(cfg); 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); QObject::connect(&dlg, &lc::SettingsDialog::settingsApplied, &window, [&] { lc::models::ModelStore::setRootOverride(cfg.modelDir); applyAppearance(window, cfg); // 透明度/字号等立刻可见 - if (backendKeyOf(cfg) != activeKey) { - if (startTranscriber()) activeKey = backendKeyOf(cfg); - } + applyBackend(); }); if (dlg.exec() == QDialog::Accepted) { lc::models::ModelStore::setRootOverride(cfg.modelDir); applyAppearance(window, cfg); - if (backendKeyOf(cfg) != activeKey) { - if (!startTranscriber()) { - QMessageBox::warning(&window, QObject::tr("启动失败"), - QObject::tr("模型加载失败,请检查设置。")); - } else { - activeKey = backendKeyOf(cfg); - } + if (!applyBackend()) { + QMessageBox::warning(&window, QObject::tr("启动失败"), + QObject::tr("模型加载失败,请检查设置。")); } } }; diff --git a/docs/gui.md b/docs/gui.md index 88c605f..19d55e7 100644 --- a/docs/gui.md +++ b/docs/gui.md @@ -65,8 +65,9 @@ 也可以指定某个具体输出设备(如 HDMI);列不出来或没列全时见 [troubleshooting.md](troubleshooting.md)。 - **当前使用**:选择实际生效的模型(只列出**已安装**的)。 -- **识别语言**:语言提示,多语言模型用它(例如 Nemotron 需要指定语言才不出错); - 「自动检测」= 留空。目前提供 自动/中文/英文/日语。 +- **识别语言**:语言提示,多语言模型用它(例如 Nemotron 必须指定语言,否则「自动」可能识别成别的语言); + 「自动检测」= 留空。**候选项按所选模型支持的语言生成** —— 中文模型只给中文,Nemotron 给它的 16 种 + 语言,Qwen3-ASR 给常用语言。改完之后左上角的语言标签会同步更新。 - **下载源**:默认 `https://hf-mirror.com`(网络可达性最好的镜像);可改成其它镜像或官方源。 - **代理**:不使用 / 跟随系统 / 手动设置(主机、端口、用户名、密码)。 diff --git a/docs/testing.md b/docs/testing.md index a5a61df..12dc905 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -53,6 +53,7 @@ ctest --test-dir build --output-on-failure # 也可以单独跑 build/tests/t | `tst_livepipeline.silenceEndsSegment` | 静音收束分段 | | `tst_vad.quietSpeechIsDetected` | 噪声底用均值跟踪,把很轻的话音全判成静音 | | `tst_transcript.*` | 输出解析与语言名映射 | +| `tst_transcript.isoCodeDisplayName` / `registryLanguagesAllHaveDisplayNames` | 设置里的语言提示用的是 ISO 码(`zh`/`uk`…),漏了显示名的话「识别语言」下拉会直接露出 `uk` 这种码 | 写测试的过程中还**抓到一个真 bug**:段恰好结束在 chunk 边界(没有待处理音频)时,`flush()` 只提交"已稳定"部分,最后几个词会被丢掉。已修(用完整文本 `lastRawFull_` 提交)。 diff --git a/src/core/transcript.cpp b/src/core/transcript.cpp index c6f6554..5b417ae 100644 --- a/src/core/transcript.cpp +++ b/src/core/transcript.cpp @@ -76,4 +76,38 @@ std::string languageDisplayName(const std::string & 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 diff --git a/src/core/transcript.h b/src/core/transcript.h index b99435f..7ce01e2 100644 --- a/src/core/transcript.h +++ b/src/core/transcript.h @@ -17,4 +17,7 @@ ParsedTranscript parseAsrOutput(const std::string & raw); // "English" -> "英语","Chinese" -> "中文";未知则原样返回 std::string languageDisplayName(const std::string & lang); +// ISO 码(注册表里用的形式)-> 显示名:"zh" -> "中文"、"ja" -> "日语";未知则原样返回 +std::string languageDisplayNameFromCode(const std::string & code); + } // namespace lc diff --git a/src/ui/appsettings.cpp b/src/ui/appsettings.cpp index 04b8fde..af6bfcd 100644 --- a/src/ui/appsettings.cpp +++ b/src/ui/appsettings.cpp @@ -16,7 +16,6 @@ void AppSettings::load() s.beginGroup(QStringLiteral("app")); modelId = s.value(QStringLiteral("modelId")).toString(); language = s.value(QStringLiteral("language")).toString(); - autoPickByLanguage = s.value(QStringLiteral("autoPickByLanguage"), false).toBool(); opacityPercent = s.value(QStringLiteral("opacity"), 84).toInt(); fontSize = s.value(QStringLiteral("fontSize"), 15).toInt(); @@ -60,7 +59,6 @@ void AppSettings::save() s.beginGroup(QStringLiteral("app")); s.setValue(QStringLiteral("modelId"), modelId); s.setValue(QStringLiteral("language"), language); - s.setValue(QStringLiteral("autoPickByLanguage"), autoPickByLanguage); s.setValue(QStringLiteral("opacity"), opacityPercent); s.setValue(QStringLiteral("fontSize"), fontSize); diff --git a/src/ui/appsettings.h b/src/ui/appsettings.h index 585a639..7c3b714 100644 --- a/src/ui/appsettings.h +++ b/src/ui/appsettings.h @@ -14,7 +14,6 @@ public: // 模型 QString modelId; // 当前使用的模型注册表 id QString language; // 识别语言提示;"" = 自动 - bool autoPickByLanguage = false; // 显示 int opacityPercent = 84; diff --git a/src/ui/captionwindow.cpp b/src/ui/captionwindow.cpp index 135e73a..f82fbae 100644 --- a/src/ui/captionwindow.cpp +++ b/src/ui/captionwindow.cpp @@ -171,8 +171,7 @@ void CaptionWindow::setShowTail(bool on) { if (showTail_ == on) return; showTail_ = on; - setCaption(QString(), QString()); // 强制重画需要外部再喂一次;这里先清空缓存 - committed_.clear(); + renderCaption(); // 用缓存的文本立刻重画(不能走 setCaption:参数没变会被早退) } void CaptionWindow::setPanelOpacity(int percent) @@ -235,8 +234,12 @@ void CaptionWindow::setCaption(const QString & committed, const QString & tail) if (committed == committed_ && tail == tail_) return; committed_ = committed; tail_ = tail; + renderCaption(); +} - // 已稳定部分正常色,不稳定尾部暗色 —— 和 CLI 版渲染一致 +// 已稳定部分正常色,不稳定尾部暗色 —— 和 CLI 版渲染一致 +void CaptionWindow::renderCaption() +{ const QString html = showTail_ ? QStringLiteral("
%1%2
") diff --git a/src/ui/captionwindow.h b/src/ui/captionwindow.h index c5eee50..51c54bf 100644 --- a/src/ui/captionwindow.h +++ b/src/ui/captionwindow.h @@ -49,6 +49,7 @@ private: int textHeight() const; // 按折叠/展开计算文本区高度 void relayout(); void scrollToEnd(); + void renderCaption(); // 按 showTail_ 重画缓存的文本 void applyStyle(); QPushButton * langPill_ = nullptr; diff --git a/src/ui/settingsdialog.cpp b/src/ui/settingsdialog.cpp index 2cab5e0..6e2a26c 100644 --- a/src/ui/settingsdialog.cpp +++ b/src/ui/settingsdialog.cpp @@ -28,6 +28,7 @@ #include "audio.h" #include "modeldownloader.h" #include "modelstore.h" +#include "transcript.h" namespace lc { @@ -77,6 +78,7 @@ SettingsDialog::SettingsDialog(bool firstRun, QWidget * parent) refreshModelTable(); refreshActiveCombo(); + rebuildLanguageCombo(); updateStreamGroupEnabled(); } @@ -145,10 +147,8 @@ QWidget * SettingsDialog::buildModelPage() form->addRow(tr("当前使用"), activeCombo_); languageCombo_ = new QComboBox(page); - languageCombo_->addItem(tr("自动检测"), QString()); - languageCombo_->addItem(tr("中文"), QStringLiteral("zh")); - languageCombo_->addItem(tr("英文"), QStringLiteral("en")); - languageCombo_->addItem(tr("日语"), QStringLiteral("ja")); + languageCombo_->setToolTip(tr("多语言模型(Nemotron / Qwen3-ASR)用它指定语言;" + "列表按所选模型支持的语言变化。")); form->addRow(tr("识别语言"), languageCombo_); mirrorEdit_ = new QLineEdit(page); @@ -201,14 +201,13 @@ QWidget * SettingsDialog::buildModelPage() proxyUser_->setText(cfg.proxyUser); proxyPass_->setText(cfg.proxyPass); updateProxyEnabled(); - languageCombo_->setCurrentIndex( - qMax(0, languageCombo_->findData(cfg.language))); connect(activeCombo_, &QComboBox::currentIndexChanged, this, [this](int idx) { if (idx >= 0) { AppSettings::instance().modelId = activeCombo_->itemData(idx).toString(); AppSettings::instance().save(); } + rebuildLanguageCombo(); // 不同模型支持的语言不同 updateStreamGroupEnabled(); }); @@ -384,6 +383,38 @@ QWidget * SettingsDialog::buildAdvancedPage() 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)有意义; // 选了流式 ONNX 模型时置灰,免得改了没效果。 void SettingsDialog::updateStreamGroupEnabled() diff --git a/src/ui/settingsdialog.h b/src/ui/settingsdialog.h index 174a560..924706b 100644 --- a/src/ui/settingsdialog.h +++ b/src/ui/settingsdialog.h @@ -38,6 +38,7 @@ private: void refreshModelTable(); void refreshActiveCombo(); + void rebuildLanguageCombo(); // 按所选模型支持的语言重建语言下拉 void updateStreamGroupEnabled(); // 流式参数只在 llama 后端下可用 void onDownload(); void onVerify(); diff --git a/tests/tst_transcript.cpp b/tests/tst_transcript.cpp index 9c88973..418ebfe 100644 --- a/tests/tst_transcript.cpp +++ b/tests/tst_transcript.cpp @@ -42,6 +42,40 @@ private slots: QCOMPARE(QString::fromStdString(lc::languageDisplayName("Klingon")), QString("Klingon")); 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)