ramedde commited on
Commit
147cb03
·
verified ·
1 Parent(s): 493d148

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +210 -77
app.py CHANGED
@@ -3,18 +3,15 @@ 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
@@ -26,7 +23,6 @@ try:
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
@@ -37,7 +33,6 @@ try:
37
  except ImportError:
38
  _LLAMA_OK = False
39
 
40
- # ── LLM singleton ─────────────────────────────────────────────────────────────
41
  _llm = None
42
  _llm_lock = threading.Lock()
43
 
@@ -65,8 +60,11 @@ def get_llm():
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
 
72
  Première partie : Les sources formelles du droit.
@@ -113,8 +111,24 @@ Troisième partie : Le principe de hiérarchie des normes.
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,
@@ -125,64 +139,107 @@ Le consentement doit être libre et éclairé. Il est vicié par l'erreur, le do
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 = 0
168
  PASS_SCORE = 6
169
 
170
- # ── Audio cache ───────────────────────────────────────────────────────────────
171
- _audio_cache = None
172
  _audio_lock = threading.Lock()
173
 
174
- def get_lecture_audio():
175
- global _audio_cache
176
- if _audio_cache and os.path.exists(_audio_cache):
177
- return _audio_cache
178
  with _audio_lock:
179
- if _audio_cache and os.path.exists(_audio_cache):
180
- return _audio_cache
181
  try:
182
- tts = gTTS(text=LECTURE_TEXT, lang="fr", slow=False)
 
183
  path = tempfile.mktemp(suffix=".mp3")
184
  tts.save(path)
185
- _audio_cache = path
186
  return path
187
  except Exception as e:
188
  print(f"[law-app] gTTS erreur : {e}")
@@ -190,7 +247,8 @@ def get_lecture_audio():
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)
@@ -223,10 +281,13 @@ def evaluate_answer(question, answer, hint):
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 (
@@ -234,6 +295,7 @@ def start_listening(ss):
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
  )
@@ -262,41 +324,96 @@ def open_quiz(ss):
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:
289
  state["phase"] = "fail"
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 ────────────────────────────────────────────────────────────────────────
@@ -307,12 +424,13 @@ with gr.Blocks(title="Cours de Droit", css=CSS) as demo:
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("")
@@ -321,30 +439,45 @@ with gr.Blocks(title="Cours de Droit", css=CSS) as demo:
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,show_error=True)
 
3
  Compatible avec le Docker HF (gradio 4.44.1 bundled + Python 3.13).
4
  """
5
 
 
6
  import sys
7
  try:
8
  import pyaudioop
9
  sys.modules["audioop"] = pyaudioop
10
  except ImportError:
11
  pass
 
12
 
13
  import gradio as gr
14
 
 
15
  try:
16
  import gradio_client.utils as _gcu
17
  import gradio.blocks as _gb
 
23
  _gb.client_utils.json_schema_to_python_type = _safe
24
  except Exception:
25
  pass
 
26
 
27
  import os, glob, json, time, tempfile, random, threading
28
  from gtts import gTTS
 
33
  except ImportError:
34
  _LLAMA_OK = False
35
 
 
36
  _llm = None
37
  _llm_lock = threading.Lock()
38
 
 
60
  print("[law-app] Modèle prêt.")
61
  return _llm
62
 
63
+ # ── Leçons ────────────────────────────────────────────────────────────────────
64
+ LESSONS = [
65
+ {
66
+ "title": "Leçon 1 — Sources du droit et hiérarchie des normes",
67
+ "text": """
68
  Cours de droit civil. Introduction aux sources du droit français.
69
 
70
  Première partie : Les sources formelles du droit.
 
111
  Hans Kelsen, juriste autrichien du vingtième siècle, a théorisé
112
  la pyramide des normes. Chaque norme tire sa validité d'une norme supérieure.
113
  En France, le Conseil constitutionnel assure le respect de cette hiérarchie.
114
+ """,
115
+ "questions": [
116
+ {"q": "Quelle est la source au sommet de la hiérarchie des normes en droit français, et quelle est sa date ?",
117
+ "hint": "Constitution, 4 octobre 1958, loi fondamentale"},
118
+ {"q": "Qu'est-ce que la pyramide des normes et qui en est le théoricien ?",
119
+ "hint": "Hans Kelsen, hiérarchie des normes, validité tirée d'une norme supérieure"},
120
+ {"q": "Quel est le rôle de la jurisprudence dans le système juridique français ?",
121
+ "hint": "interprète et précise les textes légaux, Cour de cassation, Conseil d'État"},
122
+ {"q": "Quelle différence y a-t-il entre une loi ordinaire et une loi organique ?",
123
+ "hint": "loi ordinaire votée par le Parlement, loi organique complète la Constitution, statut intermédiaire"},
124
+ ],
125
+ },
126
+ {
127
+ "title": "Leçon 2 — Le droit des contrats",
128
+ "text": """
129
+ Cours de droit civil. Le droit des contrats.
130
+
131
+ Première partie : Définition et conditions de validité.
132
 
