toy-Kapi_Rtc/play_story.py
Rdzleo d8982c3569 1、新增讲故事和播放音乐的function call配置文件(原小智项目)
2、更新cJSON格式提交AgentConfig参数更新提示词(当前项目暂不需更新提示词)
2026-03-05 17:17:42 +08:00

273 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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