ramedde commited on
Commit
ce65a6e
·
verified ·
1 Parent(s): 30ea39e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +128 -204
app.py CHANGED
@@ -1,17 +1,34 @@
1
  """
2
  Cours de Droit – Hugging Face Space
3
- ===================================
4
- Workflow:
5
- 1. L'étudiant écoute le cours audio (français, gTTS) pendant 15 minutes.
6
- 2. Après 15 min une question aléatoire apparaît (réponse ouverte).
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:
@@ -35,9 +52,9 @@ def get_llm():
35
  return _llm
36
  paths = glob.glob("/app/*.gguf") + glob.glob("*.gguf")
37
  if not paths:
38
- print("[law-app] Aucun fichier .gguf trouvé – évaluation IA désactivée.")
39
  return None
40
- print(f"[law-app] Chargement du modèle : {paths[0]}")
41
  _llm = Llama(
42
  model_path=paths[0],
43
  n_ctx=1024,
@@ -48,7 +65,7 @@ def get_llm():
48
  print("[law-app] Modèle prêt.")
49
  return _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
 
@@ -82,120 +99,75 @@ 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 ?",
163
- "hint": "Constitution, 4 octobre 1958, loi fondamentale",
164
- },
165
- {
166
- "q": "Quelles sont les trois conditions de validité d'un contrat selon le Code civil réformé en 2016 ?",
167
- "hint": "consentement, capacité, contenu licite et certain",
168
- },
169
- {
170
- "q": "Qu'est-ce que la pyramide des normes et qui en est le théoricien ?",
171
- "hint": "Hans Kelsen, hiérarchie des normes, validité tirée d'une norme supérieure",
172
- },
173
- {
174
- "q": "Quels sont les trois vices du consentement qui peuvent entraîner la nullité d'un contrat ?",
175
- "hint": "erreur, dol, violence",
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.",
183
- "hint": "civile répare le préjudice, pénale sanctionne l'auteur d'une infraction",
184
- },
185
- {
186
- "q": "Quel est le rôle de la jurisprudence dans le système juridique français ?",
187
- "hint": "interprète et précise la portée des textes légaux, Cour de cassation, Conseil d'État",
188
- },
189
- {
190
- "q": "Quelles personnes sont frappées d'incapacité contractuelle en droit français ?",
191
- "hint": "mineurs non émancipés, majeurs protégés sous tutelle ou curatelle",
192
- },
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,78 +188,66 @@ def get_lecture_audio():
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 (
@@ -296,49 +256,33 @@ def check_timer(ss):
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:
@@ -346,43 +290,30 @@ def submit_answer(answer, ss):
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():
@@ -390,37 +321,30 @@ with gr.Blocks(title="Cours de Droit", css=CSS) as demo:
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(
398
- label="Votre réponse",
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)
 
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
+ # ── Fix Python 3.13: audioop supprimé, pyaudioop le remplace ─────────────────
7
+ import sys
8
+ try:
9
+ import pyaudioop
10
+ sys.modules["audioop"] = pyaudioop
11
+ except ImportError:
12
+ pass
13
+ # ─────────────────────────────────────────────────────────────────────────────
14
+
15
  import gradio as gr
16
+
17
+ # ── Patch gradio_client 0.8 schema bug (gradio 4.44) ─────────────────────────
18
+ try:
19
+ import gradio_client.utils as _gcu
20
+ import gradio.blocks as _gb
21
+ _orig = _gcu.json_schema_to_python_type
22
+ def _safe(schema):
23
+ try: return _orig(schema)
24
+ except Exception: return "Any"
25
+ _gcu.json_schema_to_python_type = _safe
26
+ _gb.client_utils.json_schema_to_python_type = _safe
27
+ except Exception:
28
+ pass
29
+ # ─────────────────────────────────────────────────────────────────────────────
30
+
31
+ import os, glob, json, time, tempfile, random, threading
32
  from gtts import gTTS
33
 
34
  try:
 
52
  return _llm
53
  paths = glob.glob("/app/*.gguf") + glob.glob("*.gguf")
54
  if not paths:
55
+ print("[law-app] Aucun .gguf trouvé – évaluation IA désactivée.")
56
  return None
57
+ print(f"[law-app] Chargement : {paths[0]}")
58
  _llm = Llama(
59
  model_path=paths[0],
60
  n_ctx=1024,
 
65
  print("[law-app] Modèle prêt.")
66
  return _llm
67
 
68
+ # ── Texte du cours ────────────────────────────────────────────────────────────
69
  LECTURE_TEXT = """
