Spaces:
Paused
Paused
| """Polyglot Me — HF Space for App 2 (Thousand Token Wood). | |
| Record ~10s of your voice, type a line, hear yourself across English / Hindi / | |
| Telugu / Tamil. VoxCPM2 (OpenBMB) clones your voice for every language; Sarvam | |
| translates. All on Modal; this Space is CPU-only. | |
| Hackathon: Thousand Token Wood · OpenBMB · Modal. | |
| """ | |
| from __future__ import annotations | |
| import io | |
| import gradio as gr | |
| import matplotlib | |
| import modal | |
| import numpy as np | |
| import soundfile as sf | |
| from theme import build_css | |
| matplotlib.use("Agg") | |
| import matplotlib.pyplot as plt # noqa: E402 | |
| MODAL_APP = "praxy-voice" | |
| LANGS = ["en", "hi", "te", "ta"] | |
| LANG_NAMES = {"en": "English", "hi": "Hindi", "te": "Telugu", "ta": "Tamil"} | |
| LANG_SCRIPT = {"en": "Aa", "hi": "हि", "te": "తె", "ta": "த"} | |
| LANG_LABEL = {"en": "LATIN", "hi": "DEVANAGARI", "te": "TELUGU", "ta": "TAMIL"} | |
| # Header colours: WCAG-AA verified with white text (≥5:1). Jewel tones on dark. | |
| LANG_HEADER = {"en": "#3B5998", "hi": "#C2410C", "te": "#0F766E", "ta": "#991B1B"} | |
| # Brighter accents for the matplotlib share-card waveforms (graphics, not text). | |
| LANG_WAVE = {"en": "#8B7FF9", "hi": "#FF7A7A", "te": "#4FC878", "ta": "#5BB8F2"} | |
| _TRANSLATOR = modal.Cls.from_name(MODAL_APP, "SarvamTranslator") | |
| _VOX = modal.Cls.from_name(MODAL_APP, "VoxCPM2Cloner") | |
| def _make_card(lines: dict, wav_paths: dict) -> str: | |
| order = ["English", "Hindi", "Telugu", "Tamil"] | |
| wave = {LANG_NAMES[l]: LANG_WAVE[l] for l in LANGS} | |
| fig = plt.figure(figsize=(10, 6.5), facecolor="#0E1020") | |
| fig.text(.5, .965, "Polyglot Me", ha="center", va="top", color="white", | |
| fontsize=23, fontweight="bold") | |
| fig.text(.5, .905, "one voice · four languages", ha="center", va="top", | |
| color="#8E8FB4", fontsize=12) | |
| for i, lang in enumerate(order): | |
| ax = fig.add_subplot(4, 1, i + 1); ax.set_facecolor("#0E1020") | |
| c = wave[lang]; path = wav_paths.get(lang) | |
| if path: | |
| arr, _ = sf.read(path, dtype="float32") | |
| xs = np.linspace(0, 1, len(arr)) | |
| ax.fill_between(xs, arr, alpha=.4, color=c); ax.plot(xs, arr, color=c, lw=.6, alpha=.9) | |
| ax.set_xlim(0, 1) | |
| ax.text(-.01, .5, lang, transform=ax.transAxes, ha="right", va="center", | |
| color=c, fontsize=11, fontweight="bold") | |
| snip = lines.get(lang, "") | |
| if len(snip) > 64: snip = snip[:61] + "…" | |
| ax.text(.015, .5, snip, transform=ax.transAxes, ha="left", va="center", | |
| color="white", fontsize=9.5, alpha=.88) | |
| ax.set_xticks([]); ax.set_yticks([]) | |
| for sp in ax.spines.values(): sp.set_visible(False) | |
| ax.axvline(0, color=c, lw=4, solid_capstyle="round") | |
| fig.subplots_adjust(left=.13, right=.98, top=.87, bottom=.03, hspace=.1) | |
| out = "/tmp/polyglot_card.png" | |
| fig.savefig(out, dpi=160, bbox_inches="tight", facecolor="#0E1020"); plt.close(fig) | |
| return out | |
| def _lang_header_html(lang: str) -> str: | |
| return ( | |
| f'<div style="background:{LANG_HEADER[lang]};padding:16px 20px;display:flex;' | |
| f'align-items:center;gap:14px;border-radius:18px 18px 0 0;">' | |
| f'<div style="font-family:\'Fraunces\',serif;font-size:32px;font-weight:700;line-height:1;' | |
| f'color:rgba(255,255,255,.85);min-width:42px;text-align:center;">{LANG_SCRIPT[lang]}</div>' | |
| f'<div style="display:flex;flex-direction:column;gap:3px;">' | |
| f'<div style="font-size:14px;font-weight:800;color:#fff;letter-spacing:.02em;">{LANG_NAMES[lang]}</div>' | |
| f'<div style="font-size:10px;font-weight:700;color:rgba(255,255,255,.82);' | |
| f'letter-spacing:.1em;">{LANG_LABEL[lang]} SCRIPT</div></div></div>') | |
| def generate(ref_audio_path, line): | |
| empty = [None, None, None, None, None, "Record a clip and type a line.", ""] | |
| if not ref_audio_path or not (line and line.strip()): | |
| return empty | |
| with open(ref_audio_path, "rb") as f: ref_bytes = f.read() | |
| translated = _TRANSLATOR().translate.remote(line, "en", ["hi", "te", "ta"]) | |
| lines = {"en": line, **translated} | |
| outs = [] | |
| for lang in LANGS: | |
| wb, _ = _VOX().clone.remote(text=lines[lang], ref_audio_bytes=ref_bytes) | |
| path = f"/tmp/polyglot_{lang}.wav" | |
| with open(path, "wb") as f: f.write(wb) | |
| outs.append(path) | |
| card = _make_card({LANG_NAMES[l]: lines[l] for l in LANGS}, | |
| {LANG_NAMES[l]: outs[i] for i, l in enumerate(LANGS)}) | |
| transcript = "\n".join(f"{LANG_NAMES[l]}: {lines[l]}" for l in LANGS) | |
| caption = ('I typed one line and heard myself say it in four languages 🎙️\n\n' | |
| f'"{line}"\n\n' + transcript + | |
| "\n\nBuilt with VoxCPM2 + Praxy for #BuildSmall #HuggingFace #PolyglotMe") | |
| return outs + [card, transcript, caption] | |
| HERO = """ | |
| <div style="border-radius:26px;padding:62px 28px 54px;text-align:center;position:relative; | |
| overflow:hidden;min-height:300px;border:1px solid rgba(255,255,255,.08); | |
| background:linear-gradient(135deg,#1a0050,#0d1a60,#003040,#0d1a00,#400010,#200040); | |
| background-size:600% 600%;animation:gsh 12s ease infinite;"> | |
| <div style="position:absolute;top:-40px;left:-40px;width:210px;height:210px;border-radius:50%; | |
| background:radial-gradient(circle,rgba(124,111,247,.4),transparent 70%);animation:orb 4s 0s ease-in-out infinite;"></div> | |
| <div style="position:absolute;top:-20px;right:-20px;width:170px;height:170px;border-radius:50%; | |
| background:radial-gradient(circle,rgba(247,111,111,.4),transparent 70%);animation:orb 4s .7s ease-in-out infinite;"></div> | |
| <div style="position:absolute;bottom:-30px;left:30%;width:190px;height:190px;border-radius:50%; | |
| background:radial-gradient(circle,rgba(79,200,120,.35),transparent 70%);animation:orb 4s 1.3s ease-in-out infinite;"></div> | |
| <div style="position:absolute;bottom:-20px;right:10%;width:150px;height:150px;border-radius:50%; | |
| background:radial-gradient(circle,rgba(79,184,247,.35),transparent 70%);animation:orb 4s 2s ease-in-out infinite;"></div> | |
| <div style="position:relative;z-index:10;max-width:640px;margin:0 auto;"> | |
| <div style="font-size:60px;margin-bottom:10px;">🎙️</div> | |
| <h1 style="font-family:'Fraunces',serif;font-size:clamp(2.6rem,7vw,4.4rem);font-weight:900; | |
| color:#fff;margin:0 0 12px;letter-spacing:-.02em;line-height:1; | |
| text-shadow:0 0 40px rgba(255,255,255,.3);">Polyglot Me</h1> | |
| <p style="font-family:'Plus Jakarta Sans',sans-serif;font-size:clamp(1rem,2.5vw,1.2rem); | |
| color:rgba(255,255,255,.82);margin:0 auto 18px;max-width:500px;line-height:1.6;"> | |
| Record ten seconds of your voice — hear <strong style="color:#fff;">yourself</strong> | |
| speak English, Hindi, Telugu, and Tamil.</p> | |
| <div style="display:flex;justify-content:center;gap:7px;flex-wrap:wrap;margin-bottom:12px;"> | |
| <span class="pf-pill" style="background:#3B5998;color:#fff;">ENGLISH</span> | |
| <span class="pf-pill" style="background:#C2410C;color:#fff;">हिन्दी</span> | |
| <span class="pf-pill" style="background:#0F766E;color:#fff;">తెలుగు</span> | |
| <span class="pf-pill" style="background:#991B1B;color:#fff;">தமிழ்</span> | |
| </div> | |
| <div style="display:flex;justify-content:center;gap:8px;flex-wrap:wrap;"> | |
| <span class="pf-pill" style="background:rgba(255,255,255,.1);color:rgba(255,255,255,.8);border:1px solid rgba(255,255,255,.16);">VoxCPM2 · OPENBMB</span> | |
| <span class="pf-pill" style="background:rgba(255,255,255,.1);color:rgba(255,255,255,.8);border:1px solid rgba(255,255,255,.16);">MODAL · SERVERLESS</span> | |
| </div> | |
| <p style="font-size:12.5px;color:rgba(255,255,255,.5);font-family:'Plus Jakarta Sans',sans-serif;margin:16px 0 0;"> | |
| ⏳ First run warms the model on Modal (~2–4 min). After that it's quick.</p> | |
| </div> | |
| <div style="position:absolute;bottom:0;left:0;right:0;height:3px; | |
| background:linear-gradient(90deg,#3B5998,#C2410C,#0F766E,#991B1B);"></div> | |
| </div> | |
| """ | |
| EXTRA = """ | |
| @keyframes gsh {0%{background-position:0% 50%}50%{background-position:100% 50%}100%{background-position:0% 50%}} | |
| @keyframes orb {0%,100%{transform:scale(1) translateY(0)}50%{transform:scale(1.06) translateY(-6px)}} | |
| .lang-card{padding:0!important;overflow:hidden!important;} | |
| #share-card img{border-radius:16px!important;border:1px solid rgba(255,255,255,.1)!important;} | |
| #caption-out textarea{border-left:3px solid #5BB8F2!important;font-size:13px!important;line-height:1.8!important;} | |
| #transcript-out textarea{font-size:13px!important;} | |
| .rainbow-hr{height:2px;background:linear-gradient(90deg,#3B5998,#C2410C,#0F766E,#991B1B);border:none;margin:18px 0;opacity:.5;border-radius:2px;} | |
| """ | |
| with gr.Blocks(title="Polyglot Me") as demo: | |
| gr.HTML(f"<style>{build_css('polyglot', EXTRA)}</style>") | |
| gr.HTML(HERO) | |
| with gr.Row(): | |
| ref_in = gr.Audio(sources=["microphone", "upload"], type="filepath", | |
| label="Your voice (~10 seconds)", scale=1) | |
| line_in = gr.Textbox(label="Say something (in English)", lines=3, scale=2, | |
| placeholder="Good morning Amma, hope you slept well.") | |
| speak_btn = gr.Button("🌍 Speak it in 4 languages", variant="primary", elem_id="cta") | |
| gr.HTML('<hr class="rainbow-hr"/>') | |
| audios = {} | |
| with gr.Row(): | |
| for lang in LANGS: | |
| with gr.Column(elem_classes=["lang-card"]): | |
| gr.HTML(_lang_header_html(lang)) | |
| audios[lang] = gr.Audio(label="", type="filepath", show_label=False) | |
| share_card = gr.Image(label="Share card", type="filepath", elem_id="share-card") | |
| with gr.Row(): | |
| transcript_out = gr.Textbox(label="Translations", lines=4, elem_id="transcript-out", scale=1) | |
| caption_out = gr.Textbox(label="Caption (copy for your post)", lines=6, elem_id="caption-out", scale=1) | |
| speak_btn.click(generate, inputs=[ref_in, line_in], | |
| outputs=[audios["en"], audios["hi"], audios["te"], audios["ta"], | |
| share_card, transcript_out, caption_out]) | |
| if __name__ == "__main__": | |
| demo.launch() | |