"""
【通义千问 Qwen】API集成模块
用于意图理解和任务处理
"""
import json
import re
import logging
from database import config
import dashscope
from dashscope import Generation
# --- 导入快捷日志工具 ---
from Progress.utils.logger_utils import log_time, log_step, log_var, log_call
from Progress.utils.logger_config import setup_logger # 确保 logger 已配置
# --- 初始化日志器 ---
logger = logging.getLogger(__name__) # 使用模块名作为 logger 名称,便于区分来源
# --- API 配置 ---
DASHSCOPE_API_KEY = config.api_key
DASHSCOPE_MODEL = config.model
class QWENAssistant:
def __init__(self):
if not DASHSCOPE_API_KEY:
raise ValueError("缺少 DASHSCOPE_API_KEY,请检查配置文件")
dashscope.api_key = DASHSCOPE_API_KEY
self.model_name = DASHSCOPE_MODEL or 'qwen-max'
logger.info(f"✅ QWENAssistant 初始化完成,使用模型: {self.model_name}")
self.conversation_history = []
self.system_prompt = """
你是一个智能语音控制助手,能够理解用户的语音指令并执行相应的任务。
你的主要能力包括:
1. 播放音乐和控制媒体
2. 文件操作(创建、读取、编辑文件)
3. 文本生成(写文章、总结、翻译等)
4. 系统控制(打开应用、设置提醒等)
5. 多步骤任务编排
当用户发出指令时,你需要:
1. 理解用户的意图
2. 确定需要执行的具体操作
3. 返回结构化的响应,包含操作类型和参数
🎯 响应格式必须是严格合法的 JSON:
{
"intent": "操作类型",
"action": "具体动作",
"parameters": {"参数名": "参数值"},
"response": "给用户的回复",
"needs_confirmation": true/false
}
📌 支持的操作类型:
- music: 音乐相关操作
- file: 文件操作
- text: 文本生成
- system: 系统控制
- task: 多步骤任务
- chat: 普通对话
❗请始终用中文回复用户。
"""
@log_time
@log_step("处理语音指令")
def process_voice_command(self, voice_text):
"""处理语音指令,返回结构化 JSON 响应"""
log_var("原始输入", voice_text)
if not voice_text.strip():
logger.warning("收到空语音输入")
return self._create_response("chat", "empty", {}, "我没有听清楚,请重新说话。", False)
log_call("添加用户消息到历史")
self.conversation_history.append({"role": "user", "content": voice_text})
try:
messages = [{"role": "system", "content": self.system_prompt}]
messages.extend(self.conversation_history[-10:])
log_var("请求消息数", len(messages))
logger.debug("正在调用 Qwen API...")
response = Generation.call(
model=self.model_name,
messages=messages,
temperature=0.5,
top_p=0.8,
max_tokens=1024
)
if response.status_code != 200:
error_msg = f"Qwen API 调用失败: {response.status_code}, {response.message}"
logger.error(error_msg)
return self._create_response("chat", "error", {}, f"服务暂时不可用: {response.message}", False)
ai_response = response.output['text'].strip()
log_var("模型输出", ai_response)
self.conversation_history.append({"role": "assistant", "content": ai_response})
# 尝试解析 JSON
try:
parsed_response = json.loads(ai_response)
log_call("成功解析结构化响应")
return parsed_response
except json.JSONDecodeError:
logger.warning("输出非标准JSON,尝试提取片段")
json_match = re.search(r'\{[\s\S]*\}', ai_response)
if json_match:
try:
parsed_response = json.loads(json_match.group())
log_call("从文本中提取JSON成功")
return parsed_response
except json.JSONDecodeError:
pass
logger.info("降级为普通聊天响应")
return self._create_response("chat", "reply", {}, ai_response, False)
except Exception as e:
logger.exception("处理语音指令时发生未预期异常")
return self._create_response("chat", "error", {}, "抱歉,我遇到了一些技术问题,请稍后再试。", False)
def _create_response(self, intent, action, parameters, response, needs_confirmation):
resp = {
"intent": intent,
"action": action,
"parameters": parameters,
"response": response,
"needs_confirmation": needs_confirmation
}
log_var("返回响应", resp)
return resp
@log_time
def generate_text(self, prompt, task_type="general"):
log_var("任务类型", task_type)
log_var("提示词长度", len(prompt))
try:
system_prompt = f"""
你是一个专业的文本生成助手。根据用户的要求生成高质量的文本内容。
任务类型:{task_type}
要求:{prompt}
请生成相应的文本内容,确保内容准确、有逻辑、语言流畅。
"""
response = Generation.call(
model=self.model_name,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": prompt}
],
temperature=0.8,
max_tokens=2000
)
if response.status_code == 200:
result = response.output['text']
log_var("生成结果长度", len(result))
return result
else:
error_msg = f"文本生成失败: {response.message}"
logger.error(error_msg)
return error_msg
except Exception as e:
logger.exception("文本生成出错")
return f"抱歉,生成文本时遇到错误:{str(e)}"
@log_time
def summarize_text(self, text):
log_var("待总结文本长度", len(text))
try:
prompt = f"请总结以下文本的主要内容:\n\n{text}"
response = Generation.call(
model=self.model_name,
messages=[{"role": "user", "content": prompt}],
temperature=0.3,
max_tokens=500
)
if response.status_code == 200:
result = response.output['text']
log_var("总结结果长度", len(result))
return result
else:
error_msg = f"总结失败: {response.message}"
logger.error(error_msg)
return error_msg
except Exception as e:
logger.exception("文本总结出错")
return f"抱歉,总结文本时遇到错误:{str(e)}"
@log_time
def translate_text(self, text, target_language="英文"):
log_var("目标语言", target_language)
log_var("原文长度", len(text))
try:
prompt = f"请将以下文本翻译成{target_language}:\n\n{text}"
response = Generation.call(
model=self.model_name,
messages=[{"role": "user", "content": prompt}],
temperature=0.3,
max_tokens=1000
)
if response.status_code == 200:
result = response.output['text']
log_var("翻译结果长度", len(result))
return result
else:
error_msg = f"翻译失败: {response.message}"
logger.error(error_msg)
return error_msg
except Exception as e:
logger.exception("文本翻译出错")
return f"抱歉,翻译文本时遇到错误:{str(e)}"
def clear_history(self):
old_len = len(self.conversation_history)
self.conversation_history.clear()
logger.info(f"🧹 对话历史已清空,共清除 {old_len} 条记录")
# =============================
# 🧪 测试代码
# =============================
if __name__ == "__main__":
# 初始化全局日志系统
setup_logger(name="ai_assistant", log_dir="logs")
assistant = QWENAssistant()
test_commands = [
"播放周杰伦的歌曲",
"写一篇关于气候变化的文章",
"把这段话翻译成英文:今天天气真好",
"总结一下人工智能的发展历程",
"你好啊",
"打开浏览器"
]
for cmd in test_commands:
print(f"\n🔊 用户指令: {cmd}")
result = assistant.process_voice_command(cmd)
print("🤖 AI响应:")
print(json.dumps(result, ensure_ascii=False, indent=2))
"""
【系统控制模块】System Controller
提供音乐播放、文件操作、应用启动、定时提醒等本地系统级功能
"""
import os
import subprocess
import platform
import time
import psutil
import pygame
import schedule
from datetime import datetime
import logging
# --- 导入配置 ---
from database import config
# --- 导入日志工具 ---
from Progress.utils.logger_utils import log_time, log_step, log_var, log_call
from Progress.utils.logger_config import setup_logger
# --- 配置路径 ---
RESOURCE_PATH = config.resource_path
DEFAULT_MUSIC_PATH = os.path.join(RESOURCE_PATH, config.music_path)
DEFAULT_DOCUMENT_PATH = os.path.join(RESOURCE_PATH, config.doc_path)
# --- 初始化日志器 ---
logger = logging.getLogger(__name__)
class SystemController:
"""
系统功能控制器:封装常见的操作系统交互操作
"""
def __init__(self):
self.system = platform.system()
self.music_player = None
self.scheduled_tasks = {}
self.task_counter = 0
log_var("运行平台", self.system)
self._init_music_player()
@log_step("初始化音乐播放器")
@log_time
def _init_music_player(self):
"""初始化 pygame 音频引擎用于本地音乐播放"""
try:
pygame.mixer.init()
self.music_player = pygame.mixer.music
logger.info("✅ 音乐播放器初始化成功")
except Exception as e:
logger.exception("❌ 音乐播放器初始化失败")
self.music_player = None
@log_step("播放音乐")
@log_time
def play_music(self, music_path=None):
"""
播放指定目录下的第一个音乐文件
:param music_path: 音乐文件夹路径,None 则使用默认路径
:return: (success: bool, message: str)
"""
if not self.music_player:
error_msg = "🔇 音乐播放器未就绪"
logger.error(error_msg)
return False, error_msg
target_path = music_path or DEFAULT_MUSIC_PATH
log_var("目标音乐路径", target_path)
if not os.path.exists(target_path):
msg = f"📁 路径不存在: {target_path}"
logger.warning(msg)
return False, msg
music_files = self._find_music_files(target_path)
if not music_files:
msg = "🎵 未在指定路径找到支持的音乐文件 (.mp3/.wav/.flac 等)"
logger.info(msg)
return False, msg
music_file = music_files[0]
log_var("即将播放", os.path.basename(music_file))
try:
self.stop_music() # 停止当前播放
self.music_player.load(music_file)
self.music_player.play()
success_msg = f"🎶 正在播放: {os.path.basename(music_file)}"
logger.info(success_msg)
return True, success_msg
except Exception as e:
logger.exception("💥 播放音乐失败")
return False, f"播放失败: {str(e)}"
@log_time
def stop_music(self):
"""停止当前播放的音乐"""
try:
if self.music_player and pygame.mixer.get_init():
self.music_player.stop()
logger.info("⏹️ 音乐已停止")
return True, "音乐已停止"
except Exception as e:
logger.exception("❌ 停止音乐失败")
return False, f"停止失败: {str(e)}"
@log_time
def pause_music(self):
"""暂停音乐播放"""
try:
if self.music_player and pygame.mixer.get_init():
self.music_player.pause()
logger.info("⏸️ 音乐已暂停")
return True, "音乐已暂停"
except Exception as e:
logger.exception("❌ 暂停音乐失败")
return False, f"暂停失败: {str(e)}"
@log_time
def resume_music(self):
"""恢复音乐播放"""
try:
if self.music_player and pygame.mixer.get_init():
self.music_player.unpause()
logger.info("▶️ 音乐已恢复")
return True, "音乐已恢复"
except Exception as e:
logger.exception("❌ 恢复音乐失败")
return False, f"恢复失败: {str(e)}"
@log_step("打开应用程序")
@log_time
def open_application(self, app_name):
"""
根据名称启动常见应用程序(跨平台)
:param app_name: 应用中文名,如“浏览器”、“计算器”
:return: (success, message)
"""
app_commands = {
"浏览器": self._get_browser_command(),
"记事本": self._get_text_editor_command(),
"文件管理器": self._get_file_manager_command(),
"计算器": self._get_calculator_command(),
"终端": self._get_terminal_command()
}
log_var("请求打开应用", app_name)
if app_name not in app_commands:
warn_msg = f"🚫 不支持的应用: {app_name}"
logger.warning(warn_msg)
return False, warn_msg
command = app_commands[app_name]
log_var("执行命令", command)
try:
subprocess.Popen(command, shell=True)
success_msg = f"🚀 正在打开 {app_name}"
logger.info(success_msg)
return True, success_msg
except Exception as e:
logger.exception(f"💥 启动应用 {app_name} 失败")
return False, f"启动失败: {str(e)}"
@log_step("创建文件")
@log_time
def create_file(self, file_path, content=""):
"""
创建文本文件(自动创建父目录)
:param file_path: 文件完整路径
:param content: 写入内容
:return: (success, message)
"""
log_var("创建文件", {"path": file_path, "content_length": len(content)})
try:
os.makedirs(os.path.dirname(file_path), exist_ok=True)
with open(file_path, 'w', encoding='utf-8') as f:
f.write(content)
logger.info(f"📄 文件已创建: {file_path}")
return True, f"文件已创建: {file_path}"
except Exception as e:
logger.exception("❌ 创建文件失败")
return False, f"创建失败: {str(e)}"
@log_step("读取文件")
@log_time
def read_file(self, file_path):
"""
读取文本文件内容
:param file_path: 文件路径
:return: (success, content 或错误信息)
"""
log_var("读取文件", file_path)
if not os.path.exists(file_path):
msg = f"⚠️ 文件不存在: {file_path}"
logger.warning(msg)
return False, msg
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
logger.info(f"📖 成功读取文件: {file_path} ({len(content)} 字符)")
return True, content
except Exception as e:
logger.exception("❌ 读取文件失败")
return False, f"读取失败: {str(e)}"
@log_step("写入文件")
@log_time
def write_file(self, file_path, content):
"""
覆盖写入文件
:param file_path: 文件路径
:param content: 内容
:return: (success, message)
"""
log_var("写入文件", {"path": file_path, "length": len(content)})
try:
with open(file_path, 'w', encoding='utf-8') as f:
f.write(content)
logger.info(f"💾 文件已保存: {file_path}")
return True, f"文件已保存: {file_path}"
except Exception as e:
logger.exception("❌ 写入文件失败")
return False, f"写入失败: {str(e)}"
@log_step("获取系统信息")
@log_time
def get_system_info(self):
"""
获取当前系统的运行状态
:return: (success, info dict)
"""
try:
memory = psutil.virtual_memory()
disk = psutil.disk_usage('/')
cpu_percent = psutil.cpu_percent(interval=1) # 更准确
info = {
"操作系统": platform.system(),
"系统版本": platform.version(),
"处理器": platform.processor() or "未知",
"内存使用率": f"{memory.percent}%",
"CPU使用率": f"{cpu_percent}%",
"磁盘使用率": f"{disk.percent}%",
"总内存": f"{memory.total // (1024 ** 3)} GB",
"可用内存": f"{memory.available // (1024 ** 2)} MB"
}
log_var("系统信息", info)
logger.info("📊 系统信息获取成功")
return True, info
except Exception as e:
logger.exception("❌ 获取系统信息失败")
return False, f"获取失败: {str(e)}"
@log_step("设置提醒")
@log_time
def set_reminder(self, message, delay_minutes):
"""
设置一个延时提醒任务
:param message: 提醒内容
:param delay_minutes: 延迟分钟数
:return: (success, message)
"""
log_var("设置提醒", {"delay_min": delay_minutes, "message": message})
self.task_counter += 1
task_id = f"reminder_{self.task_counter}"
def reminder_job():
logger.info(f"🔔 提醒触发: {message}")
# TODO: 添加桌面通知或语音播报
try:
schedule.every(delay_minutes).minutes.do(reminder_job)
self.scheduled_tasks[task_id] = {
"message": message,
"delay": delay_minutes,
"created": datetime.now().isoformat()
}
success_msg = f"⏰ 提醒已设置: {delay_minutes} 分钟后提醒 - '{message}'"
logger.info(success_msg)
return True, success_msg
except Exception as e:
logger.exception("❌ 设置提醒失败")
return False, f"设置失败: {str(e)}"
@log_time
def run_scheduled_tasks(self):
"""运行所有待处理的定时任务(通常在主循环中调用)"""
schedule.run_pending()
@log_time
def _find_music_files(self, directory):
"""递归查找指定目录中的音乐文件"""
music_extensions = {'.mp3', '.wav', '.flac', '.m4a', '.ogg', '.aac'}
music_files = []
try:
for root, dirs, files in os.walk(directory):
for file in files:
ext = os.path.splitext(file.lower())[1]
if ext in music_extensions:
full_path = os.path.join(root, file)
music_files.append(full_path)
log_call(f"发现音乐文件: {full_path}")
except Exception as e:
logger.exception(f"🔍 搜索音乐文件时出错: {directory}")
log_var("搜索完成", f"共找到 {len(music_files)} 个音乐文件")
return music_files
# =============================
# 🛠️ 平台命令映射(私有方法)
# =============================
def _get_browser_command(self):
system = self.system
return "start chrome" if system == "Windows" \
else "open -a Safari" if system == "Darwin" \
else "xdg-open"
def _get_text_editor_command(self):
system = self.system
return "notepad" if system == "Windows" \
else "open -a TextEdit" if system == "Darwin" \
else "gedit"
def _get_file_manager_command(self):
system = self.system
return "explorer" if system == "Windows" \
else "open -a Finder" if system == "Darwin" \
else "nautilus"
def _get_calculator_command(self):
system = self.system
return "calc" if system == "Windows" \
else "open -a Calculator" if system == "Darwin" \
else "gnome-calculator"
def _get_terminal_command(self):
system = self.system
return "cmd" if system == "Windows" \
else "open -a Terminal" if system == "Darwin" \
else "gnome-terminal"
# =============================
# 🧩 任务编排器(TaskOrchestrator)
# =============================
class TaskOrchestrator:
"""
多步骤任务执行器,协调 SystemController 与 GPT 助手完成复杂流程
"""
def __init__(self, system_controller: SystemController, gpt_assistant=None):
self.system_controller = system_controller
self.gpt_assistant = gpt_assistant
self.task_queue = []
self.current_task = None
logger.info("🔧 任务编排器已初始化")
@log_step("执行任务序列")
@log_time
def execute_task_sequence(self, tasks):
"""
按顺序执行多个任务
:param tasks: 任务列表,每个任务格式见 `_execute_single_task`
:return: 结果列表
"""
results = []
log_var("开始执行任务序列", {"任务数量": len(tasks)})
for i, task in enumerate(tasks):
log_call(f"▶️ 执行第 {i+1} 个任务")
result = self._execute_single_task(task)
results.append(result)
if not result["success"]:
logger.warning(f"🟡 任务 {i+1} 执行失败: {result['message']}")
log_var("任务序列完成", {"成功数": sum(1 for r in results if r["success"])})
return results
@log_time
def _execute_single_task(self, task):
"""执行单个结构化任务"""
task_type = task.get("type")
parameters = task.get("parameters", {})
log_var("执行任务", {"type": task_type, "params": parameters})
try:
if task_type == "music":
action = parameters.get("action", "play")
handler_map = {
"play": lambda: self.system_controller.play_music(parameters.get("path")),
"stop": self.system_controller.stop_music,
"pause": self.system_controller.pause_music,
"resume": self.system_controller.resume_music
}
func = handler_map.get(action)
if func:
return {"success": True, "message": func()[1], "task": task}
else:
msg = f"不支持的音乐操作: {action}"
logger.warning(msg)
return {"success": False, "message": msg, "task": task}
elif task_type == "file":
action = parameters.get("action")
path = parameters.get("path", "")
content = parameters.get("content", "")
if action == "create":
res = self.system_controller.create_file(path, content)
elif action == "read":
res = self.system_controller.read_file(path)
elif action == "write":
res = self.system_controller.write_file(path, content)
else:
msg = f"不支持的文件操作: {action}"
logger.warning(msg)
return {"success": False, "message": msg, "task": task}
return {"success": res[0], "message": res[1], "task": task}
elif task_type == "text" and self.gpt_assistant:
action = parameters.get("action")
prompt = parameters.get("prompt", "")
if action == "generate":
content = self.gpt_assistant.generate_text(prompt)
elif action == "summarize":
content = self.gpt_assistant.summarize_text(prompt)
elif action == "translate":
target_lang = parameters.get("target_language", "英文")
content = self.gpt_assistant.translate_text(prompt, target_lang)
else:
msg = f"不支持的文本操作: {action}"
logger.warning(msg)
return {"success": False, "message": msg, "task": task}
return {"success": True, "message": content, "task": task}
elif task_type == "system":
action = parameters.get("action")
if action == "info":
res = self.system_controller.get_system_info()
elif action == "open_app":
app_name = parameters.get("app_name", "")
res = self.system_controller.open_application(app_name)
elif action == "reminder":
msg_text = parameters.get("message", "")
delay = parameters.get("delay_minutes", 5)
res = self.system_controller.set_reminder(msg_text, delay)
else:
msg = f"不支持的系统操作: {action}"
logger.warning(msg)
return {"success": False, "message": msg, "task": task}
return {"success": res[0], "message": res[1], "task": task}
else:
msg = f"不支持的任务类型: {task_type}"
logger.warning(msg)
return {"success": False, "message": msg, "task": task}
except Exception as e:
logger.exception("💥 执行单个任务时发生异常")
return {
"success": False,
"message": f"任务执行异常: {str(e)}",
"task": task
}
# =============================
# 🧪 测试代码
# =============================
if __name__ == "__main__":
# 初始化全局日志
if not logging.getLogger().handlers:
setup_logger(name="ai_assistant", log_dir="logs")
logger.info("🧪 开始测试 SystemController 和 TaskOrchestrator...")
controller = SystemController()
# 测试音乐播放
logger.info("🎵 测试音乐播放...")
success, msg = controller.play_music()
logger.info(f"播放结果: {success} | {msg}")
# 测试文件操作
test_file = "test/test_demo.txt"
logger.info("📂 测试文件创建...")
success, msg = controller.create_file(test_file, "这是由 AI 助手创建的测试文件。\n时间: " + str(datetime.now()))
logger.info(f"创建结果: {success} | {msg}")
logger.info("📖 测试文件读取...")
success, content = controller.read_file(test_file)
logger.info(f"读取结果: {success}")
if success:
log_var("文件内容预览", content[:100])
# 测试应用打开
logger.info("🖥️ 测试打开记事本...")
success, msg = controller.open_application("记事本")
logger.info(f"打开结果: {success} | {msg}")
# 测试系统信息
logger.info("📊 获取系统信息...")
success, info = controller.get_system_info()
if success:
for k, v in info.items():
log_var("系统." + k, v)
# 测试提醒
logger.info("⏰ 设置一个 1 分钟后的提醒...")
success, msg = controller.set_reminder("这是测试提醒!", 1)
logger.info(f"提醒设置结果: {success} | {msg}")
# 模拟运行一段时间的任务调度
for _ in range(3):
controller.run_scheduled_tasks()
time.sleep(1)
logger.info("🎉 系统控制模块测试完成")
"""
【语音合成模块】Text-to-Speech (TTS)
将文本转换为语音输出,支持中断、队列和多语音切换
"""
import pyttsx3
import threading
import queue
import time
import logging
# --- 导入配置 ---
from database import config
# --- 导入日志工具 ---
from Progress.utils.logger_utils import log_time, log_step, log_var, log_call
from Progress.utils.logger_config import setup_logger
# --- 配置参数 ---
TTS_RATE = config.rate # 语速
TTS_VOLUME = config.volume # 音量
# --- 初始化日志器 ---
logger = logging.getLogger(__name__)
class TTSEngine:
"""
基于 pyttsx3 的语音合成引擎
支持队列管理、语音控制、异步播放
"""
def __init__(self):
self.engine = None
self.is_speaking = False
self.speech_queue = queue.Queue()
self.speech_thread = None
self.stop_speaking = False
self._initialize_engine()
@log_step("初始化语音合成引擎")
@log_time
def _initialize_engine(self):
"""初始化 pyttsx3 引擎并设置语音参数"""
try:
self.engine = pyttsx3.init()
# 设置基础属性
self.engine.setProperty('rate', TTS_RATE)
self.engine.setProperty('volume', TTS_VOLUME)
log_var("TTS.语速", TTS_RATE)
log_var("TTS.音量", TTS_VOLUME)
# 尝试选择中文语音
voices = self.engine.getProperty('voices')
log_var("可用语音数量", len(voices))
selected_voice = None
for voice in voices:
if any(kw in voice.name.lower() for kw in ['chinese', 'mandarin', 'zh']):
selected_voice = voice
break
if selected_voice:
self.engine.setProperty('voice', selected_voice.id)
log_call(f"已选择中文语音: {selected_voice.name}")
elif voices:
fallback_voice = voices[0]
self.engine.setProperty('voice', fallback_voice.id)
log_call(f"未找到中文语音,回退到: {fallback_voice.name}")
else:
logger.warning("系统无可用语音")
logger.info("✅ 语音合成引擎初始化成功")
except Exception as e:
logger.exception("❌ 语音合成引擎初始化失败")
self.engine = None
@log_time
def speak(self, text, interrupt=True):
"""
主接口:语音播报文本(线程安全,支持队列)
:param text: 要朗读的文本
:param interrupt: 是否打断当前正在播放的内容
:return: 是否成功提交任务
"""
if not self.is_available():
logger.error("🔊 语音引擎不可用,跳过播报")
return False
if not text or not text.strip():
log_var("空输入被忽略", repr(text))
return False
cleaned_text = text.strip()
log_var("准备播报", cleaned_text)
try:
if interrupt:
self.stop_current_speech()
log_call("已中断当前语音")
self.speech_queue.put(cleaned_text)
if not self.speech_thread or not self.speech_thread.is_alive():
self.speech_thread = threading.Thread(target=self._speech_worker, name="TTSWorker")
self.speech_thread.daemon = True
self.speech_thread.start()
log_call("启动语音工作线程")
return True
except Exception as e:
logger.exception("❌ 提交语音任务时发生异常")
return False
def speak_async(self, text):
"""便捷方法:异步直接播放(不走队列)"""
thread = threading.Thread(target=self._speak_sync, args=(text,), name="TTSAsync")
thread.daemon = True
thread.start()
def _speak_sync(self, text):
"""同步播放单条语音(用于异步调用)"""
if not self.is_available() or not text.strip():
return
try:
log_call(f"异步播放: {text[:30]}...")
self.is_speaking = True
self.engine.say(text)
self.engine.runAndWait()
self.is_speaking = False
except Exception as e:
logger.exception("❌ 同步语音播放异常")
self.is_speaking = False
@log_time
def _speech_worker(self):
"""后台工作线程:从队列中取出文本并播放"""
logger.debug("🎧 语音工作线程已启动")
while not self.stop_speaking:
try:
text = self.speech_queue.get(timeout=1.0)
if text is None: # 接收到停止信号
break
log_var("正在播报", text)
self.is_speaking = True
self.engine.say(text)
self.engine.runAndWait()
self.is_speaking = False
self.speech_queue.task_done()
except queue.Empty:
continue
except Exception as e:
logger.exception("❌ 语音工作线程运行出错")
self.is_speaking = False
self.speech_queue.task_done()
logger.debug("🎧 语音工作线程已退出")
@log_time
def stop_current_speech(self):
"""停止当前正在播放的语音,并清空待播队列"""
log_call("请求停止当前语音")
try:
if self.is_speaking and self.engine:
self.engine.stop()
self.is_speaking = False
logger.info("🛑 已停止当前语音播放")
# 清空队列
cleared = 0
while not self.speech_queue.empty():
try:
self.speech_queue.get_nowait()
self.speech_queue.task_done()
cleared += 1
except queue.Empty:
break
if cleared > 0:
log_var("清空待播队列", f"移除了 {cleared} 条消息")
except Exception as e:
logger.exception("❌ 停止语音播放失败")
def pause_speech(self):
"""暂停语音(等同于 stop)"""
self.stop_current_speech()
@log_time
def set_rate(self, rate):
"""设置语速"""
log_var("TTS.设置语速", rate)
try:
if self.engine:
self.engine.setProperty('rate', rate)
return True
except Exception as e:
logger.exception("❌ 设置语速失败")
return False
@log_time
def set_volume(self, volume):
"""设置音量"""
log_var("TTS.设置音量", volume)
try:
if self.engine:
self.engine.setProperty('volume', volume)
return True
except Exception as e:
logger.exception("❌ 设置音量失败")
return False
@log_time
def get_available_voices(self):
"""获取所有可用语音列表"""
try:
if self.engine:
voices = self.engine.getProperty('voices')
voice_list = [{"id": v.id, "name": v.name} for v in voices]
log_var("TTS.可用语音", voice_list)
return voice_list
return []
except Exception as e:
logger.exception("❌ 获取语音列表失败")
return []
@log_time
def set_voice(self, voice_id):
"""切换语音"""
log_var("TTS.切换语音", voice_id)
try:
if self.engine:
self.engine.setProperty('voice', voice_id)
logger.info(f"🗣️ 已切换语音: {voice_id}")
return True
except Exception as e:
logger.exception("❌ 切换语音失败")
return False
def is_available(self):
"""检查引擎是否可用"""
available = self.engine is not None
log_var("TTS.可用状态", available)
return available
@log_step("清理 TTS 资源")
@log_time
def cleanup(self):
"""释放资源:停止播放、关闭引擎"""
log_call("开始清理 TTS 模块")
self.stop_speaking = True
self.stop_current_speech()
if self.speech_thread and self.speech_thread.is_alive():
logger.debug("等待语音线程结束...")
self.speech_thread.join(timeout=2)
if self.speech_thread.is_alive():
logger.warning("⚠️ 语音线程未能正常退出")
try:
if self.engine:
self.engine.stop()
logger.info("🧹 TTS 引擎已停止")
except Exception as e:
logger.exception("❌ 清理 TTS 引擎时出错")
# =============================
# 🧪 测试代码
# =============================
if __name__ == "__main__":
# 初始化全局日志
if not logging.getLogger().handlers:
setup_logger(name="ai_assistant", log_dir="logs")
logger.info("🧪 开始测试 TTSEngine...")
tts = TTSEngine()
if tts.is_available():
logger.info("✅ 语音引擎可用,开始功能测试")
# 测试基础语音
tts.speak("你好,我是语音助手,正在测试语音合成功能。")
time.sleep(2)
# 测试异步语音
tts.speak_async("这是异步语音播报,不会阻塞主线程。")
time.sleep(1)
# 测试语音列表
voices = tts.get_available_voices()
logger.info(f"🔍 发现 {len(voices)} 个可用语音")
for i, v in enumerate(voices):
logger.debug(f" [{i}] {v['name']} -> {v['id'][-10:]}")
# 清理资源
tts.cleanup()
logger.info("🎉 测试完成,资源已清理")
else:
logger.error("🔴 语音引擎初始化失败,请检查系统语音支持")
还有这三个文件,把五个文件统一起来修正问题