diff --git a/.gitignore b/.gitignore index f1d6420..579ffe8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,20 @@ -build* -dist* -__pycache__* -run.spec -alispeech.log -*.wav \ No newline at end of file +*.spec +__pycache__ +*.log +*.spec +*.mp4 +*.mkv +*.wav +.DS_Store +.idea +*info + +*database.db +*test.py +*.7z +*/dist/* +*/build/* +*.db +*.afphoto +icon*.png +视频封面.png diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..5296f66 --- /dev/null +++ b/Pipfile @@ -0,0 +1,15 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +keyboard = "*" +aliyunsdkcore = "*" +PyAudio = "*" +PySide2 = "*" + +[dev-packages] + +[requires] +python_version = "3.8" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..847fb09 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,81 @@ +{ + "_meta": { + "hash": { + "sha256": "0570a2f54550bb4323c9968752018b201089dc64dd9b956170572797eb8ad0d2" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.8" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "aliyunsdkcore": { + "hashes": [ + "sha256:1e3a1fbd43e29ee8438c5ca52456d3d69c86929b5e3557c5d6dc0df93b3a1d00" + ], + "index": "pypi", + "version": "==1.0.3" + }, + "keyboard": { + "hashes": [ + "sha256:63ed83305955939ca5c9a73755e5cc43e8242263f5ad5fd3bb7e0b032f3d308b", + "sha256:8e9c2422f1217e0bd84489b9ecd361027cc78415828f4fe4f88dd4acd587947b" + ], + "index": "pypi", + "version": "==0.13.5" + }, + "pyaudio": { + "hashes": [ + "sha256:0d92f6a294565260a282f7c9a0b0d309fc8cc988b5ee5b50645634ab9e2da7f7", + "sha256:259bb9c1363be895b4f9a97e320a6017dd06bc540728c1a04eb4a7b6fe75035b", + "sha256:2a19bdb8ec1445b4f3e4b7b109e0e4cec1fd1f1ce588592aeb6db0b58d4fb3b0", + "sha256:51b558d1b28c68437b53218279110db44f69f3f5dd3d81859f569a4a96962bdc", + "sha256:589bfad2c615dd4b5d3757e763019c42ab82f06fba5cae64ec02fd7f5ae407ed", + "sha256:8f89075b4844ea94dde0c951c2937581c989fabd4df09bfd3f075035f50955df", + "sha256:93bfde30e0b64e63a46f2fd77e85c41fd51182a4a3413d9edfaf9ffaa26efb74", + "sha256:cf1543ba50bd44ac0d0ab5c035bb9c3127eb76047ff12235149d9adf86f532b6", + "sha256:f78d543a98b730e64621ebf7f3e2868a79ade0a373882ef51c0293455ffa8e6e" + ], + "index": "pypi", + "version": "==0.2.11" + }, + "pycrypto": { + "hashes": [ + "sha256:f2ce1e989b272cfcb677616763e0a2e7ec659effa67a88aa92b3a65528f60a3c" + ], + "version": "==2.6.1" + }, + "pyside2": { + "hashes": [ + "sha256:0558ced3bcd7f9da638fa8b7709dba5dae82a38728e481aac8b9058ea22fcdd9", + "sha256:081d8c8a6c65fb1392856a547814c0c014e25ac04b38b987d9a3483e879e9634", + "sha256:087a0b719bb967405ea85fd202757c761f1fc73d0e2397bc3a6a15376782ee75", + "sha256:1316aa22dd330df096daf7b0defe9c00297a66e0b4907f057aaa3e88c53d1aff", + "sha256:4f17a0161995678110447711d685fcd7b15b762810e8f00f6dc239bffb70a32e", + "sha256:976cacf01ef3b397a680f9228af7d3d6273b9254457ad4204731507c1f9e6c3c" + ], + "index": "pypi", + "version": "==5.15.2" + }, + "shiboken2": { + "hashes": [ + "sha256:03f41b0693b91c7f89627f1085a4ecbe8591c03f904118a034854d935e0e766c", + "sha256:14a33169cf1bd919e4c4c4408fffbcd424c919a3f702df412b8d72b694e4c1d5", + "sha256:4aee1b91e339578f9831e824ce2a1ec3ba3a463f41fda8946b4547c7eb3cba86", + "sha256:89c157a0e2271909330e1655892e7039249f7b79a64a443d52c512337065cde0", + "sha256:ae8ca41274cfa057106268b6249674ca669c5b21009ec49b16d77665ab9619ed", + "sha256:edc12a4df2b5be7ca1e762ab94e331ba9e2fbfe3932c20378d8aa3f73f90e0af" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '3.10'", + "version": "==5.15.2" + } + }, + "develop": {} +} diff --git a/README.md b/README.md index 82006da..a4036c8 100644 --- a/README.md +++ b/README.md @@ -1,81 +1,94 @@ -# Caps Writer +[Gitee](https://gitee.com/haujet/CapsWriter) | [Github](https://github.com/HaujetZhao/CapsWriter) -### 💡 简介 +# icon.ico Caps Writer -一款电脑端语音输入工具,后台运行脚本后,按下按下 `Caps Lock`(也就是大写锁定键)超过 0.3 秒后,开始语音识别,松开按键之后,自动输入识别结果。 +## 💡 简介 -目前使用了阿里云的一句话识别 api。(有兴趣的可以自行改成百度、腾讯、讯飞、谷歌的 api ) - -因为使用了阿里云的 api,所以需要用户自己到阿里云申请,再填到 `token.ini` 中才能正常使用。 +这是一款电脑端语音输入工具。顾名思义,Caps Writer 就是按下大写锁定键来打字的工具。它的具体作用是:当你长按键盘上的大写锁定键后,软件会开始语音识别,当你松开大写锁定键时,识别的结果就可以立马上屏。 对于聊天时候进行快捷输入、写代码时快速加入中文注释非常的方便。 +目前软件内置了对阿里云一句话识别 API 的支持。如果你要使用,就需要先在阿里云上实名认证,申请语音识别 API,在设置页面添加一个语音识别引擎。 + +> 添加其它服务商的引擎也是可以做的,只是目前阿里云的引擎就够用,还没有足够的动力添加其它引擎。 + +具体使用效果、申请阿里云 API 的方法,可以参考我这个视频: [ CapsWriter 2.0 使用视频 ](https://www.bilibili.com/video/BV12A411p73r/) + +添加上引擎后,在主页面选择一个引擎,点击启用按钮,就可以进行语音识别了! + +启用后,在实际使用中,只要按下 CapsLock 键,软件就会立刻开始录音: + +* 如果只是单击 CapsLock 后松开,录音数据会立刻被删除; +* 如果按下 CapsLock 键时长超过 0.3 秒,就会开始连网进行语音识别,松开 CapsLock 键时,语音识别结果会被立刻输入。 + +所以你只需要按下 CapsLock 键,无需等待,就可以开始说话,因为当你按下按下 CapsLock 键的时候,程序就开始录音了,只要你按的时长超过 0.3 秒,就肯定能识别上。说完后,松开,识别结果立马上屏。 + +image-20201225053752740 + +## ⭐技巧 + +在设置界面,将 `点击关闭按钮时隐藏到托盘` 选项勾选,就可以将软件隐藏到托盘栏运行: + +image-20201225140607971 + ### 📝 背景 -我真是气抖冷,为什么直到 0202 年,仍然没有开发者做过一个好用的语音输入工具? +对于直到 0202 年,仍然没有开发者做过一个好用的语音输入工具,我又生气又无奈,毕竟这东西不赚钱,自然没有人做。 有人建议用搜狗输入法、讯飞输入法的语音输入,但这几个方面是真让人受不了: -* 广告太多,拒绝安装 -* 我主力五笔,不使用搜狗输入法、讯飞输入法,顶多临时用下微软拼音 +* 广告太多的软件,拒绝安装 +* 速度慢,讯飞在手机上的语音输入挺快的,但是在 PC 上的语音识别速度超级慢 * 就以搜狗输入法为例,它的语音输入快捷键只能是`Ctrl + Shift + A/B/C……`,有以下槽点: * 这个快捷键会和许多软件的快捷键冲突,且不好记 * 打字时,按这样三个快捷键,手指很别扭,不爽 - * 它的逻辑是按下快捷键后,启用语音输入,你一停顿一下,要说下一名,语音输入却结束了,不能让用户决定什么时候结束语音输入。 +* 讯飞语音输入法的快捷键是 F6,只能换成 F 功能键,离手指太远,不好够,同时和许多软件快捷键冲突 -为了在电脑上语音输入,我之前是用的 Quicker 的手机端进行语音识别,输入到电脑上,需要两个设备,非常麻烦。今天终于做好我心目中最好用的电脑端语音输入工具了! -### 📽️ 视频演示 - -作者为这个工具录制了使用视频演示、申请 api 的教程视频 - -请到 HacPai 帖子中进行查看:[Caps Wirter 发布:按住大写锁定键,进行语音识别输入](https://hacpai.com/article/1594371212477) - -或者到 Bilibili 查看:[ Caps Writer(电脑端语音输入工具)使用教程 ](https://www.bilibili.com/video/BV1qK4y1s7Fb/) - ## 🔮 开箱即用 -小白用户,只需要在 [Release](https://github.com/HaujetZhao/CapsWriter/releases) 界面下载打包好的 exe 文件,运行,会在同级目录生成一个 `token.ini` 文件,在 `token.ini` 中填入你阿里云拥有 **管理智能语音交互(NLS)** 权限的 **RAM访问控制** 用户的 **Accesskey Id**、**Accesskey Secret** 和智能语音交互语音识别项目的 **appkey** ,就可以正常使用了。 +Windows 小白用户,只需要在 [Gitee Releases](https://gitee.com/haujet/CapsWriter/releases) 或 [Github Releases](https://github.com/HaujetZhao/CapsWriter/releases) 界面下载打包好的压缩文件,解压,执行里面的 exe 文件,就可以运行了,在设置界面新建引擎,填入你在阿里云中申请的: -详细申请、填写 API 的步骤请到 [Caps Wirter 发布:按住大写锁定键,进行语音识别输入](https://hacpai.com/article/1594371212477) 或到 [ Caps Writer(电脑端语音输入工具)使用教程 ](https://www.bilibili.com/video/BV1qK4y1s7Fb/) 查看视频教程 +* 拥有 **管理智能语音交互(NLS)** 权限的 **RAM访问控制** 用户的 **Accesskey Id**、**Accesskey Secret** +* 智能语音交互语音识别项目的 **appkey** -### 🛠 开发使用 +就可以正常使用了。 -本工具是一个python脚本,上面小白下载的 Release 其实是用 pyinstaller 导出的 exe 文件,如果你想在源码基础上使用,就需要安装以下模块: +详细申请、填写 API 的步骤请到 [ CapsWriter 2.0 使用视频 ](https://www.bilibili.com/video/BV12A411p73r/) 查看视频教程。 -- keyboard -- pyaudio -- configparser -- aliyunsdkcore -- alibabacloud-nls-python-sdk +Mac 和 Linux 用户,你们也可以使用,只是我没有 Mac 和 Linux 的电脑,无法打包。需要你们下载源代码、安装依赖库,再打包或者直接运行。 + +### 🛠 源代码使用 + +小白下载的 Release 其实是用 pyinstaller 导出的 exe 文件,如果你需要在源码基础上使用,就需要安装以下模块: + +- keyboard (用于监听键盘输入) +- pyaudio (用于接收录音) +- PySide2 (图形界面框架) +- aliyun-python-sdk-core (阿里云 sdk) +- alibabacloud-nls-java-sdk (阿里云智能语音引擎 sdk) 其中: -- pyaudio 在 windows 上不是太好安装,可以先到 [这个链接](https://www.lfd.uci.edu/~gohlke/pythonlibs) 下载 pyaudio 对应版本的 whl 文件,再用 pip 安装 -- alibabacloud-nls-python-sdk 不是通过 python 安装,而是通过 [阿里云官方文档的方法](https://help.aliyun.com/document_detail/120693.html) 进行安装。 +- pyaudio 在 windows 上不是太好安装,可以先到 [这个链接](https://www.lfd.uci.edu/~gohlke/pythonlibs) 下载 pyaudio 对应版本的 whl 文件,再用 pip 安装,Mac 和 Linux 上需要先安装 port audio,才能安装上 pyaudio +- alibabacloud-nls-java-sdk 是指阿里云官方 java sdk 的 python 实现,它不是通过 pip 安装的(官方没有上传到 pypi ),而是通过 [阿里云官方文档的方法](https://www.alibabacloud.com/help/zh/doc-detail/120693.htm) 进行安装。 +- 其它模块使用 pip 安装即可 -另外,需要在 `token.ini` 中填入阿里云拥有 **管理智能语音交互(NLS)** 权限的 **RAM访问控制** 用户的 **accessID**、**accessKey** 和智能语音交互语音识别项目的 **appkey** 。 +本文件夹内有一个 `安装指南` 文件夹,在里面可以找到详细的安装指南,还包括了提前下载的 `alibabacloud-nls-python-sdk` 和 `pyaudio` 的 whl 文件。 -本文件夹内有一个 `安装指南` 文件夹,在里面可以找到详细的安装指南,还包括了提前下载的 alibabacloud-nls-python-sdk 和 pyaudio 的 whl 文件。 +## ☕ 打赏 -用 python 运行 `run.py` 后,按下 `Caps Lock`(也就是大写锁定键)超过 0.3 秒后,就会开始用阿里云的 api 进行语音识别,松开按键后,会将识别结果自动输入。 +万水千山总是情,一块几块都是情。本软件完全开源,用爱发电,如果你愿意,可以以打赏的方式支持我一下: -### 后话 +sponsor -因为作者就是本着凑合能用就可以了的心态做这个工具的,所以图形界面什么的也没做,整个工具单纯就一个脚本,功能也就一个,按住大写锁定键开始语音识别,松开后输入结果。目前作者本人已经很满意。 -欢迎有想法有能力的人将这个工具加以改进,比如加入讯飞、腾讯、百度的语音识别api,长按0.3秒后开始识别时加一个提示等等等等。 -目前已知改进的方向: +## 😀 交流 -- 使用 VoiceRecognition 中的 google_recognize 进行识别,使用的是谷歌的免费语音识别 api,优势是不用用户个人申请 api 了,但是在中国大陆不太好使用。在海外的话会非常好用。 -- 使用 Baidu AI 语音识别 api,每个账户有 200 万次的免费额度。 -- 使用 Tencent AI 语音识别 api,每个账户有 5000 次的免费额度。 -- 使用讯飞的语音识别 api,每个账户有 1 年的免费使用时间。 - -欢迎有兴趣的贡献者对项目进行翻译(国际化),添加 Google、Bing 的 api,让海外用户也可以使用这个便捷的语音输入工具! \ No newline at end of file +如果有软件方面的反馈可以提交 issues,或者加入 QQ 群:[1146626791](https://qm.qq.com/cgi-bin/qm/qr?k=DgiFh5cclAElnELH4mOxqWUBxReyEVpm&jump_from=webapi) \ No newline at end of file diff --git a/assets/image-20201225053752740.png b/assets/image-20201225053752740.png new file mode 100644 index 0000000..fdc29e7 Binary files /dev/null and b/assets/image-20201225053752740.png differ diff --git a/assets/image-20201225140607971.png b/assets/image-20201225140607971.png new file mode 100644 index 0000000..a49b831 Binary files /dev/null and b/assets/image-20201225140607971.png differ diff --git a/assets/sponsor.jpg b/assets/sponsor.jpg new file mode 100644 index 0000000..c34b59b Binary files /dev/null and b/assets/sponsor.jpg differ diff --git a/requirements.txt b/requirements.txt index 71cf397..6a96bb8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,4 @@ setuptools pyaudio keyboard aliyunsdkcore -configparser \ No newline at end of file +PySide2 \ No newline at end of file diff --git a/run.py b/run.py deleted file mode 100644 index 8cb3a00..0000000 --- a/run.py +++ /dev/null @@ -1,217 +0,0 @@ -import json -import os -import pyaudio -import threading -import keyboard - -import time -import ali_speech -from ali_speech.callbacks import SpeechRecognizerCallback -from ali_speech.constant import ASRFormat -from ali_speech.constant import ASRSampleRate - -from aliyunsdkcore.client import AcsClient -from aliyunsdkcore.request import CommonRequest -import configparser - - - -"""pyaudio参数""" -CHUNK = 1024 # 数据包或者数据片段 -FORMAT = pyaudio.paInt16 # pyaudio.paInt16表示我们使用量化位数 16位来进行录音 -CHANNELS = 1 # 声道,1为单声道,2为双声道 -RATE = 16000 # 采样率,每秒钟16000次 - -count = 1 # 计数 -pre = True # 是否准备开始录音 -run = False # 控制录音是否停止 - - -class MyCallback(SpeechRecognizerCallback): - """ - 构造函数的参数没有要求,可根据需要设置添加 - 示例中的name参数可作为待识别的音频文件名,用于在多线程中进行区分 - """ - def __init__(self, name='default'): - self._name = name - def on_started(self, message): - #print('MyCallback.OnRecognitionStarted: %s' % message) - pass - def on_result_changed(self, message): - print('任务信息: task_id: %s, result: %s' % ( - message['header']['task_id'], message['payload']['result'])) - def on_completed(self, message): - print('结果: %s' % ( - message['payload']['result'])) - result = message['payload']['result'] - try: - if result[-1] == '。': # 如果最后一个符号是句号,就去掉。 - result = result[0:-1] - except Exception as e: - pass - keyboard.write(result) # 输入识别结果 - keyboard.press_and_release('caps lock') # 再按下大写锁定键,还原大写锁定 - def on_task_failed(self, message): - print('MyCallback.OnRecognitionTaskFailed: %s' % message) - def on_channel_closed(self): - # print('MyCallback.OnRecognitionChannelClosed') - pass - -def get_token(): - - config = configparser.ConfigParser() - config.read_file(open('token.ini')) - token = config.get("Token","Id") - expireTime = config.get("Token","ExpireTime") - accessID = config.get("Token","accessKeyId") - accessKey = config.get("Token","accessKeySecret") - # 要是 token 还有 5 秒过期,那就重新获得一个。 - if (int(expireTime) - time.time()) < 5 : - # 创建AcsClient实例 - client = AcsClient( - accessID, # 填写 AccessID - accessKey, # 填写 AccessKey - "cn-shanghai" - ); - # 创建request,并设置参数 - request = CommonRequest() - request.set_method('POST') - request.set_domain('nls-meta.cn-shanghai.aliyuncs.com') - request.set_version('2019-02-28') - request.set_action_name('CreateToken') - response = json.loads(client.do_action_with_exception(request)) - token = response['Token']['Id'] - expireTime = str(response['Token']['ExpireTime']) - config.set('Token', 'Id', token) - config.set('Token', 'ExpireTime', expireTime) - config.write(open("token.ini", "w")) - print - return token - -def get_recognizer(client, appkey): - token = get_token() - audio_name = 'none' - callback = MyCallback(audio_name) - recognizer = client.create_recognizer(callback) - recognizer.set_appkey(appkey) - recognizer.set_token(token) - recognizer.set_format(ASRFormat.PCM) - recognizer.set_sample_rate(ASRSampleRate.SAMPLE_RATE_16K) - recognizer.set_enable_intermediate_result(False) - recognizer.set_enable_punctuation_prediction(True) - recognizer.set_enable_inverse_text_normalization(True) - return(recognizer) - -# 因为关闭 recognizer 有点慢,就须做成一个函数,用多线程关闭它。 -def close_recognizer(): - global recognizer - recognizer.close() - -# 处理热键响应 -def on_hotkey(event): - global pre, run - if event.event_type == "down": - if pre and (not run): - pre = False - run = True - threading.Thread(target=process).start() - else: - pass - else: - pre, run = True, False - -# 处理是否开始录音 -def process(): - global run - # 等待 6 轮 0.05 秒,如果 run 还是 True,就代表还没有松开大写键,是在长按状态,那么就可以开始识别。 - for i in range(6): - if run: - time.sleep(0.05) - else: - return - global count, recognizer, p, appkey - threading.Thread(target=recoder,args=(recognizer, p)).start() # 开始录音识别 - count += 1 - recognizer = get_recognizer(client, appkey) # 为下一次监听提前准备好 recognizer - -# 录音识别处理 -def recoder(recognizer, p): - global run - try: - stream = p.open(channels=CHANNELS, - format=FORMAT, - rate=RATE, - input=True, - frames_per_buffer=CHUNK) - print('\r{}//:在听了,说完了请松开 CapsLock 键...'.format(count), end=' ') - ret = recognizer.start() - if ret < 0: - return ret - while run: - data = stream.read(CHUNK) - ret = recognizer.send(data) - if ret < 0: - break - recognizer.stop() - stream.stop_stream() - stream.close() - # p.terminate() - except Exception as e: - print(e) - finally: - threading.Thread(target=close_recognizer).start() # 关闭 recognizer - print('{}//:按住 CapsLock 键 0.3 秒后开始说话...'.format(count), end=' ') - - - -if __name__ == '__main__': - - print("""\r\nCaps Writer 开始运行 - -开源发布地址:https://github.com/HaujetZhao/CapsWriter - -下载地址:https://github.com/HaujetZhao/CapsWriter/releases - -视频教程地址:https://www.bilibili.com/video/BV1qK4y1s7Fb/ - -作者:淳帅二代(HaujetZhao) - -软件基于 MIT 协议 - -""") - - if not os.path.exists('token.ini'): - init_id = """[Token] -id = 0000000000000000000 -expiretime = 0000000000 -accessKeyId = 000000 -accessKeySecret = 000000 -appkey = 00000""" - fp = open("token.ini",'w') - fp.write(init_id) - fp.close() - input("""\r\n 检测到没有配置文件,所以刚刚已在同级目录生成了 token.ini 配置文件,\r\n - 请打开 token.ini 配置文件,\r\n - 然后填入阿里云的 accesskeyid 和 accesskeysecret, 以及你的语音识别项目的 appkey,\r\n - 再回到本界面,按任意键后,回车继续\r\n - 如果下面出错了,那么就很有可能是 accesskeyid 、accesskeysecret 或 appkey 填错了\r\n""") - config = configparser.ConfigParser() - config.read_file(open('token.ini')) - appkey = config.get("Token","appkey") - - client = ali_speech.NlsClient() - client.set_log_level('WARNING') # 设置 client 输出日志信息的级别:DEBUG、INFO、WARNING、ERROR - - recognizer = get_recognizer(client, appkey) - p = pyaudio.PyAudio() - - print("""\r\n初始化完成,现在可以将本工具最小化,在需要输入的界面,按住 CapsLock 键 0.3 秒后开始说话,松开 CapsLock 键后识别结果会自动输入\r\n""") - - keyboard.hook_key('caps lock', on_hotkey) - print('{}//:按住 CapsLock 键 0.3 秒后开始说话...'.format(count), end=' ') - keyboard.wait() - - - - - \ No newline at end of file diff --git a/src/__init__.pyw b/src/__init__.pyw new file mode 100644 index 0000000..805b8ab --- /dev/null +++ b/src/__init__.pyw @@ -0,0 +1,40 @@ +# -*- coding: UTF-8 -*- + +import os, sys, time +try: + os.chdir(os.path.dirname(__file__)) # 更改工作目录,指向正确的当前文件夹 + sys.path.append(os.path.dirname(__file__)) # 将当前目录导入 python 寻找 package 和 moduel 的变量 +except: + print('更改使用路径失败,不过没关系') +# os.environ['PATH'] += os.pathsep + os.path.abspath('./bin') # 将可执行文件的目录加入环境变量 + +from PySide2.QtWidgets import * +from PySide2.QtCore import * +from PySide2.QtGui import * + +from moduels.function.createDB import createDB # 引入检查和创建创建数据库的函数 + +from moduels.gui.MainWindow import MainWindow +from moduels.gui.SystemTray import SystemTray +from moduels.component.NormalValue import 常量 + + +############# 主窗口和托盘 ################ + +def 高分屏变量设置(app): + os.environ['QT_SCALE_FACTOR'] = '1' + app.setAttribute(Qt.AA_EnableHighDpiScaling) + QCoreApplication.instance().setAttribute(Qt.AA_UseHighDpiPixmaps) + + +def main(): + app = QApplication(sys.argv) + 高分屏变量设置(app) + createDB() + mainWindow = MainWindow() + tray = SystemTray(mainWindow) + 常量.托盘 = tray + sys.exit(app.exec_()) + +if __name__ == '__main__': + main() diff --git a/src/misc/icon.icns b/src/misc/icon.icns new file mode 100644 index 0000000..05decdf Binary files /dev/null and b/src/misc/icon.icns differ diff --git a/src/misc/icon.ico b/src/misc/icon.ico new file mode 100644 index 0000000..d494cdd Binary files /dev/null and b/src/misc/icon.ico differ diff --git a/src/misc/icon_listning.icns b/src/misc/icon_listning.icns new file mode 100644 index 0000000..e574a21 Binary files /dev/null and b/src/misc/icon_listning.icns differ diff --git a/src/misc/icon_listning.ico b/src/misc/icon_listning.ico new file mode 100644 index 0000000..472cc9b Binary files /dev/null and b/src/misc/icon_listning.ico differ diff --git a/src/misc/png转ico和icns.bat b/src/misc/png转ico和icns.bat new file mode 100644 index 0000000..4d43924 --- /dev/null +++ b/src/misc/png转ico和icns.bat @@ -0,0 +1,2 @@ +magick convert "%1" -resize 128x128 "%~dp1%~n1.ico" +magick convert "%1" -resize 128x128 "%~dp1%~n1.icns" diff --git a/src/misc/requirements.txt b/src/misc/requirements.txt new file mode 100644 index 0000000..6a96bb8 --- /dev/null +++ b/src/misc/requirements.txt @@ -0,0 +1,5 @@ +setuptools +pyaudio +keyboard +aliyunsdkcore +PySide2 \ No newline at end of file diff --git a/src/misc/sponsor.jpg b/src/misc/sponsor.jpg new file mode 100644 index 0000000..5f78c57 Binary files /dev/null and b/src/misc/sponsor.jpg differ diff --git a/src/misc/style.css b/src/misc/style.css new file mode 100644 index 0000000..568dd2c --- /dev/null +++ b/src/misc/style.css @@ -0,0 +1,42 @@ +/*参考这里:https://doc.qt.io/qt-5/stylesheet-reference.html*/ + + +/*切换到分割视频 Tab,里面有几个上面带字的功能框,那些框框就是 QGroupBox */ +QGroupBox{ + border: 1px solid #ccc; + border-radius:6px; + margin-top: 2ex; + margin-bottom: 0.5ex; + padding: 0.3em 0.4em 0.4em 0.3em; /* 上 右 下 左*/ +} + +/* 这就是 QGroupBox 上面的标题 */ +QGroupBox:title { + color: #005980; + subcontrol-origin: margin; + margin-top: 0.5ex; + left: 2ex; +} + +/* +QPushButton { + border: 2px solid #8f8f91; + border-radius: 6px; + padding: 1em 0.4em 1em 0.3em; /* 上 右 下 左* + background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #f6f7fa, stop: 1 #dadbde); + min-width: 80px; +} + +QPushButton:pressed { + background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #dadbde, stop: 1 #f6f7fa); +} + +QPushButton:flat { + border: none; /* no border for a flat push button * +} + +QPushButton:default { + border-color: navy; /* make the default button prominent * +}*/ \ No newline at end of file diff --git a/src/moduels/component/Ali_CallBack.py b/src/moduels/component/Ali_CallBack.py new file mode 100644 index 0000000..edbc310 --- /dev/null +++ b/src/moduels/component/Ali_CallBack.py @@ -0,0 +1,38 @@ +# -*- coding: UTF-8 -*- +import json +import os +import pyaudio +import threading +import keyboard +import time + +from ali_speech.callbacks import SpeechRecognizerCallback + +class Ali_Callback(SpeechRecognizerCallback): + """ + 构造函数的参数没有要求,可根据需要设置添加 + 示例中的name参数可作为待识别的音频文件名,用于在多线程中进行区分 + """ + def __init__(self, name='default'): + self._name = name + def on_started(self, message): + #print('MyCallback.OnRecognitionStarted: %s' % message) + pass + def on_result_changed(self, message): + print('任务信息: task_id: %s, result: %s' % ( + message['header']['task_id'], message['payload']['result'])) + def on_completed(self, message): + print('结果: %s' % ( + message['payload']['result'])) + result = message['payload']['result'] + try: + if result[-1] == '。': # 如果最后一个符号是句号,就去掉。 + result = result[0:-1] + except Exception as e: + pass + keyboard.write(result) # 输入识别结果 + def on_task_failed(self, message): + print('MyCallback.OnRecognitionTaskFailed: %s' % message) + def on_channel_closed(self): + # print('MyCallback.OnRecognitionChannelClosed') + pass \ No newline at end of file diff --git a/src/moduels/component/NormalValue.py b/src/moduels/component/NormalValue.py new file mode 100644 index 0000000..de75ccf --- /dev/null +++ b/src/moduels/component/NormalValue.py @@ -0,0 +1,39 @@ +import sqlite3 +import platform +import subprocess + + +class NormalValue(): + 样式文件 = 'misc/style.css' + 软件版本 = '2.0.0' + + 主窗口 = None + 托盘 = None + 状态栏 = None + + Token配置路径 = 'misc/Token.ini' + + 数据库路径 = 'misc/database.db' + 数据库连接 = sqlite3.connect(数据库路径) + + 偏好设置表单名 = '偏好设置' + 语音引擎表单名 = '语音引擎' + + 关闭时隐藏到托盘 = False + + + 系统平台 = platform.system() + + 图标路径 = 'misc/icon.icns' if 系统平台 == 'Darwin' else 'misc/icon.ico' + 聆听图标路径 = 'misc/icon_listning.icns' if 系统平台 == 'Darwin' else 'misc/icon_listning.ico' + + subprocessStartUpInfo = subprocess.STARTUPINFO() + if 系统平台 == 'Windows': + subprocessStartUpInfo.dwFlags = subprocess.STARTF_USESHOWWINDOW + subprocessStartUpInfo.wShowWindow = subprocess.SW_HIDE + +class ThreadValue(): + pass + +常量 = NormalValue() +线程值 = ThreadValue() \ No newline at end of file diff --git a/src/moduels/component/QEditBox_StdoutBox.py b/src/moduels/component/QEditBox_StdoutBox.py new file mode 100644 index 0000000..2fdb5fc --- /dev/null +++ b/src/moduels/component/QEditBox_StdoutBox.py @@ -0,0 +1,21 @@ +# -*- coding: UTF-8 -*- + +from PySide2.QtWidgets import * +from PySide2.QtGui import * + +# 命令输出窗口中的多行文本框 +class QEditBox_StdoutBox(QTextEdit): + # 定义一个 QTextEdit 类,写入 print 方法。用于输出显示。 + def __init__(self, parent=None): + super(QEditBox_StdoutBox, self).__init__(parent) + self.setReadOnly(True) + + def print(self, text): + try: + cursor = self.textCursor() + cursor.movePosition(QTextCursor.End) + cursor.insertText(text) + self.setTextCursor(cursor) + self.ensureCursorVisible() + except: + print('文本框更新文本失败') diff --git a/src/moduels/component/SponsorDialog.py b/src/moduels/component/SponsorDialog.py new file mode 100644 index 0000000..8b9c1c5 --- /dev/null +++ b/src/moduels/component/SponsorDialog.py @@ -0,0 +1,24 @@ +# -*- coding: UTF-8 -*- + +from PySide2.QtWidgets import * +from PySide2.QtGui import * +from PySide2.QtCore import * + +from moduels.component.NormalValue import 常量 + + +# 打赏对话框 +class SponsorDialog(QDialog): + def __init__(self, parent=None): + super(SponsorDialog, self).__init__(parent) + self.resize(500, 567) + 图标路径 = 'misc/icon.icns' if 常量.系统平台 == 'Darwin' else 'misc/icon.ico' + self.setWindowIcon(QIcon(图标路径)) + self.setWindowTitle(self.tr('打赏作者')) + self.setWindowModality(Qt.NonModal) # 让窗口不要阻挡主窗口 + self.show() + + def paintEvent(self, event): + painter = QPainter(self) + pixmap = QPixmap('misc/sponsor.jpg') + painter.drawPixmap(self.rect(), pixmap) \ No newline at end of file diff --git a/src/moduels/component/Stream.py b/src/moduels/component/Stream.py new file mode 100644 index 0000000..b879f0b --- /dev/null +++ b/src/moduels/component/Stream.py @@ -0,0 +1,19 @@ +from PySide2.QtWidgets import * +from PySide2.QtGui import * +from PySide2.QtCore import * + + +class Stream(QObject): + # 用于将控制台的输出定向到一个槽 + newText = Signal(str) + + # def __init__(self): + # super().__init__() + # self.newText = Signal(str) + + def write(self, text): + self.newText.emit(str(text)) + # QApplication.processEvents() + + def flush(self): + pass \ No newline at end of file diff --git a/src/moduels/function/createDB.py b/src/moduels/function/createDB.py new file mode 100644 index 0000000..a5ad716 --- /dev/null +++ b/src/moduels/function/createDB.py @@ -0,0 +1,49 @@ +# -*- coding: UTF-8 -*- + +from moduels.component.NormalValue import 常量 + +def createDB(): + + 数据库连接 = 常量.数据库连接 + 偏好设置表单名 = 常量.偏好设置表单名 + 语音引擎表单名 = 常量.语音引擎表单名 + # 模板表单名 = 常量.数据库模板表单名 + # 皮肤表单名 = 常量.数据库皮肤表单名 + cursor = 数据库连接.cursor() + + result = cursor.execute(f'select * from sqlite_master where name = "{偏好设置表单名}";') + if result.fetchone() == None: + cursor.execute(f'''create table {偏好设置表单名} ( + id integer primary key autoincrement, + item text, + value text + )''') + else: + print('偏好设置表单已存在') + # + result = cursor.execute(f'select * from sqlite_master where name = "{语音引擎表单名}";') + if result.fetchone() == None: + cursor.execute(f'''create table {语音引擎表单名} ( + id integer primary key autoincrement, + 引擎名称 text, + 服务商 text, + AppKey text, + 语言 text, + AccessKeyId text, + AccessKeySecret text + )''') + else: + print('语音引擎表单名已存在') + # + # result = cursor.execute(f'select * from sqlite_master where name = "{皮肤表单名}";') + # if result.fetchone() == None: + # cursor.execute(f'''create table {皮肤表单名} ( + # id integer primary key autoincrement, + # skinName text, + # outputFileName text, + # sourceFilePath text, + # supportDarkMode BOOLEAN)''') + # else: + # print('皮肤表单已存在') + # + 数据库连接.commit() # 最后要提交更改 diff --git a/src/moduels/function/getAlibabaRecognizer.py b/src/moduels/function/getAlibabaRecognizer.py new file mode 100644 index 0000000..52903a5 --- /dev/null +++ b/src/moduels/function/getAlibabaRecognizer.py @@ -0,0 +1,26 @@ +# -*- coding: UTF-8 -*- +import configparser, sqlite3, json + +from ali_speech.constant import ASRFormat +from ali_speech.constant import ASRSampleRate + +from moduels.component.NormalValue import 常量 +from moduels.component.Ali_CallBack import Ali_Callback +from moduels.function.getAlibabaToken import getAlibabaToken + +def getAlibabaRecognizer(client, appkey, accessKeyId, accessKeySecret, tokenId, tokenExpireTime, 线程): + tokenId, tokenExpireTime = getAlibabaToken(accessKeyId, accessKeySecret, tokenId, tokenExpireTime) + if tokenId == False: return False + 线程.tokenId = tokenId + 线程.tokenExpireTime = tokenExpireTime + audio_name = 'none' + callback = Ali_Callback(audio_name) + recognizer = client.create_recognizer(callback) + recognizer.set_appkey(appkey) + recognizer.set_token(tokenId) + recognizer.set_format(ASRFormat.PCM) + recognizer.set_sample_rate(ASRSampleRate.SAMPLE_RATE_16K) + recognizer.set_enable_intermediate_result(False) + recognizer.set_enable_punctuation_prediction(True) + recognizer.set_enable_inverse_text_normalization(True) + return (recognizer) \ No newline at end of file diff --git a/src/moduels/function/getAlibabaToken.py b/src/moduels/function/getAlibabaToken.py new file mode 100644 index 0000000..1a9fdf8 --- /dev/null +++ b/src/moduels/function/getAlibabaToken.py @@ -0,0 +1,42 @@ +# -*- coding: UTF-8 -*- +import configparser, sqlite3, json, time, sys + + +from aliyunsdkcore.client import AcsClient +from aliyunsdkcore.request import CommonRequest + +from moduels.component.NormalValue import 常量 + +def getAlibabaToken(accessID, accessKey, tokenId, tokenExpireTime): + # 要是 token 还有 50 秒过期,那就重新获得一个。 + if (int(tokenExpireTime) - time.time()) < 50 : + # 创建AcsClient实例 + client = AcsClient( + accessID, # 填写 AccessID + accessKey, # 填写 AccessKey = 得到AccessKey(引擎名称) + "cn-shanghai" + ); + # 创建request,并设置参数 + request = CommonRequest() + request.set_method('POST') + request.set_domain('nls-meta.cn-shanghai.aliyuncs.com') + request.set_version('2019-02-28') + request.set_action_name('CreateToken') + try: + response = json.loads(client.do_action_with_exception(request)) + except Exception as e: + print(f'''获取 Token 出错了,出错信息如下:\n{e}\n''') + return False, False + tokenId = response['Token']['Id'] + tokenExpireTime = str(response['Token']['ExpireTime']) + return tokenId, tokenExpireTime + +# def 得到AccessKey(引擎名称): +# 数据库连接 = sqlite3.connect(常量.数据库路径) +# AccessKeyId, AccessKeySecret = 数据库连接.execute(f'''select AccessKeyId, +# AccessKeySecret +# from {常量.语音引擎表单名} +# where 引擎名称 = :引擎名称''', +# {'引擎名称': 引擎名称}).fetchone() +# 数据库连接.close() +# return AccessKeyId, AccessKeySecret \ No newline at end of file diff --git a/src/moduels/gui/Combo_EngineList.py b/src/moduels/gui/Combo_EngineList.py new file mode 100644 index 0000000..8a39ef7 --- /dev/null +++ b/src/moduels/gui/Combo_EngineList.py @@ -0,0 +1,62 @@ +# -*- coding: UTF-8 -*- + +import os, sqlite3 +from PySide2.QtWidgets import * +from PySide2.QtGui import * +from PySide2.QtCore import * +from moduels.component.NormalValue import 常量 + +# 添加预设对话框 +class Combo_EngineList(QComboBox): + def __init__(self): + super().__init__() + self.initElements() # 先初始化各个控件 + self.initSlots() # 再将各个控件连接到信号槽 + self.initLayouts() # 然后布局 + self.initValues() # 再定义各个控件的值 + + def initElements(self): + pass + + def initSlots(self): + pass + + def initLayouts(self): + pass + + def initValues(self): + self.初始化列表() + + def mousePressEvent(self, e): + self.列表更新() + self.showPopup() + + def 初始化列表(self): + self.列表项 = [] + 数据库连接 = 常量.数据库连接 + cursor = 数据库连接.cursor() + result = cursor.execute(f'''select 引擎名称 from {常量.语音引擎表单名} order by id;''').fetchall() + if len(result) != 0: + for item in result: + self.列表项.append(item[0]) + self.addItems(self.列表项) + # if not os.path.exists(常量.音效文件路径): os.makedirs(常量.音效文件路径) + # with os.scandir(常量.音效文件路径) as 目录条目: + # for entry in 目录条目: + # if not entry.name.startswith('.') and entry.is_dir(): + # self.列表项.append(entry.name) + + + def 列表更新(self): + 新列表 = [] + 数据库连接 = 常量.数据库连接 + cursor = 数据库连接.cursor() + result = cursor.execute(f'''select 引擎名称 from {常量.语音引擎表单名} order by id;''').fetchall() + if len(result) != 0: + for item in result: + 新列表.append(item[0]) + if self.列表项 == 新列表: return True + self.clear() + self.列表项 = 新列表 + self.addItems(self.列表项) + diff --git a/src/moduels/gui/Dialog_AddEngine.py b/src/moduels/gui/Dialog_AddEngine.py new file mode 100644 index 0000000..6ff416d --- /dev/null +++ b/src/moduels/gui/Dialog_AddEngine.py @@ -0,0 +1,190 @@ +# -*- coding: UTF-8 -*- + +from PySide2.QtWidgets import * +from PySide2.QtGui import * +from PySide2.QtCore import * +from moduels.component.NormalValue import 常量 + + +class Dialog_AddEngine(QDialog): + def __init__(self, 列表, 数据库连接, 表单名字, 显示的列名): + super().__init__(常量.主窗口) + self.列表 = 列表 + self.数据库连接 = 数据库连接 + self.表单名字 = 表单名字 + self.显示的列名 = 显示的列名 + self.initElements() # 先初始化各个控件 + self.initSlots() # 再将各个控件连接到信号槽 + self.initLayouts() # 然后布局 + self.initValues() # 再定义各个控件的值 + + def initElements(self): + self.引擎名称编辑框 = QLineEdit() + self.服务商选择框 = QComboBox() + self.appKey输入框 = QLineEdit() + self.语言Combobox = QComboBox() + self.accessKeyId输入框 = QLineEdit() + self.AccessKeySecret输入框 = QLineEdit() + + self.确定按钮 = QPushButton(self.tr('确定')) + self.取消按钮 = QPushButton(self.tr('取消')) + + self.纵向布局 = QVBoxLayout() + self.表格布局 = QFormLayout() + self.按钮横向布局 = QHBoxLayout() + + def initSlots(self): + self.服务商选择框.currentTextChanged.connect(self.服务商变化) + + self.确定按钮.clicked.connect(self.确认) + self.取消按钮.clicked.connect(self.取消) + + def initLayouts(self): + self.表格布局.addRow('引擎名称:', self.引擎名称编辑框) + self.表格布局.addRow('服务商:', self.服务商选择框) + self.表格布局.addRow('AppKey:', self.appKey输入框) + self.表格布局.addRow('语言:', self.语言Combobox) + self.表格布局.addRow('AccessKeyId:', self.accessKeyId输入框) + self.表格布局.addRow('AccessKeySecret:', self.AccessKeySecret输入框) + + self.按钮横向布局.addWidget(self.确定按钮) + self.按钮横向布局.addWidget(self.取消按钮) + + self.纵向布局.addLayout(self.表格布局) + self.纵向布局.addLayout(self.按钮横向布局) + + self.setLayout(self.纵向布局) + + def initValues(self): + self.引擎名称编辑框.setPlaceholderText(self.tr('例如:阿里-中文')) + + self.服务商选择框.addItems(['Alibaba']) + self.服务商选择框.setCurrentText('Alibaba') + + self.accessKeyId输入框.setEchoMode(QLineEdit.Password) + self.AccessKeySecret输入框.setEchoMode(QLineEdit.Password) + + self.setWindowIcon(QIcon(常量.图标路径)) + self.setWindowTitle(self.tr('添加或更新 Api')) + self.setWindowModality(Qt.NonModal) + + if self.列表.currentItem(): + 已选中的列表项 = self.列表.currentItem().text() + 填充数据 = self.从数据库得到选中项的数据(已选中的列表项) + self.引擎名称编辑框.setText(填充数据[0]) + self.服务商选择框.setCurrentText(填充数据[1]) + self.appKey输入框.setText(填充数据[2]) + self.语言Combobox.setCurrentText(填充数据[3]) + self.accessKeyId输入框.setText(填充数据[4]) + self.AccessKeySecret输入框.setText(填充数据[5]) + + self.show() + + + def 服务商变化(self): + if self.服务商选择框.currentText() == 'Alibaba': + self.语言Combobox.clear() + self.语言Combobox.addItem(self.tr('由 Api 的云端配置决定')) + self.语言Combobox.setCurrentText(self.tr('由 Api 的云端配置决定')) + self.语言Combobox.setEnabled(False) + self.appKey输入框.setEnabled(True) + # self.accessKeyId标签.setText('AccessKeyId:') + # self.AccessKeySecret标签.setText('AccessKeySecret:') + elif self.服务商选择框.currentText() == 'Tencent': + self.语言Combobox.clear() + self.语言Combobox.addItems(['中文普通话', '英语', '粤语']) + self.语言Combobox.setCurrentText('中文普通话') + self.语言Combobox.setEnabled(True) + self.appKey输入框.setEnabled(False) + # self.accessKeyId标签.setText('AccessSecretId:') + # self.AccessKeySecret标签.setText('AccessSecretKey:') + + + def 确认(self): + self.引擎名称 = self.引擎名称编辑框.text() # str + self.服务商 = self.服务商选择框.currentText() # str + self.AppKey = self.appKey输入框.text() # str + self.语言 = self.语言Combobox.currentText() # str + self.AccessKeyId = self.accessKeyId输入框.text() # str + self.AccessKeySecret = self.AccessKeySecret输入框.text() # str + self.有重名项 = self.检查数据库是否有重名项() + if self.引擎名称 == '': + return False + if self.有重名项: + 是否覆盖 = QMessageBox.warning(self, '覆盖确认', '已存在相同名字的引擎,是否覆盖?', QMessageBox.Yes | QMessageBox.Cancel, QMessageBox.Cancel) + if 是否覆盖 != QMessageBox.Yes: + return False + self.更新数据库() + else: + self.插入数据库() + self.close() + + def 取消(self): + self.close() + + def 从数据库得到选中项的数据(self, 已选中的列表项): + 数据库连接 = self.数据库连接 + cursor = 数据库连接.cursor() + result = cursor.execute(f'''select 引擎名称, + 服务商, + AppKey, + 语言, + AccessKeyId, + AccessKeySecret + from {self.表单名字} where {self.显示的列名} = :引擎名称;''', + {'引擎名称': 已选中的列表项}) + return result.fetchone() + # + def 检查数据库是否有重名项(self): + 数据库连接 = self.数据库连接 + cursor = 数据库连接.cursor() + result = cursor.execute(f'''select * from {self.表单名字} where {self.显示的列名} = :引擎名称;''', {'引擎名称': self.引擎名称}) + if result.fetchone() == None: return False # 没有重名项,返回 False + return True + # + def 更新数据库(self): + 数据库连接 = self.数据库连接 + cursor = 数据库连接.cursor() + cursor.execute(f'''update {self.表单名字} set 服务商 = :服务商, + AppKey = :AppKey, + 语言 = :语言, + AccessKeyId = :AccessKeyId, + AccessKeySecret = :AccessKeySecret + where {self.显示的列名} = :引擎名称 ''', + {'服务商': self.服务商, + 'AppKey': self.AppKey, + '语言': self.语言, + 'AccessKeyId': self.AccessKeyId, + 'AccessKeySecret': self.AccessKeySecret, + '引擎名称': self.引擎名称}) + 数据库连接.commit() + # + def 插入数据库(self): + 数据库连接 = self.数据库连接 + cursor = 数据库连接.cursor() + cursor.execute(f'''insert into {self.表单名字} (引擎名称, + 服务商, + AppKey, + 语言, + AccessKeyId, + AccessKeySecret) + values (:引擎名称, + :服务商, + :AppKey, + :语言, + :AccessKeyId, + :AccessKeySecret)''', + {'引擎名称': self.引擎名称, + '服务商': self.服务商, + 'AppKey': self.AppKey, + '语言': self.语言, + 'AccessKeyId': self.AccessKeyId, + 'AccessKeySecret': self.AccessKeySecret}) + 数据库连接.commit() + + # 根据刚开始预设名字是否为空,设置确定键可否使用 + def closeEvent(self, a0: QCloseEvent) -> None: + try: + self.列表.刷新列表() + except: + print('引擎列表刷新失败') diff --git a/src/moduels/gui/Group_EditableList.py b/src/moduels/gui/Group_EditableList.py new file mode 100644 index 0000000..c37bc4b --- /dev/null +++ b/src/moduels/gui/Group_EditableList.py @@ -0,0 +1,108 @@ +# -*- coding: UTF-8 -*- + +from PySide2.QtWidgets import * +from moduels.gui.List_List import List_List + +# 添加预设对话框 +class Group_EditableList(QGroupBox): + def __init__(self, 组名, 对话框类, 数据库连接, 表单名字, 显示的列名): + super().__init__(组名) + self.对话框类 = 对话框类 + self.数据库连接 = 数据库连接 + self.表单名字 = 表单名字 + self.显示的列名 = 显示的列名 + self.initElements() # 先初始化各个控件 + self.initSlots() # 再将各个控件连接到信号槽 + self.initLayouts() # 然后布局 + self.initValues() # 再定义各个控件的值 + + def initElements(self): + self.筛选文字输入框 = QLineEdit() + self.列表 = List_List(self.数据库连接, self.表单名字, self.显示的列名) + self.添加按钮 = QPushButton('+') + self.删除按钮 = QPushButton('-') + self.上移按钮 = QPushButton('↑') + self.下移按钮 = QPushButton('↓') + self.部件布局 = QGridLayout() + + def initSlots(self): + self.筛选文字输入框.textChanged.connect(self.筛选) + self.添加按钮.clicked.connect(self.添加或修改) + self.删除按钮.clicked.connect(self.删除) + self.上移按钮.clicked.connect(self.上移) + self.下移按钮.clicked.connect(self.下移) + + def initLayouts(self): + self.部件布局.addWidget(self.筛选文字输入框, 0, 0, 1, 2) + self.部件布局.addWidget(self.列表, 1, 0, 1, 2) + self.部件布局.addWidget(self.添加按钮, 2, 0, 1, 1) + self.部件布局.addWidget(self.删除按钮, 2, 1, 1, 1) + self.部件布局.addWidget(self.上移按钮, 3, 0, 1, 1) + self.部件布局.addWidget(self.下移按钮, 3, 1, 1, 1) + self.setLayout(self.部件布局) + + def initValues(self): + self.筛选文字输入框.setPlaceholderText('筛选') + self.列表.刷新列表() + + + def 添加或修改(self): + ''' + 打开对话框,添加或修改条目 + ''' + 对话框 = self.对话框类(self.列表, self.数据库连接, self.表单名字, self.显示的列名) + + def 删除(self): + if not self.列表.currentItem(): return False + 当前排 = self.列表.currentRow() + 已选中的列表项 = self.列表.currentItem().text() + answer = QMessageBox.question(self, self.tr('删除预设'), self.tr(f'将要删除“{已选中的列表项}”项,是否确认?')) + if answer != QMessageBox.Yes: return False + id = self.数据库连接.cursor().execute( + f'''select id from {self.表单名字} where {self.显示的列名} = :已选中的列表项''', {'已选中的列表项': 已选中的列表项}).fetchone()[0] + self.数据库连接.cursor().execute(f'''delete from {self.表单名字} where id = :id''', {'id': id}) + self.数据库连接.cursor().execute(f'''update {self.表单名字} set id=id-1 where id > :id''', {'id': id}) + self.数据库连接.commit() + self.列表.刷新列表() + if self.列表.count() >= 当前排: + self.列表.setCurrentRow(当前排) + + def 上移(self): + 当前排 = self.列表.currentRow() + if 当前排 > 0: + 已选中的列表项 = self.列表.currentItem().text() + id = self.数据库连接.cursor().execute( + f'''select id from {self.表单名字} where {self.显示的列名} = :已选中的列表项 ''', {'已选中的列表项': 已选中的列表项}).fetchone()[0] + self.数据库连接.cursor().execute(f'''update {self.表单名字} set id = 100000 where id = :id - 1 ''', {'id': id}) + self.数据库连接.cursor().execute(f'''update {self.表单名字} set id = id - 1 where {self.显示的列名} = :已选中的列表项''', {'已选中的列表项': 已选中的列表项}) + self.数据库连接.cursor().execute(f'''update {self.表单名字} set id = :id where id=100000 ''', {'id': id}) + self.数据库连接.commit() + self.列表.刷新列表() + if self.列表.筛选文字 == '': + self.列表.setCurrentRow(当前排 - 1) + return + + # 向下移动预设 + def 下移(self): + 当前排 = self.列表.currentRow() + 总行数 = self.列表.count() + if 当前排 > -1 and 当前排 < 总行数 - 1: + 已选中的列表项 = self.列表.currentItem().text() + id = self.数据库连接.cursor().execute( + f'''select id from {self.表单名字} where {self.显示的列名} = :已选中的列表项''', {'已选中的列表项': 已选中的列表项}).fetchone()[0] + self.数据库连接.cursor().execute(f'''update {self.表单名字} set id = 100000 where id = :id + 1 ''', {'id': id}) + self.数据库连接.cursor().execute(f'''update {self.表单名字} set id = id + 1 where {self.显示的列名} = :已选中的列表项''', {'已选中的列表项': 已选中的列表项}) + self.数据库连接.cursor().execute(f'''update {self.表单名字} set id = :id where id=100000 ''', {'id': id}) + self.数据库连接.commit() + self.列表.刷新列表() + if self.列表.筛选文字 == '': + if 当前排 < 总行数: + self.列表.setCurrentRow(当前排 + 1) + else: + self.列表.setCurrentRow(当前排) + return + + def 筛选(self): + self.列表.筛选文字 = self.筛选文字输入框.text() + self.列表.刷新列表() + diff --git a/src/moduels/gui/List_List.py b/src/moduels/gui/List_List.py new file mode 100644 index 0000000..1aee77a --- /dev/null +++ b/src/moduels/gui/List_List.py @@ -0,0 +1,57 @@ +# -*- coding: UTF-8 -*- + +from PySide2.QtWidgets import * +from PySide2.QtGui import * +from PySide2.QtCore import * +from moduels.component.NormalValue import 常量 + +# 添加预设对话框 +class List_List(QListWidget): + + 选中文字 = Signal(str) + + def __init__(self, 数据库连接, 表单名字, 显示的列名): + super().__init__() + self.筛选文字 = '' + self.数据库连接 = 数据库连接 + self.表单名字 = 表单名字 + self.显示的列名 = 显示的列名 + self.initElements() # 先初始化各个控件 + self.initSlots() # 再将各个控件连接到信号槽 + self.initLayouts() # 然后布局 + self.initValues() # 再定义各个控件的值 + + def initElements(self): + pass + + def initSlots(self): + pass + + def initLayouts(self): + pass + + def initValues(self): + self.刷新列表() + + def currentChanged(self, current, previous): + if current.row() > -1: + self.选中文字.emit(current.data()) + + def 刷新列表(self): + cursor = self.数据库连接.cursor() + if self.筛选文字 == '': + 显示项 = cursor.execute( + f'''select id, {self.显示的列名} from {self.表单名字} order by id''') + self.clear() + for i in 显示项: + self.addItem(i[1]) + else: + 显示项 = cursor.execute( + f'''select id, {self.显示的列名}, * from {self.表单名字} order by id''') + self.clear() + for i in 显示项: + for j in i[2:]: + if self.筛选文字 in str(j): + self.addItem(i[1]) + break + diff --git a/src/moduels/gui/MainWindow.py b/src/moduels/gui/MainWindow.py new file mode 100644 index 0000000..f85035e --- /dev/null +++ b/src/moduels/gui/MainWindow.py @@ -0,0 +1,134 @@ +# -*- coding: UTF-8 -*- + +from PySide2.QtWidgets import * +from PySide2.QtGui import * + +from moduels.component.NormalValue import 常量 +from moduels.component.Stream import Stream +from moduels.gui.Tab_CapsWriter import Tab_CapsWriter +# from moduels.gui.Tab_Stdout import Tab_Stdout +from moduels.gui.Tab_Config import Tab_Config +from moduels.gui.Tab_Help import Tab_Help + +import sys, os + +class MainWindow(QMainWindow): + def __init__(self): + super().__init__() + 常量.主窗口 = self + self.loadStyleSheet() + self.initElements() # 先初始化各个控件 + self.initSlots() # 再将各个控件连接到信号槽 + self.initLayouts() # 然后布局 + self.initValues() # 再定义各个控件的值 + + + # self.setWindowState(Qt.WindowMaximized) + # sys.stdout = Stream(newText=self.onUpdateText) + + def initElements(self): + self.状态栏 = self.statusBar() + # 定义中心控件为多 tab 页面 + self.tabs = QTabWidget() + + # 定义多个不同功能的 tab + self.设置标签页 = Tab_Config() # 设置页要在前排加载,以确保一些设置加载成功 + self.CapsWriter标签页 = Tab_CapsWriter() # 主要功能的 tab + # self.打印输出标签页 = Tab_Stdout() + self.帮助标签页 = Tab_Help() + + self.标准输出流 = Stream() + + + + def initLayouts(self): + + self.tabs.addTab(self.CapsWriter标签页, 'CapsWriter') + self.tabs.addTab(self.设置标签页, '设置') + self.tabs.addTab(self.帮助标签页, '帮助') + self.setCentralWidget(self.tabs) + + def initSlots(self): + self.CapsWriter标签页.状态栏消息.connect(lambda 消息, 时间: self.状态栏.showMessage(消息, 时间)) + # self.打印输出标签页.状态栏消息.connect(lambda 消息, 时间: self.状态栏.showMessage(消息, 时间)) + # self.设置标签页.状态栏消息.connect(lambda 消息, 时间: self.状态栏.showMessage(消息, 时间)) + self.帮助标签页.状态栏消息.connect(lambda 消息, 时间: self.状态栏.showMessage(消息, 时间)) + + self.标准输出流.newText.connect(self.CapsWriter标签页.更新控制台输出) + pass + + def initValues(self): + # self.adjustSize() + # self.setGeometry(QStyle(Qt.LeftToRight, Qt.AlignCenter, self.size(), QApplication.desktop().availableGeometry())) + 常量.状态栏 = self.状态栏 + sys.stdout = self.标准输出流 + self.setWindowIcon(QIcon(常量.图标路径)) + self.setWindowTitle('CapsWriter 语音输入工具') + self.setWindowFlag(Qt.WindowStaysOnTopHint) # 始终在前台 + print("""\n软件介绍: + +CapsWriter,顾名思义,就是按下大写锁定键来打字的工具。它的具体作用是:当你按下键盘上的大写锁定键后,软件开始语音识别,当你松开大写锁定键时,识别的结果就可以立马上屏。 + +目前软件内置了对阿里云一句话识别 API 的支持。如果你要使用,就需要先在阿里云上实名认证,申请语音识别 API,在设置页面添加一个语音识别引擎。 + +具体申请阿里云 API 的方法,可以参考我这个视频:https://www.bilibili.com/video/BV1qK4y1s7Fb/ + +添加上引擎后,在当前页面选择一个引擎,点击启用按钮,就可以进行语音识别了!嗯 + +启用后,在实际使用中,只要按下 CapsLock 键,软件就会立刻开始录音: + + 如果只是单击 CapsLock 后松开,录音数据会立刻被删除; + 如果按下 CapsLock 键时长超过 0.3 秒,就会开始连网进行语音识别,松开 CapsLock 键时,语音识别结果会被立刻输入。 + +所以你只需要按下 CapsLock 键,无需等待,就可以开始说话,因为当你按下按下 CapsLock 键的时候,程序就开始录音了。说完后,松开,识别结果立马上屏。\r\n""") + + self.show() + + def 移动到屏幕中央(self): + rectangle = self.frameGeometry() + center = QApplication.desktop().availableGeometry().center() + rectangle.moveCenter(center) + self.move(rectangle.topLeft()) + + def 更新控制台输出(self, text): + self.打印输出标签页.print(text) + + def loadStyleSheet(self): + try: + try: + with open(常量.样式文件, 'r', encoding='utf-8') as style: + self.setStyleSheet(style.read()) + except: + with open(常量.样式文件, 'r', encoding='gbk') as style: + self.setStyleSheet(style.read()) + except: + QMessageBox.warning(self, self.tr('主题载入错误'), self.tr('未能成功载入主题,请确保软件 misc 目录有 "style.css" 文件存在。')) + + def keyPressEvent(self, event) -> None: + # 在按下 F5 的时候重载 style.css 主题 + if (event.key() == Qt.Key_F5): + self.loadStyleSheet() + self.status.showMessage('已成功更新主题', 800) + + def onUpdateText(self, text): + """Write console output to text widget.""" + + cursor = self.consoleTab.consoleEditBox.textCursor() + cursor.movePosition(QTextCursor.End) + cursor.insertText(text) + self.consoleTab.consoleEditBox.setTextCursor(cursor) + self.consoleTab.consoleEditBox.ensureCursorVisible() + + def 状态栏提示(self, 提示文字:str, 时间:int): + self.状态栏.showMessage(提示文字, 时间) + + + def closeEvent(self, event): + """Shuts down application on close.""" + # Return stdout to defaults. + if 常量.关闭时隐藏到托盘: + event.ignore() + self.hide() + else: + sys.stdout = sys.__stdout__ + super().closeEvent(event) diff --git a/src/moduels/gui/SystemTray.py b/src/moduels/gui/SystemTray.py new file mode 100644 index 0000000..5f0cfc4 --- /dev/null +++ b/src/moduels/gui/SystemTray.py @@ -0,0 +1,68 @@ +# -*- coding: UTF-8 -*- + +from PySide2.QtWidgets import * +from PySide2.QtCore import * +from PySide2.QtGui import * + +import sys + +from moduels.component.NormalValue import 常量 + +class SystemTray(QSystemTrayIcon): + def __init__(self, 主窗口): + super(SystemTray, self).__init__() + self.主窗口 = 主窗口 + self.initElements() # 先初始化各个控件 + self.initSlots() # 再将各个控件连接到信号槽 + self.initLayouts() # 然后布局 + self.initValues() # 再定义各个控件的值 + + # self.RestoreAction = QAction(u'还原 ', self, triggered=self.showWindow) # 添加一级菜单动作选项(还原主窗口) + # self.StyleAction = QAction(self.tr('更新主题'), self, triggered=mainWindow.loadStyleSheet) # 添加一级菜单动作选项(更新 QSS) + # self.tray_menu.addAction(self.RestoreAction) # 为菜单添加动作 + # self.tray_menu.addAction(self.StyleAction) + + def initElements(self): + self.托盘菜单 = QMenu(QApplication.desktop()) # 创建菜单 + self.QuitAction = QAction(self.tr('退出'), self, triggered=self.退出) # 添加一级菜单动作选项(退出程序) + + def initSlots(self): + self.activated.connect(self.trayEvent) # 设置托盘点击事件处理函数 + + def initLayouts(self): + self.托盘菜单.addAction(self.QuitAction) + + def initValues(self): + self.setIcon(QIcon(常量.图标路径)) + self.setParent(self.主窗口) + self.setContextMenu(self.托盘菜单) # 设置系统托盘菜单 + self.show() + + def 显示主窗口(self): + self.主窗口.showNormal() + self.主窗口.activateWindow() + self.主窗口.setWindowFlag(Qt.Window, Qt.WindowStaysOnTopHint) # 始终在前台 + self.主窗口.show() + + def 退出(self): + sys.stdout = sys.__stdout__ + self.hide() + QApplication.quit() + + def 切换聆听中的图标(self): + self.setIcon(QIcon(常量.聆听图标路径)) + + def 切换正常图标(self): + self.setIcon(QIcon(常量.图标路径)) + + def trayEvent(self, reason): + # 鼠标点击icon传递的信号会带有一个整形的值,1是表示单击右键,2是双击,3是单击左键,4是用鼠标中键点击 + if reason == 2 or reason == 3: + if 常量.主窗口.isMinimized() or not 常量.主窗口.isVisible(): + # 若是最小化或者最小化到托盘,则先正常显示窗口,再变为活动窗口(暂时显示在最前面) + self.显示主窗口() + else: + # 若不是最小化,则最小化 + # self.window.showMinimized() + self.主窗口.hide() + pass \ No newline at end of file diff --git a/src/moduels/gui/Tab_CapsWriter.py b/src/moduels/gui/Tab_CapsWriter.py new file mode 100644 index 0000000..1413f51 --- /dev/null +++ b/src/moduels/gui/Tab_CapsWriter.py @@ -0,0 +1,189 @@ +# -*- coding: UTF-8 -*- + +from PySide2.QtWidgets import * +from PySide2.QtGui import * +from PySide2.QtCore import * +import os, re, subprocess, time + +import pyaudio + +# from moduels.component.QLEdit_FilePathQLineEdit import QLEdit_FilePathQLineEdit +from moduels.component.NormalValue import 常量 +from moduels.component.QEditBox_StdoutBox import QEditBox_StdoutBox +# from moduels.component.SpaceLine import QHLine, QVLine +from moduels.thread.Thread_AliEngine import Thread_AliEngine +# from moduels.thread.Thread_GenerateSkins import Thread_GenerateSkins +# from moduels.thread.Thread_ExtractAllSkin import Thread_ExtractAllSkin + +# from moduels.function.applyTemplate import applyTemplate +# from moduels.function.openSkinSourcePath import openSkinSourcePath +# +# from moduels.gui.Dialog_AddSkin import Dialog_AddSkin +# from moduels.gui.Dialog_DecompressSkin import Dialog_DecompressSkin +# from moduels.gui.Dialog_RestoreSkin import Dialog_RestoreSkin +# from moduels.gui.Group_EditableList import Group_EditableList +# from moduels.gui.VBox_RBtnContainer import VBox_RBtnContainer +from moduels.gui.Combo_EngineList import Combo_EngineList + + +class Tab_CapsWriter(QWidget): + 状态栏消息 = Signal(str, int) + + def __init__(self): + super().__init__() + self.initElements() # 先初始化各个控件 + self.initSlots() # 再将各个控件连接到信号槽 + self.initLayouts() # 然后布局 + self.initValues() # 再定义各个控件的值 + + + def initElements(self): + self.页面总布局 = QVBoxLayout() + + self.引擎选择Box = QGroupBox('引擎选择') + self.引擎选择下拉框 = Combo_EngineList() + self.引擎选择Box布局 = QVBoxLayout() + + self.控制台输出Box = QGroupBox('提示消息') + self.控制台输出框 = QEditBox_StdoutBox() + self.控制台输出Box布局 = QVBoxLayout() + + self.启停按钮Box = QGroupBox('开关') + self.启动按钮 = QPushButton('启用 CapsWriter') + self.停止按钮 = QPushButton('停止 CapsWriter') + self.启停按钮Box布局 = QHBoxLayout() + + def initLayouts(self): + self.引擎选择Box布局.addWidget(self.引擎选择下拉框) + + self.控制台输出Box布局.addWidget(self.控制台输出框) + + self.启停按钮Box布局.addWidget(self.启动按钮) + self.启停按钮Box布局.addWidget(self.停止按钮) + + self.引擎选择Box.setLayout(self.引擎选择Box布局) + self.控制台输出Box.setLayout(self.控制台输出Box布局) + self.启停按钮Box.setLayout(self.启停按钮Box布局) + + self.页面总布局.addWidget(self.引擎选择Box) + self.页面总布局.addWidget(self.控制台输出Box) + self.页面总布局.addWidget(self.启停按钮Box) + + self.setLayout(self.页面总布局) + + def initSlots(self): + self.启动按钮.clicked.connect(self.启动引擎) + self.停止按钮.clicked.connect(self.停止引擎) + + def 更新控制台输出(self, 文本): + self.控制台输出框.print(文本) + + + def initValues(self): + self.引擎线程 = None + # self.aliClient = ali_speech.NlsClient() + # self.aliClient.set_log_level('WARNING') # 设置 client 输出日志信息的级别:DEBUG、INFO、WARNING、ERROR + self.停止按钮.setDisabled(True) + + def 启动引擎(self): + if self.引擎线程 != None: return + 引擎名称 = self.引擎选择下拉框.currentText() + if 引擎名称 == '': return + self.启动按钮.setDisabled(True) + self.停止按钮.setEnabled(True) + result = 常量.数据库连接.execute(f'''select * from {常量.语音引擎表单名} where 引擎名称 = :引擎名称''', + {'引擎名称': 引擎名称}).fetchone() + if result == None: return + self.引擎线程 = Thread_AliEngine(引擎名称, self) + self.引擎线程.识别中的信号.connect(常量.托盘.切换聆听中的图标) + self.引擎线程.结束识别的信号.connect(常量.托盘.切换正常图标) + self.引擎线程.引擎出错信号.connect(self.停止引擎) + self.引擎线程.start() + + + def 停止引擎(self): + if self.引擎线程 != None: + self.引擎线程.停止引擎() + # print(self.引擎线程.isRunning()) + self.引擎线程 = None + self.启动按钮.setEnabled(True) + self.停止按钮.setDisabled(True) + + + # self.压缩图片_按钮纵向布局.通过id勾选单选按钮(常量.输出选项['图片压缩']) + # self.输出格式_按钮纵向布局.通过id勾选单选按钮(常量.输出选项['输出格式']) + # self.其它_安装到手机选框.setChecked(常量.输出选项['adb发送至手机']) + # self.其它_清理注释选框.setChecked(常量.输出选项['清理注释']) + # + # def 无线adb(self): + # self.无线adb线程.start() + # + # def 提取皮肤(self): + # self.提取皮肤线程.start() + # + # def 解压皮肤(self): + # 解压皮肤对话框 = Dialog_DecompressSkin() + # + # def 发送皮肤(self): + # 获得的皮肤路径 = QFileDialog.getOpenFileName(self, caption='选择皮肤', dir=常量.皮肤输出路径, filter='皮肤文件 (*.bds)')[0] + # if 获得的皮肤路径 == '': return True + # 皮肤文件名 = os.path.basename(获得的皮肤路径) + # 手机皮肤路径 = '/sdcard/baidu/ime/skins/' + 皮肤文件名 + # 发送皮肤命令 = f'''adb push "{获得的皮肤路径}" "{手机皮肤路径}"''' + # subprocess.run(发送皮肤命令, startupinfo=常量.subprocessStartUpInfo) + # 安装皮肤命令 = f'''adb shell am start -a android.intent.action.VIEW -c android.intent.category.DEFAULT -n com.baidu.input/com.baidu.input.ImeUpdateActivity -d '{手机皮肤路径}' ''' + # subprocess.run(安装皮肤命令, startupinfo=常量.subprocessStartUpInfo) + # + # + # def 备份选中皮肤(self): + # if self.皮肤列表Box.列表.currentRow() < 0: return + # 已选中的列表项 = self.皮肤列表Box.列表.currentItem().text() + # 输出文件名, 皮肤源文件目录 = 常量.数据库连接.cursor().execute( + # f'select outputFileName, sourceFilePath from {常量.数据库皮肤表单名} where skinName = :皮肤名字;', + # {'皮肤名字': 已选中的列表项}).fetchone() + # 备份时间 = time.localtime() + # 备份压缩文件名 = f'{输出文件名}_备份_{备份时间.tm_year}年{备份时间.tm_mon}月{备份时间.tm_mday}日{备份时间.tm_hour}时{备份时间.tm_min}分{备份时间.tm_sec}秒.bds' + # 备份文件完整路径 = os.path.join(常量.皮肤输出路径, '皮肤备份文件', 备份压缩文件名) + # 备份命令 = f'''winrar a -afzip -ibck -r -ep1 "{备份文件完整路径}" "{皮肤源文件目录}/*"''' + # if not os.path.exists(os.path.dirname(备份文件完整路径)): os.makedirs(os.path.dirname(备份文件完整路径)) + # subprocess.run(备份命令, startupinfo=常量.subprocessStartUpInfo) + # os.startfile(os.path.dirname(备份文件完整路径)) + # + # + # def 还原选中皮肤(self): + # if self.皮肤列表Box.列表.currentRow() < 0: return + # 已选中的列表项 = self.皮肤列表Box.列表.currentItem().text() + # 输出文件名, 皮肤源文件目录 = 常量.数据库连接.cursor().execute( + # f'select outputFileName, sourceFilePath from {常量.数据库皮肤表单名} where skinName = :皮肤名字;', + # {'皮肤名字': 已选中的列表项}).fetchone() + # 备份文件夹路径 = os.path.join(常量.皮肤输出路径, '皮肤备份文件') + # Dialog_RestoreSkin(备份文件夹路径, 输出文件名, 皮肤源文件目录) + # + # def 打开皮肤输出文件夹(self): + # if not os.path.exists(常量.皮肤输出路径): os.makedirs(常量.皮肤输出路径) + # os.startfile(常量.皮肤输出路径) + # + # def 打包选中皮肤(self): + # if self.皮肤列表Box.列表.currentRow() < 0: return True + # self.备份选中皮肤_按钮.setDisabled(True) + # self.还原选中皮肤_按钮.setDisabled(True) + # self.打包选中皮肤_按钮.setDisabled(True) + # self.打包所有皮肤_按钮.setDisabled(True) + # self.生成皮肤线程.是否要全部生成 = False + # self.生成皮肤线程.start() + # + # def 打包所有皮肤(self): + # self.备份选中皮肤_按钮.setDisabled(True) + # self.还原选中皮肤_按钮.setDisabled(True) + # self.打包选中皮肤_按钮.setDisabled(True) + # self.打包所有皮肤_按钮.setDisabled(True) + # self.生成皮肤线程.是否要全部生成 = True + # self.生成皮肤线程.start() + # + # def 生成皮肤线程完成(self): + # self.备份选中皮肤_按钮.setEnabled(True) + # self.还原选中皮肤_按钮.setEnabled(True) + # self.打包选中皮肤_按钮.setEnabled(True) + # self.打包所有皮肤_按钮.setEnabled(True) + # 常量.状态栏.showMessage('打包任务完成!', 5000) + diff --git a/src/moduels/gui/Tab_Config.py b/src/moduels/gui/Tab_Config.py new file mode 100644 index 0000000..bc91e75 --- /dev/null +++ b/src/moduels/gui/Tab_Config.py @@ -0,0 +1,134 @@ +import webbrowser +from PySide2.QtCore import * +from PySide2.QtGui import * +from PySide2.QtSql import * +from PySide2.QtWidgets import * +from moduels.component.NormalValue import 常量 +from moduels.gui.Group_EditableList import Group_EditableList +from moduels.gui.Dialog_AddEngine import Dialog_AddEngine +# from moduels.gui.Group_PathSetting import Group_PathSetting + + +class Tab_Config(QWidget): + 状态栏消息 = Signal(str, int) + + def __init__(self, parent=None): + super(Tab_Config, self).__init__(parent) + self.initElements() # 先初始化各个控件 + self.initSlots() # 再将各个控件连接到信号槽 + self.initLayouts() # 然后布局 + self.initValues() # 再定义各个控件的值 + + def initElements(self): + self.程序设置Box = QGroupBox(self.tr('程序设置')) + self.开关_关闭窗口时隐藏到托盘 = QCheckBox(self.tr('点击关闭按钮时隐藏到托盘')) + self.程序设置横向布局 = QHBoxLayout() + + self.引擎列表 = Group_EditableList('语音引擎', Dialog_AddEngine, 常量.数据库连接, 常量.语音引擎表单名, '引擎名称') + + self.常用网址Box = QGroupBox('网页控制台') + self.常用网址Box布局 = QGridLayout() + self.智能语音交互控制台按钮 = QPushButton('智能语音交互') + self.RAM访问控制控制台按钮 = QPushButton('RAM访问控制') + + self.页面布局 = QVBoxLayout() + + + def initSlots(self): + self.开关_关闭窗口时隐藏到托盘.stateChanged.connect(self.设置_隐藏到状态栏) + self.智能语音交互控制台按钮.clicked.connect(lambda: webbrowser.open(r'https://nls-portal.console.aliyun.com/')) + self.RAM访问控制控制台按钮.clicked.connect(lambda: webbrowser.open(r'https://ram.console.aliyun.com/')) + # self.路径设置Box.皮肤输出路径输入框.textChanged.connect(self.设置_皮肤输出路径) + # self.路径设置Box.音效文件路径输入框.textChanged.connect(self.设置_音效文件路径) + + def initLayouts(self): + self.程序设置横向布局.addWidget(self.开关_关闭窗口时隐藏到托盘) + self.程序设置Box.setLayout(self.程序设置横向布局) + + self.常用网址Box布局.addWidget(self.智能语音交互控制台按钮, 0, 0) + self.常用网址Box布局.addWidget(self.RAM访问控制控制台按钮, 0, 1) + self.常用网址Box.setLayout(self.常用网址Box布局) + + self.页面布局.addWidget(self.程序设置Box) + self.页面布局.addWidget(self.引擎列表) + self.页面布局.addWidget(self.常用网址Box) + self.页面布局.addStretch(1) + + self.setLayout(self.页面布局) + + def initValues(self): + self.检查数据库() + + + def 检查数据库(self): + 数据库连接 = 常量.数据库连接 + self.检查数据库_关闭时最小化(数据库连接) + + def 检查数据库_关闭时最小化(self, 数据库连接): + result = 数据库连接.cursor().execute(f'''select value from {常量.偏好设置表单名} where item = :item''', + {'item': 'hideToTrayWhenHitCloseButton'}).fetchone() + if result == None: # 如果关闭窗口最小化到状态栏这个选项还没有在数据库创建,那就创建一个 + 初始值 = 'False' + 数据库连接.cursor().execute(f'''insert into {常量.偏好设置表单名} (item, value) values (:item, :value) ''', + {'item': 'hideToTrayWhenHitCloseButton', + 'value':初始值}) + 数据库连接.commit() + self.开关_关闭窗口时隐藏到托盘.setChecked(初始值 == 'True') + else: + self.开关_关闭窗口时隐藏到托盘.setChecked(result[0] == 'True') + # + # def 检查数据库_皮肤输出路径(self, 数据库连接): + # result = 数据库连接.cursor().execute(f'''select value from {常量.偏好设置表单名} where item = :item''', + # {'item': 'skinOutputPath'}).fetchone() + # if result == None: # 如果关闭窗口最小化到状态栏这个选项还没有在数据库创建,那就创建一个 + # 初始值 = 'output' + # 数据库连接.cursor().execute(f'''insert into {常量.偏好设置表单名} (item, value) values (:item, :value) ''', + # {'item': 'skinOutputPath', + # 'value': 初始值}) + # 数据库连接.commit() + # self.路径设置Box.皮肤输出路径输入框.setText(初始值) + # else: + # self.路径设置Box.皮肤输出路径输入框.setText(result[0]) + # + # def 检查数据库_音效文件路径(self, 数据库连接): + # result = 数据库连接.cursor().execute(f'''select value from {常量.偏好设置表单名} where item = :item''', + # {'item': 'soundFilePath'}).fetchone() + # if result == None: # 如果关闭窗口最小化到状态栏这个选项还没有在数据库创建,那就创建一个 + # 初始值 = 'sound' + # 数据库连接.cursor().execute(f'''insert into {常量.偏好设置表单名} (item, value) values (:item, :value) ''', + # {'item': 'soundFilePath', + # 'value': 初始值}) + # 数据库连接.commit() + # self.路径设置Box.音效文件路径输入框.setText(初始值) + # else: + # self.路径设置Box.音效文件路径输入框.setText(result[0]) + + def 设置_隐藏到状态栏(self): + 数据库连接 = 常量.数据库连接 + 数据库连接.cursor().execute(f'''update {常量.偏好设置表单名} set value = :value where item = :item''', + {'item': 'hideToTrayWhenHitCloseButton', + 'value': str(self.开关_关闭窗口时隐藏到托盘.isChecked())}) + 数据库连接.commit() + 常量.关闭时隐藏到托盘 = self.开关_关闭窗口时隐藏到托盘.isChecked() + + # def 设置_皮肤输出路径(self): + # 数据库连接 = 常量.数据库连接 + # 数据库连接.cursor().execute(f'''update {常量.数据库偏好设置表单名} set value = :value where item = :item''', + # {'item': 'skinOutputPath', + # 'value': self.路径设置Box.皮肤输出路径输入框.text()}) + # 数据库连接.commit() + # 常量.皮肤输出路径 = self.路径设置Box.皮肤输出路径输入框.text() + # + # + # def 设置_音效文件路径(self): + # 数据库连接 = 常量.数据库连接 + # 数据库连接.cursor().execute(f'''update {常量.数据库偏好设置表单名} set value = :value where item = :item''', + # {'item': 'soundFilePath', + # 'value': self.路径设置Box.音效文件路径输入框.text()}) + # 数据库连接.commit() + # 常量.音效文件路径 = self.路径设置Box.音效文件路径输入框.text() + + def 隐藏到状态栏开关被点击(self): + cursor = 常量.数据库连接.cursor() + cursor.execute(f'''update {常量.数据库偏好设置表单名} set value='{str(self.开关_关闭窗口时隐藏到托盘.isChecked())}' where item = '{'hideToTrayWhenHitCloseButton'}';''') + 常量.数据库连接.commit() diff --git a/src/moduels/gui/Tab_Help.py b/src/moduels/gui/Tab_Help.py new file mode 100644 index 0000000..a98cd0a --- /dev/null +++ b/src/moduels/gui/Tab_Help.py @@ -0,0 +1,69 @@ +# -*- coding: UTF-8 -*- + +from PySide2.QtWidgets import * +from PySide2.QtCore import Signal +from moduels.component.NormalValue import 常量 +from moduels.component.SponsorDialog import SponsorDialog + +import os, webbrowser + + +class Tab_Help(QWidget): + 状态栏消息 = Signal(str, int) + + def __init__(self): + super().__init__() + self.initElement() # 先初始化各个控件 + self.initSlots() # 再将各个控件连接到信号槽 + self.initLayout() # 然后布局 + self.initValue() # 再定义各个控件的值 + + def initElement(self): + self.打开帮助按钮 = QPushButton(self.tr('打开帮助文档')) + self.ffmpegMannualNoteButton = QPushButton(self.tr('查看作者的 FFmpeg 笔记')) + self.openVideoHelpButtone = QPushButton(self.tr('查看视频教程')) + self.openGiteePage = QPushButton(self.tr(f'当前版本是 v{常量.软件版本},到 Gitee 检查新版本')) + self.openGithubPage = QPushButton(self.tr(f'当前版本是 v{常量.软件版本},到 Github 检查新版本')) + self.linkToDiscussPage = QPushButton(self.tr('加入 QQ 群')) + self.tipButton = QPushButton(self.tr('打赏作者')) + + self.masterLayout = QVBoxLayout() + + def initSlots(self): + self.打开帮助按钮.clicked.connect(self.openHelpDocument) + self.ffmpegMannualNoteButton.clicked.connect(lambda: webbrowser.open(self.tr(r'https://hacpai.com/article/1595480295489'))) + self.openVideoHelpButtone.clicked.connect(lambda: webbrowser.open(self.tr(r'https://www.bilibili.com/video/BV12A411p73r/'))) + self.openGiteePage.clicked.connect(lambda: webbrowser.open(self.tr(r'https://gitee.com/haujet/CapsWriter/releases'))) + self.openGithubPage.clicked.connect(lambda: webbrowser.open(self.tr(r'https://github.com/HaujetZhao/CapsWriter/releases'))) + self.linkToDiscussPage.clicked.connect(lambda: webbrowser.open( + self.tr(r'https://qm.qq.com/cgi-bin/qm/qr?k=DgiFh5cclAElnELH4mOxqWUBxReyEVpm&jump_from=webapi'))) + self.tipButton.clicked.connect(lambda: SponsorDialog(self)) + + def initLayout(self): + self.setLayout(self.masterLayout) + # self.masterLayout.addWidget(self.打开帮助按钮) + # self.masterLayout.addWidget(self.ffmpegMannualNoteButton) + self.masterLayout.addWidget(self.openVideoHelpButtone) + self.masterLayout.addWidget(self.openGiteePage) + self.masterLayout.addWidget(self.openGithubPage) + self.masterLayout.addWidget(self.linkToDiscussPage) + self.masterLayout.addWidget(self.tipButton) + + def initValue(self): + self.打开帮助按钮.setMaximumHeight(100) + self.ffmpegMannualNoteButton.setMaximumHeight(100) + self.openVideoHelpButtone.setMaximumHeight(100) + self.openGiteePage.setMaximumHeight(100) + self.openGithubPage.setMaximumHeight(100) + self.linkToDiscussPage.setMaximumHeight(100) + self.tipButton.setMaximumHeight(100) + + def openHelpDocument(self): + try: + if 常量.系统平台 == 'Darwin': + import shlex + os.system("open " + shlex.quote(self.tr("./misc/Docs/README_zh.html"))) + elif 常量.系统平台 == 'Windows': + os.startfile(os.path.realpath(self.tr('./misc/Docs/README_zh.html'))) + except: + print('未能打开帮助文档') diff --git a/src/moduels/thread/Thread_AliEngine.py b/src/moduels/thread/Thread_AliEngine.py new file mode 100644 index 0000000..0973308 --- /dev/null +++ b/src/moduels/thread/Thread_AliEngine.py @@ -0,0 +1,262 @@ +# -*- coding: UTF-8 -*- + +import json +import os +import pyaudio +import threading +import keyboard +import sqlite3 +import time + +import ali_speech + +from PySide2.QtWidgets import * +from PySide2.QtGui import * +from PySide2.QtCore import * + +from moduels.component.NormalValue import 常量 +from moduels.function.getAlibabaRecognizer import getAlibabaRecognizer + + + + +class Thread_AliEngine(QThread): + 状态栏消息 = Signal(str, int) + 引擎出错信号 = Signal() + + CHUNK = 1024 # 数据包或者数据片段 + FORMAT = pyaudio.paInt16 # pyaudio.paInt16表示我们使用量化位数 16位来进行录音 + CHANNELS = 1 # 声道,1为单声道,2为双声道 + RATE = 16000 # 采样率,每秒钟16000次 + 总共写入音频片段数 = 0 + + count = 1 # 计数 + 待命中 = True # 是否准备开始录音 + 识别中 = False # 控制录音是否停止 + + 识别中的信号 = Signal() + 结束识别的信号 = Signal() + + + def __init__(self, 引擎名称, parent=None): + super().__init__(parent) + self.正在运行 = 0 + self.引擎名称 = 引擎名称 + self.得到引擎信息() + self.tokenId = 0 + self.tokenExpireTime = 0 + self.构建按键发送器() + QApplication.instance().aboutToQuit.connect(self.要退出了) + + def 要退出了(self): + self.terminate() + + def 构建按键发送器(self): + if 常量.系统平台 == 'Windows': + import win32com.client as comclt + self.按键发送器 = comclt.Dispatch("WScript.Shell") + + def 发送大写锁定键(self): + if 常量.系统平台 == 'Windows': + self.按键发送器.SendKeys("{CAPSLOCK}") + else: + self.取消监听大写锁定键() + keyboard.press_and_release('caps lock') + self.开始监听大写锁定键() + + def 开始监听大写锁定键(self): + keyboard.hook_key('caps lock', self.大写锁定键被触发) + + def 取消监听大写锁定键(self): + try: + keyboard.unhook('caps lock') + except: + pass + + def 停止引擎(self): + self.取消监听大写锁定键() + keyboard.unhook_all() + self.setTerminationEnabled(True) + self.terminate() + print('引擎已停止\n\n') + self.正在运行 = 0 + + def 得到引擎信息(self): + 数据库连接 = sqlite3.connect(常量.数据库路径) + self.appKey, self.accessKeyId, self.accessKeySecret = 数据库连接.execute(f'''select AppKey, + AccessKeyId, + AccessKeySecret + from {常量.语音引擎表单名} + where 引擎名称 = :引擎名称''', + {'引擎名称': self.引擎名称}).fetchone() + 数据库连接.close() + + def 大写锁定键被触发(self, event): + if event.event_type == "down": + if self.识别中: + return + self.识别中 = True + try: + self.data = [] + threading.Thread(target=self.录音线程, args=[self.p]).start() # 开始录音 + threading.Thread(target=self.识别线程).start() # 开始识别 + + except: + print('process 启动失败') + elif event.event_type == "up": + # self.访问录音数据的线程锁.acquire() + self.识别中 = False + # self.访问录音数据的线程锁.release() + else: + # print(event.event_type) + pass + def 为下一次输入准备识别器(self): + self.识别器 = getAlibabaRecognizer(self.client, + self.appKey, + self.accessKeyId, + self.accessKeySecret, + self.tokenId, + self.tokenExpireTime, + 线程=self) + if self.识别器 == False: + print('获取云端识别器出错\n') + self.引擎出错信号.emit() + return False + + def 录音线程(self, p): + self.录音(p) + + def 识别线程(self): + self.识别中的信号.emit() + if not self.识别(): + self.count += 1 + self.总共写入音频片段数 = 0 + self.结束识别的信号.emit() + + def 录音(self, p): + # print('准备录制') + stream = p.open(channels=self.CHANNELS, + format=self.FORMAT, + rate=self.RATE, + input=True, + frames_per_buffer=self.CHUNK) + + # print('录制器准备完毕') + # 录音写入序号 = 1 + for i in range(5): + # self.访问录音数据的线程锁.acquire() + if not self.识别中: + self.data = [] + # self.访问录音数据的线程锁.release() + return + # print(f'录音{录音写入序号},开始写入,时间 {time.time()}') + self.data.append(stream.read(self.CHUNK)) + # print(f'录音{录音写入序号},写入结束,时间 {time.time()}') + # 录音写入序号 += 1 + # self.访问录音数据的线程锁.release() + # 在这里录下5个小片段,大约录制了0.32秒,如果这个时候松开了大写锁定键,就不打开连接。如果还继续按着,那就开始识别。 + + while self.识别中: + # self.访问录音数据的线程锁.acquire() + # print(f'录音{录音写入序号},开始写入,时间 {time.time()}') + self.data.append(stream.read(self.CHUNK)) + # print(f'录音{录音写入序号},写入结束,时间 {time.time()}\n') + # 录音写入序号 += 1 + # self.访问录音数据的线程锁.release() + # self.访问录音数据的线程锁.acquire() + time.sleep(0.0) + self.总共写入音频片段数 = len(self.data) + # self.访问录音数据的线程锁.release() + self.发送大写锁定键() # 再按下大写锁定键,还原大写锁定 + stream.stop_stream()# print('停止录制流') + stream.close() + + + # 这边开始上传识别 + def 识别(self): + # print('识别器开始等待') + for i in range(5): + time.sleep(0.06) + if not self.识别中: + return # 如果这个时候大写锁定键松开了 那就返回 + # print('识别器等待完闭') + # try: + print(self.tr('\n{}:在识别了,说完后请松开 CapsLock 键...').format(self.count)) + 识别器 = self.识别器 + self.识别器 = None + threading.Thread(target=self.为下一次输入准备识别器).start() # 用新线程为下一次识别准备识别器 + # print('准备新的识别器') + try: + ret = 识别器.start() # 识别器开始识别 + except: + print('识别器开启失败') + return False + if ret < 0: + return False # 如果开始识别出错了,那就返回 + 已发送音频片段数 = 0 # 对音频片段记数 + # j = 1 + 当前进程测得数据片段总数 = len(self.data) + while self.识别中 or 已发送音频片段数 < 当前进程测得数据片段总数 or 已发送音频片段数 < self.总共写入音频片段数: + # self.访问录音数据的线程锁.acquire() + 当前进程测得数据片段总数 = len(self.data) + # self.访问录音数据的线程锁.release() + # print(f' 已发送音频片段数: {已发送音频片段数}, 当前进程测得数据片段总数: {当前进程测得数据片段总数}') + if 已发送音频片段数 > 当前进程测得数据片段总数: + return True + elif 已发送音频片段数 == 当前进程测得数据片段总数: + time.sleep(0.05) + if 已发送音频片段数 < 当前进程测得数据片段总数: + # self.访问录音数据的线程锁.acquire() + 要发送的音频数据 = self.data[已发送音频片段数] + # self.访问录音数据的线程锁.release() + try: + # print(f' 发送器{j},开始发送,时间 {time.time()}') + ret = 识别器.send(要发送的音频数据) # 发送音频数据 + # print(f' 发送器{j},发送结束,时间 {time.time()}\n') + # j += 1 + except: + print('识别器发送失败') + return False + 已发送音频片段数 += 1 + # print(self.tr('\n{}:按住 CapsLock 键后开始说话...').format(self.count + 1)) + self.总共写入音频片段数 = 0 + self.结束识别的信号.emit() + self.count += 1 + 识别器.stop() + 识别器.close() + return True + + + def run(self): + if self.正在运行 == 1: return False + self.正在运行 = 1 + + self.client = ali_speech.NlsClient() + self.client.set_log_level('WARNING') # 设置 client 输出日志信息的级别:DEBUG、INFO、WARNING、ERROR + + self.tokenId = 0 + self.tokenExpireTime = 0 + # try: + self.识别器 = getAlibabaRecognizer(self.client, + self.appKey, + self.accessKeyId, + self.accessKeySecret, + self.tokenId, + self.tokenExpireTime, + 线程=self) + if self.识别器 == False: + print('获取云端识别器出错\n') + self.引擎出错信号.emit() + return False + + self.p = pyaudio.PyAudio() # 在 QThread 中引入 PyAudio 会使得 PySide2 图形界面阻塞 + + self.开始监听大写锁定键() + + print("""引擎初始化完成\n""") + print('按住 CapsLock 键后开始说话...'.format(self.count)) + keyboard.wait() + + + + diff --git a/token.ini b/token.ini deleted file mode 100644 index bbaa9d5..0000000 --- a/token.ini +++ /dev/null @@ -1,7 +0,0 @@ -[Token] -id = 00000000000000000000 -expiretime = 0000000000 -accesskeyid = 00000000000000 -accesskeysecret = 000000000000000000000000000 -appkey = 000000000000000000000 - diff --git a/安装指南/alibabacloud-nls-python-sdk/dist/alibabacloud-nls-java-sdk-2.0.0.tar.gz b/安装指南/alibabacloud-nls-python-sdk/dist/alibabacloud-nls-java-sdk-2.0.0.tar.gz new file mode 100644 index 0000000..db98e8c Binary files /dev/null and b/安装指南/alibabacloud-nls-python-sdk/dist/alibabacloud-nls-java-sdk-2.0.0.tar.gz differ diff --git a/安装指南/alibabacloud-nls-python-sdk/dist/alibabacloud_nls_java_sdk-2.0.0-py3.7.egg b/安装指南/alibabacloud-nls-python-sdk/dist/alibabacloud_nls_java_sdk-2.0.0-py3.7.egg deleted file mode 100644 index 8e203ec..0000000 Binary files a/安装指南/alibabacloud-nls-python-sdk/dist/alibabacloud_nls_java_sdk-2.0.0-py3.7.egg and /dev/null differ diff --git a/安装指南/requirements.txt b/安装指南/requirements.txt index 71cf397..6a96bb8 100644 --- a/安装指南/requirements.txt +++ b/安装指南/requirements.txt @@ -2,4 +2,4 @@ setuptools pyaudio keyboard aliyunsdkcore -configparser \ No newline at end of file +PySide2 \ No newline at end of file diff --git a/安装指南/安装指南.md b/安装指南/安装指南.md index 3ca4b61..1532885 100644 --- a/安装指南/安装指南.md +++ b/安装指南/安装指南.md @@ -32,17 +32,4 @@ pip install PyAudio‑0.2.11‑cp37‑cp37m‑win_amd64.whl - 分类:通用 - 场景:中文普通话 或 其它语言(想识别哪个语言就用哪个) -发布上线,再记下这个项目的 **appkey** - -### 填写 API - -用文本编辑器打开项目主目录的 `run.py` 编辑,可以看到: - -```python -""" 在这里填写你的 API 设置 """ -accessID = "xxxxxxxxxxxxxxxxxxxxxxxx" -accessKey = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" -appkey = 'xxxxxxxxxxxxxxxx' -``` - -将你在阿里云的 **accessID**、**accessKey**、**appkey** 分别填入,保存。 \ No newline at end of file +发布上线,再记下这个项目的 **appkey** diff --git a/打包/Pyinstaller 编译和打包 Win64.bat b/打包/Pyinstaller 编译和打包 Win64.bat new file mode 100644 index 0000000..cf2180a --- /dev/null +++ b/打包/Pyinstaller 编译和打包 Win64.bat @@ -0,0 +1,17 @@ +rmdir /s /q .\dist\CapsWriter + +pyinstaller --hidden-import sqlite3 --noconfirm -i "../src/misc/icon.ico" "../src/__init__.pyw" + +::pyinstaller --hidden-import sqlite3 --hidden-import PySide2.QtSql --noconfirm -i "../src/misc/icon.ico" "../src/__init__.py" + +echo d | xcopy /y /s .\dist\rely .\dist\__init__ + +ren .\dist\__init__\__init__.exe "_CapsWriter语音输入工具.exe" + +move .\dist\__init__ .\dist\CapsWriter + +del /F /Q CapsWriter_Win64.7z + +7z a -t7z CapsWriter_Win64.7z .\dist\CapsWriter -mx=9 -ms=200m -mf -mhc -mhcf -mmt -r + +pause \ No newline at end of file