from config.logger import setup_logging import os import re import time import random import asyncio import difflib import traceback from pathlib import Path from core.providers.tts.dto.dto import TTSMessageDTO, MsgType from core.utils import p3 from core.handle.sendAudioHandle import send_stt_message from plugins_func.register import register_function, ToolType, ActionResponse, Action TAG = __name__ logger = setup_logging() MUSIC_CACHE = {} play_music_function_desc = { "type": "function", "function": { "name": "play_music", "description": "唱歌、听歌、播放音乐方法。比如用户说播放音乐,参数为:random,比如用户说播放两只老虎,参数为:两只老虎", "parameters": { "type": "object", "properties": { "song_name": { "type": "string", "description": "歌曲名称,如果没有指定具体歌名则为'random'", } }, "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(): logger.bind(tag=TAG).error("事件循环未运行,无法提交任务") return ActionResponse( action=Action.RESPONSE, result="系统繁忙", response="请稍后再试" ) # 提交异步任务 future = asyncio.run_coroutine_threadsafe( handle_music_command(conn, music_intent), conn.loop ) # 非阻塞回调处理 def handle_done(f): try: f.result() # 可在此处理成功逻辑 logger.bind(tag=TAG).info("播放完成") except Exception as e: logger.bind(tag=TAG).error(f"播放失败: {e}") future.add_done_callback(handle_done) return ActionResponse( action=Action.NONE, result="指令已接收", response="正在为您播放音乐" ) except Exception as e: 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 _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 MUSIC_CACHE == {}: if "music" in conn.config: MUSIC_CACHE["music_config"] = conn.config["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 # 获取音乐文件列表 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 handle_music_command(conn, text): initialize_music_handler(conn) global MUSIC_CACHE """处理音乐播放指令""" clean_text = re.sub(r"[^\w\s]", "", text).strip() logger.bind(tag=TAG).debug(f"检查是否是音乐命令: {clean_text}") # 尝试匹配具体歌名 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() potential_song = _extract_song_name(clean_text) if potential_song: best_match = _find_best_match(potential_song, MUSIC_CACHE["music_files"]) if best_match: 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_local_music(conn, specific_file=None): global MUSIC_CACHE """播放本地音乐文件""" try: if not os.path.exists(MUSIC_CACHE["music_dir"]): 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"]: 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): logger.bind(tag=TAG).error(f"选定的音乐文件不存在: {music_path}") return text = f"正在播放{selected_music}" await send_stt_message(conn, text) conn.tts_first_text_index = 0 conn.tts_last_text_index = 0 conn.llm_finish_task = True if music_path.endswith(".p3"): opus_packets, duration = p3.decode_opus_from_file(music_path) else: opus_packets, duration = conn.tts.audio_to_opus_data(music_path) conn.tts.tts_audio_queue.put( TTSMessageDTO( u_id=conn.u_id, msg_type=MsgType.TTS_TEXT_RESPONSE, content=opus_packets, tts_finish_text="", sentence_type=None, duration=0, ) ) conn.tts.tts_audio_queue.put( TTSMessageDTO( u_id=conn.u_id, msg_type=MsgType.STOP_TTS_RESPONSE, content=[], tts_finish_text="", sentence_type=None, ) ) except Exception as e: logger.bind(tag=TAG).error(f"播放音乐失败: {str(e)}") logger.bind(tag=TAG).error(f"详细错误: {traceback.format_exc()}")