ramedde commited on
Commit
79ecd65
·
verified ·
1 Parent(s): 147cb03

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +88 -44
app.py CHANGED
@@ -1,6 +1,7 @@
1
  """
2
  Cours de Droit – Hugging Face Space
3
  Compatible avec le Docker HF (gradio 4.44.1 bundled + Python 3.13).
 
4
  """
5
 
6
  import sys
@@ -24,8 +25,9 @@ try:
24
  except Exception:
25
  pass
26
 
27
- import os, glob, json, time, tempfile, random, threading
28
  from gtts import gTTS
 
29
 
30
  try:
31
  from llama_cpp import Llama
@@ -33,28 +35,51 @@ try:
33
  except ImportError:
34
  _LLAMA_OK = False
35
 
 
36
  _llm = None
37
  _llm_lock = threading.Lock()
38
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  def get_llm():
40
  global _llm
41
  if _llm is not None:
42
  return _llm
43
  if not _LLAMA_OK:
 
44
  return None
45
  with _llm_lock:
46
  if _llm is not None:
47
  return _llm
48
- paths = glob.glob("/app/*.gguf") + glob.glob("*.gguf")
49
- if not paths:
50
- print("[law-app] Aucun .gguf trouvé – évaluation IA désactivée.")
51
  return None
52
- print(f"[law-app] Chargement : {paths[0]}")
53
  _llm = Llama(
54
- model_path=paths[0],
55
  n_ctx=1024,
56
  n_threads=int(os.getenv("N_THREADS", "2")),
57
- n_gpu_layers=int(os.getenv("N_GPU_LAYERS", "-1")),
58
  verbose=False,
59
  )
60
  print("[law-app] Modèle prêt.")
@@ -261,26 +286,50 @@ def evaluate_answer(question, answer, hint):
261
  hits = sum(1 for k in kw if k in answer.lower())
262
  score = min(10, round((hits / max(len(kw), 1)) * 10))
263
  return score, f"(Sans IA – mots-clés : {hits}/{len(kw)})"
 
 
264
  prompt = (
265
- f"Tu es un professeur de droit.\n"
 
 
 
 
266
  f"Question : {question}\n"
267
- f"Réponse : {answer}\n"
268
  f"Éléments attendus : {hint}\n\n"
269
- "Réponds UNIQUEMENT avec ce JSON sur une ligne : "
270
- '{"score": <0-10>, "feedback": "<commentaire bref en français>"}'
 
 
 
271
  )
272
  try:
273
- out = llm(prompt, max_tokens=150, temperature=0.1, stop=["###", "\n\n"])
 
 
 
 
 
274
  text = out["choices"][0]["text"].strip()
275
- data = json.loads(text[text.find("{"):text.rfind("}")+1])
 
 
 
 
 
 
276
  return max(0, min(10, int(data["score"]))), data.get("feedback", "")
277
  except Exception as e:
278
  print(f"[law-app] Erreur éval : {e}")
279
- return 0, "Erreur d'évaluation, réessayez."
 
 
 
 
280
 
281
  # ── Handlers ─────────────────────────────────────���────────────────────────────
282
  def start_listening(ss):
283
- state = S(ss)
284
  lesson_idx = state.get("lesson_idx", 0)
285
  lesson = LESSONS[lesson_idx]
286
  state.update({"phase": "listen", "listen_start": time.time(), "question_idx": None})
@@ -292,10 +341,10 @@ def start_listening(ss):
292
  )
293
  return (
294
  audio, msg,
295
- gr.update(visible=True), # listen_col
296
- gr.update(visible=False), # quiz_col
297
- gr.update(visible=False), # result_col
298
- gr.update(visible=False), # cert_col
299
  gr.update(interactive=False, value="⏳ Écoute en cours…"),
300
  D(state),
301
  )
@@ -304,7 +353,7 @@ def check_timer(ss):
304
  state = S(ss)
305
  if state["phase"] != "listen" or not state["listen_start"]:
306
  return gr.update(), "⚠️ Lancez d'abord l'écoute.", ss
307
- rem = max(0, LISTEN_SECONDS - (time.time() - state["listen_start"]))
308
  m, s = divmod(int(rem), 60)
309
  if rem <= 0:
