ramedde commited on
Commit
1f6aac5
Β·
verified Β·
1 Parent(s): ead3aa0

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +89 -151
app.py CHANGED
@@ -1,9 +1,24 @@
1
  """
2
  Spelling Practice – Hugging Face Space
3
- Multi-user safe: session state is stored as a JSON string in gr.State,
4
- which gradio 4.40 / gradio_client 0.8 can safely introspect.
5
  """
6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  import os
8
  import glob
9
  import random
@@ -23,7 +38,6 @@ try:
23
  except ImportError:
24
  _LLAMA_AVAILABLE = False
25
 
26
- # ── Shared model (loaded once, read-only during inference) ─────────────────────
27
  _llm = None
28
  _llm_lock = threading.Lock()
29
 
@@ -42,13 +56,8 @@ def get_llm():
42
  return None
43
  model_path = gguf_files[0]
44
  print(f"[spelling-app] Loading model: {model_path}")
45
- _llm = Llama(
46
- model_path=model_path,
47
- n_ctx=512,
48
- n_threads=2,
49
- n_gpu_layers=int(os.getenv("N_GPU_LAYERS", "-1")),
50
- verbose=False,
51
- )
52
  print("[spelling-app] Model ready.")
53
  return _llm
54
 
@@ -108,31 +117,20 @@ LEVELS = ["easy", "medium", "hard", "expert"]
108
  LEVEL_LABELS = ["🟒 Beginner", "πŸ”΅ Intermediate", "🟠 Advanced", "πŸ”΄ Expert"]
109
  PASS_REQ = 3
110
 
111
-
112
- # ── State helpers (serialised as JSON string to avoid gradio_client schema bug) ─
113
 
114
  def fresh_state_str():
115
- return json.dumps({
116
- "level": 0,
117
- "streak": 0,
118
- "sentence": "",
119
- "score": 0,
120
- "total": 0,
121
- "history": [],
122
- })
123
 
124
- def load(s):
125
- return json.loads(s)
126
 
127
- def dump(d):
128
- return json.dumps(d)
129
-
130
-
131
- # ── Core helpers ───────────────────────────────────────────────────────────────
132
 
133
  def _make_audio(text):
134
  try:
135
- tts = gTTS(text=text, lang="en", slow=True)
136
  path = tempfile.mktemp(suffix=".mp3")
137
  tts.save(path)
138
  return path
@@ -140,184 +138,126 @@ def _make_audio(text):
140
  print(f"[spelling-app] gTTS error: {exc}")
141
  return None
142
 
143
-
144
  def _find_errors(correct, attempt):
145
  c_words = correct.rstrip(".").lower().split()
146
  a_words = attempt.rstrip(".").lower().split()
147
  errors = []
148
- matcher = difflib.SequenceMatcher(None, a_words, c_words)
149
- for tag, i1, i2, j1, j2 in matcher.get_opcodes():
150
  if tag == "replace":
151
  for w, r in zip(a_words[i1:i2], c_words[j1:j2]):
152
- if w != r:
153
- errors.append((w, r))
154
  elif tag == "delete":
155
- for w in a_words[i1:i2]:
156
- errors.append((w, "[extra word]"))
157
  elif tag == "insert":
158
- for r in c_words[j1:j2]:
159
- errors.append(("[missing]", r))
160
  return errors
161
 
162
-
163
  def _ask_gemma(correct, attempt, errors):
164
  llm = get_llm()
165
- if not llm or not errors:
166
- return ""
167
- error_list = "\n".join(
168
- f'- You wrote "{w}", correct is "{r}"' for w, r in errors
169
- )
170
- prompt = (
171
- "You are a friendly English spelling teacher.\n"
172
- f"The student had to write: {correct}\n"
173
- f"The student wrote: {attempt}\n"
174
- f"Spelling mistakes:\n{error_list}\n\n"
175
- "For each mistake, write one short sentence explaining the error "
176
- "and one memory tip. Be encouraging. Keep it brief."
177
- )
178
  out = llm(prompt, max_tokens=200, temperature=0.3, stop=["###"])
179
  return out["choices"][0]["text"].strip()
180
 
181
-
182
  def _build_diff(correct, attempt):
183
- c_words = correct.split()
184
- a_words = attempt.split()
185
- parts = []
186
  sm = difflib.SequenceMatcher(
187
  None,
188
  [w.rstrip(".,").lower() for w in a_words],
189
- [w.rstrip(".,").lower() for w in c_words],
190
- )
191
  for tag, i1, i2, j1, j2 in sm.get_opcodes():
