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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +50 -40
app.py CHANGED
@@ -1,6 +1,7 @@
1
  """
2
  Spelling Practice – Hugging Face Space
3
- Multi-user safe: all mutable state lives in gr.State (one copy per browser session).
 
4
  """
5
 
6
  import os
@@ -11,7 +12,6 @@ import difflib
11
  import json
12
  import time
13
  import threading
14
- from pathlib import Path
15
 
16
  import gradio as gr
17
  from gtts import gTTS
@@ -23,12 +23,11 @@ try:
23
  except ImportError:
24
  _LLAMA_AVAILABLE = False
25
 
26
- # ── Model loading (shared across all users, loaded once at startup) ────────────
27
  _llm = None
28
  _llm_lock = threading.Lock()
29
 
30
  def get_llm():
31
- """Return the shared Llama instance, loading it on first call."""
32
  global _llm
33
  if _llm is not None:
34
  return _llm
@@ -110,17 +109,23 @@ LEVEL_LABELS = ["🟒 Beginner", "πŸ”΅ Intermediate", "🟠 Advanced", "πŸ”΄ Exp
110
  PASS_REQ = 3
111
 
112
 
113
- # ── Per-user state ─────────────────────────────────────────────────────────────
114
 
115
- def fresh_state():
116
- return {
117
  "level": 0,
118
  "streak": 0,
119
  "sentence": "",
120
  "score": 0,
121
  "total": 0,
122
  "history": [],
123
- }
 
 
 
 
 
 
124
 
125
 
126
  # ── Core helpers ───────────────────────────────────────────────────────────────
@@ -216,44 +221,44 @@ def _score_info(state):
216
  return f"**Score: {s}/{t}** ({pct}%)"
217
 
218
 
219
- # ── Event handlers ─────────────────────────────────────────────────────────────
220
 
221
- def new_sentence(state):
 
222
  sent = random.choice(SENTENCES[LEVELS[state["level"]]])
223
- state = {**state, "sentence": sent}
224
  audio = _make_audio(sent)
225
  hint = "🎧 Listen carefully, then type what you heard and press **Submit**."
226
  if audio is None:
227
  hint = f"⚠️ Audio unavailable. Sentence: **{sent}** *(type it below)*"
228
- return audio, "", hint, _level_info(state), _score_info(state), state
229
 
230
 
231
- def submit_answer(attempt, state):
 
232
  if not state.get("sentence"):
233
- return "", "⚠️ Press **New Sentence** first!", _level_info(state), _score_info(state), state
234
 
235
  correct = state["sentence"]
236
- state = {**state, "total": state["total"] + 1}
237
  errors = _find_errors(correct, attempt)
238
  clean_a = attempt.strip().rstrip(".").lower()
239
  clean_c = correct.strip().rstrip(".").lower()
240
 
241
  if not errors and clean_a == clean_c:
242
- state = {**state, "score": state["score"] + 1, "streak": state["streak"] + 1}
 
243
  msg = "βœ… **Correct!** Every word spelled perfectly."
244
  if state["streak"] >= PASS_REQ and state["level"] < len(LEVELS) - 1:
245
- state = {**state, "level": state["level"] + 1, "streak": 0}
 
246
  new_label = LEVEL_LABELS[state["level"]]
247
  msg += f"\n\nπŸŽ‰ **Level up! You are now {new_label}.**"
248
  else:
249
- state = {**state, "streak": 0}
250
  diff_str = _build_diff(correct, attempt)
251
  gemma_tip = _ask_gemma(correct, attempt, errors)
252
- ai_block = (
253
- f"\n\n---\n\n**πŸ’‘ Tips:**\n\n{gemma_tip}"
254
- if gemma_tip
255
- else ""
256
- )
257
  msg = (
258
  "❌ **Your answer with corrections:**\n\n"
259
  + diff_str
@@ -261,29 +266,31 @@ def submit_answer(attempt, state):
261
  + ai_block
262
  )
263
 
264
- entry = {
265
  "correct": correct,
266
  "attempt": attempt,
267
  "ok": not bool(errors) and clean_a == clean_c,
268
  "level": LEVELS[state["level"]],
269
  "ts": time.strftime("%H:%M:%S"),
270
- }
271
- state = {**state, "sentence": "", "history": state["history"] + [entry]}
272
- return attempt, msg, _level_info(state), _score_info(state), state
273
 
274
 
275
- def replay_audio(state):
 
276
  if state.get("sentence"):
277
  return _make_audio(state["sentence"])
278
  return None
279
 
280
 
281
- def reset_game(state):
282
- state = fresh_state()
283
- return None, "", "Game reset. Press **New Sentence** to start!", _level_info(state), _score_info(state), state
284
 
285
 
286
- def export_history(state):
 
287
  history = state.get("history", [])
288
  if not history:
289
  return None
@@ -300,9 +307,12 @@ body { font-family: 'Segoe UI', sans-serif; }
300
  footer { display: none !important; }
301
  """
302
 
 
 
303
  with gr.Blocks(title="Spelling Practice", css=CSS) as demo:
304
 
305
- session = gr.State(fresh_state)
 
306
 
307
  gr.Markdown(
308
  "# πŸ“ Spelling Practice\n"
@@ -310,8 +320,8 @@ with gr.Blocks(title="Spelling Practice", css=CSS) as demo:
310
  )
311
 
312
  with gr.Row():
313
- level_md = gr.Markdown(_level_info(fresh_state()))
314
- score_md = gr.Markdown(_score_info(fresh_state()))
315
 
316
  with gr.Row():
317
  new_btn = gr.Button("🎲 New Sentence", variant="primary")
@@ -334,12 +344,12 @@ with gr.Blocks(title="Spelling Practice", css=CSS) as demo:
334
 
335
  OUTS = [audio_out, answer_box, hint_md, level_md, score_md, session]
336
 
337
- new_btn.click(new_sentence, [session], OUTS)
338
- replay_btn.click(replay_audio, [session], [audio_out])
339
- submit_btn.click(submit_answer,[answer_box, session], [answer_box, feedback, level_md, score_md, session])
340
  answer_box.submit(submit_answer,[answer_box, session], [answer_box, feedback, level_md, score_md, session])
341
- reset_btn.click(reset_game, [session], OUTS)
342
  export_btn.click(export_history,[session], [history_file])
343
 
344
  if __name__ == "__main__":
345
- demo.launch()
 
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
 
12
  import json
13
  import time
14
  import threading
 
15
 
16
  import gradio as gr
17
  from gtts import gTTS
 
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
 
30
  def get_llm():
 
31
  global _llm
32
  if _llm is not None:
33
  return _llm
 
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 ───────────────────────────────────────────────────────────────
 
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
 
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
 
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"
 
320
  )
321
 
322
  with gr.Row():
323
+ level_md = gr.Markdown(_level_info(_init))
324
+ score_md = gr.Markdown(_score_info(_init))
325
 
326
  with gr.Row():
327
  new_btn = gr.Button("🎲 New Sentence", variant="primary")
 
344
 
345
  OUTS = [audio_out, answer_box, hint_md, level_md, score_md, session]
346
 
347
+ new_btn.click(new_sentence, [session], OUTS)
348
+ replay_btn.click(replay_audio, [session], [audio_out])
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)