polyglot-me / app.py
praxelhq's picture
a11y polish: theme floating component labels, AA owl roles, brighter secondary text
e9a4196 verified
Raw
History Blame Contribute Delete
10.1 kB
"""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,.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()