192
- if tag == "equal":
193
- parts.append(" ".join(a_words[i1:i2]))
194
- elif tag == "replace":
195
- parts.append(
196
- "~~" + " ".join(a_words[i1:i2])
197
- + "~~ **" + " ".join(c_words[j1:j2]) + "**"
198
- )
199
- elif tag == "delete":
200
- parts.append("~~" + " ".join(a_words[i1:i2]) + "~~")
201
- elif tag == "insert":
202
- parts.append("**[" + " ".join(c_words[j1:j2]) + "]**")
203
  return " ".join(parts)
204
 
205
-
206
  def _level_info(state):
207
- lvl = state["level"]
208
- label = LEVEL_LABELS[lvl]
209
- extra = (
210
- f" Β· streak {state['streak']}/{PASS_REQ} to level up"
211
- if lvl < len(LEVELS) - 1
212
- else " Β· MAX LEVEL πŸ†"
213
- )
214
- return f"**{label}**{extra}"
215
-
216
 
217
  def _score_info(state):
218
- t = state["total"]
219
- s = state["score"]
220
- pct = int(s / t * 100) if t else 0
221
- return f"**Score: {s}/{t}** ({pct}%)"
222
 
 
223
 
224
- # ── Event handlers (state_str in, state_str out) ───────────────────────────────
225
-
226
- def new_sentence(state_str):
227
- state = load(state_str)
228
  sent = random.choice(SENTENCES[LEVELS[state["level"]]])
229
  state["sentence"] = sent
230
  audio = _make_audio(sent)
231
  hint = "🎧 Listen carefully, then type what you heard and press **Submit**."
232
  if audio is None:
233
- hint = f"⚠️ Audio unavailable. Sentence: **{sent}** *(type it below)*"
234
- return audio, "", hint, _level_info(state), _score_info(state), dump(state)
235
-
236
 
237
- def submit_answer(attempt, state_str):
238
- state = load(state_str)
239
  if not state.get("sentence"):
240
- return "", "⚠️ Press **New Sentence** first!", _level_info(state), _score_info(state), dump(state)
241
-
242
  correct = state["sentence"]
243
  state["total"] += 1
244
  errors = _find_errors(correct, attempt)
245
  clean_a = attempt.strip().rstrip(".").lower()
246
  clean_c = correct.strip().rstrip(".").lower()
247
-
248
  if not errors and clean_a == clean_c:
249
  state["score"] += 1
250
  state["streak"] += 1
251
  msg = "βœ… **Correct!** Every word spelled perfectly."
252
- if state["streak"] >= PASS_REQ and state["level"] < len(LEVELS) - 1:
253
  state["level"] += 1
254
  state["streak"] = 0
255
- new_label = LEVEL_LABELS[state["level"]]
256
- msg += f"\n\nπŸŽ‰ **Level up! You are now {new_label}.**"
257
  else:
258
  state["streak"] = 0
259
  diff_str = _build_diff(correct, attempt)
260
  gemma_tip = _ask_gemma(correct, attempt, errors)
261
  ai_block = f"\n\n---\n\n**πŸ’‘ Tips:**\n\n{gemma_tip}" if gemma_tip else ""
262
- msg = (
263
- "❌ **Your answer with corrections:**\n\n"
264
- + diff_str
265
- + "\n\n*(~~strikethrough~~ = your error Β· **bold** = correct spelling)*"
266
- + ai_block
267
- )
268
-
269
- state["history"].append({
270
- "correct": correct,
271
- "attempt": attempt,
272
- "ok": not bool(errors) and clean_a == clean_c,
273
- "level": LEVELS[state["level"]],
274
- "ts": time.strftime("%H:%M:%S"),
275
- })
276
  state["sentence"] = ""
277
- return attempt, msg, _level_info(state), _score_info(state), dump(state)
278
-
279
-
280
- def replay_audio(state_str):
281
- state = load(state_str)
282
- if state.get("sentence"):
283
- return _make_audio(state["sentence"])
284
- return None
285
 
 
 
 
286
 
287
- def reset_game(state_str):
288
- state = load(fresh_state_str())
289
  return None, "", "Game reset. Press **New Sentence** to start!", _level_info(state), _score_info(state), fresh_state_str()
290
 
291
-
292
- def export_history(state_str):
293
- state = load(state_str)
294
- history = state.get("history", [])
295
  if not history:
296
- return None
297
- path = tempfile.mktemp(suffix=".json")
298
- with open(path, "w") as f:
299
- json.dump(history, f, indent=2, ensure_ascii=False)
300
- return path
301
-
302
 
