File size: 5,546 Bytes
4eab58f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
"""
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()