ramedde commited on
Commit
d4aaf07
·
verified ·
1 Parent(s): a47ef8f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +164 -220
app.py CHANGED
@@ -7,25 +7,11 @@ Workflow:
7
  3. Qwen3-0.6B-Q4_K_M (llama-cpp) évalue la réponse de 0 à 10.
8
  4. Score ≥ 6 → réussi. Score < 6 → retour au début, réécoute obligatoire.
9
  Multi-utilisateur sécurisé via gr.State (JSON par session).
 
10
  """
11
 
12
- # ── Patch gradio_client 0.8 schema bug ───────────────────────────────────────
13
- import gradio as gr
14
- try:
15
- import gradio_client.utils as _gcu
16
- import gradio.blocks as _gb
17
- _orig = _gcu.json_schema_to_python_type
18
- def _safe(schema):
19
- try: return _orig(schema)
20
- except Exception: return "Any"
21
- _gcu.json_schema_to_python_type = _safe
22
- _gb.client_utils.json_schema_to_python_type = _safe
23
- except Exception:
24
- pass
25
- # ─────────────────────────────────────────────────────────────────────────────
26
-
27
  import os, glob, json, time, tempfile, random, threading
28
-
29
  from gtts import gTTS
30
 
31
  try:
@@ -35,7 +21,7 @@ except ImportError:
35
  _LLAMA_OK = False
36
 
37
  # ── LLM singleton ─────────────────────────────────────────────────────────────
38
- _llm = None
39
  _llm_lock = threading.Lock()
40
 
41
  def get_llm():
@@ -64,114 +50,113 @@ def get_llm():
64
 
65
  # ── Contenu du cours (droit civil français) ───────────────────────────────────
66
  LECTURE_TEXT = """
67
- Cours de droit civil Introduction aux sources du droit français.
68
 
69
  Première partie : Les sources formelles du droit.
70
 
71
- Le droit français repose sur plusieurs sources hiérarchisées.
72
- Au sommet se trouve la Constitution du 4 octobre 1958, loi fondamentale
73
- de la République française. Elle établit les règles d'organisation de
74
  l'État et garantit les droits fondamentaux des citoyens.
75
 
76
- Sous la Constitution, on trouve les traités internationaux et le droit
77
- de l'Union européenne. En vertu de l'article 55 de la Constitution,
78
- les traités régulièrement ratifiés ont une autorité supérieure à celle
79
  des lois ordinaires.
80
 
81
- Vient ensuite la loi, votée par le Parlement. La loi ordinaire est
82
- adoptée par l'Assemblée nationale et le Sénat selon la procédure
83
- législative prévue par la Constitution. Les lois organiques,
84
- quant à elles, complètent et précisent certaines dispositions
85
  constitutionnelles et bénéficient d'un statut intermédiaire.
86
 
87
- Le règlement constitue la quatrième source majeure. Il émane du
88
- pouvoir exécutif : décrets du Président de la République et du
89
- Premier ministre, arrêtés ministériels, préfectoraux et municipaux.
90
- L'article 37 de la Constitution confère au gouvernement un domaine
91
  réglementaire autonome.
92
 
93
  Deuxième partie : Les sources matérielles du droit.
94
 
95
- La jurisprudence, bien qu'elle ne soit pas officiellement reconnue
96
- comme source formelle, joue un rôle fondamental dans le système
97
- juridique français. Les décisions de la Cour de cassation et du
98
- Conseil d'État interprètent et précisent la portée des textes légaux.
99
- Le Conseil constitutionnel, par ses décisions, contrôle la
100
  constitutionnalité des lois.
101
 
102
- La doctrine regroupe l'ensemble des travaux des juristes :
103
- professeurs, avocats, magistrats. Elle propose des analyses,
104
- des systématisations et des critiques du droit positif.
105
- Sans force obligatoire, elle influence néanmoins la jurisprudence
106
  et le législateur.
107
 
108
- La coutume, source ancienne, est aujourd'hui d'application limitée.
109
- Elle consiste en une pratique ancienne, constante et répétée,
110
- acceptée comme obligatoire par la population. En droit commercial,
111
  les usages professionnels conservent une importance certaine.
112
 
113
  Troisième partie : Le principe de hiérarchie des normes.
114
 
115
- Hans Kelsen, juriste autrichien du vingtième siècle, a théorisé
116
- la pyramide des normes. Selon cette théorie, chaque norme tire
117
- sa validité d'une norme supérieure. En France, le Conseil
118
- constitutionnel assure le respect de cette hiérarchie en contrôlant
119
  la conformité des lois à la Constitution.
120
 
