Spaces:
Running on Zero
Running on Zero
| """ | |
| 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) | |
| # ====================================================================== | |
| # 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()) |