praxelhq commited on
Commit
ae9f098
·
verified ·
1 Parent(s): c1f42ec

v1: HF Space frontend + Modal GPU backend + codemix recipe

Browse files
.gitattributes CHANGED
@@ -33,3 +33,7 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ data/cartesia_hi_female_6s.wav filter=lfs diff=lfs merge=lfs -text
37
+ data/sarvam_hi_female_10s.wav filter=lfs diff=lfs merge=lfs -text
38
+ data/sarvam_ta_male_11s.wav filter=lfs diff=lfs merge=lfs -text
39
+ data/sarvam_te_female_9s.wav filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -1,12 +1,69 @@
1
  ---
2
- title: Praxy Voice Demo
3
- emoji: 📊
4
- colorFrom: red
5
  colorTo: purple
6
  sdk: gradio
7
- sdk_version: 6.13.0
8
  app_file: app.py
9
  pinned: false
 
 
 
 
 
 
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Praxy Voice
3
+ emoji: 🎤
4
+ colorFrom: indigo
5
  colorTo: purple
6
  sdk: gradio
7
+ sdk_version: 4.44.0
8
  app_file: app.py
9
  pinned: false
10
+ license: apache-2.0
11
+ short_description: Hi · Te · Ta · En TTS · code-mix · voice cloning
12
+ models:
13
+ - Praxel/praxy-voice-r6
14
+ - ResembleAI/chatterbox
15
+ - ai4bharat/IndicF5
16
  ---
17
 