121
- Le contrôle de légalité permet quant à lui de vérifier que les
122
- actes administratifs sont conformes aux lois qui leur sont
123
- supérieures. Tout acte contraire à une norme supérieure est
124
  susceptible d'être annulé ou écarté.
125
 
126
  Quatrième partie : Le droit des contrats.
127
 
128
- Le contrat est un accord de volontés entre deux ou plusieurs
129
- personnes destiné à créer, modifier, transmettre ou éteindre
130
- des obligations. Le Code civil, dans sa rédaction issue de
131
- l'ordonnance du 10 février 2016, définit les conditions de
132
- validité du contrat : le consentement des parties, leur capacité
133
  à contracter, et un contenu licite et certain.
134
 
135
- Le consentement doit être libre et éclairé. Il est vicié lorsqu'il
136
- a été donné par erreur, obtenu par dol ou arraché par violence.
137
- Dans ces cas, le contrat peut être annulé à la demande de la
138
  partie dont le consentement a été vicié.
139
 
140
- La capacité contractuelle est la règle. L'incapacité est
141
- l'exception et concerne principalement les mineurs non émancipés
142
  et les majeurs protégés sous tutelle ou curatelle.
143
 
144
- L'exécution du contrat obéit au principe de la force obligatoire :
145
- le contrat tient lieu de loi entre les parties qui l'ont conclu.
146
- En cas d'inexécution, la partie lésée dispose de plusieurs recours :
147
  l'exécution forcée, la résolution du contrat ou des dommages et intérêts.
148
 
149
  Cinquième partie : La responsabilité civile.
150
 
151
- La responsabilité civile a pour fonction de réparer le préjudice
152
- causé à autrui. Elle se distingue de la responsabilité pénale
153
  qui vise à sanctionner l'auteur d'une infraction.
154
 
155
- La responsabilité délictuelle repose sur trois conditions :
156
- une faute, un dommage et un lien de causalité entre les deux.
157
- L'article 1240 du Code civil dispose que tout fait quelconque
158
- de l'homme qui cause à autrui un dommage oblige son auteur
159
  à le réparer.
160
 
161
- La responsabilité sans faute, dite objective, est reconnue
162
- dans certains cas : responsabilité du fait des choses,
163
- responsabilité du fait d'autrui, responsabilité des produits
164
- défectueux. L'évolution jurisprudentielle et législative tend
165
  à élargir ces hypothèses pour mieux protéger les victimes.
166
 
167
- Conclusion : Le droit est un système dynamique en constante évolution.
168
- La maîtrise de ses sources et de ses principes fondamentaux est
169
- indispensable à tout juriste. La rigueur dans l'analyse textuelle,
170
- combinée à la connaissance de la jurisprudence, permet d'appliquer
171
  le droit de manière correcte et juste.
