votspeech / app.py
multimodalart's picture
multimodalart HF Staff
Size GPU duration to text length; quieter logs
9e5f497 verified
Raw History Blame Contribute Delete
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)))
@spaces.GPU(duration=_gpu_duration)
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)