自己制作 的 豆包语音助手,配合PC端使用

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

显示包名、版本、路径即代表安装成功。

相关推荐
weixin_416660072 天前
技术分析:豆包生成带公式文案导出Word乱码的底层机理
人工智能·word·豆包
AI刀刀5 天前
千问 文心 元宝 Kimi公式乱码
ai·pdf·豆包·deepseek·ds随心转
AI刀刀5 天前
豆包怎么生成excel
ai·excel·豆包·deepseek·ds随心转
独自归家的兔8 天前
阿里千问Qwen3-ASR开源:52种语种通吃,流式+高并发双在线,歌声识别也精准!
人工智能·开源·豆包
DS随心转插件9 天前
元宝 千问 文心 Kimi排版指令
人工智能·ai·chatgpt·豆包·deepseek·ds随心转
qq_5469372711 天前
Github开源插件!最新豆包AI无水印图批量下载,免费无广告使用,支持高清无损图片下载 (1)
豆包
DS随心转APP12 天前
怎么导出豆包聊天记录
人工智能·ai·豆包·deepseek·ds随心转
RichardLau_Cx13 天前
Google Chrome 浏览器安装「豆包插件」完整教程
前端·chrome·插件·豆包
DS随心转小程序14 天前
ChatGPT和Gemini公式
人工智能·chatgpt·aigc·word·豆包·deepseek·ds随心转