"""Praxy Voice — Gradio app on HF Space, GPU inference on Modal. Live at: https://huggingface.co/spaces/Praxel/praxy-voice-demo https://voice.praxel.in (CNAME → HF Space, see README) Architecture: this Space runs the Gradio UI on HF's free CPU tier and dispatches synth requests cross-cluster to the deployed `praxy-voice` Modal app's classes (PraxyChatterboxLoRA + IndicF5TTS). Modal has the GPU + the proven IndicF5 dependency chain; HF Space is the right tool for serving Gradio static assets (which the Modal asgi_app proxy doesn't handle cleanly — see memory/project_modal_gradio_dead_end_2026-04-27.md). Required HF Space secrets: MODAL_TOKEN_ID MODAL_TOKEN_SECRET ANTHROPIC_API_KEY # for the codemix transliteration preprocessor Required HF Space hardware: free CPU is fine; this app does no GPU work. Sync from repo (run from `/Users/pushpak/Documents/GitHub/praxy_tts`): huggingface-cli upload Praxel/praxy-voice-demo gradio_demo/ . --repo-type=space Or push via git remote: git remote add space https://huggingface.co/spaces/Praxel/praxy-voice-demo git subtree push --prefix gradio_demo space main """ from __future__ import annotations import io import os import re import tempfile import time from pathlib import Path import gradio as gr import modal HERE = Path(__file__).resolve().parent # Voice library — pre-bundled reference voices, organised by lang. VOICE_LIBRARY: dict[str, dict] = { "🇮🇳 Hindi · Female (Sarvam-style)": { "lang": "hi", "ref_audio": str(HERE / "data" / "sarvam_hi_female_10s.wav"), "ref_text": "मेरे दादा जी हर शाम बरामदे में बैठकर अपने बचपन की कहानियां सुनाते हैं और हम सब मिलकर बडे चाव से उनकी बातें सुनते हैं", }, "🇮🇳 Hindi · Female (Cartesia-style)": { "lang": "hi", "ref_audio": str(HERE / "data" / "cartesia_hi_female_6s.wav"), "ref_text": "मेरे दादाजी हर शाम बरामदे में बैठकर अपने बचपन की कहानियां सुनाते हैं और हम सब मिलकर बड़े चाव से उनकी बातें सुनते हैं", }, "🇮🇳 Hindi · Male": { "lang": "hi", "ref_audio": str(HERE / "data" / "cartesia_hi_male_10s.wav"), "ref_text": "नमस्ते, मेरा नाम रोहन है। मैं प्राक्सी की हिंदी मेल आवाज़ हूँ।", }, "🌶️ Telugu · Female": { "lang": "te", "ref_audio": str(HERE / "data" / "sarvam_te_female_9s.wav"), "ref_text": "మా తాతయ్య ప్రతి సాయంత్రం వరండాలో కూర్చుని తన చిన్నతనంలో జరిగిన కథలు చెబుతూ ఉంటారు మరియు మేము అందరూ కలిసి ఆసక్తిగా వినేవాళ్లం", }, "🌶️ Telugu · Male": { "lang": "te", "ref_audio": str(HERE / "data" / "sarvam_te_male_10s.wav"), "ref_text": "నమస్తే, నా పేరు ఆదిత్య. నేను ప్రాక్సీ యొక్క తెలుగు మగ గొంతు ను.", }, "🌴 Tamil · Female": { "lang": "ta", "ref_audio": str(HERE / "data" / "sarvam_ta_female_10s.wav"), "ref_text": "வணக்கம், என் பெயர் பூஜா. நான் ப்ராக்ஸியின் தமிழ் பெண் குரல்.", }, "🌴 Tamil · Male": { "lang": "ta", "ref_audio": str(HERE / "data" / "sarvam_ta_male_11s.wav"), "ref_text": "எங்கள் தாத்தா தினமும் மாலையில் திண்ணையில் அமர்ந்து கொண்டு தன் சிறுவயதில் நடந்த கதைகளைச் சொல்லிக் கொண்டிருப்பார் நாங்கள் அனைவரும் சேர்ந்து ஆர்வமாகக் கேட்போம்", }, "🇬🇧 English · Male": { "lang": "en", "ref_audio": str(HERE / "data" / "ashwin_10s.wav"), "ref_text": "Hey, I'm really glad you're here. There's something special about this moment.", }, } LANG_CODE_TO_NAME = {"hi": "Hindi", "te": "Telugu", "ta": "Tamil", "en": "English"} EXAMPLES = [ ["🇮🇳 Hindi · Female (Sarvam-style)", "नमस्ते! मेरा नाम प्राक्सी है, और मैं भारत के लिए बनाई गई एक ओपन-सोर्स आवाज़ हूँ।"], ["🇮🇳 Hindi · Female (Sarvam-style)", "मैंने WhatsApp पे message किया but notification नहीं आया।"], ["🇮🇳 Hindi · Male", "हमारे CEO ने आज all-hands meeting में new quarterly targets announce किए।"], ["🌶️ Telugu · Female", "నమస్తే! నేను ప్రాక్సీ, ఇండియా కోసం రూపొందించిన ఓపెన్-సోర్స్ వాయిస్."], ["🌶️ Telugu · Female", "మా CEO ఇవాళ all-hands meeting లో కొత్త quarterly targets announce చేశారు."], ["🌴 Tamil · Male", "வணக்கம்! நான் ப்ராக்ஸி, இந்தியாவுக்காக உருவாக்கப்பட்ட ஓபன்-சோர்ஸ் குரல்."], ["🇬🇧 English · Male", "Hello! I'm Praxy, an open-source TTS built in India. I speak Hindi, Telugu, Tamil, and English — including code-mix."], ] # Cross-cluster Modal lookups — happen lazily on first synth call so the # Space boots fast even if Modal is slow. _indicf5 = None _chatterbox = None def _get_modal_handles(): global _indicf5, _chatterbox if _indicf5 is None: _indicf5 = modal.Cls.from_name("praxy-voice", "IndicF5TTS")() if _chatterbox is None: _chatterbox = modal.Cls.from_name("praxy-voice", "PraxyChatterboxLoRA") return _indicf5, _chatterbox def _detect_lang(text: str) -> str: if re.search(r"[ఀ-౿]", text): return "te" if re.search(r"[஀-௿]", text): return "ta" if re.search(r"[ऀ-ॿ]", text): return "hi" return "en" def _is_codemix(text: str, lang: str) -> bool: return bool(re.search(r"[A-Za-z]{2,}", text)) and lang != "en" # Codemix transliteration via Anthropic — inline because we don't want to # depend on the `serving.codemix_to_native_script` module on the HF Space # (which would require pulling in the whole repo). Same prompt as the # canonical implementation. LANG_NATIVE_NAMES = {"hi": "Hindi (Devanagari script)", "te": "Telugu", "ta": "Tamil"} _TRANSLIT_SYSTEM = """You convert Indian code-mix sentences into pure native-script form. Input: a sentence mixing {lang_name} (in {lang_name} script) with Latin-script English words/phrases. Output rules: 1. Keep every {lang_name}-script word/character unchanged. Do NOT translate them. 2. For every Latin-script English word/phrase, write its phonetic spelling in {lang_name} script — exactly the way an educated native {lang_name} speaker would write it casually (the way Bollywood subtitles, Indian news tickers, and Sarvam-Bulbul's training data spells English brand and tech terms). Examples for Hindi: WhatsApp → व्हाट्सऐप, message → मैसेज, notification → नोटिफिकेशन, CEO → सीईओ, syllabus → सिलेबस, complete → कम्प्लीट, traffic jam → ट्रैफिक जैम, weekend → वीकेंड. For Telugu: WhatsApp → వాట్సాప్, message → మెసేజ్, notification → నోటిఫికేషన్, CEO → సీఈఓ. 3. Preserve all word order, spacing, and punctuation exactly as in the input. 4. Do NOT add explanations, brackets, alternatives, or commentary. Output ONLY the converted sentence. Examples: Input (Hindi codemix): मैंने WhatsApp पे message किया but notification नहीं आया। Output: मैंने व्हाट्सऐप पे मैसेज किया बट नोटिफिकेशन नहीं आया। Input (Telugu codemix): మా CEO ఇవాళ all-hands meeting లో కొత్త quarterly targets announce చేశారు. Output: మా సీఈఓ ఇవాళ ఆల్-హ్యాండ్స్ మీటింగ్ లో కొత్త క్వార్టర్లీ టార్గెట్స్ అనౌన్స్ చేశారు.""" def _transliterate(text: str, lang: str) -> str: if not re.search(r"[A-Za-z]", text): return text import anthropic client = anthropic.Anthropic() # picks up ANTHROPIC_API_KEY from env sys_prompt = _TRANSLIT_SYSTEM.format(lang_name=LANG_NATIVE_NAMES.get(lang, lang)) resp = client.messages.create( model="claude-haiku-4-5", max_tokens=512, temperature=0.0, system=sys_prompt, messages=[{"role": "user", "content": text}], ) out = resp.content[0].text.strip() out = re.sub(r"^(Output|Translation|Result):\s*", "", out, flags=re.IGNORECASE).strip() return out def synth(text: str, voice_choice: str, custom_audio_path, custom_text: str, progress=gr.Progress()): if not text or not text.strip(): return None, "❌ Please type some text to synthesise." custom_mode = voice_choice.startswith("📤") if custom_mode and custom_audio_path is None: return None, "❌ Pick a pre-made voice or upload a reference clip." progress(0.05, desc="Loading reference voice…") if custom_mode: ref_audio_path = custom_audio_path ref_text = (custom_text or "").strip() if not ref_text: return None, "❌ Custom voice needs a reference transcript matching the audio." lang = _detect_lang(text) with open(ref_audio_path, "rb") as f: ref_bytes = f.read() else: voice = VOICE_LIBRARY[voice_choice] with open(voice["ref_audio"], "rb") as f: ref_bytes = f.read() ref_text = voice["ref_text"] lang = voice["lang"] codemix = _is_codemix(text, lang) progress(0.1, desc="Connecting to Modal GPU…") indicf5, chatterbox_cls = _get_modal_handles() # 1. Codemix → transliterate → IndicF5 if codemix: progress(0.2, desc="Transliterating English words to native script (Haiku)…") try: text_for_synth = _transliterate(text, lang) except Exception as e: return None, f"❌ Transliteration failed: {type(e).__name__}: {e}" progress(0.4, desc="Synthesising with IndicF5 (cold start = 30–60s; warm = 3–5s)…") t0 = time.time() try: wav_bytes, _ = indicf5.synthesize.remote( text=text_for_synth, ref_audio_bytes=ref_bytes, ref_text=ref_text, ) except Exception as e: return None, f"❌ IndicF5 synth failed: {type(e).__name__}: {e}" progress(0.95, desc="Saving audio…") out = tempfile.NamedTemporaryFile(suffix=".wav", delete=False) out.write(wav_bytes); out.close() return out.name, ( f"✓ {LANG_CODE_TO_NAME.get(lang, lang)} code-mix · " f"transliterate→IndicF5 · {time.time()-t0:.1f}s" ) # 2. Pure Hi or Ta → IndicF5 zero-shot if lang in ("hi", "ta"): progress(0.3, desc="Synthesising with IndicF5 (cold start = 30–60s; warm = 3–5s)…") t0 = time.time() try: wav_bytes, _ = indicf5.synthesize.remote( text=text, ref_audio_bytes=ref_bytes, ref_text=ref_text, ) except Exception as e: return None, f"❌ IndicF5 synth failed: {type(e).__name__}: {e}" progress(0.95, desc="Saving audio…") out = tempfile.NamedTemporaryFile(suffix=".wav", delete=False) out.write(wav_bytes); out.close() return out.name, ( f"✓ {LANG_CODE_TO_NAME.get(lang, lang)} pure · " f"IndicF5 zero-shot · {time.time()-t0:.1f}s" ) # 3. Te pure or English → Chatterbox + R6 LoRA progress(0.3, desc="Loading R6 LoRA on Chatterbox (cold start = 30–60s; warm = 3–5s)…") env = { "PRAXY_CKPT_PATH": "/cache/chatterbox_indic/round_6/step_8000.ckpt", "PRAXY_USE_BUPS": "1" if lang == "te" else "0", "PRAXY_NO_LORA": "1" if lang == "en" else "0", } synth_cls = chatterbox_cls.with_options( secrets=[modal.Secret.from_dict(env), modal.Secret.from_name("praxy-hf")] )() t0 = time.time() try: wav_bytes, _ = synth_cls.synthesize.remote( text=text, language_code=lang, ref_audio_bytes=ref_bytes, exaggeration=0.7, cfg_weight=0.5, temperature=0.6, repetition_penalty=2.0, min_p=0.1, top_p=1.0, ) except Exception as e: return None, f"❌ Chatterbox synth failed: {type(e).__name__}: {e}" progress(0.95, desc="Saving audio…") out = tempfile.NamedTemporaryFile(suffix=".wav", delete=False) out.write(wav_bytes); out.close() branch = "vanilla Chatterbox" if lang == "en" else "Chatterbox + R6 LoRA" return out.name, ( f"✓ {LANG_CODE_TO_NAME.get(lang, lang)} · {branch} · {time.time()-t0:.1f}s" ) CSS = """ .gradio-container { max-width: 1100px !important; } h1 { font-weight: 700; margin-bottom: 0.2em; } .warning-banner { background: #fff7e6; border-left: 4px solid #f59e0b; padding: 0.8em 1em; border-radius: 4px; margin: 0.5em 0 1em; } .tip { color: #555; font-size: 0.92em; } footer { display: none !important; } """ INTRO_MD = """\ # 🎤 Praxy Voice **Open-source Hindi · Telugu · Tamil · English TTS — with code-mix and voice cloning.** Built on [Chatterbox](https://github.com/resemble-ai/chatterbox) + [IndicF5](https://huggingface.co/ai4bharat/IndicF5) + a Haiku-driven native-script transliteration preprocessor. \ Code: [github.com/praxelhq/praxy](https://github.com/praxelhq/praxy) · Model: [Praxel/praxy-voice-r6](https://huggingface.co/Praxel/praxy-voice-r6) · Paper: arXiv (link soon). """ WARNING_HTML = """\
⏱️ First synth takes 30–60 s — Modal spins up a GPU container on cold start. Subsequent generations are ~3–5 s. Be patient on the first click.
""" INSTRUCTIONS_MD = """\ ### How to use 1. **Pick a voice** from the dropdown (or clone your own in *📤 Clone my own voice* below). 2. **Type or paste text** in the language matching the voice — Hindi text for a Hindi voice, etc. 3. **Code-mix is automatic**: write *"मैंने WhatsApp पे message किया"* and the model pronounces *WhatsApp* the way Indians say it (vaa-ts-ay-p), not the American way. 4. Click **Generate**. **Tip:** for natural code-mix output, write the way you'd actually message a friend — `meeting`, `weekend`, `CEO`, `WhatsApp`, `coffee` are all fair game. """ CLONE_MD = """\ Upload a clean **8–15 s** clip of someone speaking in the language you want output. Paste the **exact transcript** (word-for-word) in the box below. **Recording tips** - Quiet room, single speaker, natural pace. - Phone audio works fine. WhatsApp voice notes work great. - One paragraph, ~10 s of audio. **Sample reference text** — record yourself reading one of these, then paste it back as the transcript: | Lang | Sample text to read | |---|---| | Hindi | *मेरा नाम राहुल है। मैं मुंबई में रहता हूँ और एक सॉफ्टवेयर इंजीनियर हूँ।* | | Telugu | *నా పేరు రాహుల్. నేను ముంబైలో ఉంటాను, సాఫ్ట్‌వేర్ ఇంజినీర్‌గా పని చేస్తున్నాను.* | | Tamil | *என் பெயர் ராகுல். நான் மும்பையில் வசிக்கிறேன், சாஃப்ட்வேர் இன்ஜினியராக வேலை செய்கிறேன்.* | | English | *My name is Rahul. I live in Mumbai and work as a software engineer.* | """ OUTRO_MD = """\ --- **About code-mix**: when you mix English words into Hindi/Telugu/Tamil, the model auto-transliterates them to native-script phonetic spelling (*WhatsApp* → *व्हाट्सऐप*) before synth. This matches how Bollywood subtitles, news tickers, and native Indian speakers actually write code-switched messages — closer to natural Indian English than American pronunciation. **Privacy**: uploaded reference clips are processed in-memory and not stored. Generated audio is not logged. """ with gr.Blocks(title="Praxy Voice", theme=gr.themes.Soft(), css=CSS) as demo: gr.Markdown(INTRO_MD) gr.HTML(WARNING_HTML) with gr.Row(): with gr.Column(scale=2): gr.Markdown(INSTRUCTIONS_MD) text_in = gr.Textbox( label="✍️ Text to synthesise", placeholder="नमस्ते! मेरा नाम प्राक्सी है, और मैं भारत के लिए बनाई गई एक ओपन-सोर्स आवाज़ हूँ।", lines=4, ) voice_in = gr.Dropdown( list(VOICE_LIBRARY.keys()) + ["📤 Use my own uploaded voice"], value=list(VOICE_LIBRARY.keys())[0], label="🎙️ Voice", info="Pre-made voices use commercial-grade reference clips. Pick *Use my own* to clone any voice.", ) with gr.Accordion("📤 Clone my own voice (8–15 s clip + transcript)", open=False): gr.Markdown(CLONE_MD) custom_audio = gr.Audio( label="Reference audio", type="filepath", sources=["upload", "microphone"], ) custom_text = gr.Textbox( label="Reference transcript — must match the audio exactly", lines=3, placeholder="Paste the exact words spoken in your clip. Even small typos hurt voice quality.", ) btn = gr.Button("✨ Generate", variant="primary", size="lg") with gr.Column(scale=1): audio_out = gr.Audio(label="🔊 Output", autoplay=True) status = gr.Markdown("") gr.Examples( EXAMPLES, inputs=[voice_in, text_in], label="🎯 Try one of these — click any row to load it", ) btn.click( synth, inputs=[text_in, voice_in, custom_audio, custom_text], outputs=[audio_out, status], api_name=False, # skip API schema gen ) gr.Markdown(OUTRO_MD) # HF Space runs this file directly as __main__. Disable API server + # analytics; queue keeps event order under load. demo.queue(max_size=10, api_open=False) if __name__ == "__main__": demo.launch( server_name="0.0.0.0", server_port=7860, share=True, ssr_mode=False, show_api=False, show_error=True, )