ramedde commited on
Commit
37e1139
·
verified ·
1 Parent(s): 6cc6bb7

Upload 3 files

Browse files
Files changed (3) hide show
  1. README.md +51 -0
  2. app.py +482 -0
  3. requirements.txt +4 -0
README.md ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Cours de Droit – Formation Juridique
3
+ emoji: ⚖️
4
+ colorFrom: blue
5
+ colorTo: indigo
6
+ sdk: gradio
7
+ sdk_version: 4.44.1
8
+ app_file: app.py
9
+ pinned: false
10
+ ---
11
+
12
+ # ⚖️ Cours de Droit — Formation Juridique
13
+
14
+ Application pédagogique de droit civil français.
15
+
16
+ ## Fonctionnement
17
+
18
+ 1. **Écoute obligatoire** : L'étudiant doit écouter le cours audio en français (gTTS) pendant **15 minutes** complètes.
19
+ 2. **Quiz** : Après la durée d'écoute, une question ouverte tirée aléatoirement du cours est présentée.
20
+ 3. **Évaluation IA** : La réponse est évaluée de **0 à 10** par Qwen3-0.6B quantisé (GGUF via llama-cpp).
21
+ - Score ≥ 6 → **Réussi** ✅
22
+ - Score < 6 → **Réécoute obligatoire** depuis le début ❌
23
+
24
+ ## Modèle IA requis
25
+
26
+ Téléchargez le modèle GGUF et placez-le à la racine du Space :
27
+
28
+ ```bash
29
+ # Dans le Space (ou via huggingface_hub)
30
+ huggingface-cli download Qwen/Qwen3-0.6B-GGUF \
31
+ Qwen3-0.6B-Q4_K_M.gguf \
32
+ --local-dir .
33
+ ```
34
+
35
+ > Sans fichier `.gguf`, l'évaluation tombe en mode fallback (correspondance de mots-clés).
36
+
37
+ ## Contenu du cours
38
+
39
+ Introduction aux sources du droit français :
40
+ - Hiérarchie des normes (Constitution, traités, loi, règlement)
41
+ - Jurisprudence, doctrine, coutume
42
+ - Pyramide de Kelsen
43
+ - Droit des contrats (validité, vices du consentement, force obligatoire)
44
+ - Responsabilité civile (délictuelle, sans faute)
45
+
46
+ ## Variables d'environnement
47
+
48
+ | Variable | Défaut | Description |
49
+ |---|---|---|
50
+ | `N_GPU_LAYERS` | `-1` | Couches GPU pour llama-cpp (-1 = auto) |
51
+ | `N_THREADS` | `2` | Threads CPU pour l'inférence |
app.py ADDED
@@ -0,0 +1,482 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ """
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:
32
+ from llama_cpp import Llama
33
+ _LLAMA_OK = True
34
+ except ImportError:
35
+ _LLAMA_OK = False
36
+
37
+ # ── LLM singleton ─────────────────────────────────────────────────────────────
38
+ _llm = None
39
+ _llm_lock = threading.Lock()
40
+
41
+ def get_llm():
42
+ global _llm
43
+ if _llm is not None:
44
+ return _llm
45
+ if not _LLAMA_OK:
46
+ return None
47
+ with _llm_lock:
48
+ if _llm is not None:
49
+ return _llm
50
+ paths = glob.glob("/app/*.gguf") + glob.glob("*.gguf")
51
+ if not paths:
52
+ print("[law-app] Aucun fichier .gguf trouvé – évaluation IA désactivée.")
53
+ return None
54
+ print(f"[law-app] Chargement du modèle : {paths[0]}")
55
+ _llm = Llama(
56
+ model_path=paths[0],
57
+ n_ctx=1024,
58
+ n_threads=int(os.getenv("N_THREADS", "2")),
59
+ n_gpu_layers=int(os.getenv("N_GPU_LAYERS", "-1")),
60
+ verbose=False,
61
+ )
62
+ print("[law-app] Modèle prêt.")
63
+ return _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 ?",
178
+ "hint": "Constitution, 4 octobre 1958, loi fondamentale",
179
+ },
180
+ {
181
+ "q": "Quelles sont les trois conditions de validité d'un contrat selon le Code civil réformé en 2016 ?",
182
+ "hint": "consentement, capacité, contenu licite et certain",
183
+ },
184
+ {
185
+ "q": "Qu'est-ce que la pyramide des normes et qui en est le théoricien ?",
186
+ "hint": "Hans Kelsen, hiérarchie des normes, validité tirée d'une norme supérieure",
187
+ },
188
+ {
189
+ "q": "Quels sont les trois vices du consentement qui peuvent entraîner la nullité d'un contrat ?",
190
+ "hint": "erreur, dol, violence",
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.",
198
+ "hint": "civile répare le préjudice, pénale sanctionne l'auteur d'une infraction",
199
+ },
200
+ {
201
+ "q": "Quel est le rôle de la jurisprudence dans le système juridique français ?",
202
+ "hint": "interprète et précise la portée des textes légaux, Cour de cassation, Conseil d'État",
203
+ },
204
+ {
205
+ "q": "Quelles personnes sont frappées d'incapacité contractuelle en droit français ?",
206
+ "hint": "mineurs non émancipés, majeurs protégés sous tutelle ou curatelle",
207
+ },
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
+
229
+ def get_lecture_audio():
230
+ global _audio_cache
231
+ if _audio_cache and os.path.exists(_audio_cache):
232
+ return _audio_cache
233
+ with _audio_lock:
234
+ if _audio_cache and os.path.exists(_audio_cache):
235
+ return _audio_cache
236
+ try:
237
+ tts = gTTS(text=LECTURE_TEXT, lang="fr", slow=False)
238
+ path = tempfile.mktemp(suffix=".mp3")
239
+ tts.save(path)
240
+ _audio_cache = path
241
+ return path
242
+ except Exception as e:
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(
441
+ label="Votre réponse",
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)
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ gradio==4.44.1
2
+ gTTS==2.5.3
3
+ llama-cpp-python==0.3.4
4
+ huggingface_hub==0.26.2