310
  return (
@@ -324,12 +373,12 @@ def open_quiz(ss):
324
  if elapsed < LISTEN_SECONDS:
325
  m, s = divmod(int(LISTEN_SECONDS - elapsed), 60)
326
  return (
327
- gr.update(visible=True), gr.update(visible=False),
328
  gr.update(visible=False), gr.update(visible=False),
329
  f"⚠️ Patientez encore **{m} min {s:02d} s**.", "", "", ss,
330
  )
331
  lesson_idx = state.get("lesson_idx", 0)
332
- idx = random.randrange(len(LESSONS[lesson_idx]["questions"]))
333
  state.update({"phase": "quiz", "question_idx": idx})
334
  return (
335
  gr.update(visible=False), gr.update(visible=True),
@@ -341,9 +390,11 @@ def submit_answer(answer, ss):
341
  state = S(ss)
342
  lesson_idx = state.get("lesson_idx", 0)
343
  if state["phase"] != "quiz":
344
- return (gr.update(visible=True), gr.update(visible=False),
345
- gr.update(visible=False), gr.update(visible=False),
346
- "", "", ss)
 
 
347
  q = LESSONS[lesson_idx]["questions"][state["question_idx"]]
348
  score, fb = evaluate_answer(q["q"], answer, q["hint"])
349
  total = len(LESSONS)
@@ -358,14 +409,14 @@ def submit_answer(answer, ss):
358
  if is_last:
359
  next_label = "🏆 Voir mon certificat"
360
  else:
361
- next_label = f"➡️ Leçon {lesson_idx + 2} — {LESSONS[lesson_idx + 1]['title'].split('—')[1].strip()}"
362
  else:
363
  state["phase"] = "fail"
364
  result = (
365
  f"## ❌ Score insuffisant : {score}/10\n\n"
366
  f"**Commentaire :** {fb}\n\n"
367
  f"Seuil requis : **{PASS_SCORE}/10**. "
368
- f"Veuillez **réécouter la leçon** avant de retenter."
369
  )
370
  next_label = "🔄 Réécouter la leçon"
371
 
@@ -381,14 +432,11 @@ def handle_next(ss):
381
  total = len(LESSONS)
382
 
383
  if state["phase"] == "fail":
384
- # Réécouter la même leçon
385
  return start_listening(D(state))
386
 
387
- # PASS
388
  if lesson_idx >= total - 1:
389
- # Dernière leçon → certificat
390
  state["phase"] = "done"
391
- cert_md = (
392
  "# 🏆 Félicitations !\n\n"
393
  "Vous avez complété avec succès les **3 leçons** du cours de droit civil :\n\n"
394
  "✅ Leçon 1 — Sources du droit et hiérarchie des normes\n\n"
@@ -400,15 +448,14 @@ def handle_next(ss):
400
  )
401
  return (
402
  None, "",
403
- gr.update(visible=False), # listen_col
404
- gr.update(visible=False), # quiz_col
405
- gr.update(visible=False), # result_col
406
- gr.update(visible=True), # cert_col
407
  gr.update(interactive=False, value="✅ Terminé"),
408
  D(state),
409
  )
410
  else:
411
- # Leçon suivante
412
  state["lesson_idx"] = lesson_idx + 1
413
  state["listen_start"] = None
414
  return start_listening(D(state))
@@ -428,7 +475,6 @@ with gr.Blocks(title="Cours de Droit", css=CSS) as demo:
428
  elem_id="title",
429
  )
430
 
431
- # ── Étape 1 : Écoute ──────────────────────────────────────────────────────
432
  with gr.Column(visible=True) as listen_col:
433
  gr.Markdown("## 🎧 Étape 1 — Écoute de la leçon")
434
  listen_msg = gr.Markdown("Cliquez sur **Démarrer** pour commencer.")
@@ -439,26 +485,22 @@ with gr.Blocks(title="Cours de Droit", css=CSS) as demo:
439
  check_btn = gr.Button("🔄 Vérifier le temps", variant="secondary")
440
  quiz_btn = gr.Button("✅ Accéder au Quiz", variant="secondary", interactive=False)
441
 
442
- # ── Étape 2 : Quiz ────────────────────────────────────────────────────────
443
  with gr.Column(visible=False) as quiz_col:
444
  gr.Markdown("## 📝 Étape 2 — Question d'évaluation")
445
  question_md = gr.Markdown("")
446
  answer_box = gr.Textbox(label="Votre réponse", placeholder="Rédigez ici…", lines=5)
447
  submit_btn = gr.Button("📤 Soumettre", variant="primary")
448
 
449
- # ── Étape 3 : Résultat ────────────────────────────────────────────────────
450
  with gr.Column(visible=False) as result_col:
451
  gr.Markdown("## 📊 Résultat")
452
  result_md = gr.Markdown("")
453
  next_btn = gr.Button("➡️ Leçon suivante", variant="primary")
454
 
455
- # ── Étape 4 : Certificat ─────────────────────────────────────────────────
456
  with gr.Column(visible=False) as cert_col:
457
- cert_md = gr.Markdown("")
458
- restart_btn = gr.Button("🔄 Recommencer depuis le début", variant="secondary")
459
 
460
  # ── Câblage ───────────────────────────────────────────────────────────────
461
- # Outputs communs à start_listening et handle_next (quand leçon suivante)
462
  LISTEN_OUT = [audio_out, listen_msg,
463
  listen_col, quiz_col, result_col, cert_col,
464
  quiz_btn, session]
@@ -476,8 +518,10 @@ with gr.Blocks(title="Cours de Droit", css=CSS) as demo:
476
  answer_box.submit(submit_answer, [answer_box, session], RES_OUT)
477
 
478
  next_btn.click(handle_next, [session], LISTEN_OUT)
479
-
480
  restart_btn.click(restart_from_cert, [session], LISTEN_OUT)
481
 
 
 
 
482
  if __name__ == "__main__":
483
  demo.launch(server_name="0.0.0.0", server_port=7860, show_error=True)
 
1
  """
2
  Cours de Droit – Hugging Face Space
3
  Compatible avec le Docker HF (gradio 4.44.1 bundled + Python 3.13).
4
+ Téléchargement automatique du modèle au démarrage.
5
  """
6
 
7
  import sys
 
25
  except Exception:
26
  pass
27
 
28
+ import os, json, time, tempfile, random, threading
29
  from gtts import gTTS
30
+ from huggingface_hub import hf_hub_download
31
 
32
  try:
33
  from llama_cpp import Llama
 
35
  except ImportError:
36
  _LLAMA_OK = False
37
 
38
+ # ── Téléchargement et chargement du modèle ────────────────────────────────────
39
  _llm = None
40
  _llm_lock = threading.Lock()
41
 
42
+ def download_model():
43
+ model_path = "/app/model.gguf"
44
+ if os.path.exists(model_path):
45
+ print("[law-app] Modèle déjà présent.")
46
+ return model_path
47
+ print("[law-app] Téléchargement du modèle en cours...")
48
+ try:
49
+ path = hf_hub_download(
50
+ repo_id="Qwen/Qwen2.5-0.5B-Instruct-GGUF",
51
+ filename="qwen2.5-0.5b-instruct-q4_k_m.gguf",
52
+ local_dir="/app",
53
+ local_dir_use_symlinks=False,
54
+ )
55
+ if path != model_path:
56
+ os.rename(path, model_path)
57
+ print("[law-app] Téléchargement terminé.")
58
+ return model_path
59
+ except Exception as e:
60
+ print(f"[law-app] Erreur téléchargement : {e}")
61
+ return None
62
+
63
  def get_llm():
64
  global _llm
65
  if _llm is not None:
66
  return _llm
67
  if not _LLAMA_OK:
68
+ print("[law-app] llama_cpp non disponible.")
69
  return None
70
  with _llm_lock:
71
  if _llm is not None:
72
  return _llm
73
+ model_path = download_model()
74
+ if not model_path or not os.path.exists(model_path):
75
+ print("[law-app] Modèle introuvable.")
76
  return None
77
+ print(f"[law-app] Chargement : {model_path}")
78
  _llm = Llama(
79
+ model_path=model_path,
80
  n_ctx=1024,
81
  n_threads=int(os.getenv("N_THREADS", "2")),
82
+ n_gpu_layers=int(os.getenv("N_GPU_LAYERS", "0")),
83
  verbose=False,
84
  )
85
  print("[law-app] Modèle prêt.")
 
286
  hits = sum(1 for k in kw if k in answer.lower())
287
  score = min(10, round((hits / max(len(kw), 1)) * 10))
288
  return score, f"(Sans IA – mots-clés : {hits}/{len(kw)})"
289
+
290
+ # Prompt format Qwen2.5
291
  prompt = (
292
+ "<|im_start|>system\n"
293
+ "Tu es un professeur de droit français strict et bienveillant. "
294
+ "Tu évalues les réponses des étudiants et réponds UNIQUEMENT en JSON valide.\n"
295
+ "<|im_end|>\n"
296
+ "<|im_start|>user\n"
297
  f"Question : {question}\n"
298
+ f"Réponse de l'étudiant : {answer}\n"
299
  f"Éléments attendus : {hint}\n\n"
300
+ "Évalue cette réponse sur 10 et donne un commentaire bref en français.\n"
301
+ "Réponds UNIQUEMENT avec ce JSON sur une seule ligne, sans rien d'autre :\n"
302
+ '{"score": <entier 0-10>, "feedback": "<commentaire bref>"}\n'
303
+ "<|im_end|>\n"
304
+ "<|im_start|>assistant\n"
305
  )
306
  try:
307
+ out = llm(
308
+ prompt,
309
+ max_tokens=150,
310
+ temperature=0.1,
311
+ stop=["<|im_end|>", "\n\n", "###"],
312
+ )
313
  text = out["choices"][0]["text"].strip()
314
+ print(f"[law-app] Réponse modèle : {text}")
315
+ # Extraire le JSON
316
+ start = text.find("{")
317
+ end = text.rfind("}") + 1
318
+ if start == -1 or end == 0:
319
+ raise ValueError("Pas de JSON dans la réponse")
320
+ data = json.loads(text[start:end])
321
  return max(0, min(10, int(data["score"]))), data.get("feedback", "")
322
  except Exception as e:
323
  print(f"[law-app] Erreur éval : {e}")
324
+ # Fallback mots-clés
325
+ kw = [w.strip().lower() for w in hint.split(",")]
326
+ hits = sum(1 for k in kw if k in answer.lower())
327
+ score = min(10, round((hits / max(len(kw), 1)) * 10))
328
+ return score, f"(Erreur modèle – mots-clés : {hits}/{len(kw)})"
329
 
330
  # ── Handlers ─────────────────────────────────────���────────────────────────────
331
  def start_listening(ss):
332
+ state = S(ss)
333
  lesson_idx = state.get("lesson_idx", 0)
334
  lesson = LESSONS[lesson_idx]
335
  state.update({"phase": "listen", "listen_start": time.time(), "question_idx": None})
 
341
  )
