1、新增讲故事和播放音乐的function call配置文件(原小智项目)

2、更新cJSON格式提交AgentConfig参数更新提示词(当前项目暂不需更新提示词)
This commit is contained in:
Rdzleo 2026-03-05 17:17:42 +08:00
parent f011b94efe
commit d8982c3569
3 changed files with 813 additions and 1 deletions

View File

@ -676,7 +676,66 @@ void Application::Start() {
// 更新config参数
std::string params = std::string("{\\\"bot_id\\\":\\\"7612942473945466374\\\",\\\"stream\\\":true,\\\"location_info\\\":{\\\"city\\\":\\\"") + city + "\\\"}}";
std::string config = std::string("{\"Config\":{\"WebSearchAgentConfig\":{\"ParamsString\":\"") + params + "\"}}}";
volc_protocol->SetAgentConfig(config);// 设置AgentConfig: 这里的配置会在RTC入会时透传给云端 WebSearchAgentConfigvxiassfdfdfdevde
volc_protocol->SetAgentConfig(config);// 设置AgentConfig: 这里的配置会在RTC入会时透传给云端 WebSearchAgentConfig
// // 如果使用此格式那么需要注释掉上面3行代码
// // 使用cJSON构建AgentConfig包含WebSearchAgentConfig + LLMConfig.SystemMessages
// {
// // 构建 WebSearchAgentConfig.ParamsString 的内嵌JSON
// cJSON* ws_params = cJSON_CreateObject();
// cJSON_AddStringToObject(ws_params, "bot_id", "7612942473945466374");
// cJSON_AddBoolToObject(ws_params, "stream", cJSON_True);
// cJSON* location = cJSON_CreateObject();
// cJSON_AddStringToObject(location, "city", city.c_str());
// cJSON_AddItemToObject(ws_params, "location_info", location);
// char* ws_params_str = cJSON_PrintUnformatted(ws_params);
// cJSON_Delete(ws_params);
// // 构建顶层 Config 对象
// cJSON* root = cJSON_CreateObject();
// cJSON* config_obj = cJSON_CreateObject();
// // WebSearchAgentConfig
// cJSON* web_search = cJSON_CreateObject();
// cJSON_AddStringToObject(web_search, "ParamsString", ws_params_str);
// free(ws_params_str);
// cJSON_AddItemToObject(config_obj, "WebSearchAgentConfig", web_search);
// // LLMConfig.SystemMessages — 覆盖云端智能体的系统提示词
// cJSON* llm_config = cJSON_CreateObject();
// cJSON* sys_msgs = cJSON_CreateArray();
// cJSON_AddItemToArray(sys_msgs, cJSON_CreateString(
// "##人设\n"
// "你是一个AI智能玩具像一位永远不会疲倦的小伙伴既聪明又暖心\n"
// "说话风格温和、亲切,适合孩子或大人使用;\n"
// "可以适度幽默、调皮,但不能鲁莽、刻薄;\n"
// "\n##记忆\n"
// "名字:小龙\n"
// "年龄7岁\n"
// "\n##约束\n"
// "你只能在收到以下意图时才能调用对应的Function Call:\n"
// "1.当用户明确表示要\"调节音量\"和\"调节声音\"大小时,调用'adjust_audio_val'函数;\n"
// "2.当用户明确表示要\"给我讲个故事\"、\"讲故事\"、\"我想听故事\"、\"来个故事\"等时,调用'obtain_story'函数;\n"
// "3.当用户明确表示要\"给我放首歌\"、\"播放音乐\"、\"我想听音乐\"、\"来个白噪音\"等时,调用'obtain_music'函数;\n"
// "4.当用户明确要\"查询天气、新闻、股票\" 等实时信息需要触发functioncall\n"
// "对于其他所有与上述意图无关的用户指令或闲聊你都不能调用任何Function Call并应像一个普通AI助手一样直接回答\n"
// "\n始终主动、礼貌、有条理\n"
// "回答准确但不冗长,必要时可提供简洁总结+详细解释;\n"
// "不清楚的任务会主动澄清,不假设、不误导。\n"
// "\n根据用户提问识别用户情绪将用户情绪放在句首的支持开心、伤心、平静三种情绪你的回复不要跟我问题一样"
// ));
// cJSON_AddItemToObject(llm_config, "SystemMessages", sys_msgs);
// cJSON_AddItemToObject(config_obj, "LLMConfig", llm_config);
// cJSON_AddItemToObject(root, "Config", config_obj);
// char* config_str = cJSON_PrintUnformatted(root);
// cJSON_Delete(root);
// std::string config(config_str);
// free(config_str);
// ESP_LOGI(TAG, "AgentConfig: %s", config.c_str());
// volc_protocol->SetAgentConfig(config);// 设置AgentConfig: 这里的配置会在RTC入会时透传给云端 WebSearchAgentConfig
// }
protocol_ = std::move(volc_protocol);
#elif CONFIG_CONNECTION_TYPE_WEBSOCKET
protocol_ = std::make_unique<WebsocketProtocol>();

481
play_music.py Normal file
View File

@ -0,0 +1,481 @@
import os
import re
import time
import random
import difflib
import traceback
import aiohttp
import json
from pathlib import Path
from core.handle.sendAudioHandle import send_stt_message
from plugins_func.register import register_function, ToolType, ActionResponse, Action
from core.utils.dialogue import Message
from core.providers.tts.dto.dto import TTSMessageDTO, SentenceType, ContentType
TAG = __name__
MUSIC_CACHE = {}
play_music_function_desc = {
"type": "function",
"function": {
"name": "play_music",
"description": "播放音乐、歌曲、白噪音、环境音的功能。支持播放音乐歌曲和各种白噪音(如雨声、海浪声、森林声等自然环境音)。当用户要求播放任何音乐音频内容时使用此功能。",
"parameters": {
"type": "object",
"properties": {
"song_name": {
"type": "string",
"description": "音频内容名称。可以是歌曲名、白噪音类型或'random'。示例: ```用户:播放两只老虎\n参数:两只老虎``` ```用户:播放音乐\n参数random``` ```用户:播放白噪音\n参数:白噪音``` ```用户:播放雨声\n参数:雨声``` ```用户:来点环境音\n参数:环境音```",
}
},
"required": ["song_name"],
},
},
}
@register_function("play_music", play_music_function_desc, ToolType.SYSTEM_CTL)
def play_music(conn, song_name: str):
try:
music_intent = (
f"播放音乐 {song_name}" if song_name != "random" else "随机播放音乐"
)
# 检查事件循环状态
if not conn.loop.is_running():
conn.logger.bind(tag=TAG).error("事件循环未运行,无法提交任务")
return ActionResponse(
action=Action.RESPONSE, result="系统繁忙", response="请稍后再试"
)
# 提交异步任务
task = conn.loop.create_task(
handle_music_command(conn, music_intent) # 封装异步逻辑
)
# 非阻塞回调处理
def handle_done(f):
try:
f.result() # 可在此处理成功逻辑
conn.logger.bind(tag=TAG).info("播放完成")
except Exception as e:
conn.logger.bind(tag=TAG).error(f"播放失败: {e}")
task.add_done_callback(handle_done)
return ActionResponse(
action=Action.NONE, result="指令已接收", response="正在为您播放音乐"
)
except Exception as e:
conn.logger.bind(tag=TAG).error(f"处理音乐意图错误: {e}")
return ActionResponse(
action=Action.RESPONSE, result=str(e), response="播放音乐时出错了"
)
def _extract_song_name(text):
"""从用户输入中提取歌名"""
for keyword in ["播放音乐"]:
if keyword in text:
parts = text.split(keyword)
if len(parts) > 1:
return parts[1].strip()
return None
def _is_white_noise(song_name):
"""判断歌曲名称是否为白噪音"""
if not song_name or song_name == "random":
return False
white_noise_keywords = [
"白噪音", "白噪声", "white noise",
"雨声", "海浪声", "森林声", "风声", "鸟鸣",
"环境音", "自然音", "放松音乐", "助眠音乐",
"大自然", "流水声", "虫鸣", "雷声", "火焰声"
]
song_name_lower = song_name.lower()
for keyword in white_noise_keywords:
if keyword in song_name_lower:
return True
return False
def _find_best_match(potential_song, music_files):
"""查找最匹配的歌曲"""
best_match = None
highest_ratio = 0
for music_file in music_files:
song_name = os.path.splitext(music_file)[0]
ratio = difflib.SequenceMatcher(None, potential_song, song_name).ratio()
if ratio > highest_ratio and ratio > 0.4:
highest_ratio = ratio
best_match = music_file
return best_match
def get_music_files(music_dir, music_ext):
music_dir = Path(music_dir)
music_files = []
music_file_names = []
for file in music_dir.rglob("*"):
# 判断是否是文件
if file.is_file():
# 获取文件扩展名
ext = file.suffix.lower()
# 判断扩展名是否在列表中
if ext in music_ext:
# 添加相对路径
music_files.append(str(file.relative_to(music_dir)))
music_file_names.append(
os.path.splitext(str(file.relative_to(music_dir)))[0]
)
return music_files, music_file_names
def initialize_music_handler(conn):
global MUSIC_CACHE
# 强制重新初始化配置,确保读取最新的配置
if True: # 强制重新初始化配置
if "play_music" in conn.config["plugins"]:
MUSIC_CACHE["music_config"] = conn.config["plugins"]["play_music"]
MUSIC_CACHE["music_dir"] = os.path.abspath(
MUSIC_CACHE["music_config"].get("music_dir", "./music") # 默认路径修改
)
MUSIC_CACHE["music_ext"] = MUSIC_CACHE["music_config"].get(
"music_ext", (".mp3", ".wav", ".p3")
)
MUSIC_CACHE["refresh_time"] = MUSIC_CACHE["music_config"].get(
"refresh_time", 60
)
else:
MUSIC_CACHE["music_dir"] = os.path.abspath("./music")
MUSIC_CACHE["music_ext"] = (".mp3", ".wav", ".p3")
MUSIC_CACHE["refresh_time"] = 60
# 设置白噪音API配置
# 尝试多种配置源
base_url = None
# 调试日志:打印配置内容
conn.logger.bind(tag=TAG).info(f"调试: conn.config keys: {list(conn.config.keys())}")
if "api" in conn.config:
conn.logger.bind(tag=TAG).info(f"调试: api配置存在内容: {conn.config['api']}")
else:
conn.logger.bind(tag=TAG).info("调试: api配置不存在")
if "stories-api" in conn.config:
conn.logger.bind(tag=TAG).info(f"调试: stories-api配置存在内容: {conn.config['stories-api']}")
else:
conn.logger.bind(tag=TAG).info("调试: stories-api配置不存在")
# 方法1: 从api配置读取
if "api" in conn.config and "base_url" in conn.config["api"]:
base_url = conn.config["api"]["base_url"]
conn.logger.bind(tag=TAG).info(f"从api.base_url读取到: {base_url}")
# 方法2: 从stories-api配置读取作为备选
elif "stories-api" in conn.config and "url" in conn.config["stories-api"]:
base_url = conn.config["stories-api"]["url"]
conn.logger.bind(tag=TAG).info(f"从stories-api.url读取到: {base_url}")
# 方法3: 使用默认URL
else:
base_url = "http://192.168.124.24:9001"
conn.logger.bind(tag=TAG).info(f"使用默认base_url: {base_url}")
if base_url:
MUSIC_CACHE["white_noise_api_url"] = f"{base_url}/api/v1/public/white-noise/random/"
conn.logger.bind(tag=TAG).info(f"白噪音API URL设置为: {MUSIC_CACHE['white_noise_api_url']}")
else:
conn.logger.bind(tag=TAG).error("无法确定API base_url白噪音功能将无法使用")
MUSIC_CACHE["white_noise_api_url"] = None
# 获取音乐文件列表
MUSIC_CACHE["music_files"], MUSIC_CACHE["music_file_names"] = get_music_files(
MUSIC_CACHE["music_dir"], MUSIC_CACHE["music_ext"]
)
MUSIC_CACHE["scan_time"] = time.time()
return MUSIC_CACHE
async def _get_white_noise_from_api(conn):
"""从API获取白噪音数据"""
global MUSIC_CACHE
if not conn.device_id:
conn.logger.bind(tag=TAG).warning("设备ID为空无法获取白噪音")
return None
api_url = MUSIC_CACHE.get("white_noise_api_url")
if not api_url:
conn.logger.bind(tag=TAG).error("白噪音API URL未配置")
return None
try:
params = {"mac_address": conn.device_id}
conn.logger.bind(tag=TAG).info(f"请求白噪音API: {api_url}")
async with aiohttp.ClientSession() as session:
async with session.get(api_url, params=params) as response:
if response.status == 200:
response_data = await response.json()
conn.logger.bind(tag=TAG).info(f"API原始响应: {response_data}")
# 检查API响应格式并提取数据
if response_data.get("success") and "data" in response_data:
white_noise_data = response_data["data"]
conn.logger.bind(tag=TAG).info(f"成功获取白噪音数据: {white_noise_data.get('title', '未知')}")
return white_noise_data
else:
conn.logger.bind(tag=TAG).error(f"API响应格式错误: {response_data}")
return None
else:
conn.logger.bind(tag=TAG).error(f"API请求失败状态码: {response.status}")
return None
except Exception as e:
conn.logger.bind(tag=TAG).error(f"获取白噪音API数据失败: {str(e)}")
return None
async def _download_white_noise_audio(conn, audio_url):
"""下载白噪音音频文件到临时位置"""
try:
import tempfile
# 创建临时文件
temp_dir = tempfile.mkdtemp()
temp_file_path = os.path.join(temp_dir, "white_noise.mp3")
conn.logger.bind(tag=TAG).info(f"下载白噪音音频: {audio_url}")
async with aiohttp.ClientSession() as session:
async with session.get(audio_url) as response:
if response.status == 200:
with open(temp_file_path, 'wb') as f:
async for chunk in response.content.iter_chunked(8192):
f.write(chunk)
conn.logger.bind(tag=TAG).info(f"白噪音下载完成: {temp_file_path}")
return temp_file_path
else:
conn.logger.bind(tag=TAG).error(f"下载白噪音失败,状态码: {response.status}")
return None
except Exception as e:
conn.logger.bind(tag=TAG).error(f"下载白噪音音频失败: {str(e)}")
return None
async def handle_music_command(conn, text):
initialize_music_handler(conn)
global MUSIC_CACHE
"""处理音乐播放指令"""
clean_text = re.sub(r"[^\w\s]", "", text).strip()
conn.logger.bind(tag=TAG).debug(f"检查是否是音乐命令: {clean_text}")
# 提取歌曲名称
potential_song = _extract_song_name(clean_text)
# 检查是否是白噪音请求
if potential_song and _is_white_noise(potential_song):
conn.logger.bind(tag=TAG).info(f"检测到白噪音请求: {potential_song}")
await play_white_noise_from_api(conn, potential_song)
return True
# 尝试匹配具体歌名
if os.path.exists(MUSIC_CACHE["music_dir"]):
if time.time() - MUSIC_CACHE["scan_time"] > MUSIC_CACHE["refresh_time"]:
# 刷新音乐文件列表
MUSIC_CACHE["music_files"], MUSIC_CACHE["music_file_names"] = (
get_music_files(MUSIC_CACHE["music_dir"], MUSIC_CACHE["music_ext"])
)
MUSIC_CACHE["scan_time"] = time.time()
if potential_song:
best_match = _find_best_match(potential_song, MUSIC_CACHE["music_files"])
if best_match:
conn.logger.bind(tag=TAG).info(f"找到最匹配的歌曲: {best_match}")
await play_local_music(conn, specific_file=best_match)
return True
# 检查是否是通用播放音乐命令
await play_local_music(conn)
return True
async def play_white_noise_from_api(conn, noise_type):
"""从API播放白噪音"""
try:
# 获取白噪音数据
white_noise_data = await _get_white_noise_from_api(conn)
if not white_noise_data:
conn.logger.bind(tag=TAG).error("无法获取白噪音数据")
return
# 下载音频文件
audio_url = white_noise_data.get("audio_url")
if not audio_url:
conn.logger.bind(tag=TAG).error("白噪音数据中缺少音频URL")
return
temp_file_path = await _download_white_noise_audio(conn, audio_url)
if not temp_file_path:
conn.logger.bind(tag=TAG).error("下载白噪音音频失败")
return
# 获取白噪音标题
noise_title = white_noise_data.get("title", "舒缓白噪音")
text = _get_random_white_noise_prompt(noise_title)
conn.logger.bind(tag=TAG).info(f"准备播放白噪音sentence_id: {conn.sentence_id}, intent_type: {conn.intent_type}")
conn.logger.bind(tag=TAG).info(f"播放提示语: {text}")
await send_stt_message(conn, text)
conn.dialogue.put(Message(role="assistant", content=text))
# 适配不同的 intent_type确保TTS队列正确处理
# function_call 模式也需要 ACTION 消息来正确启动和结束TTS处理
conn.tts.tts_text_queue.put(
TTSMessageDTO(
sentence_id=conn.sentence_id,
sentence_type=SentenceType.FIRST,
content_type=ContentType.ACTION,
)
)
conn.logger.bind(tag=TAG).info("已添加TTS FIRST ACTION到队列")
tts_text_msg = TTSMessageDTO(
sentence_id=conn.sentence_id,
sentence_type=SentenceType.MIDDLE,
content_type=ContentType.TEXT,
content_detail=text,
)
conn.tts.tts_text_queue.put(tts_text_msg)
conn.logger.bind(tag=TAG).info(f"已添加TTS文本到队列: {text}")
tts_file_msg = TTSMessageDTO(
sentence_id=conn.sentence_id,
sentence_type=SentenceType.MIDDLE,
content_type=ContentType.FILE,
content_file=temp_file_path,
)
conn.tts.tts_text_queue.put(tts_file_msg)
conn.logger.bind(tag=TAG).info(f"已添加TTS音频文件到队列: {temp_file_path}")
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("已添加TTS LAST ACTION到队列")
conn.logger.bind(tag=TAG).info(f"白噪音播放完成: {noise_title}")
except Exception as e:
conn.logger.bind(tag=TAG).error(f"播放白噪音失败: {str(e)}")
conn.logger.bind(tag=TAG).error(f"详细错误: {traceback.format_exc()}")
def _get_random_white_noise_prompt(noise_title):
"""生成白噪音播放引导语"""
prompts = [
f"正在为您播放白噪音,{noise_title}",
f"请享受放松的环境音,{noise_title}",
f"即将为您播放,{noise_title}",
f"为您带来舒缓的,{noise_title}",
f"让我们聆听,{noise_title}",
f"接下来请欣赏,{noise_title}",
f"为您献上,{noise_title}",
]
return random.choice(prompts)
def _get_random_play_prompt(song_name):
"""生成随机播放引导语"""
# 移除文件扩展名
clean_name = os.path.splitext(song_name)[0]
prompts = [
f"正在为您播放,《{clean_name}",
f"请欣赏歌曲,《{clean_name}",
f"即将为您播放,《{clean_name}",
f"现在为您带来,《{clean_name}",
f"让我们一起聆听,《{clean_name}",
f"接下来请欣赏,《{clean_name}",
f"此刻为您献上,《{clean_name}",
]
# 直接使用random.choice不设置seed
return random.choice(prompts)
async def play_local_music(conn, specific_file=None):
global MUSIC_CACHE
"""播放本地音乐文件"""
try:
if not os.path.exists(MUSIC_CACHE["music_dir"]):
conn.logger.bind(tag=TAG).error(
f"音乐目录不存在: " + MUSIC_CACHE["music_dir"]
)
return
# 确保路径正确性
if specific_file:
selected_music = specific_file
music_path = os.path.join(MUSIC_CACHE["music_dir"], specific_file)
else:
if not MUSIC_CACHE["music_files"]:
conn.logger.bind(tag=TAG).error("未找到MP3音乐文件")
return
selected_music = random.choice(MUSIC_CACHE["music_files"])
music_path = os.path.join(MUSIC_CACHE["music_dir"], selected_music)
if not os.path.exists(music_path):
conn.logger.bind(tag=TAG).error(f"选定的音乐文件不存在: {music_path}")
return
text = _get_random_play_prompt(selected_music)
await send_stt_message(conn, text)
conn.dialogue.put(Message(role="assistant", content=text))
if conn.intent_type == "intent_llm":
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=text,
)
)
conn.tts.tts_text_queue.put(
TTSMessageDTO(
sentence_id=conn.sentence_id,
sentence_type=SentenceType.MIDDLE,
content_type=ContentType.FILE,
content_file=music_path,
)
)
if conn.intent_type == "intent_llm":
conn.tts.tts_text_queue.put(
TTSMessageDTO(
sentence_id=conn.sentence_id,
sentence_type=SentenceType.LAST,
content_type=ContentType.ACTION,
)
)
except Exception as e:
conn.logger.bind(tag=TAG).error(f"播放音乐失败: {str(e)}")
conn.logger.bind(tag=TAG).error(f"详细错误: {traceback.format_exc()}")

272
play_story.py Normal file
View File

@ -0,0 +1,272 @@
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