70
  Cours de droit civil. Introduction aux sources du droit français.
71
 
 
99
  comme source formelle, joue un rôle fondamental dans le système
100
  juridique français. Les décisions de la Cour de cassation et du
101
  Conseil d'État interprètent et précisent la portée des textes légaux.
102
+ Le Conseil constitutionnel contrôle la constitutionnalité des lois.
 
103
 
104
  La doctrine regroupe l'ensemble des travaux des juristes :
105
+ professeurs, avocats, magistrats. Sans force obligatoire, elle influence
106
+ néanmoins la jurisprudence et le législateur.
 
 
107
 
108
  La coutume, source ancienne, est aujourd'hui d'application limitée.
109
+ En droit commercial, les usages professionnels conservent une importance certaine.
 
 
110
 
111
  Troisième partie : Le principe de hiérarchie des normes.
112
 
113
  Hans Kelsen, juriste autrichien du vingtième siècle, a théorisé
114
+ la pyramide des normes. Chaque norme tire sa validité d'une norme supérieure.
115
+ En France, le Conseil constitutionnel assure le respect de cette hiérarchie.
 
 
 
 
 
 
 
116
 
117
  Quatrième partie : Le droit des contrats.
118
 
119
+ Le contrat est un accord de volontés destiné à créer, modifier, transmettre
120
+ ou éteindre des obligations. Le Code civil, issu de l'ordonnance du 10 février 2016,
121
+ définit trois conditions de validité : le consentement des parties, leur capacité
 
 
122
  à contracter, et un contenu licite et certain.
123
 
124
+ Le consentement doit être libre et éclairé. Il est vicié par l'erreur, le dol
125
+ ou la violence. L'incapacité concerne les mineurs non émancipés et les majeurs
126
+ protégés sous tutelle ou curatelle.
 
127
 
128
+ L'exécution obéit au principe de la force obligatoire : le contrat tient lieu
129
+ de loi entre les parties. En cas d'inexécution : exécution forcée, résolution
130
+ ou dommages et intérêts.
 
 
 
 
 
131
 
132
  Cinquième partie : La responsabilité civile.
133
 
134
+ La responsabilité civile répare le préjudice causé à autrui, contrairement
135
+ à la responsabilité pénale qui sanctionne l'auteur d'une infraction.
136
+
137
+ La responsabilité délictuelle repose sur trois conditions : faute, dommage
138
+ et lien de causalité. L'article 1240 du Code civil dispose que tout fait
139
+ de l'homme causant un dommage oblige son auteur à le réparer.
140
+
141
+ La responsabilité sans faute couvre le fait des choses, le fait d'autrui
142
+ et les produits défectueux.
143
+
144
+ Conclusion : la maîtrise des sources et des principes fondamentaux du droit
145
+ est indispensable à tout juriste.
 
 
 
 
 
 
 
 
 
