diff --git a/main/xiaozhi-server/app.py b/main/xiaozhi-server/app.py index 4ad57855..619fb5c7 100644 --- a/main/xiaozhi-server/app.py +++ b/main/xiaozhi-server/app.py @@ -7,6 +7,7 @@ from core.ota_server import SimpleOtaServer from core.utils.util import check_ffmpeg_installed from config.logger import setup_logging from core.utils.util import get_local_ip +from aioconsole import ainput TAG = __name__ logger = setup_logging() @@ -34,10 +35,19 @@ async def wait_for_exit() -> None: pass +async def monitor_stdin(): + """监控标准输入,消费回车键""" + while True: + await ainput() # 异步等待输入,消费回车 + + async def main(): check_ffmpeg_installed() config = load_config() + # 添加 stdin 监控任务 + stdin_task = asyncio.create_task(monitor_stdin()) + # 启动 WebSocket 服务器 ws_server = WebSocketServer(config) ws_task = asyncio.create_task(ws_server.start()) @@ -78,19 +88,22 @@ async def main(): ) try: - await wait_for_exit() # 监听退出信号 + await wait_for_exit() # 阻塞直到收到退出信号 except asyncio.CancelledError: print("任务被取消,清理资源中...") finally: + # 取消所有任务(关键修复点) + stdin_task.cancel() ws_task.cancel() if ota_task: ota_task.cancel() - try: - await ws_task - if ota_task: - await ota_task - except asyncio.CancelledError: - pass + + # 等待任务终止(必须加超时) + await asyncio.wait( + [stdin_task, ws_task, ota_task] if ota_task else [stdin_task, ws_task], + timeout=3.0, + return_when=asyncio.ALL_COMPLETED + ) print("服务器已关闭,程序退出。") diff --git a/main/xiaozhi-server/core/connection.py b/main/xiaozhi-server/core/connection.py index 58e594e2..2a85c0ad 100644 --- a/main/xiaozhi-server/core/connection.py +++ b/main/xiaozhi-server/core/connection.py @@ -1,6 +1,8 @@ import os import copy import json +import subprocess +import sys import uuid import time import queue @@ -241,6 +243,44 @@ class ConnectionHandler: elif isinstance(message, bytes): await handleAudioMessage(self, message) + async def handle_restart(self, message): + """处理服务器重启请求""" + try: + + self.logger.bind(tag=TAG).info("收到服务器重启指令,准备执行...") + + # 发送确认响应 + await self.websocket.send(json.dumps({ + "type": "server_response", + "status": "success", + "message": "服务器重启中..." + })) + + # 异步执行重启操作 + def restart_server(): + """实际执行重启的方法""" + time.sleep(1) + self.logger.bind(tag=TAG).info("执行服务器重启...") + subprocess.Popen( + [sys.executable, "app.py"], + stdin=sys.stdin, + stdout=sys.stdout, + stderr=sys.stderr, + start_new_session=True + ) + os._exit(0) + + # 使用线程执行重启避免阻塞事件循环 + threading.Thread(target=restart_server, daemon=True).start() + + except Exception as e: + self.logger.bind(tag=TAG).error(f"重启失败: {str(e)}") + await self.websocket.send(json.dumps({ + "type": "server_response", + "status": "error", + "message": f"Restart failed: {str(e)}" + })) + def _initialize_components(self): """初始化组件""" if self.config.get("prompt") is not None: diff --git a/main/xiaozhi-server/core/handle/textHandle.py b/main/xiaozhi-server/core/handle/textHandle.py index 7ed36122..a3acd3f0 100644 --- a/main/xiaozhi-server/core/handle/textHandle.py +++ b/main/xiaozhi-server/core/handle/textHandle.py @@ -136,5 +136,8 @@ async def handleTextMessage(conn, message): } ) ) + # 重启服务器 + elif msg_json["action"] == "restart": + await conn.handle_restart(msg_json) except json.JSONDecodeError: await conn.websocket.send(message) diff --git a/main/xiaozhi-server/core/providers/tts/cozecn.py b/main/xiaozhi-server/core/providers/tts/cozecn.py index 1f9244f6..56314f4f 100644 --- a/main/xiaozhi-server/core/providers/tts/cozecn.py +++ b/main/xiaozhi-server/core/providers/tts/cozecn.py @@ -38,9 +38,13 @@ class TTSProvider(TTSProviderBase): "Authorization": f"Bearer {self.access_token}", "Content-Type": "application/json", } - response = requests.request( - "POST", self.api_url, json=request_json, headers=headers - ) - data = response.content - file_to_save = open(output_file, "wb") - file_to_save.write(data) + + try: + response = requests.request( + "POST", self.api_url, json=request_json, headers=headers + ) + data = response.content + file_to_save = open(output_file, "wb") + file_to_save.write(data) + except Exception as e: + raise Exception(f"{__name__} error: {e}") \ No newline at end of file diff --git a/main/xiaozhi-server/core/providers/tts/custom.py b/main/xiaozhi-server/core/providers/tts/custom.py index 3417790f..5b8669da 100644 --- a/main/xiaozhi-server/core/providers/tts/custom.py +++ b/main/xiaozhi-server/core/providers/tts/custom.py @@ -32,4 +32,6 @@ class TTSProvider(TTSProviderBase): with open(output_file, "wb") as file: file.write(resp.content) else: - logger.bind(tag=TAG).error(f"Custom TTS请求失败: {resp.status_code} - {resp.text}") + error_msg = f"Custom TTS请求失败: {resp.status_code} - {resp.text}" + logger.bind(tag=TAG).error(error_msg) + raise Exception(error_msg) # 抛出异常,让调用方捕获 diff --git a/main/xiaozhi-server/core/providers/tts/edge.py b/main/xiaozhi-server/core/providers/tts/edge.py index b86e4087..3d9b2547 100644 --- a/main/xiaozhi-server/core/providers/tts/edge.py +++ b/main/xiaozhi-server/core/providers/tts/edge.py @@ -20,14 +20,18 @@ class TTSProvider(TTSProviderBase): ) async def text_to_speak(self, text, output_file): - communicate = edge_tts.Communicate(text, voice=self.voice) - # 确保目录存在并创建空文件 - os.makedirs(os.path.dirname(output_file), exist_ok=True) - with open(output_file, "wb") as f: - pass + try: + communicate = edge_tts.Communicate(text, voice=self.voice) + # 确保目录存在并创建空文件 + os.makedirs(os.path.dirname(output_file), exist_ok=True) + with open(output_file, "wb") as f: + pass - # 流式写入音频数据 - with open(output_file, "ab") as f: # 改为追加模式避免覆盖 - async for chunk in communicate.stream(): - if chunk["type"] == "audio": # 只处理音频数据块 - f.write(chunk["data"]) + # 流式写入音频数据 + with open(output_file, "ab") as f: # 改为追加模式避免覆盖 + async for chunk in communicate.stream(): + if chunk["type"] == "audio": # 只处理音频数据块 + f.write(chunk["data"]) + except Exception as e: + error_msg = f"Edge TTS请求失败: {e}" + raise Exception(error_msg) # 抛出异常,让调用方捕获 \ No newline at end of file diff --git a/main/xiaozhi-server/core/providers/tts/fishspeech.py b/main/xiaozhi-server/core/providers/tts/fishspeech.py index 316dbed7..627c6e99 100644 --- a/main/xiaozhi-server/core/providers/tts/fishspeech.py +++ b/main/xiaozhi-server/core/providers/tts/fishspeech.py @@ -177,5 +177,7 @@ class TTSProvider(TTSProviderBase): audio_file.write(audio_content) else: - print(f"Request failed with status code {response.status_code}") + error_msg = f"Request failed with status code {response.status_code}" + print(error_msg) print(response.json()) + raise Exception(error_msg) diff --git a/main/xiaozhi-server/core/providers/tts/gpt_sovits_v2.py b/main/xiaozhi-server/core/providers/tts/gpt_sovits_v2.py index b2aad88a..2d58679d 100644 --- a/main/xiaozhi-server/core/providers/tts/gpt_sovits_v2.py +++ b/main/xiaozhi-server/core/providers/tts/gpt_sovits_v2.py @@ -105,6 +105,6 @@ class TTSProvider(TTSProviderBase): with open(output_file, "wb") as file: file.write(resp.content) else: - logger.bind(tag=TAG).error( - f"GPT_SoVITS_V2 TTS请求失败: {resp.status_code} - {resp.text}" - ) + error_msg = f"GPT_SoVITS_V2 TTS请求失败: {resp.status_code} - {resp.text}" + logger.bind(tag=TAG).error(error_msg) + raise Exception(error_msg) diff --git a/main/xiaozhi-server/core/providers/tts/gpt_sovits_v3.py b/main/xiaozhi-server/core/providers/tts/gpt_sovits_v3.py index 488280f1..d4da23ed 100644 --- a/main/xiaozhi-server/core/providers/tts/gpt_sovits_v3.py +++ b/main/xiaozhi-server/core/providers/tts/gpt_sovits_v3.py @@ -64,6 +64,7 @@ class TTSProvider(TTSProviderBase): with open(output_file, "wb") as file: file.write(resp.content) else: - logger.bind(tag=TAG).error( - f"GPT_SoVITS_V3 TTS请求失败: {resp.status_code} - {resp.text}" - ) + error_msg = f"GPT_SoVITS_V3 TTS请求失败: {resp.status_code} - {resp.text}" + logger.bind(tag=TAG).error(error_msg) + raise Exception(error_msg) + diff --git a/main/xiaozhi-server/core/providers/tts/siliconflow.py b/main/xiaozhi-server/core/providers/tts/siliconflow.py index b2f564cc..9e30f721 100644 --- a/main/xiaozhi-server/core/providers/tts/siliconflow.py +++ b/main/xiaozhi-server/core/providers/tts/siliconflow.py @@ -39,9 +39,12 @@ class TTSProvider(TTSProviderBase): "Authorization": f"Bearer {self.access_token}", "Content-Type": "application/json", } - response = requests.request( - "POST", self.api_url, json=request_json, headers=headers - ) - data = response.content - file_to_save = open(output_file, "wb") - file_to_save.write(data) + try: + response = requests.request( + "POST", self.api_url, json=request_json, headers=headers + ) + data = response.content + file_to_save = open(output_file, "wb") + file_to_save.write(data) + except Exception as e: + raise Exception(f"{__name__} error: {e}") \ No newline at end of file diff --git a/main/xiaozhi-server/core/providers/tts/ttson.py b/main/xiaozhi-server/core/providers/tts/ttson.py index 047d7a67..f3a0fbdc 100644 --- a/main/xiaozhi-server/core/providers/tts/ttson.py +++ b/main/xiaozhi-server/core/providers/tts/ttson.py @@ -58,8 +58,8 @@ class TTSProvider(TTSProviderBase): resp = requests.request("POST", url, data=payload) if resp.status_code != 200: - logger.bind(tag=TAG).error(f"TTS请求失败: {resp.text}") - return None + logger.bind(tag=TAG).error(f"TTSON 请求失败: {resp.text}") + raise Exception(f"{__name__}: TTS请求失败") resp_json = resp.json() try: result = ( @@ -71,13 +71,15 @@ class TTSProvider(TTSProviderBase): + "&voice_audio_path=" + resp_json["voice_path"] ) + + audio_content = requests.get(result) + with open(output_file, "wb") as f: + f.write(audio_content.content) + return True + voice_path = resp_json.get("voice_path") + des_path = output_file + shutil.move(voice_path, des_path) + except Exception as e: print("error:", e) - - audio_content = requests.get(result) - with open(output_file, "wb") as f: - f.write(audio_content.content) - return True - voice_path = resp_json.get("voice_path") - des_path = output_file - shutil.move(voice_path, des_path) + raise Exception(f"{__name__}: TTS请求失败") \ No newline at end of file diff --git a/main/xiaozhi-server/requirements.txt b/main/xiaozhi-server/requirements.txt index 0a6b9b75..2b6f1dc9 100755 --- a/main/xiaozhi-server/requirements.txt +++ b/main/xiaozhi-server/requirements.txt @@ -28,4 +28,5 @@ PySocks==1.7.1 dashscope==1.23.1 baidu-aip==4.16.13 chardet==5.2.0 +aioconsole==0.8.1 markitdown==0.1.1 \ No newline at end of file