# -*- coding: utf-8 -*- """ 语音输入模块 基于 SpeechRecognition + PyAudio 实现麦克风实时识别 在独立 QThread 中运行,不阻塞 Qt 主循环 依赖: pip install SpeechRecognition pyaudio """ import logging from PyQt5.QtCore import QThread, pyqtSignal, pyqtSlot from PyQt5.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QComboBox, QProgressBar ) from uas.io.base_io import BaseInput, IOType from uas.io.signals import io_signals logger = logging.getLogger(__name__) try: import speech_recognition as sr HAS_SR = True except ImportError: HAS_SR = False logger.warning("⚠️ SpeechRecognition 未安装: pip install SpeechRecognition pyaudio") # ───────────────────────────────────────────────────────────────── # 语音识别工作线程 # ───────────────────────────────────────────────────────────────── class _SpeechWorkerThread(QThread): """在后台线程持续监听麦克风并识别语音""" recognized = pyqtSignal(str) # 识别成功 error = pyqtSignal(str) # 识别失败 listening = pyqtSignal(bool) # 正在监听状态 def __init__(self, engine: str = "google", language: str = "zh-CN", parent=None): super().__init__(parent) self._engine = engine self._language = language self._running = False def run(self): if not HAS_SR: self.error.emit("SpeechRecognition 未安装") return recognizer = sr.Recognizer() recognizer.energy_threshold = 300 recognizer.dynamic_energy_threshold = True recognizer.pause_threshold = 0.8 self._running = True logger.info(f"语音识别线程启动 engine={self._engine} lang={self._language}") with sr.Microphone() as source: recognizer.adjust_for_ambient_noise(source, duration=1) while self._running: try: self.listening.emit(True) audio = recognizer.listen(source, timeout=5, phrase_time_limit=15) self.listening.emit(False) # 根据引擎选择识别方式 if self._engine == "google": text = recognizer.recognize_google( audio, language=self._language ) elif self._engine == "whisper": text = recognizer.recognize_whisper( audio, language=self._language.split("-")[0] ) elif self._engine == "sphinx": text = recognizer.recognize_sphinx(audio) else: text = recognizer.recognize_google( audio, language=self._language ) if text: logger.info(f"识别结果: {text!r}") self.recognized.emit(text) except sr.WaitTimeoutError: pass # 超时继续等待 except sr.UnknownValueError: self.error.emit("无法识别语音") except sr.RequestError as e: self.error.emit(f"识别服务错误: {e}") except Exception as e: logger.error(f"语音识别异常: {e}") self.error.emit(str(e)) logger.info("语音识别线程退出") @pyqtSlot def output(self): pass def stop(self): self._running = False STTWorker = _SpeechWorkerThread # ───────────────────────────────────────────────────────────────── # 语音输入组件(带 UI) # ───────────────────────────────────────────────────────────────── class SpeechInputWidget(QWidget, BaseInput): """ 语音输入 UI 组件 包含:引擎选择 + 语言选择 + 开始/停止按钮 + 状态指示 """ def __init__(self, parent=None): QWidget.__init__(self, parent) BaseInput.__init__(self, "speech_input", IOType.SPEECH) self._worker: _SpeechWorkerThread = None self._build_ui() def _build_ui(self): layout = QVBoxLayout(self) layout.setContentsMargins(8, 8, 8, 8) layout.setSpacing(6) # 标题 title = QLabel("🎤 语音输入") title.setStyleSheet("font-weight: bold; font-size: 13px;") layout.addWidget(title) # 引擎 + 语言选择 row1 = QHBoxLayout() row1.addWidget(QLabel("引擎:")) self._engine_box = QComboBox() self._engine_box.addItems(["google", "whisper", "sphinx"]) row1.addWidget(self._engine_box) row1.addWidget(QLabel("语言:")) self._lang_box = QComboBox() self._lang_box.addItems(["zh-CN", "en-US", "ja-JP", "ko-KR"]) row1.addWidget(self._lang_box) layout.addLayout(row1) # 状态指示 self._status_label = QLabel("⏸ 未启动") self._status_label.setStyleSheet("color: gray;") layout.addWidget(self._status_label) # 进度条(监听动画) self._progress = QProgressBar() self._progress.setRange(0, 0) # 不确定模式 self._progress.setVisible(False) self._progress.setFixedHeight(8) layout.addWidget(self._progress) # 按钮 btn_row = QHBoxLayout() self._start_btn = QPushButton("▶ 开始监听") self._start_btn.setStyleSheet("background:#4CAF50; color:white;") self._start_btn.clicked.connect(self.start) btn_row.addWidget(self._start_btn) self._stop_btn = QPushButton("■ 停止") self._stop_btn.setStyleSheet("background:#f44336; color:white;") self._stop_btn.setEnabled(False) self._stop_btn.clicked.connect(self.stop) btn_row.addWidget(self._stop_btn) layout.addLayout(btn_row) # 识别结果预览 self._result_label = QLabel("") self._result_label.setWordWrap(True) self._result_label.setStyleSheet( "background:#f5f5f5; padding:4px; border-radius:4px;" ) layout.addWidget(self._result_label) def start(self) -> None: engine = self._engine_box.currentText() language = self._lang_box.currentText() self._worker = _SpeechWorkerThread(engine, language, self) self._worker.recognized.connect(self._on_recognized) self._worker.error.connect(self._on_error) self._worker.listening.connect(self._on_listening) self._worker.start() self._running = True self._start_btn.setEnabled(False) self._stop_btn.setEnabled(True) self._status_label.setText("🔴 监听中...") self._status_label.setStyleSheet("color: red;") def stop(self) -> None: if self._worker: self._worker.stop() self._worker.wait(3000) self._running = False self._start_btn.setEnabled(True) self._stop_btn.setEnabled(False) self._status_label.setText("⏸ 已停止") self._status_label.setStyleSheet("color: gray;") self._progress.setVisible(False) @pyqtSlot(str) def _on_recognized(self, text: str): self._result_label.setText(f"识别: {text}") io_signals.speech_recognized.emit(text) self._emit_input(text, metadata={"source": "microphone"}) @pyqtSlot(bool) def _on_listening(self, active: bool): self._progress.setVisible(active) if active: self._status_label.setText("🎙 正在聆听...") else: self._status_label.setText("⚙️ 识别中...") @pyqtSlot(str) def _on_error(self, msg: str): self._result_label.setText(f"⚠️ {msg}") self._status_label.setStyleSheet("color: orange;")