Spaces:
Running on Zero
Running on Zero
Download app.py from hugging-apps/votspeech: direct link, hf CLI and curl.
- Browser
- Download file 8.64 kB
-
https://huggingface.co/spaces/hugging-apps/votspeech/resolve/main/app.py
- Command line
-
hf download hf://spaces/hugging-apps/votspeech/app.py
-
curl -L -o app.py https://huggingface.co/spaces/hugging-apps/votspeech/resolve/main/app.py
8.64 kB
| """VoTSpeech — instruction-following voice-design TTS (48 kHz) on ZeroGPU. | |
| Given synthesis text and a natural-language voice description, VoTSpeech first | |
| designs a continuous voice latent (flow-based Voice DiT conditioned on the | |
| instruction), then generates speech in that voice with the dots.tts-soar | |
| continuous autoregressive backbone + 48 kHz vocoder. | |
| Inference mirrors the authors' reference script | |
| (`inference/scripts/run_batch_infer_voice_design.sh` → `dots_tts.batch_cli`): | |
| template `voice_design`, bf16, speaker_scale 1.5, language "zh", | |
| max_generate_length 500, checkpoint-default voice sampler settings, seed 42. | |
| """ | |
| import spaces # ZeroGPU: MUST be imported before torch / dots_tts. | |
| import os | |
| os.environ.setdefault("DOTS_TTS_DELAYED_DIT_BACKEND", "sdpa") | |
| os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True") | |
| import random | |
| import tempfile | |
| import warnings | |
| from pathlib import Path | |
| warnings.filterwarnings("ignore", category=FutureWarning) | |
| import gradio as gr | |
| import soundfile as sf | |
| from dots_tts.runtime import DotsTtsRuntime | |
| from dots_tts.utils.logging import configure_logging | |
| from dots_tts.utils.util import seed_everything | |
| configure_logging(level="INFO") # silence per-patch DEBUG progress spam | |
| MODEL_ID = "Ywbn16/VoTSpeech" | |
| DEFAULT_SEED = 42 | |
| MAX_TEXT_CHARS = 300 | |
| MAX_INSTRUCTION_CHARS = 500 | |
| LANGUAGES = ["zh", "auto_detect", "none"] | |
| runtime = DotsTtsRuntime.from_pretrained( | |
| MODEL_ID, | |
| precision="bfloat16", | |
| optimize=False, # eager sdpa — torch.compile does not persist across ZeroGPU calls | |
| max_generate_length=500, | |
| max_sequence_length=2048, | |
| ) | |
| # Examples taken from the authors' own demo page (ywbn.github.io/VoTSpeech): | |
| # the README example, InstructTTSEval-ZH (APS / DSD / RP) and MINT cases. | |
| EXAMPLES = [ | |
| [ | |
| "今天阳光很好,我们一起出去走走吧。", | |
| "一位年轻女性,声音温暖明亮,语速自然,带着轻松愉快的情绪。", | |
| ], | |
| [ | |
| "那座古老的石桥静静地横跨在河上,见证了无数个春去秋来和人世间的悲欢离合。", | |
| "声音要听起来有厚度、有质感,像一个专业的男播音员。", | |
| ], | |
| [ | |
| "刚刚长大了,一定会开飞船的。", | |
| "小公主满怀憧憬地描述她心中的理想国度。", | |
| ], | |
| [ | |
| "知道了知道了,我去做还不行吗。真是的,一天到晚就知道使唤我,就不能让我休息一下吗?我的手都酸了,腰也疼,根本干不动活儿。", | |
| "模仿一个被逼着做事的熊孩子,说话时唉声叹气,满腹牢骚。", | |
| ], | |
| [ | |
| "自从你爸爸去世以后,我把所有的心血都放在你们兄弟俩身上,艰难的经营着这家医院。我不允许任何人来破坏它。", | |
| "声音应展现出中老年女性的特质,吐字清晰,前半段语速平缓,随着情感变化逐渐加快;音量正常开始,语气由隐忍转为愤慨,最终达到激昂高亢的状态。", | |
| ], | |
| [ | |
| "有些道理,是需要用一辈子的时间去慢慢理解的。", | |
| "年龄:老年, 音色:沙哑", | |
| ], | |
| ] | |
| def _gpu_duration(text, *args, **kwargs) -> int: | |
| # Measured on ZeroGPU: ~0.19 s of audio per CJK char at RTF ~1.0 | |
| # (66 chars -> 12.3 s audio in 12.2 s). Output is capped at 500 patches (~80 s). | |
| return int(min(90, max(15, (5 + 0.2 * len(text)) * 1.15))) | |
| def _generate(text: str, instruction: str, seed: int, language: str, speaker_scale: float): | |
| seed_everything(seed) | |
| result = runtime.generate( | |
| text=text, | |
| instruction=instruction, | |
| template_name="voice_design", | |
| language=language, | |
| speaker_scale=speaker_scale, | |
| ) | |
| audio = result["audio"].detach().float().cpu().squeeze().numpy() | |
| out_path = Path(tempfile.mkdtemp()) / f"votspeech_{seed}.wav" | |
| sf.write(str(out_path), audio, int(result["sample_rate"]), subtype="PCM_16") | |
| duration = audio.shape[-1] / result["sample_rate"] | |
| status = ( | |
| f"Generated {duration:.1f} s of audio in {result['time_used']:.1f} s " | |
| f"(RTF {result['rtf']:.2f}) · seed {seed} · {result['sample_rate'] // 1000} kHz" | |
| ) | |
| return str(out_path), status | |
| def synthesize( | |
| text: str, | |
| instruction: str, | |
| seed: float = DEFAULT_SEED, | |
| randomize_seed: bool = False, | |
| language: str = "zh", | |
| speaker_scale: float = 1.5, | |
| ): | |
| """Speak `text` in a voice designed from a natural-language description (48 kHz). | |
| Args: | |
| text: The words to speak (VoTSpeech is trained for Mandarin Chinese). | |
| instruction: Natural-language description of the desired voice | |
| (gender, age, timbre, emotion, speaking style, persona / scenario...). | |
| seed: Generation seed — different seeds design different voices. | |
| randomize_seed: Ignore `seed` and pick a random one. | |
| language: Language tag added to the text: "zh", "auto_detect" or "none". | |
| speaker_scale: Strength of the designed-voice conditioning (default 1.5). | |
| Returns: | |
| (path to the generated 48 kHz wav, status line with timing and seed) | |
| """ | |
| text = (text or "").strip() | |
| instruction = (instruction or "").strip() | |
| if not text: | |
| raise gr.Error("Please enter the text to speak.") | |
| if not instruction: | |
| raise gr.Error("Please enter a voice description (instruction).") | |
| if len(text) > MAX_TEXT_CHARS: | |
| raise gr.Error(f"Text is too long; use at most {MAX_TEXT_CHARS} characters.") | |
| if len(instruction) > MAX_INSTRUCTION_CHARS: | |
| raise gr.Error( | |
| f"Instruction is too long; use at most {MAX_INSTRUCTION_CHARS} characters." | |
| ) | |
| if randomize_seed: | |
| seed = random.randint(0, 2**31 - 1) | |
| seed = int(seed) | |
| try: | |
| return _generate(text, instruction, seed, language or "zh", float(speaker_scale)) | |
| except gr.Error: | |
| raise | |
| except Exception as exc: | |
| raise gr.Error(f"Generation failed: {exc}") from exc | |
| CSS = """ | |
| #col-container { max-width: 1100px; margin: 0 auto; } | |
| .dark .gradio-container { color: var(--body-text-color); } | |
| """ | |
| with gr.Blocks(title="VoTSpeech") as demo: | |
| with gr.Column(elem_id="col-container"): | |
| gr.Markdown( | |
| "# 🗣️ VoTSpeech\n" | |
| "Instruction-following TTS that **designs a voice from a text description**, " | |
| "then speaks your text in it at 48 kHz. Trained for **Mandarin Chinese** — " | |
| "describe the voice (gender, age, timbre, emotion, role…) in Chinese.\n\n" | |
| "[Model](https://huggingface.co/Ywbn16/VoTSpeech) · " | |
| "[Code](https://github.com/ywbn/VoTSpeech) · " | |
| "[Project page](https://ywbn.github.io/VoTSpeech/) · " | |
| "Weights: CC BY-NC 4.0 (non-commercial). Outputs are synthetic speech." | |
| ) | |
| with gr.Row(): | |
| with gr.Column(): | |
| text = gr.Textbox( | |
| label="Text to speak", | |
| lines=4, | |
| placeholder="今天阳光很好,我们一起出去走走吧。", | |
| ) | |
| instruction = gr.Textbox( | |
| label="Voice description (instruction)", | |
| lines=3, | |
| placeholder="一位年轻女性,声音温暖明亮,语速自然,带着轻松愉快的情绪。", | |
| ) | |
| generate_btn = gr.Button("Generate", variant="primary") | |
| with gr.Accordion("Advanced settings", open=False): | |
| seed = gr.Number(value=DEFAULT_SEED, precision=0, label="Seed") | |
| randomize_seed = gr.Checkbox(value=False, label="Randomize seed") | |
| language = gr.Dropdown( | |
| choices=LANGUAGES, value="zh", label="Language tag" | |
| ) | |
| speaker_scale = gr.Slider( | |
| 0.5, 3.0, value=1.5, step=0.1, label="Speaker (voice) scale" | |
| ) | |
| with gr.Column(): | |
| audio_out = gr.Audio(label="Generated speech (48 kHz)", type="filepath") | |
| status = gr.Markdown() | |
| gr.Examples( | |
| examples=EXAMPLES, | |
| inputs=[text, instruction], | |
| outputs=[audio_out, status], | |
| fn=synthesize, | |
| cache_examples=True, | |
| cache_mode="lazy", | |
| ) | |
| generate_btn.click( | |
| fn=synthesize, | |
| inputs=[text, instruction, seed, randomize_seed, language, speaker_scale], | |
| outputs=[audio_out, status], | |
| ) | |
| demo.queue(max_size=16) | |
| demo.launch(mcp_server=True, theme=gr.themes.Citrus(), css=CSS) | |