From ef97b962c36adc484cfab08b73435a3b8f2a0d99 Mon Sep 17 00:00:00 2001 From: impressionyang Date: Wed, 13 May 2026 14:10:31 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E5=85=A8=E5=B1=80=E5=85=B1?= =?UTF-8?q?=E4=BA=AB=20STT=20=E6=A8=A1=E5=9E=8B=EF=BC=8C=E9=81=BF=E5=85=8D?= =?UTF-8?q?=E9=87=8D=E5=A4=8D=E5=8A=A0=E8=BD=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 SenseVoiceEngine 提升为 Application 级别的全局单例,应用启动时 异步加载一次模型,实时语音识别、文件转写和快捷键语音输入共享同一实例。 - Application 创建并管理全局 SenseVoiceEngine,启动时加载模型 - STTTestPage、FileTranscribePage、VoiceInputService 不再各自 创建引擎,改为接收全局实例 - 移除各模块中冗余的 loadModel/loadModelAsync/unloadModel 调用 - 模型未加载时提供友好的等待提示,而非加载失败的错误弹窗 Co-Authored-By: Claude Opus 4.6 --- src/app/application.cpp | 43 ++++++++++++++++++++++++ src/app/application.h | 21 ++++++++++++ src/core/voice_input_service.cpp | 33 +++--------------- src/core/voice_input_service.h | 4 ++- src/main.cpp | 4 +-- src/ui/file_transcribe_page.cpp | 57 ++++++++++---------------------- src/ui/file_transcribe_page.h | 4 ++- src/ui/main_window.cpp | 17 ++++++---- src/ui/main_window.h | 9 +++-- src/ui/stt_test_page.cpp | 42 ++++++----------------- src/ui/stt_test_page.h | 5 +-- 11 files changed, 124 insertions(+), 115 deletions(-) diff --git a/src/app/application.cpp b/src/app/application.cpp index d069d3d..162d6d7 100644 --- a/src/app/application.cpp +++ b/src/app/application.cpp @@ -1,5 +1,6 @@ #include "application.h" #include "config_manager.h" +#include "core/sense_voice_engine.h" #include "utils/logger.h" #include @@ -14,6 +15,22 @@ Application::Application(int& argc, char** argv) configManager_ = std::make_unique(this); configManager_->loadDefaults(); + + // 创建全局 STT 引擎(共享实例) + sttEngine_ = new SenseVoiceEngine(this); + connect(sttEngine_, &SenseVoiceEngine::modelLoaded, this, [this](const QString& path) { + modelLoaded_ = true; + LOG_INFO(kTag, QString("全局模型已加载: %1").arg(path)); + emit modelLoaded(); + }); + connect(sttEngine_, &SenseVoiceEngine::modelLoadError, this, [this](const QString&, const QString& err) { + modelLoaded_ = false; + LOG_ERROR(kTag, QString("全局模型加载失败: %1").arg(err)); + emit modelLoadError(err); + }); + + // 异步加载全局模型 + loadGlobalModel(); } Application::~Application() { @@ -25,4 +42,30 @@ ConfigManager* Application::configManager() const { return configManager_.get(); } +SenseVoiceEngine* Application::sttEngine() const { + return sttEngine_; +} + +bool Application::isModelLoaded() const { + return modelLoaded_; +} + +void Application::loadGlobalModel() { + QString modelPath = configManager_->get("stt.model_path").toString(); + if (modelPath.isEmpty()) { + LOG_WARNING(kTag, "模型路径为空,请在配置中设置后重启"); + return; + } + + QString tokensPath = configManager_->get("stt.tokens_path").toString(); + QString device = configManager_->get("stt.device").toString(); + int numThreads = configManager_->get("stt.num_threads").toInt(); + + bool debugSave = configManager_->get("stt.debug_save_audio").toBool(); + sttEngine_->setDebugSaveAudio(debugSave); + + LOG_INFO(kTag, QString("正在异步加载全局模型: %1").arg(modelPath)); + sttEngine_->loadModelAsync(modelPath, tokensPath, device, numThreads); +} + } // namespace impress diff --git a/src/app/application.h b/src/app/application.h index a054c18..ad4b410 100644 --- a/src/app/application.h +++ b/src/app/application.h @@ -6,9 +6,13 @@ namespace impress { class ConfigManager; +class SenseVoiceEngine; /** * @brief 应用入口封装 + * + * 管理全局共享组件:配置管理器、STT 引擎。 + * STT 引擎在启动时异步加载模型,所有页面和服务共享同一实例。 */ class Application : public QApplication { Q_OBJECT @@ -19,8 +23,25 @@ public: /** @brief 获取全局配置管理器 */ ConfigManager* configManager() const; + /** @brief 获取全局 STT 引擎(共享实例) */ + SenseVoiceEngine* sttEngine() const; + + /** @brief 获取全局 STT 引擎加载状态 */ + bool isModelLoaded() const; + +signals: + /** @brief 模型加载完成 */ + void modelLoaded(); + + /** @brief 模型加载失败 */ + void modelLoadError(const QString& error); + private: + void loadGlobalModel(); + std::unique_ptr configManager_; + SenseVoiceEngine* sttEngine_ = nullptr; + bool modelLoaded_ = false; }; } // namespace impress diff --git a/src/core/voice_input_service.cpp b/src/core/voice_input_service.cpp index ae1f1ba..6ef0910 100644 --- a/src/core/voice_input_service.cpp +++ b/src/core/voice_input_service.cpp @@ -31,11 +31,14 @@ struct VoiceInputService::Impl { WaylandTextInjector* injector = nullptr; }; -VoiceInputService::VoiceInputService(ConfigManager* configManager, QObject* parent) +VoiceInputService::VoiceInputService(ConfigManager* configManager, + SenseVoiceEngine* sttEngine, + QObject* parent) : QObject(parent) , configManager_(configManager) , impl_(std::make_unique()) { + impl_->sttEngine = sttEngine; longPressTimer_ = new QTimer(this); longPressTimer_->setSingleShot(true); connect(longPressTimer_, &QTimer::timeout, this, [this]() { @@ -59,30 +62,7 @@ bool VoiceInputService::start() { connect(impl_->audioCapture, &AudioCapture::audioDataReady, this, &VoiceInputService::onAudioData); - // 2. 初始化 STT 引擎并加载模型 - impl_->sttEngine = new SenseVoiceEngine(this); - - // 从配置加载模型 - QString modelPath = configManager_->get("stt.model_path").toString(); - QString tokensPath = configManager_->get("stt.tokens_path").toString(); - QString device = configManager_->get("stt.device").toString(); - int numThreads = configManager_->get("stt.num_threads").toInt(); - - if (!modelPath.isEmpty()) { - LOG_INFO(kTag, QString("正在加载 STT 模型: %1").arg(modelPath)); - bool modelLoaded = impl_->sttEngine->loadModelSync(modelPath, tokensPath, device, numThreads); - if (!modelLoaded) { - emit error(QString("STT 模型加载失败: %1").arg(modelPath)); - LOG_ERROR(kTag, "STT 模型加载失败"); - } else { - LOG_INFO(kTag, "STT 模型加载成功"); - // 同步调试音频设置 - bool debugSave = configManager_->get("stt.debug_save_audio").toBool(); - impl_->sttEngine->setDebugSaveAudio(debugSave); - } - } else { - LOG_WARNING(kTag, "模型路径为空,请先在配置中设置模型路径"); - } + // 2. STT 引擎已作为参数传入(共享全局实例) // 3. 初始化全局快捷键 impl_->hotkey = new CapsLockVoiceHotkey(this); @@ -124,9 +104,6 @@ void VoiceInputService::stop() { if (impl_->audioCapture) { impl_->audioCapture->stop(); } - if (impl_->sttEngine) { - impl_->sttEngine->unloadModel(); - } if (impl_->hotkey) { impl_->hotkey->stop(); } diff --git a/src/core/voice_input_service.h b/src/core/voice_input_service.h index c4fffe3..096d1dd 100644 --- a/src/core/voice_input_service.h +++ b/src/core/voice_input_service.h @@ -27,7 +27,9 @@ class ConfigManager; class VoiceInputService : public QObject { Q_OBJECT public: - explicit VoiceInputService(ConfigManager* configManager, QObject* parent = nullptr); + explicit VoiceInputService(ConfigManager* configManager, + SenseVoiceEngine* sttEngine, + QObject* parent = nullptr); ~VoiceInputService() override; /** @brief 启动服务(初始化所有组件) */ diff --git a/src/main.cpp b/src/main.cpp index d292535..10f99b5 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -56,8 +56,8 @@ int main(int argc, char* argv[]) configManager->set("stt.model_path", modelPath); } - // 创建并显示主窗口 - impress::MainWindow mainWindow(configManager); + // 创建并显示主窗口(传入全局引擎) + impress::MainWindow mainWindow(configManager, app.sttEngine()); mainWindow.show(); return app.exec(); diff --git a/src/ui/file_transcribe_page.cpp b/src/ui/file_transcribe_page.cpp index df5011c..5ff9f0c 100644 --- a/src/ui/file_transcribe_page.cpp +++ b/src/ui/file_transcribe_page.cpp @@ -31,10 +31,12 @@ static const char* const kTag = "FileTranscribePage"; namespace impress { -FileTranscribePage::FileTranscribePage(ConfigManager* configManager, QWidget* parent) +FileTranscribePage::FileTranscribePage(ConfigManager* configManager, + SenseVoiceEngine* sttEngine, + QWidget* parent) : QWidget(parent) , configManager_(configManager) - , sttEngine_(new SenseVoiceEngine(this)) + , sttEngine_(sttEngine) , audioDecoder_(new AudioDecoder(this)) { setupUI(); @@ -148,46 +150,25 @@ void FileTranscribePage::onStartTranscribe() { return; } - QString modelPath = configManager_->get("stt.model_path").toString(); - if (modelPath.isEmpty()) { - QMessageBox::warning(this, "提示", "请先在配置页面设置模型路径"); + // 检查全局模型是否已加载 + if (!sttEngine_->isLoaded()) { + QMessageBox::warning(this, "提示", + "模型尚未加载完成,请稍候再试"); return; } - // 在后台线程中加载模型(不阻塞 UI) - statusLabel_->setText("正在加载模型..."); - startBtn_->setEnabled(false); - activeWorkers_ = 1; // 标记正在加载模型 + // 从配置同步调试开关到引擎 + sttEngine_->setDebugSaveAudio( + configManager_->get("stt.debug_save_audio").toBool()); - (void)QtConcurrent::run([this, modelPath]() { - bool success = sttEngine_->loadModelSync(modelPath, - configManager_->get("stt.tokens_path").toString(), - configManager_->get("stt.device").toString(), - configManager_->get("stt.num_threads").toInt()); + isTranscribing_ = true; + currentTaskIndex_ = 0; + progressBar_->setVisible(true); + updateUIState(); + statusLabel_->setText("开始批量转写..."); - QMetaObject::invokeMethod(this, [this, success]() { - activeWorkers_--; - if (!success) { - QMessageBox::critical(this, "错误", "模型加载失败"); - statusLabel_->setText("模型加载失败"); - startBtn_->setEnabled(true); - return; - } - - // 从配置同步调试开关到引擎 - sttEngine_->setDebugSaveAudio( - configManager_->get("stt.debug_save_audio").toBool()); - - isTranscribing_ = true; - currentTaskIndex_ = 0; - progressBar_->setVisible(true); - updateUIState(); - statusLabel_->setText("开始批量转写..."); - - // 启动后台转写队列 - startBatchTranscription(); - }, Qt::QueuedConnection); - }); + // 启动后台转写队列 + startBatchTranscription(); } void FileTranscribePage::onStopTranscribe() { @@ -195,7 +176,6 @@ void FileTranscribePage::onStopTranscribe() { activeWorkers_ = 0; progressBar_->setVisible(false); statusLabel_->setText("已停止"); - sttEngine_->unloadModel(); updateUIState(); } @@ -296,7 +276,6 @@ void FileTranscribePage::onAllComplete() { isTranscribing_ = false; statusLabel_->setText("全部完成"); progressBar_->setVisible(false); - sttEngine_->unloadModel(); updateUIState(); } diff --git a/src/ui/file_transcribe_page.h b/src/ui/file_transcribe_page.h index b9f1bdf..ac72671 100644 --- a/src/ui/file_transcribe_page.h +++ b/src/ui/file_transcribe_page.h @@ -36,7 +36,9 @@ struct TranscribeTask { class FileTranscribePage : public QWidget { Q_OBJECT public: - explicit FileTranscribePage(ConfigManager* configManager, QWidget* parent = nullptr); + explicit FileTranscribePage(ConfigManager* configManager, + SenseVoiceEngine* sttEngine, + QWidget* parent = nullptr); ~FileTranscribePage() override; private slots: diff --git a/src/ui/main_window.cpp b/src/ui/main_window.cpp index 268f5f2..1d0baf9 100644 --- a/src/ui/main_window.cpp +++ b/src/ui/main_window.cpp @@ -3,6 +3,7 @@ #include "file_transcribe_page.h" #include "settings_page.h" #include "core/voice_input_service.h" +#include "core/sense_voice_engine.h" #include "app/config_manager.h" #include "utils/logger.h" @@ -16,19 +17,21 @@ static const char* const kTag = "MainWindow"; namespace impress { -MainWindow::MainWindow(ConfigManager* configManager, QWidget* parent) +MainWindow::MainWindow(ConfigManager* configManager, + SenseVoiceEngine* sttEngine, + QWidget* parent) : QMainWindow(parent) , configManager_(configManager) { setWindowTitle("Impress Voice Input"); resize(1000, 700); - setupUI(); + setupUI(sttEngine); setupMenuBar(); loadStyleSheet(); - // 初始化语音输入服务 - voiceInputService_ = new VoiceInputService(configManager_, this); + // 初始化语音输入服务(共享全局引擎) + voiceInputService_ = new VoiceInputService(configManager_, sttEngine, this); connect(voiceInputService_, &VoiceInputService::statusChanged, this, [this](const QString& status) { LOG_DEBUG(kTag, QString("语音输入状态: %1").arg(status)); @@ -54,11 +57,11 @@ MainWindow::MainWindow(ConfigManager* configManager, QWidget* parent) MainWindow::~MainWindow() = default; -void MainWindow::setupUI() { +void MainWindow::setupUI(SenseVoiceEngine* sttEngine) { tabWidget_ = new QTabWidget(this); - sttPage_ = new STTTestPage(configManager_, tabWidget_); - transcribePage_ = new FileTranscribePage(configManager_, tabWidget_); + sttPage_ = new STTTestPage(configManager_, sttEngine, tabWidget_); + transcribePage_ = new FileTranscribePage(configManager_, sttEngine, tabWidget_); settingsPage_ = new SettingsPage(configManager_, tabWidget_); tabWidget_->addTab(sttPage_, "实时语音识别"); diff --git a/src/ui/main_window.h b/src/ui/main_window.h index 5f34736..f5d6b70 100644 --- a/src/ui/main_window.h +++ b/src/ui/main_window.h @@ -7,6 +7,7 @@ namespace impress { class ConfigManager; +class SenseVoiceEngine; class STTTestPage; class FileTranscribePage; class SettingsPage; @@ -15,19 +16,21 @@ class VoiceInputService; /** * @brief 主窗口 * - * 使用 Tab 页导航管理三个功能页面。 + * 使用 Tab 页导航管理三个功能页面,共享全局 STT 引擎。 */ class MainWindow : public QMainWindow { Q_OBJECT public: - explicit MainWindow(ConfigManager* configManager, QWidget* parent = nullptr); + explicit MainWindow(ConfigManager* configManager, + SenseVoiceEngine* sttEngine, + QWidget* parent = nullptr); ~MainWindow() override; protected: void closeEvent(QCloseEvent* event) override; private: - void setupUI(); + void setupUI(SenseVoiceEngine* sttEngine); void setupMenuBar(); void loadStyleSheet(); void onVoiceInputConfigChanged(); diff --git a/src/ui/stt_test_page.cpp b/src/ui/stt_test_page.cpp index 05374ab..4265250 100644 --- a/src/ui/stt_test_page.cpp +++ b/src/ui/stt_test_page.cpp @@ -25,10 +25,12 @@ static const char* const kTag = "STTTestPage"; namespace impress { -STTTestPage::STTTestPage(ConfigManager* configManager, QWidget* parent) +STTTestPage::STTTestPage(ConfigManager* configManager, + SenseVoiceEngine* sttEngine, + QWidget* parent) : QWidget(parent) , configManager_(configManager) - , sttEngine_(new SenseVoiceEngine(this)) + , sttEngine_(sttEngine) , audioCapture_(new AudioCapture(this)) , inferenceTimer_(new QTimer(this)) { @@ -122,16 +124,14 @@ void STTTestPage::onToggleRecording() { if (isRecording_) { audioCapture_->stop(); inferenceTimer_->stop(); - sttEngine_->unloadModel(); isRecording_ = false; isInferencing_ = false; audioBuffer_.clear(); } else { - // 读取配置 - QString modelPath = configManager_->get("stt.model_path").toString(); - if (modelPath.isEmpty()) { + // 检查全局模型是否已加载 + if (!sttEngine_->isLoaded()) { QMessageBox::warning(this, "提示", - "请先在「配置」页面设置模型路径并保存"); + "模型尚未加载完成,请稍候再试"); return; } @@ -139,38 +139,17 @@ void STTTestPage::onToggleRecording() { sttEngine_->setDebugSaveAudio( configManager_->get("stt.debug_save_audio").toBool()); - // 异步加载模型 - if (!sttEngine_->isLoaded() || - currentModelPath_ != modelPath) { - isLoadingModel_ = true; - statusLabel_->setText("正在加载模型,请稍候..."); - updateUIState(); - - sttEngine_->loadModelAsync(modelPath, - configManager_->get("stt.tokens_path").toString(), - configManager_->get("stt.device").toString(), - configManager_->get("stt.num_threads").toInt()); - - currentModelPath_ = modelPath; - // 注意:startAudioCapture() 将在 onModelLoaded() 回调中调用 - } else { - startAudioCapture(); - } + startAudioCapture(); } updateUIState(); } void STTTestPage::onModelLoaded(const QString& modelPath) { - LOG_INFO(kTag, QString("模型加载成功: %1").arg(modelPath)); + LOG_INFO(kTag, QString("全局模型加载成功: %1").arg(modelPath)); isLoadingModel_ = false; statusLabel_->setText(QString("模型就绪: %1").arg( QFileInfo(modelPath).fileName())); updateUIState(); - - // 如果用户仍在录音状态(已切换 UI),启动采集 - if (!isRecording_) { - startAudioCapture(); - } } void STTTestPage::onModelLoadError(const QString& modelPath, const QString& error) { @@ -206,8 +185,7 @@ void STTTestPage::startAudioCapture() { // 启动周期性推理定时器 startInferenceTimer(); - statusLabel_->setText(QString("录音中 | 模型: %1").arg( - QFileInfo(currentModelPath_).fileName())); + statusLabel_->setText("录音中 | 模型已加载"); updateUIState(); } diff --git a/src/ui/stt_test_page.h b/src/ui/stt_test_page.h index 9b9ddad..69da3b5 100644 --- a/src/ui/stt_test_page.h +++ b/src/ui/stt_test_page.h @@ -27,7 +27,9 @@ class AudioCapture; class STTTestPage : public QWidget { Q_OBJECT public: - explicit STTTestPage(ConfigManager* configManager, QWidget* parent = nullptr); + explicit STTTestPage(ConfigManager* configManager, + SenseVoiceEngine* sttEngine, + QWidget* parent = nullptr); ~STTTestPage() override; private slots: @@ -64,7 +66,6 @@ private: bool isInferencing_ = false; int audioSampleRate_ = 16000; std::vector audioBuffer_; - QString currentModelPath_; }; } // namespace impress