""" Hugging Face Space entrypoint: Hindi / English reading & pronunciation coach. Set ASR_BACKEND (and GROQ_API_KEY as a Space secret) in Settings -> Variables. """ from __future__ import annotations import hashlib import os import tempfile import gradio as gr import pandas as pd from asr_backends import get_backend from scoring import score LANGS = {"हिंदी (Hindi)": "hi", "English": "en"} SAMPLES = { "hi": "सूरज सुबह जल्दी उठता है और अपनी किताब पढ़ता है। उसे कहानियाँ बहुत पसंद हैं।", "en": "The quick brown fox jumps over the lazy dog while the children watch quietly.", } # --------------------------------------------------------------------------- # TTS -- cached, because on a Space every gTTS call is a network round trip # --------------------------------------------------------------------------- TTS_DIR = os.path.join(tempfile.gettempdir(), "coach_tts") os.makedirs(TTS_DIR, exist_ok=True) def speak(text: str, lang_label: str, slow: bool): if not (text or "").strip(): raise gr.Error("Type or paste a passage first.") lang = LANGS[lang_label] key = hashlib.sha1(f"{lang}|{slow}|{text}".encode()).hexdigest()[:20] path = os.path.join(TTS_DIR, f"{key}.mp3") if not os.path.exists(path): from gtts import gTTS try: gTTS(text=text, lang=lang, slow=slow).save(path) except Exception as exc: raise gr.Error(f"Text-to-speech unavailable: {exc}") from exc return path def fill_sample(lang_label: str) -> str: return SAMPLES[LANGS[lang_label]] # --------------------------------------------------------------------------- # Evaluation # --------------------------------------------------------------------------- EMPTY = pd.DataFrame(columns=["अपेक्षित / Expected", "सुना गया / Heard", "प्रकार / Error type", "समानता / Similarity"]) def evaluate(audio_path, passage, lang_label, lenient_decoding): if not audio_path: return {"error": "No recording received — record or upload audio first."}, EMPTY if not (passage or "").strip(): return {"error": "Paste the passage to read first."}, EMPTY lang = LANGS[lang_label] try: backend = get_backend() hint = passage.strip() if lenient_decoding else None tr = backend.transcribe(audio_path, lang, hint=hint) except Exception as exc: return {"error": f"{type(exc).__name__}: {exc}"}, EMPTY if not tr.text: return {"error": "Nothing was transcribed. Check the mic level and try again."}, EMPTY return score(passage, tr, lang) # --------------------------------------------------------------------------- # UI # --------------------------------------------------------------------------- def build() -> gr.Blocks: with gr.Blocks(title="Reading & Pronunciation Coach") as app: gr.Markdown("## 🗣️ Reading & Pronunciation Coach — हिंदी / English") status = gr.Markdown("Resolving ASR backend…") with gr.Row(): lang = gr.Dropdown(list(LANGS), value="हिंदी (Hindi)", label="Language", scale=2) sample_btn = gr.Button("Load sample passage", scale=1) passage = gr.Textbox( label="Passage to read", lines=4, placeholder="यहाँ हिंदी टेक्स्ट लिखें… / Paste English text here…", ) with gr.Row(): slow = gr.Checkbox(label="Slow speech", value=False, scale=1) listen = gr.Button("🔊 Listen", scale=1) tts_audio = gr.Audio(label="Model reading", type="filepath") gr.Markdown("### 🎤 Now read it aloud") mic = gr.Audio(sources=["microphone", "upload"], type="filepath", label="Your recording") lenient = gr.Checkbox( label="Lenient decoding — biases the ASR toward the passage. " "Scores look better but real mistakes get hidden. Leave off for assessment.", value=False, ) submit = gr.Button("✅ Check my reading", variant="primary") metrics = gr.JSON(label="Results") table = gr.Dataframe(label="गलती तालिका / Error table", wrap=True) gr.Markdown( "**Reading the scores.** *Word accuracy* is strict: a word counts only if it " "matches exactly. *Lenient score* gives partial credit by similarity, so a " "near-miss on a hard word is not treated like a skipped line. *WER* is the " "standard ASR metric and includes extra words, so it can exceed the accuracy gap." ) def backend_line(): try: b = get_backend() extra = "" if b.supports_word_confidence else \ " · per-word confidence unavailable on this backend" return f"**Backend:** {b.describe()}{extra}" except Exception as exc: return f"⚠️ **Backend not ready:** {exc}" app.load(backend_line, None, status) sample_btn.click(fill_sample, lang, passage) listen.click(speak, [passage, lang, slow], tts_audio) submit.click(evaluate, [mic, passage, lang, lenient], [metrics, table]) return app if __name__ == "__main__": build().queue(max_size=12).launch()