133
  Le contrat est un accord de volontés destiné à créer, modifier, transmettre
134
  ou éteindre des obligations. Le Code civil, issu de l'ordonnance du 10 février 2016,
 
139
  ou la violence. L'incapacité concerne les mineurs non émancipés et les majeurs
140
  protégés sous tutelle ou curatelle.
141
 
142
+ Deuxième partie : Les effets du contrat.
143
+
144
  L'exécution obéit au principe de la force obligatoire : le contrat tient lieu
145
  de loi entre les parties. En cas d'inexécution : exécution forcée, résolution
146
  ou dommages et intérêts.
147
 
148
+ L'effet relatif du contrat signifie que le contrat ne crée d'obligations qu'entre
149
+ les parties contractantes. Les tiers ne peuvent en principe ni se prévaloir du contrat,
150
+ ni se le voir opposer.
151
+
152
+ Troisième partie : La formation du contrat.
153
+
154
+ La formation du contrat passe par une offre et une acceptation. L'offre doit être
155
+ précise et ferme. L'acceptation doit être pure et simple. Toute contre-proposition
156
+ constitue une nouvelle offre.
157
+
158
+ Les avant-contrats tels que la promesse unilatérale de vente ou le pacte de
159
+ préférence permettent d'organiser la phase précontractuelle et d'encadrer
160
+ les négociations entre les parties.
161
+ """,
162
+ "questions": [
163
+ {"q": "Quelles sont les trois conditions de validité d'un contrat selon le Code civil réformé en 2016 ?",
164
+ "hint": "consentement, capacité, contenu licite et certain"},
165
+ {"q": "Quels sont les trois vices du consentement qui peuvent entraîner la nullité d'un contrat ?",
166
+ "hint": "erreur, dol, violence"},
167
+ {"q": "Quelles personnes sont frappées d'incapacité contractuelle en droit français ?",
168
+ "hint": "mineurs non émancipés, majeurs protégés sous tutelle ou curatelle"},
169
+ {"q": "Qu'est-ce que le principe de l'effet relatif du contrat ?",
170
+ "hint": "le contrat ne crée d'obligations qu'entre les parties, les tiers ne peuvent ni s'en prévaloir ni se le voir opposer"},
171
+ ],
172
+ },
173
+ {
174
+ "title": "Leçon 3 — La responsabilité civile",
175
+ "text": """
176
+ Cours de droit civil. La responsabilité civile.
177
+
178
+ Première partie : Distinction responsabilité civile et pénale.
179
 
180
  La responsabilité civile répare le préjudice causé à autrui, contrairement
181
  à la responsabilité pénale qui sanctionne l'auteur d'une infraction.
182
+ La responsabilité civile vise à remettre la victime dans l'état où elle
183
+ se trouvait avant le dommage, par l'octroi de dommages et intérêts.
184
+
185
+ Deuxième partie : La responsabilité délictuelle.
186
 
187
  La responsabilité délictuelle repose sur trois conditions : faute, dommage
188
  et lien de causalité. L'article 1240 du Code civil dispose que tout fait
189
  de l'homme causant un dommage oblige son auteur à le réparer.
190
 