303
  # ── UI ─────────────────────────────────────────────────────────────────────────
304
 
305
- CSS = """
306
- body { font-family: 'Segoe UI', sans-serif; }
307
- footer { display: none !important; }
308
- """
309
-
310
- _init = load(fresh_state_str())
311
 
312
  with gr.Blocks(title="Spelling Practice", css=CSS) as demo:
313
 
314
- # gr.State holds a plain string – safe for gradio 4.40 / gradio_client 0.8
315
- session = gr.State(fresh_state_str())
316
 
317
- gr.Markdown(
318
- "# πŸ“ Spelling Practice\n"
319
- "Listen to the sentence Β· Type it Β· See errors highlighted Β· Get AI tips"
320
- )
321
 
322
  with gr.Row():
323
  level_md = gr.Markdown(_level_info(_init))
@@ -330,17 +270,14 @@ with gr.Blocks(title="Spelling Practice", css=CSS) as demo:
330
 
331
  audio_out = gr.Audio(label="Listen to the sentence", autoplay=True, interactive=False)
332
  hint_md = gr.Markdown("Press **New Sentence** to begin.")
333
- answer_box = gr.Textbox(
334
- label="Type the sentence you heard",
335
- placeholder="Write the full sentence here…",
336
- lines=2,
337
- )
338
  submit_btn = gr.Button("βœ… Submit", variant="primary")
339
  feedback = gr.Markdown("")
340
 
341
  with gr.Accordion("πŸ“Š Session History", open=False):
342
- export_btn = gr.Button("⬇️ Export history (JSON)")
343
- history_file = gr.File(label="Download", visible=True)
344
 
345
  OUTS = [audio_out, answer_box, hint_md, level_md, score_md, session]
346
 
@@ -349,7 +286,8 @@ with gr.Blocks(title="Spelling Practice", css=CSS) as demo:
349
  submit_btn.click(submit_answer, [answer_box, session], [answer_box, feedback, level_md, score_md, session])
350
  answer_box.submit(submit_answer,[answer_box, session], [answer_box, feedback, level_md, score_md, session])
351
  reset_btn.click(reset_game, [session], OUTS)
352
- export_btn.click(export_history,[session], [history_file])
353
 
354
  if __name__ == "__main__":
355
  demo.launch(server_name="0.0.0.0", server_port=7860)
 
 
1
  """
2
  Spelling Practice – Hugging Face Space
 
 
3
  """
4
 
5
+ # ── Monkey-patch gradio_client 0.8 bug: `"const" in <bool>` crash ────────────
6
+ # gradio_client 0.8 (shipped with gradio 4.40) has a bug in get_type() where
7
+ # it does `if "const" in schema` but schema can be a bool (from
8
+ # additionalProperties:true in gr.File's JSON schema). Patch it before
9
+ # anything imports gradio.
10
+ try:
11
+ import gradio_client.utils as _gcu
12
+ _orig_get_type = _gcu.get_type # type: ignore[attr-defined]
13
+ def _safe_get_type(schema):
14
+ if not isinstance(schema, dict):
15
+ return "Any"
16
+ return _orig_get_type(schema)
17
+ _gcu.get_type = _safe_get_type # type: ignore[attr-defined]
18
+ except Exception:
19
+ pass
20
+ # ─────────────────────────────────────────────────────────────────────────────
21
+
22
  import os
23
  import glob
24
  import random
 
38
  except ImportError:
39
  _LLAMA_AVAILABLE = False
40
 
 
41
  _llm = None
42
  _llm_lock = threading.Lock()
43
 
 
56
  return None
57
  model_path = gguf_files[0]
58
  print(f"[spelling-app] Loading model: {model_path}")
59
+ _llm = Llama(model_path=model_path, n_ctx=512, n_threads=2,
60
+ n_gpu_layers=int(os.getenv("N_GPU_LAYERS", "-1")), verbose=False)
 
 
 
 
 
61
  print("[spelling-app] Model ready.")
62
  return _llm
63
 
 
117
  LEVEL_LABELS = ["🟒 Beginner", "πŸ”΅ Intermediate", "🟠 Advanced", "πŸ”΄ Expert"]
118
  PASS_REQ = 3
119
 
120
+ # ── State (JSON string – safe for gradio_client 0.8 schema introspection) ─────
 
121
 
122
  def fresh_state_str():
123
+ return json.dumps({"level": 0, "streak": 0, "sentence": "",
124
+ "score": 0, "total": 0, "history": []})
 
 
 
 
 
 
125
 