172
  """
173
 
174
- # ── Questions du cours ────────────────────────────────────────────────────────
175
  QUESTIONS = [
176
  {
177
  "q": "Quelle est la source au sommet de la hiérarchie des normes en droit français, et quelle est sa date ?",
@@ -191,7 +176,7 @@ QUESTIONS = [
191
  },
192
  {
193
  "q": "Quelles sont les trois conditions nécessaires pour engager la responsabilité délictuelle ?",
194
- "hint": "faute, dommage, lien de causalité article 1240 Code civil",
195
  },
196
  {
197
  "q": "Expliquez la différence entre la responsabilité civile et la responsabilité pénale.",
@@ -208,21 +193,9 @@ QUESTIONS = [
208
  ]
209
 
210
  LISTEN_SECONDS = 15 * 60 # 15 minutes
211
- PASS_SCORE = 6 # seuil de réussite sur 10
212
 
213
- # ── État de session ────────────────────────────────────────────────────────────
214
- def fresh():
215
- return json.dumps({
216
- "phase": "listen", # listen | quiz | pass | fail
217
- "listen_start": None,
218
- "question_idx": None,
219
- "attempts": 0,
220
- })
221
-
222
- def S(s): return json.loads(s)
223
- def D(d): return json.dumps(d)
224
-
225
- # ── TTS français ──────────────────────────────────────────────────────────────
226
  _audio_cache = None
227
  _audio_lock = threading.Lock()
228
 
@@ -243,198 +216,182 @@ def get_lecture_audio():
243
  print(f"[law-app] gTTS erreur : {e}")
244
  return None
245
 
 
 
 
 
 
 
 
 
 
 
 
246
  # ── Évaluation Qwen3 ──────────────────────────────────────────────────────────
247
- def evaluate_answer(question: str, answer: str, hint: str) -> tuple[int, str]:
248
- """Retourne (score_0_10, feedback_str)."""
249
  llm = get_llm()
250
  if llm is None:
251
- # fallback basique si pas de modèle
252
- keywords = [w.lower() for w in hint.split(",")]
253
  ans_low = answer.lower()
254
- hits = sum(1 for k in keywords if k.strip() in ans_low)
255
  score = min(10, round((hits / max(len(keywords), 1)) * 10))
256
- fb = f"(Évaluation sans IA – mots-clés trouvés : {hits}/{len(keywords)})"
257
- return score, fb
258
-
259
- prompt = f"""Tu es un professeur de droit strict mais juste.
260
- Question posée à l'étudiant :
261
- {question}
262
-
263
- Réponse de l'étudiant :
264
- {answer}
265
-
266
- Éléments attendus dans une bonne réponse :
267
- {hint}
268
-
269
- Évalue la réponse de l'étudiant sur une échelle de 0 à 10.
270
- Réponds UNIQUEMENT avec un objet JSON valide sur une seule ligne, sans aucun texte avant ou après, au format :
271
- {{"score": <entier 0-10>, "feedback": "<commentaire bref en français>"}}"""
272
-
273
  try:
274
- out = llm(prompt, max_tokens=200, temperature=0.1, stop=["###", "\n\n"])
275
- text = out["choices"][0]["text"].strip()
276
- # extract JSON robustly
277
- start = text.find("{")
278
- end = text.rfind("}") + 1
279
- data = json.loads(text[start:end])
280
- score = max(0, min(10, int(data.get("score", 0))))
281
- fb = data.get("feedback", "")
282
- return score, fb
283
  except Exception as e:
284
  print(f"[law-app] Erreur évaluation : {e}")
285
  return 0, "Erreur lors de l'évaluation. Veuillez réessayer."
286
 
287
- # ── Fonctions Gradio ──────────────────────────────────────────────────────────
288
 
289
  def start_listening(ss):
290
  state = S(ss)
291
  state["phase"] = "listen"
292
  state["listen_start"] = time.time()
293
- state["question_idx"] = None
294
  audio = get_lecture_audio()
295
  msg = (
296
  "🎧 **Cours en cours d'écoute…**\n\n"
297
- f"Vous devez écouter pendant **15 minutes** avant de pouvoir répondre.\n\n"
298
- "Le bouton « Passer au Quiz » sera actif après la durée requise."
299
  )
300
  return (
301
  audio,
302
- gr.update(visible=True, value=msg),
303
- gr.update(visible=False), # quiz_col
304
- gr.update(visible=False), # result_col
 
305
  gr.update(interactive=False, value="⏳ Écoute en cours…"),
306
  D(state),
307
  )
308
 
309
 
310
  def check_timer(ss):
311
- """Appelé par un timer JS ou le bouton ; retourne si le quiz peut s'ouvrir."""
312
- state = S(ss)
313
  if state["phase"] != "listen" or state["listen_start"] is None:
314
- elapsed = LISTEN_SECONDS
315
- else:
316
- elapsed = time.time() - state["listen_start"]
317
  remaining = max(0, LISTEN_SECONDS - elapsed)
