diff --git a/docs/Deployment.md b/docs/Deployment.md index 0dfd93b5..94b6e363 100644 --- a/docs/Deployment.md +++ b/docs/Deployment.md @@ -117,7 +117,7 @@ docker run -it --name xiaozhi-env --restart always --security-opt seccomp:unconf -p 8000:8000 \ -p 8002:8002 \ -v ./:/app \ - kalicyh/poetry:v3.10_xiaozhi + kalicyh/python:xiaozhi ``` 然后就和正常开发一样了 @@ -127,29 +127,13 @@ docker run -it --name xiaozhi-env --restart always --security-opt seccomp:unconf 在刚刚的打开的终端运行 ```sh -poetry install --no-root -``` - -```sh -apt-get update -apt-get install -y --no-install-recommends libopus0 ffmpeg -``` - -速度慢可以尝试使用清华镜像 - -```sh -echo "deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bookworm main contrib non-free non-free-firmware" > /etc/apt/sources.list -echo "deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bookworm-updates main contrib non-free non-free-firmware" >> /etc/apt/sources.list -echo "deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bookworm-backports main contrib non-free non-free-firmware" >> /etc/apt/sources.list -echo "deb https://mirrors.tuna.tsinghua.edu.cn/debian-security bookworm-security main contrib non-free non-free-firmware" >> /etc/apt/sources.list -apt-get update -apt-get install -y --no-install-recommends libopus0 ffmpeg +pip install -r requirements.txt ``` ## 6.运行项目 ```sh -poetry run python app.py +python app.py ``` # 方式三:本地源码运行 diff --git a/main/manager-api/src/main/java/xiaozhi/AdminApplication.java b/main/manager-api/src/main/java/xiaozhi/AdminApplication.java index 87edd510..8a96edb2 100644 --- a/main/manager-api/src/main/java/xiaozhi/AdminApplication.java +++ b/main/manager-api/src/main/java/xiaozhi/AdminApplication.java @@ -8,6 +8,6 @@ public class AdminApplication { public static void main(String[] args) { SpringApplication.run(AdminApplication.class, args); - System.out.println("http://localhost:8002/xiaozhi-esp32-api/doc.html"); + System.out.println("http://localhost:8002/xiaozhi-esp32-api/api/v1/doc.html"); } } \ No newline at end of file diff --git a/main/xiaozhi-server/app.py b/main/xiaozhi-server/app.py index bd23c7bc..ca3ab545 100644 --- a/main/xiaozhi-server/app.py +++ b/main/xiaozhi-server/app.py @@ -1,10 +1,27 @@ import asyncio +import sys +import signal from config.settings import load_config, check_config_file from core.websocket_server import WebSocketServer from core.utils.util import check_ffmpeg_installed TAG = __name__ +async def wait_for_exit(): + """Windows 和 Linux 兼容的退出监听""" + loop = asyncio.get_running_loop() + stop_event = asyncio.Event() + + if sys.platform == "win32": + # Windows: 用 sys.stdin.read() 监听 Ctrl + C + await loop.run_in_executor(None, sys.stdin.read) + else: + # Linux/macOS: 用 signal 监听 Ctrl + C + def stop(): + stop_event.set() + loop.add_signal_handler(signal.SIGINT, stop) + loop.add_signal_handler(signal.SIGTERM, stop) # 支持 kill 进程 + await stop_event.wait() async def main(): check_config_file() @@ -16,11 +33,19 @@ async def main(): ws_task = asyncio.create_task(ws_server.start()) try: - # 等待 WebSocket 服务器运行 - await ws_task + await wait_for_exit() # 监听退出信号 + except asyncio.CancelledError: + print("任务被取消,清理资源中...") finally: ws_task.cancel() - + try: + await ws_task + except asyncio.CancelledError: + pass + print("服务器已关闭,程序退出。") if __name__ == "__main__": - asyncio.run(main()) + try: + asyncio.run(main()) + except KeyboardInterrupt: + print("手动中断,程序终止。") diff --git a/main/xiaozhi-server/config.yaml b/main/xiaozhi-server/config.yaml index c8fbdb9f..a441930f 100644 --- a/main/xiaozhi-server/config.yaml +++ b/main/xiaozhi-server/config.yaml @@ -311,36 +311,6 @@ TTS: media_type: "wav" streaming_mode: false threshold: 30 - GSV: - # GSV原神本地语音合成 - # 整合包下载:https://pan.ai-hobbyist.com/Infer%20Packs/TTS - # 国内镜像:https://alist.hancat.work/d/cdn/GPT-Sovits%E8%AF%AD%E9%9F%B3%E5%90%88%E6%88%90%E6%95%B4%E5%90%88%E5%8C%85%EF%BC%8C%E6%94%AF%E6%8C%81%E5%A4%9A%E4%BA%BA%E5%AF%B9%E8%AF%9D%E3%80%82%E6%B0%B8%E4%B9%85%E5%85%8D%E8%B4%B9%EF%BC%8C%E4%B8%A5%E7%A6%81%E5%80%92%E5%8D%96%EF%BC%81.7z - # 本TTS需要配合模型来使用,模型可以去这里下载: - # 二次元 & 游戏角色 GPT-Sovits V2 语音合成模型:https://www.modelscope.cn/models/aihobbyist/Anime_GPT-Sovits_Models - # 下载后将其解压到model文件夹 - # URL需要修改批处理文件才可生效,默认WebUI端口号为8080,API端口号为WebUI端口号+1 - url: "http://127.0.0.1:9881/infer_single" - app_key: "" - audio_dl_url: "http://127.0.0.1:9881" - model_name: "【原神】须弥" - speaker_name: "纳西妲" - prompt_text_lang: "中文" - emocton: "中立_neutral" - text_lang: "中文" - top_k: 10 - top_p: 1 - temperature: 1 - text_split_method: "按标点符号切" - batch_size: 10 - batch_size: 0.75 - split_bucket: true - speed_facter: 1 - fragment_interval: 0.3 - media_type: "wav" - parallel_infer: true - repetition_penalty: 1.35 - seed: -1 - output_file: tmp/ MinimaxTTS: # Minimax语音合成服务,需要先在minimax平台创建账户充值,并获取登录信息 # 平台地址:https://platform.minimaxi.com/ diff --git a/main/xiaozhi-server/core/providers/tts/GSV.py b/main/xiaozhi-server/core/providers/tts/GSV.py deleted file mode 100644 index 7a156c29..00000000 --- a/main/xiaozhi-server/core/providers/tts/GSV.py +++ /dev/null @@ -1,123 +0,0 @@ -import os -import uuid -import requests -from config.logger import setup_logging -from datetime import datetime -from core.providers.tts.base import TTSProviderBase - -TAG = __name__ -logger = setup_logging() - -class TTSProvider(TTSProviderBase): - def __init__(self, config, delete_audio_file): - super().__init__(config, delete_audio_file) - # POST请求地址 - self.url = config.get("url") - # 从配置中加载所有参数 - self.app_key = config.get("app_key") - self.audio_dl_url = config.get("audio_dl_url") - self.model_name = config.get("model_name") - self.speaker_name = config.get("speaker_name") - self.prompt_text_lang = config.get("prompt_text_lang") - self.emotion = config.get("emotion") - self.text_lang = config.get("text_lang") - self.top_k = config.get("top_k") - self.top_p = config.get("top_p") - self.temperature = config.get("temperature") - self.text_split_method = config.get("text_split_method") - self.batch_size = config.get("batch_size") - self.batch_threshold = config.get("batch_size") - self.split_bucket = config.get("split_bucket") - self.speed_facter = config.get("speed_facter") - self.fragment_interval = config.get("fragment_interval") - self.media_type = config.get("media_type") - self.parallel_infer = config.get("parallel_infer") - self.repetition_penalty = config.get("repetition_penalty") - self.seed = config.get("seed") - - def generate_filename(self): - """根据媒体类型生成文件名""" - extension = f".{self.media_type}" - return os.path.join( - self.output_file, - f"tts-{datetime.now().date()}@{uuid.uuid4().hex}{extension}" - ) - - async def text_to_speak(self, text, output_file): - """文本转语音并保存音频文件""" - # 构造请求体 - request_body = { - "app_key": self.app_key, - "audio_dl_url": self.audio_dl_url, - "model_name": self.model_name, - "speaker_name": self.speaker_name, - "prompt_text_lang": self.prompt_text_lang, - "emotion": self.emotion, - "text": text, - "text_lang": self.text_lang, - "top_k": self.top_k, - "top_p": self.top_p, - "temperature": self.temperature, - "text_split_method": self.text_split_method, - "batch_size": self.batch_size, - "batch_threshold": self.batch_threshold, - "split_bucket": self.split_bucket, - "speed_facter": self.speed_facter, - "fragment_interval": self.fragment_interval, - "media_type": self.media_type, - "parallel_infer": self.parallel_infer, - "repetition_penalty": self.repetition_penalty, - "seed": self.seed - } - - body = { - "app_key": "", - "audio_dl_url": "http://127.0.0.1:9881", - "model_name": "【原神】须弥", - "speaker_name": "纳西妲", - "prompt_text_lang": "中文", - "emotion": "中立_neutral", - "text": text, - "text_lang": "中文", - "top_k": 10, - "top_p": 1, - "temperature": 1, - "text_split_method": "按标点符号切", - "batch_size": 10, - "batch_threshold": 0.75, - "split_bucket": True, - "speed_facter": 1, - "fragment_interval": 0.3, - "media_type": "wav", - "parallel_infer": True, - "repetition_penalty": 1.35, - "seed": -1 - } - - try: - # 发送POST请求 - resp = requests.post(self.url, json=body) - resp.raise_for_status() - - # 解析响应 - result = resp.json() - if "audio_url" not in result: - logger.bind(tag=TAG).error("响应中缺少音频地址") - return - - # 下载音频文件 - audio_resp = requests.get(result["audio_url"]) - audio_resp.raise_for_status() - - # 保存文件 - with open(output_file, "wb") as f: - f.write(audio_resp.content) - - logger.bind(tag=TAG).info(f"音频文件已保存至: {output_file}") - - except requests.exceptions.HTTPError as e: - logger.bind(tag=TAG).error(f"HTTP请求失败: {str(e)}") - except requests.exceptions.JSONDecodeError: - logger.bind(tag=TAG).error("响应解析失败,无效的JSON格式") - except Exception as e: - logger.bind(tag=TAG).error(f"语音合成失败: {str(e)}") \ No newline at end of file