refactor: 全局共享 STT 模型,避免重复加载
将 SenseVoiceEngine 提升为 Application 级别的全局单例,应用启动时 异步加载一次模型,实时语音识别、文件转写和快捷键语音输入共享同一实例。 - Application 创建并管理全局 SenseVoiceEngine,启动时加载模型 - STTTestPage、FileTranscribePage、VoiceInputService 不再各自 创建引擎,改为接收全局实例 - 移除各模块中冗余的 loadModel/loadModelAsync/unloadModel 调用 - 模型未加载时提供友好的等待提示,而非加载失败的错误弹窗 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
85a0890478
commit
ef97b962c3
@ -1,5 +1,6 @@
|
||||
#include "application.h"
|
||||
#include "config_manager.h"
|
||||
#include "core/sense_voice_engine.h"
|
||||
#include "utils/logger.h"
|
||||
#include <QFile>
|
||||
|
||||
@ -14,6 +15,22 @@ Application::Application(int& argc, char** argv)
|
||||
|
||||
configManager_ = std::make_unique<ConfigManager>(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
|
||||
|
||||
@ -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> configManager_;
|
||||
SenseVoiceEngine* sttEngine_ = nullptr;
|
||||
bool modelLoaded_ = false;
|
||||
};
|
||||
|
||||
} // namespace impress
|
||||
|
||||
@ -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>())
|
||||
{
|
||||
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();
|
||||
}
|
||||
|
||||
@ -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 启动服务(初始化所有组件) */
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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_, "实时语音识别");
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
|
||||
@ -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<float> audioBuffer_;
|
||||
QString currentModelPath_;
|
||||
};
|
||||
|
||||
} // namespace impress
|
||||
|
||||
Loading…
Reference in New Issue
Block a user