firered-tts3 / app.py
multimodalart's picture
multimodalart HF Staff
Use more reliable semantic-edit examples
0db1e4f verified
Raw
History Blame Contribute Delete
24.2 kB
"""FireRedTTS3 — unified speech generation & editing demo (ZeroGPU).
Three capabilities of https://huggingface.co/FireRedTeam/FireRedTTS3 :
* Zero-shot voice cloning (FireRedTTS3-Base, 24 languages + 21 ZH dialects)
* Voice design (FireRedTTS3-Instruct, natural-language timbre prompt)
* Speech editing (FireRedTTS3-Instruct, semantic + acoustic)
"""
import functools
import os
import re
import urllib.request
import spaces # noqa: F401 (must precede torch / CUDA imports)
import numpy as np
import soundfile as sf
import torch
import gradio as gr
from huggingface_hub import snapshot_download
HERE = os.path.dirname(os.path.abspath(__file__))
# --------------------------------------------------------------------------- #
# Text front-end assets
# --------------------------------------------------------------------------- #
# fastText lid.176 powers automatic language routing (see upstream README).
_LID_URL = "https://dl.fbaipublicfiles.com/fasttext/supervised-models/lid.176.ftz"
_LID_PATH = os.path.join(HERE, "fireredtts3", "utils", "llm_tn", "models", "lid.176.ftz")
os.makedirs(os.path.dirname(_LID_PATH), exist_ok=True)
if not os.path.exists(_LID_PATH):
try:
urllib.request.urlretrieve(_LID_URL, _LID_PATH)
print(f"[INFO] fastText lid.176 downloaded to {_LID_PATH}", flush=True)
except Exception as exc: # pragma: no cover
print(f"[WARN] Could not fetch fastText lid.176: {exc}", flush=True)
# The upstream llm_tn TextNormalizer refuses to construct without API creds, and
# the fastText language detector lives on that object. We only use it for
# language *identification* (use_llm_tn=False -> local wetext TN), so give it
# placeholder creds and disable its LLM fallback further below.
os.environ.setdefault("LLM_TN_API_URL", "http://127.0.0.1:1/unused")
os.environ.setdefault("LLM_TN_API_KEY", "unused")
# --------------------------------------------------------------------------- #
# Weights
# --------------------------------------------------------------------------- #
MODEL_REPO = "FireRedTeam/FireRedTTS3"
MODEL_DIR = snapshot_download(MODEL_REPO)
print(f"[INFO] weights at {MODEL_DIR}", flush=True)
from fireredtts3.core import FireRedTTS3, FireRedTTS3Instruct # noqa: E402
from fireredtts3.redae.redae import RedAE # noqa: E402
from fireredtts3.utils.llm_tn.text_normalizer import TextNormalizer # noqa: E402
from fireredtts3.utils.text_tokenizer import ( # noqa: E402
MULTI_DIALECT_TAGS,
MULTI_LANG_TAGS,
)
# Base and Instruct each instantiate their own RedAE from the very same
# checkpoint; share one instance instead (~3.8 GB saved, identical numerics).
_redae_real_from_pretrained = RedAE.from_pretrained
_redae_singleton = None
def _shared_redae(*args, **kwargs):
global _redae_singleton
if _redae_singleton is None:
_redae_singleton = _redae_real_from_pretrained(*args, **kwargs)
return _redae_singleton
RedAE.from_pretrained = _shared_redae
tts = FireRedTTS3(MODEL_DIR, use_fasttext=True, use_llm_tn=False, use_wetext=True)
instruct = FireRedTTS3Instruct(MODEL_DIR, use_fasttext=True, use_llm_tn=False, use_wetext=True)
for _pipe in (tts, instruct):
_norm = getattr(_pipe, "_llm_tn", None)
if _norm is not None:
# No API creds here -> never let language ID fall back to an LLM call.
_norm.detect_locale = functools.partial(
TextNormalizer.detect_locale, _norm, use_llm_fallback=False
)
print("[INFO] FireRedTTS3 Base + Instruct ready", flush=True)
SAMPLE_RATE = tts.redae.sample_rate
# --------------------------------------------------------------------------- #
# Language choices
# --------------------------------------------------------------------------- #
AUTO = "Auto-detect"
LANGUAGES = [t.strip("<|>") for t in MULTI_LANG_TAGS]
DIALECTS = [t.strip("<|>") for t in MULTI_DIALECT_TAGS]
LANG_CHOICES = (
[AUTO]
+ LANGUAGES
+ [f"{d} (Chinese dialect)" for d in DIALECTS]
)
def _resolve_language(choice: str):
if not choice or choice == AUTO:
return None
return choice.split(" (")[0]
# --------------------------------------------------------------------------- #
# Audio helpers
# --------------------------------------------------------------------------- #
MAX_PROMPT_SECONDS = 20.0
MAX_EDIT_SECONDS = 20.0
MAX_TEXT_CHARS = 400
def _load_audio(path: str, max_seconds: float):
if not path:
raise gr.Error("Please provide an audio file first.")
wav, sr = sf.read(path, always_2d=True, dtype="float32")
wav = wav[:, 0]
if wav.shape[0] > int(max_seconds * sr):
wav = wav[: int(max_seconds * sr)]
gr.Info(f"Audio truncated to the first {max_seconds:.0f}s.")
peak = float(np.abs(wav).max()) if wav.size else 0.0
if peak > 0:
wav = wav / peak * 0.95
return torch.from_numpy(np.ascontiguousarray(wav)[None, :]), sr
def _to_gradio_audio(audio: torch.Tensor, sr: int):
x = audio.detach().float().cpu().numpy()
if x.ndim > 1:
x = x[0]
x = np.clip(x, -1.0, 1.0)
return sr, (x * 32767.0).astype(np.int16)
_EDIT_MASK_RE = re.compile(r"<\|edit\|>(?:<\|frame_patch\|>)*<\|end_edit\|>")
def _pretty_edit_text(text: str) -> str:
"""The model marks the re-synthesized span with edit/frame-patch tokens."""
text = _EDIT_MASK_RE.sub(" ⟨edited span⟩ ", text or "")
text = re.sub(r"<\|[^|]*\|>", "", text)
return re.sub(r"\s+", " ", text).strip()
def _check_text(text: str, what: str = "Text"):
text = (text or "").strip()
if not text:
raise gr.Error(f"{what} must not be empty.")
if len(text) > MAX_TEXT_CHARS:
gr.Info(f"{what} truncated to {MAX_TEXT_CHARS} characters.")
text = text[:MAX_TEXT_CHARS]
return text
# --------------------------------------------------------------------------- #
# Inference
# --------------------------------------------------------------------------- #
@spaces.GPU(duration=60)
def voice_clone(
prompt_audio,
prompt_text,
text,
language=AUTO,
inference_cfg=2.0,
n_timesteps=10,
seed=1234,
do_tn=True,
):
"""FireRedTTS3-Base zero-shot voice cloning."""
text = _check_text(text, "Text to synthesize")
prompt_text = (prompt_text or "").strip()
if not prompt_text:
raise gr.Error("Please provide the transcript of the reference audio.")
wav, sr = _load_audio(prompt_audio, MAX_PROMPT_SECONDS)
gen_audio, gen_sr = tts.generate(
text=text,
language=_resolve_language(language),
prompt_text=prompt_text,
prompt_audio=wav,
prompt_audio_sr=sr,
n_timesteps=int(n_timesteps),
inference_cfg=float(inference_cfg),
seed=int(seed),
do_tn=bool(do_tn),
)
return _to_gradio_audio(gen_audio, gen_sr)
@spaces.GPU(duration=60)
def voice_design(
instruction,
text,
inference_cfg=1.2,
n_timesteps=10,
seed=2,
do_tn=True,
):
"""FireRedTTS3-Instruct voice design (no reference audio)."""
instruction = _check_text(instruction, "Voice description")
text = _check_text(text, "Text to synthesize")
gen_audio, gen_sr, gen_text = instruct.generate_voice_design(
instruction=instruction,
text=text,
n_timesteps=int(n_timesteps),
inference_cfg=float(inference_cfg),
seed=int(seed),
do_tn=bool(do_tn),
)
return _to_gradio_audio(gen_audio, gen_sr), (gen_text or "").strip()
@spaces.GPU(duration=60)
def semantic_edit(
audio_in,
instruction,
inference_cfg=1.2,
n_timesteps=10,
seed=1234,
):
"""FireRedTTS3-Instruct content editing: insert / delete / substitute."""
instruction = _check_text(instruction, "Edit instruction")
wav, sr = _load_audio(audio_in, MAX_EDIT_SECONDS)
gen_audio, gen_sr, gen_text = instruct.generate_semantic_edit(
instruction=instruction,
audio_in=wav,
audio_in_sr=sr,
n_timesteps=int(n_timesteps),
inference_cfg=float(inference_cfg),
seed=int(seed),
)
return _to_gradio_audio(gen_audio, gen_sr), _pretty_edit_text(gen_text)
def compose_acoustic_instruction(attribute: str, value: float) -> str:
"""Acoustic edits only accept the templates the model was trained on."""
if attribute == "Speed":
return f"adjust the speed to {value:.1f}x"
if attribute == "Volume":
return f"adjust the volume to {value:.1f}"
steps = int(round(value))
return f"shift the pitch by {steps} step{'' if abs(steps) == 1 else 's'}"
@spaces.GPU(duration=60)
def acoustic_edit(
audio_in,
attribute="Speed",
value=0.8,
inference_cfg=1.2,
n_timesteps=10,
seed=1234,
):
"""FireRedTTS3-Instruct acoustic editing: speed / pitch / volume."""
wav, sr = _load_audio(audio_in, MAX_EDIT_SECONDS)
instruction = compose_acoustic_instruction(attribute, float(value))
gen_audio, gen_sr = instruct.generate_acoustic_edit(
instruction=instruction,
audio_in=wav,
audio_in_sr=sr,
n_timesteps=int(n_timesteps),
inference_cfg=float(inference_cfg),
seed=int(seed),
)
return _to_gradio_audio(gen_audio, gen_sr), instruction
# --------------------------------------------------------------------------- #
# UI
# --------------------------------------------------------------------------- #
EN_PROMPT = os.path.join(HERE, "examples", "en_prompt.wav")
ZH_PROMPT = os.path.join(HERE, "examples", "zh_prompt.wav")
EN_PROMPT_TEXT = (
"Just by listening a few minutes a day, you'll be able to eliminate negative "
"thoughts by conditioning your mind to be more positive."
)
ZH_PROMPT_TEXT = "比如具体一点的,他觉得最大的一个跟他预想的不一样的是在什么地方。"
CSS = """
.gradio-container {max-width: 1200px !important; margin: auto !important;}
.dark .gradio-container {color: var(--body-text-color);}
"""
with gr.Blocks(title="FireRedTTS3") as demo:
gr.Markdown(
"""
# 🔥 FireRedTTS3 — Unified Speech Generation & Editing
Zero-shot voice cloning in **24 languages + 21 Chinese dialects**, natural-language
**voice design**, and instruction-driven **speech editing** — all from
[FireRedTeam/FireRedTTS3](https://huggingface.co/FireRedTeam/FireRedTTS3).
"""
)
with gr.Tabs():
# ------------------------------------------------------------------ #
with gr.Tab("🎙️ Voice Cloning"):
gr.Markdown(
"Clone any voice from a short reference clip. For best quality the "
"reference should be spoken in the **same language / dialect** as the "
"text you synthesize."
)
with gr.Row():
with gr.Column():
clone_prompt_audio = gr.Audio(
label="Reference audio (5–20 s)",
sources=["upload", "microphone"],
type="filepath",
)
clone_prompt_text = gr.Textbox(
label="Reference transcript",
placeholder="Exactly what is said in the reference audio…",
lines=2,
)
clone_text = gr.Textbox(
label="Text to synthesize",
placeholder="Type the text you want spoken in that voice…",
lines=4,
)
clone_language = gr.Dropdown(
LANG_CHOICES, value=AUTO, label="Language / dialect"
)
clone_btn = gr.Button("Generate speech", variant="primary")
with gr.Column():
clone_out = gr.Audio(label="Generated speech", type="numpy")
with gr.Accordion("Advanced options", open=False):
clone_cfg = gr.Slider(
0.0, 4.0, value=2.0, step=0.1,
label="CFG strength",
info="Higher sticks closer to the reference timbre.",
)
clone_steps = gr.Slider(
4, 30, value=10, step=1, label="Flow-matching timesteps"
)
clone_seed = gr.Number(value=1234, precision=0, label="Seed")
clone_tn = gr.Checkbox(
value=True,
label="Text normalization (numbers, dates, units → words)",
)
gr.Examples(
examples=[
[
EN_PROMPT,
EN_PROMPT_TEXT,
"FireRedTTS3 turns a handful of seconds of speech into a voice "
"that can read anything you write.",
"English",
],
[
ZH_PROMPT,
ZH_PROMPT_TEXT,
"法院与不动产登记部门加强沟通,并督促银行提前办理抵押预约登记。",
"Chinese",
],
[
EN_PROMPT,
EN_PROMPT_TEXT,
"Le modèle peut aussi parler français avec la même voix de référence.",
"French",
],
],
inputs=[clone_prompt_audio, clone_prompt_text, clone_text, clone_language],
outputs=[clone_out],
fn=voice_clone,
cache_examples=True,
cache_mode="lazy",
)
# ------------------------------------------------------------------ #
with gr.Tab("🎨 Voice Design"):
gr.Markdown(
"Describe a voice in plain language — no reference audio needed. The "
"model first writes a voice-attribute plan, then renders the audio."
)
with gr.Row():
with gr.Column():
design_instruction = gr.Textbox(
label="Voice description",
placeholder="e.g. A young woman with a gentle voice, speaking slowly…",
lines=3,
)
design_text = gr.Textbox(
label="Text to synthesize", lines=4,
placeholder="Type the text you want spoken…",
)
design_btn = gr.Button("Design voice", variant="primary")
with gr.Column():
design_out = gr.Audio(label="Generated speech", type="numpy")
design_plan = gr.Textbox(
label="Voice-attribute plan (model chain-of-thought)", lines=4
)
with gr.Accordion("Advanced options", open=False):
design_cfg = gr.Slider(
0.0, 4.0, value=1.2, step=0.1, label="CFG strength"
)
design_steps = gr.Slider(
4, 30, value=10, step=1, label="Flow-matching timesteps"
)
design_seed = gr.Number(value=2, precision=0, label="Seed")
design_tn = gr.Checkbox(value=True, label="Text normalization")
gr.Examples(
examples=[
[
"一个年轻女性的温柔嗓音,语速稍慢,带一点俏皮。",
"今天天气很好,我们一起去公园散步吧。",
],
[
"An old sailor with a deep, gravelly voice, speaking slowly and "
"warmly, as if telling a story by the fire.",
"The sea was calm that morning, and every rope on deck was "
"still wet with salt.",
],
[
"A bright, energetic young man hosting a sports broadcast, fast "
"paced and excited.",
"And with ten seconds left on the clock, he takes the shot — "
"and it is in!",
],
],
inputs=[design_instruction, design_text],
outputs=[design_out, design_plan],
fn=voice_design,
cache_examples=True,
cache_mode="lazy",
)
# ------------------------------------------------------------------ #
with gr.Tab("✂️ Speech Editing"):
with gr.Tabs():
with gr.Tab("Semantic (content)"):
gr.Markdown(
"Insert, delete or substitute words in an existing recording "
"while keeping the original voice. The model transcribes the "
"audio itself — just say what to change."
)
with gr.Row():
with gr.Column():
sem_audio = gr.Audio(
label="Input speech (≤ 20 s)",
sources=["upload", "microphone"],
type="filepath",
)
sem_instruction = gr.Textbox(
label="Edit instruction",
placeholder="e.g. Replace 'positive' with 'optimistic'.",
lines=2,
)
sem_btn = gr.Button("Apply edit", variant="primary")
with gr.Column():
sem_out = gr.Audio(label="Edited speech", type="numpy")
sem_text = gr.Textbox(label="Edited transcript", lines=3)
with gr.Accordion("Advanced options", open=False):
sem_cfg = gr.Slider(
0.0, 4.0, value=1.2, step=0.1, label="CFG strength"
)
sem_steps = gr.Slider(
4, 30, value=10, step=1,
label="Flow-matching timesteps",
)
sem_seed = gr.Number(
value=1234, precision=0, label="Seed"
)
gr.Examples(
examples=[
[EN_PROMPT, "Replace 'positive' with 'optimistic'."],
[EN_PROMPT, "Delete the word 'negative'."],
[ZH_PROMPT, "把“最大的”替换成“最有意思的”。"],
],
inputs=[sem_audio, sem_instruction],
outputs=[sem_out, sem_text],
fn=semantic_edit,
cache_examples=True,
cache_mode="lazy",
)
with gr.Tab("Acoustic (speed / pitch / volume)"):
gr.Markdown(
"Re-render the same utterance with a different speaking rate, "
"pitch or loudness. These edits follow fixed instruction "
"templates the model was trained on."
)
with gr.Row():
with gr.Column():
aco_audio = gr.Audio(
label="Input speech (≤ 20 s)",
sources=["upload", "microphone"],
type="filepath",
)
aco_attr = gr.Radio(
["Speed", "Pitch", "Volume"],
value="Speed",
label="Attribute",
)
aco_value = gr.Slider(
0.5, 2.0, value=0.8, step=0.1,
label="Speed (×)",
)
aco_btn = gr.Button("Apply edit", variant="primary")
with gr.Column():
aco_out = gr.Audio(label="Edited speech", type="numpy")
aco_instruction = gr.Textbox(
label="Instruction sent to the model", lines=1
)
with gr.Accordion("Advanced options", open=False):
aco_cfg = gr.Slider(
0.0, 4.0, value=1.2, step=0.1, label="CFG strength"
)
aco_steps = gr.Slider(
4, 30, value=10, step=1,
label="Flow-matching timesteps",
)
aco_seed = gr.Number(
value=1234, precision=0, label="Seed"
)
gr.Examples(
examples=[
[EN_PROMPT, "Speed", 0.7],
[ZH_PROMPT, "Pitch", 2],
[EN_PROMPT, "Volume", 1.6],
],
inputs=[aco_audio, aco_attr, aco_value],
outputs=[aco_out, aco_instruction],
fn=acoustic_edit,
cache_examples=True,
cache_mode="lazy",
)
gr.Markdown(
"""
---
**Model:** [FireRedTeam/FireRedTTS3](https://huggingface.co/FireRedTeam/FireRedTTS3)
(Apache-2.0) · Base = cloning, Instruct = design + editing. Text normalization
runs locally through *wetext* (Chinese / English); other languages get basic
cleaning only. Voice cloning is provided **for academic research purposes only** —
do not use it for impersonation or any illegal activity.
"""
)
def _attr_changed(attribute, current):
lo, hi, step, label = {
"Speed": (0.5, 2.0, 0.1, "Speed (×)"),
"Volume": (0.3, 2.0, 0.1, "Volume (×)"),
}.get(attribute, (-6, 6, 1, "Pitch shift (semitone steps)"))
try:
value = min(max(float(current), lo), hi)
except (TypeError, ValueError):
value = lo
if attribute == "Pitch":
value = int(round(value)) or 1
return gr.update(minimum=lo, maximum=hi, step=step, value=value, label=label)
aco_attr.change(_attr_changed, inputs=[aco_attr, aco_value], outputs=[aco_value])
clone_btn.click(
voice_clone,
inputs=[clone_prompt_audio, clone_prompt_text, clone_text, clone_language,
clone_cfg, clone_steps, clone_seed, clone_tn],
outputs=[clone_out],
api_name="voice_clone",
)
design_btn.click(
voice_design,
inputs=[design_instruction, design_text, design_cfg, design_steps,
design_seed, design_tn],
outputs=[design_out, design_plan],
api_name="voice_design",
)
sem_btn.click(
semantic_edit,
inputs=[sem_audio, sem_instruction, sem_cfg, sem_steps, sem_seed],
outputs=[sem_out, sem_text],
api_name="semantic_edit",
)
aco_btn.click(
acoustic_edit,
inputs=[aco_audio, aco_attr, aco_value, aco_cfg, aco_steps, aco_seed],
outputs=[aco_out, aco_instruction],
api_name="acoustic_edit",
)
if __name__ == "__main__":
demo.queue().launch(theme=gr.themes.Citrus(), css=CSS, mcp_server=True)