KasbahTTS-Demo / app.py
yasuogg's picture
Replace example reference audio with 3 new refs (1/2/3.wav)
6f848f3 verified
Raw
History Blame Contribute Delete
9.09 kB
"""
KasbahTTS V0 — Algerian Dardja text-to-speech.
Zero-shot voice cloning demo for MenaVoice/KasbahTTS-V0, an F5-TTS (DiT flow
matching) model fine-tuned on Algerian Dardja.
`spaces` must be imported before torch so ZeroGPU can patch CUDA init.
"""
import os
import random
import re
import tempfile
try:
import spaces
USING_ZEROGPU = True
except ImportError:
USING_ZEROGPU = False
import gradio as gr
import soundfile as sf
import torch
from f5_tts.infer.utils_infer import (
load_model,
load_vocoder,
preprocess_ref_audio_text,
remove_silence_for_generated_wav,
)
from f5_tts.model import DiT
from huggingface_hub import hf_hub_download
from habibi_tts.infer.utils_infer import infer_process
def gpu_decorator(func):
# Only wrap with the ZeroGPU allocator when actually running on ZeroGPU
# hardware (env var set by HF). On CPU/paid-GPU spaces this is a no-op.
if USING_ZEROGPU and os.environ.get("SPACES_ZERO_GPU"):
return spaces.GPU(duration=120)(func)
return func
MODEL_REPO = "MenaVoice/KasbahTTS-V0"
CKPT_FILE = "ALGERIA.safetensors"
V1_CFG = dict(dim=1024, depth=22, heads=16, ff_mult=2, text_dim=512, conv_layers=4)
# On ZeroGPU, CUDA exists only *inside* a @spaces.GPU call — never at import time.
# So load everything on CPU here and move to GPU inside generate().
ON_ZEROGPU = USING_ZEROGPU and bool(os.environ.get("SPACES_ZERO_GPU"))
GPU_DEVICE = "cuda" if (ON_ZEROGPU or torch.cuda.is_available()) else "cpu"
ckpt_path = hf_hub_download(MODEL_REPO, CKPT_FILE)
vocab_path = hf_hub_download(MODEL_REPO, "vocab.txt")
vocoder = load_vocoder(vocoder_name="vocos", is_local=False, device="cpu")
model = load_model(DiT, V1_CFG, ckpt_path, vocab_file=vocab_path, device="cpu")
EXAMPLES_DIR = os.path.join(os.path.dirname(__file__), "examples")
# (reference wav, exact transcript of that wav) — transcripts must match the
# audio verbatim; all refs are kept under f5-tts's 12s reference clip limit.
EXAMPLE_REFS = [
(
os.path.join(EXAMPLES_DIR, "1.wav"),
"مَثَلاً ما تَلْڨاشْ واحدْ يقول لك شْجْرة قْلَقتني، شْجْرة مَرَضَتْلي حْيَاتي. تْلْڨاه غِيرْ يْڨُول لك هَذ الإنْسَان فْلَان وْ فْلَان وْ فْلَان. تسما مَشَاكِلْنا كَامَل مَنْ عَنْد النَّاس.",
),
(
os.path.join(EXAMPLES_DIR, "2.wav"),
"الطالبات يروحوا ليها يتغداو، كنت أنا نروح بعد لي كور باش نتغدى، وكان كاين واحد الشاب دائماً يشوف فيا ويحب يحكي معايا، ويعاملني معاملة خاصة.",
),
(
os.path.join(EXAMPLES_DIR, "3.wav"),
"الدراهم ولا القدره المعيشيه كيما الفلاح عمر هذا كان عادي معندوش و مخصوش عايش على قد حال",
),
]
# The model was trained on unvocalized Arabic script only: Latin letters and
# digits fall outside its vocabulary and produce unpredictable audio.
UNSUPPORTED = re.compile(r"[A-Za-z0-9٠-٩]")
@gpu_decorator
def generate(
ref_audio_orig,
ref_text,
gen_text,
speed,
nfe_step,
cfg_strength,
cross_fade,
remove_sil,
seed,
progress=gr.Progress(),
):
if not ref_audio_orig:
raise gr.Error("Please provide a reference audio clip (5-15 seconds works best).")
if not gen_text.strip():
raise gr.Error("Please enter some Algerian Dardja text to synthesize.")
if UNSUPPORTED.search(gen_text):
gr.Warning(
"Latin letters and digits are outside the model's vocabulary. "
"Spell numbers out in Arabic words (ثلاثة, not 3)."
)
if seed is None or int(seed) < 0:
seed = random.randint(0, 2**31 - 1)
seed = int(seed)
torch.manual_seed(seed)
# Leaving ref_text blank triggers Whisper auto-transcription inside f5-tts.
ref_audio, ref_text = preprocess_ref_audio_text(ref_audio_orig, ref_text or "", show_info=gr.Info)
# Move to GPU now that we're inside the @spaces.GPU context (loaded on CPU at startup).
model.to(GPU_DEVICE)
vocoder.to(GPU_DEVICE)
try:
# KasbahTTS is a *specialized* single-dialect checkpoint, so no dialect
# token is prepended — infer_process defaults dialect_id to None.
wave, sample_rate, _ = infer_process(
ref_audio,
ref_text,
gen_text,
model,
vocoder,
cross_fade_duration=cross_fade,
nfe_step=int(nfe_step),
cfg_strength=cfg_strength,
speed=speed,
show_info=gr.Info,
progress=progress,
device=GPU_DEVICE,
)
finally:
# ZeroGPU tears down CUDA after the call — return the model to CPU so the
# next call starts from a valid copy.
if ON_ZEROGPU:
model.to("cpu")
vocoder.to("cpu")
torch.cuda.empty_cache()
if remove_sil:
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f:
tmp_path = f.name
try:
sf.write(tmp_path, wave, sample_rate)
remove_silence_for_generated_wav(tmp_path)
wave, sample_rate = sf.read(tmp_path)
finally:
os.unlink(tmp_path)
return (sample_rate, wave), ref_text, seed
with gr.Blocks(title="KasbahTTS V0", theme=gr.themes.Soft()) as demo:
gr.Markdown(
"""
# 🇩🇿 KasbahTTS V0 — Algerian Dardja TTS
Zero-shot voice cloning for **Algerian Dardja** (الدارجة الجزائرية).
Upload a few seconds of any voice, type Dardja text, and hear it spoken back in that voice.
Model: [`MenaVoice/KasbahTTS-V0`](https://huggingface.co/MenaVoice/KasbahTTS-V0) ·
F5-TTS architecture, fine-tuned from Habibi-TTS · MIT licensed.
"""
)
with gr.Row():
with gr.Column():
ref_audio = gr.Audio(label="Reference voice", type="filepath")
ref_text = gr.Textbox(
label="Reference transcript (optional)",
lines=2,
placeholder="Leave blank to auto-transcribe with Whisper…",
)
gen_text = gr.Textbox(
label="Text to generate (Arabic script only)",
lines=4,
placeholder="واش راك خويا، لاباس عليك؟",
rtl=True,
)
run = gr.Button("🎤 Generate", variant="primary", size="lg")
with gr.Column():
audio_out = gr.Audio(label="Generated speech", interactive=False)
ref_text_out = gr.Textbox(label="Reference transcript used", interactive=False, lines=2)
seed_out = gr.Number(label="Seed used", interactive=False, precision=0)
with gr.Accordion("Advanced settings", open=False):
speed = gr.Slider(0.5, 1.5, value=1.0, step=0.05, label="Speed")
nfe_step = gr.Slider(
8, 64, value=32, step=8, label="NFE steps",
info="More steps = higher quality, slower.",
)
cfg_strength = gr.Slider(0.5, 5.0, value=2.0, step=0.1, label="CFG strength")
cross_fade = gr.Slider(0.0, 0.5, value=0.15, step=0.05, label="Cross-fade (s)")
remove_sil = gr.Checkbox(label="Trim long silences", value=False)
seed = gr.Number(label="Seed (-1 = random)", value=-1, precision=0)
inputs = [ref_audio, ref_text, gen_text, speed, nfe_step, cfg_strength, cross_fade, remove_sil, seed]
outputs = [audio_out, ref_text_out, seed_out]
run.click(generate, inputs=inputs, outputs=outputs)
available_refs = [(wav, text) for wav, text in EXAMPLE_REFS if os.path.exists(wav)]
if available_refs:
gen_samples = [
"واش راك خويا، لاباس عليك؟ صباح الخير.",
"من القصبة للعالم، هذا أول موديل يهدر بالدارجة الجزائرية.",
"اليوم الجو شباب، قلت نخرج نتمشى شوية في وسط البلاد.",
]
gr.Examples(
examples=[
[wav, text, gen_samples[i % len(gen_samples)]]
for i, (wav, text) in enumerate(available_refs)
],
inputs=[ref_audio, ref_text, gen_text],
)
gr.Markdown(
"""
---
### Known limitations
- **Arabic script only.** No French code-switching — `ça va` will not work.
- **No digits.** Write `ثلاثة`, not `3`.
- **No diacritics (تشكيل).** Use plain, unvocalized Arabic.
- **Occasional repetition.** Try `nfe_step` 64, adjust `cfg_strength`, or split long text.
Built with ❤️ for Algeria by MenaVoice — *من القصبة للعالم*
"""
)
if __name__ == "__main__":
demo.queue().launch()