bash
import time
import threading
import os
import json
import socket
from http.server import HTTPServer, BaseHTTPRequestHandler
from io import BytesIO
from urllib.parse import urlparse
import webbrowser
import sys
import pystray
from PIL import Image, ImageDraw, ImageFont
from pynput import keyboard
from pynput.keyboard import Key, Controller as KeyController
# 全局配置(修改为F13键触发)
TRIGGER_KEY = Key.f13 # 触发快捷键:F13(替换原End键)
VOICE_HOTKEY = [Key.ctrl, Key.shift, 'h'] # 语音键:Ctrl+Shift+H
click_count = 0 # 记录点击次数
last_click_time = 0.0 # 防抖时间(0.25秒)
is_running = False # 程序运行状态
key_listener = None # 键盘监听器
keyboard_ctrl = KeyController() # 键盘模拟控制器
tray_icon = None # 系统托盘图标
ICON_FILE_NAME = "app_icon.ico" # 图标文件名
web_server = None # Web 服务实例
server_port = 8080 # 本地 Web 服务端口
status_info = "未启动,F13触发语音输入" # 状态信息更新为F13
server_thread = None # Web服务线程
# ====================== 端口检测+兼容exe的Web服务 ======================
# 检测端口是否被占用,自动换端口
def get_available_port(start_port):
port = start_port
while port < start_port + 100:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
if s.connect_ex(('127.0.0.1', port)) != 0:
return port
port += 1
raise Exception("无可用端口,Web服务启动失败")
# 生成美化版 Web 页面(内存中生成,不落地)- 更新为F13显示
def generate_web_page():
html = f"""
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>语音助手</title>
<style>
* {{
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: "Microsoft YaHei", "PingFang SC", sans-serif;
}}
body {{
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}}
.container {{
background: white;
border-radius: 16px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
padding: 40px;
width: 100%;
max-width: 600px;
position: relative;
overflow: hidden;
}}
.container::before {{
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 8px;
background: linear-gradient(90deg, #00b42a, #0088ff);
}}
.title {{
color: #333;
font-size: 28px;
text-align: center;
margin-bottom: 40px;
font-weight: 600;
position: relative;
}}
.title::after {{
content: "";
display: block;
width: 80px;
height: 4px;
background: #00b42a;
margin: 10px auto 0;
border-radius: 2px;
}}
.config-card {{
background: #f8f9fa;
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
border-left: 4px solid #0088ff;
}}
.config-item {{
display: flex;
align-items: center;
margin: 15px 0;
font-size: 16px;
}}
.config-label {{
flex: 0 0 80px;
font-weight: 600;
color: #555;
}}
.config-value {{
flex: 1;
color: #333;
padding: 8px 12px;
background: white;
border-radius: 6px;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}}
.status-card {{
background: linear-gradient(135deg, #e8f4f8 0%, #f0f8fb 100%);
border-radius: 12px;
padding: 20px;
margin-top: 30px;
}}
.status-title {{
font-size: 18px;
font-weight: 600;
color: #2d3748;
margin-bottom: 10px;
}}
.status-content {{
color: #4a5568;
font-size: 15px;
line-height: 1.6;
}}
.count {{
color: #0088ff;
font-weight: 700;
font-size: 18px;
}}
.footer {{
text-align: center;
margin-top: 30px;
color: #999;
font-size: 14px;
}}
/* 实时刷新动画 */
@keyframes pulse {{
0% {{ opacity: 1; }}
50% {{ opacity: 0.7; }}
100% {{ opacity: 1; }}
}}
.refresh-indicator {{
display: inline-block;
width: 8px;
height: 8px;
background: #00b42a;
border-radius: 50%;
margin-left: 8px;
animation: pulse 2s infinite;
}}
</style>
<script>
// 实时刷新状态(每1秒更新一次)
setInterval(() => {{
fetch('/status')
.then(response => response.json())
.then(data => {{
document.querySelector('.count').textContent = data.click_count;
document.querySelector('.status-content').textContent = '状态:' + data.status;
}})
.catch(err => console.log('状态更新失败:', err));
}}, 1000);
</script>
</head>
<body>
<div class="container">
<h1 class="title">语音助手</h1>
<div class="config-card">
<div class="config-item">
<span class="config-label">快捷键:</span>
<span class="config-value">F13</span> <!-- 替换为F13显示 -->
</div>
<div class="config-item">
<span class="config-label">语音键:</span>
<span class="config-value">Ctrl + Shift + H</span>
</div>
<div class="config-item">
<span class="config-label">确认键:</span>
<span class="config-value">Enter</span>
</div>
<div class="config-item">
<span class="config-label">点击数:</span>
<span class="config-value count">{click_count}</span>
</div>
</div>
<div class="status-card">
<div class="status-title">运行状态 <span class="refresh-indicator"></span></div>
<div class="status-content">状态:{status_info}</div>
</div>
<div class="footer">
语音助手 v1.0 | 后台运行中 · 按F13触发操作 <!-- 替换为F13提示 -->
</div>
</div>
</body>
</html>
"""
return html.encode("utf-8")
# 兼容exe的HTTP请求处理器
class MemoryHTTPHandler(BaseHTTPRequestHandler):
# 禁用日志输出(避免exe环境下的IO问题)
def log_message(self, format, *args):
return
def do_GET(self):
try:
parsed_path = urlparse(self.path)
# 根路径返回Web页面
if parsed_path.path == "/":
self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Connection", "close")
self.end_headers()
response = BytesIO()
response.write(generate_web_page())
self.wfile.write(response.getvalue())
# 状态接口(实时刷新)
elif parsed_path.path == "/status":
self.send_response(200)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Connection", "close")
self.end_headers()
status_data = {
"click_count": click_count,
"status": status_info
}
self.wfile.write(json.dumps(status_data).encode("utf-8"))
else:
self.send_response(404)
self.send_header("Connection", "close")
self.end_headers()
self.wfile.write(b"<h1>404 Not Found</h1>")
except Exception as e:
self.send_response(500)
self.send_header("Connection", "close")
self.end_headers()
self.wfile.write(f"Error: {str(e)}".encode("utf-8"))
# 启动Web服务(适配exe环境)
def start_web_server():
global web_server, server_port, server_thread
server_port = get_available_port(8080)
server_address = ("127.0.0.1", server_port)
web_server = HTTPServer(server_address, MemoryHTTPHandler)
web_server.timeout = 1
server_thread = threading.Thread(target=web_server.serve_forever, daemon=True)
server_thread.start()
update_status(f"Web服务已启动:http://127.0.0.1:{server_port}")
# ====================== 原有逻辑保留 ======================
# 自动生成ICO图标文件
def generate_app_icon():
if os.path.exists(ICON_FILE_NAME):
return
sizes = [(16,16), (32,32), (64,64), (128,128)]
img_list = []
for size in sizes:
img = Image.new("RGB", size, color=(0, 180, 0))
draw = ImageDraw.Draw(img)
try:
if hasattr(sys, '_MEIPASS'):
font_path = os.path.join(sys._MEIPASS, "simhei.ttf")
font = ImageFont.truetype(font_path, size[0]*2//3)
else:
font = ImageFont.truetype("simhei.ttf", size[0]*2//3)
except:
font = ImageFont.load_default(size=size[0]*2//3)
text_bbox = font.getbbox("语")
text_w = text_bbox[2] - text_bbox[0]
text_h = text_bbox[3] - text_bbox[1]
text_x = (size[0] - text_w) // 2
text_y = (size[1] - text_h) // 2
draw.text((text_x, text_y), "语", fill=(255, 255, 255), font=font)
img_list.append(img)
img_list[0].save(ICON_FILE_NAME, format='ICO', sizes=sizes, append_images=img_list[1:])
# 生成默认托盘图标
def create_default_icon():
img = Image.new("RGB", (64, 64), color=(0, 180, 0))
draw = ImageDraw.Draw(img)
try:
if hasattr(sys, '_MEIPASS'):
font_path = os.path.join(sys._MEIPASS, "simhei.ttf")
font = ImageFont.truetype(font_path, 40)
else:
font = ImageFont.truetype("simhei.ttf", 40)
except:
font = ImageFont.load_default(size=40)
draw.text((12, 8), "语", fill=(255, 255, 255), font=font)
return img
# 托盘菜单 - 打开控制面板
def open_web_page(icon, item):
try:
webbrowser.open(f"http://127.0.0.1:{server_port}")
except Exception as e:
update_status(f"打开页面失败:{str(e)}")
# 托盘菜单 - 退出程序(完整清理资源)
def exit_app(icon, item):
global is_running, key_listener, tray_icon, web_server, server_thread
is_running = False
if key_listener:
key_listener.stop()
key_listener = None
if web_server:
web_server.shutdown()
web_server.server_close()
web_server = None
if server_thread and server_thread.is_alive():
server_thread.join(timeout=2)
if tray_icon:
tray_icon.stop()
tray_icon = None
os._exit(0)
# 更新状态信息(同步到Web页面)
def update_status(text):
global status_info
status_info = text
if not hasattr(sys, '_MEIPASS'):
print(f"状态更新:{text}")
# 核心双态触发逻辑(F13触发,防抖+奇启偶确)
def on_trigger():
global click_count, last_click_time
current_time = time.time()
if current_time - last_click_time < 0.25:
return
last_click_time = current_time
click_count += 1
if click_count % 2 == 1:
with keyboard_ctrl.pressed(*VOICE_HOTKEY):
pass
update_status(f"第{click_count}次:启动语音输入")
else:
keyboard_ctrl.press(Key.enter)
keyboard_ctrl.release(Key.enter)
update_status(f"第{click_count}次:Enter确认输入")
# 全局F13监听器
def global_listener():
def on_press(key):
if not is_running:
return False
if key == TRIGGER_KEY:
on_trigger()
return True
return keyboard.Listener(on_press=on_press)
# 初始化程序(启动所有服务)
def init_program():
global is_running, key_listener, tray_icon
if is_running:
return
is_running = True
key_listener = global_listener()
key_listener.start()
start_web_server()
generate_app_icon()
icon_img = Image.open(ICON_FILE_NAME) if os.path.exists(ICON_FILE_NAME) else create_default_icon()
menu = pystray.Menu(
pystray.MenuItem("控制面板", open_web_page),
pystray.MenuItem("退出", exit_app)
)
tray_icon = pystray.Icon("voice_assistant", icon_img, "语音助手", menu)
tray_thread = threading.Thread(target=tray_icon.run, daemon=True)
tray_thread.start()
update_status("运行中,监听F13按键...") # 替换为F13监听提示
# 程序入口(适配exe环境)
if __name__ == "__main__":
if hasattr(sys, '_MEIPASS'):
os.chdir(sys._MEIPASS)
generate_app_icon()
init_program()
try:
while is_running:
time.sleep(0.5)
except (KeyboardInterrupt, SystemExit):
exit_app(None, None)
更改为 F13 键映射,增加了新软件,再 使用 QKeyMapper 将 鼠标的侧位键 Mouse-X1 映射到 F13 ,避免和现有的键位冲突,更加方便。
自己拿AI分析,所有东西都写成固定值了,自己拿 AI 调,运行一下 文件名.py 会自动生成ICO图标。

一、一次性安装所有依赖命令
打开命令提示符(CMD)/终端,执行以下命令即可安装程序运行+打包所需全部依赖:
bash
pip install pillow pystray pynput tk pyinstaller
国内镜像源加速版(解决网络安装慢/失败问题),自己弄一个就行
先运行一下 语音助手.py 会在,当前文件夹生成一个 app_icon.ico
打包命令,然后复制直接打包就行,文件名自己取名就行。
php
pyinstaller -F -w -i app_icon.ico --hidden-import=pynput.keyboard._win32 --hidden-import=pynput.mouse._win32 --name "语音助手" 语音助手.py
二、各依赖包详细说明
| 依赖包名 | 安装说明 | 程序中核心作用 |
|---|---|---|
pillow |
直接安装最新版 | 替代PIL库,实现托盘图标绘制、ICO图标文件生成与图片处理 |
pystray |
直接安装最新版 | 实现系统托盘功能,支持窗口最小化到托盘、托盘菜单(显示/退出) |
pynput |
直接安装最新版 | 全局监听F4快捷键、模拟键盘输入(Ctrl+Shift+H、Enter键) |
tk/tkinter |
Windows用pip install tk,Python默认自带 |
构建GUI窗口,实现程序的可视化配置界面 |
pyinstaller |
直接安装最新版 | 将Python脚本打包为Windows可执行EXE文件,无需Python环境运行 |
三、补充避坑说明
1. tkinter 系统专属安装
- Windows :执行
pip install tk即可(Python一般自带,缺失时补充) - Linux :需安装系统级依赖,执行
sudo apt-get install python3-tk - Mac :需安装系统级依赖,执行
brew install python-tk
2. pystray 安装报错解决
仅Linux/Mac系统安装pystray可能报错,需先安装系统依赖:
- Linux:
sudo apt-get install libgirepository1.0-dev - Mac:
brew install gobject-introspection
Windows系统直接安装无额外依赖。
3. 版本兼容建议
- Python版本:推荐3.8~3.11(pyinstaller对Python3.12+的兼容性暂未完全适配)
- 依赖包版本:全部安装最新版即可,无需手动指定版本号。
4. 安装验证
安装完成后,可执行以下命令验证核心包是否安装成功:
bash
pip show pillow pystray pynput pyinstaller
显示包名、版本、路径即代表安装成功。