commit 03a9890fd9a20bd69295625b07ec2c6efe8c386b Author: ChiXiaohang Date: Mon Jan 27 10:52:24 2025 +0800 测试gitea上传 diff --git a/test.py b/test.py new file mode 100644 index 0000000..b28b3a6 --- /dev/null +++ b/test.py @@ -0,0 +1,660 @@ +# 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_() \ No newline at end of file