base_agent/uas/inout/inputs/speech_input.py

221 lines
8.2 KiB
Python
Raw Normal View History

2026-06-18 03:28:14 +00:00
# -*- 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;")