base_agent/uas/inout/inputs/speech_input.py

221 lines
8.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- 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;")