"""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'
'
f'
{LANG_SCRIPT[lang]}
'
f'
'
f'
{LANG_NAMES[lang]}
'
f'
{LANG_LABEL[lang]} SCRIPT
')
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 = """
🎙️
Polyglot Me
Record ten seconds of your voice — hear yourself
speak English, Hindi, Telugu, and Tamil.
ENGLISH
हिन्दी
తెలుగు
தமிழ்
VoxCPM2 · OPENBMB
MODAL · SERVERLESS
⏳ First run warms the model on Modal (~2–4 min). After that it's quick.
"""
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"")
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('
')
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()