diff --git a/AiMa.ico b/AiMa.ico new file mode 100644 index 0000000..5b55f2c Binary files /dev/null and b/AiMa.ico differ diff --git a/assets/FengGuo-QC.jpg b/assets/FengGuo-QC.jpg new file mode 100644 index 0000000..22fd5cb Binary files /dev/null and b/assets/FengGuo-QC.jpg differ diff --git a/assets/GUI.png b/assets/GUI.png new file mode 100644 index 0000000..697b399 Binary files /dev/null and b/assets/GUI.png differ diff --git a/config.py b/config.py index 96d7a65..d65eee4 100644 --- a/config.py +++ b/config.py @@ -1,67 +1,104 @@ -from collections.abc import Iterable -from pathlib import Path - - -# 服务端配置 -class ServerConfig: - addr = '0.0.0.0' - port = '6016' - - format_num = True # 输出时是否将中文数字转为阿拉伯数字 - format_punc = True # 输出时是否启用标点符号引擎 - format_spell = True # 输出时是否调整中英之间的空格 - - -# 客户端配置 -class ClientConfig: - addr = '127.0.0.1' # Server 地址 - port = '6016' # Server 端口 - - shortcut = 'caps lock' # 控制录音的快捷键,默认是 CapsLock - hold_mode = True # 长按模式,按下录音,松开停止,像对讲机一样用。 - # 改为 False,则关闭长按模式,也就是单击模式 - # 即:单击录音,再次单击停止 - # 且:长按会执行原本的单击功能 - suppress = False # 是否阻塞按键事件(让其它程序收不到这个按键消息) - restore_key = True # 录音完成,松开按键后,是否自动再按一遍,以恢复 CapsLock 或 Shift 等按键之前的状态 - threshold = 0.3 # 按下快捷键后,触发语音识别的时间阈值 - paste = True # 是否以写入剪切板然后模拟 Ctrl-V 粘贴的方式输出结果 - restore_clip = True # 模拟粘贴后是否恢复剪贴板 - - save_audio = True # 是否保存录音文件 - audio_name_len = 20 # 将录音识别结果的前多少个字存储到录音文件名中,建议不要超过200 - - trash_punc = ',。,.' # 识别结果要消除的末尾标点 - - hot_zh = True # 是否启用中文热词替换,中文热词存储在 hot_zh.txt 文件里 - 多音字 = True # True 表示多音字匹配 - 声调 = False # False 表示忽略声调区别,这样「黄章」就能匹配「慌张」 - - hot_en = True # 是否启用英文热词替换,英文热词存储在 hot_en.txt 文件里 - hot_rule = True # 是否启用自定义规则替换,自定义规则存储在 hot_rule.txt 文件里 - hot_kwd = True # 是否启用关键词日记功能,自定义关键词存储在 keyword.txt 文件里 - - mic_seg_duration = 15 # 麦克风听写时分段长度:15秒 - mic_seg_overlap = 2 # 麦克风听写时分段重叠:2秒 - - file_seg_duration = 25 # 转录文件时分段长度 - file_seg_overlap = 2 # 转录文件时分段重叠 - - -class ModelPaths: - model_dir = Path() / 'models' - paraformer_path = Path() / 'models' / 'paraformer-offline-zh' / 'model.int8.onnx' - tokens_path = Path() / 'models' / 'paraformer-offline-zh' / 'tokens.txt' - punc_model_dir = Path() / 'models' / 'punc_ct-transformer_cn-en' - - -class ParaformerArgs: - paraformer = f'{ModelPaths.paraformer_path}' - tokens = f'{ModelPaths.tokens_path}' - num_threads = 6 - sample_rate = 16000 - feature_dim = 80 - decoding_method = 'greedy_search' - debug = False - - +from collections.abc import Iterable +from pathlib import Path +import os + + +# 服务端配置 +class ServerConfig: + addr = '0.0.0.0' + port = '6789' + + format_num = True # 输出时是否将中文数字转为阿拉伯数字 + format_punc = True # 输出时是否启用标点符号引擎 + format_spell = True # 输出时是否调整中英之间的空格 + +# 大语言模型配置 +class ModelConfig: + #通义千问 + base_url="******" + api_key="*****" + model = "qwen-long" + +class MemoryConfig: + # 对话记忆窗口大小 + memory_size = 8 + + # 大语言模型记忆提示词 + prompts = [ + { + "input": """你的名字叫斑狗(Bango),是从2050年穿越过来的打工人助手.你不仅风趣幽默,而且知识渊博,你的工作就是辅助你的主人完成工作。作为主人的全能助手,你能够准确回答主人的问题并给予回应。请保持你的回复简洁明了。""", + "output": "明白了,主人!今天斑狗能帮你什么吗?😊" + }, + { + "input": """指令:1+1=? \n 引用资料:""", + "output": "1+1=2" + }, + { + "input": """指令:翻译成英文 \n 引用资料:我是一个优秀的打工助手,能完美的完成主人分配的任务。""", + "output": "I am an excellent working assistant, capable of perfectly completing the tasks assigned by my master." + }, + { + "input": """指令:翻译成中文 \n 引用资料:I am an excellent working assistant, capable of perfectly completing the tasks assigned by my master.""", + "output": "我是一个优秀的打工助手,能完美的完成主人分配的任务。😊" + }, + { + "input": """指令:继续 \n 引用资料:""", + "output": "为了您的工作更加效率,尽管吩咐我吧" + }, + ] + +# 客户端配置 +class ClientConfig: + addr = '127.0.0.1' # Server 地址 + port = '6789' # Server 端口 + + shortcut = 'caps lock' # 控制录音的快捷键,默认是 CapsLock + hold_mode = True # 长按模式,按下录音,松开停止,像对讲机一样用。 + # 改为 False,则关闭长按模式,也就是单击模式 + # 即:单击录音,再次单击停止 + # 且:长按会执行原本的单击功能 + suppress = False # 是否阻塞按键事件(让其它程序收不到这个按键消息) + restore_key = True # 录音完成,松开按键后,是否自动再按一遍,以恢复 CapsLock 或 Shift 等按键之前的状态 + threshold = 0.3 # 按下快捷键后,触发语音识别的时间阈值 + paste = True # 是否以写入剪切板然后模拟 Ctrl-V 粘贴的方式输出结果 + restore_clip = True # 模拟粘贴后是否恢复剪贴板 + + + save_audio = True # 是否保存录音文件 + audio_name_len = 10 # 将录音识别结果的前多少个字存储到录音文件名中,建议不要超过200 + save_md = True # 是否保存到MD文件 + + trash_punc = ',。,.' # 识别结果要消除的末尾标点 + + + hot_zh = True # 是否启用中文热词替换,中文热词存储在 hot_zh.txt 文件里 + 多音字 = True # True 表示多音字匹配 + 声调 = True # False 表示忽略声调区别,这样「黄章」就能匹配「慌张」 + + hot_en = True # 是否启用英文热词替换,英文热词存储在 hot_en.txt 文件里 + hot_rule = True # 是否启用自定义规则替换,自定义规则存储在 hot_rule.txt 文件里 + hot_kwd = True # 是否启用关键词日记功能,自定义关键词存储在 keyword.txt 文件里 + + mic_seg_duration = 15 # 麦克风听写时分段长度:15秒 + mic_seg_overlap = 2 # 麦克风听写时分段重叠:2秒 + + file_seg_duration = 25 # 转录文件时分段长度 + file_seg_overlap = 2 # 转录文件时分段重叠 + + + +class ModelPaths: + model_dir = Path() / 'models' + paraformer_path = Path() / 'models' / 'paraformer-offline-zh' / 'model.int8.onnx' + tokens_path = Path() / 'models' / 'paraformer-offline-zh' / 'tokens.txt' + punc_model_dir = Path() / 'models' / 'punc_ct-transformer_cn-en' + + +class ParaformerArgs: + paraformer = f'{ModelPaths.paraformer_path}' + tokens = f'{ModelPaths.tokens_path}' + num_threads = 6 + sample_rate = 16000 + feature_dim = 80 + decoding_method = 'greedy_search' + debug = False diff --git a/core_client.py b/core_client.py index 7c2833d..462654a 100644 --- a/core_client.py +++ b/core_client.py @@ -1,120 +1,125 @@ -# coding: utf-8 - -import os -import sys -import asyncio -import signal -from pathlib import Path -from platform import system -from typing import List - - - -import typer -import colorama -import keyboard - -from config import ClientConfig as Config -from util.client_cosmic import console, Cosmic -from util.client_stream import stream_open, stream_close -from util.client_shortcut_handler import bond_shortcut -from util.client_recv_result import recv_result -from util.client_show_tips import show_mic_tips, show_file_tips -from util.client_hot_update import update_hot_all, observe_hot - -from util.client_transcribe import transcribe_check, transcribe_send, transcribe_recv -from util.client_adjust_srt import adjust_srt - -from util.empty_working_set import empty_current_working_set - -# 确保根目录位置正确,用相对路径加载模型 -BASE_DIR = os.path.dirname(__file__); os.chdir(BASE_DIR) - -# 确保终端能使用 ANSI 控制字符 -colorama.init() - -# MacOS 的权限设置 -if system() == 'Darwin' and not sys.argv[1:]: - if os.getuid() != 0: - print('在 MacOS 上需要以管理员启动客户端才能监听键盘活动,请 sudo 启动') - input('按回车退出'); sys.exit() - else: - os.umask(0o000) - - -async def main_mic(): - Cosmic.loop = asyncio.get_event_loop() - Cosmic.queue_in = asyncio.Queue() - Cosmic.queue_out = asyncio.Queue() - - show_mic_tips() - - # 更新热词 - update_hot_all() - - # 实时更新热词 - observer = observe_hot() - - # 打开音频流 - Cosmic.stream = stream_open() - - # Ctrl-C 关闭音频流,触发自动重启 - signal.signal(signal.SIGINT, stream_close) - - # 绑定按键 - bond_shortcut() - - # 清空物理内存工作集 - if system() == 'Windows': - empty_current_working_set() - - # 接收结果 - while True: - await recv_result() - - -async def main_file(files: List[Path]): - show_file_tips() - - for file in files: - if file.suffix in ['.txt', '.json', 'srt']: - adjust_srt(file) - else: - await transcribe_check(file) - await asyncio.gather( - transcribe_send(file), - transcribe_recv(file) - ) - - if Cosmic.websocket: - await Cosmic.websocket.close() - input('\n按回车退出\n') - - -def init_mic(): - try: - asyncio.run(main_mic()) - except KeyboardInterrupt: - console.print(f'再见!') - finally: - print('...') - - -def init_file(files: List[Path]): - """ - 用 CapsWriter Server 转录音视频文件,生成 srt 字幕 - """ - try: - asyncio.run(main_file(files)) - except KeyboardInterrupt: - console.print(f'再见!') - sys.exit() - - -if __name__ == "__main__": - # 如果参数传入文件,那就转录文件 - # 如果没有多余参数,就从麦克风输入 - if sys.argv[1:]: - typer.run(init_file) - else: - init_mic() +# coding: utf-8 + +import os +import sys +import asyncio +import signal +from pathlib import Path +from platform import system +from typing import List +import time + + +import typer +import colorama +import keyboard +from config import ClientConfig as Config +from util.client_cosmic import console, Cosmic +from util.client_stream import stream_open, stream_close +from util.client_shortcut_handler import bond_shortcut +from util.client_recv_result import recv_result +from util.client_show_tips import show_mic_tips, show_file_tips +from util.client_hot_update import update_hot_all, observe_hot +from util.client_transcribe import transcribe_check, transcribe_send, transcribe_recv +from util.client_adjust_srt import adjust_srt +from util.empty_working_set import empty_current_working_set + +# 确保根目录位置正确,用相对路径加载模型 +BASE_DIR = os.path.dirname(__file__); os.chdir(BASE_DIR) + +# 确保终端能使用 ANSI 控制字符 +colorama.init() + +# MacOS 的权限设置 +if system() == 'Darwin' and not sys.argv[1:]: + if os.getuid() != 0: + print('在 MacOS 上需要以管理员启动客户端才能监听键盘活动,请 sudo 启动') + input('按回车退出'); sys.exit() + else: + os.umask(0o000) + + +async def main_mic(): + Cosmic.loop = asyncio.get_event_loop() + Cosmic.queue_in = asyncio.Queue() + Cosmic.queue_out = asyncio.Queue() + + show_mic_tips() + + # 更新热词 + update_hot_all() + + # 实时更新热词 + observer = observe_hot() + + # 打开音频流 + Cosmic.stream = stream_open() + + # # Ctrl-C 关闭音频流,触发自动重启 + # signal.signal(signal.SIGINT, stream_close) + + # 绑定按键 + bond_shortcut() + + # 清空物理内存工作集 + if system() == 'Windows': + empty_current_working_set() + + # 接收结果 + while True: + await recv_result() + + +async def main_file(files: List[Path]): + show_file_tips() + + for file in files: + if file.suffix in ['.txt', '.json', 'srt']: + adjust_srt(file) + else: + await transcribe_check(file) + await asyncio.gather( + transcribe_send(file), + transcribe_recv(file) + ) + + if Cosmic.websocket: + await Cosmic.websocket.close() + input('\n按回车退出\n') + +from util.server_client_state import client_is_running + + +def init_mic(client_is_running): + try: + #print(client_is_running.value) + if client_is_running.value: + asyncio.run(main_mic()) + else: + console.print(f'客户端已经关闭') + sys.exit() + except KeyboardInterrupt: + console.print(f'再见!') + finally: + print('...') + + + +def init_file(files: List[Path]): + """ + 用 CapsWriter Server 转录音视频文件,生成 srt 字幕 + """ + try: + asyncio.run(main_file(files)) + except KeyboardInterrupt: + console.print(f'再见!') + sys.exit() + + +if __name__ == "__main__": + # 如果参数传入文件,那就转录文件 + # 如果没有多余参数,就从麦克风输入 + if sys.argv[1:]: + typer.run(init_file) + else: + init_mic(client_is_running) diff --git a/core_server.py b/core_server.py index c124ca7..194ddd0 100644 --- a/core_server.py +++ b/core_server.py @@ -1,75 +1,75 @@ -import os -import sys -import asyncio -from multiprocessing import Process, Manager -from platform import system - -import websockets -from config import ServerConfig as Config -from util.server_cosmic import Cosmic, console -from util.server_check_model import check_model -from util.server_ws_recv import ws_recv -from util.server_ws_send import ws_send -from util.server_init_recognizer import init_recognizer -from util.empty_working_set import empty_current_working_set - -BASE_DIR = os.path.dirname(__file__); os.chdir(BASE_DIR) # 确保 os.getcwd() 位置正确,用相对路径加载模型 - -async def main(): - - # 检查模型文件 - check_model() - - console.line(2) - console.rule('[bold #d55252]CapsWriter Offline Server'); console.line() - console.print(f'项目地址:[cyan underline]https://github.com/HaujetZhao/CapsWriter-Offline', end='\n\n') - console.print(f'当前基文件夹:[cyan underline]{BASE_DIR}', end='\n\n') - console.print(f'绑定的服务地址:[cyan underline]{Config.addr}:{Config.port}', end='\n\n') - - # 跨进程列表,用于保存 socket 的 id,用于让识别进程查看连接是否中断 - Cosmic.sockets_id = Manager().list() - - # 负责识别的子进程 - recognize_process = Process(target=init_recognizer, - args=(Cosmic.queue_in, - Cosmic.queue_out, - Cosmic.sockets_id), - daemon=True) - recognize_process.start() - Cosmic.queue_out.get() - console.rule('[green3]开始服务') - console.line() - - # 清空物理内存工作集 - if system() == 'Windows': - empty_current_working_set() - - # 负责接收客户端数据的 coroutine - recv = websockets.serve(ws_recv, - Config.addr, - Config.port, - subprotocols=["binary"], - max_size=None) - - # 负责发送结果的 coroutine - send = ws_send() - await asyncio.gather(recv, send) - - -def init(): - try: - asyncio.run(main()) - except KeyboardInterrupt: # Ctrl-C 停止 - console.print('\n再见!') - except OSError as e: # 端口占用 - console.print(f'出错了:{e}', style='bright_red'); console.input('...') - except Exception as e: - print(e) - finally: - Cosmic.queue_out.put(None) - sys.exit(0) - # os._exit(0) - - -if __name__ == "__main__": - init() +import os +import sys +import asyncio +from multiprocessing import Process, Manager +from platform import system + +import websockets +from config import ServerConfig as Config +from util.server_cosmic import Cosmic, console +from util.server_check_model import check_model +from util.server_ws_recv import ws_recv +from util.server_ws_send import ws_send +from util.server_init_recognizer import init_recognizer +from util.empty_working_set import empty_current_working_set + +BASE_DIR = os.path.dirname(__file__); os.chdir(BASE_DIR) # 确保 os.getcwd() 位置正确,用相对路径加载模型 + +async def main(): + + # 检查模型文件 + check_model() + + console.line(2) + console.rule('[bold #d55252]CapsWriter Offline Server'); console.line() + console.print(f'项目地址:[cyan underline]https://github.com/HaujetZhao/CapsWriter-Offline', end='\n\n') + console.print(f'当前基文件夹:[cyan underline]{BASE_DIR}', end='\n\n') + console.print(f'绑定的服务地址:[cyan underline]{Config.addr}:{Config.port}', end='\n\n') + + # 跨进程列表,用于保存 socket 的 id,用于让识别进程查看连接是否中断 + Cosmic.sockets_id = Manager().list() + + # 负责识别的子进程 + recognize_process = Process(target=init_recognizer, + args=(Cosmic.queue_in, + Cosmic.queue_out, + Cosmic.sockets_id), + daemon=True) + recognize_process.start() + Cosmic.queue_out.get() + console.rule('[green3]开始服务') + console.line() + + # 清空物理内存工作集 + if system() == 'Windows': + empty_current_working_set() + + # 负责接收客户端数据的 coroutine + recv = websockets.serve(ws_recv, + Config.addr, + Config.port, + subprotocols=["binary"], + max_size=None) + + # 负责发送结果的 coroutine + send = ws_send() + await asyncio.gather(recv, send) + + +def init(): + try: + asyncio.run(main()) + except KeyboardInterrupt: # Ctrl-C 停止 + console.print('\n再见!') + except OSError as e: # 端口占用 + console.print(f'出错了:{e}', style='bright_red'); console.input('...') + except Exception as e: + print(e) + finally: + Cosmic.queue_out.put(None) + sys.exit(0) + # os._exit(0) + + +if __name__ == "__main__": + init() diff --git a/hot-en.txt b/hot-en.txt index 70f14b5..277d3d0 100644 --- a/hot-en.txt +++ b/hot-en.txt @@ -1,272 +1,284 @@ -# 在此文件放置英文热词 -# Put English hot words here, one per line. Line starts with # will be ignored. - - -# 杂项 -IP -IO -PC -KB -MB -GB -TB -PB - - -PDF -DPI - -Apple -Apple Watch -Apple Vision -iPhone -iPad -iMac -MacBook -Pro -iCloud -Siri -Hey Siri - -Samsung - -Android -Google - -Micro-USB -Mini-USB -Type-C -USB -HDMI -SD -Readme -3M - - -SpaceX -StarShip -Falcon -Raptor -Merlin - -Transformer -Paraformer - -China -Chinese -England -English -America -American -Germany -German -Russia -Russian -Australia -Australian -French -Japan -Japanese -Korea -Korean - -# 数字开头 -7-Zip - -# A开头 -Adobe -After Effects -AI -Airbnb -Amazon -AMD -Apple -Audition -AutoCAD -App - - -# B开头 -Bandizip -BIOS -Blender - -# C开头 -CAD -CameraRAW -CD -CD-ROM -CEO -CFO -CIA -Cisco -Clash -CMD -CPU -Creative Cloud -Cubase -ChatGPT -CapsWriter - -# D开头 -Dell -Dolby -Dropbox -Directory OPUS - -# E开头 -Edge -EPA -Epic -EU -Evernote -Excel -EXIF - -# F开头 -Facebook -FBI -FCC -FDA -Figma -Final Cut Pro -Firefox -FTP - -# G开头 -GDP -GitHub -Google -Google Docs -Google Drive -Google Sheets -Google Slides -GPT -GUI -GPU - -# H开头 -HD - -# I开头 -IBM -Illustrator -IMF -InDesign -Instagram -Intel -iPhone - - -# J开头 -Java -JavaScript - -# K开头 -Kotlin -KPI - -# L开头 -Lightroom -LinkedIn -Linux -Logic Pro -Lua - -# M开头 -Maya -Microsoft -Markdown - -# N开头 -NASA -Netflix -Notepad -Notion -NSA -NVIDIA - -# O开头 -OBS -OCR -OneNote -OpenAI -Oracle -Outlook - -# P开头 -Photoshop -PHP -Pinterest -PowerPoint -PowerShell -PPT -Premiere Pro -Pro Tools -Python - -# Q开头 -QQ - -# R开头 -RAM -RAW -Reddit -RGB -Ruby -Rust - -# S开头 -Sketch -Skype -Slack -Snapchat -SolidWorks -SSD -Steam - -# T开头 -Teams -Telegram -Tencent -TikTok -Trello -TV -Twitter -TypeScript - -# U开头 -Uber -UEFI -UI -UNESCO -UNICEF -USB - -# V开头 -VIP -VPN -VR -VRChat - -# W开头 -WeChat -WhatsApp -WHO -WiFi -Windows -WinRAR -Wix -Word -WordPress -WTO - -# X开头 -Xbox -XD - -# Y开头 -YouTube - -# Z开头 -Zoom +# 在此文件放置英文热词 +# Put English hot words here, one per line. Line starts with # will be ignored. + + +# 杂项 +IP +IO +PC +KB +MB +GB +TB +PB +IPO + +PDF +DPI + +Apple +Apple Watch +Apple Vision +iPhone +iPad +iMac +MacBook +Pro +iCloud +Siri +Hey Siri + +Samsung + +Android +Google + +Micro-USB +Mini-USB +Type-C +USB +HDMI +SD +Readme +3M + + +SpaceX +StarShip +Falcon +Raptor +Merlin + +Transformer +Paraformer + + +CrewAI +China +Chinese +England +English +America +American +Germany +German +Russia +Russian +Australia +Australian +French +Japan +Japanese +Korea +Korean +AIGC + +# 数字开头 +7-Zip + +# A开头 +Adobe +After Effects +AI +Airbnb +Amazon +AMD +Apple +Audition +AutoCAD +App + + +# B开头 +Bandizip +BIOS +Blender +BIM + +# C开头 +CAD +CameraRAW +CD +CD-ROM +CEO +CFO +CIA +Cisco +Clash +CMD +CPU +Creative Cloud +Cubase +ChatGPT +CapsWriter + +# D开头 +Dell +Dolby +Dropbox +Directory OPUS +Docker + +# E开头 +Edge +EPA +Epic +EU +Evernote +Excel +EXIF + +# F开头 +Facebook +FBI +FCC +FDA +Figma +Final Cut Pro +Firefox +FTP + +# G开头 +GDP +GitHub +Google +Google Docs +Google Drive +Google Sheets +Google Slides +GPT +GUI +GPU + +# H开头 +HD + +# I开头 +IBM +Illustrator +IMF +InDesign +Instagram +Intel +iPhone + + +# J开头 +Java +JavaScript +JSD + +# K开头 +Kotlin +KPI + +# L开头 +Lightroom +LinkedIn +Linux +Logic Pro +Lua +LangChain + + + +# M开头 +Maya +Microsoft +Markdown + +# N开头 +NASA +Netflix +Notepad +Notion +NSA +NVIDIA + +# O开头 +OBS +OCR +OneNote +OpenAI +Oracle +Outlook + +# P开头 +Photoshop +PHP +Pinterest +PowerPoint +PowerShell +PPT +Premiere Pro +Pro Tools +Python + +# Q开头 +QQ + +# R开头 +RAM +RAW +Reddit +RGB +Ruby +Rust +Revit +Rhino + +# S开头 +Sketch +Skype +Slack +Snapchat +SolidWorks +SSD +Steam + +# T开头 +Teams +Telegram +Tencent +TikTok +Trello +TV +Twitter +TypeScript + +# U开头 +Uber +UEFI +UI +UNESCO +UNICEF +USB + +# V开头 +VIP +VPN +VR +VRChat + +# W开头 +WeChat +WhatsApp +WHO +WiFi +Windows +WinRAR +Wix +Word +WordPress +WTO +WPS + +# X开头 +Xbox +XD + +# Y开头 +YouTube + +# Z开头 +Zoom diff --git a/hot-rule.txt b/hot-rule.txt index 99a102a..a5b5683 100644 --- a/hot-rule.txt +++ b/hot-rule.txt @@ -1,13 +1,18 @@ -# 在此文件放置自定义规则,每行一条正则表达式, -# 左边是查找模式,右边是替换式,中间用带空格的等号分开 -# 以 # 开头的会被忽略,将查找和匹配用等号隔开,文本两边的空格会被省略。例如: - -毫安时 = mAh -赫兹 = Hz -伏特 = V -二、 = 二 -负一 = -1 -(艾特)\s*(QQ)\s*点\s* = @qq. -(艾特)\s*([一幺]六三)\s*点\s* = @163. -(艾特)\s*(\w+)\s*(点)\s*(\w+) = @\2.\4 -\s*点\s*(\w+)$ = .\1 +# 在此文件放置自定义规则,每行一条正则表达式, +# 左边是查找模式,右边是替换式,中间用带空格的等号分开 +# 以 # 开头的会被忽略,将查找和匹配用等号隔开,文本两边的空格会被省略。例如: + +毫安时 = mAh +赫兹 = Hz +伏特 = V +二、 = 二 +负一 = -1 +负二 = -2 +(艾特)\s*(QQ)\s*点\s* = @qq. +(艾特)\s*([一幺]六三)\s*点\s* = @163. +(艾特)\s*(\w+)\s*(点)\s*(\w+) = @\2.\4 +直径符号=Φ + + +紧身 = 景森 +紧身设计 = 景森设计 \ No newline at end of file diff --git a/hot-zh.txt b/hot-zh.txt index 3ab98ec..1723840 100644 --- a/hot-zh.txt +++ b/hot-zh.txt @@ -1,3 +1,23 @@ -# 在此文件放置中文热词,每行一个,开头带井号表示注释,会被省略 - -我家鸽鸽 \ No newline at end of file +# 在此文件放置中文热词,每行一个,开头带井号表示注释,会被省略 + + + +我家鸽鸽 + + +马拉松 +越野赛 +云核心 +戈赛 +戈壁 +凤凰山 +白云山 + + +景森设计 +幕墙 +陈田 +孪生 +平台 +数字孪生 +檩条 \ No newline at end of file diff --git a/keywords.txt b/keywords.txt index 8d93445..c9e8800 100644 --- a/keywords.txt +++ b/keywords.txt @@ -1,5 +1,5 @@ -# 在此文件放置日记关键词,每行一个,开头带井号表示注释,会被省略 -# 当识别结果以关键词开头时,会被记录到 「年份/月份/关键词-日期.md」文件中 -重要 -健康 +# 在此文件放置日记关键词,每行一个,开头带井号表示注释,会被省略 +# 当识别结果以关键词开头时,会被记录到 「年份/月份/关键词-日期.md」文件中 +重要 +健康 学习 \ No newline at end of file diff --git a/readme.md b/readme.md index af0d088..0fb9aa4 100644 --- a/readme.md +++ b/readme.md @@ -9,7 +9,16 @@ 1. 按下键盘上的 `大写锁定键`,录音开始,当松开 `大写锁定键` 时,就会识别你的录音,并将识别结果立刻输入 2. 将音视频文件拖动到客户端打开,即可转录生成 srt 字幕 -视频教程:[CapsWriter-Offline 电脑端离线语音输入工具](https://www.bilibili.com/video/BV1tt4y1d75s/) +视频教程:[CapsWriter-Offline 电脑端离线语音输入工具](https://www.bilibili.com/video/BV1fo4y1T7KN/) + +## 更新的GUI版本,添加LLM应用 +![GUI](https://github.com/Walkman1W/CapsWriter-Offline/assets/48667439/b6832daa-9564-4c51-9624-3a46a44487d0) + +添加了GUI界面和LLM应用: +1. GUI界面,设置了热词、配置的按钮。 +2. 一个纯翻译Agent,选择目标语言即可把语音转的文字翻译成另一种语言。 +3. 一个全能Agent,可以根据用户的语音指令以及取鼠标所选内容,完成任务。可在`config.py`中设置记轮数。 + ## 特性 @@ -20,16 +29,19 @@ 5. 转录功能:将音视频文件拖动到客户端打开,即可转录生成 srt 字幕 6. 服务端、客户端分离,可以服务多台客户端 7. 编辑 `config.py` ,可以配置服务端地址、快捷键、录音开关…… +8. 添加了GUI界面,设置了热词、配置的按钮。 +9. 添加了LLM应用,在`config.py`中可以配置LLM入口。 +10. 一个纯翻译Agent,选择目标语言即可把语音转的文字翻译成另一种语言。 +11. 一个全能Agent,可以根据用户的语音指令以及取鼠标所选内容,完成任务。可在`config.py`中设置记轮数。 ## 懒人包 对 Windows 端: -1. 请确保电脑上安装了 [Microsoft Visual C++ Redistributable 运行库](https://learn.microsoft.com/zh-cn/cpp/windows/latest-supported-vc-redist) -2. 服务端载入模型所用的 onnxruntime 只能在 Windows 10 及以上版本的系统使用 -3. 服务端载入模型需要系统内存 4G,只能在 64 位系统上使用 -4. 额外打包了 32 位系统可用的客户端,在 Windows 7 及以上版本的系统可用 -5. 模型文件较大,单独打包,解压模型后请放入软件目录的 `models` 文件夹中 +1. 服务端载入模型所用的 onnxruntime 只能在 Windows 10 及以上版本的系统使用 +2. 服务端载入模型需要系统内存 4G,只能在 64 位系统上使用 +3. 额外打包了 32 位系统可用的客户端,在 Windows 7 及以上版本的系统可用 +4. 模型文件较大,单独打包,解压模型后请放入软件目录的 `models` 文件夹中 其它系统: @@ -133,20 +145,7 @@ **模型文件太大,并没有包含在 GitHub 库里面,你可以从百度网盘或者 GitHub Releases 界面下载已经转换好的模型文件,解压后,将 `models` 文件夹放到软件根目录** -## 自启动、隐藏窗口、拖盘图标、Docker - -Windows 隐藏黑窗口启动,见 [\#49](https://github.com/HaujetZhao/CapsWriter-Offline/issues/49),将下述内容保存为 vbs 运行: - -``` -CreateObject("Wscript.Shell").Run "start_server.exe",0,True -CreateObject("Wscript.Shell").Run "start_client.exe",0,True -``` - -Windows 自启动,新建快捷方式,放到 `shell:startup` 目录下即可。 - -带拖盘图标的 GUI 版,见 [H1DDENADM1N/CapsWriter-Offline](https://github.com/H1DDENADM1N/CapsWriter-Offline/tree/GUI-(PySide6)-and-Portable-(PyStand)) -Docker 版,见 [Garonix/CapsWriter-Offline at docker-support ](https://github.com/Garonix/CapsWriter-Offline/tree/docker-support) ## 源码安装依赖 @@ -209,4 +208,9 @@ Windows/MacOS/Linux均使用如下命令完成打包: 如果你愿意,可以以打赏的方式支持我一下: -![sponsor](assets/sponsor.jpg) \ No newline at end of file +![sponsor](assets/sponsor.jpg) + +## 此版本作者 +![FengGuo-QC](https://github.com/Walkman1W/CapsWriter-Offline/assets/48667439/f2dff3a8-a89c-4a36-9021-1b4b4ee87dd7) + + diff --git a/start_client.py b/start_client.py index f049d33..086dec4 100644 --- a/start_client.py +++ b/start_client.py @@ -7,7 +7,7 @@ import sys import typer -from core_client import init_file, init_mic +from core_client import init_file, init_mic,client_is_running if __name__ == "__main__": # 如果参数传入文件,那就转录文件 @@ -15,4 +15,4 @@ if sys.argv[1:]: typer.run(init_file) else: - init_mic() \ No newline at end of file + init_mic(client_is_running) \ No newline at end of file diff --git a/start_client_gui.py b/start_client_gui.py new file mode 100644 index 0000000..2bb689c --- /dev/null +++ b/start_client_gui.py @@ -0,0 +1,204 @@ +# coding: utf-8 +""" +这个文件仅仅是为了 PyInstaller 打包用 +""" + +import sys +import typer +from core_client import init_mic, init_file +import threading +import multiprocessing +import os +import PySimpleGUI as sg +import threading +from util.server_client_state import uic +from util.server_client_state import client_is_running, server_is_running +from util.client_cosmic import Cosmic, console + + + +# 获取程序的根目录,用于打开文件和加载模型 +BASE_DIR = os.path.dirname(__file__); os.chdir(BASE_DIR) + + +def open_file(file): + # 获取用户的主目录 + home_dir = os.path.expanduser('~') + # 在用户的主目录中查找文件 + file_path = os.path.join(home_dir, file) + extension = os.path.splitext(file_path)[1] + if extension == '.txt': + os.system(f"notepad {file_path}") + elif extension == '.json': + os.system(f"notepad {file_path}") + elif extension == '.srt': + os.system(f"start {file_path}") # 使用默认的程序打开 .srt 文件 + elif extension == '.py': + os.system(f"notepad {file_path}") # 使用 Notepad 打开 .py 文件 + else: + os.system(f"start {file_path}") # 对于其他类型的文件,使用默认的程序打开 + + +# 创建一个共享变量,用于控制客户端程序是否运行 +client_is_running = multiprocessing.Value('b', True) +# 创建一个共享变量,用于控制客户端程序是否运行 +gui_is_running = multiprocessing.Value('b', True) + + + +# 创建GUI窗口 +def create_gui(gui_is_running): + # 创建GUI线程 + gui_thread = threading.Thread(target=gui_program, args=(gui_is_running,)) + gui_thread.start() + return gui_thread + + +# 客户端程序 +def client_program(client_is_running): + #print(client_is_running.value) + while client_is_running.value: + init_mic(client_is_running)# 启动客户端程序 + # 如果客户端程序停止运行,就退出循环 + if not client_is_running.value: + break + # 如果客户端程序在运行,就继续运行 + else: + continue + + +# 创建GUI窗口 +def gui_program(gui_is_running): + + + languages = ['英文', '中文', '法文', '德文', '日文', '韩文','藏文'] # 添加你需要的语言 + + # 创建按钮 + layout = [ + #[sg.Text("连接中", key="-CONN_STATUS-", size=(6, 1),)], + [ + sg.Column( + [ + [ + sg.Radio('转文字', "RADIO1", default=True, key='-TOTEXT-', enable_events=True), + sg.Radio('翻译', "RADIO1", default=False, key='-TRANSLATE-', enable_events=True), + sg.Radio('助手', "RADIO1", default=False, key='-ASSISTANT-', enable_events=True), + ] + ], + justification='center' + ), + ], + + [ + sg.Column( + [ + [ + sg.Combo(languages, default_value=languages[0], key='-LANGUAGE-', enable_events=True, size=(6, 1), readonly=True, disabled=True), # 添加下拉框 + sg.Checkbox('保留原文', default=True, key='-KEEP_ORIGINAL-',enable_events=True,disabled=True) # 是否保留原文 + ] + ], + justification='center' + ), + ], + + [ + sg.Column( + [ + [ + sg.Button("中\n文\n热\n词", key="-CN_HOT_WORDS-", size=(3, 6), button_color=("black", "white")), + sg.Button("英\n文\n热\n词", key="-EN_HOT_WORDS-", size=(3, 6), button_color=("black", "white")), + sg.Button("关\n 键 \n字", key="-KEYWORDS-", size=(3, 6), button_color=("black", "white")), + sg.Button("定\n义\n规\n则", key="-DEFRULE-", size=(3, 6), button_color=("black", "white")), + sg.Button("配\n置\n文\n件", key="-CONFIG-", size=(3, 6), button_color=("black", "white")) + ] + ], + justification='center' + ) + ] + ] + + # 创建窗口 + # 获取屏幕大小 + screen_width, screen_height = sg.Window.get_screen_size() + + # 计算窗口的初始位置 + x = screen_width - 280 - 30 # 窗口宽度 + y = screen_height - 150 - 90 # 窗口高度,减去任务栏的高度 + + window = sg.Window("客户端配置", layout, finalize=True, alpha_channel=0.6, size=(300, 170), location=(x, y), icon='.\\AiMa.ico') + + # 事件循环 + while True: + event, values = window.read() + if event == sg.WIN_CLOSED: + client_is_running.value = False + break + elif event == '-KEEP_ORIGINAL-': # 当 "保留原文" 选项被切换时 + uic.keep_original = values['-KEEP_ORIGINAL-'] + if uic.keep_original: + console.print('保留原文开启') + else: + console.print('保留原文关闭') + + elif event in ("-TOTEXT-", "-ASSISTANT-", "-TRANSLATE-"): + uic.totext_is_running = values["-TOTEXT-"] + uic.assistant_is_running = values["-ASSISTANT-"] + uic.translate_is_running = values["-TRANSLATE-"] + + if uic.totext_is_running: + console.print('转文字已经启动') + uic.assistant_is_running = False + uic.translate_is_running = False + window['-KEEP_ORIGINAL-'].update(disabled=True) # 禁用 "保留原文" 复选框 + window['-LANGUAGE-'].update(disabled=True) # 禁用 "语言选择" 复选框 + elif uic.assistant_is_running: + console.print('助手已经启动') + uic.totext_is_running = False + uic.translate_is_running = False + window['-KEEP_ORIGINAL-'].update(disabled=True) # 禁用 "保留原文" 复选框 + window['-LANGUAGE-'].update(disabled=True) # 禁用 "语言选择" 复选框 + elif uic.translate_is_running: + console.print('翻译已经启动') + uic.totext_is_running = False + uic.assistant_is_running = False + window['-KEEP_ORIGINAL-'].update(disabled=False) # 启用 "保留原文" 复选框 + window['-LANGUAGE-'].update(disabled=False) # 启用 "语言选择" 复选框 + + elif event == "-LANGUAGE-": + uic.target_language = values[event] + console.print(f"目标语言:{uic.target_language}") + elif event == "-CN_HOT_WORDS-": + open_file(os.path.join(BASE_DIR, "hot-zh.txt")) + elif event == "-EN_HOT_WORDS-": + open_file(os.path.join(BASE_DIR, "hot-en.txt")) + elif event == "-KEYWORDS-": + open_file(os.path.join(BASE_DIR, "keywords.txt")) + elif event == "-DEFRULE-": + open_file(os.path.join(BASE_DIR, "hot-rule.txt")) + elif event == "-CONFIG-": + open_file(os.path.join(BASE_DIR, "config.py")) + elif event == sg.WINDOW_CLOSED or event == "-EXIT-": + client_is_running.value = False + gui_is_running.value = False + print(gui_is_running.value) + print(client_is_running.value) + break + + window.close() + +if __name__ == "__main__": + # 如果参数传入文件,那就转录文件 + # 如果没有多余参数,就从麦克风输入 + if sys.argv[1:]: + typer.run(init_file) + else: + # 在主程序中启动GUI程序 + create_gui(gui_is_running) + # 在主程序中启动客户端程序 + client_program(client_is_running) + + + + + + \ No newline at end of file diff --git a/start_client_gui.spec b/start_client_gui.spec new file mode 100644 index 0000000..6e26756 --- /dev/null +++ b/start_client_gui.spec @@ -0,0 +1,59 @@ +# -*- mode: python ; coding: utf-8 -*- + +# 初始化空列表 +hiddenimports = [ + 'pydantic', + 'langchain.chains.conversation.base', + 'pystray._base', + 'pystray._win32', + 'pystray._xorg', + 'pyautogui._pyautogui_x11' +] + + +a = Analysis( + ['start_client_gui.py'], + pathex=[], + binaries=[], + datas=[('config.py', '.'), ('hot-en.txt', '.'), ('hot-zh.txt', '.'), ('hot-rule.txt', '.'), ('keywords.txt', '.')], + hiddenimports = hiddenimports, + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=['IPython', 'PIL', + 'PySide6', 'PySide2', 'PyQt5', + 'matplotlib', 'wx', + ], + noarchive=False, + optimize=0, +) +pyz = PYZ(a.pure) + +exe = EXE( + pyz, + a.scripts, + [], + exclude_binaries=True, + name='start_client_gui', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=False, + console=True, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, + icon=['assets\\AiMaS.ico'], + contents_directory='internal_client_gui', +) +coll = COLLECT( + exe, + a.binaries, + a.datas, + strip=False, + upx=False, + upx_exclude=[], + name='start_client_gui', +) diff --git a/start_server_client.spec b/start_server_client.spec new file mode 100644 index 0000000..918c664 --- /dev/null +++ b/start_server_client.spec @@ -0,0 +1,181 @@ +# -*- mode: python ; coding: utf-8 -*- +from PyInstaller.utils.hooks import collect_all +from os.path import join, basename, dirname, exists +from os import walk, makedirs +from shutil import copyfile, rmtree + +# 初始化空列表 +binaries = [] +hiddenimports = [ + 'pydantic', + 'langchain.chains.conversation.base', + 'pystray._base', + 'pystray._win32', + 'pystray._xorg', + 'pyautogui._pyautogui_x11', + 'pystray._darwin', + 'pyperclip', + 'pyautogui._pyautogui_osx', + 'keyboard._darwinkeyboard', + 'pyclip.macos_clip', + 'mouseinfo', + 'pygetwindow._pygetwindow_macos', + 'pyscreeze' +] +datas = [('config.py', '.'),] + +# 额外复制 dll +modules = ['onnxruntime'] +for module in modules: + tmp_ret = collect_all(module) + binaries += tmp_ret[1] + +a_1 = Analysis( + ['start_server.py'], + pathex=[], + binaries=binaries, + datas=datas, + hiddenimports=hiddenimports, + hookspath=[], + hooksconfig={}, + runtime_hooks=['build_hook.py'], + excludes=['IPython', 'PIL', + 'PySide6', 'PySide2', 'PyQt5', + 'matplotlib', 'wx', + 'funasr', 'pydantic', 'torch', + ], + noarchive=False, +) + +a_2 = Analysis( + ['start_client_gui.py'], + pathex=[], + binaries=binaries, + datas=datas, + hiddenimports=hiddenimports, + hookspath=[], + hooksconfig={}, + runtime_hooks=['build_hook.py'], + excludes=['IPython', 'PIL', + 'PySide6', 'PySide2', 'PyQt5', + 'matplotlib', 'wx', + ], + noarchive=False, +) + +# 排除不要打包的模块 +private_module = ['util', 'config', + 'core_server', + 'core_client', + 'hot-en', 'hot-zh', 'hot-rule', 'keywords', + 'start_client_gui', + ] +pure = a_1.pure.copy() +a_1.pure.clear() +for name, src, type in pure: + condition = [name == m or name.startswith(m + '.') for m in private_module] + if condition and any(condition): + ... + else: + a_1.pure.append((name, src, type)) # 把需要保留打包的 py 文件重新添加回 a.pure + +pure = a_2.pure.copy() +a_2.pure.clear() +for name, src, type in pure: + condition = [name == m or name.startswith(m + '.') for m in private_module] + if condition and any(condition): + ... + else: + a_2.pure.append((name, src, type)) # 把需要保留打包的 py 文件重新添加回 a.pure + +pyz_1 = PYZ(a_1.pure) +pyz_2 = PYZ(a_2.pure) + +exe_1 = EXE( + pyz_1, + a_1.scripts, + [], + exclude_binaries=True, + name='start_server', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=False, # 禁用 UPX + console=True, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, + icon=['assets\\AiMaS.ico'], + contents_directory='internal', +) +exe_2 = EXE( + pyz_2, + a_2.scripts, + [], + exclude_binaries=True, + name='start_client_gui', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=False, # 禁用 UPX + console=True, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, + icon=['assets\\AiMaC.ico'], + contents_directory='internal', +) + +coll = COLLECT( + exe_1, + a_1.binaries, + a_1.datas, + + exe_2, + a_2.binaries, + a_2.datas, + + strip=False, + upx=False, # 禁用 UPX + upx_exclude=[], + name='CapsWriter-Offline-v1.3.9', +) + +# 复制额外所需的文件 +my_files = ['config.py', + 'start_client_gui.py', + 'core_server.py', + 'core_client.py', + 'hot-en.txt', 'hot-zh.txt', 'hot-rule.txt', 'keywords.txt', + 'readme.md'] +my_folders = ['assets', 'util'] +dest_root = join('dist', basename(coll.name)) +for folder in my_folders: + for dirpath, dirnames, filenames in walk(folder): + for filename in filenames: + my_files.append(join(dirpath, filename)) +for file in my_files: + if not exists(file): + continue + dest_file = join(dest_root, file) + dest_folder = dirname(dest_file) + makedirs(dest_folder, exist_ok=True) + copyfile(file, dest_file) + +# 为 models 文件夹建立链接,免去复制大文件 +from platform import system +from subprocess import run +if system() == 'Windows': + link_folders = ['models', 'util'] + for folder in link_folders: + if not exists(folder): + continue + dest_folder = join(dest_root, folder) + if exists(dest_folder): + rmtree(dest_folder) + cmd = ['mklink', '/j', dest_folder, folder] + run(cmd, shell=True)