Files
open-xiaoai/packages/server-python/xiaozhi/services/display/gui_display.py
T

285 lines
10 KiB
Python

import threading
import tkinter as tk
from tkinter import ttk
import queue
import logging
import time
from typing import Optional, Callable
from xiaozhi.services.display.base_display import BaseDisplay
class GuiDisplay(BaseDisplay):
def __init__(self):
super().__init__() # 调用父类初始化
"""创建 GUI 界面"""
# 初始化日志
self.logger = logging.getLogger("Display")
# 创建主窗口
self.root = tk.Tk()
self.root.title("小爱音箱接入小智 AI 演示")
self.root.geometry("520x360")
# 在窗口底部添加作者信息
self.author_label = ttk.Label(self.root, text="作者: https://del.wang")
self.author_label.pack(side=tk.BOTTOM, pady=5)
# 让窗口居中显示
self.root.update_idletasks()
width = self.root.winfo_width()
height = self.root.winfo_height()
x = (self.root.winfo_screenwidth() // 2) - (width // 2)
y = (self.root.winfo_screenheight() // 2) - (height // 2)
self.root.geometry(f"+{x}+{y}")
# 状态显示
self.status_frame = ttk.Frame(self.root)
self.status_frame.pack(pady=20)
self.status_label = ttk.Label(self.status_frame, text="状态: 未连接")
self.status_label.pack(side=tk.LEFT)
# 表情显示
self.emotion_label = tk.Label(self.root, text="😊", font=("Segoe UI Emoji", 32))
self.emotion_label.pack(padx=20, pady=20)
# TTS文本显示
self.tts_text_label = ttk.Label(
self.root, text="很高兴认识你!", wraplength=250
)
self.tts_text_label.pack(padx=20, pady=10)
# 控制按钮
self.btn_frame = ttk.Frame(self.root)
self.btn_frame.pack(pady=20)
# 手动模式按钮
self.manual_btn = ttk.Button(self.btn_frame, text="按住说话")
self.manual_btn.bind("<ButtonPress-1>", self._on_manual_button_press)
self.manual_btn.bind("<ButtonRelease-1>", self._on_manual_button_release)
self.manual_btn.pack(side=tk.LEFT, padx=10)
# 打断按钮
self.abort_btn = ttk.Button(
self.btn_frame, text="停止播放", command=self._on_abort_button_click
)
self.abort_btn.pack(side=tk.LEFT, padx=10)
# 对话模式标志
self.auto_mode = False
# 回调函数
self.button_press_callback = None
self.button_release_callback = None
self.status_update_callback = None
self.text_update_callback = None
self.emotion_update_callback = None
self.mode_callback = None
self.auto_callback = None
self.abort_callback = None
# 更新队列
self.update_queue = queue.Queue()
# 运行标志
self._running = True
# 设置窗口关闭处理
self.root.protocol("WM_DELETE_WINDOW", self.on_close)
# 启动更新处理
self.root.after(100, self._process_updates)
def set_callbacks(
self,
press_callback: Optional[Callable] = None,
release_callback: Optional[Callable] = None,
status_callback: Optional[Callable] = None,
text_callback: Optional[Callable] = None,
emotion_callback: Optional[Callable] = None,
mode_callback: Optional[Callable] = None,
auto_callback: Optional[Callable] = None,
abort_callback: Optional[Callable] = None,
):
"""设置回调函数"""
self.button_press_callback = press_callback
self.button_release_callback = release_callback
self.status_update_callback = status_callback
self.text_update_callback = text_callback
self.emotion_update_callback = emotion_callback
self.mode_callback = mode_callback
self.auto_callback = auto_callback
self.abort_callback = abort_callback
def _process_updates(self):
"""处理更新队列"""
try:
while True:
try:
# 非阻塞方式获取更新
update_func = self.update_queue.get_nowait()
update_func()
self.update_queue.task_done()
except queue.Empty:
break
finally:
if self._running:
self.root.after(100, self._process_updates)
def _on_manual_button_press(self, event):
"""手动模式按钮按下事件处理"""
try:
# 更新按钮文本为"松开以停止"
self.manual_btn.config(text="松开以停止")
# 调用回调函数
if self.button_press_callback:
self.button_press_callback()
except Exception as e:
self.logger.error(f"按钮按下回调执行失败: {e}")
def _on_manual_button_release(self, event):
"""手动模式按钮释放事件处理"""
try:
# 更新按钮文本为"按住说话"
self.manual_btn.config(text="按住说话")
# 调用回调函数
if self.button_release_callback:
self.button_release_callback()
except Exception as e:
self.logger.error(f"按钮释放回调执行失败: {e}")
def _on_auto_button_click(self):
"""自动模式按钮点击事件处理"""
try:
if self.auto_callback:
self.auto_callback()
except Exception as e:
self.logger.error(f"自动模式按钮回调执行失败: {e}")
def _on_abort_button_click(self):
"""打断按钮点击事件处理"""
try:
if self.abort_callback:
self.abort_callback()
except Exception as e:
self.logger.error(f"打断按钮回调执行失败: {e}")
def _on_mode_button_click(self):
"""对话模式切换按钮点击事件"""
try:
# 检查是否可以切换模式(通过回调函数询问应用程序当前状态)
if self.mode_callback:
# 如果回调函数返回False,表示当前不能切换模式
if not self.mode_callback(not self.auto_mode):
return
# 切换模式
self.auto_mode = not self.auto_mode
# 更新按钮显示
if self.auto_mode:
# 切换到自动模式
self.update_mode_button_status("自动对话")
# 隐藏手动按钮,显示自动按钮
self.update_queue.put(lambda: self._switch_to_auto_mode())
else:
# 切换到手动模式
self.update_mode_button_status("手动对话")
# 隐藏自动按钮,显示手动按钮
self.update_queue.put(lambda: self._switch_to_manual_mode())
except Exception as e:
self.logger.error(f"模式切换按钮回调执行失败: {e}")
def _switch_to_auto_mode(self):
"""切换到自动模式的UI更新"""
self.manual_btn.pack_forget() # 移除手动按钮
self.auto_btn.pack(
side=tk.LEFT, padx=10, before=self.abort_btn
) # 显示自动按钮,放在打断按钮前面
def _switch_to_manual_mode(self):
"""切换到手动模式的UI更新"""
self.auto_btn.pack_forget() # 移除自动按钮
self.manual_btn.pack(
side=tk.LEFT, padx=10, before=self.abort_btn
) # 显示手动按钮,放在打断按钮前面
def update_status(self, status: str):
"""更新状态文本"""
self.update_queue.put(lambda: self.status_label.config(text=f"状态: {status}"))
def update_text(self, text: str):
"""更新TTS文本"""
self.update_queue.put(lambda: self.tts_text_label.config(text=text))
def update_emotion(self, emotion: str):
"""更新表情"""
self.update_queue.put(lambda: self.emotion_label.config(text=emotion))
def start_update_threads(self):
"""启动更新线程"""
def update_loop():
while self._running:
try:
# 更新状态
if self.status_update_callback:
status = self.status_update_callback()
if status:
self.update_status(status)
# 更新文本
if self.text_update_callback:
text = self.text_update_callback()
if text:
self.update_text(text)
# 更新表情
if self.emotion_update_callback:
emotion = self.emotion_update_callback()
if emotion:
self.update_emotion(emotion)
except Exception as e:
self.logger.error(f"更新失败: {e}")
time.sleep(0.1)
threading.Thread(target=update_loop, daemon=True).start()
def on_close(self):
"""关闭窗口处理"""
self._running = False
self.root.destroy()
def start(self):
"""启动GUI"""
try:
# 启动更新线程
self.start_update_threads()
# 在主线程中运行主循环
self.logger.info("开始启动GUI主循环")
self.root.mainloop()
except Exception as e:
self.logger.error(f"GUI启动失败: {e}", exc_info=True)
# 尝试回退到CLI模式
print(f"GUI启动失败: {e},请尝试使用CLI模式")
def update_mode_button_status(self, text: str):
"""更新模式按钮状态"""
self.update_queue.put(lambda: self.mode_btn.config(text=text))
def update_button_status(self, text: str):
"""更新按钮状态 - 保留此方法以满足抽象基类要求"""
# 根据当前模式更新相应的按钮
if self.auto_mode:
self.update_queue.put(lambda: self.auto_btn.config(text=text))
else:
# 在手动模式下,不通过此方法更新按钮文本
# 因为按钮文本由按下/释放事件直接控制
pass