#include "ModelSettingsDialog.h" #include #include #include #include ModelSettingsDialog::ModelSettingsDialog(QWidget* parent) : QDialog(parent), settings(new QSettings(this)) { setWindowTitle("模型设置"); setModal(true); resize(800, 600); setupUI(); connectSignals(); loadSettings(); } ModelSettingsDialog::~ModelSettingsDialog() { } void ModelSettingsDialog::setupUI() { auto* mainLayout = new QVBoxLayout(this); // 创建标签页控件 tabWidget = new QTabWidget(this); setupOfflineASRTab(); setupOnlineASRTab(); setupKWSTab(); setupTTSTab(); setupAdvancedTab(); mainLayout->addWidget(tabWidget); // 按钮区域 auto* buttonLayout = new QHBoxLayout(); scanBtn = new QPushButton("扫描模型", this); scanBtn->setToolTip("自动扫描系统中的可用模型"); resetBtn = new QPushButton("重置默认", this); resetBtn->setToolTip("重置为默认配置"); buttonLayout->addWidget(scanBtn); buttonLayout->addWidget(resetBtn); buttonLayout->addStretch(); saveBtn = new QPushButton("保存", this); saveBtn->setDefault(true); cancelBtn = new QPushButton("取消", this); buttonLayout->addWidget(saveBtn); buttonLayout->addWidget(cancelBtn); mainLayout->addLayout(buttonLayout); } void ModelSettingsDialog::setupOfflineASRTab() { offlineAsrTab = new QWidget(); tabWidget->addTab(offlineAsrTab, "离线语音识别"); auto* layout = new QVBoxLayout(offlineAsrTab); // 模型选择组 auto* modelGroup = new QGroupBox("模型选择", this); auto* modelLayout = new QGridLayout(modelGroup); modelLayout->addWidget(new QLabel("预设模型:"), 0, 0); offlineAsrModelCombo = new QComboBox(this); offlineAsrModelCombo->addItem("自定义", "custom"); offlineAsrModelCombo->addItem("Paraformer中文模型", "paraformer-zh"); offlineAsrModelCombo->addItem("Whisper多语言模型", "whisper-multilingual"); modelLayout->addWidget(offlineAsrModelCombo, 0, 1, 1, 2); layout->addWidget(modelGroup); // 模型路径组 auto* pathGroup = new QGroupBox("模型路径", this); auto* pathLayout = new QGridLayout(pathGroup); pathLayout->addWidget(new QLabel("模型文件:"), 0, 0); offlineAsrModelPathEdit = new QLineEdit(this); offlineAsrModelPathEdit->setPlaceholderText("选择.onnx模型文件..."); auto* browseModelBtn = new QPushButton("浏览", this); pathLayout->addWidget(offlineAsrModelPathEdit, 0, 1); pathLayout->addWidget(browseModelBtn, 0, 2); pathLayout->addWidget(new QLabel("词汇表文件:"), 1, 0); offlineAsrTokensPathEdit = new QLineEdit(this); offlineAsrTokensPathEdit->setPlaceholderText("选择tokens.txt文件..."); auto* browseTokensBtn = new QPushButton("浏览", this); pathLayout->addWidget(offlineAsrTokensPathEdit, 1, 1); pathLayout->addWidget(browseTokensBtn, 1, 2); layout->addWidget(pathGroup); // 模型信息组 auto* infoGroup = new QGroupBox("模型信息", this); auto* infoLayout = new QVBoxLayout(infoGroup); offlineAsrModelInfoEdit = new QTextEdit(this); offlineAsrModelInfoEdit->setMaximumHeight(100); offlineAsrModelInfoEdit->setPlaceholderText("模型信息将显示在这里..."); infoLayout->addWidget(offlineAsrModelInfoEdit); auto* testLayout = new QHBoxLayout(); testOfflineASRBtn = new QPushButton("测试模型", this); testOfflineASRBtn->setEnabled(false); testLayout->addStretch(); testLayout->addWidget(testOfflineASRBtn); infoLayout->addLayout(testLayout); layout->addWidget(infoGroup); layout->addStretch(); // 连接信号 connect(browseModelBtn, &QPushButton::clicked, this, &ModelSettingsDialog::browseOfflineASRModel); connect(browseTokensBtn, &QPushButton::clicked, this, &ModelSettingsDialog::browseOfflineASRTokens); } void ModelSettingsDialog::setupOnlineASRTab() { onlineAsrTab = new QWidget(); tabWidget->addTab(onlineAsrTab, "在线语音识别"); auto* layout = new QVBoxLayout(onlineAsrTab); // 模型选择组 auto* modelGroup = new QGroupBox("模型选择", this); auto* modelLayout = new QGridLayout(modelGroup); modelLayout->addWidget(new QLabel("预设模型:"), 0, 0); onlineAsrModelCombo = new QComboBox(this); onlineAsrModelCombo->addItem("自定义", "custom"); onlineAsrModelCombo->addItem("Streaming Paraformer中英文模型", "streaming-paraformer-zh-en"); onlineAsrModelCombo->addItem("Streaming Zipformer中英文模型", "streaming-zipformer-zh-en"); modelLayout->addWidget(onlineAsrModelCombo, 0, 1, 1, 2); layout->addWidget(modelGroup); // 模型路径组 auto* pathGroup = new QGroupBox("模型路径", this); auto* pathLayout = new QGridLayout(pathGroup); pathLayout->addWidget(new QLabel("编码器文件:"), 0, 0); onlineAsrModelPathEdit = new QLineEdit(this); onlineAsrModelPathEdit->setPlaceholderText("选择encoder.onnx文件..."); auto* browseModelBtn = new QPushButton("浏览", this); pathLayout->addWidget(onlineAsrModelPathEdit, 0, 1); pathLayout->addWidget(browseModelBtn, 0, 2); pathLayout->addWidget(new QLabel("词汇表文件:"), 1, 0); onlineAsrTokensPathEdit = new QLineEdit(this); onlineAsrTokensPathEdit->setPlaceholderText("选择tokens.txt文件..."); auto* browseTokensBtn = new QPushButton("浏览", this); pathLayout->addWidget(onlineAsrTokensPathEdit, 1, 1); pathLayout->addWidget(browseTokensBtn, 1, 2); layout->addWidget(pathGroup); // 模型信息组 auto* infoGroup = new QGroupBox("模型信息", this); auto* infoLayout = new QVBoxLayout(infoGroup); onlineAsrModelInfoEdit = new QTextEdit(this); onlineAsrModelInfoEdit->setMaximumHeight(100); onlineAsrModelInfoEdit->setPlaceholderText("模型信息将显示在这里..."); infoLayout->addWidget(onlineAsrModelInfoEdit); auto* testLayout = new QHBoxLayout(); testOnlineASRBtn = new QPushButton("测试模型", this); testOnlineASRBtn->setEnabled(false); testLayout->addStretch(); testLayout->addWidget(testOnlineASRBtn); infoLayout->addLayout(testLayout); layout->addWidget(infoGroup); layout->addStretch(); // 连接信号 connect(browseModelBtn, &QPushButton::clicked, this, &ModelSettingsDialog::browseOnlineASRModel); connect(browseTokensBtn, &QPushButton::clicked, this, &ModelSettingsDialog::browseOnlineASRTokens); } void ModelSettingsDialog::setupKWSTab() { kwsTab = new QWidget(); tabWidget->addTab(kwsTab, "语音唤醒 (KWS)"); auto* layout = new QVBoxLayout(kwsTab); // 模型选择组 auto* modelGroup = new QGroupBox("模型选择", this); auto* modelLayout = new QGridLayout(modelGroup); modelLayout->addWidget(new QLabel("预设模型:"), 0, 0); kwsModelCombo = new QComboBox(this); kwsModelCombo->addItem("自定义", "custom"); kwsModelCombo->addItem("Zipformer Wenetspeech 3.3M", "zipformer-wenetspeech-3.3m"); kwsModelCombo->addItem("Zipformer Gigaspeech", "zipformer-gigaspeech"); modelLayout->addWidget(kwsModelCombo, 0, 1, 1, 2); layout->addWidget(modelGroup); // 模型路径组 auto* pathGroup = new QGroupBox("模型路径", this); auto* pathLayout = new QGridLayout(pathGroup); pathLayout->addWidget(new QLabel("模型文件:"), 0, 0); kwsModelPathEdit = new QLineEdit(this); kwsModelPathEdit->setPlaceholderText("选择.onnx模型文件..."); auto* browseModelBtn = new QPushButton("浏览", this); pathLayout->addWidget(kwsModelPathEdit, 0, 1); pathLayout->addWidget(browseModelBtn, 0, 2); pathLayout->addWidget(new QLabel("词汇表文件:"), 1, 0); kwsTokensPathEdit = new QLineEdit(this); kwsTokensPathEdit->setPlaceholderText("选择tokens.txt文件..."); auto* browseTokensBtn = new QPushButton("浏览", this); pathLayout->addWidget(kwsTokensPathEdit, 1, 1); pathLayout->addWidget(browseTokensBtn, 1, 2); pathLayout->addWidget(new QLabel("关键词文件:"), 2, 0); kwsKeywordsPathEdit = new QLineEdit(this); kwsKeywordsPathEdit->setPlaceholderText("选择keywords.txt文件..."); auto* browseKeywordsBtn = new QPushButton("浏览", this); pathLayout->addWidget(kwsKeywordsPathEdit, 2, 1); pathLayout->addWidget(browseKeywordsBtn, 2, 2); layout->addWidget(pathGroup); // 模型信息组 auto* infoGroup = new QGroupBox("模型信息", this); auto* infoLayout = new QVBoxLayout(infoGroup); kwsModelInfoEdit = new QTextEdit(this); kwsModelInfoEdit->setMaximumHeight(100); kwsModelInfoEdit->setPlaceholderText("模型信息将显示在这里..."); infoLayout->addWidget(kwsModelInfoEdit); auto* testLayout = new QHBoxLayout(); testKWSBtn = new QPushButton("测试模型", this); testKWSBtn->setEnabled(false); testLayout->addStretch(); testLayout->addWidget(testKWSBtn); infoLayout->addLayout(testLayout); layout->addWidget(infoGroup); // KWS参数设置组 kwsParamsGroup = new QGroupBox("识别参数设置", this); auto* paramsLayout = new QGridLayout(kwsParamsGroup); // 关键词阈值 paramsLayout->addWidget(new QLabel("关键词阈值:"), 0, 0); kwsThresholdEdit = new QLineEdit(this); kwsThresholdEdit->setText("0.25"); kwsThresholdEdit->setToolTip("范围: 0.01-1.0,越低越容易检测,建议: 0.25"); paramsLayout->addWidget(kwsThresholdEdit, 0, 1); paramsLayout->addWidget(new QLabel("(0.01-1.0, 推荐0.25)"), 0, 2); // 最大活跃路径数 paramsLayout->addWidget(new QLabel("最大活跃路径:"), 1, 0); kwsMaxActivePathsEdit = new QLineEdit(this); kwsMaxActivePathsEdit->setText("8"); kwsMaxActivePathsEdit->setToolTip("范围: 1-16,越大识别率越高但速度越慢,建议: 8"); paramsLayout->addWidget(kwsMaxActivePathsEdit, 1, 1); paramsLayout->addWidget(new QLabel("(1-16, 推荐8)"), 1, 2); // 尾随空白数 paramsLayout->addWidget(new QLabel("尾随空白数:"), 2, 0); kwsTrailingBlanksEdit = new QLineEdit(this); kwsTrailingBlanksEdit->setText("2"); kwsTrailingBlanksEdit->setToolTip("范围: 1-5,越大端点检测越宽松,建议: 2"); paramsLayout->addWidget(kwsTrailingBlanksEdit, 2, 1); paramsLayout->addWidget(new QLabel("(1-5, 推荐2)"), 2, 2); // 关键词分数权重 paramsLayout->addWidget(new QLabel("关键词分数权重:"), 3, 0); kwsKeywordsScoreEdit = new QLineEdit(this); kwsKeywordsScoreEdit->setText("1.5"); kwsKeywordsScoreEdit->setToolTip("范围: 0.5-3.0,越大关键词权重越高,建议: 1.5"); paramsLayout->addWidget(kwsKeywordsScoreEdit, 3, 1); paramsLayout->addWidget(new QLabel("(0.5-3.0, 推荐1.5)"), 3, 2); // 线程数 paramsLayout->addWidget(new QLabel("处理线程数:"), 4, 0); kwsNumThreadsEdit = new QLineEdit(this); kwsNumThreadsEdit->setText("2"); kwsNumThreadsEdit->setToolTip("范围: 1-4,越大速度越快但占用资源越多,建议: 2"); paramsLayout->addWidget(kwsNumThreadsEdit, 4, 1); paramsLayout->addWidget(new QLabel("(1-4, 推荐2)"), 4, 2); // 重置按钮 kwsResetParamsBtn = new QPushButton("恢复默认参数", this); kwsResetParamsBtn->setToolTip("将所有参数恢复为推荐的默认值"); paramsLayout->addWidget(kwsResetParamsBtn, 5, 0, 1, 3); // 参数说明 auto* paramsHelpLabel = new QLabel( "💡 参数调整建议:\n" "• 识别率低:降低阈值(0.15-0.25),增加活跃路径(8-12)\n" "• 误识别多:提高阈值(0.3-0.5),减少活跃路径(4-6)\n" "• 响应慢:增加线程数(2-4),减少尾随空白(1)\n" "• 修改参数后需要重启KWS检测才能生效", this); paramsHelpLabel->setWordWrap(true); paramsHelpLabel->setStyleSheet("QLabel { color: #666; font-size: 11px; padding: 10px; background-color: #f5f5f5; border-radius: 5px; }"); paramsLayout->addWidget(paramsHelpLabel, 6, 0, 1, 3); layout->addWidget(kwsParamsGroup); layout->addStretch(); // 连接信号 connect(browseModelBtn, &QPushButton::clicked, this, &ModelSettingsDialog::browseKWSModel); connect(browseTokensBtn, &QPushButton::clicked, this, &ModelSettingsDialog::browseKWSTokens); connect(browseKeywordsBtn, &QPushButton::clicked, this, &ModelSettingsDialog::browseKWSKeywords); connect(kwsResetParamsBtn, &QPushButton::clicked, this, &ModelSettingsDialog::resetKWSParams); // 连接参数变化信号 connect(kwsThresholdEdit, &QLineEdit::textChanged, this, &ModelSettingsDialog::onKWSParamsChanged); connect(kwsMaxActivePathsEdit, &QLineEdit::textChanged, this, &ModelSettingsDialog::onKWSParamsChanged); connect(kwsTrailingBlanksEdit, &QLineEdit::textChanged, this, &ModelSettingsDialog::onKWSParamsChanged); connect(kwsKeywordsScoreEdit, &QLineEdit::textChanged, this, &ModelSettingsDialog::onKWSParamsChanged); connect(kwsNumThreadsEdit, &QLineEdit::textChanged, this, &ModelSettingsDialog::onKWSParamsChanged); } void ModelSettingsDialog::setupTTSTab() { ttsTab = new QWidget(); tabWidget->addTab(ttsTab, "语音合成 (TTS)"); auto* layout = new QVBoxLayout(ttsTab); // 模型选择组 auto* modelGroup = new QGroupBox("模型选择", this); auto* modelLayout = new QGridLayout(modelGroup); modelLayout->addWidget(new QLabel("预设模型:"), 0, 0); ttsModelCombo = new QComboBox(this); ttsModelCombo->addItem("自定义", "custom"); ttsModelCombo->addItem("MeloTTS中英文混合", "melo-zh-en"); ttsModelCombo->addItem("VITS中文模型", "vits-zh"); modelLayout->addWidget(ttsModelCombo, 0, 1, 1, 2); layout->addWidget(modelGroup); // 模型路径组 auto* pathGroup = new QGroupBox("模型路径", this); auto* pathLayout = new QGridLayout(pathGroup); pathLayout->addWidget(new QLabel("模型文件:"), 0, 0); ttsModelPathEdit = new QLineEdit(this); ttsModelPathEdit->setPlaceholderText("选择.onnx模型文件..."); auto* browseModelBtn = new QPushButton("浏览", this); pathLayout->addWidget(ttsModelPathEdit, 0, 1); pathLayout->addWidget(browseModelBtn, 0, 2); pathLayout->addWidget(new QLabel("词汇表文件:"), 1, 0); ttsTokensPathEdit = new QLineEdit(this); ttsTokensPathEdit->setPlaceholderText("选择tokens.txt文件..."); auto* browseTokensBtn = new QPushButton("浏览", this); pathLayout->addWidget(ttsTokensPathEdit, 1, 1); pathLayout->addWidget(browseTokensBtn, 1, 2); pathLayout->addWidget(new QLabel("词典文件:"), 2, 0); ttsLexiconPathEdit = new QLineEdit(this); ttsLexiconPathEdit->setPlaceholderText("选择lexicon.txt文件..."); auto* browseLexiconBtn = new QPushButton("浏览", this); pathLayout->addWidget(ttsLexiconPathEdit, 2, 1); pathLayout->addWidget(browseLexiconBtn, 2, 2); pathLayout->addWidget(new QLabel("字典目录:"), 3, 0); ttsDictDirPathEdit = new QLineEdit(this); ttsDictDirPathEdit->setPlaceholderText("选择dict目录..."); auto* browseDictBtn = new QPushButton("浏览", this); pathLayout->addWidget(ttsDictDirPathEdit, 3, 1); pathLayout->addWidget(browseDictBtn, 3, 2); pathLayout->addWidget(new QLabel("数据目录:"), 4, 0); ttsDataDirPathEdit = new QLineEdit(this); ttsDataDirPathEdit->setPlaceholderText("选择espeak-ng-data目录..."); auto* browseDataBtn = new QPushButton("浏览", this); pathLayout->addWidget(ttsDataDirPathEdit, 4, 1); pathLayout->addWidget(browseDataBtn, 4, 2); layout->addWidget(pathGroup); // 模型信息组 auto* infoGroup = new QGroupBox("模型信息", this); auto* infoLayout = new QVBoxLayout(infoGroup); ttsModelInfoEdit = new QTextEdit(this); ttsModelInfoEdit->setMaximumHeight(100); ttsModelInfoEdit->setPlaceholderText("模型信息将显示在这里..."); infoLayout->addWidget(ttsModelInfoEdit); auto* testLayout = new QHBoxLayout(); testTTSBtn = new QPushButton("测试模型", this); testTTSBtn->setEnabled(false); testLayout->addStretch(); testLayout->addWidget(testTTSBtn); infoLayout->addLayout(testLayout); layout->addWidget(infoGroup); layout->addStretch(); // 连接信号 connect(browseModelBtn, &QPushButton::clicked, this, &ModelSettingsDialog::browseTTSModel); connect(browseTokensBtn, &QPushButton::clicked, this, &ModelSettingsDialog::browseTTSTokens); connect(browseLexiconBtn, &QPushButton::clicked, this, &ModelSettingsDialog::browseTTSLexicon); connect(browseDictBtn, &QPushButton::clicked, this, &ModelSettingsDialog::browseTTSDictDir); connect(browseDataBtn, &QPushButton::clicked, this, &ModelSettingsDialog::browseTTSDataDir); } void ModelSettingsDialog::setupAdvancedTab() { advancedTab = new QWidget(); tabWidget->addTab(advancedTab, "高级设置"); auto* layout = new QVBoxLayout(advancedTab); // 路径设置组 auto* pathGroup = new QGroupBox("路径设置", this); auto* pathLayout = new QGridLayout(pathGroup); pathLayout->addWidget(new QLabel("数据根目录:"), 0, 0); dataPathEdit = new QLineEdit(this); dataPathEdit->setText(getDefaultDataPath()); auto* browseDataPathBtn = new QPushButton("浏览", this); pathLayout->addWidget(dataPathEdit, 0, 1); pathLayout->addWidget(browseDataPathBtn, 0, 2); layout->addWidget(pathGroup); // 功能设置组 auto* featureGroup = new QGroupBox("功能设置", this); auto* featureLayout = new QVBoxLayout(featureGroup); autoScanCheckBox = new QCheckBox("启动时自动扫描模型", this); autoScanCheckBox->setChecked(true); featureLayout->addWidget(autoScanCheckBox); enableLoggingCheckBox = new QCheckBox("启用详细日志", this); enableLoggingCheckBox->setChecked(false); featureLayout->addWidget(enableLoggingCheckBox); layout->addWidget(featureGroup); layout->addStretch(); // 连接信号 connect(browseDataPathBtn, &QPushButton::clicked, [this]() { QString dir = QFileDialog::getExistingDirectory(this, "选择数据根目录", dataPathEdit->text()); if (!dir.isEmpty()) { dataPathEdit->setText(dir); } }); } void ModelSettingsDialog::connectSignals() { connect(offlineAsrModelCombo, QOverload::of(&QComboBox::currentIndexChanged), this, &ModelSettingsDialog::onOfflineASRModelChanged); connect(onlineAsrModelCombo, QOverload::of(&QComboBox::currentIndexChanged), this, &ModelSettingsDialog::onOnlineASRModelChanged); connect(kwsModelCombo, QOverload::of(&QComboBox::currentIndexChanged), this, &ModelSettingsDialog::onKWSModelChanged); connect(ttsModelCombo, QOverload::of(&QComboBox::currentIndexChanged), this, &ModelSettingsDialog::onTTSModelChanged); connect(offlineAsrModelPathEdit, &QLineEdit::textChanged, this, &ModelSettingsDialog::updateOfflineASRModelInfo); connect(onlineAsrModelPathEdit, &QLineEdit::textChanged, this, &ModelSettingsDialog::updateOnlineASRModelInfo); connect(kwsModelPathEdit, &QLineEdit::textChanged, this, &ModelSettingsDialog::updateKWSModelInfo); connect(ttsModelPathEdit, &QLineEdit::textChanged, this, &ModelSettingsDialog::updateTTSModelInfo); connect(testOfflineASRBtn, &QPushButton::clicked, this, &ModelSettingsDialog::testOfflineASRModel); connect(testOnlineASRBtn, &QPushButton::clicked, this, &ModelSettingsDialog::testOnlineASRModel); connect(testKWSBtn, &QPushButton::clicked, this, &ModelSettingsDialog::testKWSModel); connect(testTTSBtn, &QPushButton::clicked, this, &ModelSettingsDialog::testTTSModel); connect(saveBtn, &QPushButton::clicked, this, &ModelSettingsDialog::saveSettings); connect(cancelBtn, &QPushButton::clicked, this, &QDialog::reject); connect(resetBtn, &QPushButton::clicked, this, &ModelSettingsDialog::resetToDefaults); connect(scanBtn, &QPushButton::clicked, this, &ModelSettingsDialog::scanForModels); } QString ModelSettingsDialog::getDefaultDataPath() const { return QDir::homePath() + "/.config/QSmartAssistant/Data"; } void ModelSettingsDialog::browseOfflineASRModel() { QString fileName = QFileDialog::getOpenFileName(this, "选择离线ASR模型文件", offlineAsrModelPathEdit->text().isEmpty() ? getDefaultDataPath() : offlineAsrModelPathEdit->text(), "ONNX模型文件 (*.onnx)"); if (!fileName.isEmpty()) { offlineAsrModelPathEdit->setText(fileName); } } void ModelSettingsDialog::browseOfflineASRTokens() { QString fileName = QFileDialog::getOpenFileName(this, "选择词汇表文件", offlineAsrTokensPathEdit->text().isEmpty() ? getDefaultDataPath() : offlineAsrTokensPathEdit->text(), "文本文件 (*.txt)"); if (!fileName.isEmpty()) { offlineAsrTokensPathEdit->setText(fileName); } } void ModelSettingsDialog::browseOnlineASRModel() { QString fileName = QFileDialog::getOpenFileName(this, "选择在线ASR编码器文件", onlineAsrModelPathEdit->text().isEmpty() ? getDefaultDataPath() : onlineAsrModelPathEdit->text(), "ONNX模型文件 (*.onnx)"); if (!fileName.isEmpty()) { onlineAsrModelPathEdit->setText(fileName); } } void ModelSettingsDialog::browseOnlineASRTokens() { QString fileName = QFileDialog::getOpenFileName(this, "选择词汇表文件", onlineAsrTokensPathEdit->text().isEmpty() ? getDefaultDataPath() : onlineAsrTokensPathEdit->text(), "文本文件 (*.txt)"); if (!fileName.isEmpty()) { onlineAsrTokensPathEdit->setText(fileName); } } void ModelSettingsDialog::browseKWSModel() { QString fileName = QFileDialog::getOpenFileName(this, "选择语音唤醒模型文件", kwsModelPathEdit->text().isEmpty() ? getDefaultDataPath() : kwsModelPathEdit->text(), "ONNX模型文件 (*.onnx)"); if (!fileName.isEmpty()) { kwsModelPathEdit->setText(fileName); } } void ModelSettingsDialog::browseKWSTokens() { QString fileName = QFileDialog::getOpenFileName(this, "选择词汇表文件", kwsTokensPathEdit->text().isEmpty() ? getDefaultDataPath() : kwsTokensPathEdit->text(), "文本文件 (*.txt)"); if (!fileName.isEmpty()) { kwsTokensPathEdit->setText(fileName); } } void ModelSettingsDialog::browseKWSKeywords() { QString fileName = QFileDialog::getOpenFileName(this, "选择关键词文件", kwsKeywordsPathEdit->text().isEmpty() ? getDefaultDataPath() : kwsKeywordsPathEdit->text(), "文本文件 (*.txt)"); if (!fileName.isEmpty()) { kwsKeywordsPathEdit->setText(fileName); } } void ModelSettingsDialog::browseTTSModel() { QString fileName = QFileDialog::getOpenFileName(this, "选择TTS模型文件", ttsModelPathEdit->text().isEmpty() ? getDefaultDataPath() : ttsModelPathEdit->text(), "ONNX模型文件 (*.onnx)"); if (!fileName.isEmpty()) { ttsModelPathEdit->setText(fileName); } } void ModelSettingsDialog::browseTTSTokens() { QString fileName = QFileDialog::getOpenFileName(this, "选择词汇表文件", ttsTokensPathEdit->text().isEmpty() ? getDefaultDataPath() : ttsTokensPathEdit->text(), "文本文件 (*.txt)"); if (!fileName.isEmpty()) { ttsTokensPathEdit->setText(fileName); } } void ModelSettingsDialog::browseTTSLexicon() { QString fileName = QFileDialog::getOpenFileName(this, "选择词典文件", ttsLexiconPathEdit->text().isEmpty() ? getDefaultDataPath() : ttsLexiconPathEdit->text(), "文本文件 (*.txt)"); if (!fileName.isEmpty()) { ttsLexiconPathEdit->setText(fileName); } } void ModelSettingsDialog::browseTTSDictDir() { QString dir = QFileDialog::getExistingDirectory(this, "选择字典目录", ttsDictDirPathEdit->text().isEmpty() ? getDefaultDataPath() : ttsDictDirPathEdit->text()); if (!dir.isEmpty()) { ttsDictDirPathEdit->setText(dir); } } void ModelSettingsDialog::browseTTSDataDir() { QString dir = QFileDialog::getExistingDirectory(this, "选择数据目录", ttsDataDirPathEdit->text().isEmpty() ? getDefaultDataPath() : ttsDataDirPathEdit->text()); if (!dir.isEmpty()) { ttsDataDirPathEdit->setText(dir); } } void ModelSettingsDialog::onOfflineASRModelChanged() { QString modelType = offlineAsrModelCombo->currentData().toString(); QString dataPath = dataPathEdit->text(); if (modelType == "paraformer-zh") { offlineAsrModelPathEdit->setText(dataPath + "/sherpa-onnx-paraformer-zh-2024-03-09/model.int8.onnx"); offlineAsrTokensPathEdit->setText(dataPath + "/sherpa-onnx-paraformer-zh-2024-03-09/tokens.txt"); } else if (modelType == "whisper-multilingual") { offlineAsrModelPathEdit->setText(dataPath + "/sherpa-onnx-whisper-base/base.onnx"); offlineAsrTokensPathEdit->setText(dataPath + "/sherpa-onnx-whisper-base/base.tokens"); } // 自定义模式不自动填充路径 } void ModelSettingsDialog::onOnlineASRModelChanged() { QString modelType = onlineAsrModelCombo->currentData().toString(); QString dataPath = dataPathEdit->text(); if (modelType == "streaming-paraformer-zh-en") { onlineAsrModelPathEdit->setText(dataPath + "/sherpa-onnx-streaming-paraformer-bilingual-zh-en/encoder.int8.onnx"); onlineAsrTokensPathEdit->setText(dataPath + "/sherpa-onnx-streaming-paraformer-bilingual-zh-en/tokens.txt"); } else if (modelType == "streaming-zipformer-zh-en") { onlineAsrModelPathEdit->setText(dataPath + "/sherpa-onnx-streaming-zipformer-zh-en/encoder.onnx"); onlineAsrTokensPathEdit->setText(dataPath + "/sherpa-onnx-streaming-zipformer-zh-en/tokens.txt"); } // 自定义模式不自动填充路径 } void ModelSettingsDialog::onKWSModelChanged() { QString modelType = kwsModelCombo->currentData().toString(); QString dataPath = dataPathEdit->text(); if (modelType == "zipformer-wenetspeech-3.3m") { kwsModelPathEdit->setText(dataPath + "/sherpa-onnx-kws-zipformer-wenetspeech-3.3M-2024-01-01/encoder-epoch-12-avg-2-chunk-16-left-64.onnx"); kwsTokensPathEdit->setText(dataPath + "/sherpa-onnx-kws-zipformer-wenetspeech-3.3M-2024-01-01/tokens.txt"); kwsKeywordsPathEdit->setText(dataPath + "/sherpa-onnx-kws-zipformer-wenetspeech-3.3M-2024-01-01/keywords.txt"); } else if (modelType == "zipformer-gigaspeech") { kwsModelPathEdit->setText(dataPath + "/sherpa-onnx-kws-zipformer-gigaspeech/model.onnx"); kwsTokensPathEdit->setText(dataPath + "/sherpa-onnx-kws-zipformer-gigaspeech/tokens.txt"); kwsKeywordsPathEdit->setText(dataPath + "/sherpa-onnx-kws-zipformer-gigaspeech/keywords.txt"); } // 自定义模式不自动填充路径 } void ModelSettingsDialog::onTTSModelChanged() { QString modelType = ttsModelCombo->currentData().toString(); QString dataPath = dataPathEdit->text(); if (modelType == "melo-zh-en") { ttsModelPathEdit->setText(dataPath + "/vits-melo-tts-zh_en/model.int8.onnx"); ttsTokensPathEdit->setText(dataPath + "/vits-melo-tts-zh_en/tokens.txt"); ttsLexiconPathEdit->setText(dataPath + "/vits-melo-tts-zh_en/lexicon.txt"); ttsDictDirPathEdit->setText(dataPath + "/vits-melo-tts-zh_en/dict"); ttsDataDirPathEdit->clear(); } else if (modelType == "vits-zh") { ttsModelPathEdit->setText(dataPath + "/vits-zh-aishell3/vits-aishell3.int8.onnx"); ttsTokensPathEdit->setText(dataPath + "/vits-zh-aishell3/tokens.txt"); ttsLexiconPathEdit->setText(dataPath + "/vits-zh-aishell3/lexicon.txt"); ttsDictDirPathEdit->clear(); ttsDataDirPathEdit->clear(); } // 自定义模式不自动填充路径 } void ModelSettingsDialog::updateOfflineASRModelInfo() { QString modelPath = offlineAsrModelPathEdit->text(); if (modelPath.isEmpty()) { offlineAsrModelInfoEdit->clear(); testOfflineASRBtn->setEnabled(false); return; } QFileInfo fileInfo(modelPath); if (fileInfo.exists()) { QString info = QString("文件大小: %1 MB\n修改时间: %2\n状态: 文件存在") .arg(fileInfo.size() / 1024.0 / 1024.0, 0, 'f', 1) .arg(fileInfo.lastModified().toString("yyyy-MM-dd hh:mm:ss")); offlineAsrModelInfoEdit->setText(info); testOfflineASRBtn->setEnabled(validateOfflineASRConfig()); } else { offlineAsrModelInfoEdit->setText("状态: 文件不存在"); testOfflineASRBtn->setEnabled(false); } } void ModelSettingsDialog::updateOnlineASRModelInfo() { QString modelPath = onlineAsrModelPathEdit->text(); if (modelPath.isEmpty()) { onlineAsrModelInfoEdit->clear(); testOnlineASRBtn->setEnabled(false); return; } QFileInfo fileInfo(modelPath); if (fileInfo.exists()) { QString info = QString("文件大小: %1 MB\n修改时间: %2\n状态: 文件存在") .arg(fileInfo.size() / 1024.0 / 1024.0, 0, 'f', 1) .arg(fileInfo.lastModified().toString("yyyy-MM-dd hh:mm:ss")); onlineAsrModelInfoEdit->setText(info); testOnlineASRBtn->setEnabled(validateOnlineASRConfig()); } else { onlineAsrModelInfoEdit->setText("状态: 文件不存在"); testOnlineASRBtn->setEnabled(false); } } void ModelSettingsDialog::updateKWSModelInfo() { QString modelPath = kwsModelPathEdit->text(); if (modelPath.isEmpty()) { kwsModelInfoEdit->clear(); testKWSBtn->setEnabled(false); return; } QFileInfo fileInfo(modelPath); if (fileInfo.exists()) { QString info = QString("文件大小: %1 MB\n修改时间: %2\n状态: 文件存在") .arg(fileInfo.size() / 1024.0 / 1024.0, 0, 'f', 1) .arg(fileInfo.lastModified().toString("yyyy-MM-dd hh:mm:ss")); kwsModelInfoEdit->setText(info); testKWSBtn->setEnabled(validateKWSConfig()); } else { kwsModelInfoEdit->setText("状态: 文件不存在"); testKWSBtn->setEnabled(false); } } void ModelSettingsDialog::updateTTSModelInfo() { QString modelPath = ttsModelPathEdit->text(); if (modelPath.isEmpty()) { ttsModelInfoEdit->clear(); testTTSBtn->setEnabled(false); return; } QFileInfo fileInfo(modelPath); if (fileInfo.exists()) { QString info = QString("文件大小: %1 MB\n修改时间: %2\n状态: 文件存在") .arg(fileInfo.size() / 1024.0 / 1024.0, 0, 'f', 1) .arg(fileInfo.lastModified().toString("yyyy-MM-dd hh:mm:ss")); ttsModelInfoEdit->setText(info); testTTSBtn->setEnabled(validateTTSConfig()); } else { ttsModelInfoEdit->setText("状态: 文件不存在"); testTTSBtn->setEnabled(false); } } bool ModelSettingsDialog::validateOfflineASRConfig() const { return QFileInfo::exists(offlineAsrModelPathEdit->text()) && QFileInfo::exists(offlineAsrTokensPathEdit->text()); } bool ModelSettingsDialog::validateOnlineASRConfig() const { return QFileInfo::exists(onlineAsrModelPathEdit->text()) && QFileInfo::exists(onlineAsrTokensPathEdit->text()); } bool ModelSettingsDialog::validateKWSConfig() const { return QFileInfo::exists(kwsModelPathEdit->text()) && QFileInfo::exists(kwsTokensPathEdit->text()) && QFileInfo::exists(kwsKeywordsPathEdit->text()); } bool ModelSettingsDialog::validateTTSConfig() const { return QFileInfo::exists(ttsModelPathEdit->text()) && QFileInfo::exists(ttsTokensPathEdit->text()) && QFileInfo::exists(ttsLexiconPathEdit->text()); } void ModelSettingsDialog::testOfflineASRModel() { QMessageBox::information(this, "测试离线ASR模型", "离线ASR模型测试功能待实现"); } void ModelSettingsDialog::testOnlineASRModel() { QMessageBox::information(this, "测试在线ASR模型", "在线ASR模型测试功能待实现"); } void ModelSettingsDialog::testKWSModel() { QMessageBox::information(this, "测试语音唤醒模型", "语音唤醒模型测试功能待实现"); } void ModelSettingsDialog::testTTSModel() { QMessageBox::information(this, "测试TTS模型", "TTS模型测试功能待实现"); } void ModelSettingsDialog::saveSettings() { // 保存离线ASR设置 settings->beginGroup("OfflineASR"); settings->setValue("modelPath", offlineAsrModelPathEdit->text()); settings->setValue("tokensPath", offlineAsrTokensPathEdit->text()); settings->setValue("modelType", offlineAsrModelCombo->currentData().toString()); settings->endGroup(); // 保存在线ASR设置 settings->beginGroup("OnlineASR"); settings->setValue("modelPath", onlineAsrModelPathEdit->text()); settings->setValue("tokensPath", onlineAsrTokensPathEdit->text()); settings->setValue("modelType", onlineAsrModelCombo->currentData().toString()); settings->endGroup(); // 保存语音唤醒设置 settings->beginGroup("KWS"); settings->setValue("modelPath", kwsModelPathEdit->text()); settings->setValue("tokensPath", kwsTokensPathEdit->text()); settings->setValue("keywordsPath", kwsKeywordsPathEdit->text()); settings->setValue("modelType", kwsModelCombo->currentData().toString()); // 保存KWS参数 KWSParams params = getCurrentKWSParams(); settings->setValue("threshold", params.threshold); settings->setValue("maxActivePaths", params.maxActivePaths); settings->setValue("numTrailingBlanks", params.numTrailingBlanks); settings->setValue("keywordsScore", params.keywordsScore); settings->setValue("numThreads", params.numThreads); settings->endGroup(); // 保存TTS设置 settings->beginGroup("TTS"); settings->setValue("modelPath", ttsModelPathEdit->text()); settings->setValue("tokensPath", ttsTokensPathEdit->text()); settings->setValue("lexiconPath", ttsLexiconPathEdit->text()); settings->setValue("dictDirPath", ttsDictDirPathEdit->text()); settings->setValue("dataDirPath", ttsDataDirPathEdit->text()); settings->setValue("modelType", ttsModelCombo->currentData().toString()); settings->endGroup(); // 保存高级设置 settings->beginGroup("Advanced"); settings->setValue("dataPath", dataPathEdit->text()); settings->setValue("autoScan", autoScanCheckBox->isChecked()); settings->setValue("enableLogging", enableLoggingCheckBox->isChecked()); settings->endGroup(); emit modelsChanged(); accept(); } void ModelSettingsDialog::loadSettings() { // 加载离线ASR设置 settings->beginGroup("OfflineASR"); offlineAsrModelPathEdit->setText(settings->value("modelPath").toString()); offlineAsrTokensPathEdit->setText(settings->value("tokensPath").toString()); QString offlineAsrModelType = settings->value("modelType", "paraformer-zh").toString(); int offlineAsrIndex = offlineAsrModelCombo->findData(offlineAsrModelType); if (offlineAsrIndex >= 0) offlineAsrModelCombo->setCurrentIndex(offlineAsrIndex); settings->endGroup(); // 加载在线ASR设置 settings->beginGroup("OnlineASR"); onlineAsrModelPathEdit->setText(settings->value("modelPath").toString()); onlineAsrTokensPathEdit->setText(settings->value("tokensPath").toString()); QString onlineAsrModelType = settings->value("modelType", "streaming-paraformer-zh-en").toString(); int onlineAsrIndex = onlineAsrModelCombo->findData(onlineAsrModelType); if (onlineAsrIndex >= 0) onlineAsrModelCombo->setCurrentIndex(onlineAsrIndex); settings->endGroup(); // 加载语音唤醒设置 settings->beginGroup("KWS"); kwsModelPathEdit->setText(settings->value("modelPath").toString()); kwsTokensPathEdit->setText(settings->value("tokensPath").toString()); kwsKeywordsPathEdit->setText(settings->value("keywordsPath").toString()); QString kwsModelType = settings->value("modelType", "zipformer-wenetspeech-3.3m").toString(); int kwsIndex = kwsModelCombo->findData(kwsModelType); if (kwsIndex >= 0) kwsModelCombo->setCurrentIndex(kwsIndex); // 加载KWS参数 KWSParams params; params.threshold = settings->value("threshold", 0.25f).toFloat(); params.maxActivePaths = settings->value("maxActivePaths", 8).toInt(); params.numTrailingBlanks = settings->value("numTrailingBlanks", 2).toInt(); params.keywordsScore = settings->value("keywordsScore", 1.5f).toFloat(); params.numThreads = settings->value("numThreads", 2).toInt(); setCurrentKWSParams(params); settings->endGroup(); // 加载TTS设置 settings->beginGroup("TTS"); ttsModelPathEdit->setText(settings->value("modelPath").toString()); ttsTokensPathEdit->setText(settings->value("tokensPath").toString()); ttsLexiconPathEdit->setText(settings->value("lexiconPath").toString()); ttsDictDirPathEdit->setText(settings->value("dictDirPath").toString()); ttsDataDirPathEdit->setText(settings->value("dataDirPath").toString()); QString ttsModelType = settings->value("modelType", "melo-zh-en").toString(); int ttsIndex = ttsModelCombo->findData(ttsModelType); if (ttsIndex >= 0) ttsModelCombo->setCurrentIndex(ttsIndex); settings->endGroup(); // 加载高级设置 settings->beginGroup("Advanced"); dataPathEdit->setText(settings->value("dataPath", getDefaultDataPath()).toString()); autoScanCheckBox->setChecked(settings->value("autoScan", true).toBool()); enableLoggingCheckBox->setChecked(settings->value("enableLogging", false).toBool()); settings->endGroup(); // 如果没有保存的设置,使用默认值 if (offlineAsrModelPathEdit->text().isEmpty()) { onOfflineASRModelChanged(); } if (onlineAsrModelPathEdit->text().isEmpty()) { onOnlineASRModelChanged(); } if (kwsModelPathEdit->text().isEmpty()) { onKWSModelChanged(); } if (ttsModelPathEdit->text().isEmpty()) { onTTSModelChanged(); } } void ModelSettingsDialog::resetToDefaults() { int ret = QMessageBox::question(this, "重置设置", "确定要重置所有设置为默认值吗?这将清除当前的所有配置。", QMessageBox::Yes | QMessageBox::No); if (ret == QMessageBox::Yes) { // 重置为默认值 offlineAsrModelCombo->setCurrentIndex(1); // paraformer-zh onlineAsrModelCombo->setCurrentIndex(1); // streaming-paraformer-zh-en kwsModelCombo->setCurrentIndex(1); // zipformer-wenetspeech-3.3m ttsModelCombo->setCurrentIndex(1); // melo-zh-en dataPathEdit->setText(getDefaultDataPath()); autoScanCheckBox->setChecked(true); enableLoggingCheckBox->setChecked(false); onOfflineASRModelChanged(); onOnlineASRModelChanged(); onKWSModelChanged(); onTTSModelChanged(); } } void ModelSettingsDialog::scanForModels() { QMessageBox::information(this, "扫描模型", "模型扫描功能待实现"); } ModelConfig ModelSettingsDialog::getCurrentOfflineASRConfig() const { ModelConfig config; config.name = offlineAsrModelCombo->currentText(); config.modelPath = offlineAsrModelPathEdit->text(); config.tokensPath = offlineAsrTokensPathEdit->text(); config.isEnabled = validateOfflineASRConfig(); return config; } ModelConfig ModelSettingsDialog::getCurrentOnlineASRConfig() const { ModelConfig config; config.name = onlineAsrModelCombo->currentText(); config.modelPath = onlineAsrModelPathEdit->text(); config.tokensPath = onlineAsrTokensPathEdit->text(); config.isEnabled = validateOnlineASRConfig(); return config; } ModelConfig ModelSettingsDialog::getCurrentKWSConfig() const { ModelConfig config; config.name = kwsModelCombo->currentText(); config.modelPath = kwsModelPathEdit->text(); config.tokensPath = kwsTokensPathEdit->text(); config.lexiconPath = kwsKeywordsPathEdit->text(); // 使用lexiconPath存储关键词文件路径 config.isEnabled = validateKWSConfig(); return config; } ModelConfig ModelSettingsDialog::getCurrentTTSConfig() const { ModelConfig config; config.name = ttsModelCombo->currentText(); config.modelPath = ttsModelPathEdit->text(); config.tokensPath = ttsTokensPathEdit->text(); config.lexiconPath = ttsLexiconPathEdit->text(); config.dictDirPath = ttsDictDirPathEdit->text(); config.dataDirPath = ttsDataDirPathEdit->text(); config.isEnabled = validateTTSConfig(); return config; } void ModelSettingsDialog::setCurrentOfflineASRConfig(const ModelConfig& config) { offlineAsrModelPathEdit->setText(config.modelPath); offlineAsrTokensPathEdit->setText(config.tokensPath); } void ModelSettingsDialog::setCurrentOnlineASRConfig(const ModelConfig& config) { onlineAsrModelPathEdit->setText(config.modelPath); onlineAsrTokensPathEdit->setText(config.tokensPath); } void ModelSettingsDialog::setCurrentKWSConfig(const ModelConfig& config) { kwsModelPathEdit->setText(config.modelPath); kwsTokensPathEdit->setText(config.tokensPath); kwsKeywordsPathEdit->setText(config.lexiconPath); // 从lexiconPath读取关键词文件路径 } void ModelSettingsDialog::setCurrentTTSConfig(const ModelConfig& config) { ttsModelPathEdit->setText(config.modelPath); ttsTokensPathEdit->setText(config.tokensPath); ttsLexiconPathEdit->setText(config.lexiconPath); ttsDictDirPathEdit->setText(config.dictDirPath); ttsDataDirPathEdit->setText(config.dataDirPath); } // KWS参数相关方法实现 ModelSettingsDialog::KWSParams ModelSettingsDialog::getCurrentKWSParams() const { KWSParams params; bool ok; params.threshold = kwsThresholdEdit->text().toFloat(&ok); if (!ok || params.threshold < 0.01f || params.threshold > 1.0f) { params.threshold = 0.25f; // 默认值 } params.maxActivePaths = kwsMaxActivePathsEdit->text().toInt(&ok); if (!ok || params.maxActivePaths < 1 || params.maxActivePaths > 16) { params.maxActivePaths = 8; // 默认值 } params.numTrailingBlanks = kwsTrailingBlanksEdit->text().toInt(&ok); if (!ok || params.numTrailingBlanks < 1 || params.numTrailingBlanks > 5) { params.numTrailingBlanks = 2; // 默认值 } params.keywordsScore = kwsKeywordsScoreEdit->text().toFloat(&ok); if (!ok || params.keywordsScore < 0.5f || params.keywordsScore > 3.0f) { params.keywordsScore = 1.5f; // 默认值 } params.numThreads = kwsNumThreadsEdit->text().toInt(&ok); if (!ok || params.numThreads < 1 || params.numThreads > 4) { params.numThreads = 2; // 默认值 } return params; } void ModelSettingsDialog::setCurrentKWSParams(const KWSParams& params) { kwsThresholdEdit->setText(QString::number(params.threshold, 'f', 2)); kwsMaxActivePathsEdit->setText(QString::number(params.maxActivePaths)); kwsTrailingBlanksEdit->setText(QString::number(params.numTrailingBlanks)); kwsKeywordsScoreEdit->setText(QString::number(params.keywordsScore, 'f', 1)); kwsNumThreadsEdit->setText(QString::number(params.numThreads)); } void ModelSettingsDialog::onKWSParamsChanged() { // 实时验证参数 validateKWSParams(); } void ModelSettingsDialog::resetKWSParams() { int ret = QMessageBox::question(this, "重置KWS参数", "确定要重置KWS参数为推荐的默认值吗?", QMessageBox::Yes | QMessageBox::No); if (ret == QMessageBox::Yes) { KWSParams defaultParams; // 使用默认构造函数的默认值 setCurrentKWSParams(defaultParams); QMessageBox::information(this, "参数重置", "KWS参数已重置为默认值。\n" "请保存设置并重启KWS检测以使新参数生效。"); } } void ModelSettingsDialog::validateKWSParams() { QString errorMsg; bool hasError = false; // 验证阈值 bool ok; float threshold = kwsThresholdEdit->text().toFloat(&ok); if (!ok || threshold < 0.01f || threshold > 1.0f) { errorMsg += "• 关键词阈值必须在0.01-1.0范围内\n"; hasError = true; kwsThresholdEdit->setStyleSheet("QLineEdit { border: 2px solid red; }"); } else { kwsThresholdEdit->setStyleSheet(""); } // 验证最大活跃路径数 int maxActivePaths = kwsMaxActivePathsEdit->text().toInt(&ok); if (!ok || maxActivePaths < 1 || maxActivePaths > 16) { errorMsg += "• 最大活跃路径数必须在1-16范围内\n"; hasError = true; kwsMaxActivePathsEdit->setStyleSheet("QLineEdit { border: 2px solid red; }"); } else { kwsMaxActivePathsEdit->setStyleSheet(""); } // 验证尾随空白数 int numTrailingBlanks = kwsTrailingBlanksEdit->text().toInt(&ok); if (!ok || numTrailingBlanks < 1 || numTrailingBlanks > 5) { errorMsg += "• 尾随空白数必须在1-5范围内\n"; hasError = true; kwsTrailingBlanksEdit->setStyleSheet("QLineEdit { border: 2px solid red; }"); } else { kwsTrailingBlanksEdit->setStyleSheet(""); } // 验证关键词分数权重 float keywordsScore = kwsKeywordsScoreEdit->text().toFloat(&ok); if (!ok || keywordsScore < 0.5f || keywordsScore > 3.0f) { errorMsg += "• 关键词分数权重必须在0.5-3.0范围内\n"; hasError = true; kwsKeywordsScoreEdit->setStyleSheet("QLineEdit { border: 2px solid red; }"); } else { kwsKeywordsScoreEdit->setStyleSheet(""); } // 验证线程数 int numThreads = kwsNumThreadsEdit->text().toInt(&ok); if (!ok || numThreads < 1 || numThreads > 4) { errorMsg += "• 处理线程数必须在1-4范围内\n"; hasError = true; kwsNumThreadsEdit->setStyleSheet("QLineEdit { border: 2px solid red; }"); } else { kwsNumThreadsEdit->setStyleSheet(""); } // 更新保存按钮状态 if (saveBtn) { saveBtn->setEnabled(!hasError); if (hasError) { saveBtn->setToolTip("请修正参数错误后再保存:\n" + errorMsg); } else { saveBtn->setToolTip("保存所有设置"); } } }