# coding:utf-8 from base64 import b64encode from hashlib import md5, sha1 from hmac import new import json import re import sys from threading import Thread import time from urllib.parse import quote from pyaudio import paInt16, PyAudio import websocket from PyQt5.QtCore import Qt, QSize, QUrl, pyqtSlot, pyqtSignal from PyQt5.QtGui import QIcon, QFont, QColor from PyQt5.QtWidgets import QApplication, QWidget, QHBoxLayout, QVBoxLayout, QSizePolicy, QScrollArea from qfluentwidgets import (SmoothScrollArea, IconWidget, BodyLabel, CaptionLabel, TransparentToolButton, FluentIcon, ImageLabel, SimpleCardWidget, LineEdit, HeaderCardWidget, InfoBar, InfoBarPosition, InfoBarIcon, HyperlinkLabel, PrimaryPushButton, TitleLabel, setFont, ScrollArea, VerticalSeparator, MSFluentWindow) import ahocorasick import pandas as pd def isWin11(): return sys.platform == 'win32' and sys.getwindowsversion().build >= 22000 class StatisticsWidget(QWidget): """ Statistics widget """ def __init__(self, title: str, value: str, parent=None): super().__init__(parent=parent) self.titleLabel = CaptionLabel(title, self) self.valueLabel = BodyLabel(value, self) self.vBoxLayout = QVBoxLayout(self) self.vBoxLayout.setContentsMargins(16, 0, 16, 0) self.vBoxLayout.addWidget(self.valueLabel, 0, Qt.AlignTop) self.vBoxLayout.addWidget(self.titleLabel, 0, Qt.AlignBottom) setFont(self.valueLabel, 18, QFont.DemiBold) self.titleLabel.setTextColor(QColor(96, 96, 96), QColor(206, 206, 206)) class ConfigCard(HeaderCardWidget): """ Config card for API settings """ configChanged = pyqtSignal(str, str) # Signal for when config changes (app_id, api_key) def __init__(self, parent=None): super().__init__(parent) self.setTitle('API 配置') self.setBorderRadius(8) # Create layout self.mainLayout = QVBoxLayout() # App ID input self.appIdLayout = QHBoxLayout() self.appIdLabel = BodyLabel('App ID:', self) self.appIdInput = LineEdit(self) self.appIdInput.setPlaceholderText('输入 App ID') self.appIdLayout.addWidget(self.appIdLabel) self.appIdLayout.addWidget(self.appIdInput) # API Key input self.apiKeyLayout = QHBoxLayout() self.apiKeyLabel = BodyLabel('API Key:', self) self.apiKeyInput = LineEdit(self) self.apiKeyInput.setPlaceholderText('输入 API Key') self.apiKeyLayout.addWidget(self.apiKeyLabel) self.apiKeyLayout.addWidget(self.apiKeyInput) # Test connection button self.buttonLayout = QHBoxLayout() self.testButton = PrimaryPushButton('测试连接', self) self.saveButton = PrimaryPushButton('保存配置', self) self.buttonLayout.addWidget(self.testButton) self.buttonLayout.addWidget(self.saveButton) # Add all layouts to main layout self.mainLayout.addLayout(self.appIdLayout) self.mainLayout.addLayout(self.apiKeyLayout) self.mainLayout.addLayout(self.buttonLayout) # Set main layout self.viewLayout.addLayout(self.mainLayout) # Connect signals self.testButton.clicked.connect(self.test_connection) self.saveButton.clicked.connect(self.save_config) # Load existing config if any self.load_config() def load_config(self): """Load existing configuration if available""" # Here you might want to load from a config file # For now, we'll use default values self.appIdInput.setText("f3a9a1bc") self.apiKeyInput.setText("3ba5a497a68a930546fb95ef750abf90") def save_config(self): """Save the configuration""" app_id = self.appIdInput.text() api_key = self.apiKeyInput.text() if not app_id or not api_key: InfoBar.error( title='错误', content="App ID 和 API Key 不能为空", orient=Qt.Horizontal, isClosable=True, position=InfoBarPosition.TOP, duration=2000, parent=self ) return # Emit the config changed signal self.configChanged.emit(app_id, api_key) InfoBar.success( title='成功', content="配置已保存", orient=Qt.Horizontal, isClosable=True, position=InfoBarPosition.TOP, duration=2000, parent=self ) def test_connection(self): """Test the API connection""" app_id = self.appIdInput.text() api_key = self.apiKeyInput.text() if not app_id or not api_key: InfoBar.error( title='错误', content="请先填写 App ID 和 API Key", orient=Qt.Horizontal, isClosable=True, position=InfoBarPosition.TOP, duration=2000, parent=self ) return try: # Prepare connection parameters base_url = "ws://rtasr.xfyun.cn/v1/ws" ts = str(int(time.time())) tt = (app_id + ts).encode('utf-8') md5_ = md5() md5_.update(tt) baseString = md5_.hexdigest() baseString = bytes(baseString, encoding='utf-8') apiKey = api_key.encode('utf-8') signa = new(apiKey, baseString, sha1).digest() signa = b64encode(signa) signa = str(signa, 'utf-8') # Create WebSocket connection ws_url = f"{base_url}?appid={app_id}&ts={ts}&signa={quote(signa)}" ws = websocket.create_connection(ws_url) # Check the response result = ws.recv() ws.close() if "success" in result: InfoBar.success( title='成功', content="API连接测试成功", orient=Qt.Horizontal, isClosable=True, position=InfoBarPosition.TOP, duration=2000, parent=self ) else: InfoBar.error( title='错误', content="API连接测试失败", orient=Qt.Horizontal, isClosable=True, position=InfoBarPosition.TOP, duration=2000, parent=self ) except Exception as e: InfoBar.error( title='错误', content=f"连接测试失败: {str(e)}", orient=Qt.Horizontal, isClosable=True, position=InfoBarPosition.TOP, duration=2000, parent=self ) class AppInfoCard(SimpleCardWidget): """ App information card """ def __init__(self, parent=None): super().__init__(parent) self.iconLabel = ImageLabel(":/qfluentwidgets/images/logo.png", self) self.iconLabel.setBorderRadius(8, 8, 8, 8) self.iconLabel.scaledToWidth(120) self.nameLabel = TitleLabel('实时语音识别', self) self.installButton = PrimaryPushButton('开始', self) self.stopButton = PrimaryPushButton('停止', self) self.companyLabel = HyperlinkLabel( QUrl('https://blog.csdn.net/fjh1997/article/details/104449110'), '电脑音频设置教程', self) self.installButton.setFixedWidth(160) self.stopButton.setFixedWidth(160) self.scoreWidget = StatisticsWidget('版本', '1.0', self) self.separator = VerticalSeparator(self) self.descriptionLabel = BodyLabel( '一个实时语音识别工具,能够实时识别语言内容,并提供帮助提示。', self) self.descriptionLabel.setWordWrap(True) self.shareButton = TransparentToolButton(FluentIcon.ROBOT, self) self.shareButton.setFixedSize(32, 32) self.shareButton.setIconSize(QSize(14, 14)) self.hBoxLayout = QHBoxLayout(self) self.vBoxLayout = QVBoxLayout() self.topLayout = QHBoxLayout() self.statisticsLayout = QHBoxLayout() self.buttonLayout = QHBoxLayout() self.initLayout() self.setBorderRadius(8) def initLayout(self): self.hBoxLayout.setSpacing(30) self.hBoxLayout.setContentsMargins(34, 24, 24, 24) self.hBoxLayout.addWidget(self.iconLabel) self.hBoxLayout.addLayout(self.vBoxLayout) self.vBoxLayout.setContentsMargins(0, 0, 0, 0) self.vBoxLayout.setSpacing(0) # name label and install button self.vBoxLayout.addLayout(self.topLayout) self.topLayout.setContentsMargins(0, 0, 0, 0) self.topLayout.addWidget(self.nameLabel) self.topLayout.addWidget(self.installButton, 0, Qt.AlignRight) self.topLayout.addWidget(self.stopButton, 0, Qt.AlignRight) # company label self.vBoxLayout.addSpacing(3) self.vBoxLayout.addWidget(self.companyLabel) # statistics widgets self.vBoxLayout.addSpacing(20) self.vBoxLayout.addLayout(self.statisticsLayout) self.statisticsLayout.setContentsMargins(0, 0, 0, 0) self.statisticsLayout.setSpacing(10) self.statisticsLayout.addWidget(self.scoreWidget) self.statisticsLayout.addWidget(self.separator) self.statisticsLayout.setAlignment(Qt.AlignLeft) # description label self.vBoxLayout.addSpacing(20) self.vBoxLayout.addWidget(self.descriptionLabel) # button self.vBoxLayout.addSpacing(12) self.buttonLayout.setContentsMargins(0, 0, 0, 0) self.vBoxLayout.addLayout(self.buttonLayout) self.buttonLayout.addWidget(self.shareButton, 0, Qt.AlignRight) class InnerAudioCard(HeaderCardWidget): """ Inner audio card """ # 定义自定义信号 textChanged = pyqtSignal(str) def __init__(self, parent=None): super().__init__(parent) self.descriptionLabel = BodyLabel(self) self.descriptionLabel.setWordWrap(True) self.viewLayout.addWidget(self.descriptionLabel) self.setTitle('音频识别结果') self.setBorderRadius(8) self.init_asr() def init_asr(self): self.app_id = "f3a9a1bc" self.api_key = "3ba5a497a68a930546fb95ef750abf90" self.base_url = "ws://rtasr.xfyun.cn/v1/ws" self.ts = str(int(time.time())) tt = (self.app_id + self.ts).encode('utf-8') md5_ = md5() md5_.update(tt) baseString = md5_.hexdigest() baseString = bytes(baseString, encoding='utf-8') apiKey = self.api_key.encode('utf-8') signa = new(apiKey, baseString, sha1).digest() signa = b64encode(signa) self.signa = str(signa, 'utf-8') self.end_tag = "{\"end\": true}" self.ws = None self.p = None self.stream = None self.tsend = None self.trecv = None self.running = False def findInternalRecordingDevice(self, p): target = 'CABLE Output' for i in range(p.get_device_count()): devInfo = p.get_device_info_by_index(i) if devInfo['name'].find(target) >= 0 and devInfo['hostApi'] == 0: return i print('无法找到内录设备!') return -1 def start_asr(self): if not self.running: self.running = True # 创建WebSocket连接 self.ws = websocket.create_connection( self.base_url + "?appid=" + self.app_id + "&ts=" + self.ts + "&signa=" + quote(self.signa)) # 启动发送和接收线程 self.tsend = Thread(target=self.send) self.tsend.start() self.trecv = Thread(target=self.recv) self.trecv.start() def stop_asr(self): if self.running: self.running = False try: # 发送结束标签 if self.ws and self.ws.connected: self.ws.send(self.end_tag.encode('utf-8')) print("send end tag success") # 停止并关闭音频流 if self.stream: self.stream.stop_stream() self.stream.close() self.stream = None if self.p: self.p.terminate() self.p = None # 等待线程结束 if self.tsend and self.tsend.is_alive(): self.tsend.join(timeout=2) # 给2秒钟超时 if self.trecv and self.trecv.is_alive(): self.trecv.join(timeout=2) # 给2秒钟超时 # 关闭WebSocket连接 if self.ws: self.ws.close() self.ws = None print("ASR stopped successfully") except Exception as e: print(f"Error stopping ASR: {str(e)}") # 确保资源被释放 self.stream = None self.p = None self.ws = None def send(self): chunk_size = 1280 audio_format = paInt16 channels = 1 rate = 16000 self.p = PyAudio() dev_idx = self.findInternalRecordingDevice(self.p) if dev_idx < 0: return self.stream = self.p.open(input_device_index=dev_idx, format=audio_format, channels=channels, rate=rate, input=True, frames_per_buffer=chunk_size) try: while self.running and self.ws and self.ws.connected: data = self.stream.read(chunk_size) self.ws.send(data, opcode=websocket.ABNF.OPCODE_BINARY) time.sleep(0.04) except Exception as e: print(f"Send thread error: {e}") finally: if self.stream: self.stream.stop_stream() self.stream.close() if self.p: self.p.terminate() def recv(self): try: while self.running and self.ws and self.ws.connected: result = self.ws.recv() if len(result) == 0: print("receive result end") break result_dict = json.loads(result) if result_dict["action"] == "started": print("handshake success, result: " + result) if result_dict["action"] == "result": pattern = r'"w":"(.*?)"' extracted_words = ''.join(re.findall(pattern, result_dict["data"])) self.updateLabel(extracted_words) if result_dict["action"] == "error": print("rtasr error: " + result) self.ws.close() self.running = False return except Exception as e: print(f"Recv thread error: {e}") def updateLabel(self, text): pre = self.descriptionLabel.text() pre_list = pre.split("\n") if len(text) > len(pre_list[-1]): pre_list[-1] = text else: pre_list.append(text) new_text = "\n".join(pre_list) self.descriptionLabel.setText(new_text) # 发射信号,传递新的文本内容 self.textChanged.emit(new_text) class OuterAudioCard(HeaderCardWidget): """ Outer audio card """ def __init__(self, parent=None): super().__init__(parent) self.descriptionLabel = BodyLabel(self) self.descriptionLabel.setWordWrap(True) self.viewLayout.addWidget(self.descriptionLabel) self.setTitle('帮助提示') self.setBorderRadius(8) df = pd.read_csv('tips.csv') self.automaton = ahocorasick.Automaton() for index, row in df.iterrows(): keyword = row['关键字'] self.automaton.add_word(keyword, (index, keyword, row['解决方案'])) self.automaton.make_automaton() def updateLabel(self, text): text = text.split("\n")[-1] results = set() for end_index, (index, keyword, solution) in self.automaton.iter(text): results.add((keyword, solution)) print(results) if len(results) != 0: content = [] for keyword, solution in results: content.append(f"关键字: {keyword}, 解决方案: {solution}") pre = self.descriptionLabel.text() content_str = pre + "\n" + "\n".join(content) self.descriptionLabel.setText(content_str) class SystemRequirementCard(HeaderCardWidget): """ System requirements card """ def __init__(self, parent=None): super().__init__(parent) self.setTitle('系统要求') self.setBorderRadius(8) self.infoLabel = BodyLabel('此产品适用于你的设备。具有复选标记的项目符合开发人员的系统要求。', self) self.successIcon = IconWidget(InfoBarIcon.SUCCESS, self) self.detailButton = HyperlinkLabel('详细信息', self) self.vBoxLayout = QVBoxLayout() self.hBoxLayout = QHBoxLayout() self.successIcon.setFixedSize(16, 16) self.hBoxLayout.setSpacing(10) self.vBoxLayout.setSpacing(16) self.hBoxLayout.setContentsMargins(0, 0, 0, 0) self.vBoxLayout.setContentsMargins(0, 0, 0, 0) self.hBoxLayout.addWidget(self.successIcon) self.hBoxLayout.addWidget(self.infoLabel) self.vBoxLayout.addLayout(self.hBoxLayout) self.vBoxLayout.addWidget(self.detailButton) self.viewLayout.addLayout(self.vBoxLayout) class ConfigInterface(ScrollArea): """ Configuration interface """ def __init__(self, parent=None): super().__init__(parent=parent) self.view = QWidget(self) self.vBoxLayout = QVBoxLayout(self.view) # Create config card self.configCard = ConfigCard(self) # Setup layout self.vBoxLayout.setSpacing(30) self.vBoxLayout.setContentsMargins(36, 20, 36, 36) self.vBoxLayout.addWidget(self.configCard) self.vBoxLayout.addStretch(1) # Setup scroll area self.setWidget(self.view) self.setWidgetResizable(True) self.setObjectName("configInterface") self.enableTransparentBackground() class AppInterface(ScrollArea): def __init__(self, parent=None): super().__init__(parent) self.view = QWidget(self) self.vBoxLayout = QVBoxLayout(self.view) # Create all cards (removed config card) self.appCard = AppInfoCard(self) self.systemCard = SystemRequirementCard(self) # Create scroll areas for audio cards self.innerAudioScrollArea = ScrollArea(self) self.outerAudioScrollArea = ScrollArea(self) self.innerAudioCard = InnerAudioCard(self) self.outerAudioCard = OuterAudioCard(self) # Setup scroll areas self.innerAudioScrollArea.setWidget(self.innerAudioCard) self.outerAudioScrollArea.setWidget(self.outerAudioCard) self.innerAudioScrollArea.setWidgetResizable(True) self.outerAudioScrollArea.setWidgetResizable(True) self.innerAudioScrollArea.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) self.outerAudioScrollArea.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) self.innerAudioScrollArea.setFixedHeight(200) self.outerAudioScrollArea.setFixedHeight(200) # Create horizontal layout for audio cards self.hBoxLayout = QHBoxLayout() self.hBoxLayout.addWidget(self.innerAudioScrollArea, 1) self.hBoxLayout.addWidget(self.outerAudioScrollArea, 1) # Setup main layout self.vBoxLayout.setSpacing(10) self.vBoxLayout.setContentsMargins(0, 0, 10, 30) self.vBoxLayout.addWidget(self.appCard, 0, Qt.AlignTop) self.vBoxLayout.addLayout(self.hBoxLayout) self.vBoxLayout.addWidget(self.systemCard, 0, Qt.AlignTop) self.setWidget(self.view) self.setWidgetResizable(True) self.setObjectName("appInterface") # Connect signals self.appCard.installButton.clicked.connect(self.start_asr) self.appCard.stopButton.clicked.connect(self.stop_asr) self.innerAudioCard.textChanged.connect(self.onTextChanged) self.enableTransparentBackground() def start_asr(self): self.innerAudioCard.start_asr() def stop_asr(self): self.innerAudioCard.stop_asr() def resizeEvent(self, e): super().resizeEvent(e) @pyqtSlot(str) def onTextChanged(self, text): self.outerAudioCard.updateLabel(text) print(f"QLabel 内容已更新为: {text}") def closeEvent(self, event): # 确保所有子组件的线程被正确关闭 self.innerAudioCard.stop_asr() event.accept() class Demo3(MSFluentWindow): def __init__(self, parent=None): super().__init__(parent) # 创建界面 self.appInterface = AppInterface(self) self.configInterface = ConfigInterface(self) # 添加子界面到侧边栏,只使用基础图标 self.addSubInterface(self.appInterface, FluentIcon.HOME, "主页", FluentIcon.HOME, isTransparent=True) self.addSubInterface(self.configInterface, FluentIcon.SETTING, "配置", FluentIcon.SETTING, isTransparent=True) # 连接配置信号 self.configInterface.configCard.configChanged.connect(self.onConfigChanged) self.resize(880, 760) self.setWindowTitle('语音识别工具') self.setWindowIcon(QIcon(':/qfluentwidgets/images/logo.png')) self.titleBar.raise_() def onConfigChanged(self, app_id, api_key): """处理配置变更""" # 将配置更新传递给主页面的InnerAudioCard self.appInterface.innerAudioCard.app_id = app_id self.appInterface.innerAudioCard.api_key = api_key self.appInterface.innerAudioCard.init_asr() if __name__ == '__main__': # enable dpi scale QApplication.setHighDpiScaleFactorRoundingPolicy( Qt.HighDpiScaleFactorRoundingPolicy.PassThrough) QApplication.setAttribute(Qt.AA_EnableHighDpiScaling) QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps) app = QApplication(sys.argv) w3 = Demo3() w3.show() app.exec_()