191
+ La faute peut être intentionnelle ou non intentionnelle. Le dommage doit être
192
+ certain, direct et personnel. Le lien de causalité entre la faute et le dommage
193
+ doit être prouvé par la victime.
194
+
195
+ Troisième partie : La responsabilité sans faute.
196
+
197
+ La responsabilité sans faute couvre trois domaines principaux.
198
+ Le fait des choses : on est responsable des dommages causés par les choses
199
+ dont on a la garde. Le fait d'autrui : les parents sont responsables des dommages
200
+ causés par leurs enfants mineurs ; les commettants répondent des fautes de leurs préposés.
201
+ Les produits défectueux : le producteur est responsable du dommage causé
202
+ par un défaut de son produit, sans que la victime ait à prouver une faute.
203
+
204
+ Quatrième partie : La responsabilité contractuelle.
205
+
206
+ Lorsque le dommage résulte de l'inexécution d'un contrat, la responsabilité
207
+ contractuelle s'applique. Elle suppose un contrat valable, une inexécution
208
+ et un préjudice. Le débiteur peut s'exonérer en prouvant la force majeure,
209
+ c'est-à-dire un événement imprévisible, irrésistible et extérieur.
210
+ """,
211
+ "questions": [
212
+ {"q": "Quelle est la différence entre responsabilité civile et responsabilité pénale ?",
213
+ "hint": "civile répare le préjudice, pénale sanctionne l'auteur d'une infraction"},
214
+ {"q": "Quelles sont les trois conditions de la responsabilité délictuelle ?",
215
+ "hint": "faute, dommage, lien de causalité, article 1240 Code civil"},
216
+ {"q": "Quels sont les trois domaines de la responsabilité sans faute ?",
217
+ "hint": "fait des choses, fait d'autrui, produits défectueux"},
218
+ {"q": "Qu'est-ce que la force majeure en matière de responsabilité contractuelle ?",
219
+ "hint": "événement imprévisible, irrésistible et extérieur, exonère le débiteur"},
220
+ ],
221
+ },
222
  ]
223
 
224
+ LISTEN_SECONDS = 10 # 900 en production (15 min)
225
  PASS_SCORE = 6
226
 
227
+ # ── Cache audio par leçon ─────────────────────────────────────────────────────
228
+ _audio_cache = {}
229
  _audio_lock = threading.Lock()
230
 
231
+ def get_lecture_audio(lesson_idx):
232
+ if lesson_idx in _audio_cache and os.path.exists(_audio_cache[lesson_idx]):
233
+ return _audio_cache[lesson_idx]
 
234
  with _audio_lock:
235
+ if lesson_idx in _audio_cache and os.path.exists(_audio_cache[lesson_idx]):
236
+ return _audio_cache[lesson_idx]
237
  try:
238
+ text = LESSONS[lesson_idx]["text"]
239
+ tts = gTTS(text=text, lang="fr", slow=False)
240
  path = tempfile.mktemp(suffix=".mp3")
241
  tts.save(path)
242
+ _audio_cache[lesson_idx] = path
243
  return path
244
  except Exception as e:
245
  print(f"[law-app] gTTS erreur : {e}")
 
247
 
248
  # ── État ──────────────────────────────────────────────────────────────────────
249
  def fresh():
250
+ return json.dumps({"phase": "idle", "listen_start": None,
251
+ "question_idx": None, "lesson_idx": 0})
252
 
253
  def S(s): return json.loads(s)
254
  def D(d): return json.dumps(d)
 
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})
287
+ audio = get_lecture_audio(lesson_idx)
288
+ total = len(LESSONS)
289
  msg = (
290
+ f"🎧 **{lesson['title']}** ({lesson_idx + 1}/{total})\n\n"
291
  "Écoutez pendant **15 minutes** complètes, puis cliquez sur **🔄 Vérifier le temps**."
292
  )
293
  return (
 
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
  )
 
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),
336
+ gr.update(visible=False), gr.update(visible=False),
337
+ "", LESSONS[lesson_idx]["questions"][idx]["q"], "", D(state),
338
  )
339
 
340
  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)
350
+ is_last = (lesson_idx >= total - 1)
351
+
352
  if score >= PASS_SCORE:
353
  state["phase"] = "pass"
354
  result = (
355
+ f"## 🎉 Bravo ! Leçon {lesson_idx + 1}/{total} réussie.\n\n"
356
  f"**Score : {score}/10**\n\n**Commentaire :** {fb}"
357
  )
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
 
372
+ return (
373
+ gr.update(visible=False), gr.update(visible=False),
374
+ gr.update(visible=True), gr.update(visible=False),
375
+ result, next_label, D(state),
376
+ )
377
+
378
+ def handle_next(ss):
379
+ state = S(ss)
380
+ lesson_idx = state.get("lesson_idx", 0)
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"
395
+ "✅ Leçon 2 — Le droit des contrats\n\n"
396
+ "✅ Leçon 3 — La responsabilité civile\n\n"
397
+ "---\n\n"
398
+ "*Ce certificat atteste de votre participation et de votre réussite "
399
+ "aux évaluations du cours de droit civil.*"
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))
415
+
416
+ def restart_from_cert(ss):
417
  return start_listening(fresh())
418
 
419
  # ── UI ────────────────────────────────────────────────────────────────────────
 
424
 
425
  gr.Markdown(
426
  "# ⚖️ Cours de Droit — Formation Juridique\n"
427
+ "Écoutez chaque leçon · Répondez à la question · Progressez leçon par leçon",
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.")
435
  audio_out = gr.Audio(label="Cours audio (français)", autoplay=False, interactive=False)
436
  timer_info = gr.Markdown("")
 
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]
465
+
466
+ start_btn.click(start_listening, [session], LISTEN_OUT)
467
+ check_btn.click(check_timer, [session], [quiz_btn, timer_info, session])
468
 
469
+ QUIZ_OUT = [listen_col, quiz_col, result_col, cert_col,
470
+ timer_info, question_md, answer_box, session]
471
+ quiz_btn.click(open_quiz, [session], QUIZ_OUT)
 
472
 
473
+ RES_OUT = [listen_col, quiz_col, result_col, cert_col,
474
+ result_md, next_btn, session]
475
+ submit_btn.click(submit_answer, [answer_box, session], RES_OUT)
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)