- audio_capture 启动时输出详细设备信息(名称、Host API、采样率) - 录音停止时输出 RMS 电平和峰值,帮助诊断音频质量问题 - 设置页面新增音频输入设备下拉选择,支持从 PortAudio 设备列表中手动选择 - 语音输入服务使用配置的音频设备和采样率参数 - 检测 monitor/output 类型设备时发出警告,避免选错回环设备 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
318 lines
12 KiB
C++
318 lines
12 KiB
C++
#include "settings_page.h"
|
||
#include "app/config_manager.h"
|
||
#include "audio/audio_capture.h"
|
||
#include "widgets/hotkey_recorder.h"
|
||
#include "utils/logger.h"
|
||
|
||
#include <QVBoxLayout>
|
||
#include <QHBoxLayout>
|
||
#include <QFormLayout>
|
||
#include <QGroupBox>
|
||
#include <QLineEdit>
|
||
#include <QPushButton>
|
||
#include <QComboBox>
|
||
#include <QSpinBox>
|
||
#include <QDoubleSpinBox>
|
||
#include <QCheckBox>
|
||
#include <QLabel>
|
||
#include <QFileDialog>
|
||
#include <QMessageBox>
|
||
|
||
static const char* const kTag = "SettingsPage";
|
||
|
||
namespace impress {
|
||
|
||
SettingsPage::SettingsPage(ConfigManager* configManager, QWidget* parent)
|
||
: QWidget(parent)
|
||
, configManager_(configManager)
|
||
{
|
||
setupUI();
|
||
loadFromConfig();
|
||
}
|
||
|
||
SettingsPage::~SettingsPage() = default;
|
||
|
||
void SettingsPage::setupUI() {
|
||
auto* mainLayout = new QVBoxLayout(this);
|
||
|
||
// STT 设置
|
||
auto* sttGroup = new QGroupBox("STT 推理设置", this);
|
||
auto* sttLayout = new QFormLayout(sttGroup);
|
||
|
||
auto* modelRow = new QHBoxLayout();
|
||
modelPathEdit_ = new QLineEdit(this);
|
||
modelPathEdit_->setPlaceholderText("选择 ONNX 模型文件路径...");
|
||
browseBtn_ = new QPushButton("浏览...", this);
|
||
connect(browseBtn_, &QPushButton::clicked, this, &SettingsPage::onBrowseModelPath);
|
||
modelRow->addWidget(modelPathEdit_);
|
||
modelRow->addWidget(browseBtn_);
|
||
sttLayout->addRow("模型路径:", modelRow);
|
||
|
||
modelTypeCombo_ = new QComboBox(this);
|
||
modelTypeCombo_->addItems({"sense_voice", "whisper", "paraformer", "conformer"});
|
||
sttLayout->addRow("模型类型:", modelTypeCombo_);
|
||
|
||
deviceCombo_ = new QComboBox(this);
|
||
deviceCombo_->addItems({"cpu", "gpu"});
|
||
sttLayout->addRow("推理设备:", deviceCombo_);
|
||
|
||
threadSpin_ = new QSpinBox(this);
|
||
threadSpin_->setRange(1, 32);
|
||
threadSpin_->setValue(4);
|
||
sttLayout->addRow("推理线程数:", threadSpin_);
|
||
|
||
auto* tokensRow = new QHBoxLayout();
|
||
tokensPathEdit_ = new QLineEdit(this);
|
||
tokensPathEdit_->setPlaceholderText("选择 tokens.txt 文件路径...");
|
||
tokensBrowseBtn_ = new QPushButton("浏览...", this);
|
||
connect(tokensBrowseBtn_, &QPushButton::clicked, this, &SettingsPage::onBrowseTokensPath);
|
||
tokensRow->addWidget(tokensPathEdit_);
|
||
tokensRow->addWidget(tokensBrowseBtn_);
|
||
sttLayout->addRow("词表路径:", tokensRow);
|
||
|
||
sampleRateSpin_ = new QSpinBox(this);
|
||
sampleRateSpin_->setRange(8000, 192000);
|
||
sampleRateSpin_->setSingleStep(1000);
|
||
sampleRateSpin_->setValue(16000);
|
||
sampleRateSpin_->setSuffix(" Hz");
|
||
sttLayout->addRow("采样率:", sampleRateSpin_);
|
||
|
||
languageCombo_ = new QComboBox(this);
|
||
languageCombo_->addItems({"zh", "en", "ja", "ko", "fr", "de", "auto"});
|
||
sttLayout->addRow("识别语言:", languageCombo_);
|
||
|
||
streamingCheck_ = new QCheckBox("启用流式识别", this);
|
||
streamingCheck_->setChecked(true);
|
||
sttLayout->addRow("流式识别:", streamingCheck_);
|
||
|
||
debugSaveAudioCheck_ = new QCheckBox("保存调试音频到临时文件夹", this);
|
||
debugSaveAudioCheck_->setToolTip("开启后,每次识别会将原始音频保存为 WAV 文件到系统临时目录,用于调试音频质量问题");
|
||
sttLayout->addRow("调试录音:", debugSaveAudioCheck_);
|
||
|
||
// 快捷键录制按钮
|
||
hotkeyRecorder_ = new HotkeyRecorder("语音快捷键:", this);
|
||
hotkeyRecorder_->setToolTip("点击按钮后按下快捷键(如 Ctrl+Alt+K),支持组合键。Esc 取消录制。");
|
||
connect(hotkeyRecorder_, &HotkeyRecorder::hotkeyChanged,
|
||
this, [this](const QString& key) {
|
||
configManager_->set("shortcuts.voice_hotkey", key);
|
||
});
|
||
sttLayout->addRow(hotkeyRecorder_);
|
||
|
||
beamSizeSpin_ = new QSpinBox(this);
|
||
beamSizeSpin_->setRange(1, 20);
|
||
beamSizeSpin_->setValue(5);
|
||
sttLayout->addRow("Beam Size:", beamSizeSpin_);
|
||
|
||
temperatureSpin_ = new QDoubleSpinBox(this);
|
||
temperatureSpin_->setRange(0.0, 2.0);
|
||
temperatureSpin_->setSingleStep(0.1);
|
||
temperatureSpin_->setValue(0.0);
|
||
sttLayout->addRow("温度 (Temperature):", temperatureSpin_);
|
||
|
||
mainLayout->addWidget(sttGroup);
|
||
|
||
// 音频设置
|
||
auto* audioGroup = new QGroupBox("音频设置", this);
|
||
auto* audioLayout = new QFormLayout(audioGroup);
|
||
|
||
// 音频输入设备选择器
|
||
audioDeviceCombo_ = new QComboBox(this);
|
||
populateAudioDevices();
|
||
audioLayout->addRow("输入设备:", audioDeviceCombo_);
|
||
|
||
bufferSizeSpin_ = new QSpinBox(this);
|
||
bufferSizeSpin_->setRange(10, 100);
|
||
bufferSizeSpin_->setValue(20);
|
||
bufferSizeSpin_->setSuffix(" ms");
|
||
audioLayout->addRow("缓冲区大小:", bufferSizeSpin_);
|
||
|
||
chunkDurationSpin_ = new QSpinBox(this);
|
||
chunkDurationSpin_->setRange(500, 10000);
|
||
chunkDurationSpin_->setSingleStep(500);
|
||
chunkDurationSpin_->setValue(3000);
|
||
chunkDurationSpin_->setSuffix(" ms");
|
||
audioLayout->addRow("推理块时长:", chunkDurationSpin_);
|
||
|
||
paddingSpin_ = new QSpinBox(this);
|
||
paddingSpin_->setRange(0, 2000);
|
||
paddingSpin_->setSingleStep(100);
|
||
paddingSpin_->setValue(500);
|
||
paddingSpin_->setSuffix(" ms");
|
||
audioLayout->addRow("块间重叠:", paddingSpin_);
|
||
|
||
mainLayout->addWidget(audioGroup);
|
||
|
||
// UI 设置
|
||
auto* uiGroup = new QGroupBox("界面设置", this);
|
||
auto* uiLayout = new QFormLayout(uiGroup);
|
||
|
||
themeCombo_ = new QComboBox(this);
|
||
themeCombo_->addItems({"light", "dark"});
|
||
uiLayout->addRow("主题:", themeCombo_);
|
||
|
||
fontSizeSpin_ = new QSpinBox(this);
|
||
fontSizeSpin_->setRange(10, 24);
|
||
fontSizeSpin_->setValue(14);
|
||
uiLayout->addRow("字体大小:", fontSizeSpin_);
|
||
|
||
showWaveformCheck_ = new QCheckBox("显示波形", this);
|
||
showWaveformCheck_->setChecked(true);
|
||
uiLayout->addRow("波形显示:", showWaveformCheck_);
|
||
|
||
showConfidenceCheck_ = new QCheckBox("显示置信度", this);
|
||
showConfidenceCheck_->setChecked(true);
|
||
uiLayout->addRow("置信度显示:", showConfidenceCheck_);
|
||
|
||
mainLayout->addWidget(uiGroup);
|
||
|
||
// 操作按钮
|
||
auto* btnLayout = new QHBoxLayout();
|
||
auto* saveBtn = new QPushButton("保存配置", this);
|
||
saveBtn->setStyleSheet("QPushButton { font-weight: bold; padding: 8px 16px; }");
|
||
connect(saveBtn, &QPushButton::clicked, this, &SettingsPage::onSaveConfig);
|
||
btnLayout->addWidget(saveBtn);
|
||
|
||
auto* resetBtn = new QPushButton("恢复默认", this);
|
||
connect(resetBtn, &QPushButton::clicked, this, &SettingsPage::onResetConfig);
|
||
btnLayout->addWidget(resetBtn);
|
||
btnLayout->addStretch();
|
||
|
||
statusLabel_ = new QLabel("配置未修改", this);
|
||
statusLabel_->setStyleSheet("color: gray;");
|
||
btnLayout->addWidget(statusLabel_);
|
||
|
||
mainLayout->addLayout(btnLayout);
|
||
mainLayout->addStretch();
|
||
}
|
||
|
||
void SettingsPage::loadFromConfig() {
|
||
modelPathEdit_->setText(configManager_->get("stt.model_path").toString());
|
||
tokensPathEdit_->setText(configManager_->get("stt.tokens_path").toString());
|
||
modelTypeCombo_->setCurrentText(configManager_->get("stt.model_type").toString());
|
||
deviceCombo_->setCurrentText(configManager_->get("stt.device").toString());
|
||
threadSpin_->setValue(configManager_->get("stt.num_threads").toInt());
|
||
sampleRateSpin_->setValue(configManager_->get("stt.sample_rate").toInt());
|
||
languageCombo_->setCurrentText(configManager_->get("stt.language").toString());
|
||
streamingCheck_->setChecked(configManager_->get("stt.streaming").toBool());
|
||
debugSaveAudioCheck_->setChecked(configManager_->get("stt.debug_save_audio").toBool());
|
||
hotkeyRecorder_->setHotkeyText(configManager_->get("shortcuts.voice_hotkey").toString());
|
||
beamSizeSpin_->setValue(configManager_->get("stt.beam_size").toInt());
|
||
temperatureSpin_->setValue(configManager_->get("stt.temperature").toDouble());
|
||
|
||
bufferSizeSpin_->setValue(configManager_->get("audio.buffer_size_ms").toInt());
|
||
chunkDurationSpin_->setValue(configManager_->get("audio.chunk_duration_ms").toInt());
|
||
paddingSpin_->setValue(configManager_->get("audio.padding_ms").toInt());
|
||
|
||
// 恢复音频设备选择
|
||
int savedDevice = configManager_->get("audio.input_device").toInt();
|
||
selectAudioDevice(savedDevice);
|
||
|
||
themeCombo_->setCurrentText(configManager_->get("ui.theme").toString());
|
||
fontSizeSpin_->setValue(configManager_->get("ui.font_size").toInt());
|
||
showWaveformCheck_->setChecked(configManager_->get("ui.show_waveform").toBool());
|
||
showConfidenceCheck_->setChecked(configManager_->get("ui.show_confidence").toBool());
|
||
}
|
||
|
||
void SettingsPage::saveToConfig() {
|
||
// 批量写入所有配置,只发射一次 configChanged 信号
|
||
QMap<QString, QVariant> batch;
|
||
batch["stt.model_path"] = modelPathEdit_->text();
|
||
batch["stt.tokens_path"] = tokensPathEdit_->text();
|
||
batch["stt.model_type"] = modelTypeCombo_->currentText();
|
||
batch["stt.device"] = deviceCombo_->currentText();
|
||
batch["stt.num_threads"] = threadSpin_->value();
|
||
batch["stt.sample_rate"] = sampleRateSpin_->value();
|
||
batch["stt.language"] = languageCombo_->currentText();
|
||
batch["stt.streaming"] = streamingCheck_->isChecked();
|
||
batch["stt.debug_save_audio"] = debugSaveAudioCheck_->isChecked();
|
||
batch["stt.beam_size"] = beamSizeSpin_->value();
|
||
batch["stt.temperature"] = temperatureSpin_->value();
|
||
batch["shortcuts.voice_hotkey"] = hotkeyRecorder_->hotkeyText();
|
||
batch["audio.input_device"] = getSelectedAudioDeviceIndex();
|
||
batch["audio.buffer_size_ms"] = bufferSizeSpin_->value();
|
||
batch["audio.chunk_duration_ms"] = chunkDurationSpin_->value();
|
||
batch["audio.padding_ms"] = paddingSpin_->value();
|
||
batch["ui.theme"] = themeCombo_->currentText();
|
||
batch["ui.font_size"] = fontSizeSpin_->value();
|
||
batch["ui.show_waveform"] = showWaveformCheck_->isChecked();
|
||
batch["ui.show_confidence"] = showConfidenceCheck_->isChecked();
|
||
|
||
configManager_->setBatch(batch);
|
||
}
|
||
|
||
void SettingsPage::onBrowseModelPath() {
|
||
QString path = QFileDialog::getOpenFileName(this, "选择 ONNX 模型", "",
|
||
"ONNX 模型 (*.onnx);;所有文件 (*.*)");
|
||
if (!path.isEmpty()) {
|
||
modelPathEdit_->setText(path);
|
||
}
|
||
}
|
||
|
||
void SettingsPage::populateAudioDevices() {
|
||
audioDeviceCombo_->clear();
|
||
audioDeviceCombo_->addItem("默认设备", -1);
|
||
|
||
#ifdef HAVE_PORTAUDIO
|
||
// 直接使用 PortAudio 枚举所有输入设备
|
||
QStringList devices = AudioCapture::getDeviceList();
|
||
// 跳过第一个 "默认设备"(已手动添加)
|
||
for (int i = 1; i < devices.size(); i++) {
|
||
audioDeviceCombo_->addItem(devices[i], i - 1); // display text, PortAudio index
|
||
}
|
||
#else
|
||
audioDeviceCombo_->addItem("PortAudio 未启用", -1);
|
||
#endif
|
||
}
|
||
|
||
void SettingsPage::selectAudioDevice(int deviceIndex) {
|
||
// deviceIndex == -1 表示默认设备,对应 combo 的第一项(index 0)
|
||
if (deviceIndex < 0) {
|
||
audioDeviceCombo_->setCurrentIndex(0);
|
||
} else {
|
||
// 在 combo 中查找 data == deviceIndex 的项
|
||
for (int i = 0; i < audioDeviceCombo_->count(); i++) {
|
||
if (audioDeviceCombo_->itemData(i).toInt() == deviceIndex) {
|
||
audioDeviceCombo_->setCurrentIndex(i);
|
||
return;
|
||
}
|
||
}
|
||
// 如果没找到,使用默认设备
|
||
audioDeviceCombo_->setCurrentIndex(0);
|
||
}
|
||
}
|
||
|
||
int SettingsPage::getSelectedAudioDeviceIndex() const {
|
||
return audioDeviceCombo_->currentData().toInt();
|
||
}
|
||
|
||
void SettingsPage::onBrowseTokensPath() {
|
||
QString path = QFileDialog::getOpenFileName(this, "选择词表文件", "",
|
||
"词表文件 (tokens.txt);;所有文件 (*.*)");
|
||
if (!path.isEmpty()) {
|
||
tokensPathEdit_->setText(path);
|
||
}
|
||
}
|
||
|
||
void SettingsPage::onSaveConfig() {
|
||
saveToConfig();
|
||
if (configManager_->save()) {
|
||
statusLabel_->setText(QString("配置已保存到: %1").arg(configManager_->configPath()));
|
||
LOG_INFO(kTag, QString("配置已持久化: %1").arg(configManager_->configPath()));
|
||
} else {
|
||
statusLabel_->setText("配置保存失败,请检查路径权限");
|
||
LOG_ERROR(kTag, "配置持久化失败");
|
||
}
|
||
}
|
||
|
||
void SettingsPage::onResetConfig() {
|
||
auto reply = QMessageBox::question(this, "确认", "确定要恢复默认配置吗?",
|
||
QMessageBox::Yes | QMessageBox::No);
|
||
if (reply == QMessageBox::Yes) {
|
||
configManager_->resetToDefaults();
|
||
loadFromConfig();
|
||
statusLabel_->setText("已恢复默认配置");
|
||
}
|
||
}
|
||
|
||
} // namespace impress
|