add api server
This commit is contained in:
12
README.md
12
README.md
@@ -21,6 +21,10 @@ This repository is based on [Bert-VITS2](https://github.com/fishaudio/Bert-VITS2
|
|||||||
|
|
||||||
<!-- 詳しくは[こちら](docs/tutorial.md)を参照してください。 -->
|
<!-- 詳しくは[こちら](docs/tutorial.md)を参照してください。 -->
|
||||||
|
|
||||||
|
### 動作環境
|
||||||
|
|
||||||
|
各UIとAPI Serverにおいて、Windows コマンドプロンプト・WSL2・Linux(Ubuntu Desktop)での動作を確認しています(WSLでのパス指定は相対パスなど工夫ください)。
|
||||||
|
|
||||||
### インストール
|
### インストール
|
||||||
|
|
||||||
Windows環境で最近のNVIDIA製グラボがあることを前提にしています。
|
Windows環境で最近のNVIDIA製グラボがあることを前提にしています。
|
||||||
@@ -83,6 +87,12 @@ model_assets
|
|||||||
|
|
||||||
注意: データセットの手動修正やノイズ除去や、より高品質なデータセットを作りたい場合は、[Aivis](https://github.com/tsukumijima/Aivis)や、そのデータセット部分のWindows対応版 [Aivis Dataset](https://github.com/litagin02/Aivis-Dataset) を使うのをおすすめします。
|
注意: データセットの手動修正やノイズ除去や、より高品質なデータセットを作りたい場合は、[Aivis](https://github.com/tsukumijima/Aivis)や、そのデータセット部分のWindows対応版 [Aivis Dataset](https://github.com/litagin02/Aivis-Dataset) を使うのをおすすめします。
|
||||||
|
|
||||||
|
### API Server
|
||||||
|
|
||||||
|
構築した環境下で`python server_fastapi.py`するとAPIサーバーが起動します。
|
||||||
|
|
||||||
|
API仕様は起動後に`/docs`にて確認ください。
|
||||||
|
|
||||||
## Bert-VITS2 v2.1との関係
|
## Bert-VITS2 v2.1との関係
|
||||||
|
|
||||||
基本的にはBert-VITS2 v2.1のモデル構造を少し改造しただけです。[事前学習モデル](https://huggingface.co/litagin/Style-Bert-VITS2-1.0-base)も、実質Bert-VITS2 v2.1と同じものを使用しています(不要な重みを削ってsafetensorsに変換したもの)。
|
基本的にはBert-VITS2 v2.1のモデル構造を少し改造しただけです。[事前学習モデル](https://huggingface.co/litagin/Style-Bert-VITS2-1.0-base)も、実質Bert-VITS2 v2.1と同じものを使用しています(不要な重みを削ってsafetensorsに変換したもの)。
|
||||||
@@ -102,7 +112,7 @@ model_assets
|
|||||||
- [ ] LinuxやWSL等、Windowsの通常環境以外でのサポート?
|
- [ ] LinuxやWSL等、Windowsの通常環境以外でのサポート?
|
||||||
- [ ] 複数話者学習での音声合成対応(学習は現在でも可能)
|
- [ ] 複数話者学習での音声合成対応(学習は現在でも可能)
|
||||||
- [ ] 本家のver 2.1, 2.2, 2.3モデルの推論対応?(ver 2.1以外は明らかにめんどいのでたぶんやらない)
|
- [ ] 本家のver 2.1, 2.2, 2.3モデルの推論対応?(ver 2.1以外は明らかにめんどいのでたぶんやらない)
|
||||||
- [ ] `server_fastapi.py`の対応、とくにAPIで使えるようになると嬉しい人が増えるのかもしれない
|
- [x] `server_fastapi.py`の対応、とくにAPIで使えるようになると嬉しい人が増えるのかもしれない
|
||||||
- [ ] モデルのマージで声音と感情表現を混ぜる機能の実装
|
- [ ] モデルのマージで声音と感情表現を混ぜる機能の実装
|
||||||
- [ ] 英語等多言語対応?
|
- [ ] 英語等多言語対応?
|
||||||
|
|
||||||
|
|||||||
56
app.py
56
app.py
@@ -8,12 +8,24 @@ import gradio as gr
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
import torch
|
import torch
|
||||||
from gradio.processing_utils import convert_to_16_bit_wav
|
from gradio.processing_utils import convert_to_16_bit_wav
|
||||||
|
from typing import Dict, List
|
||||||
|
|
||||||
import utils
|
import utils
|
||||||
from config import config
|
from config import config
|
||||||
from infer import get_net_g, infer
|
from infer import get_net_g, infer
|
||||||
from tools.log import logger
|
from tools.log import logger
|
||||||
|
|
||||||
|
languages = ["JP", "EN", "ZH"]
|
||||||
|
|
||||||
|
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:
|
class Model:
|
||||||
def __init__(self, model_path, config_path, style_vec_path, device):
|
def __init__(self, model_path, config_path, style_vec_path, device):
|
||||||
@@ -21,11 +33,9 @@ class Model:
|
|||||||
self.config_path = config_path
|
self.config_path = config_path
|
||||||
self.device = device
|
self.device = device
|
||||||
self.style_vec_path = style_vec_path
|
self.style_vec_path = style_vec_path
|
||||||
self.load()
|
|
||||||
|
|
||||||
def load(self):
|
|
||||||
self.hps = utils.get_hparams_from_file(self.config_path)
|
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
|
self.num_styles = self.hps.data.num_styles
|
||||||
if hasattr(self.hps.data, "style2id"):
|
if hasattr(self.hps.data, "style2id"):
|
||||||
self.style2id = self.hps.data.style2id
|
self.style2id = self.hps.data.style2id
|
||||||
@@ -63,17 +73,17 @@ class Model:
|
|||||||
language="JP",
|
language="JP",
|
||||||
sid=0,
|
sid=0,
|
||||||
reference_audio_path=None,
|
reference_audio_path=None,
|
||||||
sdp_ratio=0.2,
|
sdp_ratio=DEFAULT_SDP_RATIO,
|
||||||
noise=0.6,
|
noise=DEFAULT_NOISE,
|
||||||
noisew=0.8,
|
noisew=DEFAULT_NOISEW,
|
||||||
length=1.0,
|
length=DEFAULT_LENGTH,
|
||||||
line_split=True,
|
line_split=DEFAULT_LINE_SPLIT,
|
||||||
split_interval=0.2,
|
split_interval=DEFAULT_SPLIT_INTERVAL,
|
||||||
style_text="",
|
style_text="",
|
||||||
style_weight=0.7,
|
style_weight=DEFAULT_STYLE_WEIGHT,
|
||||||
use_style_text=False,
|
use_style_text=False,
|
||||||
style="0",
|
style="0",
|
||||||
emotion_weight=1.0,
|
emotion_weight=DEFAULT_EMOTION_WEIGHT,
|
||||||
):
|
):
|
||||||
if reference_audio_path == "":
|
if reference_audio_path == "":
|
||||||
reference_audio_path = None
|
reference_audio_path = None
|
||||||
@@ -149,8 +159,8 @@ class ModelHolder:
|
|||||||
self.refresh()
|
self.refresh()
|
||||||
|
|
||||||
def refresh(self):
|
def refresh(self):
|
||||||
self.model_files_dict = {}
|
self.model_files_dict: Dict[str, List[str]] = {}
|
||||||
self.model_names = []
|
self.model_names: List[str] = []
|
||||||
self.current_model = None
|
self.current_model = None
|
||||||
model_dirs = [
|
model_dirs = [
|
||||||
d
|
d
|
||||||
@@ -370,8 +380,6 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
model_holder = ModelHolder(model_dir, device)
|
model_holder = ModelHolder(model_dir, device)
|
||||||
|
|
||||||
languages = ["JP", "EN", "ZH"]
|
|
||||||
|
|
||||||
model_names = model_holder.model_names
|
model_names = model_holder.model_names
|
||||||
if len(model_names) == 0:
|
if len(model_names) == 0:
|
||||||
logger.error(f"モデルが見つかりませんでした。{model_dir}にモデルを置いてください。")
|
logger.error(f"モデルが見つかりませんでした。{model_dir}にモデルを置いてください。")
|
||||||
@@ -401,27 +409,27 @@ if __name__ == "__main__":
|
|||||||
load_button = gr.Button("ロード", scale=1, variant="primary")
|
load_button = gr.Button("ロード", scale=1, variant="primary")
|
||||||
text_input = gr.TextArea(label="テキスト", value=initial_text)
|
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(
|
split_interval = gr.Slider(
|
||||||
minimum=0.0,
|
minimum=0.0,
|
||||||
maximum=2,
|
maximum=2,
|
||||||
value=0.5,
|
value=DEFAULT_SPLIT_INTERVAL,
|
||||||
step=0.1,
|
step=0.1,
|
||||||
label="分けた場合に挟む無音の長さ(秒)",
|
label="分けた場合に挟む無音の長さ(秒)",
|
||||||
)
|
)
|
||||||
language = gr.Dropdown(choices=languages, value="JP", label="Language")
|
language = gr.Dropdown(choices=languages, value="JP", label="Language")
|
||||||
with gr.Accordion(label="詳細設定", open=False):
|
with gr.Accordion(label="詳細設定", open=False):
|
||||||
sdp_ratio = gr.Slider(
|
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(
|
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(
|
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(
|
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)
|
use_style_text = gr.Checkbox(label="Style textを使う", value=False)
|
||||||
style_text = gr.Textbox(
|
style_text = gr.Textbox(
|
||||||
@@ -433,7 +441,7 @@ if __name__ == "__main__":
|
|||||||
style_text_weight = gr.Slider(
|
style_text_weight = gr.Slider(
|
||||||
minimum=0,
|
minimum=0,
|
||||||
maximum=1,
|
maximum=1,
|
||||||
value=0.7,
|
value=DEFAULT_STYLE_WEIGHT,
|
||||||
step=0.1,
|
step=0.1,
|
||||||
label="Style textの強さ",
|
label="Style textの強さ",
|
||||||
visible=False,
|
visible=False,
|
||||||
@@ -459,7 +467,7 @@ if __name__ == "__main__":
|
|||||||
style_weight = gr.Slider(
|
style_weight = gr.Slider(
|
||||||
minimum=0,
|
minimum=0,
|
||||||
maximum=50,
|
maximum=50,
|
||||||
value=1,
|
value=DEFAULT_EMOTION_WEIGHT,
|
||||||
step=0.1,
|
step=0.1,
|
||||||
label="スタイルの強さ",
|
label="スタイルの強さ",
|
||||||
)
|
)
|
||||||
|
|||||||
10
config.py
10
config.py
@@ -172,11 +172,13 @@ class Webui_config:
|
|||||||
|
|
||||||
class Server_config:
|
class Server_config:
|
||||||
def __init__(
|
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"
|
||||||
):
|
):
|
||||||
self.models: List[Dict[str, any]] = models # 需要加载的所有模型的配置
|
self.port: int = port
|
||||||
self.port: int = port # 端口号
|
self.device: str = device
|
||||||
self.device: str = device # 模型默认使用设备
|
self.language: str = language
|
||||||
|
self.limit: int = limit
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_dict(cls, data: Dict[str, any]):
|
def from_dict(cls, data: Dict[str, any]):
|
||||||
|
|||||||
@@ -65,17 +65,8 @@ webui:
|
|||||||
language_identification_library: "langid"
|
language_identification_library: "langid"
|
||||||
|
|
||||||
# server_fastapi's config
|
# server_fastapi's config
|
||||||
# TODO: `server_fastapi.py` is not implemented yet for this version
|
|
||||||
server:
|
server:
|
||||||
port: 5000
|
port: 5000
|
||||||
device: "cuda"
|
device: "cuda"
|
||||||
models:
|
language: "JP"
|
||||||
- model: ""
|
limit: 100
|
||||||
config: ""
|
|
||||||
device: "cuda"
|
|
||||||
language: "ZH"
|
|
||||||
- model: ""
|
|
||||||
config: ""
|
|
||||||
device: "cpu"
|
|
||||||
language: "JP"
|
|
||||||
speakers: []
|
|
||||||
|
|||||||
@@ -1,370 +1,163 @@
|
|||||||
"""
|
"""
|
||||||
TODO: This file is not supported in this fork.
|
|
||||||
api服务 多版本多模型 fastapi实现
|
api服务 多版本多模型 fastapi实现
|
||||||
"""
|
"""
|
||||||
import logging
|
import argparse
|
||||||
import gc
|
from fastapi import FastAPI, Query, Request
|
||||||
import random
|
|
||||||
|
|
||||||
import librosa
|
|
||||||
import gradio
|
|
||||||
import numpy as np
|
|
||||||
import utils
|
|
||||||
from fastapi import FastAPI, Query, Request, File, UploadFile, Form
|
|
||||||
from fastapi.responses import Response, FileResponse
|
from fastapi.responses import Response, FileResponse
|
||||||
from fastapi.staticfiles import StaticFiles
|
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from scipy.io import wavfile
|
from scipy.io import wavfile
|
||||||
import uvicorn
|
import uvicorn
|
||||||
import torch
|
import torch
|
||||||
import webbrowser
|
|
||||||
import psutil
|
import psutil
|
||||||
import GPUtil
|
import GPUtil
|
||||||
from typing import Dict, Optional, List, Set, Union
|
from typing import Dict, Optional, List, Union
|
||||||
import os
|
import os, sys
|
||||||
from tools.log import logger
|
from tools.log import logger
|
||||||
from urllib.parse import unquote
|
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 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
|
||||||
|
available_languages = languages + ["mix", "auto"]
|
||||||
|
|
||||||
|
def load_models(model_holder: ModelHolder):
|
||||||
class Model:
|
model_holder.models = []
|
||||||
"""模型封装类"""
|
for model_name, model_paths in model_holder.model_files_dict.items():
|
||||||
|
model = Model(
|
||||||
def __init__(self, config_path: str, model_path: str, device: str, language: str):
|
model_path=model_paths[0],
|
||||||
self.config_path: str = os.path.normpath(config_path)
|
config_path=os.path.join(model_holder.root_dir, model_name, "config.json"),
|
||||||
self.model_path: str = os.path.normpath(model_path)
|
style_vec_path=os.path.join(model_holder.root_dir, model_name, "style_vectors.npy"),
|
||||||
self.device: str = device
|
device=model_holder.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.load_net_g()
|
||||||
model_path=model_path,
|
model_holder.models.append(model)
|
||||||
version=self.version,
|
|
||||||
device=device,
|
|
||||||
hps=self.hps,
|
|
||||||
)
|
|
||||||
|
|
||||||
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 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,
|
|
||||||
)
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
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 = FastAPI()
|
||||||
app.logger = logger
|
app.logger = logger
|
||||||
# 挂载静态文件
|
|
||||||
logger.info("开始挂载网页页面")
|
|
||||||
StaticDir: str = "./Web"
|
|
||||||
if not os.path.isdir(StaticDir):
|
|
||||||
logger.warning(
|
|
||||||
"缺少网页资源,无法开启网页页面,如有需要请在 https://github.com/jiangyuxiaoxiao/Bert-VITS2-UI 或者Bert-VITS对应版本的release页面下载"
|
|
||||||
)
|
|
||||||
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.get("/")
|
|
||||||
async def index():
|
|
||||||
return FileResponse("./Web/index.html")
|
|
||||||
|
|
||||||
async def _voice(
|
async def _voice(
|
||||||
text: str,
|
text: str,
|
||||||
model_id: int,
|
model_id: int = 0,
|
||||||
speaker_name: str,
|
speaker_name: str = None,
|
||||||
speaker_id: int,
|
speaker_id: int = 0,
|
||||||
sdp_ratio: float,
|
sdp_ratio: float = DEFAULT_SDP_RATIO,
|
||||||
noise: float,
|
noise: float = DEFAULT_NOISE,
|
||||||
noisew: float,
|
noisew: float = DEFAULT_NOISEW,
|
||||||
length: float,
|
length: float = DEFAULT_LENGTH,
|
||||||
language: str,
|
language: str = ln,
|
||||||
auto_translate: bool,
|
auto_split: bool = DEFAULT_LINE_SPLIT,
|
||||||
auto_split: bool,
|
split_interval: float = DEFAULT_SPLIT_INTERVAL,
|
||||||
emotion: Optional[Union[int, str]] = None,
|
|
||||||
reference_audio=None,
|
|
||||||
style_text: Optional[str] = None,
|
style_text: Optional[str] = None,
|
||||||
style_weight: float = 0.7,
|
style_weight: float = DEFAULT_STYLE_WEIGHT,
|
||||||
|
emotion: Optional[Union[int, str]] = DEFAULT_EMOTION,
|
||||||
|
emotion_weight: float = DEFAULT_EMOTION_WEIGHT,
|
||||||
|
reference_audio_path: str = None,
|
||||||
) -> Union[Response, Dict[str, any]]:
|
) -> Union[Response, Dict[str, any]]:
|
||||||
"""TTS实现函数"""
|
if model_id >= len(model_holder.models):
|
||||||
# 检查模型是否存在
|
return {"status": 10, "detail": f"model_id={model_id} not found"}
|
||||||
if model_id not in loaded_models.models.keys():
|
elif len(text) > limit:
|
||||||
logger.error(f"/voice 请求错误:模型model_id={model_id}未加载")
|
return {"status": 9, "detail": f"too long text: over {limit}"}
|
||||||
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)
|
|
||||||
|
|
||||||
|
model = model_holder.models[model_id]
|
||||||
|
if speaker_name is None:
|
||||||
|
if speaker_id is None:
|
||||||
|
return {"status": 11, "detail": "Required speaker_name or speaker_id"}
|
||||||
|
if speaker_id not in model.id2spk.keys():
|
||||||
|
return {"status": 12, "detail": f"peaker_id={speaker_id} not found"}
|
||||||
else:
|
else:
|
||||||
ref_audio = reference_audio
|
if speaker_name not in model.spk2id.keys():
|
||||||
if not auto_split:
|
return {"status": 13, "detail": f"speaker_name={speaker_name} not found"}
|
||||||
with torch.no_grad():
|
speaker_id = model.spk2id[speaker_name]
|
||||||
audio = infer(
|
if emotion not in model.style2id.keys():
|
||||||
text=text,
|
return {"status": 14, "detail": f"emotion={speaker_name} not found"}
|
||||||
sdp_ratio=sdp_ratio,
|
if language not in available_languages:
|
||||||
noise_scale=noise,
|
language = ln
|
||||||
noise_scale_w=noisew,
|
sr, audio = model.infer(
|
||||||
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")
|
|
||||||
async def voice(
|
|
||||||
request: Request, # fastapi自动注入
|
|
||||||
text: str = Form(...),
|
|
||||||
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"),
|
|
||||||
reference_audio: UploadFile = File(None),
|
|
||||||
style_text: Optional[str] = Form(None, description="风格文本"),
|
|
||||||
style_weight: float = Query(0.7, description="风格权重"),
|
|
||||||
):
|
|
||||||
"""语音接口,若需要上传参考音频请仅使用post请求"""
|
|
||||||
logger.info(
|
|
||||||
f"{request.client.host}:{request.client.port}/voice { unquote(str(request.query_params) )} text={text}"
|
|
||||||
)
|
|
||||||
return await _voice(
|
|
||||||
text=text,
|
text=text,
|
||||||
model_id=model_id,
|
language=language,
|
||||||
speaker_name=speaker_name,
|
sid=speaker_id,
|
||||||
speaker_id=speaker_id,
|
reference_audio_path=reference_audio_path,
|
||||||
sdp_ratio=sdp_ratio,
|
sdp_ratio=sdp_ratio,
|
||||||
noise=noise,
|
noise=noise,
|
||||||
noisew=noisew,
|
noisew=noisew,
|
||||||
length=length,
|
length=length,
|
||||||
language=language,
|
line_split=auto_split,
|
||||||
auto_translate=auto_translate,
|
split_interval=split_interval,
|
||||||
auto_split=auto_split,
|
|
||||||
emotion=emotion,
|
|
||||||
reference_audio=reference_audio,
|
|
||||||
style_text=style_text,
|
style_text=style_text,
|
||||||
style_weight=style_weight,
|
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
|
||||||
|
)
|
||||||
|
response = Response(content=wavContent.getvalue(), media_type="audio/wav")
|
||||||
|
return response
|
||||||
|
|
||||||
@app.get("/voice")
|
@app.get("/voice")
|
||||||
async def voice(
|
async def voice(
|
||||||
request: Request, # fastapi自动注入
|
request: Request,
|
||||||
text: str = Query(..., description="输入文字"),
|
text: str = Query(..., description="セリフ"),
|
||||||
model_id: int = Query(..., description="模型ID"), # 模型序号
|
model_id: int = Query(0, description="モデルID。`GET /models/info`のkeyの値を指定ください。"),
|
||||||
speaker_name: str = Query(
|
speaker_name: str = Query(
|
||||||
None, description="说话人名"
|
None, description="話者名(speaker_idより優先)。esd.listの2列目記載の文字列を指定。"
|
||||||
), # speaker_name与 speaker_id二者选其一
|
),
|
||||||
speaker_id: int = Query(None, description="说话人id,与speaker_name二选一"),
|
speaker_id: int = Query(
|
||||||
sdp_ratio: float = Query(0.2, description="SDP/DP混合比"),
|
0, description="話者ID。model_assets.[model].config.json内のspk2idを確認。"),
|
||||||
noise: float = Query(0.2, description="感情"),
|
sdp_ratio: float = Query(DEFAULT_SDP_RATIO, description="SDP(Stochastic Duration Predictor)/DP混合比。比率が高くなるほど、トーンのばらつきが大きくなる。"),
|
||||||
noisew: float = Query(0.9, description="音素长度"),
|
noise: float = Query(DEFAULT_NOISE, description="サンプルノイズの割合。大きくするほどランダム性が高まる"),
|
||||||
length: float = Query(1, description="语速"),
|
noisew: float = Query(DEFAULT_NOISEW, description="SDPノイズ。大きくするほど発音の間隔にばらつきが出やすくなる。"),
|
||||||
language: str = Query(None, description="语言"), # 若不指定使用语言则使用默认值
|
length: float = Query(DEFAULT_LENGTH, description="話速。基準は1で大きくするほど音声は長くなり読み上げが遅まる。"),
|
||||||
auto_translate: bool = Query(False, description="自动翻译"),
|
language: str = Query(ln, description=f"{'/'.join(available_languages)}のいずれか。"),
|
||||||
auto_split: bool = Query(False, description="自动切分"),
|
auto_split: bool = Query(True, description="改行で分けて生成"),
|
||||||
emotion: Optional[Union[int, str]] = Query(None, description="emo"),
|
split_interval: float = Query(DEFAULT_SPLIT_INTERVAL, description="分けた場合に挟む無音の長さ(秒)"),
|
||||||
style_text: Optional[str] = Query(None, description="风格文本"),
|
style_text: Optional[str] = Query(
|
||||||
style_weight: float = Query(0.7, description="风格权重"),
|
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を音声ファイルで行う"),
|
||||||
):
|
):
|
||||||
"""语音接口"""
|
"""Infer text to speech(テキストから感情付き音声を生成する)"""
|
||||||
logger.info(
|
logger.info(
|
||||||
f"{request.client.host}:{request.client.port}/voice { unquote(str(request.query_params) )}"
|
f"{request.client.host}:{request.client.port}/voice { unquote(str(request.query_params) )}"
|
||||||
)
|
)
|
||||||
@@ -378,158 +171,41 @@ if __name__ == "__main__":
|
|||||||
noisew=noisew,
|
noisew=noisew,
|
||||||
length=length,
|
length=length,
|
||||||
language=language,
|
language=language,
|
||||||
auto_translate=auto_translate,
|
|
||||||
auto_split=auto_split,
|
auto_split=auto_split,
|
||||||
emotion=emotion,
|
split_interval=split_interval,
|
||||||
style_text=style_text,
|
style_text=style_text,
|
||||||
style_weight=style_weight,
|
style_weight=style_weight,
|
||||||
|
emotion=emotion,
|
||||||
|
emotion_weight=emotion_weight,
|
||||||
|
reference_audio_path=reference_audio_path,
|
||||||
)
|
)
|
||||||
|
|
||||||
@app.get("/models/info")
|
@app.get("/models/info")
|
||||||
def get_loaded_models_info(request: Request):
|
def get_loaded_models_info(request: Request):
|
||||||
"""获取已加载模型信息"""
|
"""ロードされたモデル情報の取得"""
|
||||||
|
|
||||||
result: Dict[str, Dict] = dict()
|
result: Dict[str, Dict] = dict()
|
||||||
for key, model in loaded_models.models.items():
|
for model_id, model in enumerate(model_holder.models):
|
||||||
result[str(key)] = model.to_dict()
|
result[str(model_id)] = {
|
||||||
return result
|
"config_path": model.config_path,
|
||||||
|
"model_path": model.model_path,
|
||||||
@app.get("/models/delete")
|
"device": model.device,
|
||||||
def delete_model(
|
"spk2id": model.spk2id,
|
||||||
request: Request, model_id: int = Query(..., description="删除模型id")
|
"id2spk": model.id2spk,
|
||||||
):
|
"style2id": model.style2id,
|
||||||
"""删除指定模型"""
|
|
||||||
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
|
return result
|
||||||
|
|
||||||
@app.get("/models/get_unloaded")
|
@app.get("/models/refresh")
|
||||||
def get_unloaded_models_info(
|
def refresh():
|
||||||
request: Request, root_dir: str = Query("Data", description="搜索根目录")
|
"""モデルをパスに追加/削除した際などに読み込ませる"""
|
||||||
):
|
model_holder.refresh()
|
||||||
"""获取未加载模型"""
|
load_models(model_holder)
|
||||||
logger.info(
|
return {}
|
||||||
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.get("/status")
|
@app.get("/status")
|
||||||
def get_status():
|
def get_status():
|
||||||
"""获取电脑运行状态"""
|
"""実行環境のステータスを取得"""
|
||||||
cpu_percent = psutil.cpu_percent(interval=1)
|
cpu_percent = psutil.cpu_percent(interval=1)
|
||||||
memory_info = psutil.virtual_memory()
|
memory_info = psutil.virtual_memory()
|
||||||
memory_total = memory_info.total
|
memory_total = memory_info.total
|
||||||
@@ -563,104 +239,9 @@ if __name__ == "__main__":
|
|||||||
"gpu": gpuInfo,
|
"gpu": gpuInfo,
|
||||||
}
|
}
|
||||||
|
|
||||||
@app.get("/tools/translate")
|
|
||||||
def translate(
|
|
||||||
request: Request,
|
|
||||||
texts: str = Query(..., description="待翻译文本"),
|
|
||||||
to_language: str = Query(..., description="翻译目标语言"),
|
|
||||||
):
|
|
||||||
"""翻译"""
|
|
||||||
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")
|
@app.get("/tools/get_audio")
|
||||||
def get_audio(request: Request, path: str = Query(..., description="本地音频路径")):
|
def get_audio(request: Request, path: str = Query(..., description="local wav path")):
|
||||||
|
"""wavデータを取得する"""
|
||||||
logger.info(
|
logger.info(
|
||||||
f"{request.client.host}:{request.client.port}/tools/get_audio { unquote(str(request.query_params) )}"
|
f"{request.client.host}:{request.client.port}/tools/get_audio { unquote(str(request.query_params) )}"
|
||||||
)
|
)
|
||||||
@@ -672,10 +253,9 @@ if __name__ == "__main__":
|
|||||||
return {"status": 19, "detail": "非wav格式文件"}
|
return {"status": 19, "detail": "非wav格式文件"}
|
||||||
return FileResponse(path=path)
|
return FileResponse(path=path)
|
||||||
|
|
||||||
logger.warning("本地服务,请勿将服务端口暴露于外网")
|
|
||||||
logger.info(f"api文档地址 http://127.0.0.1:{config.server_config.port}/docs")
|
logger.info(f"server listen: http://127.0.0.1:{config.server_config.port}")
|
||||||
if os.path.isdir(StaticDir):
|
logger.info(f"API docs: http://127.0.0.1:{config.server_config.port}/docs")
|
||||||
webbrowser.open(f"http://127.0.0.1:{config.server_config.port}")
|
|
||||||
uvicorn.run(
|
uvicorn.run(
|
||||||
app, port=config.server_config.port, host="0.0.0.0", log_level="warning"
|
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
|
from config import config
|
||||||
|
|
||||||
MAX_CLUSTER_NUM = 10
|
MAX_CLUSTER_NUM = 10
|
||||||
|
DEFAULT_EMOTION: str = 'Neutral'
|
||||||
|
|
||||||
tsne = TSNE(n_components=2, random_state=42, metric="cosine")
|
tsne = TSNE(n_components=2, random_state=42, metric="cosine")
|
||||||
|
|
||||||
@@ -132,7 +133,7 @@ def save_only_mean(model_name):
|
|||||||
with open(config_path, "r") as f:
|
with open(config_path, "r") as f:
|
||||||
json_dict = json.load(f)
|
json_dict = json.load(f)
|
||||||
json_dict["data"]["num_styles"] = 1
|
json_dict["data"]["num_styles"] = 1
|
||||||
json_dict["data"]["style2id"] = {"Neutral": 0}
|
json_dict["data"]["style2id"] = {DEFAULT_EMOTION: 0}
|
||||||
with open(config_path, "w") as f:
|
with open(config_path, "w") as f:
|
||||||
json.dump(json_dict, f, indent=2)
|
json.dump(json_dict, f, indent=2)
|
||||||
return f"成功!\n{style_vector_path}に保存し{config_path}を更新しました。"
|
return f"成功!\n{style_vector_path}に保存し{config_path}を更新しました。"
|
||||||
@@ -152,7 +153,7 @@ def save_style_vectors(model_name, style_names: str):
|
|||||||
config_path = os.path.join(result_dir, "config.json")
|
config_path = os.path.join(result_dir, "config.json")
|
||||||
if not os.path.exists(config_path):
|
if not os.path.exists(config_path):
|
||||||
return f"{config_path}が存在しません。"
|
return f"{config_path}が存在しません。"
|
||||||
style_name_list = ["Neutral"]
|
style_name_list = [DEFAULT_EMOTION]
|
||||||
style_name_list = style_name_list + style_names.split(",")
|
style_name_list = style_name_list + style_names.split(",")
|
||||||
if len(style_name_list) != len(centroids) + 1:
|
if len(style_name_list) != len(centroids) + 1:
|
||||||
return f"スタイルの数が合いません。`,`で正しく{len(centroids)}個に区切られているか確認してください: {style_names}"
|
return f"スタイルの数が合いません。`,`で正しく{len(centroids)}個に区切られているか確認してください: {style_names}"
|
||||||
@@ -201,7 +202,7 @@ def save_style_vectors_from_files(model_name, audio_files_text, style_names_text
|
|||||||
config_path = os.path.join(result_dir, "config.json")
|
config_path = os.path.join(result_dir, "config.json")
|
||||||
if not os.path.exists(config_path):
|
if not os.path.exists(config_path):
|
||||||
return f"{config_path}が存在しません。"
|
return f"{config_path}が存在しません。"
|
||||||
style_name_list = ["Neutral"]
|
style_name_list = [DEFAULT_EMOTION]
|
||||||
style_name_list = style_name_list + style_names
|
style_name_list = style_name_list + style_names
|
||||||
assert len(style_name_list) == len(style_vectors)
|
assert len(style_name_list) == len(style_vectors)
|
||||||
|
|
||||||
@@ -216,7 +217,7 @@ def save_style_vectors_from_files(model_name, audio_files_text, style_names_text
|
|||||||
return f"成功!\n{style_vector_path}に保存し{config_path}を更新しました。"
|
return f"成功!\n{style_vector_path}に保存し{config_path}を更新しました。"
|
||||||
|
|
||||||
|
|
||||||
initial_md = """
|
initial_md = f"""
|
||||||
# Style Bert-VITS2 スタイルベクトルの作成
|
# Style Bert-VITS2 スタイルベクトルの作成
|
||||||
|
|
||||||
Style-Bert-VITS2で音声合成するには、スタイルベクトルのファイル`style_vectors.npy`が必要です。これをモデルごとに作成する必要があります。
|
Style-Bert-VITS2で音声合成するには、スタイルベクトルのファイル`style_vectors.npy`が必要です。これをモデルごとに作成する必要があります。
|
||||||
@@ -225,7 +226,7 @@ Style-Bert-VITS2で音声合成するには、スタイルベクトルのファ
|
|||||||
## 方法
|
## 方法
|
||||||
|
|
||||||
どうやってスタイルベクトルファイルを作るかはいくつか方法があります。
|
どうやってスタイルベクトルファイルを作るかはいくつか方法があります。
|
||||||
- 方法1: めんどくさいから平均スタイルのみを使う(使えるスタイルは標準のNeutralのみ)
|
- 方法1: めんどくさいから平均スタイルのみを使う(使えるスタイルは標準の{DEFAULT_EMOTION}のみ)
|
||||||
- 方法2: 音声ファイルを自動でスタイル別に分け、その各スタイルの平均を取って保存
|
- 方法2: 音声ファイルを自動でスタイル別に分け、その各スタイルの平均を取って保存
|
||||||
- 方法3: スタイルを代表する音声ファイルを手動で選んで、その音声のスタイルベクトルを保存
|
- 方法3: スタイルを代表する音声ファイルを手動で選んで、その音声のスタイルベクトルを保存
|
||||||
- 方法4: 自分でもっと頑張ってこだわって作る(JVNVコーパスなど、もともとスタイルラベル等が利用可能な場合はこれがよいかも)
|
- 方法4: 自分でもっと頑張ってこだわって作る(JVNVコーパスなど、もともとスタイルラベル等が利用可能な場合はこれがよいかも)
|
||||||
@@ -233,7 +234,7 @@ Style-Bert-VITS2で音声合成するには、スタイルベクトルのファ
|
|||||||
基本的には方法2を使うことを、めんどくさかったりあまり感情に幅がないデータセットなら方法1をおすすめします。
|
基本的には方法2を使うことを、めんどくさかったりあまり感情に幅がないデータセットなら方法1をおすすめします。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
method2 = """
|
method2 = f"""
|
||||||
学習の時に取り出したスタイルベクトルを読み込んで、可視化を見ながらスタイルを分けていきます。
|
学習の時に取り出したスタイルベクトルを読み込んで、可視化を見ながらスタイルを分けていきます。
|
||||||
|
|
||||||
手順:
|
手順:
|
||||||
@@ -245,102 +246,103 @@ method2 = """
|
|||||||
|
|
||||||
詳細: スタイルベクトル(256次元)たちを適当なアルゴリズムでクラスタリングして、各クラスタの中心のベクトル(と全体の平均ベクトル)を保存します。
|
詳細: スタイルベクトル(256次元)たちを適当なアルゴリズムでクラスタリングして、各クラスタの中心のベクトル(と全体の平均ベクトル)を保存します。
|
||||||
|
|
||||||
平均スタイル(Neutral)は自動的に保存されます。
|
平均スタイル({DEFAULT_EMOTION})は自動的に保存されます。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
with gr.Blocks(theme="NoCrypt/miku") as app:
|
if __name__ == "__main__":
|
||||||
gr.Markdown(initial_md)
|
with gr.Blocks(theme="NoCrypt/miku") as app:
|
||||||
with gr.Row():
|
gr.Markdown(initial_md)
|
||||||
model_name = gr.Textbox("your_model_name", label="モデル名")
|
|
||||||
load_button = gr.Button("スタイルベクトルを読み込む", variant="primary")
|
|
||||||
output = gr.Plot(label="音声スタイルの可視化")
|
|
||||||
load_button.click(load, inputs=[model_name], outputs=[output])
|
|
||||||
with gr.Tab("方法1: 平均スタイルのみを保存"):
|
|
||||||
gr.Markdown("平均(Neutral)スタイルのみを保存する場合は、以下のボタンを押してください。")
|
|
||||||
with gr.Row():
|
with gr.Row():
|
||||||
save_button1 = gr.Button("スタイルベクトルを保存", variant="primary")
|
model_name = gr.Textbox("your_model_name", label="モデル名")
|
||||||
info1 = gr.Textbox(label="保存結果")
|
load_button = gr.Button("スタイルベクトルを読み込む", variant="primary")
|
||||||
save_button1.click(save_only_mean, inputs=[model_name], outputs=[info1])
|
output = gr.Plot(label="音声スタイルの可視化")
|
||||||
with gr.Tab("方法2: スタイル分けを自動で行う"):
|
load_button.click(load, inputs=[model_name], outputs=[output])
|
||||||
n_clusters = gr.Slider(
|
with gr.Tab("方法1: 平均スタイルのみを保存"):
|
||||||
minimum=2,
|
gr.Markdown(f"平均({DEFAULT_EMOTION})スタイルのみを保存する場合は、以下のボタンを押してください。")
|
||||||
maximum=10,
|
|
||||||
step=1,
|
|
||||||
value=4,
|
|
||||||
label="作るスタイルの数(平均スタイルを除く)",
|
|
||||||
info="上の図を見ながらスタイルの数を試行錯誤してください。",
|
|
||||||
)
|
|
||||||
c_method = gr.Radio(
|
|
||||||
choices=[
|
|
||||||
"Agglomerative after t-SNE",
|
|
||||||
"KMeans after t-SNE",
|
|
||||||
"Agglomerative",
|
|
||||||
"KMeans",
|
|
||||||
],
|
|
||||||
label="アルゴリズム",
|
|
||||||
info="分類する(クラスタリング)アルゴリズムを選択します。いろいろ試してみてください。",
|
|
||||||
value="Agglomerative after t-SNE",
|
|
||||||
)
|
|
||||||
c_button = gr.Button("スタイル分けを実行")
|
|
||||||
audio_list = []
|
|
||||||
md_list = []
|
|
||||||
gr.Markdown("スタイル分けの結果と、各スタイルの特徴的な代表音声(図の黒い x 印)")
|
|
||||||
gr.Markdown("注意: もともと256次元なものをを2次元に落としているので、正確なベクトルの位置関係ではありません。")
|
|
||||||
with gr.Row():
|
|
||||||
gr_plot = gr.Plot()
|
|
||||||
with gr.Row():
|
with gr.Row():
|
||||||
for i in range(MAX_CLUSTER_NUM):
|
save_button1 = gr.Button("スタイルベクトルを保存", variant="primary")
|
||||||
with gr.Column():
|
info1 = gr.Textbox(label="保存結果")
|
||||||
md_list.append(gr.Markdown(visible=False))
|
save_button1.click(save_only_mean, inputs=[model_name], outputs=[info1])
|
||||||
audio_list.append(
|
with gr.Tab("方法2: スタイル分けを自動で行う"):
|
||||||
gr.Audio(
|
n_clusters = gr.Slider(
|
||||||
visible=False,
|
minimum=2,
|
||||||
scale=1,
|
maximum=10,
|
||||||
show_label=True,
|
step=1,
|
||||||
label=f"スタイル{i+1}",
|
value=4,
|
||||||
|
label="作るスタイルの数(平均スタイルを除く)",
|
||||||
|
info="上の図を見ながらスタイルの数を試行錯誤してください。",
|
||||||
|
)
|
||||||
|
c_method = gr.Radio(
|
||||||
|
choices=[
|
||||||
|
"Agglomerative after t-SNE",
|
||||||
|
"KMeans after t-SNE",
|
||||||
|
"Agglomerative",
|
||||||
|
"KMeans",
|
||||||
|
],
|
||||||
|
label="アルゴリズム",
|
||||||
|
info="分類する(クラスタリング)アルゴリズムを選択します。いろいろ試してみてください。",
|
||||||
|
value="Agglomerative after t-SNE",
|
||||||
|
)
|
||||||
|
c_button = gr.Button("スタイル分けを実行")
|
||||||
|
audio_list = []
|
||||||
|
md_list = []
|
||||||
|
gr.Markdown("スタイル分けの結果と、各スタイルの特徴的な代表音声(図の黒い x 印)")
|
||||||
|
gr.Markdown("注意: もともと256次元なものをを2次元に落としているので、正確なベクトルの位置関係ではありません。")
|
||||||
|
with gr.Row():
|
||||||
|
gr_plot = gr.Plot()
|
||||||
|
with gr.Row():
|
||||||
|
for i in range(MAX_CLUSTER_NUM):
|
||||||
|
with gr.Column():
|
||||||
|
md_list.append(gr.Markdown(visible=False))
|
||||||
|
audio_list.append(
|
||||||
|
gr.Audio(
|
||||||
|
visible=False,
|
||||||
|
scale=1,
|
||||||
|
show_label=True,
|
||||||
|
label=f"スタイル{i+1}",
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
c_button.click(
|
||||||
c_button.click(
|
do_clustering_gradio,
|
||||||
do_clustering_gradio,
|
inputs=[n_clusters, c_method],
|
||||||
inputs=[n_clusters, c_method],
|
outputs=[gr_plot] + audio_list + md_list,
|
||||||
outputs=[gr_plot] + audio_list + md_list,
|
)
|
||||||
)
|
gr.Markdown("結果が良さそうなら、これを保存します。")
|
||||||
gr.Markdown("結果が良さそうなら、これを保存します。")
|
style_names = gr.Textbox(
|
||||||
style_names = gr.Textbox(
|
"Angry, Sad, Happy",
|
||||||
"Angry, Sad, Happy",
|
label="スタイルの名前",
|
||||||
label="スタイルの名前",
|
info=f"スタイルの名前を`,`で区切って入力してください(日本語可)。例: `Angry, Sad, Happy`や`怒り, 悲しみ, 喜び`など。平均音声は{DEFAULT_EMOTION}として自動的に保存されます。",
|
||||||
info="スタイルの名前を`,`で区切って入力してください(日本語可)。例: `Angry, Sad, Happy`や`怒り, 悲しみ, 喜び`など。平均音声はNeutralとして自動的に保存されます。",
|
)
|
||||||
)
|
with gr.Row():
|
||||||
with gr.Row():
|
save_button = gr.Button("スタイルベクトルを保存", variant="primary")
|
||||||
save_button = gr.Button("スタイルベクトルを保存", variant="primary")
|
info2 = gr.Textbox(label="保存結果")
|
||||||
info2 = gr.Textbox(label="保存結果")
|
|
||||||
|
|
||||||
save_button.click(
|
save_button.click(
|
||||||
save_style_vectors, inputs=[model_name, style_names], outputs=[info2]
|
save_style_vectors, inputs=[model_name, style_names], outputs=[info2]
|
||||||
)
|
|
||||||
with gr.Tab("方法3: 手動でスタイルを選ぶ"):
|
|
||||||
gr.Markdown("下のテキスト欄に、各スタイルの代表音声のファイル名を`,`区切りで、その横に対応するスタイル名を`,`区切りで入力してください。")
|
|
||||||
gr.Markdown("例: `angry.wav, sad.wav, happy.wav`と`Angry, Sad, Happy`")
|
|
||||||
gr.Markdown("注意: Neutralスタイルは自動的に保存されます、手動ではNeutralという名前のスタイルは指定しないでください。")
|
|
||||||
with gr.Row():
|
|
||||||
audio_files_text = gr.Textbox(
|
|
||||||
label="音声ファイル名", placeholder="angry.wav, sad.wav, happy.wav"
|
|
||||||
)
|
)
|
||||||
style_names_text = gr.Textbox(
|
with gr.Tab("方法3: 手動でスタイルを選ぶ"):
|
||||||
label="スタイル名", placeholder="Angry, Sad, Happy"
|
gr.Markdown("下のテキスト欄に、各スタイルの代表音声のファイル名を`,`区切りで、その横に対応するスタイル名を`,`区切りで入力してください。")
|
||||||
|
gr.Markdown("例: `angry.wav, sad.wav, happy.wav`と`Angry, Sad, Happy`")
|
||||||
|
gr.Markdown(f"注意: {DEFAULT_EMOTION}スタイルは自動的に保存されます、手動では{DEFAULT_EMOTION}という名前のスタイルは指定しないでください。")
|
||||||
|
with gr.Row():
|
||||||
|
audio_files_text = gr.Textbox(
|
||||||
|
label="音声ファイル名", placeholder="angry.wav, sad.wav, happy.wav"
|
||||||
|
)
|
||||||
|
style_names_text = gr.Textbox(
|
||||||
|
label="スタイル名", placeholder="Angry, Sad, Happy"
|
||||||
|
)
|
||||||
|
with gr.Row():
|
||||||
|
save_button3 = gr.Button("スタイルベクトルを保存", variant="primary")
|
||||||
|
info3 = gr.Textbox(label="保存結果")
|
||||||
|
save_button3.click(
|
||||||
|
save_style_vectors_from_files,
|
||||||
|
inputs=[model_name, audio_files_text, style_names_text],
|
||||||
|
outputs=[info3],
|
||||||
|
)
|
||||||
|
with gr.Tab("方法4: がんばる"):
|
||||||
|
gr.Markdown(
|
||||||
|
"`clustering.ipynb`にjvnvコーパスの場合の作り方とかクラスタ分けのいろいろを書いています。これを参考に自分で頑張って作ってください。"
|
||||||
)
|
)
|
||||||
with gr.Row():
|
|
||||||
save_button3 = gr.Button("スタイルベクトルを保存", variant="primary")
|
|
||||||
info3 = gr.Textbox(label="保存結果")
|
|
||||||
save_button3.click(
|
|
||||||
save_style_vectors_from_files,
|
|
||||||
inputs=[model_name, audio_files_text, style_names_text],
|
|
||||||
outputs=[info3],
|
|
||||||
)
|
|
||||||
with gr.Tab("方法4: がんばる"):
|
|
||||||
gr.Markdown(
|
|
||||||
"`clustering.ipynb`にjvnvコーパスの場合の作り方とかクラスタ分けのいろいろを書いています。これを参考に自分で頑張って作ってください。"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
app.launch(inbrowser=True)
|
app.launch(inbrowser=True)
|
||||||
|
|||||||
Reference in New Issue
Block a user