482 lines
18 KiB
Python
482 lines
18 KiB
Python
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()}")
|