| """ |
| Spelling Practice β Hugging Face Space |
| Multi-user safe via gr.State (JSON string per session). |
| """ |
|
|
| |
| |
| |
| |
| |
| import gradio as gr |
|
|
| try: |
| import gradio_client.utils as _gcu |
| import gradio.blocks as _gb |
| _orig_jspt = _gcu.json_schema_to_python_type |
| def _safe_jspt(schema): |
| try: |
| return _orig_jspt(schema) |
| except Exception: |
| return "Any" |
| _gcu.json_schema_to_python_type = _safe_jspt |
| _gb.client_utils.json_schema_to_python_type = _safe_jspt |
| except Exception: |
| pass |
| |
|
|
| import os |
| import glob |
| import random |
| import tempfile |
| import difflib |
| import json |
| import time |
| import threading |
|
|
| from gtts import gTTS |
|
|
| try: |
| from llama_cpp import Llama |
| _LLAMA_AVAILABLE = True |
| except ImportError: |
| _LLAMA_AVAILABLE = False |
|
|
| _llm = None |
| _llm_lock = threading.Lock() |
|
|
| def get_llm(): |
| global _llm |
| if _llm is not None: |
| return _llm |
| if not _LLAMA_AVAILABLE: |
| return None |
| with _llm_lock: |
| if _llm is not None: |
| return _llm |
| gguf_files = glob.glob("/app/*.gguf") + glob.glob("*.gguf") |
| if not gguf_files: |
| print("[spelling-app] No .gguf file found β AI tips disabled.") |
| return None |
| model_path = gguf_files[0] |
| print(f"[spelling-app] Loading model: {model_path}") |
| _llm = Llama(model_path=model_path, n_ctx=512, n_threads=2, |
| n_gpu_layers=int(os.getenv("N_GPU_LAYERS", "-1")), verbose=False) |
| print("[spelling-app] Model ready.") |
| return _llm |
|
|
| SENTENCES = { |
| "easy": [ |
| "The student read a book.", "I like to learn English.", |
| "The bank has no money.", "She has a new job.", |
| "He can read very fast.", "The map shows a river.", |
| "We write notes in class.", "Put the book on the desk.", |
| "I see a small farm.", "The goal is very clear.", |
| ], |
| "medium": [ |
| "The Southern colonies grew tobacco for Europe.", |
| "The workers found jobs in the factory.", |
| "He forgot to cite the original source.", |
| "They experienced a major economic boom.", |
| "My teacher helps me write better essays.", |
| "The market was very weak in December.", |
| "She received a new evening gown quickly.", |
| "We need to manage our time properly.", |
| "The group discussion went very well.", |
| "The online platform closes at midnight.", |
| ], |
| "hard": [ |
| "The government announced policies to address the downturn.", |
| "She has an extraordinary talent for analyzing economic literature.", |
| "The archaeologist made a fascinating discovery of ancient artifacts.", |
| "It is necessary to manage your time effectively.", |
| "The corporation was highly profitable after the merger.", |
| "He struggled to translate the complex economic metaphors.", |
| "The instructor approved the research outline immediately.", |
| "She demonstrated remarkable perseverance throughout her academic journey.", |
| "Maintaining a regular study schedule is very important.", |
| "The researcher examined the primary historical document.", |
| ], |
| "expert": [ |
| "The multinational corporation acknowledged the financial discrepancy.", |
| "His idiosyncratic research methodologies bewildered his academic colleagues.", |
| "The distinguished economist delivered a highly influential speech.", |
| "Technological advancements fundamentally transformed nineteenth-century transportation.", |
| "Maslow conceptualized self-actualization as the highest human need.", |
| "Agricultural consolidation overwhelmed small family farming operations.", |
| "Translating metaphorical expressions requires extraordinary cultural competence.", |
| "Plagiarism often leads to severe academic consequences.", |
| "The entrepreneur demonstrated extraordinary economic decision-making.", |
| "The researcher meticulously catalogued primary historical sources.", |
| ], |
| } |
|
|
| LEVELS = ["easy", "medium", "hard", "expert"] |
| LEVEL_LABELS = ["π’ Beginner", "π΅ Intermediate", "π Advanced", "π΄ Expert"] |
| PASS_REQ = 3 |
|
|
| def fresh_state_str(): |
| return json.dumps({"level": 0, "streak": 0, "sentence": "", |
| "score": 0, "total": 0, "history": []}) |
|
|
| def S(s): return json.loads(s) |
| def D(d): return json.dumps(d) |
|
|
| def _make_audio(text): |
| try: |
| tts = gTTS(text=text, lang="en", slow=True) |
| path = tempfile.mktemp(suffix=".mp3") |
| tts.save(path) |
| return path |
| except Exception as exc: |
| print(f"[spelling-app] gTTS error: {exc}") |
| return None |
|
|
| def _find_errors(correct, attempt): |
| c_words = correct.rstrip(".").lower().split() |
| a_words = attempt.rstrip(".").lower().split() |
| errors = [] |
| for tag, i1, i2, j1, j2 in difflib.SequenceMatcher(None, a_words, c_words).get_opcodes(): |
| if tag == "replace": |
| for w, r in zip(a_words[i1:i2], c_words[j1:j2]): |
| if w != r: errors.append((w, r)) |
| elif tag == "delete": |
| for w in a_words[i1:i2]: errors.append((w, "[extra word]")) |
| elif tag == "insert": |
| for r in c_words[j1:j2]: errors.append(("[missing]", r)) |
| return errors |
|
|
| def _ask_gemma(correct, attempt, errors): |
| llm = get_llm() |
| if not llm or not errors: return "" |
| error_list = "\n".join(f'- You wrote "{w}", correct is "{r}"' for w, r in errors) |
| prompt = (f"You are a friendly English spelling teacher.\n" |
| f"The student had to write: {correct}\n" |
| f"The student wrote: {attempt}\n" |
| f"Spelling mistakes:\n{error_list}\n\n" |
| "For each mistake, write one short sentence explaining the error " |
| "and one memory tip. Be encouraging. Keep it brief.") |
| out = llm(prompt, max_tokens=200, temperature=0.3, stop=["###"]) |
| return out["choices"][0]["text"].strip() |
|
|
| def _build_diff(correct, attempt): |
| c_words, a_words = correct.split(), attempt.split() |
| parts = [] |
| sm = difflib.SequenceMatcher( |
| None, [w.rstrip(".,").lower() for w in a_words], |
| [w.rstrip(".,").lower() for w in c_words]) |
| for tag, i1, i2, j1, j2 in sm.get_opcodes(): |
| if tag == "equal": parts.append(" ".join(a_words[i1:i2])) |
| elif tag == "replace": parts.append("~~"+" ".join(a_words[i1:i2])+"~~ **"+" ".join(c_words[j1:j2])+"**") |
| elif tag == "delete": parts.append("~~"+" ".join(a_words[i1:i2])+"~~") |
| elif tag == "insert": parts.append("**["+" ".join(c_words[j1:j2])+"]**") |
| return " ".join(parts) |
|
|
| def _level_info(state): |
| lvl = state["level"] |
| extra = f" Β· streak {state['streak']}/{PASS_REQ} to level up" if lvl < len(LEVELS)-1 else " Β· MAX LEVEL π" |
| return f"**{LEVEL_LABELS[lvl]}**{extra}" |
|
|
| def _score_info(state): |
| t, s = state["total"], state["score"] |
| return f"**Score: {s}/{t}** ({int(s/t*100) if t else 0}%)" |
|
|
| def new_sentence(ss): |
| state = S(ss) |
| sent = random.choice(SENTENCES[LEVELS[state["level"]]]) |
| state["sentence"] = sent |
| audio = _make_audio(sent) |
| hint = "π§ Listen carefully, then type what you heard and press **Submit**." |
| if audio is None: |
| hint = f"β οΈ Audio unavailable. Sentence: **{sent}**" |
| return audio, "", hint, _level_info(state), _score_info(state), D(state) |
|
|
| def submit_answer(attempt, ss): |
| state = S(ss) |
| if not state.get("sentence"): |
| return "", "β οΈ Press **New Sentence** first!", _level_info(state), _score_info(state), ss |
| correct = state["sentence"] |
| state["total"] += 1 |
| errors = _find_errors(correct, attempt) |
| clean_a = attempt.strip().rstrip(".").lower() |
| clean_c = correct.strip().rstrip(".").lower() |
| if not errors and clean_a == clean_c: |
| state["score"] += 1 |
| state["streak"] += 1 |
| msg = "β
**Correct!** Every word spelled perfectly." |
| if state["streak"] >= PASS_REQ and state["level"] < len(LEVELS)-1: |
| state["level"] += 1 |
| state["streak"] = 0 |
| msg += f"\n\nπ **Level up! You are now {LEVEL_LABELS[state['level']]}.**" |
| else: |
| state["streak"] = 0 |
| diff_str = _build_diff(correct, attempt) |
| gemma_tip = _ask_gemma(correct, attempt, errors) |
| ai_block = f"\n\n---\n\n**π‘ Tips:**\n\n{gemma_tip}" if gemma_tip else "" |
| msg = ("β **Your answer with corrections:**\n\n" + diff_str + |
| "\n\n*(~~strikethrough~~ = your error Β· **bold** = correct spelling)*" + ai_block) |
| state["history"].append({"correct": correct, "attempt": attempt, |
| "ok": not bool(errors) and clean_a == clean_c, |
| "level": LEVELS[state["level"]], "ts": time.strftime("%H:%M:%S")}) |
| state["sentence"] = "" |
| return attempt, msg, _level_info(state), _score_info(state), D(state) |
|
|
| def replay_audio(ss): |
| state = S(ss) |
| return _make_audio(state["sentence"]) if state.get("sentence") else None |
|
|
| def reset_game(_ss): |
| state = S(fresh_state_str()) |
| return None, "", "Game reset. Press **New Sentence** to start!", _level_info(state), _score_info(state), fresh_state_str() |
|
|
| def export_history(ss): |
| history = S(ss).get("history", []) |
| if not history: |
| return "No history yet." |
| lines = [] |
| for h in history: |
| ok = "β
" if h["ok"] else "β" |
| lines.append(f'{ok} [{h["level"]}] {h["ts"]} | {h["correct"]} β {h["attempt"]}') |
| return "\n".join(lines) |
|
|
| CSS = "footer { display: none !important; }" |
| _init = S(fresh_state_str()) |
|
|
| with gr.Blocks(title="Spelling Practice", css=CSS) as demo: |
| session = gr.State(fresh_state_str()) |
|
|
| gr.Markdown("# π Spelling Practice\nListen Β· Type Β· See errors Β· Get tips") |
|
|
| with gr.Row(): |
| level_md = gr.Markdown(_level_info(_init)) |
| score_md = gr.Markdown(_score_info(_init)) |
|
|
| with gr.Row(): |
| new_btn = gr.Button("π² New Sentence", variant="primary") |
| replay_btn = gr.Button("π Replay Audio", variant="secondary") |
| reset_btn = gr.Button("π Reset Game", variant="secondary") |
|
|
| audio_out = gr.Audio(label="Listen to the sentence", autoplay=True, interactive=False) |
| hint_md = gr.Markdown("Press **New Sentence** to begin.") |
| answer_box = gr.Textbox(label="Type the sentence you heard", |
| placeholder="Write the full sentence hereβ¦", lines=2) |
| submit_btn = gr.Button("β
Submit", variant="primary") |
| feedback = gr.Markdown("") |
|
|
| with gr.Accordion("π Session History", open=False): |
| export_btn = gr.Button("π Show history") |
| history_box = gr.Textbox(label="Your session", lines=10, interactive=False) |
|
|
| OUTS = [audio_out, answer_box, hint_md, level_md, score_md, session] |
| new_btn.click(new_sentence, [session], OUTS) |
| replay_btn.click(replay_audio, [session], [audio_out]) |
| submit_btn.click(submit_answer, [answer_box, session], [answer_box, feedback, level_md, score_md, session]) |
| answer_box.submit(submit_answer,[answer_box, session], [answer_box, feedback, level_md, score_md, session]) |
| reset_btn.click(reset_game, [session], OUTS) |
| export_btn.click(export_history,[session], [history_box]) |
|
|
| if __name__ == "__main__": |
| demo.launch(server_name="0.0.0.0", server_port=7860) |
| """ |
| Spelling Practice β Hugging Face Space |
| Multi-user safe via gr.State (JSON string per session). |
| """ |
|
|
| |
| |
| |
| |
| |
| import gradio as gr |
|
|
| try: |
| import gradio_client.utils as _gcu |
| import gradio.blocks as _gb |
| _orig_jspt = _gcu.json_schema_to_python_type |
| def _safe_jspt(schema): |
| try: |
| return _orig_jspt(schema) |
| except Exception: |
| return "Any" |
| _gcu.json_schema_to_python_type = _safe_jspt |
| _gb.client_utils.json_schema_to_python_type = _safe_jspt |
| except Exception: |
| pass |
| |
|
|
| import os |
| import glob |
| import random |
| import tempfile |
| import difflib |
| import json |
| import time |
| import threading |
|
|
| from gtts import gTTS |
|
|
| try: |
| from llama_cpp import Llama |
| _LLAMA_AVAILABLE = True |
| except ImportError: |
| _LLAMA_AVAILABLE = False |
|
|
| _llm = None |
| _llm_lock = threading.Lock() |
|
|
| def get_llm(): |
| global _llm |
| if _llm is not None: |
| return _llm |
| if not _LLAMA_AVAILABLE: |
| return None |
| with _llm_lock: |
| if _llm is not None: |
| return _llm |
| gguf_files = glob.glob("/app/*.gguf") + glob.glob("*.gguf") |
| if not gguf_files: |
| print("[spelling-app] No .gguf file found β AI tips disabled.") |
| return None |
| model_path = gguf_files[0] |
| print(f"[spelling-app] Loading model: {model_path}") |
| _llm = Llama(model_path=model_path, n_ctx=512, n_threads=2, |
| n_gpu_layers=int(os.getenv("N_GPU_LAYERS", "-1")), verbose=False) |
| print("[spelling-app] Model ready.") |
| return _llm |
|
|
| SENTENCES = { |
| "easy": [ |
| "The student read a book.", "I like to learn English.", |
| "The bank has no money.", "She has a new job.", |
| "He can read very fast.", "The map shows a river.", |
| "We write notes in class.", "Put the book on the desk.", |
| "I see a small farm.", "The goal is very clear.", |
| ], |
| "medium": [ |
| "The Southern colonies grew tobacco for Europe.", |
| "The workers found jobs in the factory.", |
| "He forgot to cite the original source.", |
| "They experienced a major economic boom.", |
| "My teacher helps me write better essays.", |
| "The market was very weak in December.", |
| "She received a new evening gown quickly.", |
| "We need to manage our time properly.", |
| "The group discussion went very well.", |
| "The online platform closes at midnight.", |
| ], |
| "hard": [ |
| "The government announced policies to address the downturn.", |
| "She has an extraordinary talent for analyzing economic literature.", |
| "The archaeologist made a fascinating discovery of ancient artifacts.", |
| "It is necessary to manage your time effectively.", |
| "The corporation was highly profitable after the merger.", |
| "He struggled to translate the complex economic metaphors.", |
| "The instructor approved the research outline immediately.", |
| "She demonstrated remarkable perseverance throughout her academic journey.", |
| "Maintaining a regular study schedule is very important.", |
| "The researcher examined the primary historical document.", |
| ], |
| "expert": [ |
| "The multinational corporation acknowledged the financial discrepancy.", |
| "His idiosyncratic research methodologies bewildered his academic colleagues.", |
| "The distinguished economist delivered a highly influential speech.", |
| "Technological advancements fundamentally transformed nineteenth-century transportation.", |
| "Maslow conceptualized self-actualization as the highest human need.", |
| "Agricultural consolidation overwhelmed small family farming operations.", |
| "Translating metaphorical expressions requires extraordinary cultural competence.", |
| "Plagiarism often leads to severe academic consequences.", |
| "The entrepreneur demonstrated extraordinary economic decision-making.", |
| "The researcher meticulously catalogued primary historical sources.", |
| ], |
| } |
|
|
| LEVELS = ["easy", "medium", "hard", "expert"] |
| LEVEL_LABELS = ["π’ Beginner", "π΅ Intermediate", "π Advanced", "π΄ Expert"] |
| PASS_REQ = 3 |
|
|
| def fresh_state_str(): |
| return json.dumps({"level": 0, "streak": 0, "sentence": "", |
| "score": 0, "total": 0, "history": []}) |
|
|
| def S(s): return json.loads(s) |
| def D(d): return json.dumps(d) |
|
|
| def _make_audio(text): |
| try: |
| tts = gTTS(text=text, lang="en", slow=True) |
| path = tempfile.mktemp(suffix=".mp3") |
| tts.save(path) |
| return path |
| except Exception as exc: |
| print(f"[spelling-app] gTTS error: {exc}") |
| return None |
|
|
| def _find_errors(correct, attempt): |
| c_words = correct.rstrip(".").lower().split() |
| a_words = attempt.rstrip(".").lower().split() |
| errors = [] |
| for tag, i1, i2, j1, j2 in difflib.SequenceMatcher(None, a_words, c_words).get_opcodes(): |
| if tag == "replace": |
| for w, r in zip(a_words[i1:i2], c_words[j1:j2]): |
| if w != r: errors.append((w, r)) |
| elif tag == "delete": |
| for w in a_words[i1:i2]: errors.append((w, "[extra word]")) |
| elif tag == "insert": |
| for r in c_words[j1:j2]: errors.append(("[missing]", r)) |
| return errors |
|
|
| def _ask_gemma(correct, attempt, errors): |
| llm = get_llm() |
| if not llm or not errors: return "" |
| error_list = "\n".join(f'- You wrote "{w}", correct is "{r}"' for w, r in errors) |
| prompt = (f"You are a friendly English spelling teacher.\n" |
| f"The student had to write: {correct}\n" |
| f"The student wrote: {attempt}\n" |
| f"Spelling mistakes:\n{error_list}\n\n" |
| "For each mistake, write one short sentence explaining the error " |
| "and one memory tip. Be encouraging. Keep it brief.") |
| out = llm(prompt, max_tokens=200, temperature=0.3, stop=["###"]) |
| return out["choices"][0]["text"].strip() |
|
|
| def _build_diff(correct, attempt): |
| c_words, a_words = correct.split(), attempt.split() |
| parts = [] |
| sm = difflib.SequenceMatcher( |
| None, [w.rstrip(".,").lower() for w in a_words], |
| [w.rstrip(".,").lower() for w in c_words]) |
| for tag, i1, i2, j1, j2 in sm.get_opcodes(): |
| if tag == "equal": parts.append(" ".join(a_words[i1:i2])) |
| elif tag == "replace": parts.append("~~"+" ".join(a_words[i1:i2])+"~~ **"+" ".join(c_words[j1:j2])+"**") |
| elif tag == "delete": parts.append("~~"+" ".join(a_words[i1:i2])+"~~") |
| elif tag == "insert": parts.append("**["+" ".join(c_words[j1:j2])+"]**") |
| return " ".join(parts) |
|
|
| def _level_info(state): |
| lvl = state["level"] |
| extra = f" Β· streak {state['streak']}/{PASS_REQ} to level up" if lvl < len(LEVELS)-1 else " Β· MAX LEVEL π" |
| return f"**{LEVEL_LABELS[lvl]}**{extra}" |
|
|
| def _score_info(state): |
| t, s = state["total"], state["score"] |
| return f"**Score: {s}/{t}** ({int(s/t*100) if t else 0}%)" |
|
|
| def new_sentence(ss): |
| state = S(ss) |
| sent = random.choice(SENTENCES[LEVELS[state["level"]]]) |
| state["sentence"] = sent |
| audio = _make_audio(sent) |
| hint = "π§ Listen carefully, then type what you heard and press **Submit**." |
| if audio is None: |
| hint = f"β οΈ Audio unavailable. Sentence: **{sent}**" |
| return audio, "", hint, _level_info(state), _score_info(state), D(state) |
|
|
| def submit_answer(attempt, ss): |
| state = S(ss) |
| if not state.get("sentence"): |
| return "", "β οΈ Press **New Sentence** first!", _level_info(state), _score_info(state), ss |
| correct = state["sentence"] |
| state["total"] += 1 |
| errors = _find_errors(correct, attempt) |
| clean_a = attempt.strip().rstrip(".").lower() |
| clean_c = correct.strip().rstrip(".").lower() |
| if not errors and clean_a == clean_c: |
| state["score"] += 1 |
| state["streak"] += 1 |
| msg = "β
**Correct!** Every word spelled perfectly." |
| if state["streak"] >= PASS_REQ and state["level"] < len(LEVELS)-1: |
| state["level"] += 1 |
| state["streak"] = 0 |
| msg += f"\n\nπ **Level up! You are now {LEVEL_LABELS[state['level']]}.**" |
| else: |
| state["streak"] = 0 |
| diff_str = _build_diff(correct, attempt) |
| gemma_tip = _ask_gemma(correct, attempt, errors) |
| ai_block = f"\n\n---\n\n**π‘ Tips:**\n\n{gemma_tip}" if gemma_tip else "" |
| msg = ("β **Your answer with corrections:**\n\n" + diff_str + |
| "\n\n*(~~strikethrough~~ = your error Β· **bold** = correct spelling)*" + ai_block) |
| state["history"].append({"correct": correct, "attempt": attempt, |
| "ok": not bool(errors) and clean_a == clean_c, |
| "level": LEVELS[state["level"]], "ts": time.strftime("%H:%M:%S")}) |
| state["sentence"] = "" |
| return attempt, msg, _level_info(state), _score_info(state), D(state) |
|
|
| def replay_audio(ss): |
| state = S(ss) |
| return _make_audio(state["sentence"]) if state.get("sentence") else None |
|
|
| def reset_game(_ss): |
| state = S(fresh_state_str()) |
| return None, "", "Game reset. Press **New Sentence** to start!", _level_info(state), _score_info(state), fresh_state_str() |
|
|
| def export_history(ss): |
| history = S(ss).get("history", []) |
| if not history: |
| return "No history yet." |
| lines = [] |
| for h in history: |
| ok = "β
" if h["ok"] else "β" |
| lines.append(f'{ok} [{h["level"]}] {h["ts"]} | {h["correct"]} β {h["attempt"]}') |
| return "\n".join(lines) |
|
|
| CSS = "footer { display: none !important; }" |
| _init = S(fresh_state_str()) |
|
|
| with gr.Blocks(title="Spelling Practice", css=CSS) as demo: |
| session = gr.State(fresh_state_str()) |
|
|
| gr.Markdown("# π Spelling Practice\nListen Β· Type Β· See errors Β· Get tips") |
|
|
| with gr.Row(): |
| level_md = gr.Markdown(_level_info(_init)) |
| score_md = gr.Markdown(_score_info(_init)) |
|
|
| with gr.Row(): |
| new_btn = gr.Button("π² New Sentence", variant="primary") |
| replay_btn = gr.Button("π Replay Audio", variant="secondary") |
| reset_btn = gr.Button("π Reset Game", variant="secondary") |
|
|
| audio_out = gr.Audio(label="Listen to the sentence", autoplay=True, interactive=False) |
| hint_md = gr.Markdown("Press **New Sentence** to begin.") |
| answer_box = gr.Textbox(label="Type the sentence you heard", |
| placeholder="Write the full sentence hereβ¦", lines=2) |
| submit_btn = gr.Button("β
Submit", variant="primary") |
| feedback = gr.Markdown("") |
|
|
| with gr.Accordion("π Session History", open=False): |
| export_btn = gr.Button("π Show history") |
| history_box = gr.Textbox(label="Your session", lines=10, interactive=False) |
|
|
| OUTS = [audio_out, answer_box, hint_md, level_md, score_md, session] |
| new_btn.click(new_sentence, [session], OUTS) |
| replay_btn.click(replay_audio, [session], [audio_out]) |
| submit_btn.click(submit_answer, [answer_box, session], [answer_box, feedback, level_md, score_md, session]) |
| answer_box.submit(submit_answer,[answer_box, session], [answer_box, feedback, level_md, score_md, session]) |
| reset_btn.click(reset_game, [session], OUTS) |
| export_btn.click(export_history,[session], [history_box]) |
|
|
| if __name__ == "__main__": |
| demo.launch(server_name="0.0.0.0", server_port=7860) |
|
|
|
|
|
|