diff --git a/main/xiaozhi-server/core/connection.py b/main/xiaozhi-server/core/connection.py index f92a21cb..f3abc934 100644 --- a/main/xiaozhi-server/core/connection.py +++ b/main/xiaozhi-server/core/connection.py @@ -14,7 +14,11 @@ from config.logger import setup_logging from core.providers.tts.dto.dto import TTSMessageDTO, MsgType from core.utils.dialogue import Message, Dialogue from core.handle.textHandle import handleTextMessage -from core.utils.util import get_string_no_punctuation_or_emoji, extract_json_from_string, get_ip_info +from core.utils.util import ( + get_string_no_punctuation_or_emoji, + extract_json_from_string, + get_ip_info, +) from concurrent.futures import ThreadPoolExecutor, TimeoutError from core.handle.sendAudioHandle import sendAudioMessage from core.handle.receiveAudioHandle import handleAudioMessage @@ -32,7 +36,9 @@ class TTSException(RuntimeError): class ConnectionHandler: - def __init__(self, config: Dict[str, Any], _vad, _asr, _llm, _tts, _memory, _intent): + def __init__( + self, config: Dict[str, Any], _vad, _asr, _llm, _tts, _memory, _intent + ): self.config = config self.logger = setup_logging() self.auth = AuthMiddleware(config) @@ -104,7 +110,7 @@ class ConnectionHandler: self.is_device_verified = False # 添加设备验证状态标志 self.close_after_chat = False # 是否在聊天结束后关闭连接 self.use_function_call_mode = False - if self.config["selected_module"]["Intent"] == 'function_call': + if self.config["selected_module"]["Intent"] == "function_call": self.use_function_call_mode = True async def handle_connection(self, ws): @@ -113,7 +119,9 @@ class ConnectionHandler: self.headers = dict(ws.request.headers) # 获取客户端ip地址 self.client_ip = ws.remote_address[0] - self.logger.bind(tag=TAG).info(f"{self.client_ip} conn - Headers: {self.headers}") + self.logger.bind(tag=TAG).info( + f"{self.client_ip} conn - Headers: {self.headers}" + ) # 进行认证 await self.auth.authenticate(self.headers) @@ -124,10 +132,14 @@ class ConnectionHandler: # Load private configuration if device_id is provided bUsePrivateConfig = self.config.get("use_private_config", False) - self.logger.bind(tag=TAG).info(f"bUsePrivateConfig: {bUsePrivateConfig}, device_id: {device_id}") + self.logger.bind(tag=TAG).info( + f"bUsePrivateConfig: {bUsePrivateConfig}, device_id: {device_id}" + ) if bUsePrivateConfig and device_id: try: - self.private_config = PrivateConfig(device_id, self.config, self.auth_code_gen) + self.private_config = PrivateConfig( + device_id, self.config, self.auth_code_gen + ) await self.private_config.load_or_create() # 判断是否已经绑定 owner = self.private_config.get_owner() @@ -140,12 +152,18 @@ class ConnectionHandler: if all([llm, tts]): self.llm = llm self.tts = tts - self.logger.bind(tag=TAG).info(f"Loaded private config and instances for device {device_id}") + self.logger.bind(tag=TAG).info( + f"Loaded private config and instances for device {device_id}" + ) else: - self.logger.bind(tag=TAG).error(f"Failed to create instances for device {device_id}") + self.logger.bind(tag=TAG).error( + f"Failed to create instances for device {device_id}" + ) self.private_config = None except Exception as e: - self.logger.bind(tag=TAG).error(f"Error initializing private config: {e}") + self.logger.bind(tag=TAG).error( + f"Error initializing private config: {e}" + ) self.private_config = None raise @@ -162,7 +180,9 @@ class ConnectionHandler: # 音频播放 消化线程 self.stop_event.clear() - audio_play_priority = threading.Thread(target=self._audio_play_priority_thread, daemon=True) + audio_play_priority = threading.Thread( + target=self._audio_play_priority_thread, daemon=True + ) audio_play_priority.start() # 打开音频通道 @@ -236,7 +256,9 @@ class ConnectionHandler: def chat(self, query): if self.isNeedAuth(): self.llm_finish_task = True - future = asyncio.run_coroutine_threadsafe(self._check_and_broadcast_auth_code(), self.loop) + future = asyncio.run_coroutine_threadsafe( + self._check_and_broadcast_auth_code(), self.loop + ) future.result() return True @@ -247,13 +269,14 @@ class ConnectionHandler: try: start_time = time.time() # 使用带记忆的对话 - future = asyncio.run_coroutine_threadsafe(self.memory.query_memory(query), self.loop) + future = asyncio.run_coroutine_threadsafe( + self.memory.query_memory(query), self.loop + ) memory_str = future.result() self.logger.bind(tag=TAG).debug(f"记忆内容: {memory_str}") llm_responses = self.llm.response( - self.session_id, - self.dialogue.get_llm_dialogue_with_memory(memory_str) + self.session_id, self.dialogue.get_llm_dialogue_with_memory(memory_str) ) except Exception as e: self.logger.bind(tag=TAG).error(f"LLM 处理出错 {query}: {e}") @@ -270,20 +293,34 @@ class ConnectionHandler: break end_time = time.time() - self.logger.bind(tag=TAG).debug(f"大模型返回时间: {end_time - start_time} 秒, 生成token={content}") + self.logger.bind(tag=TAG).debug( + f"大模型返回时间: {end_time - start_time} 秒, 生成token={content}" + ) if text_index == 0: self.tts.tts_text_queue.put( - TTSMessageDTO(u_id=uuid_str, msg_type=MsgType.START_TTS_REQUEST, content='')) + TTSMessageDTO( + u_id=uuid_str, msg_type=MsgType.START_TTS_REQUEST, content="" + ) + ) self.start_tts_request_flag = True self.tts.tts_text_queue.put( - TTSMessageDTO(u_id=uuid_str, msg_type=MsgType.TTS_TEXT_REQUEST, content=content)) + TTSMessageDTO( + u_id=uuid_str, msg_type=MsgType.TTS_TEXT_REQUEST, content=content + ) + ) text_index += 1 if self.start_tts_request_flag: self.start_tts_request_flag = False - self.tts.tts_text_queue.put(TTSMessageDTO(u_id=uuid_str, msg_type=MsgType.STOP_TTS_REQUEST, content='')) + self.tts.tts_text_queue.put( + TTSMessageDTO( + u_id=uuid_str, msg_type=MsgType.STOP_TTS_REQUEST, content="" + ) + ) self.llm_finish_task = True self.dialogue.put(Message(role="assistant", content="".join(response_message))) - self.logger.bind(tag=TAG).debug(json.dumps(self.dialogue.get_llm_dialogue(), indent=4, ensure_ascii=False)) + self.logger.bind(tag=TAG).debug( + json.dumps(self.dialogue.get_llm_dialogue(), indent=4, ensure_ascii=False) + ) return True def chat_with_function_calling(self, query, tool_call=False): @@ -291,7 +328,9 @@ class ConnectionHandler: """Chat with function calling for intent detection using streaming""" if self.isNeedAuth(): self.llm_finish_task = True - future = asyncio.run_coroutine_threadsafe(self._check_and_broadcast_auth_code(), self.loop) + future = asyncio.run_coroutine_threadsafe( + self._check_and_broadcast_auth_code(), self.loop + ) future.result() return True @@ -308,7 +347,9 @@ class ConnectionHandler: start_time = time.time() # 使用带记忆的对话 - future = asyncio.run_coroutine_threadsafe(self.memory.query_memory(query), self.loop) + future = asyncio.run_coroutine_threadsafe( + self.memory.query_memory(query), self.loop + ) memory_str = future.result() # self.logger.bind(tag=TAG).info(f"对话记录: {self.dialogue.get_llm_dialogue_with_memory(memory_str)}") @@ -317,7 +358,7 @@ class ConnectionHandler: llm_responses = self.llm.response_with_functions( self.session_id, self.dialogue.get_llm_dialogue_with_memory(memory_str), - functions=functions + functions=functions, ) except Exception as e: self.logger.bind(tag=TAG).error(f"LLM 处理出错 {query}: {e}") @@ -338,7 +379,9 @@ class ConnectionHandler: for response in llm_responses: content, tools_call = response if content is not None and len(content) > 0: - if len(response_message) <= 0 and (content == "```" or "" in content): + if len(response_message) <= 0 and ( + content == "```" or "" in content + ): tool_call_flag = True if tools_call is not None: @@ -360,17 +403,33 @@ class ConnectionHandler: break end_time = time.time() - self.logger.bind(tag=TAG).debug(f"大模型返回时间: {end_time - start_time} 秒, 生成token={content}") + self.logger.bind(tag=TAG).debug( + f"大模型返回时间: {end_time - start_time} 秒, 生成token={content}" + ) if text_index == 0: self.tts.tts_text_queue.put( - TTSMessageDTO(u_id=uuid_str, msg_type=MsgType.START_TTS_REQUEST, content='')) + TTSMessageDTO( + u_id=uuid_str, + msg_type=MsgType.START_TTS_REQUEST, + content="", + ) + ) self.start_tts_request_flag = True self.tts.tts_text_queue.put( - TTSMessageDTO(u_id=uuid_str, msg_type=MsgType.TTS_TEXT_REQUEST, content=content)) + TTSMessageDTO( + u_id=uuid_str, + msg_type=MsgType.TTS_TEXT_REQUEST, + content=content, + ) + ) text_index += 1 if self.start_tts_request_flag: self.start_tts_request_flag = False - self.tts.tts_text_queue.put(TTSMessageDTO(u_id=uuid_str, msg_type=MsgType.STOP_TTS_REQUEST, content='')) + self.tts.tts_text_queue.put( + TTSMessageDTO( + u_id=uuid_str, msg_type=MsgType.STOP_TTS_REQUEST, content="" + ) + ) # 处理function call if tool_call_flag: @@ -381,7 +440,9 @@ class ConnectionHandler: try: content_arguments_json = json.loads(a) function_name = content_arguments_json["name"] - function_arguments = json.dumps(content_arguments_json["arguments"], ensure_ascii=False) + function_arguments = json.dumps( + content_arguments_json["arguments"], ensure_ascii=False + ) function_id = str(uuid.uuid4().hex) except Exception as e: bHasError = True @@ -390,26 +451,35 @@ class ConnectionHandler: bHasError = True response_message.append(content_arguments) if bHasError: - self.logger.bind(tag=TAG).error(f"function call error: {content_arguments}") + self.logger.bind(tag=TAG).error( + f"function call error: {content_arguments}" + ) else: function_arguments = json.loads(function_arguments) if not bHasError: self.logger.bind(tag=TAG).info( - f"function_name={function_name}, function_id={function_id}, function_arguments={function_arguments}") + f"function_name={function_name}, function_id={function_id}, function_arguments={function_arguments}" + ) function_call_data = { "name": function_name, "id": function_id, - "arguments": function_arguments + "arguments": function_arguments, } - result = self.func_handler.handle_llm_function_call(self, function_call_data) + result = self.func_handler.handle_llm_function_call( + self, function_call_data + ) self._handle_function_result(result, function_call_data, text_index + 1) # 存储对话内容 if len(response_message) > 0: - self.dialogue.put(Message(role="assistant", content="".join(response_message))) + self.dialogue.put( + Message(role="assistant", content="".join(response_message)) + ) self.llm_finish_task = True - self.logger.bind(tag=TAG).debug(json.dumps(self.dialogue.get_llm_dialogue(), indent=4, ensure_ascii=False)) + self.logger.bind(tag=TAG).debug( + json.dumps(self.dialogue.get_llm_dialogue(), indent=4, ensure_ascii=False) + ) return True @@ -417,7 +487,9 @@ class ConnectionHandler: if result.action == Action.RESPONSE: # 直接回复前端 text = result.response self.recode_first_last_text(text, text_index) - asyncio.run_coroutine_threadsafe(self.tts.tts_one_sentence(text), loop=self.loop) + asyncio.run_coroutine_threadsafe( + self.tts.tts_one_sentence(text), loop=self.loop + ) self.dialogue.put(Message(role="assistant", content=text)) elif result.action == Action.REQLLM: # 调用函数后再请求llm生成回复 text = result.result @@ -425,24 +497,40 @@ class ConnectionHandler: function_id = function_call_data["id"] function_name = function_call_data["name"] function_arguments = function_call_data["arguments"] - self.dialogue.put(Message(role='assistant', - tool_calls=[{"id": function_id, - "function": {"arguments": function_arguments, - "name": function_name}, - "type": 'function', - "index": 0}])) + self.dialogue.put( + Message( + role="assistant", + tool_calls=[ + { + "id": function_id, + "function": { + "arguments": function_arguments, + "name": function_name, + }, + "type": "function", + "index": 0, + } + ], + ) + ) - self.dialogue.put(Message(role="tool", tool_call_id=function_id, content=text)) + self.dialogue.put( + Message(role="tool", tool_call_id=function_id, content=text) + ) self.chat_with_function_calling(text, tool_call=True) elif result.action == Action.NOTFOUND: text = result.result self.recode_first_last_text(text, text_index) - asyncio.run_coroutine_threadsafe(self.tts.tts_one_sentence(text), loop=self.loop) + asyncio.run_coroutine_threadsafe( + self.tts.tts_one_sentence(text), loop=self.loop + ) self.dialogue.put(Message(role="assistant", content=text)) else: text = result.result self.recode_first_last_text(text, text_index) - asyncio.run_coroutine_threadsafe(self.tts.tts_one_sentence(text), loop=self.loop) + asyncio.run_coroutine_threadsafe( + self.tts.tts_one_sentence(text), loop=self.loop + ) self.dialogue.put(Message(role="assistant", content=text)) def _tts_priority_thread(self): @@ -465,15 +553,23 @@ class ConnectionHandler: tts_timeout = self.config.get("tts_timeout", 10) tts_file, text, text_index = future.result(timeout=tts_timeout) if text is None or len(text) <= 0: - self.logger.bind(tag=TAG).error(f"TTS出错:{text_index}: tts text is empty") + self.logger.bind(tag=TAG).error( + f"TTS出错:{text_index}: tts text is empty" + ) elif tts_file is None: - self.logger.bind(tag=TAG).error(f"TTS出错: file is empty: {text_index}: {text}") + self.logger.bind(tag=TAG).error( + f"TTS出错: file is empty: {text_index}: {text}" + ) else: - self.logger.bind(tag=TAG).debug(f"TTS生成:文件路径: {tts_file}") + self.logger.bind(tag=TAG).debug( + f"TTS生成:文件路径: {tts_file}" + ) if os.path.exists(tts_file): opus_datas, duration = self.tts.audio_to_opus_data(tts_file) else: - self.logger.bind(tag=TAG).error(f"TTS出错:文件不存在{tts_file}") + self.logger.bind(tag=TAG).error( + f"TTS出错:文件不存在{tts_file}" + ) except TimeoutError: self.logger.bind(tag=TAG).error("TTS超时") except Exception as e: @@ -481,16 +577,30 @@ class ConnectionHandler: if not self.client_abort: # 如果没有中途打断就发送语音 self.audio_play_queue.put((opus_datas, text, text_index)) - if self.tts.delete_audio_file and tts_file is not None and os.path.exists(tts_file): + if ( + self.tts.delete_audio_file + and tts_file is not None + and os.path.exists(tts_file) + ): os.remove(tts_file) except Exception as e: self.logger.bind(tag=TAG).error(f"TTS任务处理错误: {e}") self.clearSpeakStatus() asyncio.run_coroutine_threadsafe( - self.websocket.send(json.dumps({"type": "tts", "state": "stop", "session_id": self.session_id})), - self.loop + self.websocket.send( + json.dumps( + { + "type": "tts", + "state": "stop", + "session_id": self.session_id, + } + ) + ), + self.loop, + ) + self.logger.bind(tag=TAG).error( + f"tts_priority priority_thread: {text} {e}" ) - self.logger.bind(tag=TAG).error(f"tts_priority priority_thread: {text} {e}") def _tts_priority_thread_stream(self): while not self.stop_event.is_set(): @@ -513,8 +623,16 @@ class ConnectionHandler: except Exception as e: self.clearSpeakStatus() asyncio.run_coroutine_threadsafe( - self.websocket.send(json.dumps({"type": "tts", "state": "stop", "session_id": self.session_id})), - self.loop + self.websocket.send( + json.dumps( + { + "type": "tts", + "state": "stop", + "session_id": self.session_id, + } + ) + ), + self.loop, ) self.logger.error(f"tts_priority priority_thread: {text}{e}") @@ -523,11 +641,14 @@ class ConnectionHandler: text = None try: ttsMessageDTO = self.tts.tts_audio_queue.get() - future = asyncio.run_coroutine_threadsafe(sendAudioMessage(self, ttsMessageDTO), - self.loop) + future = asyncio.run_coroutine_threadsafe( + sendAudioMessage(self, ttsMessageDTO), self.loop + ) future.result() except Exception as e: - self.logger.bind(tag=TAG).error(f"audio_play_priority priority_thread: {text} {e}") + self.logger.bind(tag=TAG).error( + f"audio_play_priority priority_thread: {text} {e}" + ) def speak_and_play(self, text, text_index=0): if text is None or len(text) <= 0: diff --git a/main/xiaozhi-server/core/handle/sendAudioHandle.py b/main/xiaozhi-server/core/handle/sendAudioHandle.py index 51010fc5..9543509f 100644 --- a/main/xiaozhi-server/core/handle/sendAudioHandle.py +++ b/main/xiaozhi-server/core/handle/sendAudioHandle.py @@ -6,7 +6,10 @@ import asyncio import time from core.providers.tts.dto.dto import TTSMessageDTO, SentenceType, MsgType -from core.utils.util import remove_punctuation_and_length, get_string_no_punctuation_or_emoji +from core.utils.util import ( + remove_punctuation_and_length, + get_string_no_punctuation_or_emoji, +) TAG = __name__ logger = setup_logging() @@ -14,7 +17,9 @@ logger = setup_logging() async def sendAudioMessage(conn, ttsMessageDTO: TTSMessageDTO): if ttsMessageDTO.u_id != conn.u_id: - logger.bind(tag=TAG).info(f"msg id:{ttsMessageDTO.u_id},不是当前对话,当前对话id:{conn.u_id}") + logger.bind(tag=TAG).info( + f"msg id:{ttsMessageDTO.u_id},不是当前对话,当前对话id:{conn.u_id}" + ) return # 发送句子开始消息 if SentenceType.SENTENCE_START == ttsMessageDTO.sentence_type: @@ -25,7 +30,9 @@ async def sendAudioMessage(conn, ttsMessageDTO: TTSMessageDTO): original_frame_duration = 60 # 原始帧时长(毫秒) adjusted_frame_duration = int(original_frame_duration * 0.8) # 缩短20% total_frames = len(ttsMessageDTO.content) # 获取总帧数 - compensation = total_frames * (original_frame_duration - adjusted_frame_duration) / 1000 # 补偿时间(秒) + compensation = ( + total_frames * (original_frame_duration - adjusted_frame_duration) / 1000 + ) # 补偿时间(秒) start_time = time.perf_counter() play_position = 0 # 已播放时长(毫秒) @@ -55,18 +62,14 @@ async def sendAudioMessage(conn, ttsMessageDTO: TTSMessageDTO): # 发送结束消息(如果是最后一个文本) if conn.llm_finish_task and MsgType.STOP_TTS_RESPONSE == ttsMessageDTO.msg_type: - await send_tts_message(conn, 'stop', None) + await send_tts_message(conn, "stop", None) if conn.close_after_chat: await conn.close() async def send_tts_message(conn, state, text=None): """发送 TTS 状态消息""" - message = { - "type": "tts", - "state": state, - "session_id": conn.session_id - } + message = {"type": "tts", "state": state, "session_id": conn.session_id} if text is not None: message["text"] = text @@ -78,16 +81,17 @@ async def send_tts_message(conn, state, text=None): async def send_stt_message(conn, text): """发送 STT 状态消息""" stt_text = get_string_no_punctuation_or_emoji(text) - await conn.websocket.send(json.dumps({ - "type": "stt", - "text": stt_text, - "session_id": conn.session_id} - )) await conn.websocket.send( - json.dumps({ - "type": "llm", - "text": "😊", - "emotion": "happy", - "session_id": conn.session_id} - )) + json.dumps({"type": "stt", "text": stt_text, "session_id": conn.session_id}) + ) + await conn.websocket.send( + json.dumps( + { + "type": "llm", + "text": "😊", + "emotion": "happy", + "session_id": conn.session_id, + } + ) + ) await send_tts_message(conn, "start") diff --git a/main/xiaozhi-server/core/providers/tts/aliyun.py b/main/xiaozhi-server/core/providers/tts/aliyun.py index 9df50884..611f5a2c 100644 --- a/main/xiaozhi-server/core/providers/tts/aliyun.py +++ b/main/xiaozhi-server/core/providers/tts/aliyun.py @@ -24,52 +24,65 @@ class AccessToken: @staticmethod def _encode_text(text): encoded_text = parse.quote_plus(text) - return encoded_text.replace('+', '%20').replace('*', '%2A').replace('%7E', '~') - + return encoded_text.replace("+", "%20").replace("*", "%2A").replace("%7E", "~") + @staticmethod def _encode_dict(dic): keys = dic.keys() dic_sorted = [(key, dic[key]) for key in sorted(keys)] encoded_text = parse.urlencode(dic_sorted) - return encoded_text.replace('+', '%20').replace('*', '%2A').replace('%7E', '~') - + return encoded_text.replace("+", "%20").replace("*", "%2A").replace("%7E", "~") + @staticmethod def create_token(access_key_id, access_key_secret): - parameters = {'AccessKeyId': access_key_id, - 'Action': 'CreateToken', - 'Format': 'JSON', - 'RegionId': 'cn-shanghai', - 'SignatureMethod': 'HMAC-SHA1', - 'SignatureNonce': str(uuid.uuid1()), - 'SignatureVersion': '1.0', - 'Timestamp': time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), - 'Version': '2019-02-28'} + parameters = { + "AccessKeyId": access_key_id, + "Action": "CreateToken", + "Format": "JSON", + "RegionId": "cn-shanghai", + "SignatureMethod": "HMAC-SHA1", + "SignatureNonce": str(uuid.uuid1()), + "SignatureVersion": "1.0", + "Timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), + "Version": "2019-02-28", + } # 构造规范化的请求字符串 query_string = AccessToken._encode_dict(parameters) - print('规范化的请求字符串: %s' % query_string) + print("规范化的请求字符串: %s" % query_string) # 构造待签名字符串 - string_to_sign = 'GET' + '&' + AccessToken._encode_text('/') + '&' + AccessToken._encode_text(query_string) - print('待签名的字符串: %s' % string_to_sign) + string_to_sign = ( + "GET" + + "&" + + AccessToken._encode_text("/") + + "&" + + AccessToken._encode_text(query_string) + ) + print("待签名的字符串: %s" % string_to_sign) # 计算签名 - secreted_string = hmac.new(bytes(access_key_secret + '&', encoding='utf-8'), - bytes(string_to_sign, encoding='utf-8'), - hashlib.sha1).digest() + secreted_string = hmac.new( + bytes(access_key_secret + "&", encoding="utf-8"), + bytes(string_to_sign, encoding="utf-8"), + hashlib.sha1, + ).digest() signature = base64.b64encode(secreted_string) - print('签名: %s' % signature) + print("签名: %s" % signature) # 进行URL编码 signature = AccessToken._encode_text(signature) - print('URL编码后的签名: %s' % signature) + print("URL编码后的签名: %s" % signature) # 调用服务 - full_url = 'http://nls-meta.cn-shanghai.aliyuncs.com/?Signature=%s&%s' % (signature, query_string) - print('url: %s' % full_url) + full_url = "http://nls-meta.cn-shanghai.aliyuncs.com/?Signature=%s&%s" % ( + signature, + query_string, + ) + print("url: %s" % full_url) # 提交HTTP GET请求 response = requests.get(full_url) if response.ok: root_obj = response.json() - key = 'Token' + key = "Token" if key in root_obj: - token = root_obj[key]['Id'] - expire_time = root_obj[key]['ExpireTime'] + token = root_obj[key]["Id"] + expire_time = root_obj[key]["ExpireTime"] return token, expire_time print(response.text) return None, None @@ -77,22 +90,23 @@ class AccessToken: class TTSProvider(TTSProviderBase): - def __init__(self, config, delete_audio_file): super().__init__(config, delete_audio_file) - + # 新增空值判断逻辑 access_key_id = config.get("access_key_id") access_key_secret = config.get("access_key_secret") if access_key_id and access_key_secret: # 使用密钥对生成临时token - token, expire_time = AccessToken.create_token(access_key_id, access_key_secret) + token, expire_time = AccessToken.create_token( + access_key_id, access_key_secret + ) else: # 直接使用预生成的长期token token = config.get("token") expire_time = None - - print('token: %s, expire time(s): %s' % (token, expire_time)) + + print("token: %s, expire time(s): %s" % (token, expire_time)) self.appkey = config.get("appkey") self.token = token @@ -105,12 +119,13 @@ class TTSProvider(TTSProviderBase): self.host = config.get("host", "nls-gateway-cn-shanghai.aliyuncs.com") self.api_url = f"https://{self.host}/stream/v1/tts" - self.header = { - "Content-Type": "application/json" - } + self.header = {"Content-Type": "application/json"} def generate_filename(self, extension=".wav"): - return os.path.join(self.output_file, f"tts-{__name__}{datetime.now().date()}@{uuid.uuid4().hex}{extension}") + return os.path.join( + self.output_file, + f"tts-{__name__}{datetime.now().date()}@{uuid.uuid4().hex}{extension}", + ) async def text_to_speak(self, u_id, text, is_last_text=False, is_first_text=False): request_json = { @@ -122,25 +137,34 @@ class TTSProvider(TTSProviderBase): "voice": self.voice, "volume": self.volume, "speech_rate": self.speech_rate, - "pitch_rate": self.pitch_rate + "pitch_rate": self.pitch_rate, } print(self.api_url, json.dumps(request_json, ensure_ascii=False)) tmp_file = self.generate_filename() try: - resp = requests.post(self.api_url, json.dumps(request_json), headers=self.header) + resp = requests.post( + self.api_url, json.dumps(request_json), headers=self.header + ) # 检查返回请求数据的mime类型是否是audio/***,是则保存到指定路径下;返回的是binary格式的 - if resp.headers['Content-Type'].startswith('audio/'): - with open(tmp_file, 'wb') as f: + if resp.headers["Content-Type"].startswith("audio/"): + with open(tmp_file, "wb") as f: f.write(resp.content) else: - raise Exception(f"{__name__} status_code: {resp.status_code} response: {resp.content}") + raise Exception( + f"{__name__} status_code: {resp.status_code} response: {resp.content}" + ) # 使用 pydub 读取临时文件 audio = AudioSegment.from_file(tmp_file, format="wav") audio = audio.set_channels(1).set_frame_rate(16000) opus_datas = self.wav_to_opus_data_audio_raw(audio.raw_data) - yield TTSMessageDTO(u_id=u_id, msg_type=MsgType.TTS_TEXT_RESPONSE, content=opus_datas, - tts_finish_text=text, sentence_type=SentenceType.SENTENCE_START) + yield TTSMessageDTO( + u_id=u_id, + msg_type=MsgType.TTS_TEXT_RESPONSE, + content=opus_datas, + tts_finish_text=text, + sentence_type=SentenceType.SENTENCE_START, + ) # 用完后删除临时文件 try: os.remove(tmp_file) diff --git a/main/xiaozhi-server/core/providers/tts/base.py b/main/xiaozhi-server/core/providers/tts/base.py index a363c3d7..f009e4c4 100644 --- a/main/xiaozhi-server/core/providers/tts/base.py +++ b/main/xiaozhi-server/core/providers/tts/base.py @@ -35,11 +35,25 @@ class TTSProviderBase(ABC): self.stop_event = threading.Event() self.tts_text_buff = [] - self.punctuations = ("。", "?", "!", ";", ":", ".", "?", "!", ";", ":", " ", ",", ",") + self.punctuations = ( + "。", + "?", + "!", + ";", + ":", + ".", + "?", + "!", + ";", + ":", + " ", + ",", + ",", + ) self.tts_request = False self.processed_chars = 0 self.stream = False - self.last_to_opus_raw = b'' + self.last_to_opus_raw = b"" # 启动tts_text_queue监听线程 # 线程任务相关 @@ -51,7 +65,9 @@ class TTSProviderBase(ABC): async def open_audio_channels(self): # 启动tts_text_queue监听线程 - tts_priority = threading.Thread(target=self._tts_text_priority_thread, daemon=True) + tts_priority = threading.Thread( + target=self._tts_text_priority_thread, daemon=True + ) tts_priority.start() async def stop_listen_resource(self): @@ -67,15 +83,19 @@ class TTSProviderBase(ABC): def _get_segment_text(self): # 合并当前全部文本并处理未分割部分 full_text = "".join(self.tts_text_buff) - current_text = full_text[self.processed_chars:] # 从未处理的位置开始 + current_text = full_text[self.processed_chars :] # 从未处理的位置开始 last_punct_pos = -1 for punct in self.punctuations: pos = current_text.rfind(punct) - if (pos != -1 and last_punct_pos == -1) or (pos != -1 and pos < last_punct_pos): + if (pos != -1 and last_punct_pos == -1) or ( + pos != -1 and pos < last_punct_pos + ): last_punct_pos = pos if last_punct_pos != -1: - segment_text_raw = current_text[:last_punct_pos + 1] - segment_text = textUtils.get_string_no_punctuation_or_emoji(segment_text_raw) + segment_text_raw = current_text[: last_punct_pos + 1] + segment_text = textUtils.get_string_no_punctuation_or_emoji( + segment_text_raw + ) self.processed_chars += len(segment_text_raw) # 更新已处理字符位置 return segment_text else: @@ -98,11 +118,21 @@ class TTSProviderBase(ABC): async def finish_session(self, session_id): pass - async def tts_one_sentence(self,text): + async def tts_one_sentence(self, text): uuid_str = str(uuid.uuid4()).replace("-", "") - self.tts.tts_text_queue.put(TTSMessageDTO(u_id=uuid_str, msg_type=MsgType.START_TTS_REQUEST, content='')) - self.tts.tts_text_queue.put(TTSMessageDTO(u_id=uuid_str, msg_type=MsgType.TTS_TEXT_REQUEST, content=text)) - self.tts.tts_text_queue.put(TTSMessageDTO(u_id=uuid_str, msg_type=MsgType.STOP_TTS_REQUEST, content=text)) + self.tts.tts_text_queue.put( + TTSMessageDTO(u_id=uuid_str, msg_type=MsgType.START_TTS_REQUEST, content="") + ) + self.tts.tts_text_queue.put( + TTSMessageDTO( + u_id=uuid_str, msg_type=MsgType.TTS_TEXT_REQUEST, content=text + ) + ) + self.tts.tts_text_queue.put( + TTSMessageDTO( + u_id=uuid_str, msg_type=MsgType.STOP_TTS_REQUEST, content=text + ) + ) def _enable_two_way_tts(self): while not self.stop_event.is_set(): @@ -114,27 +144,36 @@ class TTSProviderBase(ABC): self.tts_request = True self.u_id = ttsMessageDTO.u_id # 开启session - future = asyncio.run_coroutine_threadsafe(self.start_session(ttsMessageDTO.u_id), loop=self.loop) + future = asyncio.run_coroutine_threadsafe( + self.start_session(ttsMessageDTO.u_id), loop=self.loop + ) future.result() # await self.start_session(ttsMessageDTO.u_id) elif self.tts_request and msg_type == MsgType.TTS_TEXT_REQUEST: future = asyncio.run_coroutine_threadsafe( - self.text_to_speak(u_id=ttsMessageDTO.u_id, text=ttsMessageDTO.content), loop=self.loop) + self.text_to_speak( + u_id=ttsMessageDTO.u_id, text=ttsMessageDTO.content + ), + loop=self.loop, + ) future.result() elif msg_type == MsgType.STOP_TTS_REQUEST: self.tts_request = False - future = asyncio.run_coroutine_threadsafe(self.finish_session(ttsMessageDTO.u_id), loop=self.loop) + future = asyncio.run_coroutine_threadsafe( + self.finish_session(ttsMessageDTO.u_id), loop=self.loop + ) future.result() - except Exception as e: logger.bind(tag=TAG).error(f"Failed to process TTS text: {e}") # 报错了。要关闭说话 self.tts_audio_queue.put( TTSMessageDTO( - u_id=self.u_id, msg_type=MsgType.STOP_TTS_RESPONSE, content=[], - tts_finish_text='', - sentence_type=None + u_id=self.u_id, + msg_type=MsgType.STOP_TTS_RESPONSE, + content=[], + tts_finish_text="", + sentence_type=None, ) ) traceback.print_exc() @@ -162,25 +201,43 @@ class TTSProviderBase(ABC): if segment_text: # 修改部分:创建协程对象 # 修改部分:创建协程对象 - tts_generator = self.text_to_speak(ttsMessageDTO.u_id, segment_text, - True if msg_type == MsgType.STOP_TTS_REQUEST else False, - True if msg_type == MsgType.START_TTS_REQUEST else False) - future = asyncio.run_coroutine_threadsafe(self.process_generator(tts_generator), self.loop) + tts_generator = self.text_to_speak( + ttsMessageDTO.u_id, + segment_text, + True if msg_type == MsgType.STOP_TTS_REQUEST else False, + ( + True + if msg_type == MsgType.START_TTS_REQUEST + else False + ), + ) + future = asyncio.run_coroutine_threadsafe( + self.process_generator(tts_generator), self.loop + ) self.active_tasks.add(future) if self.active_tasks: + async def wrap_future(future): return await asyncio.wrap_future(future) - wrapped_tasks = [wrap_future(task) for task in self.active_tasks] - done, _ = loop.run_until_complete(asyncio.wait(wrapped_tasks)) + wrapped_tasks = [ + wrap_future(task) for task in self.active_tasks + ] + done, _ = loop.run_until_complete( + asyncio.wait(wrapped_tasks) + ) self.active_tasks -= done # 发送合成结束 - self.tts_audio_queue.put(TTSMessageDTO(u_id=ttsMessageDTO.u_id, - msg_type=MsgType.STOP_TTS_RESPONSE, - content=[], - tts_finish_text='', - sentence_type=SentenceType.SENTENCE_END)) + self.tts_audio_queue.put( + TTSMessageDTO( + u_id=ttsMessageDTO.u_id, + msg_type=MsgType.STOP_TTS_RESPONSE, + content=[], + tts_finish_text="", + sentence_type=SentenceType.SENTENCE_END, + ) + ) segment_text = self._get_segment_text() if segment_text: @@ -189,25 +246,31 @@ class TTSProviderBase(ABC): ttsMessageDTO.u_id, segment_text, msg_type == MsgType.STOP_TTS_REQUEST, - msg_type == MsgType.START_TTS_REQUEST + msg_type == MsgType.START_TTS_REQUEST, ) # 提交协程到事件循环 tts_generator_future = asyncio.run_coroutine_threadsafe( - self.process_generator(tts_generator), - loop + self.process_generator(tts_generator), loop ) self.active_tasks.add(tts_generator_future) if len(self.active_tasks) >= self.max_workers: # 等待所有任务完成 try: + async def wrap_future(future): return await asyncio.wrap_future(future) - wrapped_tasks = [wrap_future(task) for task in self.active_tasks] - done, _ = loop.run_until_complete(asyncio.wait(wrapped_tasks)) + wrapped_tasks = [ + wrap_future(task) for task in self.active_tasks + ] + done, _ = loop.run_until_complete( + asyncio.wait(wrapped_tasks) + ) self.active_tasks -= done except Exception as e: - logger.bind(tag=TAG).error(f"Failed to process TTS text: {e}") + logger.bind(tag=TAG).error( + f"Failed to process TTS text: {e}" + ) traceback.print_exc() else: pass @@ -227,10 +290,14 @@ class TTSProviderBase(ABC): asyncio.run(self.text_to_speak(text, tmp_file)) if not os.path.exists(tmp_file): max_repeat_time = max_repeat_time - 1 - logger.bind(tag=TAG).error(f"语音生成失败: {text}:{tmp_file},再试{max_repeat_time}次") + logger.bind(tag=TAG).error( + f"语音生成失败: {text}:{tmp_file},再试{max_repeat_time}次" + ) if max_repeat_time > 0: - logger.bind(tag=TAG).info(f"语音生成成功: {text}:{tmp_file},重试{5 - max_repeat_time}次") + logger.bind(tag=TAG).info( + f"语音生成成功: {text}:{tmp_file},重试{5 - max_repeat_time}次" + ) return tmp_file except Exception as e: @@ -256,7 +323,7 @@ class TTSProviderBase(ABC): # 获取文件后缀名 file_type = os.path.splitext(audio_file_path)[1] if file_type: - file_type = file_type.lstrip('.') + file_type = file_type.lstrip(".") audio = AudioSegment.from_file(audio_file_path, format=file_type) # 转换为单声道/16kHz采样率/16位小端编码(确保与编码器匹配) @@ -279,11 +346,11 @@ class TTSProviderBase(ABC): # 按帧处理所有音频数据(包括最后一帧可能补零) for i in range(0, len(raw_data), frame_size * 2): # 16bit=2bytes/sample # 获取当前帧的二进制数据 - chunk = raw_data[i:i + frame_size * 2] + chunk = raw_data[i : i + frame_size * 2] # 如果最后一帧不足,补零 if len(chunk) < frame_size * 2: - chunk += b'\x00' * (frame_size * 2 - len(chunk)) + chunk += b"\x00" * (frame_size * 2 - len(chunk)) # 转换为numpy数组处理 np_frame = np.frombuffer(chunk, dtype=np.int16) @@ -295,7 +362,9 @@ class TTSProviderBase(ABC): return opus_datas, duration def get_audio_from_tts(self, data_bytes, src_rate, to_rate=16000): - tts_speech = torch.from_numpy(np.array(np.frombuffer(data_bytes, dtype=np.int16))).unsqueeze(dim=0) + tts_speech = torch.from_numpy( + np.array(np.frombuffer(data_bytes, dtype=np.int16)) + ).unsqueeze(dim=0) with io.BytesIO() as bf: torchaudio.save(bf, tts_speech, src_rate, format="wav") audio = AudioSegment.from_file(bf, format="wav") @@ -304,7 +373,7 @@ class TTSProviderBase(ABC): def wav_to_opus_data_audio_raw(self, raw_data_var, is_end=False): raw_data = self.last_to_opus_raw + raw_data_var - self.last_to_opus_raw = b'' + self.last_to_opus_raw = b"" # 初始化Opus编码器 encoder = opuslib_next.Encoder(16000, 1, opuslib_next.APPLICATION_AUDIO) @@ -316,7 +385,7 @@ class TTSProviderBase(ABC): # 按帧处理所有音频数据(包括最后一帧可能补零) for i in range(0, len(raw_data), frame_size * 2): # 16bit=2bytes/sample # 获取当前帧的二进制数据 - chunk = raw_data[i:i + frame_size * 2] + chunk = raw_data[i : i + frame_size * 2] # 如果最后一帧不足,补零 # 缓存记录一下 @@ -326,7 +395,7 @@ class TTSProviderBase(ABC): break if len(chunk) < frame_size * 2 and is_end: logger.bind(tag=TAG).info("是最后一句了,补零") - chunk += b'\x00' * (frame_size * 2 - len(chunk)) + chunk += b"\x00" * (frame_size * 2 - len(chunk)) # 转换为numpy数组处理 np_frame = np.frombuffer(chunk, dtype=np.int16) diff --git a/main/xiaozhi-server/core/providers/tts/edge.py b/main/xiaozhi-server/core/providers/tts/edge.py index a9fda52a..8ebba47a 100644 --- a/main/xiaozhi-server/core/providers/tts/edge.py +++ b/main/xiaozhi-server/core/providers/tts/edge.py @@ -13,17 +13,23 @@ from core.providers.tts.dto.dto import TTSMessageDTO, MsgType, SentenceType TAG = __name__ logger = setup_logging() + class TTSProvider(TTSProviderBase): def __init__(self, config, delete_audio_file): super().__init__(config, delete_audio_file) self.voice = config.get("voice") def generate_filename(self, extension=".mp3"): - return os.path.join(self.output_file, f"tts-{datetime.now().date()}@{uuid.uuid4().hex}{extension}") + return os.path.join( + self.output_file, + f"tts-{datetime.now().date()}@{uuid.uuid4().hex}{extension}", + ) async def text_to_speak(self, u_id, text, is_last_text=False, is_first_text=False): try: - communicate = edge_tts.Communicate(text, voice=self.voice) # Use your preferred voice + communicate = edge_tts.Communicate( + text, voice=self.voice + ) # Use your preferred voice tmp_file = self.generate_filename() await communicate.save(tmp_file) @@ -31,7 +37,13 @@ class TTSProvider(TTSProviderBase): audio = AudioSegment.from_file(tmp_file, format="mp3") audio = audio.set_channels(1).set_frame_rate(16000) opus_datas = self.wav_to_opus_data_audio_raw(audio.raw_data) - yield TTSMessageDTO(u_id=u_id, msg_type=MsgType.TTS_TEXT_RESPONSE, content=opus_datas, tts_finish_text=text,sentence_type=SentenceType.SENTENCE_START) + yield TTSMessageDTO( + u_id=u_id, + msg_type=MsgType.TTS_TEXT_RESPONSE, + content=opus_datas, + tts_finish_text=text, + sentence_type=SentenceType.SENTENCE_START, + ) # 用完后删除临时文件 try: os.remove(tmp_file) @@ -40,6 +52,10 @@ class TTSProvider(TTSProviderBase): pass except Exception as e: logger.bind(tag=TAG).error(f"TTSProvider text_to_speak error: {e}") - yield TTSMessageDTO(u_id=u_id, msg_type=MsgType.TTS_TEXT_RESPONSE, content=[], tts_finish_text=text,sentence_type=SentenceType.SENTENCE_START) - - + yield TTSMessageDTO( + u_id=u_id, + msg_type=MsgType.TTS_TEXT_RESPONSE, + content=[], + tts_finish_text=text, + sentence_type=SentenceType.SENTENCE_START, + ) diff --git a/main/xiaozhi-server/core/websocket_server.py b/main/xiaozhi-server/core/websocket_server.py index 78ac230c..b289ad92 100644 --- a/main/xiaozhi-server/core/websocket_server.py +++ b/main/xiaozhi-server/core/websocket_server.py @@ -12,50 +12,72 @@ class WebSocketServer: def __init__(self, config: dict): self.config = config self.logger = setup_logging() - self._vad, self._asr, self._llm, self._tts, self._memory, self.intent = self._create_processing_instances() + self._vad, self._asr, self._llm, self._tts, self._memory, self.intent = ( + self._create_processing_instances() + ) self.active_connections = set() # 添加全局连接记录 def _create_processing_instances(self): - memory_cls_name = self.config["selected_module"].get("Memory", "nomem") # 默认使用nomem - has_memory_cfg = self.config.get("Memory") and memory_cls_name in self.config["Memory"] + memory_cls_name = self.config["selected_module"].get( + "Memory", "nomem" + ) # 默认使用nomem + has_memory_cfg = ( + self.config.get("Memory") and memory_cls_name in self.config["Memory"] + ) memory_cfg = self.config["Memory"][memory_cls_name] if has_memory_cfg else {} """创建处理模块实例""" return ( vad.create_instance( self.config["selected_module"]["VAD"], - self.config["VAD"][self.config["selected_module"]["VAD"]] + self.config["VAD"][self.config["selected_module"]["VAD"]], ), asr.create_instance( - self.config["selected_module"]["ASR"] - if not 'type' in self.config["ASR"][self.config["selected_module"]["ASR"]] - else - self.config["ASR"][self.config["selected_module"]["ASR"]]["type"], + ( + self.config["selected_module"]["ASR"] + if not "type" + in self.config["ASR"][self.config["selected_module"]["ASR"]] + else self.config["ASR"][self.config["selected_module"]["ASR"]][ + "type" + ] + ), self.config["ASR"][self.config["selected_module"]["ASR"]], - self.config["delete_audio"] + self.config["delete_audio"], ), llm.create_instance( - self.config["selected_module"]["LLM"] - if not 'type' in self.config["LLM"][self.config["selected_module"]["LLM"]] - else - self.config["LLM"][self.config["selected_module"]["LLM"]]['type'], + ( + self.config["selected_module"]["LLM"] + if not "type" + in self.config["LLM"][self.config["selected_module"]["LLM"]] + else self.config["LLM"][self.config["selected_module"]["LLM"]][ + "type" + ] + ), self.config["LLM"][self.config["selected_module"]["LLM"]], ), tts.create_instance( - self.config["selected_module"]["TTS"] - if not 'type' in self.config["TTS"][self.config["selected_module"]["TTS"]] - else - self.config["TTS"][self.config["selected_module"]["TTS"]]["type"], + ( + self.config["selected_module"]["TTS"] + if not "type" + in self.config["TTS"][self.config["selected_module"]["TTS"]] + else self.config["TTS"][self.config["selected_module"]["TTS"]][ + "type" + ] + ), self.config["TTS"][self.config["selected_module"]["TTS"]], - self.config["delete_audio"] + self.config["delete_audio"], ), memory.create_instance(memory_cls_name, memory_cfg), intent.create_instance( - self.config["selected_module"]["Intent"] - if not 'type' in self.config["Intent"][self.config["selected_module"]["Intent"]] - else - self.config["Intent"][self.config["selected_module"]["Intent"]]["type"], - self.config["Intent"][self.config["selected_module"]["Intent"]] + ( + self.config["selected_module"]["Intent"] + if not "type" + in self.config["Intent"][self.config["selected_module"]["Intent"]] + else self.config["Intent"][ + self.config["selected_module"]["Intent"] + ]["type"] + ), + self.config["Intent"][self.config["selected_module"]["Intent"]], ), ) @@ -64,22 +86,32 @@ class WebSocketServer: host = server_config["ip"] port = server_config["port"] selected_module = self.config.get("selected_module") - self.logger.bind(tag=TAG).info(f"selected_module values: {', '.join(selected_module.values())}") + self.logger.bind(tag=TAG).info( + f"selected_module values: {', '.join(selected_module.values())}" + ) - self.logger.bind(tag=TAG).info("Server is running at ws://{}:{}", get_local_ip(), port) - self.logger.bind(tag=TAG).info("=======上面的地址是websocket协议地址,请勿用浏览器访问=======") - async with websockets.serve( - self._handle_connection, - host, - port - ): + self.logger.bind(tag=TAG).info( + "Server is running at ws://{}:{}", get_local_ip(), port + ) + self.logger.bind(tag=TAG).info( + "=======上面的地址是websocket协议地址,请勿用浏览器访问=======" + ) + async with websockets.serve(self._handle_connection, host, port): await asyncio.Future() async def _handle_connection(self, websocket): """处理新连接,每次创建独立的ConnectionHandler""" # 创建ConnectionHandler时传入当前server实例 # tts 变成链接的时候创建,避免并非问题 - handler = ConnectionHandler(self.config, self._vad, self._asr, self._llm, self._tts, self._memory, self.intent) + handler = ConnectionHandler( + self.config, + self._vad, + self._asr, + self._llm, + self._tts, + self._memory, + self.intent, + ) self.active_connections.add(handler) try: await handler.handle_connection(websocket) diff --git a/main/xiaozhi-server/plugins_func/functions/play_music.py b/main/xiaozhi-server/plugins_func/functions/play_music.py index f89ff8ef..5dd6de17 100644 --- a/main/xiaozhi-server/plugins_func/functions/play_music.py +++ b/main/xiaozhi-server/plugins_func/functions/play_music.py @@ -28,29 +28,32 @@ play_music_function_desc = { "properties": { "song_name": { "type": "string", - "description": "歌曲名称,如果没有指定具体歌名则为'random'" + "description": "歌曲名称,如果没有指定具体歌名则为'random'", } }, - "required": ["song_name"] - } - } + "required": ["song_name"], + }, + }, } -@register_function('play_music', play_music_function_desc, ToolType.SYSTEM_CTL) +@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 "随机播放音乐" + 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="请稍后再试") + return ActionResponse( + action=Action.RESPONSE, result="系统繁忙", response="请稍后再试" + ) # 提交异步任务 future = asyncio.run_coroutine_threadsafe( - handle_music_command(conn, music_intent), - conn.loop + handle_music_command(conn, music_intent), conn.loop ) # 非阻塞回调处理 @@ -63,10 +66,14 @@ def play_music(conn, song_name: str): future.add_done_callback(handle_done) - return ActionResponse(action=Action.RESPONSE, result="指令已接收", response="正在为您播放音乐") + return ActionResponse( + action=Action.RESPONSE, result="指令已接收", response="正在为您播放音乐" + ) except Exception as e: logger.bind(tag=TAG).error(f"处理音乐意图错误: {e}") - return ActionResponse(action=Action.RESPONSE, result=str(e), response="播放音乐时出错了") + return ActionResponse( + action=Action.RESPONSE, result=str(e), response="播放音乐时出错了" + ) def _extract_song_name(text): @@ -106,7 +113,9 @@ def get_music_files(music_dir, music_ext): 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]) + music_file_names.append( + os.path.splitext(str(file.relative_to(music_dir)))[0] + ) return music_files, music_file_names @@ -118,15 +127,20 @@ def initialize_music_handler(conn): 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) + 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["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 @@ -136,15 +150,16 @@ async def handle_music_command(conn, text): global MUSIC_CACHE """处理音乐播放指令""" - clean_text = re.sub(r'[^\w\s]', '', text).strip() + 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["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) @@ -192,8 +207,12 @@ async def play_local_music(conn, specific_file=None): 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 + u_id=conn.u_id, + msg_type=MsgType.TTS_TEXT_RESPONSE, + content=opus_packets, + tts_finish_text="", + sentence_type=None, + duration=0, ) )