Saudi-TTS-V2 / app.py
FatimahEmadEldin's picture
Update app.py
1674414 verified
Raw
History Blame Contribute Delete
9.83 kB
"""
ZeroGPU (free tier) only attaches a GPU during execution of @spaces.GPU
functions. We:
1. Install F5-TTS + Habibi-TTS at startup with --no-deps
2. Load the model on CPU at startup (ZeroGPU has none during import)
3. Move to CUDA + run inference inside a @spaces.GPU-decorated function
"""
import os
import sys
import subprocess
import tempfile
import traceback
from pathlib import Path
# ======================================================================
# 0. Install F5-TTS + Habibi-TTS at startup
# ======================================================================
def _ensure_tts_packages():
try:
import f5_tts # noqa: F401
import habibi_tts # noqa: F401
print("โœ… f5_tts and habibi_tts already installed")
return
except ImportError:
pass
print("๐Ÿ“ฆ Installing F5-TTS and Habibi-TTS with --no-deps...")
for pkg_url in [
"git+https://github.com/SWivid/F5-TTS.git",
"git+https://github.com/SWivid/Habibi-TTS.git",
]:
print(f" โ†’ {pkg_url}")
result = subprocess.run(
[sys.executable, "-m", "pip", "install",
"--no-cache-dir", "--no-deps", "-q", pkg_url],
capture_output=True, text=True,
)
if result.returncode != 0:
print(f" โŒ install failed:\n{result.stderr}")
raise RuntimeError(f"pip install failed for {pkg_url}")
print(f" โœ… installed")
_ensure_tts_packages()
# ======================================================================
# 1. Imports
# ======================================================================
import spaces # โ† ZeroGPU decorator
import gradio as gr
import torch
import soundfile as sf
from huggingface_hub import hf_hub_download
from f5_tts.model import DiT
from f5_tts.infer.utils_infer import (
load_model, load_vocoder, preprocess_ref_audio_text,
)
from habibi_tts.infer.utils_infer import infer_process
# ======================================================================
# 2. CONFIG
# ======================================================================
REPO_ID = "NAMAA-Space/NAMAA-Saudi-TTS-V2"
CKPT_FILE = "model_2000.safetensors"
VOCAB = "vocab.txt"
# ======================================================================
# 3. Load model on CPU at startup (no GPU available yet on ZeroGPU)
# ======================================================================
V1_BASE_CFG = dict(dim=1024, depth=22, heads=16,
ff_mult=2, text_dim=512, conv_layers=4)
print("๐Ÿ“ฅ Downloading model weights + vocab from HF Hub...")
CKPT_PATH = hf_hub_download(repo_id=REPO_ID, filename=CKPT_FILE)
VOCAB_PATH = hf_hub_download(repo_id=REPO_ID, filename=VOCAB)
print("๐Ÿ”ง Loading model on CPU (will move to GPU per-request on ZeroGPU)...")
# Load on CPU. Inside generate(), we move to cuda when GPU is attached.
MODEL = load_model(DiT, V1_BASE_CFG, CKPT_PATH,
vocab_file=VOCAB_PATH, device="cpu")
MODEL = MODEL.to(torch.float32).eval()
VOCODER = load_vocoder()
VOCODER = VOCODER.to("cpu")
print("โœ… Model + vocoder ready on CPU")
# ======================================================================
# 4. Inference function (GPU-decorated for ZeroGPU)
# ======================================================================
@spaces.GPU(duration=60) # request up to 60s of GPU per call
def generate(ref_audio, ref_text, gen_text, nfe_step, speed, remove_silence):
if not ref_audio:
raise gr.Error("Please upload a reference audio clip.")
if not ref_text or not ref_text.strip():
raise gr.Error("Please provide the reference transcript.")
if not gen_text or not gen_text.strip():
raise gr.Error("Please provide text to generate.")
try:
info = sf.info(ref_audio)
dur = info.frames / info.samplerate
if dur < 2.0:
gr.Warning(f"Reference is only {dur:.1f}s โ€” aim for 5-8s.")
elif dur > 15.0:
gr.Warning(f"Reference is {dur:.1f}s โ€” will be truncated to 15s.")
except Exception:
pass
try:
# GPU is now available inside this decorated function.
# Move model + vocoder onto cuda for this call.
global MODEL, VOCODER
device = "cuda" if torch.cuda.is_available() else "cpu"
MODEL_GPU = MODEL.to(device)
VOCODER_GPU = VOCODER.to(device)
ref_audio_p, ref_text_p = preprocess_ref_audio_text(
ref_audio, ref_text.strip()
)
wave, sr, _ = infer_process(
ref_audio_p, ref_text_p, gen_text.strip(),
MODEL_GPU, VOCODER_GPU,
nfe_step=int(nfe_step), speed=float(speed),
)
# Release the GPU copy (keep CPU copies in MODEL / VOCODER)
if device == "cuda":
del MODEL_GPU, VOCODER_GPU
torch.cuda.empty_cache()
if remove_silence:
try:
from f5_tts.infer.utils_infer import remove_silence_for_generated_wav
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp:
sf.write(tmp.name, wave, sr)
remove_silence_for_generated_wav(tmp.name)
wave, sr = sf.read(tmp.name)
os.unlink(tmp.name)
except Exception as e:
gr.Warning(f"Silence removal skipped: {e}")
duration = len(wave) / sr
status = f"โœ… Generated {duration:.1f}s audio at {sr}Hz on {device}"
return (sr, wave), status
except Exception as e:
print(traceback.format_exc())
raise gr.Error(f"Generation failed: {type(e).__name__}: {e}")
# ======================================================================
# 5. Examples
# ======================================================================
EXAMPLES = [
["examples/najdi_reference.wav",
"ุชูƒูู‰ ุทู…ู†ูŠ ุงู†ุง ุงู„ูŠูˆู… ู…ุงู†ูŠ ุจู†ุงูŠู…",
"ู…ุฑุญุจุงุŒ ูƒูŠู ุญุงู„ูƒ ุงู„ูŠูˆู…ุŸ", 32, 1.0, False],
["examples/najdi_reference.wav",
"ุชูƒูู‰ ุทู…ู†ูŠ ุงู†ุง ุงู„ูŠูˆู… ู…ุงู†ูŠ ุจู†ุงูŠู…",
"ุฃู‡ู„ุง ูˆุณู‡ู„ุง ุจูƒ ููŠ ุงู„ู…ู…ู„ูƒุฉ ุงู„ุนุฑุจูŠุฉ ุงู„ุณุนูˆุฏูŠุฉ", 32, 1.0, False],
["examples/najdi_reference.wav",
"ุชูƒูู‰ ุทู…ู†ูŠ ุงู†ุง ุงู„ูŠูˆู… ู…ุงู†ูŠ ุจู†ุงูŠู…",
"ุงู„ุฑูŠุงุถ ุนุงุตู…ุฉ ุงู„ู…ู…ู„ูƒุฉ ุงู„ุนุฑุจูŠุฉ ุงู„ุณุนูˆุฏูŠุฉ", 32, 1.0, False],
]
# ======================================================================
# 6. UI (Gradio 6.x compatible)
# ======================================================================
DESCRIPTION = """
# ๐Ÿ‡ธ๐Ÿ‡ฆ Saudi TTS V2 โ€” Voice Cloning TTS
Fine-tuned from [SWivid/Habibi-TTS](https://huggingface.co/SWivid/Habibi-TTS)
on ~18 hours of Najdi/Saudi Arabic audio. **Running on free ZeroGPU** โ€”
first request per session takes ~30s to warm up.
**How to use:**
1. Upload a clean **5-8 second** reference clip (any Arabic voice)
2. Type the **exact transcript** of that clip
3. Type new Arabic text you want spoken in that voice
4. Click Generate
"""
ARTICLE = f"""
---
**Model:** [{REPO_ID}](https://huggingface.co/{REPO_ID}) โ€ข
**License:** CC-BY-NC-SA-4.0 โ€ข
**Architecture:** F5-TTS DiT (335M) + Vocos
Do not clone someone's voice without their consent.
"""
with gr.Blocks(title="Habibi-TTS Najdi") as demo:
gr.Markdown(DESCRIPTION)
with gr.Row():
with gr.Column(scale=1):
ref_audio_in = gr.Audio(
label="Reference audio (5-8s, clean, single speaker)",
type="filepath",
sources=["upload", "microphone"],
)
ref_text_in = gr.Textbox(
label="Reference transcript",
placeholder="ุงูƒุชุจ ุงู„ู†ุต ุงู„ู…ู†ุทูˆู‚ ููŠ ุงู„ุตูˆุช ุงู„ู…ุฑุฌุนูŠ",
lines=2, rtl=True,
)
gen_text_in = gr.Textbox(
label="Text to generate",
placeholder="ุงูƒุชุจ ุงู„ู†ุต ุงู„ู…ุทู„ูˆุจ",
lines=3, rtl=True,
value="ู…ุฑุญุจุงุŒ ูƒูŠู ุญุงู„ูƒ ุงู„ูŠูˆู…ุŸ",
)
with gr.Accordion("Advanced", open=False):
nfe_step_in = gr.Slider(
minimum=8, maximum=64, value=32, step=4,
label="NFE steps (quality vs speed)",
)
speed_in = gr.Slider(
minimum=0.5, maximum=2.0, value=1.0, step=0.1,
label="Speech speed",
)
remove_silence_in = gr.Checkbox(
value=False,
label="Trim leading/trailing silence",
)
gen_btn = gr.Button("๐ŸŽ™ Generate", variant="primary", size="lg")
with gr.Column(scale=1):
audio_out = gr.Audio(
label="Generated audio",
type="numpy",
autoplay=True,
)
status_out = gr.Textbox(
label="Status", interactive=False, lines=2,
)
existing_examples = [ex for ex in EXAMPLES if os.path.exists(ex[0])]
if existing_examples:
gr.Examples(
examples=existing_examples,
inputs=[ref_audio_in, ref_text_in, gen_text_in,
nfe_step_in, speed_in, remove_silence_in],
outputs=[audio_out, status_out],
fn=generate,
cache_examples=False,
label="Examples (click to try)",
)
gr.Markdown(ARTICLE)
gen_btn.click(
fn=generate,
inputs=[ref_audio_in, ref_text_in, gen_text_in,
nfe_step_in, speed_in, remove_silence_in],
outputs=[audio_out, status_out],
)
if __name__ == "__main__":
demo.queue(max_size=10).launch(theme=gr.themes.Soft())