"""Say It Back in Kalenjin — say a word in English, hear it in Kalenjin. English speech --(Whisper)--> English text --(Kalenjin cascade, Modal)--> Kalenjin text --(Cheps TTS, Modal)--> Kalenjin audio (auto-plays) Built for the HF × Gradio "Build Small" hackathon — Backyard AI track. Custom-designed UI (off-white + terracotta, editorial serif) — not stock Gradio. """ from __future__ import annotations import functools import hashlib import json import os import gradio as gr import requests HERE = os.path.dirname(os.path.abspath(__file__)) TRANSLATE_URL = os.environ.get( "MODAL_TRANSLATE_URL", "https://tonykipkemboi--kalenjin-cascade-api-textcascade-translate.modal.run", ) TTS_URL = os.environ.get( "MODAL_SYNTHESIZE_URL", "https://tonykipkemboi--kalenjin-tts-serve-kalenjintts-fastapi-app.modal.run/synthesize", ) INFERENCE_TOKEN = os.environ.get("INFERENCE_TOKEN", "") # Space secret; never committed AUDIO_DIR = os.path.join(HERE, "audio") os.makedirs(AUDIO_DIR, exist_ok=True) try: with open(os.path.join(HERE, "phrasebook.json"), encoding="utf-8") as f: PHRASEBOOK = json.load(f) except FileNotFoundError: PHRASEBOOK = {} FEATURED = list(PHRASEBOOK.keys()) or [ "Thank you", "Good morning", "How are you?", "I love you", "Water", "Milk", "My child", "Come and eat", ] # ── models ─────────────────────────────────────────────────────────────────── _whisper = None def _get_whisper(): global _whisper if _whisper is None: from faster_whisper import WhisperModel _whisper = WhisperModel("base.en", device="cpu", compute_type="int8") return _whisper def transcribe_english(audio_path): if not audio_path: return "" segments, _ = _get_whisper().transcribe(audio_path, language="en", beam_size=1) return " ".join(s.text for s in segments).strip() @functools.lru_cache(maxsize=512) def translate_en_to_kal(text): r = requests.post(TRANSLATE_URL, json={"text": text}, timeout=70) r.raise_for_status() d = r.json() return (d.get("kalenjin", "").strip(), d.get("swahili", "").strip()) @functools.lru_cache(maxsize=512) def synthesize_kalenjin(text): headers = {"Content-Type": "application/json"} if INFERENCE_TOKEN: headers["Authorization"] = f"Bearer {INFERENCE_TOKEN}" r = requests.post(TTS_URL, json={"text": text}, headers=headers, timeout=150) r.raise_for_status() key = hashlib.sha256(text.encode()).hexdigest()[:16] path = os.path.join(AUDIO_DIR, f"tts_{key}.wav") with open(path, "wb") as f: f.write(r.content) return path def _kal(text): return f"
{text}
" def _meta(english, swahili=None): bits = [] if english: bits.append(f"You said “{english}”") if swahili: bits.append(f"Swahili: {swahili}") return "
" + "  ·  ".join(bits) + "
" if bits else "" def say_it_back(typed_text, audio_path): english = (typed_text or "").strip() if not english and audio_path: yield _kal("listening…"), None, "" english = transcribe_english(audio_path) if not english: yield _kal("Say or type something in English"), None, "" return if english in PHRASEBOOK: e = PHRASEBOOK[english] yield _kal(e["kalenjin"]), os.path.join(HERE, e["wav"]), _meta(english, e["swahili"]) return yield _kal("translating…"), None, _meta(english) try: kalenjin, swahili = translate_en_to_kal(english) except Exception as e: # noqa: BLE001 yield _kal("translation failed"), None, _meta(english) return if not kalenjin: yield _kal("no translation"), None, _meta(english) return yield _kal(kalenjin), None, _meta(english, swahili) + "
voicing…
" try: wav = synthesize_kalenjin(kalenjin) except Exception: # noqa: BLE001 yield _kal(kalenjin), None, _meta(english, swahili) + "
voice unavailable
" return yield _kal(kalenjin), wav, _meta(english, swahili) def featured_click(phrase): e = PHRASEBOOK.get(phrase) if e: return _kal(e["kalenjin"]), os.path.join(HERE, e["wav"]), _meta(phrase, e["swahili"]) kalenjin, swahili = translate_en_to_kal(phrase) return _kal(kalenjin), synthesize_kalenjin(kalenjin), _meta(phrase, swahili) # ── custom design ──────────────────────────────────────────────────────────── HEAD = """ """ CSS = """ :root { --ink:#2b2622; --cream:#fbf6ef; --terra:#cf5a26; --terra-d:#a8431a; --muted:#8a7d70; --line:#ece2d6; } .gradio-container { max-width: 680px !important; margin: auto !important; background: transparent !important; } body, .gradio-container { background: radial-gradient(1200px 500px at 50% -200px, #f7e6d6 0%, rgba(247,230,214,0) 60%), var(--cream) !important; font-family: 'Inter', system-ui, sans-serif; color: var(--ink); } footer { display:none !important; } /* hero */ .hero { text-align:center; padding: 28px 8px 6px; } .hero .eyebrow { font-family:'JetBrains Mono',monospace; font-size:12px; letter-spacing:.18em; color:var(--terra-d); text-transform:uppercase; margin-bottom:10px; } .hero h1 { font-family:'Fraunces',serif; font-weight:600; font-size:46px; line-height:1.02; margin:0 0 12px; color:var(--ink); } .hero h1 em { font-style:italic; color:var(--terra); } .hero .tag { font-size:17px; color:var(--ink); margin:0 0 6px; font-weight:500; } .hero .why { font-size:15px; color:var(--muted); max-width:460px; margin:6px auto 0; line-height:1.5; } /* cards */ #in-card, #out-card { background:#fff !important; border:1px solid var(--line) !important; border-radius:20px !important; box-shadow: 0 8px 30px rgba(120,80,40,.06) !important; padding:16px !important; margin-top:14px !important; } /* the big Kalenjin word */ .kal-word { font-family:'Fraunces',serif; font-weight:600; font-size:44px; line-height:1.1; color:var(--terra); text-align:center; padding:14px 6px 6px; word-break:break-word; } .kal-word .dim { color:var(--muted); font-style:italic; font-size:24px; font-weight:500; } .meta { text-align:center; color:var(--muted); font-size:14px; margin-top:2px; } .meta.dim { opacity:.8; font-style:italic; } /* primary button */ #go-btn { background:var(--terra) !important; color:#fff !important; border:none !important; border-radius:14px !important; font-weight:600 !important; font-size:17px !important; } #go-btn:hover { background:var(--terra-d) !important; } /* featured pills */ .pills .gr-button, .pills button { background:#fff !important; border:1px solid var(--line) !important; border-radius:999px !important; color:var(--ink) !important; font-weight:500 !important; } .pills .gr-button:hover, .pills button:hover { border-color:var(--terra) !important; color:var(--terra-d) !important; } .section-label { font-family:'JetBrains Mono',monospace; font-size:12px; letter-spacing:.12em; text-transform:uppercase; color:var(--muted); margin:18px 4px 2px; } .footnote { text-align:center; color:var(--muted); font-size:12.5px; line-height:1.6; margin:22px 6px 8px; } .footnote b { color:var(--ink); font-weight:600; } """ # Force light mode regardless of the device's system theme (the custom design is # built for light; dark mode made it look broken). Reload once into ?__theme=light. FORCE_LIGHT = """ () => { const u = new URL(window.location.href); if (u.searchParams.get('__theme') !== 'light') { u.searchParams.set('__theme', 'light'); window.location.replace(u.href); } } """ with gr.Blocks(title="Say It Back in Kalenjin", theme=gr.themes.Soft(primary_hue="orange"), css=CSS, head=HEAD, js=FORCE_LIGHT) as demo: gr.HTML( "
" "
[ English → Kalenjin · spoken ]
" "

Say It Back in Kalenjin

" "

Say a word in English. Hear it in Kalenjin.

" "

Kalenjin is slipping away in my family. I built this so we can " "learn it back — in a real Kalenjin voice (Cheps, a TTS I fine-tuned).

" "
" ) with gr.Group(elem_id="in-card"): mic = gr.Audio(sources=["microphone"], type="filepath", label="🎤 Say it in English") txt = gr.Textbox(label="…or type English", placeholder="e.g. good morning, how are you?") go = gr.Button("Say it back →", variant="primary", elem_id="go-btn", size="lg") with gr.Group(elem_id="out-card"): kal_out = gr.HTML(_kal("your Kalenjin will appear here")) audio_out = gr.Audio(label="🔊 Kalenjin voice (Cheps)", autoplay=True, interactive=False) meta_out = gr.HTML("") gr.HTML("
Tap a phrase to hear it
") with gr.Row(elem_classes="pills"): for _phrase in FEATURED: gr.Button(_phrase, size="sm").click( featured_click, inputs=gr.State(_phrase), outputs=[kal_out, audio_out, meta_out] ) gr.HTML( "
Tiny by design — every model is small: " "Whisper 74M (speech), NLLB 600M (translation), " "Cheps / Orpheus 3B (voice). All under 4B.
" "Models run on Modal · Built with Gradio for the Build Small hackathon 🇰🇪
" ) go.click(say_it_back, inputs=[txt, mic], outputs=[kal_out, audio_out, meta_out]) txt.submit(say_it_back, inputs=[txt, mic], outputs=[kal_out, audio_out, meta_out]) if __name__ == "__main__": demo.queue().launch()