660 lines
23 KiB
Python
660 lines
23 KiB
Python
|
# 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_()
|