273 lines
11 KiB
Python
273 lines
11 KiB
Python
from config.logger import setup_logging
|
||
import random
|
||
import uuid
|
||
import tempfile
|
||
import os
|
||
import time
|
||
import requests
|
||
from plugins_func.register import register_function, ToolType, ActionResponse, Action
|
||
from core.handle.sendAudioHandle import send_stt_message
|
||
from core.utils.dialogue import Message
|
||
from core.providers.tts.dto.dto import TTSMessageDTO, SentenceType, ContentType
|
||
from config.manage_api_client import get_device_stories
|
||
|
||
TAG = __name__
|
||
|
||
play_story_function_desc = {
|
||
"type": "function",
|
||
"function": {
|
||
"name": "play_story",
|
||
"description": "播放故事、讲故事的功能。当用户说:讲故事、播放故事、听故事、来个故事、给我讲个故事、我想听故事等时调用此功能。",
|
||
"parameters": {
|
||
"type": "object",
|
||
"properties": {
|
||
"story_name": {
|
||
"type": "string",
|
||
"description": "故事名称,如果用户没有指定具体故事名则为'random', 明确指定的时返回故事名字 示例: ```用户:讲下王子与公主的故事\n参数:王子与公主``` ```用户:讲个故事 \n参数:random ```",
|
||
}
|
||
},
|
||
"required": ["story_name"],
|
||
},
|
||
},
|
||
}
|
||
|
||
|
||
def _get_random_play_prompt(story_title):
|
||
"""生成随机播放引导语"""
|
||
prompts = [
|
||
f"正在为您播放,{story_title}",
|
||
f"请欣赏故事,{story_title}",
|
||
f"即将为您播放,{story_title}",
|
||
f"为您带来,{story_title}",
|
||
f"让我们聆听,{story_title}",
|
||
f"接下来请欣赏,{story_title}",
|
||
f"为您献上,{story_title}",
|
||
]
|
||
return random.choice(prompts)
|
||
|
||
|
||
def _download_audio_from_url(logger, audio_url: str):
|
||
"""从URL下载音频文件到临时目录"""
|
||
try:
|
||
# 创建临时文件
|
||
temp_dir = tempfile.gettempdir()
|
||
temp_filename = f"function_story_{int(time.time())}.mp3"
|
||
temp_path = os.path.join(temp_dir, temp_filename)
|
||
|
||
logger.bind(tag=TAG).info(f"开始下载音频: {audio_url} -> {temp_path}")
|
||
|
||
# 下载音频文件
|
||
response = requests.get(audio_url, stream=True, timeout=60)
|
||
response.raise_for_status()
|
||
|
||
with open(temp_path, 'wb') as f:
|
||
for chunk in response.iter_content(chunk_size=8192):
|
||
if chunk:
|
||
f.write(chunk)
|
||
|
||
# 验证文件大小
|
||
file_size = os.path.getsize(temp_path)
|
||
if file_size == 0:
|
||
logger.bind(tag=TAG).error("下载的音频文件大小为0")
|
||
os.remove(temp_path)
|
||
return None
|
||
|
||
logger.bind(tag=TAG).info(f"音频下载完成: {temp_path}, 大小: {file_size} 字节")
|
||
return temp_path
|
||
|
||
except requests.exceptions.RequestException as e:
|
||
logger.bind(tag=TAG).error(f"下载音频失败: {str(e)}")
|
||
return None
|
||
except Exception as e:
|
||
logger.bind(tag=TAG).error(f"下载音频时发生未知错误: {str(e)}")
|
||
return None
|
||
|
||
|
||
@register_function("play_story", play_story_function_desc, ToolType.SYSTEM_CTL)
|
||
def play_story(conn, story_name: str):
|
||
"""播放故事函数"""
|
||
try:
|
||
conn.logger.bind(tag=TAG).info(f"play_story 被调用: story_name={story_name}, device_id={conn.device_id}")
|
||
|
||
# 获取设备ID
|
||
device_id = conn.device_id
|
||
if not device_id:
|
||
conn.logger.bind(tag=TAG).error("设备ID不存在,无法播放故事")
|
||
return ActionResponse(
|
||
action=Action.RESPONSE,
|
||
result="设备ID不存在",
|
||
response="抱歉,无法播放故事"
|
||
)
|
||
|
||
# 检查事件循环状态
|
||
if not hasattr(conn, 'loop') or not conn.loop.is_running():
|
||
conn.logger.bind(tag=TAG).error("事件循环未运行,无法提交任务")
|
||
return ActionResponse(
|
||
action=Action.RESPONSE,
|
||
result="系统繁忙",
|
||
response="请稍后再试"
|
||
)
|
||
|
||
# 默认故事信息(作为兜底)
|
||
default_story_name = "侦探与龙:宝石的秘密"
|
||
default_audio_url = "https://toy-storage.airlabs.art/audio/volcano_tts/permanent/20250904/story_7369183431743246336_cfd71314.mp3"
|
||
default_story_title = "侦探与龙:宝石的秘密"
|
||
|
||
# 调用API获取设备故事信息
|
||
conn.logger.bind(tag=TAG).info(f"开始调用API获取设备故事,device_id: {device_id}")
|
||
stories_data = get_device_stories(device_id)
|
||
conn.logger.bind(tag=TAG).info(f"API返回的stories_data: {stories_data}")
|
||
|
||
final_story_name = default_story_name
|
||
final_audio_url = default_audio_url
|
||
final_story_title = default_story_title
|
||
|
||
if stories_data:
|
||
# 从API响应中提取故事信息
|
||
story_info = None
|
||
|
||
# 检查API返回格式:{"success": true, "data": {"title": "...", "audio_url": "..."}}
|
||
if isinstance(stories_data, dict) and stories_data.get("success") and "data" in stories_data:
|
||
data = stories_data["data"]
|
||
# 新格式:data直接包含故事信息
|
||
if isinstance(data, dict) and "title" in data and "audio_url" in data:
|
||
story_info = data
|
||
conn.logger.bind(tag=TAG).info(f"从API获取到直接故事数据: {story_info.get('title')}")
|
||
# 原格式:data包含stories数组
|
||
else:
|
||
story_list = data.get("stories", [])
|
||
if len(story_list) > 0:
|
||
# 随机选择一个故事
|
||
story_info = random.choice(story_list)
|
||
conn.logger.bind(tag=TAG).info(f"从API获取到{len(story_list)}个故事,随机选择其中一个")
|
||
# 兼容其他格式
|
||
elif isinstance(stories_data, list) and len(stories_data) > 0:
|
||
story_info = random.choice(stories_data)
|
||
elif isinstance(stories_data, dict) and 'stories' in stories_data:
|
||
story_list = stories_data['stories']
|
||
if len(story_list) > 0:
|
||
story_info = random.choice(story_list)
|
||
|
||
if story_info:
|
||
final_story_name = story_info.get("title", default_story_name)
|
||
final_audio_url = story_info.get("audio_url", default_audio_url)
|
||
final_story_title = story_info.get("title", default_story_title)
|
||
conn.logger.bind(tag=TAG).info(f"使用API获取的故事: {final_story_title}")
|
||
else:
|
||
conn.logger.bind(tag=TAG).warning("API返回数据格式不正确,使用默认故事信息")
|
||
else:
|
||
conn.logger.bind(tag=TAG).warning("无法获取设备故事信息,使用默认故事信息")
|
||
|
||
# 下载音频文件
|
||
conn.logger.bind(tag=TAG).info(f"开始下载音频文件: {final_audio_url}")
|
||
temp_file_path = _download_audio_from_url(conn.logger, final_audio_url)
|
||
|
||
if not temp_file_path:
|
||
conn.logger.bind(tag=TAG).error("下载音频文件失败")
|
||
return ActionResponse(
|
||
action=Action.RESPONSE,
|
||
result="下载失败",
|
||
response="抱歉,无法播放故事"
|
||
)
|
||
|
||
# 生成播放提示语
|
||
play_prompt = _get_random_play_prompt(final_story_title)
|
||
conn.logger.bind(tag=TAG).info(f"播放提示语: {play_prompt}")
|
||
|
||
# 发送消息到对话
|
||
import asyncio
|
||
asyncio.run_coroutine_threadsafe(
|
||
send_stt_message(conn, play_prompt),
|
||
conn.loop
|
||
)
|
||
conn.dialogue.put(Message(role="assistant", content=play_prompt))
|
||
|
||
# 生成新的sentence_id并重置TTS状态
|
||
conn.sentence_id = str(uuid.uuid4())
|
||
conn.tts.tts_audio_first_sentence = True
|
||
|
||
# 重置播放状态
|
||
conn.client_abort = False
|
||
conn.llm_finish_task = False
|
||
|
||
# 将音频加入TTS队列
|
||
conn.tts.tts_text_queue.put(
|
||
TTSMessageDTO(
|
||
sentence_id=conn.sentence_id,
|
||
sentence_type=SentenceType.FIRST,
|
||
content_type=ContentType.ACTION,
|
||
)
|
||
)
|
||
conn.tts.tts_text_queue.put(
|
||
TTSMessageDTO(
|
||
sentence_id=conn.sentence_id,
|
||
sentence_type=SentenceType.MIDDLE,
|
||
content_type=ContentType.TEXT,
|
||
content_detail=play_prompt,
|
||
)
|
||
)
|
||
conn.tts.tts_text_queue.put(
|
||
TTSMessageDTO(
|
||
sentence_id=conn.sentence_id,
|
||
sentence_type=SentenceType.MIDDLE,
|
||
content_type=ContentType.FILE,
|
||
content_file=temp_file_path,
|
||
content_detail=final_story_title,
|
||
)
|
||
)
|
||
conn.tts.tts_text_queue.put(
|
||
TTSMessageDTO(
|
||
sentence_id=conn.sentence_id,
|
||
sentence_type=SentenceType.LAST,
|
||
content_type=ContentType.ACTION,
|
||
)
|
||
)
|
||
|
||
conn.logger.bind(tag=TAG).info(f"故事播放任务已加入队列: {final_story_title}")
|
||
|
||
# 返回成功响应
|
||
return ActionResponse(
|
||
action=Action.NONE,
|
||
result="播放成功",
|
||
response=play_prompt
|
||
)
|
||
|
||
except Exception as e:
|
||
conn.logger.bind(tag=TAG).error(f"播放故事失败: {str(e)}")
|
||
import traceback
|
||
conn.logger.bind(tag=TAG).error(f"详细错误: {traceback.format_exc()}")
|
||
return ActionResponse(
|
||
action=Action.RESPONSE,
|
||
result=str(e),
|
||
response="播放故事时出错了"
|
||
)
|
||
|
||
|
||
async def handle_story_command(conn, text="播放随机故事"):
|
||
"""
|
||
向后兼容函数,供 story_handler.py 调用
|
||
将异步调用转换为同步的 play_story 函数调用
|
||
"""
|
||
try:
|
||
conn.logger.bind(tag=TAG).info(f"handle_story_command 被调用: {text}")
|
||
|
||
# 从 text 中提取故事名称
|
||
story_name = "random"
|
||
if "讲故事" in text and text != "随机播放故事":
|
||
# 提取故事名称,例如 "讲故事 小红帽" -> "小红帽"
|
||
parts = text.split("讲故事", 1)
|
||
if len(parts) > 1:
|
||
story_name = parts[1].strip()
|
||
|
||
# 调用同步的 play_story 函数
|
||
result = play_story(conn, story_name)
|
||
|
||
conn.logger.bind(tag=TAG).info(f"handle_story_command 完成: {result}")
|
||
return True
|
||
|
||
except Exception as e:
|
||
conn.logger.bind(tag=TAG).error(f"handle_story_command 失败: {str(e)}")
|
||
import traceback
|
||
conn.logger.bind(tag=TAG).error(f"详细错误: {traceback.format_exc()}")
|
||
return False
|