spell / app.py
ramedde's picture
Update app.py
e8aeffc verified
Raw
History Blame Contribute Delete
24 kB
"""
Spelling Practice – Hugging Face Space
Multi-user safe via gr.State (JSON string per session).
"""
# ── Import gradio first, then patch the gradio_client 0.8 schema bug ──────────
# gradio_client 0.8 (bundled with gradio 4.40) crashes in get_api_info() when
# any component schema contains `additionalProperties: true` (a bool).
# The fix: wrap json_schema_to_python_type to catch the exception, patching
# both the gradio_client module AND the already-imported reference in gradio.blocks.
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 first, then patch the gradio_client 0.8 schema bug ──────────
# gradio_client 0.8 (bundled with gradio 4.40) crashes in get_api_info() when
# any component schema contains `additionalProperties: true` (a bool).
# The fix: wrap json_schema_to_python_type to catch the exception, patching
# both the gradio_client module AND the already-imported reference in gradio.blocks.
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)