ramedde commited on
Commit
e8aeffc
Β·
verified Β·
1 Parent(s): e007e2c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +307 -40
app.py CHANGED
@@ -1,20 +1,26 @@
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
  # ─────────────────────────────────────────────────────────────────────────────
@@ -28,10 +34,8 @@ import json
28
  import time
29
  import threading
30
 
31
- import gradio as gr
32
  from gtts import gTTS
33
 
34
- # ── Optional: llama-cpp for Gemma tips ────────────────────────────────────────
35
  try:
36
  from llama_cpp import Llama
37
  _LLAMA_AVAILABLE = True
@@ -61,19 +65,13 @@ def get_llm():
61
  print("[spelling-app] Model ready.")
62
  return _llm
63
 
64
- # ── Sentence bank ──────────────────────────────────────────────────────────────
65
  SENTENCES = {
66
  "easy": [
67
- "The student read a book.",
68
- "I like to learn English.",
69
- "The bank has no money.",
70
- "She has a new job.",
71
- "He can read very fast.",
72
- "The map shows a river.",
73
- "We write notes in class.",
74
- "Put the book on the desk.",
75
- "I see a small farm.",
76
- "The goal is very clear.",
77
  ],
78
  "medium": [
79
  "The Southern colonies grew tobacco for Europe.",
@@ -117,16 +115,12 @@ LEVELS = ["easy", "medium", "hard", "expert"]
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:
@@ -169,11 +163,10 @@ 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])+"]**")
@@ -188,8 +181,6 @@ 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"]]])
@@ -242,20 +233,17 @@ 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
 
@@ -280,7 +268,285 @@ with gr.Blocks(title="Spelling Practice", css=CSS) as demo:
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
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
284
  new_btn.click(new_sentence, [session], OUTS)
285
  replay_btn.click(replay_audio, [session], [audio_out])
286
  submit_btn.click(submit_answer, [answer_box, session], [answer_box, feedback, level_md, score_md, session])
@@ -289,5 +555,6 @@ with gr.Blocks(title="Spelling Practice", css=CSS) as demo:
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,share=True)
 
293
 
 
1
  """
2
  Spelling Practice – Hugging Face Space
3
+ Multi-user safe via gr.State (JSON string per session).
4
  """
5
 
6
+ # ── Import gradio first, then patch the gradio_client 0.8 schema bug ──────────
7
+ # gradio_client 0.8 (bundled with gradio 4.40) crashes in get_api_info() when
8
+ # any component schema contains `additionalProperties: true` (a bool).
9
+ # The fix: wrap json_schema_to_python_type to catch the exception, patching
10
+ # both the gradio_client module AND the already-imported reference in gradio.blocks.
11
+ import gradio as gr
12
+
13
  try:
14
  import gradio_client.utils as _gcu
15
+ import gradio.blocks as _gb
16
+ _orig_jspt = _gcu.json_schema_to_python_type
17
+ def _safe_jspt(schema):
18
+ try:
19
+ return _orig_jspt(schema)
20
+ except Exception:
21
  return "Any"
22
+ _gcu.json_schema_to_python_type = _safe_jspt
23
+ _gb.client_utils.json_schema_to_python_type = _safe_jspt
24
  except Exception:
25
  pass
26
  # ─────────────────────────────────────────────────────────────────────────────
 
34
  import time
35
  import threading
36
 
 
37
  from gtts import gTTS
38
 
 
39
  try:
40
  from llama_cpp import Llama
41
  _LLAMA_AVAILABLE = True
 
65
  print("[spelling-app] Model ready.")
66
  return _llm