146
  """
147
 
148
  QUESTIONS = [
149
+ {"q": "Quelle est la source au sommet de la hiérarchie des normes en droit français, et quelle est sa date ?",
150
+ "hint": "Constitution, 4 octobre 1958, loi fondamentale"},
151
+ {"q": "Quelles sont les trois conditions de validité d'un contrat selon le Code civil réformé en 2016 ?",
152
+ "hint": "consentement, capacité, contenu licite et certain"},
153
+ {"q": "Qu'est-ce que la pyramide des normes et qui en est le théoricien ?",
154
+ "hint": "Hans Kelsen, hiérarchie des normes, validité tirée d'une norme supérieure"},
155
+ {"q": "Quels sont les trois vices du consentement qui peuvent entraîner la nullité d'un contrat ?",
156
+ "hint": "erreur, dol, violence"},
157
+ {"q": "Quelles sont les trois conditions de la responsabilité délictuelle ?",
158
+ "hint": "faute, dommage, lien de causalité, article 1240 Code civil"},
159
+ {"q": "Quelle est la différence entre responsabilité civile et responsabilité pénale ?",
160
+ "hint": "civile répare le préjudice, pénale sanctionne l'auteur d'une infraction"},
161
+ {"q": "Quel est le rôle de la jurisprudence dans le système juridique français ?",
162
+ "hint": "interprète et précise les textes légaux, Cour de cassation, Conseil d'État"},
163
+ {"q": "Quelles personnes sont frappées d'incapacité contractuelle en droit français ?",
164
+ "hint": "mineurs non émancipés, majeurs protégés sous tutelle ou curatelle"},
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
165
  ]
166
 
167
+ LISTEN_SECONDS = 15 * 60
168
  PASS_SCORE = 6
169
 
170
+ # ── Audio cache ───────────────────────────────────────────────────────────────
171
  _audio_cache = None
172
  _audio_lock = threading.Lock()
173
 
 
188
  print(f"[law-app] gTTS erreur : {e}")
189
  return None
190
 
191
+ # ── État ──────────────────────────────────────────────────────────────────────
192
  def fresh():
193
+ return json.dumps({"phase": "idle", "listen_start": None, "question_idx": None})
 
 
 
 
194
 
195
  def S(s): return json.loads(s)
196
  def D(d): return json.dumps(d)
197
 
198
+ # ── Évaluation ────────────────────────────────────────────────────────────────
199
+ def evaluate_answer(question, answer, hint):
200
  llm = get_llm()
201
  if llm is None:
202
+ kw = [w.strip().lower() for w in hint.split(",")]
203
+ hits = sum(1 for k in kw if k in answer.lower())
204
+ score = min(10, round((hits / max(len(kw), 1)) * 10))
205
+ return score, f"(Sans IA mots-clés : {hits}/{len(kw)})"
 
 
206
  prompt = (
207
+ f"Tu es un professeur de droit.\n"
208
  f"Question : {question}\n"
209
+ f"Réponse : {answer}\n"
210
  f"Éléments attendus : {hint}\n\n"
211
+ "Réponds UNIQUEMENT avec ce JSON sur une ligne : "
212
+ '{"score": <0-10>, "feedback": "<commentaire bref en français>"}'
213
  )
214
  try:
215
+ out = llm(prompt, max_tokens=150, temperature=0.1, stop=["###", "\n\n"])
216
  text = out["choices"][0]["text"].strip()
217
  data = json.loads(text[text.find("{"):text.rfind("}")+1])
218
  return max(0, min(10, int(data["score"]))), data.get("feedback", "")
219
  except Exception as e:
220
+ print(f"[law-app] Erreur éval : {e}")
221
+ return 0, "Erreur d'évaluation, réessayez."
222
 
223
  # ── Handlers ──────────────────────────────────────────────────────────────────
 
224
  def start_listening(ss):
225
  state = S(ss)
226
+ state.update({"phase": "listen", "listen_start": time.time(), "question_idx": None})
 
227
  audio = get_lecture_audio()
228
  msg = (
229
  "🎧 **Cours en cours d'écoute…**\n\n"
230
+ "Écoutez pendant **15 minutes** complètes, puis cliquez sur **🔄 Vérifier le temps**."
 
231
  )
232
  return (
233
+ audio, msg,
234
+ gr.update(visible=True), # listen_col
235
+ gr.update(visible=False), # quiz_col
236
+ gr.update(visible=False), # result_col
 
237
  gr.update(interactive=False, value="⏳ Écoute en cours…"),
238
  D(state),
239
  )
240
 
 
241
  def check_timer(ss):
242
+ state = S(ss)
243
+ if state["phase"] != "listen" or not state["listen_start"]:
244
  return gr.update(), "⚠️ Lancez d'abord l'écoute.", ss
245
+ rem = max(0, LISTEN_SECONDS - (time.time() - state["listen_start"]))
246
+ m, s = divmod(int(rem), 60)
247
+ if rem <= 0:
 
248
  return (
249
  gr.update(interactive=True, value="✅ Accéder au Quiz"),
250
+ "✅ Durée atteinte ! Cliquez sur **Accéder au Quiz**.",
251
  ss,
252
  )
253
  return (
 
256
  ss,
257
  )
258
 
 
259
  def open_quiz(ss):
260
  state = S(ss)
261
  elapsed = time.time() - (state["listen_start"] or 0)
262
  if elapsed < LISTEN_SECONDS:
263
  m, s = divmod(int(LISTEN_SECONDS - elapsed), 60)
264
  return (
265
+ gr.update(visible=True), gr.update(visible=False), gr.update(visible=False),
266
+ f"⚠️ Patientez encore **{m} min {s:02d} s**.", "", "", ss,
 
 
 
267
  )
268
  idx = random.randrange(len(QUESTIONS))
269
+ state.update({"phase": "quiz", "question_idx": idx})
 
270
  return (
271
+ gr.update(visible=False), gr.update(visible=True), gr.update(visible=False),
272
+ "", QUESTIONS[idx]["q"], "", D(state),
 
 
 
 
 
273
  )
274
 
 
275
  def submit_answer(answer, ss):
276
  state = S(ss)
277
  if state["phase"] != "quiz":
278
  return gr.update(visible=True), gr.update(visible=False), "", "", ss
279
+ q = QUESTIONS[state["question_idx"]]
 
 
280
  score, fb = evaluate_answer(q["q"], answer, q["hint"])
 
281
  if score >= PASS_SCORE:
282
  state["phase"] = "pass"
283
  result = (
284
  f"## 🎉 Félicitations ! Vous avez réussi.\n\n"
285
+ f"**Score : {score}/10**\n\n**Commentaire :** {fb}"
 
 
286
  )
287
  btn = "🔄 Recommencer"
288
  else:
 
290
  result = (
291
  f"## ❌ Score insuffisant : {score}/10\n\n"
292
  f"**Commentaire :** {fb}\n\n"
293
+ f"Seuil requis : **{PASS_SCORE}/10**. "
294
+ f"Veuillez **réécouter le cours complet** avant de retenter."
 
 
295
  )
296
  btn = "🔄 Réécouter le cours"
297
+ return gr.update(visible=False), gr.update(visible=True), result, btn, D(state)
 
 
 
 
 
 
 
 
298
 
299
  def restart(ss):
300
  return start_listening(fresh())
301
 
302
+ # ── UI ────────────────────────────────────────────────────────────────────────
 
303
  CSS = "footer{display:none!important;} #title{text-align:center;}"
304
 
305
  with gr.Blocks(title="Cours de Droit", css=CSS) as demo:
 
306
  session = gr.State(fresh())
307
 
308
  gr.Markdown(
309
  "# ⚖️ Cours de Droit — Formation Juridique\n"
310
+ "Écoutez le cours · Répondez à la question · Note sur 10 par l'IA",
311
  elem_id="title",
312
  )
313
 
314
+ with gr.Column(visible=True) as listen_col:
 
315
  gr.Markdown("## 🎧 Étape 1 — Écoute du cours (15 minutes)")
316
+ listen_msg = gr.Markdown("Cliquez sur **Démarrer** pour commencer.")
317
  audio_out = gr.Audio(label="Cours audio (français)", autoplay=False, interactive=False)
318
  timer_info = gr.Markdown("")
319
  with gr.Row():
 
321
  check_btn = gr.Button("🔄 Vérifier le temps", variant="secondary")
322
  quiz_btn = gr.Button("✅ Accéder au Quiz", variant="secondary", interactive=False)
323
 
324
+ with gr.Column(visible=False) as quiz_col:
 
325
  gr.Markdown("## 📝 Étape 2 — Question d'évaluation")
326
  question_md = gr.Markdown("")
327
+ answer_box = gr.Textbox(label="Votre réponse", placeholder="Rédigez ici…", lines=5)
328
+ submit_btn = gr.Button("📤 Soumettre", variant="primary")
 
 
 
 
329
 
330
+ with gr.Column(visible=False) as result_col:
 
331
  gr.Markdown("## 📊 Résultat")
332
  result_md = gr.Markdown("")
333
  restart_btn = gr.Button("🔄 Recommencer", variant="secondary")
334
 
335
+ # Câblage
336
+ START = [audio_out, listen_msg, listen_col, quiz_col, result_col, quiz_btn, session]
337
+ start_btn.click(start_listening, [session], START)
 
338
  check_btn.click(check_timer, [session], [quiz_btn, timer_info, session])
339
 
340
+ QUIZ = [listen_col, quiz_col, result_col, timer_info, question_md, answer_box, session]
341
+ quiz_btn.click(open_quiz, [session], QUIZ)
342
 
343
+ RES = [quiz_col, result_col, result_md, restart_btn, session]
344
+ submit_btn.click(submit_answer, [answer_box, session], RES)
345
+ answer_box.submit(submit_answer, [answer_box, session], RES)
346
 
347
+ restart_btn.click(restart, [session], START)
348
 
349
  if __name__ == "__main__":
350
  demo.launch(server_name="0.0.0.0", server_port=7860)