play_music.py 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. import os
  2. import re
  3. import time
  4. import random
  5. import difflib
  6. import traceback
  7. from pathlib import Path
  8. from core.handle.sendAudioHandle import send_stt_message
  9. from plugins_func.register import register_function, ToolType, ActionResponse, Action
  10. from core.utils.dialogue import Message
  11. from core.providers.tts.dto.dto import TTSMessageDTO, SentenceType, ContentType
  12. from typing import TYPE_CHECKING
  13. if TYPE_CHECKING:
  14. from core.connection import ConnectionHandler
  15. TAG = __name__
  16. MUSIC_CACHE = {}
  17. play_music_function_desc = {
  18. "type": "function",
  19. "function": {
  20. "name": "play_music",
  21. "description": "唱歌、听歌、播放音乐的方法。",
  22. "parameters": {
  23. "type": "object",
  24. "properties": {
  25. "song_name": {
  26. "type": "string",
  27. "description": "歌曲名称,如果用户没有指定具体歌名则为'random', 明确指定的时返回音乐的名字 示例: ```用户:播放两只老虎\n参数:两只老虎``` ```用户:播放音乐 \n参数:random ```",
  28. }
  29. },
  30. "required": ["song_name"],
  31. },
  32. },
  33. }
  34. @register_function("play_music", play_music_function_desc, ToolType.SYSTEM_CTL)
  35. def play_music(conn: "ConnectionHandler", song_name: str):
  36. try:
  37. music_intent = (
  38. f"播放音乐 {song_name}" if song_name != "random" else "随机播放音乐"
  39. )
  40. # 检查事件循环状态
  41. if not conn.loop.is_running():
  42. conn.logger.bind(tag=TAG).error("事件循环未运行,无法提交任务")
  43. return ActionResponse(
  44. action=Action.RESPONSE, result="系统繁忙", response="请稍后再试"
  45. )
  46. # 提交异步任务
  47. task = conn.loop.create_task(
  48. handle_music_command(conn, music_intent) # 封装异步逻辑
  49. )
  50. # 非阻塞回调处理
  51. def handle_done(f):
  52. try:
  53. f.result() # 可在此处理成功逻辑
  54. conn.logger.bind(tag=TAG).info("播放完成")
  55. except Exception as e:
  56. conn.logger.bind(tag=TAG).error(f"播放失败: {e}")
  57. task.add_done_callback(handle_done)
  58. return ActionResponse(
  59. action=Action.NONE, result="指令已接收", response="正在为您播放音乐"
  60. )
  61. except Exception as e:
  62. conn.logger.bind(tag=TAG).error(f"处理音乐意图错误: {e}")
  63. return ActionResponse(
  64. action=Action.RESPONSE, result=str(e), response="播放音乐时出错了"
  65. )
  66. def _extract_song_name(text):
  67. """从用户输入中提取歌名"""
  68. for keyword in ["播放音乐"]:
  69. if keyword in text:
  70. parts = text.split(keyword)
  71. if len(parts) > 1:
  72. return parts[1].strip()
  73. return None
  74. def _find_best_match(potential_song, music_files):
  75. """查找最匹配的歌曲"""
  76. best_match = None
  77. highest_ratio = 0
  78. for music_file in music_files:
  79. song_name = os.path.splitext(music_file)[0]
  80. ratio = difflib.SequenceMatcher(None, potential_song, song_name).ratio()
  81. if ratio > highest_ratio and ratio > 0.4:
  82. highest_ratio = ratio
  83. best_match = music_file
  84. return best_match
  85. def get_music_files(music_dir, music_ext):
  86. music_dir = Path(music_dir)
  87. music_files = []
  88. music_file_names = []
  89. for file in music_dir.rglob("*"):
  90. # 判断是否是文件
  91. if file.is_file():
  92. # 获取文件扩展名
  93. ext = file.suffix.lower()
  94. # 判断扩展名是否在列表中
  95. if ext in music_ext:
  96. # 添加相对路径
  97. music_files.append(str(file.relative_to(music_dir)))
  98. music_file_names.append(
  99. os.path.splitext(str(file.relative_to(music_dir)))[0]
  100. )
  101. return music_files, music_file_names
  102. def initialize_music_handler(conn: "ConnectionHandler"):
  103. global MUSIC_CACHE
  104. if MUSIC_CACHE == {}:
  105. plugins_config = conn.config.get("plugins", {})
  106. if "play_music" in plugins_config:
  107. MUSIC_CACHE["music_config"] = plugins_config["play_music"]
  108. MUSIC_CACHE["music_dir"] = os.path.abspath(
  109. MUSIC_CACHE["music_config"].get("music_dir", "./music") # 默认路径修改
  110. )
  111. MUSIC_CACHE["music_ext"] = MUSIC_CACHE["music_config"].get(
  112. "music_ext", (".mp3", ".wav", ".p3")
  113. )
  114. MUSIC_CACHE["refresh_time"] = MUSIC_CACHE["music_config"].get(
  115. "refresh_time", 60
  116. )
  117. else:
  118. MUSIC_CACHE["music_dir"] = os.path.abspath("./music")
  119. MUSIC_CACHE["music_ext"] = (".mp3", ".wav", ".p3")
  120. MUSIC_CACHE["refresh_time"] = 60
  121. # 获取音乐文件列表
  122. MUSIC_CACHE["music_files"], MUSIC_CACHE["music_file_names"] = get_music_files(
  123. MUSIC_CACHE["music_dir"], MUSIC_CACHE["music_ext"]
  124. )
  125. MUSIC_CACHE["scan_time"] = time.time()
  126. return MUSIC_CACHE
  127. async def handle_music_command(conn: "ConnectionHandler", text):
  128. initialize_music_handler(conn)
  129. global MUSIC_CACHE
  130. """处理音乐播放指令"""
  131. clean_text = re.sub(r"[^\w\s]", "", text).strip()
  132. conn.logger.bind(tag=TAG).debug(f"检查是否是音乐命令: {clean_text}")
  133. # 尝试匹配具体歌名
  134. if os.path.exists(MUSIC_CACHE["music_dir"]):
  135. if time.time() - MUSIC_CACHE["scan_time"] > MUSIC_CACHE["refresh_time"]:
  136. # 刷新音乐文件列表
  137. MUSIC_CACHE["music_files"], MUSIC_CACHE["music_file_names"] = (
  138. get_music_files(MUSIC_CACHE["music_dir"], MUSIC_CACHE["music_ext"])
  139. )
  140. MUSIC_CACHE["scan_time"] = time.time()
  141. potential_song = _extract_song_name(clean_text)
  142. if potential_song:
  143. best_match = _find_best_match(potential_song, MUSIC_CACHE["music_files"])
  144. if best_match:
  145. conn.logger.bind(tag=TAG).info(f"找到最匹配的歌曲: {best_match}")
  146. await play_local_music(conn, specific_file=best_match)
  147. return True
  148. # 检查是否是通用播放音乐命令
  149. await play_local_music(conn)
  150. return True
  151. def _get_random_play_prompt(song_name):
  152. """生成随机播放引导语"""
  153. # 移除文件扩展名
  154. clean_name = os.path.splitext(song_name)[0]
  155. prompts = [
  156. f"正在为您播放,《{clean_name}》",
  157. f"请欣赏歌曲,《{clean_name}》",
  158. f"即将为您播放,《{clean_name}》",
  159. f"现在为您带来,《{clean_name}》",
  160. f"让我们一起聆听,《{clean_name}》",
  161. f"接下来请欣赏,《{clean_name}》",
  162. f"此刻为您献上,《{clean_name}》",
  163. ]
  164. # 直接使用random.choice,不设置seed
  165. return random.choice(prompts)
  166. async def play_local_music(conn: "ConnectionHandler", specific_file=None):
  167. global MUSIC_CACHE
  168. """播放本地音乐文件"""
  169. try:
  170. if not os.path.exists(MUSIC_CACHE["music_dir"]):
  171. conn.logger.bind(tag=TAG).error(
  172. f"音乐目录不存在: " + MUSIC_CACHE["music_dir"]
  173. )
  174. return
  175. # 确保路径正确性
  176. if specific_file:
  177. selected_music = specific_file
  178. music_path = os.path.join(MUSIC_CACHE["music_dir"], specific_file)
  179. else:
  180. if not MUSIC_CACHE["music_files"]:
  181. conn.logger.bind(tag=TAG).error("未找到MP3音乐文件")
  182. return
  183. selected_music = random.choice(MUSIC_CACHE["music_files"])
  184. music_path = os.path.join(MUSIC_CACHE["music_dir"], selected_music)
  185. if not os.path.exists(music_path):
  186. conn.logger.bind(tag=TAG).error(f"选定的音乐文件不存在: {music_path}")
  187. return
  188. text = _get_random_play_prompt(selected_music)
  189. conn.dialogue.put(Message(role="assistant", content=text))
  190. if conn.intent_type == "intent_llm":
  191. conn.tts.tts_text_queue.put(
  192. TTSMessageDTO(
  193. sentence_id=conn.sentence_id,
  194. sentence_type=SentenceType.FIRST,
  195. content_type=ContentType.ACTION,
  196. )
  197. )
  198. conn.tts.tts_text_queue.put(
  199. TTSMessageDTO(
  200. sentence_id=conn.sentence_id,
  201. sentence_type=SentenceType.MIDDLE,
  202. content_type=ContentType.TEXT,
  203. content_detail=text,
  204. )
  205. )
  206. conn.tts.tts_text_queue.put(
  207. TTSMessageDTO(
  208. sentence_id=conn.sentence_id,
  209. sentence_type=SentenceType.MIDDLE,
  210. content_type=ContentType.FILE,
  211. content_file=music_path,
  212. )
  213. )
  214. if conn.intent_type == "intent_llm":
  215. conn.tts.tts_text_queue.put(
  216. TTSMessageDTO(
  217. sentence_id=conn.sentence_id,
  218. sentence_type=SentenceType.LAST,
  219. content_type=ContentType.ACTION,
  220. )
  221. )
  222. except Exception as e:
  223. conn.logger.bind(tag=TAG).error(f"播放音乐失败: {str(e)}")
  224. conn.logger.bind(tag=TAG).error(f"详细错误: {traceback.format_exc()}")