| print("Starting up. Please be patient...") |
|
|
| import argparse |
| import datetime |
| import os |
| import sys |
| from typing import Optional |
| import json |
| import utils |
| import shutil |
|
|
| import gradio as gr |
| import torch |
| import yaml |
|
|
| from common.constants import ( |
| DEFAULT_ASSIST_TEXT_WEIGHT, |
| DEFAULT_LENGTH, |
| DEFAULT_LINE_SPLIT, |
| DEFAULT_NOISE, |
| DEFAULT_NOISEW, |
| DEFAULT_SDP_RATIO, |
| DEFAULT_SPLIT_INTERVAL, |
| DEFAULT_STYLE, |
| DEFAULT_STYLE_WEIGHT, |
| Languages, |
| ) |
| from common.log import logger |
|
|
| |
| from huggingface_hub import snapshot_download |
|
|
| def download_assets(): |
| print("Checking and downloading assets from main repo...") |
| REPO_ID = "Plana-Archive/Plana-TTS" |
| SUBFOLDER = "Hololive-Style-Bert-VITS2" |
| TEMP_DIR = "temp_download" |
| |
| if not os.path.exists("./bert"): |
| try: |
| print(f"Downloading folder bert and model_assets from {REPO_ID}...") |
| snapshot_download( |
| repo_id=REPO_ID, |
| allow_patterns=[ |
| f"{SUBFOLDER}/bert/**/*", |
| f"{SUBFOLDER}/model_assets/**/*" |
| ], |
| local_dir=TEMP_DIR, |
| token=os.getenv("HF_TOKEN") |
| ) |
| |
| src_path = os.path.join(TEMP_DIR, SUBFOLDER) |
| if os.path.exists(src_path): |
| for item in os.listdir(src_path): |
| s = os.path.join(src_path, item) |
| d = os.path.join(".", item) |
| if os.path.isdir(s): |
| if os.path.exists(d): |
| shutil.rmtree(d) |
| shutil.move(s, d) |
| else: |
| shutil.move(s, d) |
| |
| shutil.rmtree(TEMP_DIR) |
| print("✅ Assets moved to root successfully.") |
| else: |
| print("⚠️ Source path not found in download.") |
| except Exception as e: |
| print(f"❌ Download/Move failed: {e}") |
| else: |
| print("✅ Assets already exist, skipping download.") |
|
|
| download_assets() |
|
|
| from common.tts_model import ModelHolder |
| from infer import InvalidToneError |
| from text.japanese import g2kata_tone, kata_tone2phone_tone, text_normalize |
|
|
| import nltk |
| nltk.download('averaged_perceptron_tagger_eng') |
|
|
| is_hf_spaces = os.getenv("SYSTEM") == "spaces" |
| limit = 150 |
|
|
| with open(os.path.join("configs", "paths.yml"), "r", encoding="utf-8") as f: |
| path_config: dict[str, str] = yaml.safe_load(f.read()) |
| assets_root = path_config["assets_root"] |
|
|
| def tts_fn( |
| model_name, |
| model_path, |
| text, |
| language, |
| reference_audio_path, |
| sdp_ratio, |
| noise_scale, |
| noise_scale_w, |
| speed_rate, |
| line_split, |
| split_interval, |
| assist_text, |
| assist_text_weight, |
| use_assist_text, |
| style, |
| style_weight, |
| kata_tone_json_str, |
| use_tone, |
| speaker, |
| ): |
| if len(text)<2: |
| return "Please enter some text.", None, kata_tone_json_str |
| |
| if is_hf_spaces and len(text) > limit: |
| return f"Too long! There is a character limit of {limit} characters.", None, kata_tone_json_str |
|
|
| if(not model_holder.current_model): |
| model_holder.load_model_gr(model_name, model_path) |
| logger.info(f"Loaded model '{model_name}'") |
| if(model_holder.current_model.model_path != model_path): |
| model_holder.load_model_gr(model_name, model_path) |
| logger.info(f"Swapped to model '{model_name}'") |
| speaker_id = model_holder.current_model.spk2id[speaker] |
| start_time = datetime.datetime.now() |
|
|
| |
| length_scale = 1.0 / speed_rate |
|
|
| wrong_tone_message = "" |
| kata_tone: Optional[list[tuple[str, int]]] = None |
| if use_tone and kata_tone_json_str != "": |
| if language != "JP": |
| wrong_tone_message = "アクセント指定は現在日本語のみ対応しています。" |
| if line_split: |
| wrong_tone_message = ( |
| "アクセント指定は改行で分けて生成を使わない場合のみ対応しています。" |
| ) |
| try: |
| kata_tone = [] |
| json_data = json.loads(kata_tone_json_str) |
| for kana, tone in json_data: |
| assert isinstance(kana, str) and tone in (0, 1), f"{kana}, {tone}" |
| kata_tone.append((kana, tone)) |
| except Exception as e: |
| logger.warning(f"Error occurred when parsing kana_tone_json: {e}") |
| wrong_tone_message = f"アクセント指定が不正です: {e}" |
| kata_tone = None |
|
|
| tone: Optional[list[int]] = None |
| if kata_tone is not None: |
| phone_tone = kata_tone2phone_tone(kata_tone) |
| tone = [t for _, t in phone_tone] |
| |
| try: |
| sr, audio = model_holder.current_model.infer( |
| text=text, |
| language=language, |
| reference_audio_path=reference_audio_path, |
| sdp_ratio=sdp_ratio, |
| noise=noise_scale, |
| noisew=noise_scale_w, |
| length=length_scale, |
| line_split=line_split, |
| split_interval=split_interval, |
| assist_text=assist_text, |
| assist_text_weight=assist_text_weight, |
| use_assist_text=use_assist_text, |
| style=style, |
| style_weight=style_weight, |
| given_tone=tone, |
| sid=speaker_id, |
| ) |
| except InvalidToneError as e: |
| logger.error(f"Tone error: {e}") |
| return f"Error: アクセント指定が不正です:\n{e}", None, kata_tone_json_str |
| except ValueError as e: |
| logger.error(f"Value error: {e}") |
| return f"Error: {e}", None, kata_tone_json_str |
|
|
| end_time = datetime.datetime.now() |
| duration = (end_time - start_time).total_seconds() |
|
|
| if tone is None and language == "JP": |
| norm_text = text_normalize(text) |
| kata_tone = g2kata_tone(norm_text) |
| kata_tone_json_str = json.dumps(kata_tone, ensure_ascii=False) |
| elif tone is None: |
| kata_tone_json_str = "" |
|
|
| if reference_audio_path: |
| style="External Audio" |
| logger.info(f"Successful inference, took {duration}s | {speaker} | {language}/{sdp_ratio}/{noise_scale}/{noise_scale_w}/{length_scale}/{style}/{style_weight} | {text}") |
| message = f"Success, time: {duration} seconds." |
| if wrong_tone_message != "": |
| message = wrong_tone_message + "\n" + message |
| return message, (sr, audio), kata_tone_json_str |
|
|
| def load_voicedata(): |
| print("Loading voice data...") |
| envoices = [] |
| jpvoices = [] |
| styledict = {} |
| with open("voicelist.json", "r", encoding="utf-8") as f: |
| voc_info = json.load(f) |
| for name, info in voc_info.items(): |
| if not info['enable']: |
| continue |
| model_path = info['model_path'] |
| model_path_full = f"{model_dir}/{model_path}/{model_path}.safetensors" |
| if not os.path.exists(model_path_full): |
| model_path_full = f"{model_dir}\\{model_path}\\{model_path}.safetensors" |
| voice_name = info['title'] |
| speakerid = info['speakerid'] |
| datasetauthor = info['datasetauthor'] |
| image = info['cover'] |
| if not os.path.exists(f"images/{image}"): |
| image="none.png" |
| nospace=False |
| if 'disableonspace' in info: |
| nospace=info['disableonspace'] |
| if not model_path in styledict.keys(): |
| conf=f"{model_dir}/{model_path}/config.json" |
| hps = utils.get_hparams_from_file(conf) |
| s2id = hps.data.style2id |
| styledict[model_path] = s2id.keys() |
| print(f"Set up hyperparameters for model {model_path}") |
| if(info['primarylang']=="JP"): |
| jpvoices.append((name, model_path, model_path_full, voice_name, speakerid, datasetauthor, image, nospace)) |
| else: |
| envoices.append((name, model_path, model_path_full, voice_name, speakerid, datasetauthor, image, nospace)) |
| return [envoices, jpvoices], styledict |
| |
|
|
| initial_text = "Hello there! This is test audio of a new Hololive text to speech tool." |
| initial_md = """""" |
| style_md = """ |
| - You can control things like voice tone, emotion, and reading style through presets or through voice files. |
| - Neutral acts as an average across all speakers. Styling options act as an override to Neutral. |
| - Setting the intensity too high will likely break the output. |
| """ |
|
|
| 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=assets_root) |
| parser.add_argument("--share", action="store_true", help="Share this app publicly", default=False) |
| parser.add_argument("--server-name", type=str, default=None, help="Server name for Gradio app") |
| parser.add_argument("--no-autolaunch", action="store_true", default=False, help="Do not launch app automatically") |
| args = parser.parse_args() |
| model_dir = args.dir |
|
|
| if args.cpu: |
| device = "cpu" |
| else: |
| device = "cuda" if torch.cuda.is_available() else "cpu" |
|
|
| model_holder = ModelHolder(model_dir, device) |
| languages = ["EN", "JP", "ZH"] |
| langnames = ["English", "Japanese"] |
|
|
| model_names = model_holder.model_names |
| if len(model_names) == 0: |
| logger.error(f"No models found. Please place the model in {model_dir}.") |
| sys.exit(1) |
| |
| voicedata, styledict = load_voicedata() |
| total_characters = sum(len(lang) for lang in voicedata) |
|
|
| text_input = gr.TextArea(label="Text", value=initial_text) |
| line_split = gr.Checkbox(label="Divide text seperately by line breaks", value=True) |
| split_interval = gr.Slider(minimum=0.0, maximum=2, value=0.5, step=0.1, label="Length of division seperation time (in seconds)") |
| language = gr.Dropdown(choices=languages, value="EN", label="Language") |
| sdp_ratio = gr.Slider(minimum=0, maximum=1, value=0.2, step=0.1, label="SDP Ratio") |
| noise_scale = gr.Slider(minimum=0.1, maximum=2, value=0.6, step=0.1, label="Noise") |
| noise_scale_w = gr.Slider(minimum=0.1, maximum=2, value=0.8, step=0.1, label="Noise_W") |
| speed_rate = gr.Slider(minimum=0.5, maximum=2.0, value=1.0, step=0.1, label="Kecepatan Suara") |
| use_style_text = gr.Checkbox(label="Use stylization text", value=False) |
| style_text = gr.Textbox(label="Style text", placeholder="Check the box to use this option!", visible=True) |
| style_text_weight = gr.Slider(minimum=0, maximum=1, value=0.7, step=0.1, label="Text stylization strength", visible=True) |
|
|
| |
| with gr.Blocks(theme=gr.themes.Soft(primary_hue="slate", secondary_hue="zinc"), title="Hololive Style-Bert-VITS2") as app: |
| |
| gr.HTML(f""" |
| <div style="font-family: 'Arial', sans-serif; max-width: 800px; margin: 20px auto 10px auto; border: 1px solid #e2e8f0; border-radius: 12px; padding: 15px; background-color: #ffffff; text-align: center; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);"> |
| <h1 style="color: #475569; margin: 0; font-weight: 800;">HOLOLIVE VTUBER</h1> |
| <p style="color: #94a3b8; font-size: 14px; margin-top: 5px; font-weight: 500;">Hololive TTS • VTUBER by Plana-Chan</p> |
| </div> |
| |
| <div style="font-family: 'Arial', sans-serif; max-width: 800px; margin: 0 auto 20px auto; border: 1px solid #e2e8f0; border-radius: 10px; padding: 15px; background-color: white; display: flex; justify-content: space-around; align-items: center; box-shadow: 0 2px 4px rgba(0,0,0,0.02);"> |
| <div style="text-align: center;"> |
| <p style="color: #94a3b8; font-size: 11px; font-weight: 700; margin: 0; text-transform: uppercase; letter-spacing: 1px;">System Status</p> |
| <p style="color: #64748b; font-size: 14px; font-weight: 700; margin: 0;">● ONLINE</p> |
| </div> |
| <div style="height: 30px; border-left: 1px solid #f1f5f9;"></div> |
| <div style="text-align: center;"> |
| <p style="color: #94a3b8; font-size: 11px; font-weight: 700; margin: 0; text-transform: uppercase; letter-spacing: 1px;">Total Characters</p> |
| <p style="color: #475569; font-size: 14px; font-weight: 700; margin: 0;">{total_characters} Models</p> |
| </div> |
| </div> |
| """) |
| |
| gr.Markdown(initial_md) |
| tone = gr.Textbox(label="Accent adjustment", visible=False) |
| use_tone = gr.Checkbox(label="Use accent adjustment", value=False, visible=False) |
|
|
| for vi in range(len(voicedata)): |
| with gr.TabItem(langnames[vi]): |
| for (name, model_path, model_path_full, voice_name, speakerid, datasetauthor, image, nospace) in voicedata[vi]: |
| if(nospace and is_hf_spaces): continue |
| with gr.TabItem(name): |
| mn = gr.Textbox(value=model_path, visible=False, interactive=False) |
| mp = gr.Textbox(value=model_path_full, visible=False, interactive=False) |
| spk = gr.Textbox(value=speakerid, visible=False, interactive=False) |
| with gr.Row(): |
| with gr.Column(): |
| gr.Markdown(f"### {voice_name}") |
| gr.Image(f"images/{image}", label=None, show_label=False, width=300, show_download_button=False, container=False, show_share_button=False) |
| with gr.Column(): |
| with gr.TabItem("Style using a preset"): |
| style = gr.Dropdown(label="Current style", choices=styledict[model_path], value="Neutral") |
| with gr.TabItem("Style using existing audio"): |
| ref_audio_path = gr.Audio(label="Reference Audio", type="filepath", sources=["upload"]) |
| |
| style_weight = gr.Slider(minimum=0, maximum=20, value=3, step=0.1, label="Style strength") |
| with gr.Column(): |
| tts_button = gr.Button("Generate Voice", variant="primary", interactive=True) |
| text_output = gr.Textbox(label="Info") |
| audio_output = gr.Audio(label="Result") |
|
|
| tts_button.click( |
| tts_fn, |
| inputs=[mn, mp, text_input, language, ref_audio_path, sdp_ratio, noise_scale, noise_scale_w, speed_rate, line_split, split_interval, style_text, style_text_weight, use_style_text, style, style_weight, tone, use_tone, spk], |
| outputs=[text_output, audio_output, tone], |
| ) |
|
|
| with gr.Row(): |
| with gr.Column(): |
| text_input.render() |
| line_split.render() |
| split_interval.render() |
| language.render() |
| with gr.Column(): |
| sdp_ratio.render() |
| noise_scale.render() |
| noise_scale_w.render() |
| speed_rate.render() |
| use_style_text.render() |
| style_text.render() |
| style_text_weight.render() |
|
|
| with gr.Accordion("Styling Guide", open=False): |
| gr.Markdown(style_md) |
|
|
| |
| gr.HTML(""" |
| <div style="font-family: 'Arial', sans-serif; max-width: 800px; margin: 30px auto 20px auto; border: 1px solid #e2e8f0; border-radius: 12px; padding: 20px; background-color: white; text-align: center; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);"> |
| <h3 style="color: #475569; font-size: 16px; margin: 0; font-weight: 700; letter-spacing: 1px;">CREATED BY PLANA-CHAN</h3> |
| <p style="color: #94a3b8; font-size: 13px; margin-top: 4px; font-weight: 500;">Hololive TTS Implementation</p> |
| </div> |
| """) |
|
|
| app.launch(allowed_paths=['/file/images/']) |