18
+ ## Required Space secrets
19
+ - `MODAL_TOKEN_ID` — get from Modal dashboard
20
+ - `MODAL_TOKEN_SECRET`
21
+ - `ANTHROPIC_API_KEY` — for the codemix transliteration preprocessor
22
+
23
+ ## Hardware
24
+ Free CPU is sufficient — this Space dispatches all GPU work to Modal.
25
+
26
+ ## Custom domain (voice.praxel.in)
27
+ HF Space settings → Custom Domain → add `voice.praxel.in`, then add CNAME at praxel.in DNS pointing to the Space URL.
28
+
29
+ # Praxy Voice — Gradio Demo
30
+
31
+ Live demo of **Praxy Voice**, an open-source commercial-class Indic TTS system. Type Telugu, Tamil, or Hindi text and hear it spoken in a natural voice — with your own uploaded reference clip, or a bundled default.
32
+
33
+ ## How it works
34
+
35
+ Three branches behind a single interface:
36
+
37
+ | Language | Path |
38
+ |---|---|
39
+ | Telugu | R6 LoRA on Chatterbox + BUPS romanisation + Hi-proxy `language_id` |
40
+ | Tamil | R6 LoRA on Chatterbox + BUPS romanisation + Hi-proxy `language_id` |
41
+ | Hindi | **Vanilla** Chatterbox (LoRA not needed — Chatterbox natively covers Hi) |
42
+
43
+ All three share the **voice-prompt recovery** recipe: an 8–11 s same-language reference audio clip + Config B sampling overrides (exaggeration 0.7, temperature 0.6, min_p 0.1).
44
+
45
+ See the companion paper for the full methodology: *"Praxy Voice: Voice-Prompt Recovery + BUPS for Commercial-Class Indic TTS"* (arXiv 2026).
46
+
47
+ ## Local run
48
+
49
+ ```bash
50
+ cd gradio_demo
51
+ pip install -r requirements.txt
52
+ python app.py
53
+ ```
54
+
55
+ ## Files
56
+
57
+ - `app.py` — Gradio interface + language router + inference
58
+ - `requirements.txt` — Python deps
59
+ - `data/` — bundled default reference clips (place your own here before deploy)
60
+
61
+ ## Defaults and limits
62
+
63
+ - Default reference clips: Sarvam Bulbul Te/Ta + Cartesia Sonic-3 Hi (8–11 s each).
64
+ - Max input text length: not enforced in UI but practically ~400 chars.
65
+ - Hardware: runs on ZeroGPU (T4-class; ~2–5 s per generation).
66
+
67
+ ## License
68
+
69
+ Apache-2.0. See `LICENSE` in the model repo `Praxel/praxy-voice-r6`.
app.py ADDED
@@ -0,0 +1,298 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Praxy Voice — Gradio app on HF Space, GPU inference on Modal.
2
+
3
+ Live at:
4
+ https://huggingface.co/spaces/Praxel/praxy-voice-demo
5
+ https://voice.praxel.in (CNAME → HF Space, see README)
6
+
7
+ Architecture: this Space runs the Gradio UI on HF's free CPU tier and
8
+ dispatches synth requests cross-cluster to the deployed `praxy-voice`
9
+ Modal app's classes (PraxyChatterboxLoRA + IndicF5TTS). Modal has the
10
+ GPU + the proven IndicF5 dependency chain; HF Space is the right tool
11
+ for serving Gradio static assets (which the Modal asgi_app proxy
12
+ doesn't handle cleanly — see memory/project_modal_gradio_dead_end_2026-04-27.md).
13
+
14
+ Required HF Space secrets:
15
+ MODAL_TOKEN_ID
16
+ MODAL_TOKEN_SECRET
17
+ ANTHROPIC_API_KEY # for the codemix transliteration preprocessor
18
+
19
+ Required HF Space hardware: free CPU is fine; this app does no GPU work.
20
+
21
+ Sync from repo (run from `/Users/pushpak/Documents/GitHub/praxy_tts`):
22
+ huggingface-cli upload Praxel/praxy-voice-demo gradio_demo/ . --repo-type=space
23
+
24
+ Or push via git remote:
25
+ git remote add space https://huggingface.co/spaces/Praxel/praxy-voice-demo
26
+ git subtree push --prefix gradio_demo space main
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import io
32
+ import os
33
+ import re
34
+ import tempfile
35
+ import time
36
+ from pathlib import Path
37
+
38
+ import gradio as gr
39
+ import modal
40
+
41
+ HERE = Path(__file__).resolve().parent
42
+
43
+ # Voice library — pre-bundled reference voices.
44
+ VOICE_LIBRARY: dict[str, dict] = {
45
+ "Hindi · Female (default)": {
46
+ "lang": "hi",
47
+ "ref_audio": str(HERE / "data" / "sarvam_hi_female_10s.wav"),
48
+ "ref_text": "मेरे दादा जी हर शाम बरामदे में बैठकर अपने बचपन की कहानियां सुनाते हैं और हम सब मिलकर बडे चाव से उनकी बातें सुनते हैं",
49
+ },
50
+ "Hindi · Female (alt)": {
51
+ "lang": "hi",
52
+ "ref_audio": str(HERE / "data" / "cartesia_hi_female_6s.wav"),
53
+ "ref_text": "मेरे दादाजी हर शाम बरामदे में बैठकर अपने बचपन की कहानियां सुनाते हैं और हम सब मिलकर बड़े चाव से उनकी बातें सुनते हैं",
54
+ },
55
+ "Telugu · Female": {
56
+ "lang": "te",
57
+ "ref_audio": str(HERE / "data" / "sarvam_te_female_9s.wav"),
58
+ "ref_text": "మా తాతయ్య ప్రతి సాయంత్రం వరండాలో కూర్చుని తన చిన్నతనంలో జరిగిన కథలు చెబుతూ ఉంటారు మరియు మేము అందరూ కలిసి ఆసక్తిగా వినేవాళ్లం",
59
+ },
60
+ "Tamil · Male": {
61
+ "lang": "ta",
62
+ "ref_audio": str(HERE / "data" / "sarvam_ta_male_11s.wav"),
63
+ "ref_text": "எங்கள் தாத்தா தினமும் மாலையில் திண்ணையில் அமர்ந்து கொண்டு தன் சிறுவயதில் நடந்த கதைகளைச் சொல்லிக் கொண்டிருப்பார் நாங்கள் அனைவரும் சேர்ந்து ஆர்வமாகக் கேட்போம்",
64
+ },
65
+ }
66
+
67
+ LANG_CODE_TO_NAME = {"hi": "Hindi", "te": "Telugu", "ta": "Tamil", "en": "English"}
68
+
69
+ EXAMPLES = [
70
+ ["Hindi · Female (default)", "नमस्ते! मेरा नाम प्राक्सी है, और मैं भारत के लिए बनाई गई एक ओपन-सोर्स आवाज़ हूँ।"],
71
+ ["Hindi · Female (default)", "मैंने WhatsApp पे message किया but notification नहीं आया।"],
72
+ ["Telugu · Female", "నమస్తే! నేను ప్రాక్సీ, ఇండియా కోసం రూపొందించిన ఓపెన్-సోర్స్ వాయిస్."],
73
+ ["Telugu · Female", "మా CEO ఇవాళ all-hands meeting లో కొత్త quarterly targets announce చేశారు."],
74
+ ["Tamil · Male", "வணக்கம்! நான் ப்ராக்ஸி, இந்தியாவுக்காக உருவாக்கப்பட்ட ஓபன்-சோர்ஸ் குரல்."],
75
+ ]
76
+
77
+ # Cross-cluster Modal lookups — happen lazily on first synth call so the
78
+ # Space boots fast even if Modal is slow.
79
+ _indicf5 = None
80
+ _chatterbox = None
81
+
82
+
83
+ def _get_modal_handles():
84
+ global _indicf5, _chatterbox
85
+ if _indicf5 is None:
86
+ _indicf5 = modal.Cls.from_name("praxy-voice", "IndicF5TTS")()
87
+ if _chatterbox is None:
88
+ _chatterbox = modal.Cls.from_name("praxy-voice", "PraxyChatterboxLoRA")
89
+ return _indicf5, _chatterbox
90
+
91
+
92
+ def _detect_lang(text: str) -> str:
93
+ if re.search(r"[ఀ-౿]", text):
94
+ return "te"
95
+ if re.search(r"[஀-௿]", text):
96
+ return "ta"
97
+ if re.search(r"[ऀ-ॿ]", text):
98
+ return "hi"
99
+ return "en"
100
+
101
+
102
+ def _is_codemix(text: str, lang: str) -> bool:
103
+ return bool(re.search(r"[A-Za-z]{2,}", text)) and lang != "en"
104
+
105
+
106
+ # Codemix transliteration via Anthropic — inline because we don't want to
107
+ # depend on the `serving.codemix_to_native_script` module on the HF Space
108
+ # (which would require pulling in the whole repo). Same prompt as the
109
+ # canonical implementation.
110
+ LANG_NATIVE_NAMES = {"hi": "Hindi (Devanagari script)", "te": "Telugu", "ta": "Tamil"}
111
+
112
+ _TRANSLIT_SYSTEM = """You convert Indian code-mix sentences into pure native-script form.
113
+
114
+ Input: a sentence mixing {lang_name} (in {lang_name} script) with Latin-script English words/phrases.
115
+
116
+ Output rules:
117
+ 1. Keep every {lang_name}-script word/character unchanged. Do NOT translate them.
118
+ 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 → సీఈఓ.
119
+ 3. Preserve all word order, spacing, and punctuation exactly as in the input.
120
+ 4. Do NOT add explanations, brackets, alternatives, or commentary. Output ONLY the converted sentence.
121
+
122
+ Examples:
123
+ Input (Hindi codemix): मैंने WhatsApp पे message किया but notification नहीं आया।
124
+ Output: मैंने व्हाट्सऐप पे मैसेज किया बट नोटिफिकेशन नहीं आया।
125
+
126
+ Input (Telugu codemix): మా CEO ఇవాళ all-hands meeting లో కొత్త quarterly targets announce చేశారు.
127
+ Output: మా సీఈఓ ఇవాళ ఆల్-హ్యాండ్స్ మీటింగ్ లో కొత్త క్వార్టర్లీ టార్గెట్స్ అనౌన్స్ చేశారు."""
128
+
129
+
130
+ def _transliterate(text: str, lang: str) -> str:
131
+ if not re.search(r"[A-Za-z]", text):
132
+ return text
133
+ import anthropic
134
+ client = anthropic.Anthropic() # picks up ANTHROPIC_API_KEY from env
135
+ sys_prompt = _TRANSLIT_SYSTEM.format(lang_name=LANG_NATIVE_NAMES.get(lang, lang))
136
+ resp = client.messages.create(
137
+ model="claude-haiku-4-5",
138
+ max_tokens=512,
139
+ temperature=0.0,
140
+ system=sys_prompt,
141
+ messages=[{"role": "user", "content": text}],
142
+ )
143
+ out = resp.content[0].text.strip()
144
+ out = re.sub(r"^(Output|Translation|Result):\s*", "", out, flags=re.IGNORECASE).strip()
145
+ return out
146
+
147
+
148
+ def synth(text: str, voice_choice: str, custom_audio_path, custom_text: str):
149
+ if not text or not text.strip():
150
+ return None, "❌ Please type some text to synthesise."
151
+
152
+ custom_mode = voice_choice.startswith("📤")
153
+ if custom_mode and custom_audio_path is None:
154
+ return None, "❌ Pick a pre-made voice or upload a reference clip."
155
+
156
+ if custom_mode:
157
+ ref_audio_path = custom_audio_path
158
+ ref_text = (custom_text or "").strip()
159
+ if not ref_text:
160
+ return None, "❌ Custom voice needs a reference transcript matching the audio."
161
+ lang = _detect_lang(text)
162
+ with open(ref_audio_path, "rb") as f:
163
+ ref_bytes = f.read()
164
+ else:
165
+ voice = VOICE_LIBRARY[voice_choice]
166
+ with open(voice["ref_audio"], "rb") as f:
167
+ ref_bytes = f.read()
168
+ ref_text = voice["ref_text"]
169
+ lang = voice["lang"]
170
+
171
+ codemix = _is_codemix(text, lang)
172
+ indicf5, chatterbox_cls = _get_modal_handles()
173
+
174
+ # 1. Codemix → transliterate → IndicF5
175
+ if codemix:
176
+ try:
177
+ text_for_synth = _transliterate(text, lang)
178
+ except Exception as e:
179
+ return None, f"❌ Transliteration failed: {type(e).__name__}: {e}"
180
+ t0 = time.time()
181
+ try:
182
+ wav_bytes, _ = indicf5.synthesize.remote(
183
+ text=text_for_synth, ref_audio_bytes=ref_bytes, ref_text=ref_text,
184
+ )
185
+ except Exception as e:
186
+ return None, f"❌ IndicF5 synth failed: {type(e).__name__}: {e}"
187
+ out = tempfile.NamedTemporaryFile(suffix=".wav", delete=False)
188
+ out.write(wav_bytes); out.close()
189
+ return out.name, (
190
+ f"✓ {LANG_CODE_TO_NAME.get(lang, lang)} code-mix · "
191
+ f"transliterate→IndicF5 · {time.time()-t0:.1f}s"
192
+ )
193
+
194
+ # 2. Pure Hi or Ta → IndicF5 zero-shot
195
+ if lang in ("hi", "ta"):
196
+ t0 = time.time()
197
+ try:
198
+ wav_bytes, _ = indicf5.synthesize.remote(
199
+ text=text, ref_audio_bytes=ref_bytes, ref_text=ref_text,
200
+ )
201
+ except Exception as e:
202
+ return None, f"❌ IndicF5 synth failed: {type(e).__name__}: {e}"
203
+ out = tempfile.NamedTemporaryFile(suffix=".wav", delete=False)
204
+ out.write(wav_bytes); out.close()
205
+ return out.name, (
206
+ f"✓ {LANG_CODE_TO_NAME.get(lang, lang)} pure · "
207
+ f"IndicF5 zero-shot · {time.time()-t0:.1f}s"
208
+ )
209
+
210
+ # 3. Te pure or English → Chatterbox + R6 LoRA
211
+ env = {
212
+ "PRAXY_CKPT_PATH": "/cache/chatterbox_indic/round_6/step_8000.ckpt",
213
+ "PRAXY_USE_BUPS": "1" if lang == "te" else "0",
214
+ "PRAXY_NO_LORA": "1" if lang == "en" else "0",
215
+ }
216
+ synth_cls = chatterbox_cls.with_options(
217
+ secrets=[modal.Secret.from_dict(env), modal.Secret.from_name("praxy-hf")]
218
+ )()
219
+ t0 = time.time()
220
+ try:
221
+ wav_bytes, _ = synth_cls.synthesize.remote(
222
+ text=text, language_code=lang, ref_audio_bytes=ref_bytes,
223
+ exaggeration=0.7, cfg_weight=0.5, temperature=0.6,
224
+ repetition_penalty=2.0, min_p=0.1, top_p=1.0,
225
+ )
226
+ except Exception as e:
227
+ return None, f"❌ Chatterbox synth failed: {type(e).__name__}: {e}"
228
+ out = tempfile.NamedTemporaryFile(suffix=".wav", delete=False)
229
+ out.write(wav_bytes); out.close()
230
+ branch = "vanilla Chatterbox" if lang == "en" else "Chatterbox + R6 LoRA"
231
+ return out.name, (
232
+ f"✓ {LANG_CODE_TO_NAME.get(lang, lang)} · {branch} · {time.time()-t0:.1f}s"
233
+ )
234
+
235
+
236
+ CSS = """
237
+ .gradio-container { max-width: 980px !important; }
238
+ h1 { font-weight: 700; margin-bottom: 0.4em; }
239
+ footer { display: none !important; }
240
+ """
241
+
242
+ with gr.Blocks(title="Praxy Voice", theme=gr.themes.Soft(), css=CSS) as demo:
243
+ gr.Markdown(
244
+ "# 🎤 Praxy Voice\n"
245
+ "Open-source Hindi · Telugu · Tamil · English TTS, including code-mix. "
246
+ "Voice cloning from any 8–15 s reference clip. "
247
+ "Built on [Chatterbox](https://github.com/resemble-ai/chatterbox) + "
248
+ "[IndicF5](https://huggingface.co/ai4bharat/IndicF5) + a Haiku-driven "
249
+ "native-script transliteration preprocessor.\n\n"
250
+ "*v1 · Code: [github.com/praxelhq/praxy-tts](https://github.com/praxelhq/praxy-tts) · "
251
+ "Paper: arXiv (link soon).*"
252
+ )
253
+
254
+ with gr.Row():
255
+ with gr.Column(scale=2):
256
+ text_in = gr.Textbox(
257
+ label="Text to synthesise",
258
+ placeholder="मैंने WhatsApp पे message किया but notification नहीं आया।",
259
+ lines=4,
260
+ )
261
+ voice_in = gr.Dropdown(
262
+ list(VOICE_LIBRARY.keys()) + ["📤 Use custom upload below"],
263
+ value=list(VOICE_LIBRARY.keys())[0],
264
+ label="Voice",
265
+ )
266
+ with gr.Accordion("📤 Or clone your own voice (8–15 s clip)", open=False):
267
+ custom_audio = gr.Audio(label="Reference audio", type="filepath")
268
+ custom_text = gr.Textbox(
269
+ label="Reference transcript (must match the audio exactly)",
270
+ lines=2,
271
+ )
272
+ btn = gr.Button("Generate", variant="primary", size="lg")
273
+ with gr.Column(scale=1):
274
+ audio_out = gr.Audio(label="Output", autoplay=True)
275
+ status = gr.Markdown("")
276
+
277
+ gr.Examples(EXAMPLES, inputs=[voice_in, text_in], label="Try one of these")
278
+
279
+ btn.click(
280
+ synth,
281
+ inputs=[text_in, voice_in, custom_audio, custom_text],
282
+ outputs=[audio_out, status],
283
+ )
284
+
285
+ gr.Markdown(
286
+ "---\n"
287
+ "**Code-mix** (Latin English words inside Indic text) is auto-detected and "
288
+ "transliterated to native script before synth — that's why *WhatsApp* gets "
289
+ "pronounced *vaa-ts-ay-p* rather than American *whats-app*. It matches how "
290
+ "Indians actually code-switch.\n\n"
291
+ "**First request** triggers a Modal cold-start (~30 s while the GPU container warms). "
292
+ "Subsequent requests are sub-5 s.\n\n"
293
+ "**Privacy**: uploaded reference clips are not logged or stored."
294
+ )
295
+
296
+
297
+ if __name__ == "__main__":
298
+ demo.queue(max_size=10).launch(server_name="0.0.0.0")
data/cartesia_hi_female_6s.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1129c0c56aa8e82a724dbfba93083e21f74c131be5864272922665162a0a6044
3
+ size 305828
data/sarvam_hi_female_10s.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:55b8cc3d3ad8cf35235b93be3c866dd9f7555a404b4fee12f9a88104814f6021
3
+ size 471084
data/sarvam_ta_male_11s.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:871fbd6838ab53e85d6e0657c9fe10c07a7a2b13016cb43fc6d395fd32843c35
3
+ size 540716
data/sarvam_te_female_9s.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:dd5b1f59fb031abd8498069d1790129989b5beb415238c1d41b2bb2390004f30
3
+ size 454700
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ gradio==4.44.0
2
+ huggingface_hub<1.0
3
+ modal>=0.64
4
+ anthropic>=0.40
5
+ soundfile==0.12.1
6
+ numpy<2