126
+ def S(s): return json.loads(s) # decode
127
+ def D(d): return json.dumps(d) # encode
128
 
129
+ # ── Helpers ────────────────────────────────────────────────────────────────────
 
 
 
 
130
 
131
  def _make_audio(text):
132
  try:
133
+ tts = gTTS(text=text, lang="en", slow=True)
134
  path = tempfile.mktemp(suffix=".mp3")
135
  tts.save(path)
136
  return path
 
138
  print(f"[spelling-app] gTTS error: {exc}")
139
  return None
140
 
 
141
  def _find_errors(correct, attempt):
142
  c_words = correct.rstrip(".").lower().split()
143
  a_words = attempt.rstrip(".").lower().split()
144
  errors = []
145
+ for tag, i1, i2, j1, j2 in difflib.SequenceMatcher(None, a_words, c_words).get_opcodes():
 
146
  if tag == "replace":
147
  for w, r in zip(a_words[i1:i2], c_words[j1:j2]):
148
+ if w != r: errors.append((w, r))
 
149
  elif tag == "delete":
150
+ for w in a_words[i1:i2]: errors.append((w, "[extra word]"))
 
151
  elif tag == "insert":
152
+ for r in c_words[j1:j2]: errors.append(("[missing]", r))
 
153
  return errors
154
 
 
155
  def _ask_gemma(correct, attempt, errors):
156
  llm = get_llm()
157
+ if not llm or not errors: return ""
158
+ error_list = "\n".join(f'- You wrote "{w}", correct is "{r}"' for w, r in errors)
159
+ prompt = (f"You are a friendly English spelling teacher.\n"
160
+ f"The student had to write: {correct}\n"
161
+ f"The student wrote: {attempt}\n"
162
+ f"Spelling mistakes:\n{error_list}\n\n"
163
+ "For each mistake, write one short sentence explaining the error "
164
+ "and one memory tip. Be encouraging. Keep it brief.")
 
 
 
 
 
165
  out = llm(prompt, max_tokens=200, temperature=0.3, stop=["###"])
166
  return out["choices"][0]["text"].strip()
167
 
 
168
  def _build_diff(correct, attempt):
169
+ c_words, a_words = correct.split(), attempt.split()
170
+ parts = []
 
171
  sm = difflib.SequenceMatcher(
172
  None,
173
  [w.rstrip(".,").lower() for w in a_words],
174
+ [w.rstrip(".,").lower() for w in c_words])
 
175
  for tag, i1, i2, j1, j2 in sm.get_opcodes():
176
+ if tag == "equal": parts.append(" ".join(a_words[i1:i2]))
177
+ elif tag == "replace": parts.append("~~"+" ".join(a_words[i1:i2])+"~~ **"+" ".join(c_words[j1:j2])+"**")
178
+ elif tag == "delete": parts.append("~~"+" ".join(a_words[i1:i2])+"~~")
179
+ elif tag == "insert": parts.append("**["+" ".join(c_words[j1:j2])+"]**")
 
 
 
 
 
 
 
180
  return " ".join(parts)
181
 
 
182
  def _level_info(state):
183
+ lvl = state["level"]
184
+ extra = f" Β· streak {state['streak']}/{PASS_REQ} to level up" if lvl < len(LEVELS)-1 else " Β· MAX LEVEL πŸ†"
185
+ return f"**{LEVEL_LABELS[lvl]}**{extra}"
 
 
 
 
 
 
186
 
187
  def _score_info(state):
188
+ t, s = state["total"], state["score"]
189
+ return f"**Score: {s}/{t}** ({int(s/t*100) if t else 0}%)"
 
 
190
 
191
+ # ── Event handlers ─────────────────────────────────────────────────────────────
192
 
193
+ def new_sentence(ss):
194
+ state = S(ss)
 
 
195
  sent = random.choice(SENTENCES[LEVELS[state["level"]]])
196
  state["sentence"] = sent
197
  audio = _make_audio(sent)
198
  hint = "🎧 Listen carefully, then type what you heard and press **Submit**."
199
  if audio is None:
200
+ hint = f"⚠️ Audio unavailable. Sentence: **{sent}**"
201
+ return audio, "", hint, _level_info(state), _score_info(state), D(state)
 
202
 
203
+ def submit_answer(attempt, ss):
204
+ state = S(ss)
205
  if not state.get("sentence"):
206
+ return "", "⚠️ Press **New Sentence** first!", _level_info(state), _score_info(state), ss
 