342
  return (
343
  audio, msg,
344
+ gr.update(visible=True),
345
+ gr.update(visible=False),
346
+ gr.update(visible=False),
347
+ gr.update(visible=False),
348
  gr.update(interactive=False, value="⏳ Écoute en cours…"),
349
  D(state),
350
  )
 
353
  state = S(ss)
354
  if state["phase"] != "listen" or not state["listen_start"]:
355
  return gr.update(), "⚠️ Lancez d'abord l'écoute.", ss
356
+ rem = max(0, LISTEN_SECONDS - (time.time() - state["listen_start"]))
357
  m, s = divmod(int(rem), 60)
358
  if rem <= 0:
359
  return (
 
373
  if elapsed < LISTEN_SECONDS:
374
  m, s = divmod(int(LISTEN_SECONDS - elapsed), 60)
375
  return (
376
+ gr.update(visible=True), gr.update(visible=False),
377
  gr.update(visible=False), gr.update(visible=False),
378
  f"⚠️ Patientez encore **{m} min {s:02d} s**.", "", "", ss,
379
  )
380
  lesson_idx = state.get("lesson_idx", 0)
381
+ idx = random.randrange(len(LESSONS[lesson_idx]["questions"]))
382
  state.update({"phase": "quiz", "question_idx": idx})
383
  return (
384
  gr.update(visible=False), gr.update(visible=True),
 
390
  state = S(ss)
391
  lesson_idx = state.get("lesson_idx", 0)
392
  if state["phase"] != "quiz":
393
+ return (
394
+ gr.update(visible=True), gr.update(visible=False),
395
+ gr.update(visible=False), gr.update(visible=False),
396
+ "", "➡️ Leçon suivante", ss,
397
+ )
398
  q = LESSONS[lesson_idx]["questions"][state["question_idx"]]
399
  score, fb = evaluate_answer(q["q"], answer, q["hint"])
400
  total = len(LESSONS)
 
409
  if is_last:
410
  next_label = "🏆 Voir mon certificat"
411
  else:
412
+ next_label = f"➡️ {LESSONS[lesson_idx + 1]['title']}"
413
  else:
414
  state["phase"] = "fail"
415
  result = (
416
  f"## ❌ Score insuffisant : {score}/10\n\n"
417
  f"**Commentaire :** {fb}\n\n"
418
  f"Seuil requis : **{PASS_SCORE}/10**. "
419
+ "Veuillez **réécouter la leçon** avant de retenter."
420
  )
421
  next_label = "🔄 Réécouter la leçon"
422
 
 
432
  total = len(LESSONS)
433
 
434
  if state["phase"] == "fail":
 
435
  return start_listening(D(state))
436
 
 
437
  if lesson_idx >= total - 1:
 
438
  state["phase"] = "done"
439
+ cert = (
440
  "# 🏆 Félicitations !\n\n"
441
  "Vous avez complété avec succès les **3 leçons** du cours de droit civil :\n\n"
442
  "✅ Leçon 1 — Sources du droit et hiérarchie des normes\n\n"
 
448
  )
449
  return (
450
  None, "",
451
+ gr.update(visible=False),
452
+ gr.update(visible=False),
453
+ gr.update(visible=False),
454
+ gr.update(visible=True),
455
  gr.update(interactive=False, value="✅ Terminé"),
456
  D(state),
457
  )
458
  else:
 
459
  state["lesson_idx"] = lesson_idx + 1
460
  state["listen_start"] = None
461
  return start_listening(D(state))
 
475
  elem_id="title",
476
  )
477
 
 
478
  with gr.Column(visible=True) as listen_col:
479
  gr.Markdown("## 🎧 Étape 1 — Écoute de la leçon")
480
  listen_msg = gr.Markdown("Cliquez sur **Démarrer** pour commencer.")
 
485
  check_btn = gr.Button("🔄 Vérifier le temps", variant="secondary")
486
  quiz_btn = gr.Button("✅ Accéder au Quiz", variant="secondary", interactive=False)
487
 
 
488
  with gr.Column(visible=False) as quiz_col:
489
  gr.Markdown("## 📝 Étape 2 — Question d'évaluation")
490
  question_md = gr.Markdown("")
491
  answer_box = gr.Textbox(label="Votre réponse", placeholder="Rédigez ici…", lines=5)
492
  submit_btn = gr.Button("📤 Soumettre", variant="primary")
493
 
 
494
  with gr.Column(visible=False) as result_col:
495
  gr.Markdown("## 📊 Résultat")
496
  result_md = gr.Markdown("")
497
  next_btn = gr.Button("➡️ Leçon suivante", variant="primary")
498
 
 
499
  with gr.Column(visible=False) as cert_col:
500
+ cert_md = gr.Markdown("")
501
+ restart_btn = gr.Button("🔄 Recommencer depuis le début", variant="secondary")
502
 
503
  # ── Câblage ───────────────────────────────────────────────────────────────
 
504
  LISTEN_OUT = [audio_out, listen_msg,
505
  listen_col, quiz_col, result_col, cert_col,
506
  quiz_btn, session]
 
518
  answer_box.submit(submit_answer, [answer_box, session], RES_OUT)
519
 
520
  next_btn.click(handle_next, [session], LISTEN_OUT)
 
521
  restart_btn.click(restart_from_cert, [session], LISTEN_OUT)
522
 
523
+ # ── Préchargement du modèle au démarrage ──────────────────────────────────────
524
+ threading.Thread(target=get_llm, daemon=True).start()
525
+
526
  if __name__ == "__main__":
527
  demo.launch(server_name="0.0.0.0", server_port=7860, show_error=True)