| """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 |
|
|
| 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"} |
| |
| LANG_HEADER = {"en": "#3B5998", "hi": "#C2410C", "te": "#0F766E", "ta": "#991B1B"} |
| |
| 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,.66);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() |
|
|