318
- minutes = int(remaining // 60)
319
- seconds = int(remaining % 60)
320
  if remaining <= 0:
321
  return (
322
- gr.update(interactive=True, value="✅ Accéder au Quiz"),
323
- f"✅ Durée d'écoute atteinte ! Cliquez sur **Accéder au Quiz**.",
324
  ss,
325
  )
326
  return (
327
- gr.update(interactive=False, value=f"⏳ Encore {minutes:02d}:{seconds:02d}"),
328
- f"🎧 Veuillez patienter encore **{minutes} min {seconds:02d} s**.",
329
  ss,
330
  )
331
 
332
 
333
  def open_quiz(ss):
334
- state = S(ss)
335
- elapsed = (time.time() - (state["listen_start"] or 0))
336
  if elapsed < LISTEN_SECONDS:
337
- remaining = int(LISTEN_SECONDS - elapsed)
338
- m, s = divmod(remaining, 60)
339
  return (
340
- gr.update(visible=True), # listen_col
341
- gr.update(visible=False), # quiz_col
342
- gr.update(visible=False), # result_col
343
- f"⚠️ Patientez encore **{m} min {s:02d} s** avant d'accéder au quiz.",
344
  "", "", ss,
345
  )
346
- idx = random.randrange(len(QUESTIONS))
347
  state["phase"] = "quiz"
348
  state["question_idx"] = idx
349
- q_text = QUESTIONS[idx]["q"]
350
  return (
351
- gr.update(visible=False), # listen_col
352
- gr.update(visible=True), # quiz_col
353
- gr.update(visible=False), # result_col
354
  "",
355
- q_text,
356
  "",
357
  D(state),
358
  )
359
 
360
 
361
- def submit_answer(answer: str, ss: str):
362
  state = S(ss)
363
  if state["phase"] != "quiz":
364
- return gr.update(visible=False), gr.update(visible=True), "", "", ss
365
 
366
- idx = state["question_idx"]
367
- q = QUESTIONS[idx]
368
- score, feedback = evaluate_answer(q["q"], answer, q["hint"])
369
- state["attempts"] += 1
370
 
371
  if score >= PASS_SCORE:
372
  state["phase"] = "pass"
373
- result_md = (
374
  f"## 🎉 Félicitations ! Vous avez réussi.\n\n"
375
  f"**Score : {score}/10**\n\n"
376
- f"**Commentaire :** {feedback}\n\n"
377
- f"---\n*Vous maîtrisez bien le contenu de ce cours de droit.*"
378
  )
379
- btn_label = "🔄 Recommencer"
380
  else:
381
  state["phase"] = "fail"
382
- result_md = (
383
  f"## ❌ Score insuffisant : {score}/10\n\n"
384
- f"**Commentaire :** {feedback}\n\n"
385
  f"---\n"
386
- f"Vous devez obtenir au moins **{PASS_SCORE}/10** pour valider ce cours.\n\n"
387
  f"⬇️ Veuillez **réécouter l'intégralité du cours** (15 minutes) "
388
  f"avant de tenter à nouveau le quiz."
389
  )
390
- btn_label = "🔄 Réécouter le cours"
391
 
392
  return (
393
- gr.update(visible=False), # quiz_col
394
- gr.update(visible=True), # result_col
395
- result_md,
396
- btn_label,
397
  D(state),
398
  )
399
 
400
 
401
  def restart(ss):
402
- """Relance depuis l'écoute."""
403
  return start_listening(fresh())
404
 
405
 
406
  # ── Interface ─────────────────────────────────────────────────────────────────
407
- CSS = """
408
- footer { display: none !important; }
409
- #title { text-align: center; }
410
- #timer_btn { min-width: 220px; }
411
- """
412
 
413
- with gr.Blocks(title="Cours de Droit – Formation Juridique", css=CSS) as demo:
414
 
415
  session = gr.State(fresh())
416
 
417
  gr.Markdown(
418
  "# ⚖️ Cours de Droit — Formation Juridique\n"
419
- "Écoutez le cours en français · Répondez à une question · Obtenez une note de 0 à 10",
420
  elem_id="title",
421
  )
422
 
423
- # ── Phase 1 : Écoute ──────────────────────────────────────────────────────
424
- with gr.Column(visible=True) as listen_col:
425
  gr.Markdown("## 🎧 Étape 1 — Écoute du cours (15 minutes)")
426
- listen_msg = gr.Markdown("Cliquez sur **Démarrer l'écoute** pour commencer.")
427
- audio_out = gr.Audio(label="Cours audio (français)", autoplay=False, interactive=False)
428
- timer_info = gr.Markdown("")
429
-
430
  with gr.Row():
431
  start_btn = gr.Button("▶️ Démarrer l'écoute", variant="primary")
432
- quiz_btn = gr.Button(" Accéder au Quiz", variant="secondary",
433
- interactive=False, elem_id="timer_btn")
434
- check_btn = gr.Button("🔄 Vérifier le temps", variant="secondary")
435
 
436
- # ── Phase 2 : Quiz ────────────────────────────────────────────────────────
437
- with gr.Column(visible=False) as quiz_col:
438
  gr.Markdown("## 📝 Étape 2 — Question d'évaluation")
439
  question_md = gr.Markdown("")
440
  answer_box = gr.Textbox(
@@ -442,41 +399,28 @@ with gr.Blocks(title="Cours de Droit – Formation Juridique", css=CSS) as demo:
442
  placeholder="Rédigez votre réponse ici…",
443
  lines=5,
444
  )
445
- submit_btn = gr.Button("📤 Soumettre la réponse", variant="primary")
446
 
447
- # ── Phase 3 : Résultat ────────────────────────────────────────────────────
448
- with gr.Column(visible=False) as result_col:
449
  gr.Markdown("## 📊 Résultat")
450
  result_md = gr.Markdown("")
451
  restart_btn = gr.Button("🔄 Recommencer", variant="secondary")
452
 
453
  # ── Câblage ───────────────────────────────────────────────────────────────
454
- LISTEN_OUTS = [audio_out, listen_msg, quiz_col, result_col, quiz_btn, session]
455
-
456
- start_btn.click(
457
- start_listening, [session], LISTEN_OUTS
458
- )
459
 
460
- check_btn.click(
461
- check_timer, [session], [quiz_btn, timer_info, session]
462
- )
463
 
464
- QUIZ_OUTS = [listen_col, quiz_col, result_col, timer_info, question_md, answer_box, session]
465
- quiz_btn.click(
466
- open_quiz, [session], QUIZ_OUTS
467
- )
468
 
469
- RESULT_OUTS = [quiz_col, result_col, result_md, restart_btn, session]
470
- submit_btn.click(
471
- submit_answer, [answer_box, session], RESULT_OUTS
472
- )
473
- answer_box.submit(
474
- submit_answer, [answer_box, session], RESULT_OUTS
475
- )
476
 
477
- restart_btn.click(
478
- restart, [session], LISTEN_OUTS
479
- )
480
 
481
  if __name__ == "__main__":
482
- demo.launch(server_name="0.0.0.0", server_port=7860)
 
7
  3. Qwen3-0.6B-Q4_K_M (llama-cpp) évalue la réponse de 0 à 10.
8
  4. Score ≥ 6 → réussi. Score < 6 → retour au début, réécoute obligatoire.
9
  Multi-utilisateur sécurisé via gr.State (JSON par session).
10
+ Compatible gradio 5.x + Python 3.13.
11
  """
12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  import os, glob, json, time, tempfile, random, threading
14
+ import gradio as gr
15
  from gtts import gTTS
16
 
17
  try:
 
21
  _LLAMA_OK = False
22
 
23
  # ── LLM singleton ─────────────────────────────────────────────────────────────
24
+ _llm = None
25
  _llm_lock = threading.Lock()
26
 
27
  def get_llm():
 
50
 
51
  # ── Contenu du cours (droit civil français) ───────────────────────────────────
52
  LECTURE_TEXT = """
53
+ Cours de droit civil. Introduction aux sources du droit français.
54
 
55
  Première partie : Les sources formelles du droit.
56
 
57
+ Le droit français repose sur plusieurs sources hiérarchisées.
58
+ Au sommet se trouve la Constitution du 4 octobre 1958, loi fondamentale
59
+ de la République française. Elle établit les règles d'organisation de
60
  l'État et garantit les droits fondamentaux des citoyens.
61
 
62
+ Sous la Constitution, on trouve les traités internationaux et le droit
63
+ de l'Union européenne. En vertu de l'article 55 de la Constitution,
64
+ les traités régulièrement ratifiés ont une autorité supérieure à celle
65
  des lois ordinaires.
66
 
67
+ Vient ensuite la loi, votée par le Parlement. La loi ordinaire est
68
+ adoptée par l'Assemblée nationale et le Sénat selon la procédure
69
+ législative prévue par la Constitution. Les lois organiques,
70
+ quant à elles, complètent et précisent certaines dispositions
71
  constitutionnelles et bénéficient d'un statut intermédiaire.
72
 
73
+ Le règlement constitue la quatrième source majeure. Il émane du
74
+ pouvoir exécutif : décrets du Président de la République et du
75
+ Premier ministre, arrêtés ministériels, préfectoraux et municipaux.
76
+ L'article 37 de la Constitution confère au gouvernement un domaine
77
  réglementaire autonome.
78
 
79
  Deuxième partie : Les sources matérielles du droit.
80
 
81
+ La jurisprudence, bien qu'elle ne soit pas officiellement reconnue
82
+ comme source formelle, joue un rôle fondamental dans le système
83
+ juridique français. Les décisions de la Cour de cassation et du
84
+ Conseil d'État interprètent et précisent la portée des textes légaux.
85
+ Le Conseil constitutionnel, par ses décisions, contrôle la
86
  constitutionnalité des lois.
87
 
88
+ La doctrine regroupe l'ensemble des travaux des juristes :
89
+ professeurs, avocats, magistrats. Elle propose des analyses,
90
+ des systématisations et des critiques du droit positif.
91
+ Sans force obligatoire, elle influence néanmoins la jurisprudence
92
  et le législateur.
93
 
94
+ La coutume, source ancienne, est aujourd'hui d'application limitée.
95
+ Elle consiste en une pratique ancienne, constante et répétée,
96
+ acceptée comme obligatoire par la population. En droit commercial,
97
  les usages professionnels conservent une importance certaine.
98
 
99
  Troisième partie : Le principe de hiérarchie des normes.
100
 
101
+ Hans Kelsen, juriste autrichien du vingtième siècle, a théorisé
102
+ la pyramide des normes. Selon cette théorie, chaque norme tire
103
+ sa validité d'une norme supérieure. En France, le Conseil
104
+ constitutionnel assure le respect de cette hiérarchie en contrôlant
105
  la conformité des lois à la Constitution.
106
 
107
+ Le contrôle de légalité permet quant à lui de vérifier que les
108
+ actes administratifs sont conformes aux lois qui leur sont
109
+ supérieures. Tout acte contraire à une norme supérieure est
110
  susceptible d'être annulé ou écarté.
111
 
112
  Quatrième partie : Le droit des contrats.
113
 
114
+ Le contrat est un accord de volontés entre deux ou plusieurs
115
+ personnes destiné à créer, modifier, transmettre ou éteindre
116
+ des obligations. Le Code civil, dans sa rédaction issue de
117
+ l'ordonnance du 10 février 2016, définit les conditions de
118
+ validité du contrat : le consentement des parties, leur capacité
119
  à contracter, et un contenu licite et certain.
120
 
121
+ Le consentement doit être libre et éclairé. Il est vicié lorsqu'il
122
+ a été donné par erreur, obtenu par dol ou arraché par violence.
123
+ Dans ces cas, le contrat peut être annulé à la demande de la
124
  partie dont le consentement a été vicié.
125
 
126
+ La capacité contractuelle est la règle. L'incapacité est
127
+ l'exception et concerne principalement les mineurs non émancipés
128
  et les majeurs protégés sous tutelle ou curatelle.
129
 
130
+ L'exécution du contrat obéit au principe de la force obligatoire :
131
+ le contrat tient lieu de loi entre les parties qui l'ont conclu.
132
+ En cas d'inexécution, la partie lésée dispose de plusieurs recours :
133
  l'exécution forcée, la résolution du contrat ou des dommages et intérêts.
134
 
135
  Cinquième partie : La responsabilité civile.
136
 
137
+ La responsabilité civile a pour fonction de réparer le préjudice
138
+ causé à autrui. Elle se distingue de la responsabilité pénale
139
  qui vise à sanctionner l'auteur d'une infraction.
140
 
141
+ La responsabilité délictuelle repose sur trois conditions :
142
+ une faute, un dommage et un lien de causalité entre les deux.
143
+ L'article 1240 du Code civil dispose que tout fait quelconque
144
+ de l'homme qui cause à autrui un dommage oblige son auteur
145
  à le réparer.
146
 
147
+ La responsabilité sans faute, dite objective, est reconnue
148
+ dans certains cas : responsabilité du fait des choses,
149
+ responsabilité du fait d'autrui, responsabilité des produits
150
+ défectueux. L'évolution jurisprudentielle et législative tend
151
  à élargir ces hypothèses pour mieux protéger les victimes.
152
 
153
+ Conclusion : Le droit est un système dynamique en constante évolution.
154
+ La maîtrise de ses sources et de ses principes fondamentaux est
155
+ indispensable à tout juriste. La rigueur dans l'analyse textuelle,
156
+ combinée à la connaissance de la jurisprudence, permet d'appliquer
157
  le droit de manière correcte et juste.
158
  """
159
 
 
160
  QUESTIONS = [
161
  {
162
  "q": "Quelle est la source au sommet de la hiérarchie des normes en droit français, et quelle est sa date ?",
 
176
  },
177
  {
178
  "q": "Quelles sont les trois conditions nécessaires pour engager la responsabilité délictuelle ?",
179
+ "hint": "faute, dommage, lien de causalité, article 1240 Code civil",
180
  },
181
  {
182
  "q": "Expliquez la différence entre la responsabilité civile et la responsabilité pénale.",
 
193
  ]
194
 
195
  LISTEN_SECONDS = 15 * 60 # 15 minutes
196
+ PASS_SCORE = 6
197
 
198
+ # ── Audio cache (partagé entre sessions) ─────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
199
  _audio_cache = None
200
  _audio_lock = threading.Lock()
201
 
 
216
  print(f"[law-app] gTTS erreur : {e}")
217
  return None
218
 
219
+ # ── État de session ────────────────────────────────────────────────────────────
220
+ def fresh():
221
+ return json.dumps({
222
+ "phase": "idle",
223
+ "listen_start": None,
224
+ "question_idx": None,
225
+ })
226
+
227
+ def S(s): return json.loads(s)
228
+ def D(d): return json.dumps(d)
229
+
230
  # ── Évaluation Qwen3 ──────────────────────────────────────────────────────────
231
+ def evaluate_answer(question: str, answer: str, hint: str):
 
232
  llm = get_llm()
233
  if llm is None:
234
+ keywords = [w.strip().lower() for w in hint.split(",")]
 
235
  ans_low = answer.lower()
236
+ hits = sum(1 for k in keywords if k in ans_low)
237
  score = min(10, round((hits / max(len(keywords), 1)) * 10))
238
+ return score, f"(Évaluation sans IA – mots-clés trouvés : {hits}/{len(keywords)})"
239
+
240
+ prompt = (
241
+ f"Tu es un professeur de droit strict mais juste.\n"
242
+ f"Question : {question}\n"
243
+ f"Réponse de l'étudiant : {answer}\n"
244
+ f"Éléments attendus : {hint}\n\n"
245
+ "Évalue la réponse de 0 à 10. Réponds UNIQUEMENT avec un JSON sur une seule ligne, "
246
+ 'sans texte avant ni après : {"score": <entier 0-10>, "feedback": "<commentaire bref en français>"}'
247
+ )
 
 
 
 
 
 
 
248
  try:
249
+ out = llm(prompt, max_tokens=200, temperature=0.1, stop=["###", "\n\n"])
250
+ text = out["choices"][0]["text"].strip()
251
+ data = json.loads(text[text.find("{"):text.rfind("}")+1])
252
+ return max(0, min(10, int(data["score"]))), data.get("feedback", "")
 
 
 
 
 
253
  except Exception as e:
254
  print(f"[law-app] Erreur évaluation : {e}")
255
  return 0, "Erreur lors de l'évaluation. Veuillez réessayer."
256
 
257
+ # ── Handlers ──────────────────────────────────────────────────────────────────
258
 
259
  def start_listening(ss):
260
  state = S(ss)
261
  state["phase"] = "listen"
262
  state["listen_start"] = time.time()
 
263
  audio = get_lecture_audio()
264
  msg = (
265
  "🎧 **Cours en cours d'écoute…**\n\n"
266
+ "Vous devez écouter pendant **15 minutes** complètes avant d'accéder au quiz.\n\n"
267
+ "Cliquez sur **🔄 Vérifier le temps** pour voir combien il reste."
268
  )
269
  return (
270
  audio,
271
+ msg,
272
+ gr.update(visible=True), # listen_group
273
+ gr.update(visible=False), # quiz_group
274
+ gr.update(visible=False), # result_group
275
  gr.update(interactive=False, value="⏳ Écoute en cours…"),
276
  D(state),
277
  )
278
 
279
 
280
  def check_timer(ss):
281
+ state = S(ss)
 
282
  if state["phase"] != "listen" or state["listen_start"] is None:
283
+ return gr.update(), "⚠️ Lancez d'abord l'écoute.", ss
284
+ elapsed = time.time() - state["listen_start"]
 
285
  remaining = max(0, LISTEN_SECONDS - elapsed)
286
+ m, s = divmod(int(remaining), 60)
 
287
  if remaining <= 0:
288
  return (
289
+ gr.update(interactive=True, value="✅ Accéder au Quiz"),
290
+ "✅ Durée d'écoute atteinte ! Cliquez sur **Accéder au Quiz**.",
291
  ss,
292
  )
293
  return (
294
+ gr.update(interactive=False, value=f"⏳ Encore {m:02d}:{s:02d}"),
295
+ f"🎧 Patientez encore **{m} min {s:02d} s**.",
296
  ss,
297
  )
298
 
299
 
300
  def open_quiz(ss):
301
+ state = S(ss)
302
+ elapsed = time.time() - (state["listen_start"] or 0)
303
  if elapsed < LISTEN_SECONDS:
304
+ m, s = divmod(int(LISTEN_SECONDS - elapsed), 60)
 
305
  return (
306
+ gr.update(visible=True),
307
+ gr.update(visible=False),
308
+ gr.update(visible=False),
309
+ f"⚠️ Patientez encore **{m} min {s:02d} s**.",
310
  "", "", ss,
311
  )
312
+ idx = random.randrange(len(QUESTIONS))
313
  state["phase"] = "quiz"
314
  state["question_idx"] = idx
 
315
  return (
316
+ gr.update(visible=False),
317
+ gr.update(visible=True),
318
+ gr.update(visible=False),
319
  "",
320
+ QUESTIONS[idx]["q"],
321
  "",
322
  D(state),
323
  )
324
 
325
 
326
+ def submit_answer(answer, ss):
327
  state = S(ss)
328
  if state["phase"] != "quiz":
329
+ return gr.update(visible=True), gr.update(visible=False), "", "", ss
330
 
331
+ idx = state["question_idx"]
332
+ q = QUESTIONS[idx]
333
+ score, fb = evaluate_answer(q["q"], answer, q["hint"])
 
334
 
335
  if score >= PASS_SCORE:
336
  state["phase"] = "pass"
337
+ result = (
338
  f"## 🎉 Félicitations ! Vous avez réussi.\n\n"
339
  f"**Score : {score}/10**\n\n"
340
+ f"**Commentaire :** {fb}\n\n"
341
+ f"---\n*Vous maîtrisez bien ce cours de droit civil.*"
342
  )
343
+ btn = "🔄 Recommencer"
344
  else:
345
  state["phase"] = "fail"
346
+ result = (
347
  f"## ❌ Score insuffisant : {score}/10\n\n"
348
+ f"**Commentaire :** {fb}\n\n"
349
  f"---\n"
350
+ f"Seuil requis : **{PASS_SCORE}/10**.\n\n"
351
  f"⬇️ Veuillez **réécouter l'intégralité du cours** (15 minutes) "
352
  f"avant de tenter à nouveau le quiz."
353
  )
354
+ btn = "🔄 Réécouter le cours"
355
 
356
  return (
357
+ gr.update(visible=False),
358
+ gr.update(visible=True),
359
+ result,
360
+ btn,
361
  D(state),
362
  )
363
 
364
 
365
  def restart(ss):
 
366
  return start_listening(fresh())
367
 
368
 
369
  # ── Interface ─────────────────────────────────────────────────────────────────
370
+ CSS = "footer{display:none!important;} #title{text-align:center;}"
 
 
 
 
371
 
372
+ with gr.Blocks(title="Cours de Droit", css=CSS) as demo:
373
 
374
  session = gr.State(fresh())
375
 
376
  gr.Markdown(
377
  "# ⚖️ Cours de Droit — Formation Juridique\n"
378
+ "Écoutez le cours en français · Répondez à une question · Obtenez une note sur 10",
379
  elem_id="title",
380
  )
381
 
382
+ # ── Phase écoute ─────────────────────────────────────────────────────────
383
+ with gr.Group(visible=True) as listen_group:
384
  gr.Markdown("## 🎧 Étape 1 — Écoute du cours (15 minutes)")
385
+ listen_msg = gr.Markdown("Cliquez sur **Démarrer l'écoute** pour commencer.")
386
+ audio_out = gr.Audio(label="Cours audio (français)", autoplay=False, interactive=False)
387
+ timer_info = gr.Markdown("")
 
388
  with gr.Row():
389
  start_btn = gr.Button("▶️ Démarrer l'écoute", variant="primary")
390
+ check_btn = gr.Button("🔄 Vérifier le temps", variant="secondary")
391
+ quiz_btn = gr.Button("✅ Accéder au Quiz", variant="secondary", interactive=False)
 
392
 
393
+ # ── Phase quiz ───────────────────────────────────────────────────────────
394
+ with gr.Group(visible=False) as quiz_group:
395
  gr.Markdown("## 📝 Étape 2 — Question d'évaluation")
396
  question_md = gr.Markdown("")
397
  answer_box = gr.Textbox(
 
399
  placeholder="Rédigez votre réponse ici…",
400
  lines=5,
401
  )
402
+ submit_btn = gr.Button("📤 Soumettre la réponse", variant="primary")
403
 
404
+ # ── Phase résultat ───────────────────────────────────────────────────────
405
+ with gr.Group(visible=False) as result_group:
406
  gr.Markdown("## 📊 Résultat")
407
  result_md = gr.Markdown("")
408
  restart_btn = gr.Button("🔄 Recommencer", variant="secondary")
409
 
410
  # ── Câblage ───────────────────────────────────────────────────────────────
411
+ START_OUT = [audio_out, listen_msg, listen_group, quiz_group, result_group, quiz_btn, session]
 
 
 
 
412
 
413
+ start_btn.click(start_listening, [session], START_OUT)
414
+ check_btn.click(check_timer, [session], [quiz_btn, timer_info, session])
 
415
 
416
+ QUIZ_OUT = [listen_group, quiz_group, result_group, timer_info, question_md, answer_box, session]
417
+ quiz_btn.click(open_quiz, [session], QUIZ_OUT)
 
 
418
 
419
+ RESULT_OUT = [quiz_group, result_group, result_md, restart_btn, session]
420
+ submit_btn.click(submit_answer, [answer_box, session], RESULT_OUT)
421
+ answer_box.submit(submit_answer, [answer_box, session], RESULT_OUT)
 
 
 
 
422
 
423
+ restart_btn.click(restart, [session], START_OUT)
 
 
424
 
425
  if __name__ == "__main__":
426
+ demo.launch(server_name="0.0.0.0", server_port=7860)