Spaces:
Running on Zero
Running on Zero
| import os | |
| # HF Spaces only: the runtime container cannot write to $HF_HOME | |
| # (/home/user/.cache/huggingface) — the build stage creates it as root while the | |
| # app runs as `user`. The xet downloader keeps both its cache and its logs under | |
| # that path, so hf_hub_download() dies with "Permission denied (os error 13)". | |
| # Point xet at /tmp, which is writable. This must run before huggingface_hub is | |
| # first imported, because its constants resolve these paths at import time. | |
| os.environ.setdefault("HF_XET_CACHE", "/tmp/hf_xet") | |
| import spaces | |
| import html | |
| import json | |
| import sys | |
| import threading | |
| import time | |
| import warnings | |
| warnings.filterwarnings("ignore", category=FutureWarning) | |
| warnings.filterwarnings("ignore", category=UserWarning) | |
| import pandas as pd | |
| current_dir = os.path.dirname(os.path.abspath(__file__)) | |
| sys.path.append(current_dir) | |
| sys.path.append(os.path.join(current_dir, "indextts")) | |
| import argparse | |
| parser = argparse.ArgumentParser( | |
| description="IndexTTS WebUI", | |
| formatter_class=argparse.ArgumentDefaultsHelpFormatter, | |
| ) | |
| parser.add_argument("--verbose", action="store_true", default=False, help="Enable verbose mode") | |
| parser.add_argument("--port", type=int, default=7860, help="Port to run the web UI on") | |
| parser.add_argument("--host", type=str, default="0.0.0.0", help="Host to run the web UI on") | |
| parser.add_argument("--model_dir", type=str, default="./checkpoints", help="Model checkpoints directory") | |
| parser.add_argument("--version", type=str, default="2.5", choices=["2", "2.5"], help="Model version to use") | |
| parser.add_argument("--fp16", action="store_true", default=False, help="Use FP16 for inference if available") | |
| parser.add_argument("--deepspeed", action="store_true", default=False, help="Use DeepSpeed to accelerate if available") | |
| parser.add_argument("--cuda_kernel", action="store_true", default=False, help="Use CUDA kernel for inference if available") | |
| parser.add_argument("--accel", action="store_true", default=False, help="Use GPT2 acceleration engine if available") | |
| parser.add_argument("--torch_compile", action="store_true", default=False, help="Use torch.compile to optimize s2mel if available") | |
| parser.add_argument("--qwen_emo", action="store_true", default=False, help="Load QwenEmotion even on a low-VRAM GPU, where it is skipped by default") | |
| parser.add_argument("--gui_seg_tokens", type=int, default=120, help="GUI: Max tokens per generation segment") | |
| cmd_args = parser.parse_args() | |
| # Validate optional acceleration dependencies early, so missing extras fail | |
| # at startup instead of halfway through inference. | |
| def _require_optional_extra(flag_name, module_name, install_cmd): | |
| try: | |
| __import__(module_name) | |
| except ImportError: | |
| parser.error( | |
| f"--{flag_name} requires {module_name}, which is not installed. " | |
| f"Install it with: {install_cmd}" | |
| ) | |
| if cmd_args.accel: | |
| _require_optional_extra("accel", "flash_attn", "uv sync --extra accel") | |
| if cmd_args.torch_compile: | |
| _require_optional_extra("torch_compile", "triton", "uv sync --extra torch_compile") | |
| REQUIRED_FILES = { | |
| "2": ["bpe.model", "gpt.pth", "s2mel.pth", "wav2vec2bert_stats.pt"], | |
| "2.5": [ | |
| "gpt.pth", | |
| "s2mel.pth", | |
| "codec.pth", | |
| "multilingual_zh_ja_yue_char_del.tiktoken", | |
| "wav2vec2bert_stats.pt", | |
| ], | |
| } | |
| MODEL_REPO = { | |
| "2": "IndexTeam/IndexTTS-2", | |
| "2.5": "IndexTeam/IndexTTS-2.5", | |
| } | |
| required_files = REQUIRED_FILES[cmd_args.version] | |
| missing = [f for f in required_files if not os.path.exists(os.path.join(cmd_args.model_dir, f))] | |
| if missing: | |
| print( | |
| f"Model directory {cmd_args.model_dir} is incomplete for v{cmd_args.version} " | |
| f"(missing: {', '.join(missing)}). Downloading {MODEL_REPO[cmd_args.version]}..." | |
| ) | |
| from indextts.utils.model_download import snapshot_download | |
| try: | |
| snapshot_download(MODEL_REPO[cmd_args.version], local_dir=cmd_args.model_dir) | |
| except Exception as e: | |
| print(f"Failed to download model to {cmd_args.model_dir}: {e}") | |
| sys.exit(1) | |
| missing = [f for f in required_files if not os.path.exists(os.path.join(cmd_args.model_dir, f))] | |
| if missing: | |
| print(f"Failed to download model to {cmd_args.model_dir} (still missing: {', '.join(missing)}). Please download it manually.") | |
| sys.exit(1) | |
| print("Model downloaded successfully.") | |
| from indextts.utils.model_download import ensure_config_available | |
| try: | |
| ensure_config_available(cmd_args.model_dir, version=cmd_args.version) | |
| except Exception as e: | |
| print(f"Failed to download config.yaml: {e}") | |
| sys.exit(1) | |
| IS_V25 = cmd_args.version == "2.5" | |
| import gradio as gr | |
| from indextts.utils.examples_downloader import ensure_examples_available | |
| from tools.i18n.i18n import I18nAuto | |
| if IS_V25: | |
| from indextts.infer_v2_5 import IndexTTS2 | |
| else: | |
| from indextts.infer_v2 import IndexTTS2 | |
| i18n = I18nAuto(language="Auto") | |
| MODE = 'local' | |
| # Download example audio files if missing | |
| ensure_examples_available() | |
| LOW_VRAM_THRESHOLD_GB = 10.0 | |
| def detect_vram_gb(): | |
| # HF Spaces only: ZeroGPU attaches the device inside @spaces.GPU functions, | |
| # so there is no GPU at import time and probing here would either report | |
| # nothing useful or initialise CUDA too early. This Space runs on a 24 GB | |
| # A10G, so the low-VRAM path is not needed. | |
| return None | |
| _vram_gb = detect_vram_gb() | |
| LOW_VRAM = _vram_gb is not None and _vram_gb < LOW_VRAM_THRESHOLD_GB | |
| HALF_PRECISION = cmd_args.fp16 or LOW_VRAM | |
| LOAD_QWEN_EMO = cmd_args.qwen_emo or not LOW_VRAM | |
| if LOW_VRAM: | |
| print( | |
| f">> {_vram_gb:.1f} GB VRAM detected (< {LOW_VRAM_THRESHOLD_GB:.0f} GB): " | |
| "enabling half precision" | |
| + ("" if LOAD_QWEN_EMO else " and skipping QwenEmotion") | |
| ) | |
| if not LOAD_QWEN_EMO: | |
| print(">> Emotion control from a text description is unavailable; pass --qwen_emo to force it.") | |
| def build_tts(use_accel=False, use_torch_compile=False): | |
| """Build an IndexTTS2 instance with the requested acceleration options.""" | |
| import torch | |
| kwargs = dict( | |
| model_dir=cmd_args.model_dir, | |
| cfg_path=os.path.join(cmd_args.model_dir, "config.yaml"), | |
| use_deepspeed=cmd_args.deepspeed, | |
| use_cuda_kernel=cmd_args.cuda_kernel, | |
| use_accel=use_accel, | |
| use_torch_compile=use_torch_compile, | |
| use_qwen_emo=LOAD_QWEN_EMO, | |
| ) | |
| if IS_V25: | |
| use_bf16 = HALF_PRECISION and torch.cuda.is_bf16_supported() | |
| if HALF_PRECISION and not use_bf16: | |
| print(">> BF16 is not supported on this device, falling back to full precision.") | |
| kwargs["use_bf16"] = use_bf16 | |
| else: | |
| kwargs["use_fp16"] = HALF_PRECISION | |
| return IndexTTS2(**kwargs) | |
| tts = build_tts(use_accel=cmd_args.accel, use_torch_compile=cmd_args.torch_compile) | |
| # 支持的语言列表 | |
| LANGUAGES = { | |
| "中文": "zh_CN", | |
| "English": "en_US" | |
| } | |
| EMO_CHOICES_ALL = [i18n("与音色参考音频相同"), | |
| i18n("使用情感参考音频"), | |
| i18n("使用情感向量控制"), | |
| i18n("使用情感描述文本控制")] | |
| EMO_CHOICES_OFFICIAL = EMO_CHOICES_ALL[:-1] # skip experimental features | |
| EMO_CHOICES_EXPERIMENTAL = EMO_CHOICES_ALL if LOAD_QWEN_EMO else EMO_CHOICES_OFFICIAL | |
| os.makedirs("outputs/tasks",exist_ok=True) | |
| os.makedirs("prompts",exist_ok=True) | |
| MAX_LENGTH_TO_USE_SPEED = 70 | |
| example_cases = [] | |
| with open("examples/cases.jsonl", "r", encoding="utf-8") as f: | |
| for line in f: | |
| line = line.strip() | |
| if not line: | |
| continue | |
| example = json.loads(line) | |
| if example.get("emo_audio",None): | |
| emo_audio_path = os.path.join("examples",example["emo_audio"]) | |
| else: | |
| emo_audio_path = None | |
| case = [os.path.join("examples", example.get("prompt_audio", "sample_prompt.wav")), | |
| EMO_CHOICES_ALL[example.get("emo_mode",0)], | |
| example.get("text"), | |
| emo_audio_path, | |
| example.get("emo_weight",1.0), | |
| example.get("emo_text",""), | |
| example.get("emo_vec_1",0), | |
| example.get("emo_vec_2",0), | |
| example.get("emo_vec_3",0), | |
| example.get("emo_vec_4",0), | |
| example.get("emo_vec_5",0), | |
| example.get("emo_vec_6",0), | |
| example.get("emo_vec_7",0), | |
| example.get("emo_vec_8",0), | |
| ] | |
| # v2.5 needs the language per-example so ja/es/ar demos auto-select it | |
| if IS_V25: | |
| case.append(example.get("lang", "ZH")) | |
| example_cases.append(case) | |
| def get_example_cases(include_experimental = False): | |
| if include_experimental and LOAD_QWEN_EMO: | |
| return example_cases # show every example | |
| # exclude emotion control mode 3 (emotion from text description) | |
| return [x for x in example_cases if x[1] != EMO_CHOICES_ALL[3]] | |
| def format_glossary_markdown(): | |
| """将词汇表转换为Markdown表格格式""" | |
| if IS_V25 or not hasattr(tts, 'normalizer') or not tts.normalizer.term_glossary: | |
| return i18n("暂无术语") | |
| lines = [f"| {i18n('术语')} | {i18n('中文读法')} | {i18n('英文读法')} |"] | |
| lines.append("|---|---|---|") | |
| for term, reading in tts.normalizer.term_glossary.items(): | |
| zh = reading.get("zh", "") if isinstance(reading, dict) else reading | |
| en = reading.get("en", "") if isinstance(reading, dict) else reading | |
| lines.append(f"| {term} | {zh} | {en} |") | |
| return "\n".join(lines) | |
| def gen_single(emo_control_method,prompt, text, | |
| lang_choice, | |
| emo_ref_path, emo_weight, | |
| vec1, vec2, vec3, vec4, vec5, vec6, vec7, vec8, | |
| emo_text,emo_random, | |
| max_text_tokens_per_segment=120, | |
| duration_factor=1.0, | |
| *args, progress=gr.Progress()): | |
| output_path = None | |
| if not output_path: | |
| output_path = os.path.join("outputs", f"spk_{int(time.time())}.wav") | |
| # set gradio progress | |
| tts.gr_progress = progress | |
| do_sample, top_p, top_k, temperature, \ | |
| length_penalty, num_beams, repetition_penalty, max_mel_tokens = args | |
| kwargs = { | |
| "do_sample": bool(do_sample), | |
| "top_p": float(top_p), | |
| "top_k": int(top_k) if int(top_k) > 0 else None, | |
| "temperature": float(temperature), | |
| "length_penalty": float(length_penalty), | |
| "num_beams": num_beams, | |
| "repetition_penalty": float(repetition_penalty), | |
| "max_mel_tokens": int(max_mel_tokens), | |
| # "typical_sampling": bool(typical_sampling), | |
| # "typical_mass": float(typical_mass), | |
| } | |
| if type(emo_control_method) is not int: | |
| emo_control_method = emo_control_method.value | |
| if emo_control_method == 0: # emotion from speaker | |
| emo_ref_path = None # remove external reference audio | |
| if emo_control_method == 1: # emotion from reference audio | |
| pass | |
| if emo_control_method == 2: # emotion from custom vectors | |
| vec = [vec1, vec2, vec3, vec4, vec5, vec6, vec7, vec8] | |
| vec = tts.normalize_emo_vec(vec, apply_bias=True) | |
| else: | |
| # don't use the emotion vector inputs for the other modes | |
| vec = None | |
| if emo_text == "": | |
| # erase empty emotion descriptions; `infer()` will then automatically use the main prompt | |
| emo_text = None | |
| print(f"Emo control mode:{emo_control_method},weight:{emo_weight},vec:{vec}") | |
| infer_kwargs = dict( | |
| spk_audio_prompt=prompt, text=text, | |
| output_path=output_path, | |
| emo_audio_prompt=emo_ref_path, emo_alpha=emo_weight, | |
| emo_vector=vec, | |
| use_emo_text=(emo_control_method==3), emo_text=emo_text, use_random=emo_random, | |
| verbose=cmd_args.verbose, | |
| max_text_tokens_per_segment=int(max_text_tokens_per_segment), | |
| duration_factor=float(duration_factor), | |
| **kwargs, | |
| ) | |
| if IS_V25: | |
| infer_kwargs["lang"] = lang_choice or "ZH" | |
| output = tts.infer(**infer_kwargs) | |
| return gr.update(value=output,visible=True) | |
| def update_prompt_audio(): | |
| update_button = gr.update(interactive=True) | |
| return update_button | |
| def create_warning_message(warning_text): | |
| return gr.HTML(f"<div style=\"padding: 0.5em 0.8em; border-radius: 0.5em; background: #ffa87d; color: #000; font-weight: bold\">{html.escape(warning_text)}</div>") | |
| def create_experimental_warning_message(): | |
| return create_warning_message(i18n('提示:此功能为实验版,结果尚不稳定,我们正在持续优化中。')) | |
| with gr.Blocks( | |
| title=f"IndexTTS-{cmd_args.version} Demo", | |
| css=""" | |
| /* Make the voice reference audio upload area more compact. */ | |
| #prompt_audio_compact .audio-container, | |
| #prompt_audio_compact .upload-container { | |
| min-height: 110px !important; | |
| } | |
| #prompt_audio_compact .empty { | |
| min-height: 80px !important; | |
| } | |
| """, | |
| ) as demo: | |
| mutex = threading.Lock() | |
| arxiv_id = "2601.03888" if IS_V25 else "2506.21619" | |
| gr.HTML(f''' | |
| <h2 style="text-align:center">IndexTTS-{cmd_args.version}</h2> | |
| <p align="center"> | |
| <a href='https://arxiv.org/abs/{arxiv_id}'><img src='https://img.shields.io/badge/ArXiv-{arxiv_id}-red'></a> | |
| </p> | |
| ''') | |
| with gr.Tab(i18n("音频生成")): | |
| os.makedirs("prompts", exist_ok=True) | |
| # Voice reference section | |
| gr.Markdown(f"### {i18n('音色参考音频')}") | |
| prompt_audio = gr.Audio( | |
| label="", | |
| key="prompt_audio", | |
| sources=["upload", "microphone"], | |
| type="filepath", | |
| elem_classes=["compact-audio"], | |
| elem_id="prompt_audio_compact", | |
| ) | |
| # Text input and generation section | |
| gr.Markdown(f"### {i18n('文本')}") | |
| with gr.Row(equal_height=False): | |
| with gr.Column(scale=2): | |
| input_text_single = gr.TextArea( | |
| label="", | |
| key="input_text_single", | |
| placeholder=i18n("请输入目标文本"), | |
| info=f"{i18n('当前模型版本')}{tts.model_version or '1.0'}", | |
| lines=5, | |
| ) | |
| if IS_V25: | |
| lang_dropdown = gr.Dropdown( | |
| choices=["ZH", "EN", "JA", "AR", "ES"], | |
| value="ZH", | |
| label=i18n("语言"), | |
| ) | |
| else: | |
| lang_dropdown = gr.State(value=None) | |
| duration_factor = gr.Slider( | |
| label=i18n("时长系数"), minimum=0.5, maximum=2.0, value=1.0, step=0.01, | |
| info=f'{i18n("快")} ← — {i18n("不变")} — → {i18n("慢")}', | |
| key="duration_factor", | |
| ) | |
| with gr.Column(scale=1): | |
| gen_button = gr.Button( | |
| i18n("生成语音"), key="gen_button", interactive=True | |
| ) | |
| output_audio = gr.Audio( | |
| label=i18n("生成结果"), visible=True, key="output_audio" | |
| ) | |
| with gr.Row(): | |
| experimental_checkbox = gr.Checkbox(label=i18n("显示实验功能"), value=False) | |
| _has_glossary = not IS_V25 and hasattr(tts, 'normalizer') | |
| glossary_checkbox = gr.Checkbox(label=i18n("开启术语词汇读音"), value=tts.normalizer.enable_glossary if _has_glossary else False, visible=_has_glossary) | |
| with gr.Accordion(i18n("功能设置")): | |
| # 情感控制选项部分 | |
| with gr.Row(): | |
| emo_control_method = gr.Radio( | |
| choices=EMO_CHOICES_OFFICIAL, | |
| type="index", | |
| value=EMO_CHOICES_OFFICIAL[0],label=i18n("情感控制方式")) | |
| # we MUST have an extra, INVISIBLE list of *all* emotion control | |
| # methods so that gr.Dataset() can fetch ALL control mode labels! | |
| # otherwise, the gr.Dataset()'s experimental labels would be empty! | |
| emo_control_method_all = gr.Radio( | |
| choices=EMO_CHOICES_ALL, | |
| type="index", | |
| value=EMO_CHOICES_ALL[0], label=i18n("情感控制方式"), | |
| visible=False) # do not render | |
| # 情感参考音频部分 | |
| with gr.Group(visible=False) as emotion_reference_group: | |
| with gr.Row(): | |
| emo_upload = gr.Audio(label=i18n("上传情感参考音频"), type="filepath") | |
| # 情感随机采样 | |
| with gr.Row(visible=False) as emotion_randomize_group: | |
| emo_random = gr.Checkbox(label=i18n("情感随机采样"), value=False) | |
| # 情感向量控制部分 | |
| with gr.Group(visible=False) as emotion_vector_group: | |
| with gr.Row(): | |
| with gr.Column(): | |
| vec1 = gr.Slider(label=i18n("喜"), minimum=0.0, maximum=1.0, value=0.0, step=0.05) | |
| vec2 = gr.Slider(label=i18n("怒"), minimum=0.0, maximum=1.0, value=0.0, step=0.05) | |
| vec3 = gr.Slider(label=i18n("哀"), minimum=0.0, maximum=1.0, value=0.0, step=0.05) | |
| vec4 = gr.Slider(label=i18n("惧"), minimum=0.0, maximum=1.0, value=0.0, step=0.05) | |
| with gr.Column(): | |
| vec5 = gr.Slider(label=i18n("厌恶"), minimum=0.0, maximum=1.0, value=0.0, step=0.05) | |
| vec6 = gr.Slider(label=i18n("低落"), minimum=0.0, maximum=1.0, value=0.0, step=0.05) | |
| vec7 = gr.Slider(label=i18n("惊喜"), minimum=0.0, maximum=1.0, value=0.0, step=0.05) | |
| vec8 = gr.Slider(label=i18n("平静"), minimum=0.0, maximum=1.0, value=0.0, step=0.05) | |
| with gr.Group(visible=False) as emo_text_group: | |
| create_experimental_warning_message() | |
| with gr.Row(): | |
| emo_text = gr.Textbox(label=i18n("情感描述文本"), | |
| placeholder=i18n("请输入情绪描述(或留空以自动使用目标文本作为情绪描述)"), | |
| value="", | |
| info=i18n("例如:委屈巴巴、危险在悄悄逼近")) | |
| with gr.Row(visible=False) as emo_weight_group: | |
| emo_weight = gr.Slider(label=i18n("情感权重"), minimum=0.0, maximum=1.0, value=0.65, step=0.01) | |
| # 术语词汇表管理 | |
| with gr.Accordion(i18n("自定义术语词汇读音"), open=False, visible=_has_glossary and tts.normalizer.enable_glossary) as glossary_accordion: | |
| gr.Markdown(i18n("自定义个别专业术语的读音")) | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| glossary_term = gr.Textbox( | |
| label=i18n("术语"), | |
| placeholder="IndexTTS2", | |
| ) | |
| glossary_reading_zh = gr.Textbox( | |
| label=i18n("中文读法"), | |
| placeholder="Index T-T-S 二", | |
| ) | |
| glossary_reading_en = gr.Textbox( | |
| label=i18n("英文读法"), | |
| placeholder="Index T-T-S two", | |
| ) | |
| btn_add_term = gr.Button(i18n("添加术语"), scale=1) | |
| with gr.Column(scale=2): | |
| glossary_table = gr.Markdown( | |
| value=format_glossary_markdown() | |
| ) | |
| with gr.Accordion(i18n("高级生成参数设置"), open=False, visible=True) as advanced_settings_group: | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| gr.Markdown(f"**{i18n('GPT2 采样设置')}** _{i18n('参数会影响音频多样性和生成速度详见')} [Generation strategies](https://huggingface.co/docs/transformers/main/en/generation_strategies)._") | |
| with gr.Row(): | |
| do_sample = gr.Checkbox(label="do_sample", value=True, info=i18n("是否进行采样")) | |
| temperature = gr.Slider(label="temperature", minimum=0.1, maximum=2.0, value=0.8, step=0.1) | |
| with gr.Row(): | |
| top_p = gr.Slider(label="top_p", minimum=0.0, maximum=1.0, value=0.8, step=0.01) | |
| top_k = gr.Slider(label="top_k", minimum=0, maximum=100, value=30, step=1) | |
| num_beams = gr.Slider(label="num_beams", value=3, minimum=1, maximum=10, step=1) | |
| with gr.Row(): | |
| repetition_penalty = gr.Number(label="repetition_penalty", precision=None, value=10.0, minimum=0.1, maximum=20.0, step=0.1) | |
| length_penalty = gr.Number(label="length_penalty", precision=None, value=0.0, minimum=-2.0, maximum=2.0, step=0.1) | |
| max_mel_tokens = gr.Slider(label="max_mel_tokens", value=1500, minimum=50, maximum=tts.cfg.gpt.max_mel_tokens, step=10, info=i18n("生成Token最大数量,过小导致音频被截断"), key="max_mel_tokens") | |
| # with gr.Row(): | |
| # typical_sampling = gr.Checkbox(label="typical_sampling", value=False, info="不建议使用") | |
| # typical_mass = gr.Slider(label="typical_mass", value=0.9, minimum=0.0, maximum=1.0, step=0.1) | |
| with gr.Column(scale=2): | |
| gr.Markdown(f'**{i18n("分句设置")}** _{i18n("参数会影响音频质量和生成速度")}_') | |
| with gr.Row(): | |
| initial_value = max(20, min(tts.cfg.gpt.max_text_tokens, cmd_args.gui_seg_tokens)) | |
| max_text_tokens_per_segment = gr.Slider( | |
| label=i18n("分句最大Token数"), value=initial_value, minimum=20, maximum=tts.cfg.gpt.max_text_tokens, step=2, key="max_text_tokens_per_segment", | |
| info=i18n("建议80~200之间,值越大,分句越长;值越小,分句越碎;过小过大都可能导致音频质量不高"), | |
| ) | |
| with gr.Accordion(i18n("预览分句结果"), open=True) as segments_settings: | |
| segments_preview = gr.Dataframe( | |
| headers=[i18n("序号"), i18n("分句内容"), i18n("Token数")], | |
| key="segments_preview", | |
| wrap=True, | |
| ) | |
| advanced_params = [ | |
| do_sample, top_p, top_k, temperature, | |
| length_penalty, num_beams, repetition_penalty, max_mel_tokens, | |
| # typical_sampling, typical_mass, | |
| ] | |
| # we must use `gr.Dataset` to support dynamic UI rewrites, since `gr.Examples` | |
| # binds tightly to UI and always restores the initial state of all components, | |
| # such as the list of available choices in emo_control_method. | |
| # these components are NOT "connected". it just reads the column labels/available | |
| # states from them, so we MUST link to the "all options" versions of all components, | |
| # such as `emo_control_method_all` (to be able to see EXPERIMENTAL text labels)! | |
| example_components = [prompt_audio, | |
| emo_control_method_all, # important: support all mode labels! | |
| input_text_single, | |
| emo_upload, | |
| emo_weight, | |
| emo_text, | |
| vec1, vec2, vec3, vec4, vec5, vec6, vec7, vec8] | |
| # v2.5: append the language column so ja/es/ar examples auto-select it | |
| if IS_V25: | |
| example_components.append(lang_dropdown) | |
| example_table = gr.Dataset(label="Examples", | |
| samples_per_page=20, | |
| samples=get_example_cases(include_experimental=False), | |
| type="values", | |
| components=example_components | |
| ) | |
| def on_example_click(example): | |
| print(f"Example clicked: ({len(example)} values) = {example!r}") | |
| updates = [ | |
| gr.update(value=example[0]), | |
| gr.update(value=example[1]), | |
| gr.update(value=example[2]), | |
| gr.update(value=example[3]), | |
| gr.update(value=example[4]), | |
| gr.update(value=example[5]), | |
| gr.update(value=example[6]), | |
| gr.update(value=example[7]), | |
| gr.update(value=example[8]), | |
| gr.update(value=example[9]), | |
| gr.update(value=example[10]), | |
| gr.update(value=example[11]), | |
| gr.update(value=example[12]), | |
| gr.update(value=example[13]), | |
| ] | |
| # v2.5: also restore the per-example language | |
| if IS_V25: | |
| updates.append(gr.update(value=example[14])) | |
| return updates | |
| # click() event works on both desktop and mobile UI | |
| example_outputs = [prompt_audio, | |
| emo_control_method, | |
| input_text_single, | |
| emo_upload, | |
| emo_weight, | |
| emo_text, | |
| vec1, vec2, vec3, vec4, vec5, vec6, vec7, vec8] | |
| if IS_V25: | |
| example_outputs.append(lang_dropdown) | |
| example_table.click(on_example_click, | |
| inputs=[example_table], | |
| outputs=example_outputs | |
| ) | |
| def on_input_text_change(text, max_text_tokens_per_segment): | |
| if text and len(text) > 0: | |
| if IS_V25: | |
| # v2.5 uses tiktoken encoder, no split_segments; show token count only | |
| tokens = tts.tokenizer.encode(text, allowed_special='all') | |
| data = [[0, text, len(tokens)]] | |
| else: | |
| text_tokens_list = tts.tokenizer.tokenize(text) | |
| segments = tts.tokenizer.split_segments(text_tokens_list, max_text_tokens_per_segment=int(max_text_tokens_per_segment)) | |
| data = [] | |
| for i, s in enumerate(segments): | |
| segment_str = ''.join(s) | |
| tokens_count = len(s) | |
| data.append([i, segment_str, tokens_count]) | |
| return { | |
| segments_preview: gr.update(value=data, visible=True, type="array"), | |
| } | |
| else: | |
| df = pd.DataFrame([], columns=[i18n("序号"), i18n("分句内容"), i18n("Token数")]) | |
| return { | |
| segments_preview: gr.update(value=df), | |
| } | |
| # 术语词汇表事件处理函数 | |
| def on_add_glossary_term(term, reading_zh, reading_en): | |
| """添加术语到词汇表并自动保存""" | |
| if IS_V25 or not hasattr(tts, 'normalizer'): | |
| return gr.update() | |
| term = term.rstrip() | |
| reading_zh = reading_zh.rstrip() | |
| reading_en = reading_en.rstrip() | |
| if not term: | |
| gr.Warning(i18n("请输入术语")) | |
| return gr.update() | |
| if not reading_zh and not reading_en: | |
| gr.Warning(i18n("请至少输入一种读法")) | |
| return gr.update() | |
| # 构建读法数据 | |
| if reading_zh and reading_en: | |
| reading = {"zh": reading_zh, "en": reading_en} | |
| elif reading_zh: | |
| reading = {"zh": reading_zh} | |
| elif reading_en: | |
| reading = {"en": reading_en} | |
| else: | |
| reading = reading_zh or reading_en | |
| # 添加到词汇表 | |
| tts.normalizer.term_glossary[term] = reading | |
| # 自动保存到文件 | |
| try: | |
| tts.normalizer.save_glossary_to_yaml(tts.glossary_path) | |
| gr.Info(i18n("词汇表已更新"), duration=1) | |
| except Exception as e: | |
| gr.Error(i18n("保存词汇表时出错")) | |
| print(f"Error details: {e}") | |
| return gr.update() | |
| # 更新Markdown表格 | |
| return gr.update(value=format_glossary_markdown()) | |
| def on_method_change(emo_control_method): | |
| if emo_control_method == 1: # emotion reference audio | |
| return (gr.update(visible=True), | |
| gr.update(visible=False), | |
| gr.update(visible=False), | |
| gr.update(visible=False), | |
| gr.update(visible=True) | |
| ) | |
| elif emo_control_method == 2: # emotion vectors | |
| return (gr.update(visible=False), | |
| gr.update(visible=True), | |
| gr.update(visible=True), | |
| gr.update(visible=False), | |
| gr.update(visible=True) | |
| ) | |
| elif emo_control_method == 3: # emotion text description | |
| return (gr.update(visible=False), | |
| gr.update(visible=True), | |
| gr.update(visible=False), | |
| gr.update(visible=True), | |
| gr.update(visible=True) | |
| ) | |
| else: # 0: same as speaker voice | |
| return (gr.update(visible=False), | |
| gr.update(visible=False), | |
| gr.update(visible=False), | |
| gr.update(visible=False), | |
| gr.update(visible=False) | |
| ) | |
| emo_control_method.change(on_method_change, | |
| inputs=[emo_control_method], | |
| outputs=[emotion_reference_group, | |
| emotion_randomize_group, | |
| emotion_vector_group, | |
| emo_text_group, | |
| emo_weight_group] | |
| ) | |
| def on_experimental_change(is_experimental, current_mode_index): | |
| # 切换情感控制选项 | |
| new_choices = EMO_CHOICES_EXPERIMENTAL if is_experimental else EMO_CHOICES_OFFICIAL | |
| # if their current mode selection doesn't exist in new choices, reset to 0. | |
| # we don't verify that OLD index means the same in NEW list, since we KNOW it does. | |
| new_index = current_mode_index if current_mode_index < len(new_choices) else 0 | |
| return ( | |
| gr.update(choices=new_choices, value=new_choices[new_index]), | |
| gr.update(samples=get_example_cases(include_experimental=is_experimental)), | |
| ) | |
| experimental_checkbox.change( | |
| on_experimental_change, | |
| inputs=[experimental_checkbox, emo_control_method], | |
| outputs=[emo_control_method, example_table] | |
| ) | |
| def on_glossary_checkbox_change(is_enabled): | |
| """控制术语词汇表的可见性""" | |
| if IS_V25 or not hasattr(tts, 'normalizer'): | |
| return gr.update(visible=False) | |
| tts.normalizer.enable_glossary = is_enabled | |
| return gr.update(visible=is_enabled) | |
| glossary_checkbox.change( | |
| on_glossary_checkbox_change, | |
| inputs=[glossary_checkbox], | |
| outputs=[glossary_accordion] | |
| ) | |
| input_text_single.change( | |
| on_input_text_change, | |
| inputs=[input_text_single, max_text_tokens_per_segment], | |
| outputs=[segments_preview] | |
| ) | |
| max_text_tokens_per_segment.change( | |
| on_input_text_change, | |
| inputs=[input_text_single, max_text_tokens_per_segment], | |
| outputs=[segments_preview] | |
| ) | |
| prompt_audio.upload(update_prompt_audio, | |
| inputs=[], | |
| outputs=[gen_button]) | |
| def on_demo_load(): | |
| """页面加载时重新加载glossary数据""" | |
| if IS_V25 or not hasattr(tts, 'normalizer'): | |
| return gr.update() | |
| try: | |
| tts.normalizer.load_glossary_from_yaml(tts.glossary_path) | |
| except Exception as e: | |
| gr.Error(i18n("加载词汇表时出错")) | |
| print(f"Failed to reload glossary on page load: {e}") | |
| return gr.update(value=format_glossary_markdown()) | |
| # 术语词汇表事件绑定 | |
| btn_add_term.click( | |
| on_add_glossary_term, | |
| inputs=[glossary_term, glossary_reading_zh, glossary_reading_en], | |
| outputs=[glossary_table] | |
| ) | |
| # 页面加载时重新加载glossary | |
| demo.load( | |
| on_demo_load, | |
| inputs=[], | |
| outputs=[glossary_table] | |
| ) | |
| gen_button.click(gen_single, | |
| inputs=[emo_control_method,prompt_audio, input_text_single, | |
| lang_dropdown, | |
| emo_upload, emo_weight, | |
| vec1, vec2, vec3, vec4, vec5, vec6, vec7, vec8, | |
| emo_text,emo_random, | |
| max_text_tokens_per_segment, | |
| duration_factor, | |
| *advanced_params, | |
| ], | |
| outputs=[output_audio]) | |
| if __name__ == "__main__": | |
| demo.queue(20) | |
| demo.launch(server_name=cmd_args.host, server_port=cmd_args.port) | |