67
 
 
68
  SENTENCES = {
69
  "easy": [
70
+ "The student read a book.", "I like to learn English.",
71
+ "The bank has no money.", "She has a new job.",
72
+ "He can read very fast.", "The map shows a river.",
73
+ "We write notes in class.", "Put the book on the desk.",
74
+ "I see a small farm.", "The goal is very clear.",
 
 
 
 
 
75
  ],
76
  "medium": [
77
  "The Southern colonies grew tobacco for Europe.",
 
115
  LEVEL_LABELS = ["🟒 Beginner", "πŸ”΅ Intermediate", "🟠 Advanced", "πŸ”΄ Expert"]
116
  PASS_REQ = 3
117
 
 
 
118
  def fresh_state_str():
119
  return json.dumps({"level": 0, "streak": 0, "sentence": "",
120
  "score": 0, "total": 0, "history": []})
121
 
122
+ def S(s): return json.loads(s)
123
+ def D(d): return json.dumps(d)
 
 
124
 
125
  def _make_audio(text):
126
  try:
 
163
  c_words, a_words = correct.split(), attempt.split()
164
  parts = []
165
  sm = difflib.SequenceMatcher(
166
+ None, [w.rstrip(".,").lower() for w in a_words],
 
167
  [w.rstrip(".,").lower() for w in c_words])
168
  for tag, i1, i2, j1, j2 in sm.get_opcodes():
169
+ if tag == "equal": parts.append(" ".join(a_words[i1:i2]))
170
  elif tag == "replace": parts.append("~~"+" ".join(a_words[i1:i2])+"~~ **"+" ".join(c_words[j1:j2])+"**")
171
  elif tag == "delete": parts.append("~~"+" ".join(a_words[i1:i2])+"~~")
172
  elif tag == "insert": parts.append("**["+" ".join(c_words[j1:j2])+"]**")
 
181
  t, s = state["total"], state["score"]
182
  return f"**Score: {s}/{t}** ({int(s/t*100) if t else 0}%)"
183
 
 
 
184
  def new_sentence(ss):
185
  state = S(ss)
186
  sent = random.choice(SENTENCES[LEVELS[state["level"]]])
 
233
  history = S(ss).get("history", [])
234
  if not history:
235
  return "No history yet."
236
+ lines = []
237
  for h in history:
238
  ok = "βœ…" if h["ok"] else "❌"
239
  lines.append(f'{ok} [{h["level"]}] {h["ts"]} | {h["correct"]} β†’ {h["attempt"]}')
240
  return "\n".join(lines)
241
 
 
 
242
  CSS = "footer { display: none !important; }"
243
  _init = S(fresh_state_str())
244
 
245
  with gr.Blocks(title="Spelling Practice", css=CSS) as demo:
246
+ session = gr.State(fresh_state_str())
 
247
 
248
  gr.Markdown("# πŸ“ Spelling Practice\nListen Β· Type Β· See errors Β· Get tips")
249
 
 
268
  history_box = gr.Textbox(label="Your session", lines=10, interactive=False)
269
 
270
  OUTS = [audio_out, answer_box, hint_md, level_md, score_md, session]
271
+ new_btn.click(new_sentence, [session], OUTS)
272
+ replay_btn.click(replay_audio, [session], [audio_out])
273
+ submit_btn.click(submit_answer, [answer_box, session], [answer_box, feedback, level_md, score_md, session])
274
+ answer_box.submit(submit_answer,[answer_box, session], [answer_box, feedback, level_md, score_md, session])
275
+ reset_btn.click(reset_game, [session], OUTS)
276
+ export_btn.click(export_history,[session], [history_box])
277
+
278
+ if __name__ == "__main__":
279
+ demo.launch(server_name="0.0.0.0", server_port=7860)
280
+ """
281
+ Spelling Practice – Hugging Face Space
282
+ Multi-user safe via gr.State (JSON string per session).
283
+ """
284
+
285
+ # ── Import gradio first, then patch the gradio_client 0.8 schema bug ──────────
286
+ # gradio_client 0.8 (bundled with gradio 4.40) crashes in get_api_info() when
287
+ # any component schema contains `additionalProperties: true` (a bool).
288
+ # The fix: wrap json_schema_to_python_type to catch the exception, patching
289
+ # both the gradio_client module AND the already-imported reference in gradio.blocks.
290
+ import gradio as gr
291
+
292
+ try:
293
+ import gradio_client.utils as _gcu
294
+ import gradio.blocks as _gb
295
+ _orig_jspt = _gcu.json_schema_to_python_type
296
+ def _safe_jspt(schema):
297
+ try:
298
+ return _orig_jspt(schema)
299
+ except Exception:
300
+ return "Any"
301
+ _gcu.json_schema_to_python_type = _safe_jspt
302
+ _gb.client_utils.json_schema_to_python_type = _safe_jspt
303
+ except Exception:
304
+ pass
305
+ # ─────────────────────────────────────────────────────────────────────────────
306
+
307
+ import os
308
+ import glob
309
+ import random
310
+ import tempfile
311
+ import difflib
312
+ import json
313
+ import time
314
+ import threading
315
 
316
+ from gtts import gTTS
317
+
318
+ try:
319
+ from llama_cpp import Llama
320
+ _LLAMA_AVAILABLE = True
321
+ except ImportError:
322
+ _LLAMA_AVAILABLE = False
323
+
324
+ _llm = None
325
+ _llm_lock = threading.Lock()
326
+
327
+ def get_llm():
328
+ global _llm
329
+ if _llm is not None:
330
+ return _llm
331
+ if not _LLAMA_AVAILABLE:
332
+ return None
333
+ with _llm_lock:
334
+ if _llm is not None:
335
+ return _llm
336
+ gguf_files = glob.glob("/app/*.gguf") + glob.glob("*.gguf")
337
+ if not gguf_files:
338
+ print("[spelling-app] No .gguf file found – AI tips disabled.")
339
+ return None
340
+ model_path = gguf_files[0]
341
+ print(f"[spelling-app] Loading model: {model_path}")
342
+ _llm = Llama(model_path=model_path, n_ctx=512, n_threads=2,
343
+ n_gpu_layers=int(os.getenv("N_GPU_LAYERS", "-1")), verbose=False)
344
+ print("[spelling-app] Model ready.")
345
+ return _llm
346
+
347
+ SENTENCES = {
348
+ "easy": [
349
+ "The student read a book.", "I like to learn English.",
350
+ "The bank has no money.", "She has a new job.",
351
+ "He can read very fast.", "The map shows a river.",
352
+ "We write notes in class.", "Put the book on the desk.",
353
+ "I see a small farm.", "The goal is very clear.",
354
+ ],
355
+ "medium": [
356
+ "The Southern colonies grew tobacco for Europe.",
357
+ "The workers found jobs in the factory.",
358
+ "He forgot to cite the original source.",
359
+ "They experienced a major economic boom.",
360
+ "My teacher helps me write better essays.",
361
+ "The market was very weak in December.",
362
+ "She received a new evening gown quickly.",
363
+ "We need to manage our time properly.",
364
+ "The group discussion went very well.",
365
+ "The online platform closes at midnight.",
366
+ ],
367
+ "hard": [
368
+ "The government announced policies to address the downturn.",
369
+ "She has an extraordinary talent for analyzing economic literature.",
370
+ "The archaeologist made a fascinating discovery of ancient artifacts.",
371
+ "It is necessary to manage your time effectively.",
372
+ "The corporation was highly profitable after the merger.",
373
+ "He struggled to translate the complex economic metaphors.",
374
+ "The instructor approved the research outline immediately.",
375
+ "She demonstrated remarkable perseverance throughout her academic journey.",
376
+ "Maintaining a regular study schedule is very important.",
377
+ "The researcher examined the primary historical document.",
378
+ ],
379
+ "expert": [
380
+ "The multinational corporation acknowledged the financial discrepancy.",
381
+ "His idiosyncratic research methodologies bewildered his academic colleagues.",
382
+ "The distinguished economist delivered a highly influential speech.",
383
+ "Technological advancements fundamentally transformed nineteenth-century transportation.",
384
+ "Maslow conceptualized self-actualization as the highest human need.",
385
+ "Agricultural consolidation overwhelmed small family farming operations.",
386
+ "Translating metaphorical expressions requires extraordinary cultural competence.",
387
+ "Plagiarism often leads to severe academic consequences.",
388
+ "The entrepreneur demonstrated extraordinary economic decision-making.",
389
+ "The researcher meticulously catalogued primary historical sources.",
390
+ ],
391
+ }
392
+
393
+ LEVELS = ["easy", "medium", "hard", "expert"]
394
+ LEVEL_LABELS = ["🟒 Beginner", "πŸ”΅ Intermediate", "🟠 Advanced", "πŸ”΄ Expert"]
395
+ PASS_REQ = 3
396
+
397
+ def fresh_state_str():
398
+ return json.dumps({"level": 0, "streak": 0, "sentence": "",
399
+ "score": 0, "total": 0, "history": []})
400
+
401
+ def S(s): return json.loads(s)
402
+ def D(d): return json.dumps(d)
403
+
404
+ def _make_audio(text):
405
+ try:
406
+ tts = gTTS(text=text, lang="en", slow=True)
407
+ path = tempfile.mktemp(suffix=".mp3")
408
+ tts.save(path)
409
+ return path
410
+ except Exception as exc:
411
+ print(f"[spelling-app] gTTS error: {exc}")
412
+ return None
413
+
414
+ def _find_errors(correct, attempt):
415
+ c_words = correct.rstrip(".").lower().split()
416
+ a_words = attempt.rstrip(".").lower().split()
417
+ errors = []
418
+ for tag, i1, i2, j1, j2 in difflib.SequenceMatcher(None, a_words, c_words).get_opcodes():
419
+ if tag == "replace":
420
+ for w, r in zip(a_words[i1:i2], c_words[j1:j2]):
421
+ if w != r: errors.append((w, r))
422
+ elif tag == "delete":
423
+ for w in a_words[i1:i2]: errors.append((w, "[extra word]"))
424
+ elif tag == "insert":
425
+ for r in c_words[j1:j2]: errors.append(("[missing]", r))
426
+ return errors
427
+
428
+ def _ask_gemma(correct, attempt, errors):
429
+ llm = get_llm()
430
+ if not llm or not errors: return ""
431
+ error_list = "\n".join(f'- You wrote "{w}", correct is "{r}"' for w, r in errors)
432
+ prompt = (f"You are a friendly English spelling teacher.\n"
433
+ f"The student had to write: {correct}\n"
434
+ f"The student wrote: {attempt}\n"
435
+ f"Spelling mistakes:\n{error_list}\n\n"
436
+ "For each mistake, write one short sentence explaining the error "
437
+ "and one memory tip. Be encouraging. Keep it brief.")
438
+ out = llm(prompt, max_tokens=200, temperature=0.3, stop=["###"])
439
+ return out["choices"][0]["text"].strip()
440
+
441
+ def _build_diff(correct, attempt):
442
+ c_words, a_words = correct.split(), attempt.split()
443
+ parts = []
444
+ sm = difflib.SequenceMatcher(
445
+ None, [w.rstrip(".,").lower() for w in a_words],
446
+ [w.rstrip(".,").lower() for w in c_words])
447
+ for tag, i1, i2, j1, j2 in sm.get_opcodes():
448
+ if tag == "equal": parts.append(" ".join(a_words[i1:i2]))
449
+ elif tag == "replace": parts.append("~~"+" ".join(a_words[i1:i2])+"~~ **"+" ".join(c_words[j1:j2])+"**")
450
+ elif tag == "delete": parts.append("~~"+" ".join(a_words[i1:i2])+"~~")
451
+ elif tag == "insert": parts.append("**["+" ".join(c_words[j1:j2])+"]**")
452
+ return " ".join(parts)
453
+
454
+ def _level_info(state):
455
+ lvl = state["level"]
456
+ extra = f" Β· streak {state['streak']}/{PASS_REQ} to level up" if lvl < len(LEVELS)-1 else " Β· MAX LEVEL πŸ†"
457
+ return f"**{LEVEL_LABELS[lvl]}**{extra}"
458
+
459
+ def _score_info(state):
460
+ t, s = state["total"], state["score"]
461
+ return f"**Score: {s}/{t}** ({int(s/t*100) if t else 0}%)"
462
+
463
+ def new_sentence(ss):
464
+ state = S(ss)
465
+ sent = random.choice(SENTENCES[LEVELS[state["level"]]])
466
+ state["sentence"] = sent
467
+ audio = _make_audio(sent)
468
+ hint = "🎧 Listen carefully, then type what you heard and press **Submit**."
469
+ if audio is None:
470
+ hint = f"⚠️ Audio unavailable. Sentence: **{sent}**"
471
+ return audio, "", hint, _level_info(state), _score_info(state), D(state)
472
+
473
+ def submit_answer(attempt, ss):
474
+ state = S(ss)
475
+ if not state.get("sentence"):
476
+ return "", "⚠️ Press **New Sentence** first!", _level_info(state), _score_info(state), ss
477
+ correct = state["sentence"]
478
+ state["total"] += 1
479
+ errors = _find_errors(correct, attempt)
480
+ clean_a = attempt.strip().rstrip(".").lower()
481
+ clean_c = correct.strip().rstrip(".").lower()
482
+ if not errors and clean_a == clean_c:
483
+ state["score"] += 1
484
+ state["streak"] += 1
485
+ msg = "βœ… **Correct!** Every word spelled perfectly."
486
+ if state["streak"] >= PASS_REQ and state["level"] < len(LEVELS)-1:
487
+ state["level"] += 1
488
+ state["streak"] = 0
489
+ msg += f"\n\nπŸŽ‰ **Level up! You are now {LEVEL_LABELS[state['level']]}.**"
490
+ else:
491
+ state["streak"] = 0
492
+ diff_str = _build_diff(correct, attempt)
493
+ gemma_tip = _ask_gemma(correct, attempt, errors)
494
+ ai_block = f"\n\n---\n\n**πŸ’‘ Tips:**\n\n{gemma_tip}" if gemma_tip else ""
495
+ msg = ("❌ **Your answer with corrections:**\n\n" + diff_str +
496
+ "\n\n*(~~strikethrough~~ = your error Β· **bold** = correct spelling)*" + ai_block)
497
+ state["history"].append({"correct": correct, "attempt": attempt,
498
+ "ok": not bool(errors) and clean_a == clean_c,
499
+ "level": LEVELS[state["level"]], "ts": time.strftime("%H:%M:%S")})
500
+ state["sentence"] = ""
501
+ return attempt, msg, _level_info(state), _score_info(state), D(state)
502
+
503
+ def replay_audio(ss):
504
+ state = S(ss)
505
+ return _make_audio(state["sentence"]) if state.get("sentence") else None
506
+
507
+ def reset_game(_ss):
508
+ state = S(fresh_state_str())
509
+ return None, "", "Game reset. Press **New Sentence** to start!", _level_info(state), _score_info(state), fresh_state_str()
510
+
511
+ def export_history(ss):
512
+ history = S(ss).get("history", [])
513
+ if not history:
514
+ return "No history yet."
515
+ lines = []
516
+ for h in history:
517
+ ok = "βœ…" if h["ok"] else "❌"
518
+ lines.append(f'{ok} [{h["level"]}] {h["ts"]} | {h["correct"]} β†’ {h["attempt"]}')
519
+ return "\n".join(lines)
520
+
521
+ CSS = "footer { display: none !important; }"
522
+ _init = S(fresh_state_str())
523
+
524
+ with gr.Blocks(title="Spelling Practice", css=CSS) as demo:
525
+ session = gr.State(fresh_state_str())
526
+
527
+ gr.Markdown("# πŸ“ Spelling Practice\nListen Β· Type Β· See errors Β· Get tips")
528
+
529
+ with gr.Row():
530
+ level_md = gr.Markdown(_level_info(_init))
531
+ score_md = gr.Markdown(_score_info(_init))
532
+
533
+ with gr.Row():
534
+ new_btn = gr.Button("🎲 New Sentence", variant="primary")
535
+ replay_btn = gr.Button("πŸ” Replay Audio", variant="secondary")
536
+ reset_btn = gr.Button("πŸ”„ Reset Game", variant="secondary")
537
+
538
+ audio_out = gr.Audio(label="Listen to the sentence", autoplay=True, interactive=False)
539
+ hint_md = gr.Markdown("Press **New Sentence** to begin.")
540
+ answer_box = gr.Textbox(label="Type the sentence you heard",
541
+ placeholder="Write the full sentence here…", lines=2)
542
+ submit_btn = gr.Button("βœ… Submit", variant="primary")
543
+ feedback = gr.Markdown("")
544
+
545
+ with gr.Accordion("πŸ“Š Session History", open=False):
546
+ export_btn = gr.Button("πŸ“‹ Show history")
547
+ history_box = gr.Textbox(label="Your session", lines=10, interactive=False)
548
+
549
+ OUTS = [audio_out, answer_box, hint_md, level_md, score_md, session]
550
  new_btn.click(new_sentence, [session], OUTS)
551
  replay_btn.click(replay_audio, [session], [audio_out])
552
  submit_btn.click(submit_answer, [answer_box, session], [answer_box, feedback, level_md, score_md, session])
 
555
  export_btn.click(export_history,[session], [history_box])
556
 
557
  if __name__ == "__main__":
558
+ demo.launch(server_name="0.0.0.0", server_port=7860)
559
+
560