From 26c81a14cb1d8a928b6dd9c79a537d6cf27af781 Mon Sep 17 00:00:00 2001 From: litongjava Date: Mon, 20 May 2024 18:58:21 -1000 Subject: [PATCH 1/6] 1.add trained path to scan model path --- .gitignore | 5 +++- GPT_SoVITS/inference_webui.py | 46 ++++++++++++++++++++++++++++------ GPT_SoVITS/pyutils/__init__.py | 0 GPT_SoVITS/pyutils/logs.py | 24 ++++++++++++++++++ GPT_SoVITS/utils.py | 11 +++----- docs/cn/inference_cpu.md | 1 + trained/.gitignore | 3 +++ 7 files changed, 74 insertions(+), 16 deletions(-) create mode 100644 GPT_SoVITS/pyutils/__init__.py create mode 100644 GPT_SoVITS/pyutils/logs.py create mode 100644 docs/cn/inference_cpu.md create mode 100644 trained/.gitignore diff --git a/.gitignore b/.gitignore index c484cf22..c938d376 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,7 @@ logs reference GPT_weights SoVITS_weights -TEMP \ No newline at end of file +TEMP +app.log +gweight.txt +sweight.txt \ No newline at end of file diff --git a/GPT_SoVITS/inference_webui.py b/GPT_SoVITS/inference_webui.py index 4fe8045d..bca4a43e 100644 --- a/GPT_SoVITS/inference_webui.py +++ b/GPT_SoVITS/inference_webui.py @@ -8,6 +8,8 @@ ''' import os, re, logging import LangSegment +from pyutils.logs import llog + logging.getLogger("markdown_it").setLevel(logging.ERROR) logging.getLogger("urllib3").setLevel(logging.ERROR) logging.getLogger("httpcore").setLevel(logging.ERROR) @@ -530,19 +532,47 @@ def change_choices(): pretrained_sovits_name = "GPT_SoVITS/pretrained_models/s2G488k.pth" pretrained_gpt_name = "GPT_SoVITS/pretrained_models/s1bert25hz-2kh-longer-epoch=68e-step=50232.ckpt" -SoVITS_weight_root = "SoVITS_weights" -GPT_weight_root = "GPT_weights" -os.makedirs(SoVITS_weight_root, exist_ok=True) -os.makedirs(GPT_weight_root, exist_ok=True) +SoVITS_weight_root = ["SoVITS_weights","trained"] +GPT_weight_root = ["GPT_weights","trained"] + +for path in SoVITS_weight_root: + os.makedirs(path, exist_ok=True) + +for path in GPT_weight_root: + os.makedirs(path, exist_ok=True) def get_weights_names(): SoVITS_names = [pretrained_sovits_name] - for name in os.listdir(SoVITS_weight_root): - if name.endswith(".pth"): SoVITS_names.append("%s/%s" % (SoVITS_weight_root, name)) + for path in SoVITS_weight_root: + llog.info(f"scan model path:{path}") + for name in os.listdir(path): + llog.info(f"scan sub model path:{name}") + #if os.path.isdir(name): no working + if os.path.isfile(name): + if name.endswith(".pth"): SoVITS_names.append("%s/%s" % (path, name)) + else: + subPath = os.path.join(path, name) + for modelName in os.listdir(subPath): + if modelName.endswith(".pth"): + modelPath = os.path.join(subPath,modelName) + llog.info(f"add model path:{modelPath}") + SoVITS_names.append(modelPath) + + GPT_names = [pretrained_gpt_name] - for name in os.listdir(GPT_weight_root): - if name.endswith(".ckpt"): GPT_names.append("%s/%s" % (GPT_weight_root, name)) + for path in GPT_weight_root: + for name in os.listdir(path): + if os.path.isfile(name): + if name.endswith(".ckpt"): GPT_names.append("%s/%s" % (path, name)) + else: + subPath = os.path.join(path, name) + for modelName in os.listdir(subPath): + if modelName.endswith(".pth"): + modelPath = os.path.join(subPath, modelName) + llog.info(f"add model path:{modelPath}") + GPT_names.append(modelPath) + return SoVITS_names, GPT_names diff --git a/GPT_SoVITS/pyutils/__init__.py b/GPT_SoVITS/pyutils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/GPT_SoVITS/pyutils/logs.py b/GPT_SoVITS/pyutils/logs.py new file mode 100644 index 00000000..1fe2ffcd --- /dev/null +++ b/GPT_SoVITS/pyutils/logs.py @@ -0,0 +1,24 @@ +import logging +from logging.handlers import RotatingFileHandler + +# 设置日志记录器 +llog = logging.getLogger(__name__) +llog.setLevel(logging.INFO) +llog.propagate = False # 防止日志事件传递给根记录器 + +# 创建控制台日志处理器 +console_handler = logging.StreamHandler() +console_handler.setLevel(logging.INFO) + +# 创建文件日志处理器 +file_handler = RotatingFileHandler('app.log', maxBytes=1024 * 1024 * 10, backupCount=5) +file_handler.setLevel(logging.INFO) + +# 设置日志格式,包括文件名和行号 +formatter = logging.Formatter('%(asctime)s - %(filename)s:%(lineno)d - %(levelname)s - %(message)s') +console_handler.setFormatter(formatter) +file_handler.setFormatter(formatter) + +# 将处理器添加到日志记录器 +llog.addHandler(console_handler) +#llog.addHandler(file_handler) \ No newline at end of file diff --git a/GPT_SoVITS/utils.py b/GPT_SoVITS/utils.py index 7984b5a8..99cd5066 100644 --- a/GPT_SoVITS/utils.py +++ b/GPT_SoVITS/utils.py @@ -1,17 +1,14 @@ -import os -import glob -import sys import argparse -import logging +import glob import json +import logging +import os import subprocess +import sys import traceback import librosa -import numpy as np -from scipy.io.wavfile import read import torch -import logging logging.getLogger("numba").setLevel(logging.ERROR) logging.getLogger("matplotlib").setLevel(logging.ERROR) diff --git a/docs/cn/inference_cpu.md b/docs/cn/inference_cpu.md new file mode 100644 index 00000000..f3a6aa13 --- /dev/null +++ b/docs/cn/inference_cpu.md @@ -0,0 +1 @@ +# 使用cpu推理 diff --git a/trained/.gitignore b/trained/.gitignore new file mode 100644 index 00000000..aa4e5510 --- /dev/null +++ b/trained/.gitignore @@ -0,0 +1,3 @@ +* +!.gitignore +!character_info.json \ No newline at end of file From 6c172903ca857b178461a12a55ed3cbae2cb2412 Mon Sep 17 00:00:00 2001 From: litongjava Date: Mon, 20 May 2024 19:38:17 -1000 Subject: [PATCH 2/6] add inference.md --- docs/cn/inference.md | 68 ++++++++++++++++++++++++++++++ docs/cn/inference_cpu.md | 64 +++++++++++++++++++++++++++- docs/cn/inference_cpu_files/1.jpg | Bin 0 -> 136885 bytes 3 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 docs/cn/inference.md create mode 100644 docs/cn/inference_cpu_files/1.jpg diff --git a/docs/cn/inference.md b/docs/cn/inference.md new file mode 100644 index 00000000..ceeab3e3 --- /dev/null +++ b/docs/cn/inference.md @@ -0,0 +1,68 @@ +# 推理 + +## Windows + +### 使用cpu推理 +本文档介绍如何使用cpu进行推理,使用cpu的推理速度有点慢,但不是很慢 + +#### 安装依赖 +``` +# 拉取项目代码 +git clone --depth=1 https://github.com/RVC-Boss/GPT-SoVITS +cd GPT-SoVITS + +# 安装好 Miniconda 之后,先创建一个虚拟环境: +conda create -n GPTSoVits python=3.9 +conda activate GPTSoVits + +# 安装依赖: +pip install -r requirements.txt + +# (可选)如果网络环境不好,可以考虑换源(比如清华源): +pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt +``` + +#### 添加预训练模型 +``` +# 安装 huggingface-cli 用于和 huggingface hub 交互 +pip install huggingface_hub +# 登录 huggingface-cli +huggingface-cli login + +# 下载模型, 由于模型文件较大,可能需要一段时间 +# --local-dir-use-symlinks False 用于解决 macOS alias 文件的问题 +# 会下载到 GPT_SoVITS/pretrained_models 文件夹下 +huggingface-cli download --resume-download lj1995/GPT-SoVITS --local-dir GPT_SoVITS/pretrained_models --local-dir-use-symlinks False +``` + +#### 添加微调模型(可选) +笔者是将微调添加到了GPT-SoVITS/trained 内容如下,正常情况下包含 openai_alloy-e15.ckpt 和openai_alloy_e8_s112.pth 即可 +``` +├── .gitignore +├── openai_alloy +│ ├── infer_config.json +│ ├── openai_alloy-e15.ckpt +│ ├── openai_alloy_e8_s112.pth +│ ├── output-2.txt +│ ├── output-2.wav +``` + +#### 启动推理webtui +``` +python.exe GPT_SoVITS/inference_webui.py +``` +配置如下 +![](inference_cpu_files/1.jpg) + +### 使用gpu推理 +请根据你的操作系统选择合适的cuda版本 +``` +pip uninstall torch torchaudio torchvision -y +pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu117 +``` +检查 torch和cuda是否可用 +``` +>>> import torch +>>> torch.cuda.is_available() +True +``` diff --git a/docs/cn/inference_cpu.md b/docs/cn/inference_cpu.md index f3a6aa13..e72c8b20 100644 --- a/docs/cn/inference_cpu.md +++ b/docs/cn/inference_cpu.md @@ -1 +1,63 @@ -# 使用cpu推理 +# 推理 + +## Windows + +### 使用cpu推理 +本文档介绍如何使用cpu进行推理,使用cpu的推理速度有点慢,但不是很慢 + +#### 安装依赖 +``` +# 拉取项目代码 +git clone --depth=1 https://github.com/RVC-Boss/GPT-SoVITS +cd GPT-SoVITS + +# 安装好 Miniconda 之后,先创建一个虚拟环境: +conda create -n GPTSoVits python=3.9 +conda activate GPTSoVits + +# 安装依赖: +pip install -r requirements.txt + +# (可选)如果网络环境不好,可以考虑换源(比如清华源): +pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt +``` + +#### 添加预训练模型 +``` +# 安装 huggingface-cli 用于和 huggingface hub 交互 +pip install huggingface_hub +# 登录 huggingface-cli +huggingface-cli login + +# 下载模型, 由于模型文件较大,可能需要一段时间 +# --local-dir-use-symlinks False 用于解决 macOS alias 文件的问题 +# 会下载到 GPT_SoVITS/pretrained_models 文件夹下 +huggingface-cli download --resume-download lj1995/GPT-SoVITS --local-dir GPT_SoVITS/pretrained_models --local-dir-use-symlinks False +``` + +#### 添加微调模型(可选) +笔者是将微调添加到了GPT-SoVITS/trained目录,内容如下,正常情况下包含 openai_alloy-e15.ckpt 和openai_alloy_e8_s112.pth 即可 +如果仅仅测试合成效果,不添加微调模型 使用预训练模型作为微调模型也可以 +``` +├── .gitignore +├── openai_alloy +│ ├── infer_config.json +│ ├── openai_alloy-e15.ckpt +│ ├── openai_alloy_e8_s112.pth +│ ├── output-2.txt +│ ├── output-2.wav +``` + +#### 启动推理webtui +``` +python.exe GPT_SoVITS/inference_webui.py +``` +配置如下 +![](inference_cpu_files/1.jpg) + +### 使用gpu推理 +``` +pip uninstall torch torchaudio -y +pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu117 +``` +请根据你的环境选择合适的cuda版本 diff --git a/docs/cn/inference_cpu_files/1.jpg b/docs/cn/inference_cpu_files/1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4af71698dd07fdf7a86d4d1c43b85d5099edf082 GIT binary patch literal 136885 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 W{^J|`@Be>)IQ;)szM%az{yzY+(1r>C literal 0 HcmV?d00001 From d46a29b35bd02acb9fc2117f4ae49d0eb041a513 Mon Sep 17 00:00:00 2001 From: litongmacos Date: Tue, 21 May 2024 05:23:09 -1000 Subject: [PATCH 3/6] add np_utils.py and api.md --- GPT_SoVITS/inference_webui.py | 3 +- GPT_SoVITS/module/data_utils.py | 11 +- GPT_SoVITS/onnx_export.py | 1 - .../prepare_datasets/2-get-hubert-wav32k.py | 4 +- .../{my_utils.py => pyutils/np_utils.py} | 42 +++--- api.md | 115 +++++++++++++++ api.py | 133 +----------------- 7 files changed, 147 insertions(+), 162 deletions(-) rename GPT_SoVITS/{my_utils.py => pyutils/np_utils.py} (97%) create mode 100644 api.md diff --git a/GPT_SoVITS/inference_webui.py b/GPT_SoVITS/inference_webui.py index bca4a43e..668b36ae 100644 --- a/GPT_SoVITS/inference_webui.py +++ b/GPT_SoVITS/inference_webui.py @@ -17,7 +17,6 @@ logging.getLogger("httpx").setLevel(logging.ERROR) logging.getLogger("asyncio").setLevel(logging.ERROR) logging.getLogger("charset_normalizer").setLevel(logging.ERROR) logging.getLogger("torchaudio._extension").setLevel(logging.ERROR) -import pdb import torch if os.path.exists("./gweight.txt"): @@ -66,7 +65,7 @@ from text import cleaned_text_to_sequence from text.cleaner import clean_text from time import time as ttime from module.mel_processing import spectrogram_torch -from my_utils import load_audio +from pyutils.np_utils import load_audio from tools.i18n.i18n import I18nAuto i18n = I18nAuto() diff --git a/GPT_SoVITS/module/data_utils.py b/GPT_SoVITS/module/data_utils.py index ff4c4f43..ba870b85 100644 --- a/GPT_SoVITS/module/data_utils.py +++ b/GPT_SoVITS/module/data_utils.py @@ -1,23 +1,14 @@ -import time -import logging import os import random import traceback -import numpy as np import torch import torch.utils.data from tqdm import tqdm -from module import commons from module.mel_processing import spectrogram_torch from text import cleaned_text_to_sequence -from utils import load_wav_to_torch, load_filepaths_and_text import torch.nn.functional as F -from functools import lru_cache -import requests -from scipy.io import wavfile -from io import BytesIO -from my_utils import load_audio +from pyutils.np_utils import load_audio # ZeroDivisionError fixed by Tybost (https://github.com/RVC-Boss/GPT-SoVITS/issues/79) class TextAudioSpeakerLoader(torch.utils.data.Dataset): diff --git a/GPT_SoVITS/onnx_export.py b/GPT_SoVITS/onnx_export.py index b82e987f..f4132f9e 100644 --- a/GPT_SoVITS/onnx_export.py +++ b/GPT_SoVITS/onnx_export.py @@ -9,7 +9,6 @@ cnhubert.cnhubert_base_path=cnhubert_base_path ssl_model = cnhubert.get_model() from text import cleaned_text_to_sequence import soundfile -from my_utils import load_audio import os import json diff --git a/GPT_SoVITS/prepare_datasets/2-get-hubert-wav32k.py b/GPT_SoVITS/prepare_datasets/2-get-hubert-wav32k.py index 9a2f73c0..1c539d27 100644 --- a/GPT_SoVITS/prepare_datasets/2-get-hubert-wav32k.py +++ b/GPT_SoVITS/prepare_datasets/2-get-hubert-wav32k.py @@ -12,12 +12,12 @@ opt_dir= os.environ.get("opt_dir") cnhubert.cnhubert_base_path= os.environ.get("cnhubert_base_dir") is_half=eval(os.environ.get("is_half","True")) -import pdb,traceback,numpy as np,logging +import traceback,numpy as np from scipy.io import wavfile import librosa,torch now_dir = os.getcwd() sys.path.append(now_dir) -from my_utils import load_audio +from pyutils.np_utils import load_audio # from config import cnhubert_base_path # cnhubert.cnhubert_base_path=cnhubert_base_path diff --git a/GPT_SoVITS/my_utils.py b/GPT_SoVITS/pyutils/np_utils.py similarity index 97% rename from GPT_SoVITS/my_utils.py rename to GPT_SoVITS/pyutils/np_utils.py index 776939dd..a5258394 100644 --- a/GPT_SoVITS/my_utils.py +++ b/GPT_SoVITS/pyutils/np_utils.py @@ -1,21 +1,21 @@ -import ffmpeg -import numpy as np - - -def load_audio(file, sr): - try: - # https://github.com/openai/whisper/blob/main/whisper/audio.py#L26 - # This launches a subprocess to decode audio while down-mixing and resampling as necessary. - # Requires the ffmpeg CLI and `ffmpeg-python` package to be installed. - file = ( - file.strip(" ").strip('"').strip("\n").strip('"').strip(" ") - ) # 防止小白拷路径头尾带了空格和"和回车 - out, _ = ( - ffmpeg.input(file, threads=0) - .output("-", format="f32le", acodec="pcm_f32le", ac=1, ar=sr) - .run(cmd=["ffmpeg", "-nostdin"], capture_stdout=True, capture_stderr=True) - ) - except Exception as e: - raise RuntimeError(f"Failed to load audio: {e}") - - return np.frombuffer(out, np.float32).flatten() +import ffmpeg +import numpy as np + + +def load_audio(file, sr): + try: + # https://github.com/openai/whisper/blob/main/whisper/audio.py#L26 + # This launches a subprocess to decode audio while down-mixing and resampling as necessary. + # Requires the ffmpeg CLI and `ffmpeg-python` package to be installed. + file = ( + file.strip(" ").strip('"').strip("\n").strip('"').strip(" ") + ) # 防止小白拷路径头尾带了空格和"和回车 + out, _ = ( + ffmpeg.input(file, threads=0) + .output("-", format="f32le", acodec="pcm_f32le", ac=1, ar=sr) + .run(cmd=["ffmpeg", "-nostdin"], capture_stdout=True, capture_stderr=True) + ) + except Exception as e: + raise RuntimeError(f"Failed to load audio: {e}") + + return np.frombuffer(out, np.float32).flatten() diff --git a/api.md b/api.md new file mode 100644 index 00000000..1c0e3a87 --- /dev/null +++ b/api.md @@ -0,0 +1,115 @@ +# api + +` python api.py -dr "123.wav" -dt "一二三。" -dl "zh" ` + +## 执行参数: + +`-s` - `SoVITS模型路径, 可在 config.py 中指定` +`-g` - `GPT模型路径, 可在 config.py 中指定` + +调用请求缺少参考音频时使用 +`-dr` - `默认参考音频路径` +`-dt` - `默认参考音频文本` +`-dl` - `默认参考音频语种, "中文","英文","日文","zh","en","ja"` + +`-d` - `推理设备, "cuda","cpu"` +`-a` - `绑定地址, 默认"127.0.0.1"` +`-p` - `绑定端口, 默认9880, 可在 config.py 中指定` +`-fp` - `覆盖 config.py 使用全精度` +`-hp` - `覆盖 config.py 使用半精度` +`-sm` - `流式返回模式, 默认不启用, "close","c", "normal","n", "keepalive","k"` +·-mt` - `返回的音频编码格式, 流式默认ogg, 非流式默认wav, "wav", "ogg", "aac"` +·-cp` - `文本切分符号设定, 默认为空, 以",.,。"字符串的方式传入` + +`-hb` - `cnhubert路径` +`-b` - `bert路径` + +## 调用: + +### 推理 + +endpoint: `/` + +使用执行参数指定的参考音频: +- GET: + `http://127.0.0.1:9880?text=先帝创业未半而中道崩殂,今天下三分,益州疲弊,此诚危急存亡之秋也。&text_language=zh` + +- POST: +```json +{ + "text": "先帝创业未半而中道崩殂,今天下三分,益州疲弊,此诚危急存亡之秋也。", + "text_language": "zh" +} +``` + +使用执行参数指定的参考音频并设定分割符号: +- GET: + `http://127.0.0.1:9880?text=先帝创业未半而中道崩殂,今天下三分,益州疲弊,此诚危急存亡之秋也。&text_language=zh&cut_punc=,。` +- POST: +```json +{ + "text": "先帝创业未半而中道崩殂,今天下三分,益州疲弊,此诚危急存亡之秋也。", + "text_language": "zh", + "cut_punc": ",。" +} +``` + +手动指定当次推理所使用的参考音频: +- GET: + `http://127.0.0.1:9880?refer_wav_path=123.wav&prompt_text=一二三。&prompt_language=zh&text=先帝创业未半而中道崩殂,今天下三分,益州疲弊,此诚危急存亡之秋也。&text_language=zh` +- POST: +```json +{ + "refer_wav_path": "123.wav", + "prompt_text": "一二三。", + "prompt_language": "zh", + "text": "先帝创业未半而中道崩殂,今天下三分,益州疲弊,此诚危急存亡之秋也。", + "text_language": "zh" +} +``` + +RESP: +- 成功: 直接返回 wav 音频流, http code 200 +- 失败: 返回包含错误信息的 json, http code 400 + + +### 更换默认参考音频 + +endpoint: `/change_refer` + +key与推理端一样 + +- GET: + `http://127.0.0.1:9880/change_refer?refer_wav_path=123.wav&prompt_text=一二三。&prompt_language=zh` +- POST: +```json +{ + "refer_wav_path": "123.wav", + "prompt_text": "一二三。", + "prompt_language": "zh" +} +``` + +RESP: +成功: json, http code 200 +失败: json, 400 + + +### 命令控制 + +endpoint: `/control` + +command: +"restart": 重新运行 +"exit": 结束运行 + +- GET: + `http://127.0.0.1:9880/control?command=restart` +- POST: +```json +{ + "command": "restart" +} +``` + +RESP: 无 \ No newline at end of file diff --git a/api.py b/api.py index 041fa349..7b6b07f3 100644 --- a/api.py +++ b/api.py @@ -1,129 +1,10 @@ -""" -# api.py usage - -` python api.py -dr "123.wav" -dt "一二三。" -dl "zh" ` - -## 执行参数: - -`-s` - `SoVITS模型路径, 可在 config.py 中指定` -`-g` - `GPT模型路径, 可在 config.py 中指定` - -调用请求缺少参考音频时使用 -`-dr` - `默认参考音频路径` -`-dt` - `默认参考音频文本` -`-dl` - `默认参考音频语种, "中文","英文","日文","zh","en","ja"` - -`-d` - `推理设备, "cuda","cpu"` -`-a` - `绑定地址, 默认"127.0.0.1"` -`-p` - `绑定端口, 默认9880, 可在 config.py 中指定` -`-fp` - `覆盖 config.py 使用全精度` -`-hp` - `覆盖 config.py 使用半精度` -`-sm` - `流式返回模式, 默认不启用, "close","c", "normal","n", "keepalive","k"` -·-mt` - `返回的音频编码格式, 流式默认ogg, 非流式默认wav, "wav", "ogg", "aac"` -·-cp` - `文本切分符号设定, 默认为空, 以",.,。"字符串的方式传入` - -`-hb` - `cnhubert路径` -`-b` - `bert路径` - -## 调用: - -### 推理 - -endpoint: `/` - -使用执行参数指定的参考音频: -GET: - `http://127.0.0.1:9880?text=先帝创业未半而中道崩殂,今天下三分,益州疲弊,此诚危急存亡之秋也。&text_language=zh` -POST: -```json -{ - "text": "先帝创业未半而中道崩殂,今天下三分,益州疲弊,此诚危急存亡之秋也。", - "text_language": "zh" -} -``` - -使用执行参数指定的参考音频并设定分割符号: -GET: - `http://127.0.0.1:9880?text=先帝创业未半而中道崩殂,今天下三分,益州疲弊,此诚危急存亡之秋也。&text_language=zh&cut_punc=,。` -POST: -```json -{ - "text": "先帝创业未半而中道崩殂,今天下三分,益州疲弊,此诚危急存亡之秋也。", - "text_language": "zh", - "cut_punc": ",。", -} -``` - -手动指定当次推理所使用的参考音频: -GET: - `http://127.0.0.1:9880?refer_wav_path=123.wav&prompt_text=一二三。&prompt_language=zh&text=先帝创业未半而中道崩殂,今天下三分,益州疲弊,此诚危急存亡之秋也。&text_language=zh` -POST: -```json -{ - "refer_wav_path": "123.wav", - "prompt_text": "一二三。", - "prompt_language": "zh", - "text": "先帝创业未半而中道崩殂,今天下三分,益州疲弊,此诚危急存亡之秋也。", - "text_language": "zh" -} -``` - -RESP: -成功: 直接返回 wav 音频流, http code 200 -失败: 返回包含错误信息的 json, http code 400 - - -### 更换默认参考音频 - -endpoint: `/change_refer` - -key与推理端一样 - -GET: - `http://127.0.0.1:9880/change_refer?refer_wav_path=123.wav&prompt_text=一二三。&prompt_language=zh` -POST: -```json -{ - "refer_wav_path": "123.wav", - "prompt_text": "一二三。", - "prompt_language": "zh" -} -``` - -RESP: -成功: json, http code 200 -失败: json, 400 - - -### 命令控制 - -endpoint: `/control` - -command: -"restart": 重新运行 -"exit": 结束运行 - -GET: - `http://127.0.0.1:9880/control?command=restart` -POST: -```json -{ - "command": "restart" -} -``` - -RESP: 无 - -""" - - import argparse import os,re import sys -now_dir = os.getcwd() -sys.path.append(now_dir) -sys.path.append("%s/GPT_SoVITS" % (now_dir)) +current_project_dir = os.getcwd() +sys.path.append(current_project_dir) +sys.path.append("%s/GPT_SoVITS" % (current_project_dir)) import signal import LangSegment @@ -131,7 +12,7 @@ from time import time as ttime import torch import librosa import soundfile as sf -from fastapi import FastAPI, Request, HTTPException +from fastapi import FastAPI, Request from fastapi.responses import StreamingResponse, JSONResponse import uvicorn from transformers import AutoModelForMaskedLM, AutoTokenizer @@ -143,7 +24,7 @@ from AR.models.t2s_lightning_module import Text2SemanticLightningModule from text import cleaned_text_to_sequence from text.cleaner import clean_text from module.mel_processing import spectrogram_torch -from my_utils import load_audio +from pyutils.np_utils import load_audio import config as global_config import logging import subprocess @@ -159,7 +40,7 @@ class DefaultRefer: return is_full(self.path, self.text, self.language) -def is_empty(*items): # 任意一项不为空返回False +def is_not_empty(*items): # 任意一项不为空返回False for item in items: if item is not None and item != "": return False @@ -496,7 +377,7 @@ def handle_control(command): def handle_change(path, text, language): - if is_empty(path, text, language): + if is_not_empty(path, text, language): return JSONResponse({"code": 400, "message": '缺少任意一项以下参数: "path", "text", "language"'}, status_code=400) if path != "" or path is not None: From 259f201984134184ce1132dd02722c953ffa4289 Mon Sep 17 00:00:00 2001 From: litongmacos Date: Tue, 21 May 2024 05:30:57 -1000 Subject: [PATCH 4/6] update some method name --- api.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/api.py b/api.py index 7b6b07f3..ae353737 100644 --- a/api.py +++ b/api.py @@ -37,17 +37,17 @@ class DefaultRefer: self.language = args.default_refer_language def is_ready(self) -> bool: - return is_full(self.path, self.text, self.language) + return is_not_empty(self.path, self.text, self.language) -def is_not_empty(*items): # 任意一项不为空返回False +def is_empty(*items): # 任意一项不为空返回False for item in items: if item is not None and item != "": return False return True -def is_full(*items): # 任意一项为空返回False +def is_not_empty(*items): # 任意一项为空返回False for item in items: if item is None or item == "": return False @@ -377,7 +377,7 @@ def handle_control(command): def handle_change(path, text, language): - if is_not_empty(path, text, language): + if is_empty(path, text, language): return JSONResponse({"code": 400, "message": '缺少任意一项以下参数: "path", "text", "language"'}, status_code=400) if path != "" or path is not None: From 799168a5f7c2ef27bf065bf1b49d89ef7fe88e15 Mon Sep 17 00:00:00 2001 From: litongjava Date: Tue, 21 May 2024 17:39:22 -1000 Subject: [PATCH 5/6] Refactor the server api code --- api.py | 617 ---------------------------------------- config.py | 65 ++--- main.py | 49 ++++ api.md => server/api.md | 0 server/app.py | 6 + server/cmd_args.py | 13 + server/handlers.py | 72 +++++ server/server.py | 3 + server/tts_service.py | 510 +++++++++++++++++++++++++++++++++ 9 files changed, 686 insertions(+), 649 deletions(-) delete mode 100644 api.py create mode 100644 main.py rename api.md => server/api.md (100%) create mode 100644 server/app.py create mode 100644 server/cmd_args.py create mode 100644 server/handlers.py create mode 100644 server/server.py create mode 100644 server/tts_service.py diff --git a/api.py b/api.py deleted file mode 100644 index ae353737..00000000 --- a/api.py +++ /dev/null @@ -1,617 +0,0 @@ -import argparse -import os,re -import sys - -current_project_dir = os.getcwd() -sys.path.append(current_project_dir) -sys.path.append("%s/GPT_SoVITS" % (current_project_dir)) - -import signal -import LangSegment -from time import time as ttime -import torch -import librosa -import soundfile as sf -from fastapi import FastAPI, Request -from fastapi.responses import StreamingResponse, JSONResponse -import uvicorn -from transformers import AutoModelForMaskedLM, AutoTokenizer -import numpy as np -from feature_extractor import cnhubert -from io import BytesIO -from module.models import SynthesizerTrn -from AR.models.t2s_lightning_module import Text2SemanticLightningModule -from text import cleaned_text_to_sequence -from text.cleaner import clean_text -from module.mel_processing import spectrogram_torch -from pyutils.np_utils import load_audio -import config as global_config -import logging -import subprocess - - -class DefaultRefer: - def __init__(self, path, text, language): - self.path = args.default_refer_path - self.text = args.default_refer_text - self.language = args.default_refer_language - - def is_ready(self) -> bool: - return is_not_empty(self.path, self.text, self.language) - - -def is_empty(*items): # 任意一项不为空返回False - for item in items: - if item is not None and item != "": - return False - return True - - -def is_not_empty(*items): # 任意一项为空返回False - for item in items: - if item is None or item == "": - return False - return True - - -def change_sovits_weights(sovits_path): - global vq_model, hps - dict_s2 = torch.load(sovits_path, map_location="cpu") - hps = dict_s2["config"] - hps = DictToAttrRecursive(hps) - hps.model.semantic_frame_rate = "25hz" - model_params_dict = vars(hps.model) - vq_model = SynthesizerTrn( - hps.data.filter_length // 2 + 1, - hps.train.segment_size // hps.data.hop_length, - n_speakers=hps.data.n_speakers, - **model_params_dict - ) - if ("pretrained" not in sovits_path): - del vq_model.enc_q - if is_half == True: - vq_model = vq_model.half().to(device) - else: - vq_model = vq_model.to(device) - vq_model.eval() - vq_model.load_state_dict(dict_s2["weight"], strict=False) - - -def change_gpt_weights(gpt_path): - global hz, max_sec, t2s_model, config - hz = 50 - dict_s1 = torch.load(gpt_path, map_location="cpu") - config = dict_s1["config"] - max_sec = config["data"]["max_sec"] - t2s_model = Text2SemanticLightningModule(config, "****", is_train=False) - t2s_model.load_state_dict(dict_s1["weight"]) - if is_half == True: - t2s_model = t2s_model.half() - t2s_model = t2s_model.to(device) - t2s_model.eval() - total = sum([param.nelement() for param in t2s_model.parameters()]) - logger.info("Number of parameter: %.2fM" % (total / 1e6)) - - -def get_bert_feature(text, word2ph): - with torch.no_grad(): - inputs = tokenizer(text, return_tensors="pt") - for i in inputs: - inputs[i] = inputs[i].to(device) #####输入是long不用管精度问题,精度随bert_model - res = bert_model(**inputs, output_hidden_states=True) - res = torch.cat(res["hidden_states"][-3:-2], -1)[0].cpu()[1:-1] - assert len(word2ph) == len(text) - phone_level_feature = [] - for i in range(len(word2ph)): - repeat_feature = res[i].repeat(word2ph[i], 1) - phone_level_feature.append(repeat_feature) - phone_level_feature = torch.cat(phone_level_feature, dim=0) - # if(is_half==True):phone_level_feature=phone_level_feature.half() - return phone_level_feature.T - - -def clean_text_inf(text, language): - phones, word2ph, norm_text = clean_text(text, language) - phones = cleaned_text_to_sequence(phones) - return phones, word2ph, norm_text - - -def get_bert_inf(phones, word2ph, norm_text, language): - language=language.replace("all_","") - if language == "zh": - bert = get_bert_feature(norm_text, word2ph).to(device)#.to(dtype) - else: - bert = torch.zeros( - (1024, len(phones)), - dtype=torch.float16 if is_half == True else torch.float32, - ).to(device) - - return bert - - -def get_phones_and_bert(text,language): - if language in {"en","all_zh","all_ja"}: - language = language.replace("all_","") - if language == "en": - LangSegment.setfilters(["en"]) - formattext = " ".join(tmp["text"] for tmp in LangSegment.getTexts(text)) - else: - # 因无法区别中日文汉字,以用户输入为准 - formattext = text - while " " in formattext: - formattext = formattext.replace(" ", " ") - phones, word2ph, norm_text = clean_text_inf(formattext, language) - if language == "zh": - bert = get_bert_feature(norm_text, word2ph).to(device) - else: - bert = torch.zeros( - (1024, len(phones)), - dtype=torch.float16 if is_half == True else torch.float32, - ).to(device) - elif language in {"zh", "ja","auto"}: - textlist=[] - langlist=[] - LangSegment.setfilters(["zh","ja","en","ko"]) - if language == "auto": - for tmp in LangSegment.getTexts(text): - if tmp["lang"] == "ko": - langlist.append("zh") - textlist.append(tmp["text"]) - else: - langlist.append(tmp["lang"]) - textlist.append(tmp["text"]) - else: - for tmp in LangSegment.getTexts(text): - if tmp["lang"] == "en": - langlist.append(tmp["lang"]) - else: - # 因无法区别中日文汉字,以用户输入为准 - langlist.append(language) - textlist.append(tmp["text"]) - # logger.info(textlist) - # logger.info(langlist) - phones_list = [] - bert_list = [] - norm_text_list = [] - for i in range(len(textlist)): - lang = langlist[i] - phones, word2ph, norm_text = clean_text_inf(textlist[i], lang) - bert = get_bert_inf(phones, word2ph, norm_text, lang) - phones_list.append(phones) - norm_text_list.append(norm_text) - bert_list.append(bert) - bert = torch.cat(bert_list, dim=1) - phones = sum(phones_list, []) - norm_text = ''.join(norm_text_list) - - return phones,bert.to(torch.float16 if is_half == True else torch.float32),norm_text - - -class DictToAttrRecursive: - def __init__(self, input_dict): - for key, value in input_dict.items(): - if isinstance(value, dict): - # 如果值是字典,递归调用构造函数 - setattr(self, key, DictToAttrRecursive(value)) - else: - setattr(self, key, value) - - -def get_spepc(hps, filename): - audio = load_audio(filename, int(hps.data.sampling_rate)) - audio = torch.FloatTensor(audio) - audio_norm = audio - audio_norm = audio_norm.unsqueeze(0) - spec = spectrogram_torch(audio_norm, hps.data.filter_length, hps.data.sampling_rate, hps.data.hop_length, - hps.data.win_length, center=False) - return spec - - -def pack_audio(audio_bytes, data, rate): - if media_type == "ogg": - audio_bytes = pack_ogg(audio_bytes, data, rate) - elif media_type == "aac": - audio_bytes = pack_aac(audio_bytes, data, rate) - else: - # wav无法流式, 先暂存raw - audio_bytes = pack_raw(audio_bytes, data, rate) - - return audio_bytes - - -def pack_ogg(audio_bytes, data, rate): - with sf.SoundFile(audio_bytes, mode='w', samplerate=rate, channels=1, format='ogg') as audio_file: - audio_file.write(data) - - return audio_bytes - - -def pack_raw(audio_bytes, data, rate): - audio_bytes.write(data.tobytes()) - - return audio_bytes - - -def pack_wav(audio_bytes, rate): - data = np.frombuffer(audio_bytes.getvalue(),dtype=np.int16) - wav_bytes = BytesIO() - sf.write(wav_bytes, data, rate, format='wav') - - return wav_bytes - - -def pack_aac(audio_bytes, data, rate): - process = subprocess.Popen([ - 'ffmpeg', - '-f', 's16le', # 输入16位有符号小端整数PCM - '-ar', str(rate), # 设置采样率 - '-ac', '1', # 单声道 - '-i', 'pipe:0', # 从管道读取输入 - '-c:a', 'aac', # 音频编码器为AAC - '-b:a', '192k', # 比特率 - '-vn', # 不包含视频 - '-f', 'adts', # 输出AAC数据流格式 - 'pipe:1' # 将输出写入管道 - ], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - out, _ = process.communicate(input=data.tobytes()) - audio_bytes.write(out) - - return audio_bytes - - -def read_clean_buffer(audio_bytes): - audio_chunk = audio_bytes.getvalue() - audio_bytes.truncate(0) - audio_bytes.seek(0) - - return audio_bytes, audio_chunk - - -def cut_text(text, punc): - punc_list = [p for p in punc if p in {",", ".", ";", "?", "!", "、", ",", "。", "?", "!", ";", ":", "…"}] - if len(punc_list) > 0: - punds = r"[" + "".join(punc_list) + r"]" - text = text.strip("\n") - items = re.split(f"({punds})", text) - mergeitems = ["".join(group) for group in zip(items[::2], items[1::2])] - # 在句子不存在符号或句尾无符号的时候保证文本完整 - if len(items)%2 == 1: - mergeitems.append(items[-1]) - text = "\n".join(mergeitems) - - while "\n\n" in text: - text = text.replace("\n\n", "\n") - - return text - - -def only_punc(text): - return not any(t.isalnum() or t.isalpha() for t in text) - - -def get_tts_wav(ref_wav_path, prompt_text, prompt_language, text, text_language): - t0 = ttime() - prompt_text = prompt_text.strip("\n") - prompt_language, text = prompt_language, text.strip("\n") - zero_wav = np.zeros(int(hps.data.sampling_rate * 0.3), dtype=np.float16 if is_half == True else np.float32) - with torch.no_grad(): - wav16k, sr = librosa.load(ref_wav_path, sr=16000) - wav16k = torch.from_numpy(wav16k) - zero_wav_torch = torch.from_numpy(zero_wav) - if (is_half == True): - wav16k = wav16k.half().to(device) - zero_wav_torch = zero_wav_torch.half().to(device) - else: - wav16k = wav16k.to(device) - zero_wav_torch = zero_wav_torch.to(device) - wav16k = torch.cat([wav16k, zero_wav_torch]) - ssl_content = ssl_model.model(wav16k.unsqueeze(0))["last_hidden_state"].transpose(1, 2) # .float() - codes = vq_model.extract_latent(ssl_content) - prompt_semantic = codes[0, 0] - t1 = ttime() - prompt_language = dict_language[prompt_language.lower()] - text_language = dict_language[text_language.lower()] - phones1, bert1, norm_text1 = get_phones_and_bert(prompt_text, prompt_language) - texts = text.split("\n") - audio_bytes = BytesIO() - - for text in texts: - # 简单防止纯符号引发参考音频泄露 - if only_punc(text): - continue - - audio_opt = [] - phones2, bert2, norm_text2 = get_phones_and_bert(text, text_language) - bert = torch.cat([bert1, bert2], 1) - - all_phoneme_ids = torch.LongTensor(phones1 + phones2).to(device).unsqueeze(0) - bert = bert.to(device).unsqueeze(0) - all_phoneme_len = torch.tensor([all_phoneme_ids.shape[-1]]).to(device) - prompt = prompt_semantic.unsqueeze(0).to(device) - t2 = ttime() - with torch.no_grad(): - # pred_semantic = t2s_model.model.infer( - pred_semantic, idx = t2s_model.model.infer_panel( - all_phoneme_ids, - all_phoneme_len, - prompt, - bert, - # prompt_phone_len=ph_offset, - top_k=config['inference']['top_k'], - early_stop_num=hz * max_sec) - t3 = ttime() - # print(pred_semantic.shape,idx) - pred_semantic = pred_semantic[:, -idx:].unsqueeze(0) # .unsqueeze(0)#mq要多unsqueeze一次 - refer = get_spepc(hps, ref_wav_path) # .to(device) - if (is_half == True): - refer = refer.half().to(device) - else: - refer = refer.to(device) - # audio = vq_model.decode(pred_semantic, all_phoneme_ids, refer).detach().cpu().numpy()[0, 0] - audio = \ - vq_model.decode(pred_semantic, torch.LongTensor(phones2).to(device).unsqueeze(0), - refer).detach().cpu().numpy()[ - 0, 0] ###试试重建不带上prompt部分 - audio_opt.append(audio) - audio_opt.append(zero_wav) - t4 = ttime() - audio_bytes = pack_audio(audio_bytes,(np.concatenate(audio_opt, 0) * 32768).astype(np.int16),hps.data.sampling_rate) - # logger.info("%.3f\t%.3f\t%.3f\t%.3f" % (t1 - t0, t2 - t1, t3 - t2, t4 - t3)) - if stream_mode == "normal": - audio_bytes, audio_chunk = read_clean_buffer(audio_bytes) - yield audio_chunk - - if not stream_mode == "normal": - if media_type == "wav": - audio_bytes = pack_wav(audio_bytes,hps.data.sampling_rate) - yield audio_bytes.getvalue() - - - -def handle_control(command): - if command == "restart": - os.execl(g_config.python_exec, g_config.python_exec, *sys.argv) - elif command == "exit": - os.kill(os.getpid(), signal.SIGTERM) - exit(0) - - -def handle_change(path, text, language): - if is_empty(path, text, language): - return JSONResponse({"code": 400, "message": '缺少任意一项以下参数: "path", "text", "language"'}, status_code=400) - - if path != "" or path is not None: - default_refer.path = path - if text != "" or text is not None: - default_refer.text = text - if language != "" or language is not None: - default_refer.language = language - - logger.info(f"当前默认参考音频路径: {default_refer.path}") - logger.info(f"当前默认参考音频文本: {default_refer.text}") - logger.info(f"当前默认参考音频语种: {default_refer.language}") - logger.info(f"is_ready: {default_refer.is_ready()}") - - - return JSONResponse({"code": 0, "message": "Success"}, status_code=200) - - -def handle(refer_wav_path, prompt_text, prompt_language, text, text_language, cut_punc): - if ( - refer_wav_path == "" or refer_wav_path is None - or prompt_text == "" or prompt_text is None - or prompt_language == "" or prompt_language is None - ): - refer_wav_path, prompt_text, prompt_language = ( - default_refer.path, - default_refer.text, - default_refer.language, - ) - if not default_refer.is_ready(): - return JSONResponse({"code": 400, "message": "未指定参考音频且接口无预设"}, status_code=400) - - if cut_punc == None: - text = cut_text(text,default_cut_punc) - else: - text = cut_text(text,cut_punc) - - return StreamingResponse(get_tts_wav(refer_wav_path, prompt_text, prompt_language, text, text_language), media_type="audio/"+media_type) - - - - -# -------------------------------- -# 初始化部分 -# -------------------------------- -dict_language = { - "中文": "all_zh", - "英文": "en", - "日文": "all_ja", - "中英混合": "zh", - "日英混合": "ja", - "多语种混合": "auto", #多语种启动切分识别语种 - "all_zh": "all_zh", - "en": "en", - "all_ja": "all_ja", - "zh": "zh", - "ja": "ja", - "auto": "auto", -} - -# logger -logging.config.dictConfig(uvicorn.config.LOGGING_CONFIG) -logger = logging.getLogger('uvicorn') - -# 获取配置 -g_config = global_config.Config() - -# 获取参数 -parser = argparse.ArgumentParser(description="GPT-SoVITS api") - -parser.add_argument("-s", "--sovits_path", type=str, default=g_config.sovits_path, help="SoVITS模型路径") -parser.add_argument("-g", "--gpt_path", type=str, default=g_config.gpt_path, help="GPT模型路径") -parser.add_argument("-dr", "--default_refer_path", type=str, default="", help="默认参考音频路径") -parser.add_argument("-dt", "--default_refer_text", type=str, default="", help="默认参考音频文本") -parser.add_argument("-dl", "--default_refer_language", type=str, default="", help="默认参考音频语种") -parser.add_argument("-d", "--device", type=str, default=g_config.infer_device, help="cuda / cpu") -parser.add_argument("-a", "--bind_addr", type=str, default="0.0.0.0", help="default: 0.0.0.0") -parser.add_argument("-p", "--port", type=int, default=g_config.api_port, help="default: 9880") -parser.add_argument("-fp", "--full_precision", action="store_true", default=False, help="覆盖config.is_half为False, 使用全精度") -parser.add_argument("-hp", "--half_precision", action="store_true", default=False, help="覆盖config.is_half为True, 使用半精度") -# bool值的用法为 `python ./api.py -fp ...` -# 此时 full_precision==True, half_precision==False -parser.add_argument("-sm", "--stream_mode", type=str, default="close", help="流式返回模式, close / normal / keepalive") -parser.add_argument("-mt", "--media_type", type=str, default="wav", help="音频编码格式, wav / ogg / aac") -parser.add_argument("-cp", "--cut_punc", type=str, default="", help="文本切分符号设定, 符号范围,.;?!、,。?!;:…") -# 切割常用分句符为 `python ./api.py -cp ".?!。?!"` -parser.add_argument("-hb", "--hubert_path", type=str, default=g_config.cnhubert_path, help="覆盖config.cnhubert_path") -parser.add_argument("-b", "--bert_path", type=str, default=g_config.bert_path, help="覆盖config.bert_path") - -args = parser.parse_args() -sovits_path = args.sovits_path -gpt_path = args.gpt_path -device = args.device -port = args.port -host = args.bind_addr -cnhubert_base_path = args.hubert_path -bert_path = args.bert_path -default_cut_punc = args.cut_punc - -# 应用参数配置 -default_refer = DefaultRefer(args.default_refer_path, args.default_refer_text, args.default_refer_language) - -# 模型路径检查 -if sovits_path == "": - sovits_path = g_config.pretrained_sovits_path - logger.warn(f"未指定SoVITS模型路径, fallback后当前值: {sovits_path}") -if gpt_path == "": - gpt_path = g_config.pretrained_gpt_path - logger.warn(f"未指定GPT模型路径, fallback后当前值: {gpt_path}") - -# 指定默认参考音频, 调用方 未提供/未给全 参考音频参数时使用 -if default_refer.path == "" or default_refer.text == "" or default_refer.language == "": - default_refer.path, default_refer.text, default_refer.language = "", "", "" - logger.info("未指定默认参考音频") -else: - logger.info(f"默认参考音频路径: {default_refer.path}") - logger.info(f"默认参考音频文本: {default_refer.text}") - logger.info(f"默认参考音频语种: {default_refer.language}") - -# 获取半精度 -is_half = g_config.is_half -if args.full_precision: - is_half = False -if args.half_precision: - is_half = True -if args.full_precision and args.half_precision: - is_half = g_config.is_half # 炒饭fallback -logger.info(f"半精: {is_half}") - -# 流式返回模式 -if args.stream_mode.lower() in ["normal","n"]: - stream_mode = "normal" - logger.info("流式返回已开启") -else: - stream_mode = "close" - -# 音频编码格式 -if args.media_type.lower() in ["aac","ogg"]: - media_type = args.media_type.lower() -elif stream_mode == "close": - media_type = "wav" -else: - media_type = "ogg" -logger.info(f"编码格式: {media_type}") - -# 初始化模型 -cnhubert.cnhubert_base_path = cnhubert_base_path -tokenizer = AutoTokenizer.from_pretrained(bert_path) -bert_model = AutoModelForMaskedLM.from_pretrained(bert_path) -ssl_model = cnhubert.get_model() -if is_half: - bert_model = bert_model.half().to(device) - ssl_model = ssl_model.half().to(device) -else: - bert_model = bert_model.to(device) - ssl_model = ssl_model.to(device) -change_sovits_weights(sovits_path) -change_gpt_weights(gpt_path) - - - - -# -------------------------------- -# 接口部分 -# -------------------------------- -app = FastAPI() - -@app.post("/set_model") -async def set_model(request: Request): - json_post_raw = await request.json() - global gpt_path - gpt_path=json_post_raw.get("gpt_model_path") - global sovits_path - sovits_path=json_post_raw.get("sovits_model_path") - logger.info("gptpath"+gpt_path+";vitspath"+sovits_path) - change_sovits_weights(sovits_path) - change_gpt_weights(gpt_path) - return "ok" - - -@app.post("/control") -async def control(request: Request): - json_post_raw = await request.json() - return handle_control(json_post_raw.get("command")) - - -@app.get("/control") -async def control(command: str = None): - return handle_control(command) - - -@app.post("/change_refer") -async def change_refer(request: Request): - json_post_raw = await request.json() - return handle_change( - json_post_raw.get("refer_wav_path"), - json_post_raw.get("prompt_text"), - json_post_raw.get("prompt_language") - ) - - -@app.get("/change_refer") -async def change_refer( - refer_wav_path: str = None, - prompt_text: str = None, - prompt_language: str = None -): - return handle_change(refer_wav_path, prompt_text, prompt_language) - - -@app.post("/") -async def tts_endpoint(request: Request): - json_post_raw = await request.json() - return handle( - json_post_raw.get("refer_wav_path"), - json_post_raw.get("prompt_text"), - json_post_raw.get("prompt_language"), - json_post_raw.get("text"), - json_post_raw.get("text_language"), - json_post_raw.get("cut_punc"), - ) - - -@app.get("/") -async def tts_endpoint( - refer_wav_path: str = None, - prompt_text: str = None, - prompt_language: str = None, - text: str = None, - text_language: str = None, - cut_punc: str = None, -): - return handle(refer_wav_path, prompt_text, prompt_language, text, text_language, cut_punc) - - -if __name__ == "__main__": - uvicorn.run(app, host=host, port=port, workers=1) diff --git a/config.py b/config.py index 1f741285..442019c5 100644 --- a/config.py +++ b/config.py @@ -1,4 +1,4 @@ -import sys,os +import sys, os import torch @@ -7,8 +7,8 @@ sovits_path = "" gpt_path = "" is_half_str = os.environ.get("is_half", "True") is_half = True if is_half_str.lower() == 'true' else False -is_share_str = os.environ.get("is_share","False") -is_share= True if is_share_str.lower() == 'true' else False +is_share_str = os.environ.get("is_share", "False") +is_share = True if is_share_str.lower() == 'true' else False cnhubert_path = "GPT_SoVITS/pretrained_models/chinese-hubert-base" bert_path = "GPT_SoVITS/pretrained_models/chinese-roberta-wwm-ext-large" @@ -18,9 +18,9 @@ pretrained_gpt_path = "GPT_SoVITS/pretrained_models/s1bert25hz-2kh-longer-epoch= exp_root = "logs" python_exec = sys.executable or "python" if torch.cuda.is_available(): - infer_device = "cuda" + infer_device = "cuda" else: - infer_device = "cpu" + infer_device = "cpu" webui_port_main = 9874 webui_port_uvr5 = 9873 @@ -30,37 +30,38 @@ webui_port_subfix = 9871 api_port = 9880 if infer_device == "cuda": - gpu_name = torch.cuda.get_device_name(0) - if ( - ("16" in gpu_name and "V100" not in gpu_name.upper()) - or "P40" in gpu_name.upper() - or "P10" in gpu_name.upper() - or "1060" in gpu_name - or "1070" in gpu_name - or "1080" in gpu_name - ): - is_half=False + gpu_name = torch.cuda.get_device_name(0) + if ( + ("16" in gpu_name and "V100" not in gpu_name.upper()) + or "P40" in gpu_name.upper() + or "P10" in gpu_name.upper() + or "1060" in gpu_name + or "1070" in gpu_name + or "1080" in gpu_name + ): + is_half = False + +if (infer_device == "cpu"): is_half = False -if(infer_device=="cpu"):is_half=False class Config: - def __init__(self): - self.sovits_path = sovits_path - self.gpt_path = gpt_path - self.is_half = is_half + def __init__(self): + self.sovits_path = sovits_path + self.gpt_path = gpt_path + self.is_half = is_half - self.cnhubert_path = cnhubert_path - self.bert_path = bert_path - self.pretrained_sovits_path = pretrained_sovits_path - self.pretrained_gpt_path = pretrained_gpt_path + self.cnhubert_path = cnhubert_path + self.bert_path = bert_path + self.pretrained_sovits_path = pretrained_sovits_path + self.pretrained_gpt_path = pretrained_gpt_path - self.exp_root = exp_root - self.python_exec = python_exec - self.infer_device = infer_device + self.exp_root = exp_root + self.python_exec = python_exec + self.infer_device = infer_device - self.webui_port_main = webui_port_main - self.webui_port_uvr5 = webui_port_uvr5 - self.webui_port_infer_tts = webui_port_infer_tts - self.webui_port_subfix = webui_port_subfix + self.webui_port_main = webui_port_main + self.webui_port_uvr5 = webui_port_uvr5 + self.webui_port_infer_tts = webui_port_infer_tts + self.webui_port_subfix = webui_port_subfix - self.api_port = api_port + self.api_port = api_port diff --git a/main.py b/main.py new file mode 100644 index 00000000..d0279bb2 --- /dev/null +++ b/main.py @@ -0,0 +1,49 @@ +import argparse + +import uvicorn +import config as global_config + +from app import app +from cmd_args import CmdArgs + +g_config = global_config.Config() +# 获取参数 +parser = argparse.ArgumentParser(description="GPT-SoVITS api") + +parser.add_argument("-s", "--sovits_path", type=str, default=g_config.sovits_path, help="SoVITS模型路径") +parser.add_argument("-g", "--gpt_path", type=str, default=g_config.gpt_path, help="GPT模型路径") +parser.add_argument("-dr", "--default_refer_path", type=str, default="", help="默认参考音频路径") +parser.add_argument("-dt", "--default_refer_text", type=str, default="", help="默认参考音频文本") +parser.add_argument("-dl", "--default_refer_language", type=str, default="", help="默认参考音频语种") +parser.add_argument("-d", "--device", type=str, default=g_config.infer_device, help="cuda / cpu") +parser.add_argument("-a", "--bind_addr", type=str, default="0.0.0.0", help="default: 0.0.0.0") +parser.add_argument("-p", "--port", type=int, default=g_config.api_port, help="default: 9880") +parser.add_argument("-fp", "--full_precision", action="store_true", default=False, + help="覆盖config.is_half为False, 使用全精度") +parser.add_argument("-hp", "--half_precision", action="store_true", default=False, + help="覆盖config.is_half为True, 使用半精度") +# bool值的用法为 `python ./tts_service.py -fp ...` +# 此时 full_precision==True, half_precision==False +parser.add_argument("-sm", "--stream_mode", type=str, default="close", help="流式返回模式, close / normal / keepalive") +parser.add_argument("-mt", "--media_type", type=str, default="wav", help="音频编码格式, wav / ogg / aac") +parser.add_argument("-cp", "--cut_punc", type=str, default="", help="文本切分符号设定, 符号范围,.;?!、,。?!;:…") +# 切割常用分句符为 `python ./tts_service.py -cp ".?!。?!"` +parser.add_argument("-hb", "--hubert_path", type=str, default=g_config.cnhubert_path, help="覆盖config.cnhubert_path") +parser.add_argument("-b", "--bert_path", type=str, default=g_config.bert_path, help="覆盖config.bert_path") + +args = parser.parse_args() + +# 保存参数到单例对象中 +cmd_args = CmdArgs() +cmd_args.set_args(args) +import server +server.register_Hanlder(app) + + +if __name__ == "__main__": + + port = args.port + host = args.bind_addr + uvicorn.run(app, host=host, port=port, workers=1) + + diff --git a/api.md b/server/api.md similarity index 100% rename from api.md rename to server/api.md diff --git a/server/app.py b/server/app.py new file mode 100644 index 00000000..ee1b8dd5 --- /dev/null +++ b/server/app.py @@ -0,0 +1,6 @@ +from fastapi import FastAPI + +from pyutils.logs import llog + +llog.info("start server") +app = FastAPI() diff --git a/server/cmd_args.py b/server/cmd_args.py new file mode 100644 index 00000000..be1e1673 --- /dev/null +++ b/server/cmd_args.py @@ -0,0 +1,13 @@ +class CmdArgs: + _instance = None + + def __new__(cls, *args, **kwargs): + if cls._instance is None: + cls._instance = super(CmdArgs, cls).__new__(cls) + return cls._instance + + def set_args(self, args): + self.args = args + + def get_args(self): + return self.args diff --git a/server/handlers.py b/server/handlers.py new file mode 100644 index 00000000..bc164745 --- /dev/null +++ b/server/handlers.py @@ -0,0 +1,72 @@ +from fastapi import APIRouter, Request +from pyutils.logs import llog +from tts_service import change_sovits_weights, change_gpt_weights, handle_control, handle_change, handle + +index_router = APIRouter() + +@index_router.post("/set_model") +async def set_model(request: Request): + json_post_raw = await request.json() + global gpt_path + gpt_path = json_post_raw.get("gpt_model_path") + global sovits_path + sovits_path = json_post_raw.get("sovits_model_path") + llog.info("gptpath" + gpt_path + ";vitspath" + sovits_path) + change_sovits_weights(sovits_path) + change_gpt_weights(gpt_path) + return "ok" + + +@index_router.post("/control") +async def control(request: Request): + json_post_raw = await request.json() + return handle_control(json_post_raw.get("command")) + + +@index_router.get("/control") +async def control(command: str = None): + return handle_control(command) + + +@index_router.post("/change_refer") +async def change_refer(request: Request): + json_post_raw = await request.json() + return handle_change( + json_post_raw.get("refer_wav_path"), + json_post_raw.get("prompt_text"), + json_post_raw.get("prompt_language") + ) + + +@index_router.get("/change_refer") +async def change_refer( + refer_wav_path: str = None, + prompt_text: str = None, + prompt_language: str = None +): + return handle_change(refer_wav_path, prompt_text, prompt_language) + + +@index_router.post("/") +async def tts_endpoint(request: Request): + json_post_raw = await request.json() + return handle( + json_post_raw.get("refer_wav_path"), + json_post_raw.get("prompt_text"), + json_post_raw.get("prompt_language"), + json_post_raw.get("text"), + json_post_raw.get("text_language"), + json_post_raw.get("cut_punc"), + ) + + +@index_router.get("/") +async def tts_endpoint( + refer_wav_path: str = None, + prompt_text: str = None, + prompt_language: str = None, + text: str = None, + text_language: str = None, + cut_punc: str = None, +): + return handle(refer_wav_path, prompt_text, prompt_language, text, text_language, cut_punc) diff --git a/server/server.py b/server/server.py new file mode 100644 index 00000000..34e8beec --- /dev/null +++ b/server/server.py @@ -0,0 +1,3 @@ +def register_Hanlder(app): + from handlers import index_router + app.include_router(index_router) diff --git a/server/tts_service.py b/server/tts_service.py new file mode 100644 index 00000000..43b70845 --- /dev/null +++ b/server/tts_service.py @@ -0,0 +1,510 @@ +import os +import re +import sys + +from cmd_args import CmdArgs +from pyutils.logs import llog + +current_project_dir = os.getcwd() +sys.path.append(current_project_dir) +sys.path.append("%s/GPT_SoVITS" % (current_project_dir)) + +import signal +import LangSegment +from time import time as ttime +import torch +import librosa +import soundfile as sf +from fastapi.responses import StreamingResponse, JSONResponse +from transformers import AutoModelForMaskedLM, AutoTokenizer +import numpy as np +from feature_extractor import cnhubert +from io import BytesIO +from module.models import SynthesizerTrn +from AR.models.t2s_lightning_module import Text2SemanticLightningModule +from text import cleaned_text_to_sequence +from text.cleaner import clean_text +from module.mel_processing import spectrogram_torch +from pyutils.np_utils import load_audio +import subprocess +import config as global_config + +g_config = global_config.Config() + + +class DefaultRefer: + def __init__(self, path, text, language): + self.path = args.default_refer_path + self.text = args.default_refer_text + self.language = args.default_refer_language + + def is_ready(self) -> bool: + return is_not_empty(self.path, self.text, self.language) + + +def is_empty(*items): # 任意一项不为空返回False + for item in items: + if item is not None and item != "": + return False + return True + + +def is_not_empty(*items): # 任意一项为空返回False + for item in items: + if item is None or item == "": + return False + return True + + +def change_sovits_weights(sovits_path): + global vq_model, hps + dict_s2 = torch.load(sovits_path, map_location="cpu") + hps = dict_s2["config"] + hps = DictToAttrRecursive(hps) + hps.model.semantic_frame_rate = "25hz" + model_params_dict = vars(hps.model) + vq_model = SynthesizerTrn( + hps.data.filter_length // 2 + 1, + hps.train.segment_size // hps.data.hop_length, + n_speakers=hps.data.n_speakers, + **model_params_dict + ) + if ("pretrained" not in sovits_path): + del vq_model.enc_q + if is_half == True: + vq_model = vq_model.half().to(device) + else: + vq_model = vq_model.to(device) + vq_model.eval() + vq_model.load_state_dict(dict_s2["weight"], strict=False) + + +def change_gpt_weights(gpt_path): + global hz, max_sec, t2s_model, config + hz = 50 + dict_s1 = torch.load(gpt_path, map_location="cpu") + config = dict_s1["config"] + max_sec = config["data"]["max_sec"] + t2s_model = Text2SemanticLightningModule(config, "****", is_train=False) + t2s_model.load_state_dict(dict_s1["weight"]) + if is_half == True: + t2s_model = t2s_model.half() + t2s_model = t2s_model.to(device) + t2s_model.eval() + total = sum([param.nelement() for param in t2s_model.parameters()]) + llog.info("Number of parameter: %.2fM" % (total / 1e6)) + + +def get_bert_feature(text, word2ph): + with torch.no_grad(): + inputs = tokenizer(text, return_tensors="pt") + for i in inputs: + inputs[i] = inputs[i].to(device) #####输入是long不用管精度问题,精度随bert_model + res = bert_model(**inputs, output_hidden_states=True) + res = torch.cat(res["hidden_states"][-3:-2], -1)[0].cpu()[1:-1] + assert len(word2ph) == len(text) + phone_level_feature = [] + for i in range(len(word2ph)): + repeat_feature = res[i].repeat(word2ph[i], 1) + phone_level_feature.append(repeat_feature) + phone_level_feature = torch.cat(phone_level_feature, dim=0) + # if(is_half==True):phone_level_feature=phone_level_feature.half() + return phone_level_feature.T + + +def clean_text_inf(text, language): + phones, word2ph, norm_text = clean_text(text, language) + phones = cleaned_text_to_sequence(phones) + return phones, word2ph, norm_text + + +def get_bert_inf(phones, word2ph, norm_text, language): + language = language.replace("all_", "") + if language == "zh": + bert = get_bert_feature(norm_text, word2ph).to(device) # .to(dtype) + else: + bert = torch.zeros( + (1024, len(phones)), + dtype=torch.float16 if is_half == True else torch.float32, + ).to(device) + + return bert + + +def get_phones_and_bert(text, language): + if language in {"en", "all_zh", "all_ja"}: + language = language.replace("all_", "") + if language == "en": + LangSegment.setfilters(["en"]) + formattext = " ".join(tmp["text"] for tmp in LangSegment.getTexts(text)) + else: + # 因无法区别中日文汉字,以用户输入为准 + formattext = text + while " " in formattext: + formattext = formattext.replace(" ", " ") + phones, word2ph, norm_text = clean_text_inf(formattext, language) + if language == "zh": + bert = get_bert_feature(norm_text, word2ph).to(device) + else: + bert = torch.zeros( + (1024, len(phones)), + dtype=torch.float16 if is_half == True else torch.float32, + ).to(device) + elif language in {"zh", "ja", "auto"}: + textlist = [] + langlist = [] + LangSegment.setfilters(["zh", "ja", "en", "ko"]) + if language == "auto": + for tmp in LangSegment.getTexts(text): + if tmp["lang"] == "ko": + langlist.append("zh") + textlist.append(tmp["text"]) + else: + langlist.append(tmp["lang"]) + textlist.append(tmp["text"]) + else: + for tmp in LangSegment.getTexts(text): + if tmp["lang"] == "en": + langlist.append(tmp["lang"]) + else: + # 因无法区别中日文汉字,以用户输入为准 + langlist.append(language) + textlist.append(tmp["text"]) + # llog.info(textlist) + # llog.info(langlist) + phones_list = [] + bert_list = [] + norm_text_list = [] + for i in range(len(textlist)): + lang = langlist[i] + phones, word2ph, norm_text = clean_text_inf(textlist[i], lang) + bert = get_bert_inf(phones, word2ph, norm_text, lang) + phones_list.append(phones) + norm_text_list.append(norm_text) + bert_list.append(bert) + bert = torch.cat(bert_list, dim=1) + phones = sum(phones_list, []) + norm_text = ''.join(norm_text_list) + + return phones, bert.to(torch.float16 if is_half == True else torch.float32), norm_text + + +class DictToAttrRecursive: + def __init__(self, input_dict): + for key, value in input_dict.items(): + if isinstance(value, dict): + # 如果值是字典,递归调用构造函数 + setattr(self, key, DictToAttrRecursive(value)) + else: + setattr(self, key, value) + + +def get_spepc(hps, filename): + audio = load_audio(filename, int(hps.data.sampling_rate)) + audio = torch.FloatTensor(audio) + audio_norm = audio + audio_norm = audio_norm.unsqueeze(0) + spec = spectrogram_torch(audio_norm, hps.data.filter_length, hps.data.sampling_rate, hps.data.hop_length, + hps.data.win_length, center=False) + return spec + + +def pack_audio(audio_bytes, data, rate): + if media_type == "ogg": + audio_bytes = pack_ogg(audio_bytes, data, rate) + elif media_type == "aac": + audio_bytes = pack_aac(audio_bytes, data, rate) + else: + # wav无法流式, 先暂存raw + audio_bytes = pack_raw(audio_bytes, data, rate) + + return audio_bytes + + +def pack_ogg(audio_bytes, data, rate): + with sf.SoundFile(audio_bytes, mode='w', samplerate=rate, channels=1, format='ogg') as audio_file: + audio_file.write(data) + + return audio_bytes + + +def pack_raw(audio_bytes, data, rate): + audio_bytes.write(data.tobytes()) + + return audio_bytes + + +def pack_wav(audio_bytes, rate): + data = np.frombuffer(audio_bytes.getvalue(), dtype=np.int16) + wav_bytes = BytesIO() + sf.write(wav_bytes, data, rate, format='wav') + + return wav_bytes + + +def pack_aac(audio_bytes, data, rate): + process = subprocess.Popen([ + 'ffmpeg', + '-f', 's16le', # 输入16位有符号小端整数PCM + '-ar', str(rate), # 设置采样率 + '-ac', '1', # 单声道 + '-i', 'pipe:0', # 从管道读取输入 + '-c:a', 'aac', # 音频编码器为AAC + '-b:a', '192k', # 比特率 + '-vn', # 不包含视频 + '-f', 'adts', # 输出AAC数据流格式 + 'pipe:1' # 将输出写入管道 + ], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + out, _ = process.communicate(input=data.tobytes()) + audio_bytes.write(out) + + return audio_bytes + + +def read_clean_buffer(audio_bytes): + audio_chunk = audio_bytes.getvalue() + audio_bytes.truncate(0) + audio_bytes.seek(0) + + return audio_bytes, audio_chunk + + +def cut_text(text, punc): + punc_list = [p for p in punc if p in {",", ".", ";", "?", "!", "、", ",", "。", "?", "!", ";", ":", "…"}] + if len(punc_list) > 0: + punds = r"[" + "".join(punc_list) + r"]" + text = text.strip("\n") + items = re.split(f"({punds})", text) + mergeitems = ["".join(group) for group in zip(items[::2], items[1::2])] + # 在句子不存在符号或句尾无符号的时候保证文本完整 + if len(items) % 2 == 1: + mergeitems.append(items[-1]) + text = "\n".join(mergeitems) + + while "\n\n" in text: + text = text.replace("\n\n", "\n") + + return text + + +def only_punc(text): + return not any(t.isalnum() or t.isalpha() for t in text) + + +def get_tts_wav(ref_wav_path, prompt_text, prompt_language, text, text_language): + t0 = ttime() + prompt_text = prompt_text.strip("\n") + prompt_language, text = prompt_language, text.strip("\n") + zero_wav = np.zeros(int(hps.data.sampling_rate * 0.3), dtype=np.float16 if is_half == True else np.float32) + with torch.no_grad(): + wav16k, sr = librosa.load(ref_wav_path, sr=16000) + wav16k = torch.from_numpy(wav16k) + zero_wav_torch = torch.from_numpy(zero_wav) + if (is_half == True): + wav16k = wav16k.half().to(device) + zero_wav_torch = zero_wav_torch.half().to(device) + else: + wav16k = wav16k.to(device) + zero_wav_torch = zero_wav_torch.to(device) + wav16k = torch.cat([wav16k, zero_wav_torch]) + ssl_content = ssl_model.model(wav16k.unsqueeze(0))["last_hidden_state"].transpose(1, 2) # .float() + codes = vq_model.extract_latent(ssl_content) + prompt_semantic = codes[0, 0] + t1 = ttime() + prompt_language = dict_language[prompt_language.lower()] + text_language = dict_language[text_language.lower()] + phones1, bert1, norm_text1 = get_phones_and_bert(prompt_text, prompt_language) + texts = text.split("\n") + audio_bytes = BytesIO() + + for text in texts: + # 简单防止纯符号引发参考音频泄露 + if only_punc(text): + continue + + audio_opt = [] + phones2, bert2, norm_text2 = get_phones_and_bert(text, text_language) + bert = torch.cat([bert1, bert2], 1) + + all_phoneme_ids = torch.LongTensor(phones1 + phones2).to(device).unsqueeze(0) + bert = bert.to(device).unsqueeze(0) + all_phoneme_len = torch.tensor([all_phoneme_ids.shape[-1]]).to(device) + prompt = prompt_semantic.unsqueeze(0).to(device) + t2 = ttime() + with torch.no_grad(): + # pred_semantic = t2s_model.model.infer( + pred_semantic, idx = t2s_model.model.infer_panel( + all_phoneme_ids, + all_phoneme_len, + prompt, + bert, + # prompt_phone_len=ph_offset, + top_k=config['inference']['top_k'], + early_stop_num=hz * max_sec) + t3 = ttime() + # print(pred_semantic.shape,idx) + pred_semantic = pred_semantic[:, -idx:].unsqueeze(0) # .unsqueeze(0)#mq要多unsqueeze一次 + refer = get_spepc(hps, ref_wav_path) # .to(device) + if (is_half == True): + refer = refer.half().to(device) + else: + refer = refer.to(device) + # audio = vq_model.decode(pred_semantic, all_phoneme_ids, refer).detach().cpu().numpy()[0, 0] + audio = \ + vq_model.decode(pred_semantic, torch.LongTensor(phones2).to(device).unsqueeze(0), + refer).detach().cpu().numpy()[ + 0, 0] ###试试重建不带上prompt部分 + audio_opt.append(audio) + audio_opt.append(zero_wav) + t4 = ttime() + audio_bytes = pack_audio(audio_bytes, (np.concatenate(audio_opt, 0) * 32768).astype(np.int16), + hps.data.sampling_rate) + # llog.info("%.3f\t%.3f\t%.3f\t%.3f" % (t1 - t0, t2 - t1, t3 - t2, t4 - t3)) + if stream_mode == "normal": + audio_bytes, audio_chunk = read_clean_buffer(audio_bytes) + yield audio_chunk + + if not stream_mode == "normal": + if media_type == "wav": + audio_bytes = pack_wav(audio_bytes, hps.data.sampling_rate) + yield audio_bytes.getvalue() + + +def handle_control(command): + if command == "restart": + os.execl(g_config.python_exec, g_config.python_exec, *sys.argv) + elif command == "exit": + os.kill(os.getpid(), signal.SIGTERM) + exit(0) + + +def handle_change(path, text, language): + if is_empty(path, text, language): + return JSONResponse({"code": 400, "message": '缺少任意一项以下参数: "path", "text", "language"'}, status_code=400) + + if path != "" or path is not None: + default_refer.path = path + if text != "" or text is not None: + default_refer.text = text + if language != "" or language is not None: + default_refer.language = language + + llog.info(f"当前默认参考音频路径: {default_refer.path}") + llog.info(f"当前默认参考音频文本: {default_refer.text}") + llog.info(f"当前默认参考音频语种: {default_refer.language}") + llog.info(f"is_ready: {default_refer.is_ready()}") + + return JSONResponse({"code": 0, "message": "Success"}, status_code=200) + + +def handle(refer_wav_path, prompt_text, prompt_language, text, text_language, cut_punc): + if ( + refer_wav_path == "" or refer_wav_path is None + or prompt_text == "" or prompt_text is None + or prompt_language == "" or prompt_language is None + ): + refer_wav_path, prompt_text, prompt_language = ( + default_refer.path, + default_refer.text, + default_refer.language, + ) + if not default_refer.is_ready(): + return JSONResponse({"code": 400, "message": "未指定参考音频且接口无预设"}, status_code=400) + + if cut_punc == None: + text = cut_text(text, default_cut_punc) + else: + text = cut_text(text, cut_punc) + + return StreamingResponse(get_tts_wav(refer_wav_path, prompt_text, prompt_language, text, text_language), + media_type="audio/" + media_type) + + +# -------------------------------- +# 初始化部分 +# -------------------------------- +dict_language = { + "中文": "all_zh", + "英文": "en", + "日文": "all_ja", + "中英混合": "zh", + "日英混合": "ja", + "多语种混合": "auto", # 多语种启动切分识别语种 + "all_zh": "all_zh", + "en": "en", + "all_ja": "all_ja", + "zh": "zh", + "ja": "ja", + "auto": "auto", +} + +# 获取配置 +cmd_args = CmdArgs() +args = cmd_args.get_args() +sovits_path = args.sovits_path +gpt_path = args.gpt_path +device = args.device + +cnhubert_base_path = args.hubert_path +bert_path = args.bert_path +default_cut_punc = args.cut_punc + +# 应用参数配置 +default_refer = DefaultRefer(args.default_refer_path, args.default_refer_text, args.default_refer_language) + +# 模型路径检查 +if sovits_path == "": + sovits_path = g_config.pretrained_sovits_path + llog.warn(f"未指定SoVITS模型路径, fallback后当前值: {sovits_path}") +if gpt_path == "": + gpt_path = g_config.pretrained_gpt_path + llog.warn(f"未指定GPT模型路径, fallback后当前值: {gpt_path}") + +# 指定默认参考音频, 调用方 未提供/未给全 参考音频参数时使用 +if default_refer.path == "" or default_refer.text == "" or default_refer.language == "": + default_refer.path, default_refer.text, default_refer.language = "", "", "" + llog.info("未指定默认参考音频") +else: + llog.info(f"默认参考音频路径: {default_refer.path}") + llog.info(f"默认参考音频文本: {default_refer.text}") + llog.info(f"默认参考音频语种: {default_refer.language}") + +# 获取半精度 +is_half = g_config.is_half +if args.full_precision: + is_half = False +if args.half_precision: + is_half = True +if args.full_precision and args.half_precision: + is_half = g_config.is_half # 炒饭fallback +llog.info(f"半精: {is_half}") + +# 流式返回模式 +if args.stream_mode.lower() in ["normal", "n"]: + stream_mode = "normal" + llog.info("流式返回已开启") +else: + stream_mode = "close" + +# 音频编码格式 +if args.media_type.lower() in ["aac", "ogg"]: + media_type = args.media_type.lower() +elif stream_mode == "close": + media_type = "wav" +else: + media_type = "ogg" +llog.info(f"编码格式: {media_type}") + +# 初始化模型 +cnhubert.cnhubert_base_path = cnhubert_base_path +tokenizer = AutoTokenizer.from_pretrained(bert_path) +bert_model = AutoModelForMaskedLM.from_pretrained(bert_path) +ssl_model = cnhubert.get_model() +if is_half: + bert_model = bert_model.half().to(device) + ssl_model = ssl_model.half().to(device) +else: + bert_model = bert_model.to(device) + ssl_model = ssl_model.to(device) +change_sovits_weights(sovits_path) +change_gpt_weights(gpt_path) From 2f85395675d35fabcea00131a70f040a899d0dcc Mon Sep 17 00:00:00 2001 From: litongjava Date: Tue, 21 May 2024 18:37:49 -1000 Subject: [PATCH 6/6] add memory endpoint to the server api code --- docs/cn/inference_cpu.md | 63 --------------------------- docs/cn/inference_cpu_files/memory.md | 18 ++++++++ requirements.txt | 2 +- server/handlers.py | 12 +++++ server/memory_service.py | 37 ++++++++++++++++ 5 files changed, 68 insertions(+), 64 deletions(-) delete mode 100644 docs/cn/inference_cpu.md create mode 100644 docs/cn/inference_cpu_files/memory.md create mode 100644 server/memory_service.py diff --git a/docs/cn/inference_cpu.md b/docs/cn/inference_cpu.md deleted file mode 100644 index e72c8b20..00000000 --- a/docs/cn/inference_cpu.md +++ /dev/null @@ -1,63 +0,0 @@ -# 推理 - -## Windows - -### 使用cpu推理 -本文档介绍如何使用cpu进行推理,使用cpu的推理速度有点慢,但不是很慢 - -#### 安装依赖 -``` -# 拉取项目代码 -git clone --depth=1 https://github.com/RVC-Boss/GPT-SoVITS -cd GPT-SoVITS - -# 安装好 Miniconda 之后,先创建一个虚拟环境: -conda create -n GPTSoVits python=3.9 -conda activate GPTSoVits - -# 安装依赖: -pip install -r requirements.txt - -# (可选)如果网络环境不好,可以考虑换源(比如清华源): -pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt -``` - -#### 添加预训练模型 -``` -# 安装 huggingface-cli 用于和 huggingface hub 交互 -pip install huggingface_hub -# 登录 huggingface-cli -huggingface-cli login - -# 下载模型, 由于模型文件较大,可能需要一段时间 -# --local-dir-use-symlinks False 用于解决 macOS alias 文件的问题 -# 会下载到 GPT_SoVITS/pretrained_models 文件夹下 -huggingface-cli download --resume-download lj1995/GPT-SoVITS --local-dir GPT_SoVITS/pretrained_models --local-dir-use-symlinks False -``` - -#### 添加微调模型(可选) -笔者是将微调添加到了GPT-SoVITS/trained目录,内容如下,正常情况下包含 openai_alloy-e15.ckpt 和openai_alloy_e8_s112.pth 即可 -如果仅仅测试合成效果,不添加微调模型 使用预训练模型作为微调模型也可以 -``` -├── .gitignore -├── openai_alloy -│ ├── infer_config.json -│ ├── openai_alloy-e15.ckpt -│ ├── openai_alloy_e8_s112.pth -│ ├── output-2.txt -│ ├── output-2.wav -``` - -#### 启动推理webtui -``` -python.exe GPT_SoVITS/inference_webui.py -``` -配置如下 -![](inference_cpu_files/1.jpg) - -### 使用gpu推理 -``` -pip uninstall torch torchaudio -y -pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu117 -``` -请根据你的环境选择合适的cuda版本 diff --git a/docs/cn/inference_cpu_files/memory.md b/docs/cn/inference_cpu_files/memory.md new file mode 100644 index 00000000..af64077a --- /dev/null +++ b/docs/cn/inference_cpu_files/memory.md @@ -0,0 +1,18 @@ +加载模型后需要占用的内存容量如下,单位是MB +``` +{ + "memory_usage": { + "rss": 1029.30078125, + "vms": 4505.546875, + "percent": 6.5181980027997115 + } +} + +{ + "gpu_memory_usage": { + "used_memory": 2640, + "total_memory": 4096, + "percent": 64.453125 + } +} +``` diff --git a/requirements.txt b/requirements.txt index 73912d01..19294040 100644 --- a/requirements.txt +++ b/requirements.txt @@ -25,4 +25,4 @@ jieba_fast jieba LangSegment>=0.2.0 Faster_Whisper -wordsegment \ No newline at end of file +wordsegmentpsutil diff --git a/server/handlers.py b/server/handlers.py index bc164745..7bc4f376 100644 --- a/server/handlers.py +++ b/server/handlers.py @@ -1,4 +1,6 @@ from fastapi import APIRouter, Request + +from memory_service import get_memory_usage, get_gpu_memory_usage from pyutils.logs import llog from tts_service import change_sovits_weights, change_gpt_weights, handle_control, handle_change, handle @@ -70,3 +72,13 @@ async def tts_endpoint( cut_punc: str = None, ): return handle(refer_wav_path, prompt_text, prompt_language, text, text_language, cut_punc) + +@index_router.get("/memory-usage") +def read_memory_usage(): + memory_usage = get_memory_usage() + return {"memory_usage": memory_usage} + +@index_router.get("/gpu-memory-usage") +def read_gpu_memory_usage(): + gpu_memory_usage = get_gpu_memory_usage() + return {"gpu_memory_usage": gpu_memory_usage} \ No newline at end of file diff --git a/server/memory_service.py b/server/memory_service.py new file mode 100644 index 00000000..e1474637 --- /dev/null +++ b/server/memory_service.py @@ -0,0 +1,37 @@ +import psutil +import subprocess + + +def get_memory_usage(): + process = psutil.Process() + mem_info = process.memory_info() + memory_usage = { + "rss": mem_info.rss / (1024 ** 2), # Resident Set Size + "vms": mem_info.vms / (1024 ** 2), # Virtual Memory Size + "percent": process.memory_percent() # Percentage of memory usage + } + return memory_usage + + +def get_gpu_memory_usage(): + try: + result = subprocess.run( + ["nvidia-smi", "--query-gpu=memory.used,memory.total", "--format=csv,nounits,noheader"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=True, + text=True + ) + output = result.stdout.strip() + if output: + used_memory, total_memory = map(int, output.split(', ')) + gpu_memory_usage = { + "used_memory": used_memory, # in MiB + "total_memory": total_memory, # in MiB + "percent": (used_memory / total_memory) * 100 # Percentage of GPU memory usage + } + return gpu_memory_usage + else: + return {"error": "No GPU found or unable to query GPU memory usage."} + except Exception as e: + return {"error": str(e)}