207
  correct = state["sentence"]
208
  state["total"] += 1
209
  errors = _find_errors(correct, attempt)
210
  clean_a = attempt.strip().rstrip(".").lower()
211
  clean_c = correct.strip().rstrip(".").lower()
 
212
  if not errors and clean_a == clean_c:
213
  state["score"] += 1
214
  state["streak"] += 1
215
  msg = "βœ… **Correct!** Every word spelled perfectly."
216
+ if state["streak"] >= PASS_REQ and state["level"] < len(LEVELS)-1:
217
  state["level"] += 1
218
  state["streak"] = 0
219
+ msg += f"\n\nπŸŽ‰ **Level up! You are now {LEVEL_LABELS[state['level']]}.**"
 
220
  else:
221
  state["streak"] = 0
222
  diff_str = _build_diff(correct, attempt)
223
  gemma_tip = _ask_gemma(correct, attempt, errors)
224
  ai_block = f"\n\n---\n\n**πŸ’‘ Tips:**\n\n{gemma_tip}" if gemma_tip else ""
225
+ msg = ("❌ **Your answer with corrections:**\n\n" + diff_str +
226
+ "\n\n*(~~strikethrough~~ = your error Β· **bold** = correct spelling)*" + ai_block)
227
+ state["history"].append({"correct": correct, "attempt": attempt,
228
+ "ok": not bool(errors) and clean_a == clean_c,
229
+ "level": LEVELS[state["level"]], "ts": time.strftime("%H:%M:%S")})
 
 
 
 
 
 
 
 
 
230
  state["sentence"] = ""
231
+ return attempt, msg, _level_info(state), _score_info(state), D(state)
 
 
 
 
 
 
 
232
 
233
+ def replay_audio(ss):
234
+ state = S(ss)
235
+ return _make_audio(state["sentence"]) if state.get("sentence") else None
236
 
237
+ def reset_game(_ss):
238
+ state = S(fresh_state_str())
239
  return None, "", "Game reset. Press **New Sentence** to start!", _level_info(state), _score_info(state), fresh_state_str()
240
 
241
+ def export_history(ss):
242
+ history = S(ss).get("history", [])
 
 
243
  if not history:
244
+ return "No history yet."
245
+ lines = ["Correct | Your answer | Level | Time | Result"]
246
+ for h in history:
247
+ ok = "βœ…" if h["ok"] else "❌"
248
+ lines.append(f'{ok} [{h["level"]}] {h["ts"]} | {h["correct"]} β†’ {h["attempt"]}')
249
+ return "\n".join(lines)
250
 
251
  # ── UI ─────────────────────────────────────────────────────────────────────────
252
 
253
+ CSS = "footer { display: none !important; }"
254
+ _init = S(fresh_state_str())
 
 
 
 
255
 
256
  with gr.Blocks(title="Spelling Practice", css=CSS) as demo:
257
 
258
+ session = gr.State(fresh_state_str()) # plain str – no schema crash
 
259
 
260
+ gr.Markdown("# πŸ“ Spelling Practice\nListen Β· Type Β· See errors Β· Get tips")
 
 
 
261
 
262
  with gr.Row():
263
  level_md = gr.Markdown(_level_info(_init))
 
270
 
271
  audio_out = gr.Audio(label="Listen to the sentence", autoplay=True, interactive=False)
272
  hint_md = gr.Markdown("Press **New Sentence** to begin.")
273
+ answer_box = gr.Textbox(label="Type the sentence you heard",
274
+ placeholder="Write the full sentence here…", lines=2)
 
 
 
275
  submit_btn = gr.Button("βœ… Submit", variant="primary")
276
  feedback = gr.Markdown("")
277
 
278
  with gr.Accordion("πŸ“Š Session History", open=False):
279
+ export_btn = gr.Button("πŸ“‹ Show history")
280
+ history_box = gr.Textbox(label="Your session", lines=10, interactive=False)
281
 
282
  OUTS = [audio_out, answer_box, hint_md, level_md, score_md, session]
283
 
 
286
  submit_btn.click(submit_answer, [answer_box, session], [answer_box, feedback, level_md, score_md, session])
287
  answer_box.submit(submit_answer,[answer_box, session], [answer_box, feedback, level_md, score_md, session])
288
  reset_btn.click(reset_game, [session], OUTS)
289
+ export_btn.click(export_history,[session], [history_box])
290
 
291
  if __name__ == "__main__":
292
  demo.launch(server_name="0.0.0.0", server_port=7860)
293
+