""" NaijaML TTS — Yorùbá — HuggingFace Space Calls the Modal API for inference. No GPU needed on HF side. API key is loaded from HF Space secrets. """ import gradio as gr import requests import base64 import tempfile import os API_BASE = "https://ecclesia-team--naija-tts-api-ttsservice-{endpoint}.modal.run" API_KEY = os.environ.get("TTS_API_KEY", "") VOICES = { "Títílayọ̀ (Female)": "titilayo", "Àrínọlá (Female, News)": "arinola", "Fọláké (Female)": "folake", "Bímpe (Female)": "bimpe", "Ìyábọ̀ (Female)": "iyabo", "Adéwálé (Male)": "adewale", "Ọlátúnjí (Male)": "olatunji", "Kúnlé (Male)": "kunle", "Túndé (Male)": "tunde", "Babájídé (Male)": "babajide", } EXAMPLES = [ # Proverbs (wisdom) ["wúrà tó máa dán, á la iná kọjá.", "Ọlátúnjí (Male)"], ["ìlé ọba tó jó, ẹwà ló bùsi.", "Àrínọlá (Female, News)"], ["bó ti wù kí ojú kan tóbi tó, ojú méjì sàn ju ojú kan lọ.", "Babájídé (Male)"], # News style ["ìjọba ìpínlẹ̀ èkó ti ṣí ilé ìwòsàn tuntun sí àwọn ará ìbàdàn. oríṣiríṣi dókítà yóò wà níbẹ̀.", "Túndé (Male)"], # Storytelling ["nígbà kan rí, ìjàpá àti ehoro bá ara wọn lọ sí ọjà. wọ́n rí nǹkan tí ó yà wọ́n lẹ́nu.", "Títílayọ̀ (Female)"], # Casual / everyday ["ṣé o ti jẹun lónìí? mo fẹ́ lọ ra oúnjẹ ní ọjà.", "Fọláké (Female)"], ["ẹ jọ̀wọ́, ibo ni mo ti lè rí ọkọ̀ tí yóò gbé mi lọ sí àgbègbè ìkẹjà?", "Kúnlé (Male)"], # Cultural ["orin àwọn àgbàlagbà dùn láti gbọ́, ó sì kún fún ọgbọ́n àtijọ́.", "Bímpe (Female)"], # Inspirational ["ọ̀nà kan ò wọ ọjà. tí ọ̀nà kan bá dì, ọ̀nà mìíràn á ṣí sílẹ̀.", "Adéwálé (Male)"], # Personal ["mo nífẹ̀ẹ́ èdè Yorùbá púpọ̀. ó jẹ́ èdè àwọn baba wa.", "Ìyábọ̀ (Female)"], ] def synthesize(text, voice_name, speed): if not text or not text.strip(): return None, "Please enter some Yorùbá text." voice = VOICES.get(voice_name, "titilayo") url = API_BASE.format(endpoint="synthesize") try: resp = requests.post( url, json={"text": text, "voice": voice, "speed": speed, "api_key": API_KEY}, timeout=30, ) if resp.status_code == 401: return None, "API authentication failed. Please check configuration." if resp.status_code != 200: try: err = resp.json().get("error", resp.text) except Exception: err = resp.text return None, f"API error: {err}" tmp = tempfile.NamedTemporaryFile(suffix=".wav", delete=False) tmp.write(resp.content) tmp.close() inference_ms = resp.headers.get("X-Inference-Ms", "?") audio_dur = resp.headers.get("X-Audio-Duration", "?") rtf = resp.headers.get("X-RTF", "?") return tmp.name, f"Generated {audio_dur}s of audio in {inference_ms}ms (RTF={rtf})" except requests.Timeout: return None, "Request timed out. The server may be cold-starting, try again in 15s." except Exception as e: return None, f"Error: {str(e)}" def clone_voice(text, ref_audio, ref_text, speed): if not text or not text.strip(): return None, "Please enter some Yorùbá text." if ref_audio is None: return None, "Please upload a reference audio clip." if not ref_text or not ref_text.strip(): return None, "Please provide the text spoken in your reference audio." url = API_BASE.format(endpoint="clone") try: with open(ref_audio, "rb") as f: audio_b64 = base64.b64encode(f.read()).decode() resp = requests.post( url, json={ "text": text, "ref_audio": audio_b64, "ref_text": ref_text, "speed": speed, "api_key": API_KEY, }, timeout=60, ) if resp.status_code == 401: return None, "API authentication failed. Please check configuration." if resp.status_code != 200: try: err = resp.json().get("error", resp.text) except Exception: err = resp.text return None, f"API error: {err}" tmp = tempfile.NamedTemporaryFile(suffix=".wav", delete=False) tmp.write(resp.content) tmp.close() inference_ms = resp.headers.get("X-Inference-Ms", "?") audio_dur = resp.headers.get("X-Audio-Duration", "?") return tmp.name, f"Generated {audio_dur}s of cloned audio in {inference_ms}ms" except requests.Timeout: return None, "Request timed out. The server may be cold-starting, try again in 15s." except Exception as e: return None, f"Error: {str(e)}" with gr.Blocks( title="NaijaML TTS | Yorùbá Voice Cloning", theme=gr.themes.Soft(), ) as demo: gr.Markdown( """ # NaijaML TTS ### First zero-shot voice cloning TTS for Yorùbá 10 built-in voices | Clone any voice | Tonal diacritics supported **Tip:** Use full diacritics for best results. "oko" (hoe), "okó" (husband), "okò" (vehicle) are different words. """ ) with gr.Tab("Synthesize"): with gr.Row(): with gr.Column(scale=3): text_input = gr.Textbox( label="Yorùbá Text", placeholder="ọmọdé tó bá mọ̀wẹ́ á bá àgbà jẹun.", lines=3, info="Use full diacritics (ẹ, ọ, ṣ, à, é, etc.) for correct pronunciation", ) with gr.Row(): voice_dropdown = gr.Dropdown( choices=list(VOICES.keys()), value="Àrínọlá (Female, News)", label="Voice", scale=2, ) speed_slider = gr.Slider( minimum=0.7, maximum=1.3, value=1.0, step=0.05, label="Speed", scale=1, ) generate_btn = gr.Button("Generate Speech", variant="primary", size="lg") with gr.Column(scale=2): audio_output = gr.Audio(label="Generated Speech", type="filepath") status_text = gr.Textbox(label="Status", interactive=False) gr.Examples( examples=EXAMPLES, inputs=[text_input, voice_dropdown], label="Try these", ) generate_btn.click( fn=synthesize, inputs=[text_input, voice_dropdown, speed_slider], outputs=[audio_output, status_text], ) with gr.Tab("Voice Cloning"): gr.Markdown( "Upload a **5-10 second** clip of any voice + the text spoken. " "The model will speak Yorùbá in that voice." ) with gr.Row(): with gr.Column(scale=3): clone_text = gr.Textbox( label="Yorùbá Text to Generate", placeholder="ọmọdé tó bá mọ̀wẹ́ á bá àgbà jẹun.", lines=3, ) clone_ref_audio = gr.Audio( label="Reference Audio (5-10 seconds)", type="filepath", ) clone_ref_text = gr.Textbox( label="What is said in the reference?", placeholder="Any language works. Type what the person says in the clip.", lines=2, ) clone_speed = gr.Slider( minimum=0.7, maximum=1.3, value=1.0, step=0.05, label="Speed", ) clone_btn = gr.Button("Clone & Generate", variant="primary", size="lg") with gr.Column(scale=2): clone_audio_output = gr.Audio(label="Cloned Speech", type="filepath") clone_status = gr.Textbox(label="Status", interactive=False) clone_btn.click( fn=clone_voice, inputs=[clone_text, clone_ref_audio, clone_ref_text, clone_speed], outputs=[clone_audio_output, clone_status], ) gr.Markdown( """ --- **Model**: [F5-TTS v1 Base](https://github.com/SWivid/F5-TTS) fine-tuned on 13.4h of Yorùbá (BibleTTS + SLR86 + WAXAL) · 335M params · 150K steps · 10 built-in voices **Note**: First request may take ~15s if the server is cold-starting. Subsequent requests are fast (~500ms). Built by [NaijaML](https://huggingface.co/naijaml) · [Model](https://huggingface.co/naijaml/f5-tts-yoruba) · [GitHub](https://github.com/naijaml) """ ) demo.launch()