"""Simple Gradio demo for the canonical TeraTTSv2 Hub model.""" from __future__ import annotations import os import logging import unicodedata from functools import lru_cache import numpy as np import gradio as gr from transformers import AutoModel MODEL_ID = os.getenv("TERATTS_MODEL_ID", "TeraSpace/TeraTTSv2") THREADS = int(os.getenv("TERATTS_THREADS", str(max(1, (os.cpu_count() or 1) // 2)))) LOGGER = logging.getLogger(__name__) @lru_cache(maxsize=1) def load_tts(diffusion_model: str, stress_mode: str) -> object: """Load one sampler/stress mode combination to limit CPU memory use.""" return AutoModel.from_pretrained( MODEL_ID, trust_remote_code=True, provider="CPUExecutionProvider", threads=THREADS, diffusion_model=diffusion_model, russian_stress=stress_mode != "off", ruaccent_mode="dictionary" if stress_mode == "dictionary" else "full", ) def synthesize( text: str, voice: str, diffusion_model: str, stress_mode: str, duration_scale: float, ) -> tuple[tuple[int, np.ndarray], str]: text = text.strip() if not text: raise gr.Error("Enter text to synthesize.") if len(text) > 4_000: raise gr.Error("Text is limited to 4,000 characters.") try: model = load_tts(diffusion_model, stress_mode) normalized_text = model.normalize_text(text) waveform = model.generate_speech( normalized_text, voice=voice, duration_scale=duration_scale, ) except Exception as error: LOGGER.exception("TeraTTS synthesis failed") raise gr.Error(f"Synthesis failed: {error}") from error # ONNX receives NFKD because that is how its vocabulary was trained. Show # users the identical text in normal composed Unicode so ``й``/``ё`` stay # legible in the browser instead of appearing as a letter plus a detached # combining mark. return (44_100, waveform), unicodedata.normalize("NFC", normalized_text) demo = gr.Interface( fn=synthesize, inputs=[ gr.Textbox( label="Text", value=( "Сразу после этого слово взял директор по И И Яндекса. " "Он согласился с Пашей, повторив его цитату слово в слово. " "Присутствующие в зале инженеры отметили, что спикер " "сгенерировал ответ на основе высокой вероятности успеха " "предыдущей фразы." ), lines=5, ), gr.Dropdown( [ ("★ Best: Russian female 1", "ru_f1"), ("★ Best: Russian male 5", "ru_m5"), ("Russian female 2", "ru_f2"), ("Russian male 1", "ru_m1"), ("English female 3", "eng_f3"), ("English female 4 — whisper", "eng_f4_whisper"), ("English female 5", "eng_f5"), ("English male 2 — whisper", "eng_m2_whisper"), ("English male 3", "eng_m3"), ("English male 4", "eng_m4"), ], value="ru_f1", label="Voice", ), gr.Dropdown( [("Distilled — fast", "distilled"), ("Teacher — adjustable CFG", "teacher")], value="distilled", label="Diffusion model", ), gr.Dropdown( [ ("Full — neural + dictionary", "full"), ("Dictionary only — no RUAccent neural models", "dictionary"), ("Off", "off"), ], value="full", label="Russian stress", ), gr.Slider( minimum=0.5, maximum=1.5, step=0.05, value=1.0, label="Duration scale", info="Higher values produce slower, longer speech.", ), ], outputs=[ gr.Audio(label="TeraTTSv2", type="numpy"), gr.Textbox(label="Normalized text", lines=5, interactive=False), ], title="TeraTTSv2", description=""" ### Important usage notes - **Use TeraTTS locally:** visit [TeraSpace/TeraTTSv2](https://huggingface.co/TeraSpace/TeraTTSv2) for installation, Python examples, ONNX options, and downloadable model files. - **Recommended speakers:** **Russian female 1** and **Russian male 5** are the preferred voice prompts and appear first in the selector. - **Russian stress is automatic:** text inside `` receives stress markers by default; any manual `+` marker is preserved. - **English voice speaking Russian:** lower **Duration scale** below `1` (start around `0.8`) and adjust by ear. Language tags are required: use `` or ``. Distilled is faster; Teacher uses the higher-fidelity sampler. Dictionary-only stress saves memory by skipping RUAccent neural models. Numbers are spelled out from their language tags. Unsupported characters are skipped and reported in the Space logs. """, ) if __name__ == "__main__": demo.queue(default_concurrency_limit=1).launch()