diff --git a/CMakeLists.txt b/CMakeLists.txt index 9d1e190..817b0d3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,7 +28,15 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") # Qt 6 -find_package(Qt6 REQUIRED COMPONENTS Core Widgets Concurrent Network DBus) +find_package(Qt6 REQUIRED COMPONENTS Core Widgets Concurrent Network) + +# Windows 不需要 D-Bus,Linux/macOS 按需加载 +if(WIN32) + find_package(Qt6 REQUIRED COMPONENTS Gui) +else() + find_package(Qt6 REQUIRED COMPONENTS DBus) +endif() + qt_standard_project_setup() # ONNX Runtime @@ -44,7 +52,7 @@ set(SOURCES src/app/application.cpp src/app/config_manager.cpp - # Core + # Core (平台无关) src/core/stt_engine.cpp src/core/sense_voice_engine.cpp src/core/sense_voice_features.cpp @@ -53,8 +61,6 @@ set(SOURCES src/core/whisper_tokenizer.cpp src/core/audio_processor.cpp src/core/vad.cpp - src/core/caps_lock_voice_hotkey.cpp - src/core/wayland_text_injector.cpp src/core/voice_input_service.cpp # Audio @@ -90,8 +96,6 @@ set(HEADERS src/core/whisper_tokenizer.h src/core/audio_processor.h src/core/vad.h - src/core/caps_lock_voice_hotkey.h - src/core/wayland_text_injector.h src/core/voice_input_service.h src/audio/audio_capture.h @@ -112,8 +116,24 @@ set(HEADERS ) # ============================================================================ -# 可执行文件 +# 平台特定源文件 # ============================================================================ +if(WIN32) + # Windows 实现(RegisterHotKey + SendInput) + list(APPEND SOURCES src/core/win_hotkey.cpp src/core/win_text_injector.cpp) + list(APPEND HEADERS src/core/win_hotkey.h src/core/win_text_injector.h) + add_compile_definitions(PLATFORM_WINDOWS) +elseif(APPLE) + # macOS 实现(待完善:Carbon EventManager / CGEvent) + list(APPEND SOURCES src/core/mac_hotkey.cpp src/core/mac_text_injector.cpp) + list(APPEND HEADERS src/core/mac_hotkey.h src/core/mac_text_injector.h) + add_compile_definitions(PLATFORM_MACOS) +else() + # Linux 实现(D-Bus Portal + XTest) + list(APPEND SOURCES src/core/caps_lock_voice_hotkey.cpp src/core/wayland_text_injector.cpp) + list(APPEND HEADERS src/core/caps_lock_voice_hotkey.h src/core/wayland_text_injector.h) + add_compile_definitions(PLATFORM_LINUX) +endif() add_executable(${PROJECT_NAME} ${SOURCES} ${HEADERS}) target_include_directories(${PROJECT_NAME} PRIVATE @@ -130,10 +150,11 @@ target_link_libraries(${PROJECT_NAME} PRIVATE Qt6::Widgets Qt6::Concurrent Qt6::Network - Qt6::DBus + $<$:Qt6::Gui> + $<$>:Qt6::DBus> ${ONNXRUNTIME_LIBRARIES} ${PORTAUDIO_LIBRARIES} - pthread + $<$>:pthread> ) target_compile_options(${PROJECT_NAME} PRIVATE diff --git a/src/core/mac_hotkey.cpp b/src/core/mac_hotkey.cpp new file mode 100644 index 0000000..9f606ea --- /dev/null +++ b/src/core/mac_hotkey.cpp @@ -0,0 +1,34 @@ +#include "mac_hotkey.h" +#include "utils/logger.h" + +static const char* const kTag = "CapsLockVoiceHotkey"; + +namespace impress { + +struct CapsLockVoiceHotkey::Impl {}; + +CapsLockVoiceHotkey::CapsLockVoiceHotkey(QObject* parent) + : QObject(parent) + , impl_(std::make_unique()) +{} + +CapsLockVoiceHotkey::~CapsLockVoiceHotkey() { + stop(); +} + +bool CapsLockVoiceHotkey::start() { + if (active_) return true; + // macOS: 使用 CGEventTap 或 Carbon EventManager 实现 + LOG_WARNING(kTag, "macOS 全局快捷键尚未实现"); + emit error("macOS 全局快捷键尚未实现(待完善 CGEventTap)"); + return false; +} + +void CapsLockVoiceHotkey::stop() { + if (!active_) return; + active_ = false; + recording_ = false; + LOG_INFO(kTag, "CapsLock 快捷键已停止"); +} + +} // namespace impress diff --git a/src/core/mac_hotkey.h b/src/core/mac_hotkey.h new file mode 100644 index 0000000..b376634 --- /dev/null +++ b/src/core/mac_hotkey.h @@ -0,0 +1,33 @@ +#pragma once + +#include +#include +#include + +namespace impress { + +class CapsLockVoiceHotkey : public QObject { + Q_OBJECT +public: + explicit CapsLockVoiceHotkey(QObject* parent = nullptr); + ~CapsLockVoiceHotkey() override; + + bool start(); + void stop(); + bool isActive() const { return active_; } + bool isRecording() const { return recording_; } + +signals: + void recordingStarted(); + void recordingStopped(); + void ready(); + void error(const QString& message); + +private: + struct Impl; + std::unique_ptr impl_; + bool active_ = false; + bool recording_ = false; +}; + +} // namespace impress diff --git a/src/core/mac_text_injector.cpp b/src/core/mac_text_injector.cpp new file mode 100644 index 0000000..1aa3440 --- /dev/null +++ b/src/core/mac_text_injector.cpp @@ -0,0 +1,103 @@ +#include "mac_text_injector.h" +#include "utils/logger.h" + +#ifdef Q_OS_MACOS +#include +#endif + +#include + +static const char* const kTag = "WaylandTextInjector"; + +namespace impress { + +WaylandTextInjector::WaylandTextInjector(QObject* parent) + : QObject(parent) +{} + +WaylandTextInjector::~WaylandTextInjector() = default; + +bool WaylandTextInjector::initialize() { + if (initialized_) return true; + +#ifdef Q_OS_MACOS + // macOS: 需要辅助功能权限 + initialized_ = true; + LOG_INFO(kTag, "macOS 文本注入器已初始化(需要辅助功能权限)"); + return true; +#else + LOG_ERROR(kTag, "WaylandTextInjector 仅支持 macOS 平台"); + return false; +#endif +} + +bool WaylandTextInjector::injectText(const QString& text) { + if (!initialized_) { + LOG_ERROR(kTag, "文本注入器未初始化"); + return false; + } + + if (text.isEmpty()) return true; + + LOG_DEBUG(kTag, QString("注入文本 (%1 字符): %2").arg(text.length()).arg(text)); + + for (int i = 0; i < text.length(); i++) { + if (!injectChar(text[i])) { + LOG_WARNING(kTag, QString("字符注入失败: '%1'").arg(text[i])); + } + QThread::usleep(10000); + } + + LOG_DEBUG(kTag, "文本注入完成"); + return true; +} + +bool WaylandTextInjector::injectChar(QChar ch) { +#ifdef Q_OS_MACOS + CGEventSourceRef source = CGEventSourceCreate(kCGEventSourceStateHIDSystemState); + if (!source) return false; + + UniChar code = ch.unicode(); + + // 按键按下 + CGEventRef keyDown = CGEventCreateKeyboardEvent(source, 0, true); + CGEventKeyboardSetUnicodeString(keyDown, 1, &code); + CGEventPost(kCGHIDEventTap, keyDown); + CFRelease(keyDown); + + // 按键释放 + CGEventRef keyUp = CGEventCreateKeyboardEvent(source, 0, false); + CGEventKeyboardSetUnicodeString(keyUp, 1, &code); + CGEventPost(kCGHIDEventTap, keyUp); + CFRelease(keyUp); + + CFRelease(source); + return true; +#else + (void)ch; + return false; +#endif +} + +bool WaylandTextInjector::simulateKeycode(unsigned int keycode) { +#ifdef Q_OS_MACOS + CGEventSourceRef source = CGEventSourceCreate(kCGEventSourceStateHIDSystemState); + if (!source) return false; + + CGEventRef keyDown = CGEventCreateKeyboardEvent(source, (CGKeyCode)keycode, true); + CGEventPost(kCGHIDEventTap, keyDown); + CFRelease(keyDown); + + CGEventRef keyUp = CGEventCreateKeyboardEvent(source, (CGKeyCode)keycode, false); + CGEventPost(kCGHIDEventTap, keyUp); + CFRelease(keyUp); + + CFRelease(source); + return true; +#else + (void)keycode; + return false; +#endif +} + +} // namespace impress diff --git a/src/core/mac_text_injector.h b/src/core/mac_text_injector.h new file mode 100644 index 0000000..70603cf --- /dev/null +++ b/src/core/mac_text_injector.h @@ -0,0 +1,29 @@ +#pragma once + +#include +#include +#include + +namespace impress { + +class WaylandTextInjector : public QObject { + Q_OBJECT +public: + explicit WaylandTextInjector(QObject* parent = nullptr); + ~WaylandTextInjector() override; + + bool initialize(); + bool injectText(const QString& text); + bool isInitialized() const { return initialized_; } + bool simulateKeycode(unsigned int keycode); + +signals: + void error(const QString& message); + +private: + bool initialized_ = false; + + bool injectChar(QChar ch); +}; + +} // namespace impress diff --git a/src/core/sense_voice_engine.cpp b/src/core/sense_voice_engine.cpp index e0138b3..a05ef12 100644 --- a/src/core/sense_voice_engine.cpp +++ b/src/core/sense_voice_engine.cpp @@ -346,7 +346,7 @@ RecognitionResult SenseVoiceEngine::infer(const std::vector& samples, // 调试模式:保存原始音频到 WAV 文件 if (debugSaveAudio_ && !samples.empty()) { - QString debugDir = "/tmp/impress_audio_debug"; + QString debugDir = QDir::tempPath() + "/impress_audio_debug"; QDir dir; if (!dir.exists(debugDir)) { dir.mkpath(debugDir); diff --git a/src/core/voice_input_service.cpp b/src/core/voice_input_service.cpp index 1ad2035..ae1f1ba 100644 --- a/src/core/voice_input_service.cpp +++ b/src/core/voice_input_service.cpp @@ -1,11 +1,21 @@ #include "voice_input_service.h" #include "audio/audio_capture.h" #include "sense_voice_engine.h" -#include "caps_lock_voice_hotkey.h" -#include "wayland_text_injector.h" #include "app/config_manager.h" #include "utils/logger.h" +// 平台特定的快捷键和文本注入 +#ifdef PLATFORM_WINDOWS +#include "win_hotkey.h" +#include "win_text_injector.h" +#elif defined(PLATFORM_MACOS) +#include "mac_hotkey.h" +#include "mac_text_injector.h" +#else +#include "caps_lock_voice_hotkey.h" +#include "wayland_text_injector.h" +#endif + #include #include #include diff --git a/src/core/win_hotkey.cpp b/src/core/win_hotkey.cpp new file mode 100644 index 0000000..c10d952 --- /dev/null +++ b/src/core/win_hotkey.cpp @@ -0,0 +1,215 @@ +#include "win_hotkey.h" +#include "utils/logger.h" + +#ifdef Q_OS_WIN +#include +#include +#include +#endif + +static const char* const kTag = "CapsLockVoiceHotkey"; + +namespace impress { + +struct CapsLockVoiceHotkey::Impl { +#ifdef Q_OS_WIN + int hotkeyId = 0; + QTimer* longPressTimer = nullptr; + QTimer* keyUpDebounce = nullptr; + bool isHolding = false; + bool longPressFired = false; + bool pollThreadRunning = false; + void* nativeEventFilter = nullptr; + static constexpr int kLongPressMs = 1000; +#endif +}; + +#ifdef Q_OS_WIN +/** Native event filter to catch WM_HOTKEY */ +class HotkeyNativeEventFilter : public QAbstractNativeEventFilter { +public: + explicit HotkeyNativeEventFilter(CapsLockVoiceHotkey* hotkey) + : hotkey_(hotkey) {} + + bool nativeEventFilter(const QByteArray& eventType, void* message, + qintptr* /*result*/) override { + if (eventType == "windows_generic_MSG" || eventType == "windows_dispatcher_MSG") { + auto* msg = static_cast(message); + if (msg->message == WM_HOTKEY) { + if (hotkey_) { + hotkey_->onHotkeyEvent(static_cast(msg->wParam)); + } + return true; + } + } + return false; + } + +private: + CapsLockVoiceHotkey* hotkey_ = nullptr; +}; +#endif + +CapsLockVoiceHotkey::CapsLockVoiceHotkey(QObject* parent) + : QObject(parent) + , impl_(std::make_unique()) +{} + +CapsLockVoiceHotkey::~CapsLockVoiceHotkey() { + stop(); +} + +bool CapsLockVoiceHotkey::start() { + if (active_) return true; + +#ifdef Q_OS_WIN + HWND hwnd = reinterpret_cast(QGuiApplication::instance()->winId()); + if (!hwnd) { + // Try to get the top-level widget's window handle + hwnd = GetForegroundWindow(); + } + + if (!hwnd) { + emit error("无法获取窗口句柄"); + return false; + } + + // 注册 CapsLock (VK_CAPITAL = 0x14) 全局快捷键 + // MOD_NOREPEAT 防止按住时重复触发 + const int vkCapsLock = 0x14; + impl_->hotkeyId = GlobalAddAtom(L"ImpressVoiceHotkey"); + + BOOL ok = RegisterHotKey(hwnd, impl_->hotkeyId, MOD_NOREPEAT, vkCapsLock); + if (!ok) { + DWORD err = GetLastError(); + emit error(QString("注册 CapsLock 快捷键失败 (错误码: %1)").arg(err)); + LOG_ERROR(kTag, QString("RegisterHotKey failed: %1").arg(err)); + return false; + } + + // 安装原生事件过滤器 + auto* filter = new HotkeyNativeEventFilter(this); + QGuiApplication::instance()->installNativeEventFilter(filter); + impl_->nativeEventFilter = filter; + + // 长按定时器 + impl_->longPressTimer = new QTimer(this); + impl_->longPressTimer->setSingleShot(true); + impl_->longPressTimer->setInterval(Impl::kLongPressMs); + connect(impl_->longPressTimer, &QTimer::timeout, this, [this]() { + if (impl_->isHolding && !impl_->longPressFired) { + impl_->longPressFired = true; + recording_ = true; + emit recordingStarted(); + LOG_DEBUG(kTag, "长按触发,开始录音"); + } + }); + + // 松开后延迟重置,避免 CapsLock 状态闪烁 + impl_->keyUpDebounce = new QTimer(this); + impl_->keyUpDebounce->setSingleShot(true); + impl_->keyUpDebounce->setInterval(200); + connect(impl_->keyUpDebounce, &QTimer::timeout, this, [this]() { + impl_->isHolding = false; + impl_->longPressFired = false; + }); + + active_ = true; + emit ready(); + LOG_INFO(kTag, "CapsLock 快捷键已注册"); + return true; +#else + emit error("CapsLockVoiceHotkey 仅支持 Windows 平台"); + return false; +#endif +} + +void CapsLockVoiceHotkey::stop() { + if (!active_) return; + +#ifdef Q_OS_WIN + impl_->pollThreadRunning = false; + + if (impl_->longPressTimer) { + impl_->longPressTimer->stop(); + } + if (impl_->keyUpDebounce) { + impl_->keyUpDebounce->stop(); + } + + // 移除原生事件过滤器 + if (impl_->nativeEventFilter) { + auto* filter = static_cast(impl_->nativeEventFilter); + QGuiApplication::instance()->removeNativeEventFilter(filter); + delete filter; + impl_->nativeEventFilter = nullptr; + } + + // 注销快捷键 + HWND hwnd = reinterpret_cast(QGuiApplication::instance()->winId()); + if (hwnd && impl_->hotkeyId) { + UnregisterHotKey(hwnd, impl_->hotkeyId); + GlobalDeleteAtom(impl_->hotkeyId); + impl_->hotkeyId = 0; + } + + active_ = false; + recording_ = false; + impl_->isHolding = false; + impl_->longPressFired = false; + LOG_INFO(kTag, "CapsLock 快捷键已停止"); +#endif +} + +#ifdef Q_OS_WIN +void CapsLockVoiceHotkey::onHotkeyEvent(int /*hotkeyId*/) { + if (!active_) return; + + // Windows 只在按键按下时触发 WM_HOTKEY + // 我们通过 GetAsyncKeyState 轮询检测松开 + impl_->isHolding = true; + impl_->longPressFired = false; + + // 启动长按定时器 + if (impl_->longPressTimer) { + impl_->longPressTimer->start(); + } + + // 启动轮询线程检测松开 + if (impl_->pollThreadRunning) return; + impl_->pollThreadRunning = true; + QThread::create([this]() { + const int vkCapsLock = 0x14; + while (impl_->pollThreadRunning && impl_->isHolding) { + SHORT state = GetAsyncKeyState(vkCapsLock); + if (!(state & 0x8000)) { + // 按键松开 + impl_->isHolding = false; + if (impl_->longPressTimer) { + impl_->longPressTimer->stop(); + } + QMetaObject::invokeMethod(this, [this]() { + if (impl_->longPressFired) { + // 长按 → 停止录音 + recording_ = false; + emit recordingStopped(); + LOG_DEBUG(kTag, "长按结束,停止录音"); + } else { + // 短按 → 不处理(让系统处理 CapsLock) + LOG_DEBUG(kTag, "短按,不拦截 CapsLock"); + } + impl_->longPressFired = false; + if (impl_->keyUpDebounce) { + impl_->keyUpDebounce->start(); + } + }, Qt::QueuedConnection); + break; + } + QThread::msleep(50); + } + impl_->pollThreadRunning = false; + }).start(); +} +#endif + +} // namespace impress diff --git a/src/core/win_hotkey.h b/src/core/win_hotkey.h new file mode 100644 index 0000000..791aaa5 --- /dev/null +++ b/src/core/win_hotkey.h @@ -0,0 +1,61 @@ +#pragma once + +#include +#include +#include +#include + +namespace impress { + +/** + * @brief CapsLock 长按语音输入快捷键管理器(Windows) + * + * 使用 Windows RegisterHotKey API 实现全局快捷键。 + * 工作流程: + * 1. 用户长按 CapsLock 1 秒后触发录音 + * 2. 长按期间持续录音 + * 3. 松开 CapsLock 后停止录音并触发转写 + * 4. 短按(< 1s)直接传递 CapsLock 事件(切换大小写锁定) + */ +class CapsLockVoiceHotkey : public QObject { + Q_OBJECT +public: + explicit CapsLockVoiceHotkey(QObject* parent = nullptr); + ~CapsLockVoiceHotkey() override; + + /** @brief 初始化并注册快捷键 */ + bool start(); + + /** @brief 停止并注销快捷键 */ + void stop(); + + /** @brief 是否已激活 */ + bool isActive() const { return active_; } + + /** @brief 当前是否正在录音(CapsLock 长按超过 1s 后) */ + bool isRecording() const { return recording_; } + +signals: + /** @brief 开始录音(长按超过 1 秒后) */ + void recordingStarted(); + + /** @brief 停止录音(松开快捷键后) */ + void recordingStopped(); + + /** @brief 快捷键已注册 */ + void ready(); + + /** @brief 初始化失败 */ + void error(const QString& message); + + /** @brief 处理 WM_HOTKEY 事件(由原生事件过滤器调用) */ + void onHotkeyEvent(int hotkeyId); + +private: + struct Impl; + std::unique_ptr impl_; + bool active_ = false; + bool recording_ = false; +}; + +} // namespace impress diff --git a/src/core/win_text_injector.cpp b/src/core/win_text_injector.cpp new file mode 100644 index 0000000..2abdaed --- /dev/null +++ b/src/core/win_text_injector.cpp @@ -0,0 +1,114 @@ +#include "win_text_injector.h" +#include "utils/logger.h" + +#ifdef Q_OS_WIN +#include +#endif + +#include + +static const char* const kTag = "WaylandTextInjector"; + +namespace impress { + +WaylandTextInjector::WaylandTextInjector(QObject* parent) + : QObject(parent) +{} + +WaylandTextInjector::~WaylandTextInjector() = default; + +bool WaylandTextInjector::initialize() { + if (initialized_) return true; + +#ifdef Q_OS_WIN + // SendInput 不需要额外初始化 + initialized_ = true; + LOG_INFO(kTag, "Windows 文本注入器已初始化 (SendInput)"); + return true; +#else + LOG_ERROR(kTag, "WaylandTextInjector 仅支持 Windows 平台"); + return false; +#endif +} + +bool WaylandTextInjector::injectText(const QString& text) { + if (!initialized_) { + LOG_ERROR(kTag, "文本注入器未初始化"); + return false; + } + + if (text.isEmpty()) return true; + + LOG_DEBUG(kTag, QString("注入文本 (%1 字符): %2").arg(text.length()).arg(text)); + + for (int i = 0; i < text.length(); i++) { + if (!injectChar(text[i])) { + LOG_WARNING(kTag, QString("字符注入失败: '%1'").arg(text[i])); + } + // 字符间短暂延迟 + QThread::usleep(10000); // 10ms + } + + LOG_DEBUG(kTag, "文本注入完成"); + return true; +} + +bool WaylandTextInjector::injectChar(QChar ch) { +#ifdef Q_OS_WIN + INPUT inputs[4] = {}; + int count = 0; + + // 尝试使用 Unicode 输入法(支持所有 Unicode 字符) + inputs[0].type = INPUT_KEYBOARD; + inputs[0].ki.wScan = ch.unicode(); + inputs[0].ki.dwFlags = KEYEVENTF_UNICODE; + count++; + + inputs[1].type = INPUT_KEYBOARD; + inputs[1].ki.wScan = ch.unicode(); + inputs[1].ki.dwFlags = KEYEVENTF_UNICODE | KEYEVENTF_KEYUP; + count++; + + UINT sent = SendInput(count, inputs, sizeof(INPUT)); + if (sent == 0) { + DWORD err = GetLastError(); + LOG_ERROR(kTag, QString("SendInput 失败: %1").arg(err)); + return false; + } + return true; +#else + (void)ch; + return false; +#endif +} + +bool WaylandTextInjector::simulateKeycode(unsigned int keycode) { +#ifdef Q_OS_WIN + INPUT inputs[2] = {}; + + // 按下 + inputs[0].type = INPUT_KEYBOARD; + inputs[0].ki.wVk = static_cast(keycode); + inputs[0].ki.dwFlags = 0; + + // 释放 + inputs[1].type = INPUT_KEYBOARD; + inputs[1].ki.wVk = static_cast(keycode); + inputs[1].ki.dwFlags = KEYEVENTF_KEYUP; + + UINT sent = SendInput(2, inputs, sizeof(INPUT)); + if (sent == 0) { + DWORD err = GetLastError(); + LOG_ERROR(kTag, QString("SendInput keycode 注入失败: %1").arg(err)); + return false; + } + + LOG_DEBUG(kTag, QString("模拟 keycode: 0x%1").arg(keycode, 0, 16)); + return true; +#else + (void)keycode; + return false; +#endif +} + +} // namespace impress diff --git a/src/core/win_text_injector.h b/src/core/win_text_injector.h new file mode 100644 index 0000000..46809c2 --- /dev/null +++ b/src/core/win_text_injector.h @@ -0,0 +1,41 @@ +#pragma once + +#include +#include +#include + +namespace impress { + +/** + * @brief 文本注入器(Windows) + * + * 通过 Windows SendInput API 将文本注入到当前光标位置。 + */ +class WaylandTextInjector : public QObject { + Q_OBJECT +public: + explicit WaylandTextInjector(QObject* parent = nullptr); + ~WaylandTextInjector() override; + + /** @brief 初始化 */ + bool initialize(); + + /** @brief 将文本注入到当前光标位置 */ + bool injectText(const QString& text); + + /** @brief 是否已初始化 */ + bool isInitialized() const { return initialized_; } + + /** @brief 模拟 keycode 按下+释放(Windows 使用虚拟键码) */ + bool simulateKeycode(unsigned int keycode); + +signals: + void error(const QString& message); + +private: + bool initialized_ = false; + + bool injectChar(QChar ch); +}; + +} // namespace impress diff --git a/src/ui/settings_page.cpp b/src/ui/settings_page.cpp index b2eca74..07c8553 100644 --- a/src/ui/settings_page.cpp +++ b/src/ui/settings_page.cpp @@ -83,8 +83,8 @@ void SettingsPage::setupUI() { streamingCheck_->setChecked(true); sttLayout->addRow("流式识别:", streamingCheck_); - debugSaveAudioCheck_ = new QCheckBox("保存调试音频到 /tmp/impress_audio_debug/", this); - debugSaveAudioCheck_->setToolTip("开启后,每次识别会将原始音频保存为 WAV 文件,用于调试音频质量问题"); + debugSaveAudioCheck_ = new QCheckBox("保存调试音频到临时文件夹", this); + debugSaveAudioCheck_->setToolTip("开启后,每次识别会将原始音频保存为 WAV 文件到系统临时目录,用于调试音频质量问题"); sttLayout->addRow("调试录音:", debugSaveAudioCheck_); capslockVoiceCheck_ = new QCheckBox("启用 CapsLock 长按语音输入", this);