Merge branch 'dev-api' into dev
This commit is contained in:
14
README.md
14
README.md
@@ -24,6 +24,10 @@ This repository is based on [Bert-VITS2](https://github.com/fishaudio/Bert-VITS2
|
||||
|
||||
<!-- 詳しくは[こちら](docs/tutorial.md)を参照してください。 -->
|
||||
|
||||
### 動作環境
|
||||
|
||||
各UIとAPI Serverにおいて、Windows コマンドプロンプト・WSL2・Linux(Ubuntu Desktop)での動作を確認しています(WSLでのパス指定は相対パスなど工夫ください)。
|
||||
|
||||
### インストール
|
||||
|
||||
#### GitやPythonに馴染みが無い方
|
||||
@@ -90,6 +94,14 @@ model_assets
|
||||
|
||||
注意: データセットの手動修正やノイズ除去や、より高品質なデータセットを作りたい場合は、[Aivis](https://github.com/tsukumijima/Aivis)や、そのデータセット部分のWindows対応版 [Aivis Dataset](https://github.com/litagin02/Aivis-Dataset) を使うのをおすすめします。
|
||||
|
||||
### API Server
|
||||
|
||||
構築した環境下で`python server_fastapi.py`するとAPIサーバーが起動します。
|
||||
API仕様は起動後に`/docs`にて確認ください。
|
||||
|
||||
デフォルトではCORS設定を全てのドメインで許可しています。
|
||||
できる限り、`config.yml`の`server.origins`の値を変更し、信頼できるドメインに制限ください(キーを消せばCORS設定を無効にできます)。
|
||||
|
||||
## Bert-VITS2 v2.1との関係
|
||||
|
||||
基本的にはBert-VITS2 v2.1のモデル構造を少し改造しただけです。[事前学習モデル](https://huggingface.co/litagin/Style-Bert-VITS2-1.0-base)も、実質Bert-VITS2 v2.1と同じものを使用しています(不要な重みを削ってsafetensorsに変換したもの)。
|
||||
@@ -109,7 +121,7 @@ model_assets
|
||||
- [ ] LinuxやWSL等、Windowsの通常環境以外でのサポート?
|
||||
- [ ] 複数話者学習での音声合成対応(学習は現在でも可能)
|
||||
- [ ] 本家のver 2.1, 2.2, 2.3モデルの推論対応?(ver 2.1以外は明らかにめんどいのでたぶんやらない)
|
||||
- [ ] `server_fastapi.py`の対応、とくにAPIで使えるようになると嬉しい人が増えるのかもしれない
|
||||
- [x] `server_fastapi.py`の対応、とくにAPIで使えるようになると嬉しい人が増えるのかもしれない
|
||||
- [ ] モデルのマージで声音と感情表現を混ぜる機能の実装
|
||||
- [ ] 英語等多言語対応?
|
||||
|
||||
|
||||
81
app.py
81
app.py
@@ -3,11 +3,13 @@ import datetime
|
||||
import os
|
||||
import sys
|
||||
import warnings
|
||||
import enum
|
||||
|
||||
import gradio as gr
|
||||
import numpy as np
|
||||
import torch
|
||||
from gradio.processing_utils import convert_to_16_bit_wav
|
||||
from typing import Dict, List
|
||||
|
||||
import utils
|
||||
from config import config
|
||||
@@ -15,17 +17,33 @@ from infer import get_net_g, infer
|
||||
from tools.log import logger
|
||||
|
||||
|
||||
class Languages(str, enum.Enum):
|
||||
JP = "JP"
|
||||
EN = "EN"
|
||||
ZH = "ZH"
|
||||
|
||||
|
||||
languages = [l.value for l in Languages]
|
||||
|
||||
DEFAULT_SDP_RATIO: float = 0.2
|
||||
DEFAULT_NOISE: float = 0.6
|
||||
DEFAULT_NOISEW: float = 0.8
|
||||
DEFAULT_LENGTH: float = 1
|
||||
DEFAULT_LINE_SPLIT: bool = True
|
||||
DEFAULT_SPLIT_INTERVAL: float = 0.5
|
||||
DEFAULT_STYLE_WEIGHT: float = 0.7
|
||||
DEFAULT_EMOTION_WEIGHT: float = 1.0
|
||||
|
||||
|
||||
class Model:
|
||||
def __init__(self, model_path, config_path, style_vec_path, device):
|
||||
self.model_path = model_path
|
||||
self.config_path = config_path
|
||||
self.device = device
|
||||
self.style_vec_path = style_vec_path
|
||||
self.load()
|
||||
|
||||
def load(self):
|
||||
self.hps = utils.get_hparams_from_file(self.config_path)
|
||||
self.spk2id = self.hps.data.spk2id
|
||||
self.spk2id: Dict[str, int] = self.hps.data.spk2id
|
||||
self.id2spk: Dict[int, str] = {v: k for k, v in self.spk2id.items()}
|
||||
self.num_styles = self.hps.data.num_styles
|
||||
if hasattr(self.hps.data, "style2id"):
|
||||
self.style2id = self.hps.data.style2id
|
||||
@@ -63,17 +81,17 @@ class Model:
|
||||
language="JP",
|
||||
sid=0,
|
||||
reference_audio_path=None,
|
||||
sdp_ratio=0.2,
|
||||
noise=0.6,
|
||||
noisew=0.8,
|
||||
length=1.0,
|
||||
line_split=True,
|
||||
split_interval=0.2,
|
||||
sdp_ratio=DEFAULT_SDP_RATIO,
|
||||
noise=DEFAULT_NOISE,
|
||||
noisew=DEFAULT_NOISEW,
|
||||
length=DEFAULT_LENGTH,
|
||||
line_split=DEFAULT_LINE_SPLIT,
|
||||
split_interval=DEFAULT_SPLIT_INTERVAL,
|
||||
style_text="",
|
||||
style_weight=0.7,
|
||||
style_weight=DEFAULT_STYLE_WEIGHT,
|
||||
use_style_text=False,
|
||||
style="0",
|
||||
emotion_weight=1.0,
|
||||
emotion_weight=DEFAULT_EMOTION_WEIGHT,
|
||||
):
|
||||
if reference_audio_path == "":
|
||||
reference_audio_path = None
|
||||
@@ -149,8 +167,8 @@ class ModelHolder:
|
||||
self.refresh()
|
||||
|
||||
def refresh(self):
|
||||
self.model_files_dict = {}
|
||||
self.model_names = []
|
||||
self.model_files_dict: Dict[str, List[str]] = {}
|
||||
self.model_names: List[str] = []
|
||||
self.current_model = None
|
||||
model_dirs = [
|
||||
d
|
||||
@@ -168,6 +186,7 @@ class ModelHolder:
|
||||
logger.info(
|
||||
f"No model files found in {self.root_dir}/{model_name}, so skip it"
|
||||
)
|
||||
continue
|
||||
self.model_files_dict[model_name] = model_files
|
||||
self.model_names.append(model_name)
|
||||
|
||||
@@ -373,8 +392,6 @@ if __name__ == "__main__":
|
||||
|
||||
model_holder = ModelHolder(model_dir, device)
|
||||
|
||||
languages = ["JP", "EN", "ZH"]
|
||||
|
||||
model_names = model_holder.model_names
|
||||
if len(model_names) == 0:
|
||||
logger.error(f"モデルが見つかりませんでした。{model_dir}にモデルを置いてください。")
|
||||
@@ -404,27 +421,43 @@ if __name__ == "__main__":
|
||||
load_button = gr.Button("ロード", scale=1, variant="primary")
|
||||
text_input = gr.TextArea(label="テキスト", value=initial_text)
|
||||
|
||||
line_split = gr.Checkbox(label="改行で分けて生成", value=True)
|
||||
line_split = gr.Checkbox(label="改行で分けて生成", value=DEFAULT_LINE_SPLIT)
|
||||
split_interval = gr.Slider(
|
||||
minimum=0.0,
|
||||
maximum=2,
|
||||
value=0.5,
|
||||
value=DEFAULT_SPLIT_INTERVAL,
|
||||
step=0.1,
|
||||
label="分けた場合に挟む無音の長さ(秒)",
|
||||
)
|
||||
language = gr.Dropdown(choices=languages, value="JP", label="Language")
|
||||
with gr.Accordion(label="詳細設定", open=False):
|
||||
sdp_ratio = gr.Slider(
|
||||
minimum=0, maximum=1, value=0.2, step=0.1, label="SDP Ratio"
|
||||
minimum=0,
|
||||
maximum=1,
|
||||
value=DEFAULT_SDP_RATIO,
|
||||
step=0.1,
|
||||
label="SDP Ratio",
|
||||
)
|
||||
noise_scale = gr.Slider(
|
||||
minimum=0.1, maximum=2, value=0.6, step=0.1, label="Noise"
|
||||
minimum=0.1,
|
||||
maximum=2,
|
||||
value=DEFAULT_NOISE,
|
||||
step=0.1,
|
||||
label="Noise",
|
||||
)
|
||||
noise_scale_w = gr.Slider(
|
||||
minimum=0.1, maximum=2, value=0.8, step=0.1, label="Noise_W"
|
||||
minimum=0.1,
|
||||
maximum=2,
|
||||
value=DEFAULT_NOISEW,
|
||||
step=0.1,
|
||||
label="Noise_W",
|
||||
)
|
||||
length_scale = gr.Slider(
|
||||
minimum=0.1, maximum=2, value=1.0, step=0.1, label="Length"
|
||||
minimum=0.1,
|
||||
maximum=2,
|
||||
value=DEFAULT_LENGTH,
|
||||
step=0.1,
|
||||
label="Length",
|
||||
)
|
||||
use_style_text = gr.Checkbox(label="Style textを使う", value=False)
|
||||
style_text = gr.Textbox(
|
||||
@@ -436,7 +469,7 @@ if __name__ == "__main__":
|
||||
style_text_weight = gr.Slider(
|
||||
minimum=0,
|
||||
maximum=1,
|
||||
value=0.7,
|
||||
value=DEFAULT_STYLE_WEIGHT,
|
||||
step=0.1,
|
||||
label="Style textの強さ",
|
||||
visible=False,
|
||||
@@ -462,7 +495,7 @@ if __name__ == "__main__":
|
||||
style_weight = gr.Slider(
|
||||
minimum=0,
|
||||
maximum=50,
|
||||
value=1,
|
||||
value=DEFAULT_EMOTION_WEIGHT,
|
||||
step=0.1,
|
||||
label="スタイルの強さ",
|
||||
)
|
||||
|
||||
31
config.py
31
config.py
@@ -2,11 +2,13 @@
|
||||
@Desc: 全局配置文件读取
|
||||
"""
|
||||
import argparse
|
||||
import yaml
|
||||
from typing import Dict, List
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
from typing import Dict, List
|
||||
|
||||
import yaml
|
||||
|
||||
from tools.log import logger
|
||||
|
||||
|
||||
class Resample_config:
|
||||
@@ -172,11 +174,18 @@ class Webui_config:
|
||||
|
||||
class Server_config:
|
||||
def __init__(
|
||||
self, models: List[Dict[str, any]], port: int = 5000, device: str = "cuda"
|
||||
self,
|
||||
port: int = 5000,
|
||||
device: str = "cuda",
|
||||
limit: int = 100,
|
||||
language: str = "JP",
|
||||
origins: List[str] = None,
|
||||
):
|
||||
self.models: List[Dict[str, any]] = models # 需要加载的所有模型的配置
|
||||
self.port: int = port # 端口号
|
||||
self.device: str = device # 模型默认使用设备
|
||||
self.port: int = port
|
||||
self.device: str = device
|
||||
self.language: str = language
|
||||
self.limit: int = limit
|
||||
self.origins: List[str] = origins
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, any]):
|
||||
@@ -251,4 +260,10 @@ parser = argparse.ArgumentParser()
|
||||
# 为避免与以前的config.json起冲突,将其更名如下
|
||||
parser.add_argument("-y", "--yml_config", type=str, default="config.yml")
|
||||
args, _ = parser.parse_known_args()
|
||||
config = Config(args.yml_config)
|
||||
|
||||
try:
|
||||
config = Config(args.yml_config)
|
||||
except TypeError:
|
||||
logger.warning("Old config.yml found. Replace it with default_config.yml.")
|
||||
shutil.copy(src="default_config.yml", dst="config.yml")
|
||||
config = Config("config.yml")
|
||||
|
||||
@@ -65,17 +65,10 @@ webui:
|
||||
language_identification_library: "langid"
|
||||
|
||||
# server_fastapi's config
|
||||
# TODO: `server_fastapi.py` is not implemented yet for this version
|
||||
server:
|
||||
port: 5000
|
||||
device: "cuda"
|
||||
models:
|
||||
- model: ""
|
||||
config: ""
|
||||
device: "cuda"
|
||||
language: "ZH"
|
||||
- model: ""
|
||||
config: ""
|
||||
device: "cpu"
|
||||
language: "JP"
|
||||
speakers: []
|
||||
limit: 100
|
||||
origins:
|
||||
- "*"
|
||||
|
||||
@@ -1,535 +1,210 @@
|
||||
"""
|
||||
TODO: This file is not supported in this fork.
|
||||
api服务 多版本多模型 fastapi实现
|
||||
"""
|
||||
import logging
|
||||
import gc
|
||||
import random
|
||||
|
||||
import librosa
|
||||
import gradio
|
||||
import numpy as np
|
||||
import utils
|
||||
from fastapi import FastAPI, Query, Request, File, UploadFile, Form
|
||||
import argparse
|
||||
from fastapi import FastAPI, Query, Request, status, HTTPException
|
||||
from fastapi.responses import Response, FileResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from io import BytesIO
|
||||
from scipy.io import wavfile
|
||||
import uvicorn
|
||||
import torch
|
||||
import webbrowser
|
||||
import psutil
|
||||
import GPUtil
|
||||
from typing import Dict, Optional, List, Set, Union
|
||||
import os
|
||||
from typing import Dict, Optional, List, Union
|
||||
import os, sys
|
||||
from tools.log import logger
|
||||
from urllib.parse import unquote
|
||||
|
||||
from infer import infer, get_net_g, latest_version
|
||||
import tools.translate as trans
|
||||
from re_matching import cut_sent
|
||||
|
||||
|
||||
from config import config
|
||||
from app import (
|
||||
Model,
|
||||
ModelHolder,
|
||||
Languages,
|
||||
DEFAULT_SDP_RATIO,
|
||||
DEFAULT_NOISE,
|
||||
DEFAULT_NOISEW,
|
||||
DEFAULT_LENGTH,
|
||||
DEFAULT_LINE_SPLIT,
|
||||
DEFAULT_SPLIT_INTERVAL,
|
||||
DEFAULT_STYLE_WEIGHT,
|
||||
DEFAULT_EMOTION_WEIGHT,
|
||||
)
|
||||
from webui_style_vectors import DEFAULT_EMOTION
|
||||
|
||||
os.environ["TOKENIZERS_PARALLELISM"] = "false"
|
||||
ln = config.server_config.language
|
||||
|
||||
|
||||
class Model:
|
||||
"""模型封装类"""
|
||||
|
||||
def __init__(self, config_path: str, model_path: str, device: str, language: str):
|
||||
self.config_path: str = os.path.normpath(config_path)
|
||||
self.model_path: str = os.path.normpath(model_path)
|
||||
self.device: str = device
|
||||
self.language: str = language
|
||||
self.hps = utils.get_hparams_from_file(config_path)
|
||||
self.spk2id: Dict[str, int] = self.hps.data.spk2id # spk - id 映射字典
|
||||
self.id2spk: Dict[int, str] = dict() # id - spk 映射字典
|
||||
for speaker, speaker_id in self.hps.data.spk2id.items():
|
||||
self.id2spk[speaker_id] = speaker
|
||||
self.version: str = (
|
||||
self.hps.version if hasattr(self.hps, "version") else latest_version
|
||||
)
|
||||
self.net_g = get_net_g(
|
||||
model_path=model_path,
|
||||
version=self.version,
|
||||
device=device,
|
||||
hps=self.hps,
|
||||
def raise_validation_error(msg: str, param: str):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
detail=[dict(type="invalid_params", msg=msg, loc=["query", param])],
|
||||
)
|
||||
|
||||
def to_dict(self) -> Dict[str, any]:
|
||||
return {
|
||||
"config_path": self.config_path,
|
||||
"model_path": self.model_path,
|
||||
"device": self.device,
|
||||
"language": self.language,
|
||||
"spk2id": self.spk2id,
|
||||
"id2spk": self.id2spk,
|
||||
"version": self.version,
|
||||
}
|
||||
|
||||
class AudioResponse(Response):
|
||||
media_type = "audio/wav"
|
||||
|
||||
|
||||
class Models:
|
||||
def __init__(self):
|
||||
self.models: Dict[int, Model] = dict()
|
||||
self.num = 0
|
||||
# spkInfo[角色名][模型id] = 角色id
|
||||
self.spk_info: Dict[str, Dict[int, int]] = dict()
|
||||
self.path2ids: Dict[str, Set[int]] = dict() # 路径指向的model的id
|
||||
|
||||
def init_model(
|
||||
self, config_path: str, model_path: str, device: str, language: str
|
||||
) -> int:
|
||||
"""
|
||||
初始化并添加一个模型
|
||||
|
||||
:param config_path: 模型config.json路径
|
||||
:param model_path: 模型路径
|
||||
:param device: 模型推理使用设备
|
||||
:param language: 模型推理默认语言
|
||||
"""
|
||||
# 若文件不存在则不进行加载
|
||||
if not os.path.isfile(model_path):
|
||||
if model_path != "":
|
||||
logger.warning(f"模型文件{model_path} 不存在,不进行初始化")
|
||||
return self.num
|
||||
if not os.path.isfile(config_path):
|
||||
if config_path != "":
|
||||
logger.warning(f"配置文件{config_path} 不存在,不进行初始化")
|
||||
return self.num
|
||||
|
||||
# 若路径中的模型已存在,则不添加模型,若不存在,则进行初始化。
|
||||
model_path = os.path.realpath(model_path)
|
||||
if model_path not in self.path2ids.keys():
|
||||
self.path2ids[model_path] = {self.num}
|
||||
self.models[self.num] = Model(
|
||||
config_path=config_path,
|
||||
model_path=model_path,
|
||||
device=device,
|
||||
language=language,
|
||||
def load_models(model_holder: ModelHolder):
|
||||
model_holder.models = []
|
||||
for model_name, model_paths in model_holder.model_files_dict.items():
|
||||
model = Model(
|
||||
model_path=model_paths[0],
|
||||
config_path=os.path.join(model_holder.root_dir, model_name, "config.json"),
|
||||
style_vec_path=os.path.join(
|
||||
model_holder.root_dir, model_name, "style_vectors.npy"
|
||||
),
|
||||
device=model_holder.device,
|
||||
)
|
||||
logger.success(f"添加模型{model_path},使用配置文件{os.path.realpath(config_path)}")
|
||||
else:
|
||||
# 获取一个指向id
|
||||
m_id = next(iter(self.path2ids[model_path]))
|
||||
self.models[self.num] = self.models[m_id]
|
||||
self.path2ids[model_path].add(self.num)
|
||||
logger.success("模型已存在,添加模型引用。")
|
||||
# 添加角色信息
|
||||
for speaker, speaker_id in self.models[self.num].spk2id.items():
|
||||
if speaker not in self.spk_info.keys():
|
||||
self.spk_info[speaker] = {self.num: speaker_id}
|
||||
else:
|
||||
self.spk_info[speaker][self.num] = speaker_id
|
||||
# 修改计数
|
||||
self.num += 1
|
||||
return self.num - 1
|
||||
|
||||
def del_model(self, index: int) -> Optional[int]:
|
||||
"""删除对应序号的模型,若不存在则返回None"""
|
||||
if index not in self.models.keys():
|
||||
return None
|
||||
# 删除角色信息
|
||||
for speaker, speaker_id in self.models[index].spk2id.items():
|
||||
self.spk_info[speaker].pop(index)
|
||||
if len(self.spk_info[speaker]) == 0:
|
||||
# 若对应角色的所有模型都被删除,则清除该角色信息
|
||||
self.spk_info.pop(speaker)
|
||||
# 删除路径信息
|
||||
model_path = os.path.realpath(self.models[index].model_path)
|
||||
self.path2ids[model_path].remove(index)
|
||||
if len(self.path2ids[model_path]) == 0:
|
||||
self.path2ids.pop(model_path)
|
||||
logger.success(f"删除模型{model_path}, id = {index}")
|
||||
else:
|
||||
logger.success(f"删除模型引用{model_path}, id = {index}")
|
||||
# 删除模型
|
||||
self.models.pop(index)
|
||||
gc.collect()
|
||||
if torch.cuda.is_available():
|
||||
torch.cuda.empty_cache()
|
||||
return index
|
||||
|
||||
def get_models(self):
|
||||
"""获取所有模型"""
|
||||
return self.models
|
||||
model.load_net_g()
|
||||
model_holder.models.append(model)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--cpu", action="store_true", help="Use CPU instead of GPU")
|
||||
parser.add_argument(
|
||||
"--dir", "-d", type=str, help="Model directory", default=config.out_dir
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.cpu:
|
||||
device = "cpu"
|
||||
else:
|
||||
device = "cuda" if torch.cuda.is_available() else "cpu"
|
||||
|
||||
model_dir = args.dir
|
||||
model_holder = ModelHolder(model_dir, device)
|
||||
if len(model_holder.model_names) == 0:
|
||||
logger.error(f"Models not found in {model_dir}.")
|
||||
sys.exit(1)
|
||||
|
||||
logger.info("Loading models...")
|
||||
load_models(model_holder)
|
||||
limit = config.server_config.limit
|
||||
app = FastAPI()
|
||||
app.logger = logger
|
||||
# 挂载静态文件
|
||||
logger.info("开始挂载网页页面")
|
||||
StaticDir: str = "./Web"
|
||||
if not os.path.isdir(StaticDir):
|
||||
allow_origins = config.server_config.origins
|
||||
if allow_origins:
|
||||
logger.warning(
|
||||
"缺少网页资源,无法开启网页页面,如有需要请在 https://github.com/jiangyuxiaoxiao/Bert-VITS2-UI 或者Bert-VITS对应版本的release页面下载"
|
||||
f"CORS allow_origins={config.server_config.origins}. If you don't want, modify config.yml"
|
||||
)
|
||||
else:
|
||||
dirs = [fir.name for fir in os.scandir(StaticDir) if fir.is_dir()]
|
||||
files = [fir.name for fir in os.scandir(StaticDir) if fir.is_dir()]
|
||||
for dirName in dirs:
|
||||
app.mount(
|
||||
f"/{dirName}",
|
||||
StaticFiles(directory=f"./{StaticDir}/{dirName}"),
|
||||
name=dirName,
|
||||
)
|
||||
loaded_models = Models()
|
||||
# 加载模型
|
||||
logger.info("开始加载模型")
|
||||
models_info = config.server_config.models
|
||||
for model_info in models_info:
|
||||
loaded_models.init_model(
|
||||
config_path=model_info["config"],
|
||||
model_path=model_info["model"],
|
||||
device=model_info["device"],
|
||||
language=model_info["language"],
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=config.server_config.origins,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
app.logger = logger
|
||||
|
||||
@app.get("/")
|
||||
async def index():
|
||||
return FileResponse("./Web/index.html")
|
||||
|
||||
async def _voice(
|
||||
text: str,
|
||||
model_id: int,
|
||||
speaker_name: str,
|
||||
speaker_id: int,
|
||||
sdp_ratio: float,
|
||||
noise: float,
|
||||
noisew: float,
|
||||
length: float,
|
||||
language: str,
|
||||
auto_translate: bool,
|
||||
auto_split: bool,
|
||||
emotion: Optional[Union[int, str]] = None,
|
||||
reference_audio=None,
|
||||
style_text: Optional[str] = None,
|
||||
style_weight: float = 0.7,
|
||||
) -> Union[Response, Dict[str, any]]:
|
||||
"""TTS实现函数"""
|
||||
# 检查模型是否存在
|
||||
if model_id not in loaded_models.models.keys():
|
||||
logger.error(f"/voice 请求错误:模型model_id={model_id}未加载")
|
||||
return {"status": 10, "detail": f"模型model_id={model_id}未加载"}
|
||||
# 检查是否提供speaker
|
||||
if speaker_name is None and speaker_id is None:
|
||||
logger.error("/voice 请求错误:推理请求未提供speaker_name或speaker_id")
|
||||
return {"status": 11, "detail": "请提供speaker_name或speaker_id"}
|
||||
elif speaker_name is None:
|
||||
# 检查speaker_id是否存在
|
||||
if speaker_id not in loaded_models.models[model_id].id2spk.keys():
|
||||
logger.error(f"/voice 请求错误:角色speaker_id={speaker_id}不存在")
|
||||
return {"status": 12, "detail": f"角色speaker_id={speaker_id}不存在"}
|
||||
speaker_name = loaded_models.models[model_id].id2spk[speaker_id]
|
||||
# 检查speaker_name是否存在
|
||||
if speaker_name not in loaded_models.models[model_id].spk2id.keys():
|
||||
logger.error(f"/voice 请求错误:角色speaker_name={speaker_name}不存在")
|
||||
return {"status": 13, "detail": f"角色speaker_name={speaker_name}不存在"}
|
||||
# 未传入则使用默认语言
|
||||
if language is None:
|
||||
language = loaded_models.models[model_id].language
|
||||
# 翻译会破坏mix结构,auto也会变得无意义。不要在这两个模式下使用
|
||||
if auto_translate:
|
||||
if language == "auto" or language == "mix":
|
||||
logger.error(
|
||||
f"/voice 请求错误:请勿同时使用language = {language}与auto_translate模式"
|
||||
)
|
||||
return {
|
||||
"status": 20,
|
||||
"detail": f"请勿同时使用language = {language}与auto_translate模式",
|
||||
}
|
||||
text = trans.translate(Sentence=text, to_Language=language.lower())
|
||||
if reference_audio is not None:
|
||||
ref_audio = BytesIO(await reference_audio.read())
|
||||
# 2.2 适配
|
||||
if loaded_models.models[model_id].version == "2.2":
|
||||
ref_audio, _ = librosa.load(ref_audio, 48000)
|
||||
|
||||
else:
|
||||
ref_audio = reference_audio
|
||||
if not auto_split:
|
||||
with torch.no_grad():
|
||||
audio = infer(
|
||||
text=text,
|
||||
sdp_ratio=sdp_ratio,
|
||||
noise_scale=noise,
|
||||
noise_scale_w=noisew,
|
||||
length_scale=length,
|
||||
sid=speaker_name,
|
||||
language=language,
|
||||
hps=loaded_models.models[model_id].hps,
|
||||
net_g=loaded_models.models[model_id].net_g,
|
||||
device=loaded_models.models[model_id].device,
|
||||
emotion=emotion,
|
||||
reference_audio=ref_audio,
|
||||
style_text=style_text,
|
||||
style_weight=style_weight,
|
||||
)
|
||||
audio = gradio.processing_utils.convert_to_16_bit_wav(audio)
|
||||
else:
|
||||
texts = cut_sent(text)
|
||||
audios = []
|
||||
with torch.no_grad():
|
||||
for t in texts:
|
||||
audios.append(
|
||||
infer(
|
||||
text=t,
|
||||
sdp_ratio=sdp_ratio,
|
||||
noise_scale=noise,
|
||||
noise_scale_w=noisew,
|
||||
length_scale=length,
|
||||
sid=speaker_name,
|
||||
language=language,
|
||||
hps=loaded_models.models[model_id].hps,
|
||||
net_g=loaded_models.models[model_id].net_g,
|
||||
device=loaded_models.models[model_id].device,
|
||||
emotion=emotion,
|
||||
reference_audio=ref_audio,
|
||||
style_text=style_text,
|
||||
style_weight=style_weight,
|
||||
)
|
||||
)
|
||||
audios.append(np.zeros(int(44100 * 0.2)))
|
||||
audio = np.concatenate(audios)
|
||||
audio = gradio.processing_utils.convert_to_16_bit_wav(audio)
|
||||
with BytesIO() as wavContent:
|
||||
wavfile.write(
|
||||
wavContent, loaded_models.models[model_id].hps.data.sampling_rate, audio
|
||||
)
|
||||
response = Response(content=wavContent.getvalue(), media_type="audio/wav")
|
||||
return response
|
||||
|
||||
@app.post("/voice")
|
||||
@app.get("/voice", response_class=AudioResponse)
|
||||
async def voice(
|
||||
request: Request, # fastapi自动注入
|
||||
text: str = Form(...),
|
||||
model_id: int = Query(..., description="模型ID"), # 模型序号
|
||||
request: Request,
|
||||
text: str = Query(..., min_length=1, max_length=limit, description=f"セリフ"),
|
||||
encoding: str = Query(None, description="textをURLデコードする(ex, `utf-8`)"),
|
||||
model_id: int = Query(0, description="モデルID。`GET /models/info`のkeyの値を指定ください"),
|
||||
speaker_name: str = Query(
|
||||
None, description="说话人名"
|
||||
), # speaker_name与 speaker_id二者选其一
|
||||
speaker_id: int = Query(None, description="说话人id,与speaker_name二选一"),
|
||||
sdp_ratio: float = Query(0.2, description="SDP/DP混合比"),
|
||||
noise: float = Query(0.2, description="感情"),
|
||||
noisew: float = Query(0.9, description="音素长度"),
|
||||
length: float = Query(1, description="语速"),
|
||||
language: str = Query(None, description="语言"), # 若不指定使用语言则使用默认值
|
||||
auto_translate: bool = Query(False, description="自动翻译"),
|
||||
auto_split: bool = Query(False, description="自动切分"),
|
||||
emotion: Optional[Union[int, str]] = Query(None, description="emo"),
|
||||
reference_audio: UploadFile = File(None),
|
||||
style_text: Optional[str] = Form(None, description="风格文本"),
|
||||
style_weight: float = Query(0.7, description="风格权重"),
|
||||
None, description="話者名(speaker_idより優先)。esd.listの2列目の文字列を指定"
|
||||
),
|
||||
speaker_id: int = Query(
|
||||
0, description="話者ID。model_assets>[model]>config.json内のspk2idを確認"
|
||||
),
|
||||
sdp_ratio: float = Query(
|
||||
DEFAULT_SDP_RATIO,
|
||||
description="SDP(Stochastic Duration Predictor)/DP混合比。比率が高くなるほどトーンのばらつきが大きくなる",
|
||||
),
|
||||
noise: float = Query(DEFAULT_NOISE, description="サンプルノイズの割合。大きくするほどランダム性が高まる"),
|
||||
noisew: float = Query(
|
||||
DEFAULT_NOISEW, description="SDPノイズ。大きくするほど発音の間隔にばらつきが出やすくなる"
|
||||
),
|
||||
length: float = Query(
|
||||
DEFAULT_LENGTH, description="話速。基準は1で大きくするほど音声は長くなり読み上げが遅まる"
|
||||
),
|
||||
language: Languages = Query(ln, description=f"textの言語"),
|
||||
auto_split: bool = Query(DEFAULT_LINE_SPLIT, description="改行で分けて生成"),
|
||||
split_interval: float = Query(
|
||||
DEFAULT_SPLIT_INTERVAL, description="分けた場合に挟む無音の長さ(秒)"
|
||||
),
|
||||
style_text: Optional[str] = Query(
|
||||
None, description="このテキストの読み上げと似た声音・感情になりやすくなる。ただし抑揚やテンポ等が犠牲になる傾向がある"
|
||||
),
|
||||
style_weight: float = Query(DEFAULT_STYLE_WEIGHT, description="style_textの強さ"),
|
||||
emotion: Optional[Union[int, str]] = Query(DEFAULT_EMOTION, description="スタイル"),
|
||||
emotion_weight: float = Query(DEFAULT_EMOTION_WEIGHT, description="emotionの強さ"),
|
||||
reference_audio_path: Optional[str] = Query(
|
||||
None, description="emotionを音声ファイルで行う"
|
||||
),
|
||||
):
|
||||
"""语音接口,若需要上传参考音频请仅使用post请求"""
|
||||
logger.info(
|
||||
f"{request.client.host}:{request.client.port}/voice { unquote(str(request.query_params) )} text={text}"
|
||||
)
|
||||
return await _voice(
|
||||
text=text,
|
||||
model_id=model_id,
|
||||
speaker_name=speaker_name,
|
||||
speaker_id=speaker_id,
|
||||
sdp_ratio=sdp_ratio,
|
||||
noise=noise,
|
||||
noisew=noisew,
|
||||
length=length,
|
||||
language=language,
|
||||
auto_translate=auto_translate,
|
||||
auto_split=auto_split,
|
||||
emotion=emotion,
|
||||
reference_audio=reference_audio,
|
||||
style_text=style_text,
|
||||
style_weight=style_weight,
|
||||
)
|
||||
|
||||
@app.get("/voice")
|
||||
async def voice(
|
||||
request: Request, # fastapi自动注入
|
||||
text: str = Query(..., description="输入文字"),
|
||||
model_id: int = Query(..., description="模型ID"), # 模型序号
|
||||
speaker_name: str = Query(
|
||||
None, description="说话人名"
|
||||
), # speaker_name与 speaker_id二者选其一
|
||||
speaker_id: int = Query(None, description="说话人id,与speaker_name二选一"),
|
||||
sdp_ratio: float = Query(0.2, description="SDP/DP混合比"),
|
||||
noise: float = Query(0.2, description="感情"),
|
||||
noisew: float = Query(0.9, description="音素长度"),
|
||||
length: float = Query(1, description="语速"),
|
||||
language: str = Query(None, description="语言"), # 若不指定使用语言则使用默认值
|
||||
auto_translate: bool = Query(False, description="自动翻译"),
|
||||
auto_split: bool = Query(False, description="自动切分"),
|
||||
emotion: Optional[Union[int, str]] = Query(None, description="emo"),
|
||||
style_text: Optional[str] = Query(None, description="风格文本"),
|
||||
style_weight: float = Query(0.7, description="风格权重"),
|
||||
):
|
||||
"""语音接口"""
|
||||
"""Infer text to speech(テキストから感情付き音声を生成する)"""
|
||||
logger.info(
|
||||
f"{request.client.host}:{request.client.port}/voice { unquote(str(request.query_params) )}"
|
||||
)
|
||||
return await _voice(
|
||||
if model_id >= len(model_holder.models): # /models/refresh があるためQuery(le)で表現不可
|
||||
raise_validation_error(f"model_id={model_id} not found", "model_id")
|
||||
|
||||
model = model_holder.models[model_id]
|
||||
if speaker_name is None:
|
||||
if speaker_id not in model.id2spk.keys():
|
||||
raise_validation_error(
|
||||
f"speaker_id={speaker_id} not found", "speaker_id"
|
||||
)
|
||||
else:
|
||||
if speaker_name not in model.spk2id.keys():
|
||||
raise_validation_error(
|
||||
f"speaker_name={speaker_name} not found", "speaker_name"
|
||||
)
|
||||
speaker_id = model.spk2id[speaker_name]
|
||||
if emotion not in model.style2id.keys():
|
||||
raise_validation_error(f"emotion={emotion} not found", "emotion")
|
||||
if encoding is not None:
|
||||
text = unquote(text, encoding=encoding)
|
||||
sr, audio = model.infer(
|
||||
text=text,
|
||||
model_id=model_id,
|
||||
speaker_name=speaker_name,
|
||||
speaker_id=speaker_id,
|
||||
language=language,
|
||||
sid=speaker_id,
|
||||
reference_audio_path=reference_audio_path,
|
||||
sdp_ratio=sdp_ratio,
|
||||
noise=noise,
|
||||
noisew=noisew,
|
||||
length=length,
|
||||
language=language,
|
||||
auto_translate=auto_translate,
|
||||
auto_split=auto_split,
|
||||
emotion=emotion,
|
||||
line_split=auto_split,
|
||||
split_interval=split_interval,
|
||||
style_text=style_text,
|
||||
style_weight=style_weight,
|
||||
use_style_text=bool(style_text),
|
||||
style=emotion,
|
||||
emotion_weight=emotion_weight,
|
||||
)
|
||||
with BytesIO() as wavContent:
|
||||
wavfile.write(wavContent, sr, audio)
|
||||
return Response(content=wavContent.getvalue(), media_type="audio/wav")
|
||||
|
||||
@app.get("/models/info")
|
||||
def get_loaded_models_info(request: Request):
|
||||
"""获取已加载模型信息"""
|
||||
def get_loaded_models_info():
|
||||
"""ロードされたモデル情報の取得"""
|
||||
|
||||
result: Dict[str, Dict] = dict()
|
||||
for key, model in loaded_models.models.items():
|
||||
result[str(key)] = model.to_dict()
|
||||
for model_id, model in enumerate(model_holder.models):
|
||||
result[str(model_id)] = {
|
||||
"config_path": model.config_path,
|
||||
"model_path": model.model_path,
|
||||
"device": model.device,
|
||||
"spk2id": model.spk2id,
|
||||
"id2spk": model.id2spk,
|
||||
"style2id": model.style2id,
|
||||
}
|
||||
return result
|
||||
|
||||
@app.get("/models/delete")
|
||||
def delete_model(
|
||||
request: Request, model_id: int = Query(..., description="删除模型id")
|
||||
):
|
||||
"""删除指定模型"""
|
||||
logger.info(
|
||||
f"{request.client.host}:{request.client.port}/models/delete { unquote(str(request.query_params) )}"
|
||||
)
|
||||
result = loaded_models.del_model(model_id)
|
||||
if result is None:
|
||||
logger.error(f"/models/delete 模型删除错误:模型{model_id}不存在,删除失败")
|
||||
return {"status": 14, "detail": f"模型{model_id}不存在,删除失败"}
|
||||
|
||||
return {"status": 0, "detail": "删除成功"}
|
||||
|
||||
@app.get("/models/add")
|
||||
def add_model(
|
||||
request: Request,
|
||||
model_path: str = Query(..., description="添加模型路径"),
|
||||
config_path: str = Query(
|
||||
None, description="添加模型配置文件路径,不填则使用./config.json或../config.json"
|
||||
),
|
||||
device: str = Query("cuda", description="推理使用设备"),
|
||||
language: str = Query("ZH", description="模型默认语言"),
|
||||
):
|
||||
"""添加指定模型:允许重复添加相同路径模型,且不重复占用内存"""
|
||||
logger.info(
|
||||
f"{request.client.host}:{request.client.port}/models/add { unquote(str(request.query_params) )}"
|
||||
)
|
||||
if config_path is None:
|
||||
model_dir = os.path.dirname(model_path)
|
||||
if os.path.isfile(os.path.join(model_dir, "config.json")):
|
||||
config_path = os.path.join(model_dir, "config.json")
|
||||
elif os.path.isfile(os.path.join(model_dir, "../config.json")):
|
||||
config_path = os.path.join(model_dir, "../config.json")
|
||||
else:
|
||||
logger.error("/models/add 模型添加失败:未在模型所在目录以及上级目录找到config.json文件")
|
||||
return {
|
||||
"status": 15,
|
||||
"detail": "查询未传入配置文件路径,同时默认路径./与../中不存在配置文件config.json。",
|
||||
}
|
||||
try:
|
||||
model_id = loaded_models.init_model(
|
||||
config_path=config_path,
|
||||
model_path=model_path,
|
||||
device=device,
|
||||
language=language,
|
||||
)
|
||||
except Exception:
|
||||
logging.exception("模型加载出错")
|
||||
return {
|
||||
"status": 16,
|
||||
"detail": "模型加载出错,详细查看日志",
|
||||
}
|
||||
return {
|
||||
"status": 0,
|
||||
"detail": "模型添加成功",
|
||||
"Data": {
|
||||
"model_id": model_id,
|
||||
"model_info": loaded_models.models[model_id].to_dict(),
|
||||
},
|
||||
}
|
||||
|
||||
def _get_all_models(root_dir: str = "Data", only_unloaded: bool = False):
|
||||
"""从root_dir搜索获取所有可用模型"""
|
||||
result: Dict[str, List[str]] = dict()
|
||||
files = os.listdir(root_dir) + ["."]
|
||||
for file in files:
|
||||
if os.path.isdir(os.path.join(root_dir, file)):
|
||||
sub_dir = os.path.join(root_dir, file)
|
||||
# 搜索 "sub_dir" 、 "sub_dir/models" 两个路径
|
||||
result[file] = list()
|
||||
sub_files = os.listdir(sub_dir)
|
||||
model_files = []
|
||||
for sub_file in sub_files:
|
||||
relpath = os.path.realpath(os.path.join(sub_dir, sub_file))
|
||||
if only_unloaded and relpath in loaded_models.path2ids.keys():
|
||||
continue
|
||||
if sub_file.endswith(".pth") and sub_file.startswith("G_"):
|
||||
if os.path.isfile(relpath):
|
||||
model_files.append(sub_file)
|
||||
# 对模型文件按步数排序
|
||||
model_files = sorted(
|
||||
model_files,
|
||||
key=lambda pth: int(pth.lstrip("G_").rstrip(".pth"))
|
||||
if pth.lstrip("G_").rstrip(".pth").isdigit()
|
||||
else 10**10,
|
||||
)
|
||||
result[file] = model_files
|
||||
models_dir = os.path.join(sub_dir, "models")
|
||||
model_files = []
|
||||
if os.path.isdir(models_dir):
|
||||
sub_files = os.listdir(models_dir)
|
||||
for sub_file in sub_files:
|
||||
relpath = os.path.realpath(os.path.join(models_dir, sub_file))
|
||||
if only_unloaded and relpath in loaded_models.path2ids.keys():
|
||||
continue
|
||||
if sub_file.endswith(".pth") and sub_file.startswith("G_"):
|
||||
if os.path.isfile(os.path.join(models_dir, sub_file)):
|
||||
model_files.append(f"models/{sub_file}")
|
||||
# 对模型文件按步数排序
|
||||
model_files = sorted(
|
||||
model_files,
|
||||
key=lambda pth: int(pth.lstrip("models/G_").rstrip(".pth"))
|
||||
if pth.lstrip("models/G_").rstrip(".pth").isdigit()
|
||||
else 10**10,
|
||||
)
|
||||
result[file] += model_files
|
||||
if len(result[file]) == 0:
|
||||
result.pop(file)
|
||||
|
||||
return result
|
||||
|
||||
@app.get("/models/get_unloaded")
|
||||
def get_unloaded_models_info(
|
||||
request: Request, root_dir: str = Query("Data", description="搜索根目录")
|
||||
):
|
||||
"""获取未加载模型"""
|
||||
logger.info(
|
||||
f"{request.client.host}:{request.client.port}/models/get_unloaded { unquote(str(request.query_params) )}"
|
||||
)
|
||||
return _get_all_models(root_dir, only_unloaded=True)
|
||||
|
||||
@app.get("/models/get_local")
|
||||
def get_local_models_info(
|
||||
request: Request, root_dir: str = Query("Data", description="搜索根目录")
|
||||
):
|
||||
"""获取全部本地模型"""
|
||||
logger.info(
|
||||
f"{request.client.host}:{request.client.port}/models/get_local { unquote(str(request.query_params) )}"
|
||||
)
|
||||
return _get_all_models(root_dir, only_unloaded=False)
|
||||
@app.post("/models/refresh")
|
||||
def refresh():
|
||||
"""モデルをパスに追加/削除した際などに読み込ませる"""
|
||||
model_holder.refresh()
|
||||
load_models(model_holder)
|
||||
return get_loaded_models_info()
|
||||
|
||||
@app.get("/status")
|
||||
def get_status():
|
||||
"""获取电脑运行状态"""
|
||||
"""実行環境のステータスを取得"""
|
||||
cpu_percent = psutil.cpu_percent(interval=1)
|
||||
memory_info = psutil.virtual_memory()
|
||||
memory_total = memory_info.total
|
||||
@@ -563,119 +238,22 @@ if __name__ == "__main__":
|
||||
"gpu": gpuInfo,
|
||||
}
|
||||
|
||||
@app.get("/tools/translate")
|
||||
def translate(
|
||||
request: Request,
|
||||
texts: str = Query(..., description="待翻译文本"),
|
||||
to_language: str = Query(..., description="翻译目标语言"),
|
||||
@app.get("/tools/get_audio", response_class=AudioResponse)
|
||||
def get_audio(
|
||||
request: Request, path: str = Query(..., description="local wav path")
|
||||
):
|
||||
"""翻译"""
|
||||
logger.info(
|
||||
f"{request.client.host}:{request.client.port}/tools/translate { unquote(str(request.query_params) )}"
|
||||
)
|
||||
return {"texts": trans.translate(Sentence=texts, to_Language=to_language)}
|
||||
|
||||
all_examples: Dict[str, Dict[str, List]] = dict() # 存放示例
|
||||
|
||||
@app.get("/tools/random_example")
|
||||
def random_example(
|
||||
request: Request,
|
||||
language: str = Query(None, description="指定语言,未指定则随机返回"),
|
||||
root_dir: str = Query("Data", description="搜索根目录"),
|
||||
):
|
||||
"""
|
||||
获取一个随机音频+文本,用于对比,音频会从本地目录随机选择。
|
||||
"""
|
||||
logger.info(
|
||||
f"{request.client.host}:{request.client.port}/tools/random_example { unquote(str(request.query_params) )}"
|
||||
)
|
||||
global all_examples
|
||||
# 数据初始化
|
||||
if root_dir not in all_examples.keys():
|
||||
all_examples[root_dir] = {"ZH": [], "JP": [], "EN": []}
|
||||
|
||||
examples = all_examples[root_dir]
|
||||
|
||||
# 从项目Data目录中搜索train/val.list
|
||||
for root, directories, _files in os.walk(root_dir):
|
||||
for file in _files:
|
||||
if file in ["train.list", "val.list"]:
|
||||
with open(
|
||||
os.path.join(root, file), mode="r", encoding="utf-8"
|
||||
) as f:
|
||||
lines = f.readlines()
|
||||
for line in lines:
|
||||
data = line.split("|")
|
||||
if len(data) != 7:
|
||||
continue
|
||||
# 音频存在 且语言为ZH/EN/JP
|
||||
if os.path.isfile(data[0]) and data[2] in [
|
||||
"ZH",
|
||||
"JP",
|
||||
"EN",
|
||||
]:
|
||||
examples[data[2]].append(
|
||||
{
|
||||
"text": data[3],
|
||||
"audio": data[0],
|
||||
"speaker": data[1],
|
||||
}
|
||||
)
|
||||
|
||||
examples = all_examples[root_dir]
|
||||
if language is None:
|
||||
if len(examples["ZH"]) + len(examples["JP"]) + len(examples["EN"]) == 0:
|
||||
return {"status": 17, "detail": "没有加载任何示例数据"}
|
||||
else:
|
||||
# 随机选一个
|
||||
rand_num = random.randint(
|
||||
0,
|
||||
len(examples["ZH"]) + len(examples["JP"]) + len(examples["EN"]) - 1,
|
||||
)
|
||||
# ZH
|
||||
if rand_num < len(examples["ZH"]):
|
||||
return {"status": 0, "Data": examples["ZH"][rand_num]}
|
||||
# JP
|
||||
if rand_num < len(examples["ZH"]) + len(examples["JP"]):
|
||||
return {
|
||||
"status": 0,
|
||||
"Data": examples["JP"][rand_num - len(examples["ZH"])],
|
||||
}
|
||||
# EN
|
||||
return {
|
||||
"status": 0,
|
||||
"Data": examples["EN"][
|
||||
rand_num - len(examples["ZH"]) - len(examples["JP"])
|
||||
],
|
||||
}
|
||||
|
||||
else:
|
||||
if len(examples[language]) == 0:
|
||||
return {"status": 17, "detail": f"没有加载任何{language}数据"}
|
||||
return {
|
||||
"status": 0,
|
||||
"Data": examples[language][
|
||||
random.randint(0, len(examples[language]) - 1)
|
||||
],
|
||||
}
|
||||
|
||||
@app.get("/tools/get_audio")
|
||||
def get_audio(request: Request, path: str = Query(..., description="本地音频路径")):
|
||||
"""wavデータを取得する"""
|
||||
logger.info(
|
||||
f"{request.client.host}:{request.client.port}/tools/get_audio { unquote(str(request.query_params) )}"
|
||||
)
|
||||
if not os.path.isfile(path):
|
||||
logger.error(f"/tools/get_audio 获取音频错误:指定音频{path}不存在")
|
||||
return {"status": 18, "detail": "指定音频不存在"}
|
||||
raise_validation_error(f"path={path} not found", "path")
|
||||
if not path.lower().endswith(".wav"):
|
||||
logger.error(f"/tools/get_audio 获取音频错误:音频{path}非wav文件")
|
||||
return {"status": 19, "detail": "非wav格式文件"}
|
||||
return FileResponse(path=path)
|
||||
raise_validation_error(f"wav file not found in {path}", "path")
|
||||
return FileResponse(path=path, media_type="audio/wav")
|
||||
|
||||
logger.warning("本地服务,请勿将服务端口暴露于外网")
|
||||
logger.info(f"api文档地址 http://127.0.0.1:{config.server_config.port}/docs")
|
||||
if os.path.isdir(StaticDir):
|
||||
webbrowser.open(f"http://127.0.0.1:{config.server_config.port}")
|
||||
logger.info(f"server listen: http://127.0.0.1:{config.server_config.port}")
|
||||
logger.info(f"API docs: http://127.0.0.1:{config.server_config.port}/docs")
|
||||
uvicorn.run(
|
||||
app, port=config.server_config.port, host="0.0.0.0", log_level="warning"
|
||||
)
|
||||
|
||||
@@ -9,6 +9,7 @@ from sklearn.manifold import TSNE
|
||||
from config import config
|
||||
|
||||
MAX_CLUSTER_NUM = 10
|
||||
DEFAULT_EMOTION: str = "Neutral"
|
||||
|
||||
tsne = TSNE(n_components=2, random_state=42, metric="cosine")
|
||||
|
||||
@@ -124,7 +125,7 @@ def save_style_vectors(model_name, style_names: str):
|
||||
config_path = os.path.join(result_dir, "config.json")
|
||||
if not os.path.exists(config_path):
|
||||
return f"{config_path}が存在しません。"
|
||||
style_name_list = ["Neutral"]
|
||||
style_name_list = [DEFAULT_EMOTION]
|
||||
style_name_list = style_name_list + style_names.split(",")
|
||||
if len(style_name_list) != len(centroids) + 1:
|
||||
return f"スタイルの数が合いません。`,`で正しく{len(centroids)}個に区切られているか確認してください: {style_names}"
|
||||
@@ -173,7 +174,7 @@ def save_style_vectors_from_files(model_name, audio_files_text, style_names_text
|
||||
config_path = os.path.join(result_dir, "config.json")
|
||||
if not os.path.exists(config_path):
|
||||
return f"{config_path}が存在しません。"
|
||||
style_name_list = ["Neutral"]
|
||||
style_name_list = [DEFAULT_EMOTION]
|
||||
style_name_list = style_name_list + style_names
|
||||
assert len(style_name_list) == len(style_vectors)
|
||||
|
||||
@@ -188,7 +189,7 @@ def save_style_vectors_from_files(model_name, audio_files_text, style_names_text
|
||||
return f"成功!\n{style_vector_path}に保存し{config_path}を更新しました。"
|
||||
|
||||
|
||||
initial_md = """
|
||||
initial_md = f"""
|
||||
# Style Bert-VITS2 スタイルベクトルの作成
|
||||
|
||||
Style-Bert-VITS2でこまかくスタイルを指定して音声合成するには、モデルごとにスタイルベクトルのファイル`style_vectors.npy`を手動で作成する必要があります。
|
||||
@@ -216,7 +217,7 @@ method1 = """
|
||||
|
||||
詳細: スタイルベクトル(256次元)たちを適当なアルゴリズムでクラスタリングして、各クラスタの中心のベクトル(と全体の平均ベクトル)を保存します。
|
||||
|
||||
平均スタイル(Neutral)は自動的に保存されます。
|
||||
平均スタイル({DEFAULT_EMOTION})は自動的に保存されます。
|
||||
"""
|
||||
|
||||
with gr.Blocks(theme="NoCrypt/miku") as app:
|
||||
@@ -274,7 +275,7 @@ with gr.Blocks(theme="NoCrypt/miku") as app:
|
||||
style_names = gr.Textbox(
|
||||
"Angry, Sad, Happy",
|
||||
label="スタイルの名前",
|
||||
info="スタイルの名前を`,`で区切って入力してください(日本語可)。例: `Angry, Sad, Happy`や`怒り, 悲しみ, 喜び`など。平均音声はNeutralとして自動的に保存されます。",
|
||||
info=f"スタイルの名前を`,`で区切って入力してください(日本語可)。例: `Angry, Sad, Happy`や`怒り, 悲しみ, 喜び`など。平均音声は{DEFAULT_EMOTION}として自動的に保存されます。",
|
||||
)
|
||||
with gr.Row():
|
||||
save_button = gr.Button("スタイルベクトルを保存", variant="primary")
|
||||
@@ -286,7 +287,9 @@ with gr.Blocks(theme="NoCrypt/miku") as app:
|
||||
with gr.Tab("方法2: 手動でスタイルを選ぶ"):
|
||||
gr.Markdown("下のテキスト欄に、各スタイルの代表音声のファイル名を`,`区切りで、その横に対応するスタイル名を`,`区切りで入力してください。")
|
||||
gr.Markdown("例: `angry.wav, sad.wav, happy.wav`と`Angry, Sad, Happy`")
|
||||
gr.Markdown("注意: Neutralスタイルは自動的に保存されます、手動ではNeutralという名前のスタイルは指定しないでください。")
|
||||
gr.Markdown(
|
||||
f"注意: {DEFAULT_EMOTION}スタイルは自動的に保存されます、手動では{DEFAULT_EMOTION}という名前のスタイルは指定しないでください。"
|
||||
)
|
||||
with gr.Row():
|
||||
audio_files_text = gr.Textbox(
|
||||
label="音声ファイル名", placeholder="angry.wav, sad.wav, happy.wav"
|
||||
@@ -307,5 +310,4 @@ with gr.Blocks(theme="NoCrypt/miku") as app:
|
||||
"`clustering.ipynb`にjvnvコーパスの場合の作り方とかクラスタ分けのいろいろを書いています。これを参考に自分で頑張って作ってください。"
|
||||
)
|
||||
|
||||
|
||||
app.launch(inbrowser=True)
|
||||
app.launch(inbrowser=True)
|
||||
|
||||
Reference in New Issue
Block a user