"""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) + "
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 🇰🇪