ramedde commited on
Commit
329ea19
·
verified ·
1 Parent(s): b58170d

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +575 -0
app.py ADDED
@@ -0,0 +1,575 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Cours de Droit – Hugging Face Space
3
+ llama-server (pre-built binary) via OpenAI-compatible HTTP API.
4
+ """
5
+ import os, json, time, tempfile, random, threading, sys
6
+ import requests
7
+
8
+ import gradio as gr
9
+ from gtts import gTTS
10
+
11
+ # ── llama-server connection ──────────────────────────────────────────────────
12
+ LLAMA_URL = os.getenv("LLAMA_SERVER_URL", "http://127.0.0.1:8080")
13
+ CHAT_ENDPOINT = f"{LLAMA_URL}/v1/chat/completions"
14
+ _server_ok = False
15
+ _server_lock = threading.Lock()
16
+
17
+ def _check_server():
18
+ """Background thread: poll until llama-server is ready."""
19
+ global _server_ok
20
+ print("[law-app] Waiting for llama-server...")
21
+ for _ in range(60): # up to 120 s
22
+ try:
23
+ r = requests.get(f"{LLAMA_URL}/health", timeout=2)
24
+ if r.status_code == 200:
25
+ _server_ok = True
26
+ print("[law-app] llama-server ready ✓")
27
+ return
28
+ except Exception:
29
+ pass
30
+ time.sleep(2)
31
+ print("[law-app] llama-server not reachable – keyword fallback active")
32
+
33
+ threading.Thread(target=_check_server, daemon=True).start()
34
+
35
+ # ── Leçons ───────────────────────────────────────────────────────────────────
36
+ LESSONS = [
37
+ {
38
+ "title": "Leçon 1 — Les principes directeurs et la preuve pénale",
39
+ "text": """
40
+ Cours de procédure pénale. Les principes du procès pénal et la preuve.
41
+ Première partie : Le droit à un procès équitable.
42
+ Consacré par l'article préliminaire du Code de procédure pénale et l'article 6 de la
43
+ Convention européenne des droits de l'homme (CEDH), le droit à un procès équitable
44
+ irrigue l'ensemble de la matière. Il implique l'accès à un tribunal indépendant et
45
+ impartial, le respect du délai raisonnable, et la garantie des droits de la défense
46
+ (égalité des armes, principe du contradictoire).
47
+
48
+ Deuxième partie : La charge de la preuve et la présomption d'innocence.
49
+ La preuve incombe au ministère public (la partie poursuivante), qui doit renverser
50
+ la présomption d'innocence dont bénéficie toute personne suspectée ou poursuivie.
51
+ Le doute doit toujours profiter à l'accusé (in dubio pro reo). Des présomptions de
52
+ culpabilité exceptionnelles existent (ex: infractions douanières), mais elles doivent
53
+ rester réfragables (pouvoir être renversées par la défense) et proportionnées.
54
+
55
+ Troisième partie : Le principe de l'intime conviction et la liberté de la preuve.
56
+ En vertu de l'article 427 du Code de procédure pénale, hors les cas où la loi en dispose
57
+ autrement, les infractions peuvent être établies par tout mode de preuve et le juge
58
+ décide d'après son intime conviction. Le juge ne peut fonder sa décision que sur des
59
+ preuves qui lui sont apportées au cours des débats et contradictoirement discutées.
60
+
61
+ Quatrième partie : La loyauté de la preuve.
62
+ Les autorités publiques (policiers, magistrats) sont soumises à un devoir de loyauté.
63
+ La provocation policière à l'infraction est strictement interdite car elle vicie la
64
+ procédure. En revanche, la jurisprudence tolère la simple provocation à la preuve,
65
+ à condition qu'elle ne constitue pas un stratagème actif poussant l'individu à
66
+ s'auto-incriminer (ex: sonorisation déloyale de cellules de garde à vue censurée
67
+ par l'Assemblée plénière de la Cour de cassation le 6 mars 2015). À l'inverse,
68
+ les preuves apportées par des particuliers, même obtenues de façon illicite, sont
69
+ recevables et soumises au débat contradictoire.
70
+ """,
71
+ "questions": [
72
+ {"q": "Quels textes fondamentaux garantissent le droit à un procès équitable en droit français ?",
73
+ "hint": "Article préliminaire du Code de procédure pénale, Article 6 de la CEDH"},
74
+ {"q": "Quelle est la différence entre la provocation à l'infraction et la provocation à la preuve ?",
75
+ "hint": "La provocation à l'infraction (création de l'infraction par la police) est interdite, la provocation à la preuve est tolérée sous conditions (absence de stratagème déloyal). Pensez à l'auto-incrimination."},
76
+ {"q": "Que signifie le principe de l'intime conviction (Article 427 du CPP) ?",
77
+ "hint": "Liberté de la preuve, le juge évalue librement la valeur des preuves débattues contradictoirement à l'audience."},
78
+ ],
79
+ },
80
+ {
81
+ "title": "Leçon 2 — La police judiciaire, l'enquête et la garde à vue",
82
+ "text": """
83
+ Cours de procédure pénale. Les acteurs de l'enquête et les mesures de contrainte.
84
+ Première partie : Les organes de la police judiciaire (PJ).
85
+ La police judiciaire est chargée de constater les infractions, d'en rassembler les
86
+ preuves et d'en chercher les auteurs. Elle agit sous la direction du procureur de la
87
+ République et le contrôle de la chambre de l'instruction. Il faut distinguer les
88
+ Officiers de Police Judiciaire (OPJ), qui ont des pouvoirs étendus (décider d'une
89
+ garde à vue, mener des perquisitions), des Agents de Police Judiciaire (APJ) qui
90
+ les secondent.
91
+
92
+ Deuxième partie : Les cadres d'enquête.
93
+ L'enquête de flagrance s'applique aux crimes et délits qui se commettent ou viennent
94
+ de se commettre. En raison de l'urgence, la police dispose de pouvoirs coercitifs
95
+ importants (perquisitions sans assentiment). Elle dure 8 jours (prolongeable 8 jours).
96
+ L'enquête préliminaire intervient en l'absence de flagrance. Elle est en principe non
97
+ coercitive (ex: perquisition avec assentiment exprès), sauf autorisation spéciale du
98
+ Juge des libertés et de la détention (JLD) pour les infractions les plus graves.
99
+
100
+ Troisième partie : La garde à vue.
101
+ Réformée en profondeur par la loi du 14 avril 2011 à la suite de condamnations par la
102
+ CEDH, la garde à vue est une mesure de contrainte décidée par un OPJ. Elle suppose des
103
+ raisons plausibles de soupçonner qu'une personne a commis ou tenté de commettre un crime
104
+ ou un délit puni d'une peine d'emprisonnement. Sa durée initiale est de 24h,
105
+ renouvelable sous contrôle du procureur, voire prolongeable jusqu'à 96h (terrorisme,
106
+ criminalité organisée) ou 144h par un magistrat.
107
+ Dès le début de la mesure, le suspect bénéficie de droits fondamentaux : droit au
108
+ silence, droit de faire prévenir un proche, examen médical, et surtout droit à
109
+ l'assistance d'un avocat lors des auditions et confrontations.
110
+ """,
111
+ "questions": [
112
+ {"q": "Quelle différence majeure existe-t-il entre un OPJ et un APJ ?",
113
+ "hint": "L'OPJ (Officier) a le pouvoir de prendre des mesures coercitives comme le placement en garde à vue, l'APJ (Agent) le seconde."},
114
+ {"q": "Qu'est-ce qui caractérise l'enquête de flagrance par rapport à l'enquête préliminaire ?",
115
+ "hint": "Infraction qui se commet ou vient de se commettre. Pouvoirs coercitifs immédiats de la police sans avoir besoin du consentement de la personne."},
116
+ {"q": "Quels sont les droits notifiés à un suspect dès son placement en garde à vue ?",
117
+ "hint": "Droit au silence, assistance d'un avocat dès la première heure, examen médical, information d'un proche."},
118
+ ],
119
+ },
120
+ {
121
+ "title": "Leçon 3 — L'action publique, l'action civile et la prescription",
122
+ "text": """
123
+ Cours de procédure pénale. Les actions nées de l'infraction.
124
+ Première partie : L'action publique et l'action civile.
125
+ Toute infraction donne naissance à deux actions. L'action publique vise à l'application
126
+ d'une peine pour réparer le trouble à l'ordre social. Elle est exercée par le Ministère
127
+ public (le Parquet, magistrats soumis à une hiérarchie).
128
+ L'action civile vise la réparation du préjudice privé causé par l'infraction. Elle est
129
+ exercée par la victime. La victime peut déclencher l'action publique en se constituant
130
+ partie civile (saisine du juge d'instruction ou citation directe).
131
+
132
+ Deuxième partie : L'extinction de l'action publique et la prescription.
133
+ L'action publique peut s'éteindre par le décès du mis en cause, l'amnistie, l'abrogation
134
+ de la loi pénale, la chose jugée ou la prescription.
135
+ La loi du 27 février 2017 a profondément réformé les délais de prescription de l'action
136
+ publique, les allongeant à : 1 an pour les contraventions, 6 ans pour les délits, et
137
+ 20 ans pour les crimes (des délais plus longs existent pour le terrorisme ou les
138
+ crimes sexuels sur mineurs).
139
+
140
+ Troisième partie : L'infraction occulte et dissimulée.
141
+ La loi de 2017 a consacré la jurisprudence sur le point de départ de la prescription.
142
+ Pour une infraction occulte (qui ne peut être connue en raison de ses éléments constitutifs)
143
+ ou dissimulée (manœuvres de l'auteur pour empêcher sa découverte, ex: abus de biens sociaux),
144
+ le délai de prescription ne court qu'à partir du jour où l'infraction a pu être constatée.
145
+ Cependant, la loi a fixé un "délai butoir" : l'action publique ne peut plus être engagée
146
+ au-delà de 12 ans (délits) ou 30 ans (crimes) à compter de la commission des faits.
147
+
148
+ Quatrième partie : Les alternatives aux poursuites.
149
+ En vertu de l'opportunité des poursuites, le procureur peut classer sans suite,
150
+ poursuivre, ou proposer des mesures alternatives. La composition pénale permet de
151
+ proposer une amende ou des mesures de réparation à une personne qui reconnait les faits.
152
+ La Convention Judiciaire d'Intérêt Public (CJIP), créée par la loi Sapin II (2016),
153
+ permet à une personne morale d'échapper à une condamnation pénale en payant une
154
+ forte amende de conformité.
155
+ """,
156
+ "questions": [
157
+ {"q": "Quels sont les délais de droit commun de la prescription de l'action publique depuis la loi de 2017 ?",
158
+ "hint": "1 an (contraventions), 6 ans (délits), 20 ans (crimes)."},
159
+ {"q": "Comment fonctionne la prescription pour une infraction occulte ou dissimulée ?",
160
+ "hint": "Le point de départ est reporté au jour où l'infraction est apparue/constatée, avec un délai butoir maximal de 12 ans ou 30 ans depuis les faits."},
161
+ {"q": "Quelles sont les finalités respectives de l'action publique et de l'action civile ?",
162
+ "hint": "Action publique = application d'une peine (trouble social) / Action civile = réparation du préjudice (dommages-intérêts)."},
163
+ ],
164
+ },
165
+ {
166
+ "title": "Leçon 4 — L'instruction préparatoire et les mesures de sûreté",
167
+ "text": """
168
+ Cours de procédure pénale. L'instruction par le juge d'instruction.
169
+ Première partie : Le rôle et la saisine du juge d'instruction.
170
+ L'instruction préparatoire est obligatoire en matière de crime, facultative pour les
171
+ délits. Elle est menée par le juge d'instruction, un magistrat du siège indépendant.
172
+ Il instruit "à charge et à décharge" pour manifester la vérité.
173
+ Il est saisi in rem (il ne peut enquêter que sur les faits visés par le réquisitoire
174
+ du procureur ou la plainte de la victime). S'il découvre des faits nouveaux, il a
175
+ besoin d'un réquisitoire supplétif.
176
+
177
+ Deuxième partie : Les statuts des personnes visées.
178
+ Le juge peut placer une personne sous le statut de témoin assisté s'il existe de
179
+ simples indices vraisemblables.
180
+ Il ne peut prononcer une mise en examen que s'il existe des "indices graves ou concordants"
181
+ rendant vraisemblable la participation aux faits. Le mis en examen dispose de droits
182
+ larges (demandes d'actes, requêtes en nullité, avocat).
183
+
184
+ Troisième partie : La détention provisoire et le contrôle judiciaire.
185
+ La liberté est le principe, la détention l'exception. Le juge d'instruction a perdu
186
+ le pouvoir d'incarcérer depuis 2000. C'est le Juge des Libertés et de la Détention (JLD)
187
+ qui peut prononcer la détention provisoire si le contrôle judiciaire (obligations à respecter,
188
+ ex: interdiction de paraître) ou l'Assignation à Résidence sous Surveillance Électronique
189
+ (ARSE) se révèlent insuffisants.
190
+ La détention provisoire doit être strictement motivée (risque de fuite, protection
191
+ des preuves, trouble à l'ordre public) et est limitée dans le temps.
192
+
193
+ Quatrième partie : La clôture de l'instruction.
194
+ À la fin de ses investigations, le juge rend une ordonnance de règlement. Soit un
195
+ non-lieu (charges insuffisantes), soit une ordonnance de renvoi (devant le tribunal
196
+ correctionnel) ou de mise en accusation (devant la Cour d'assises). Les actes
197
+ irréguliers accomplis pendant l'instruction peuvent être sanctionnés par la nullité,
198
+ prononcée par la Chambre de l'instruction.
199
+ """,
200
+ "questions": [
201
+ {"q": "Que signifie la règle de la saisine « in rem » du juge d'instruction ?",
202
+ "hint": "Le juge ne peut instruire que sur les faits expressément visés dans le réquisitoire du parquet ou la plainte. Il a besoin d'un réquisitoire supplétif pour des faits nouveaux."},
203
+ {"q": "Quelle est la condition posée par l'article 80-1 du CPP pour mettre une personne en examen ?",
204
+ "hint": "L'existence d'indices graves ou concordants de participation aux faits."},
205
+ {"q": "Quel magistrat est compétent pour placer une personne en détention provisoire au cours de l'instruction ?",
206
+ "hint": "Le Juge des libertés et de la détention (JLD), sur saisine du juge d'instruction ou du procureur."},
207
+ ],
208
+ },
209
+ {
210
+ "title": "Leçon 5 — Les juridictions de jugement et les voies de recours",
211
+ "text": """
212
+ Cours de procédure pénale. Le procès pénal et les recours.
213
+ Première partie : Les juridictions de droit commun.
214
+ La compétence matérielle répartit les affaires selon la gravité :
215
+ - Le Tribunal de police (juge unique) statue sur les contraventions.
216
+ - Le Tribunal correctionnel (formation collégiale ou juge unique) juge les délits.
217
+ - La Cour d'assises juge les crimes. Composée de 3 magistrats professionnels et
218
+ d'un jury populaire (6 citoyens en premier ressort, 9 en appel). Les décisions sont
219
+ prises à la majorité et depuis 2012, les arrêts d'assises doivent être motivés.
220
+ Une expérimentation (Loi de programmation 2018-2022) a créé les Cours Criminelles
221
+ Départementales pour juger les crimes punis de 15 à 20 ans de réclusion sans jury populaire.
222
+
223
+ Deuxième partie : Les modes de comparution (matière correctionnelle).
224
+ Le Parquet peut opter pour plusieurs voies de poursuites rapides : la comparution
225
+ immédiate (délits punis d'au moins 2 ans, ou 6 mois si flagrance), la Comparution
226
+ sur Reconnaissance Préalable de Culpabilité (CRPC, ou "plaider-coupable",
227
+ qui suppose l'accord du prévenu et l'homologation du juge), ou l'ordonnance pénale
228
+ (procédure sans audience).
229
+
230
+ Troisième partie : Les voies de recours ordinaires.
231
+ L'appel est une voie de réformation qui soumet l'affaire à la Cour d'appel
232
+ (Chambre des appels correctionnels) ou à une cour d'assises d'appel. L'appel a un
233
+ effet suspensif et dévolutif. Il interdit d'aggraver le sort de l'appelant s'il est
234
+ seul à faire appel (prohibition de la reformatio in pejus).
235
+ L'opposition permet de rétracter un jugement rendu par défaut (en l'absence du prévenu
236
+ non informé). Le même tribunal rejuge l'affaire.
237
+
238
+ Quatrième partie : Les voies de recours extraordinaires.
239
+ Le pourvoi en cassation est jugé par la Chambre criminelle de la Cour de cassation.
240
+ Elle ne rejuge pas les faits, mais contrôle uniquement l'exacte application de la loi
241
+ par les juges du fond.
242
+ La demande en révision ou en réexamen (réformée en 2014) permet d'annuler une
243
+ condamnation pénale définitive en cas de "fait nouveau" faisant naître un doute sur
244
+ la culpabilité, ou suite à une condamnation de la France par la CEDH.
245
+ """,
246
+ "questions": [
247
+ {"q": "Quelle est la composition de la Cour d'assises en premier ressort ?",
248
+ "hint": "3 magistrats professionnels (la cour) et un jury populaire composé de 6 citoyens (jurés)."},
249
+ {"q": "Qu'est-ce que la procédure de CRPC ?",
250
+ "hint": "Comparution sur Reconnaissance Préalable de Culpabilité. Un 'plaider-coupable' où le procureur propose une peine que le prévenu accepte, avant homologation par le juge du siège."},
251
+ {"q": "Le pourvoi en cassation représente-t-il un troisième degré de juridiction ?",
252
+ "hint": "Non. La Cour de cassation est juge du droit et non du fait. Elle contrôle l'application de la loi sans rejuger les faits."},
253
+ ],
254
+ },
255
+ ]
256
+
257
+ LISTEN_SECONDS = 10 # 900 en production (15 min)
258
+ PASS_SCORE = 6
259
+
260
+ # ── Cache audio par leçon ─────────────────────────────────────────────────────
261
+ _audio_cache = {}
262
+ _audio_lock = threading.Lock()
263
+
264
+ def get_lecture_audio(lesson_idx):
265
+ if lesson_idx in _audio_cache and os.path.exists(_audio_cache[lesson_idx]):
266
+ return _audio_cache[lesson_idx]
267
+ with _audio_lock:
268
+ if lesson_idx in _audio_cache and os.path.exists(_audio_cache[lesson_idx]):
269
+ return _audio_cache[lesson_idx]
270
+ try:
271
+ text = LESSONS[lesson_idx]["text"]
272
+ tts = gTTS(text=text, lang="fr", slow=False)
273
+ path = tempfile.mktemp(suffix=".mp3")
274
+ tts.save(path)
275
+ _audio_cache[lesson_idx] = path
276
+ return path
277
+ except Exception as e:
278
+ print(f"[law-app] gTTS erreur : {e}")
279
+ return None
280
+
281
+ # ── État ──────────────────────────────────────────────────────────────────────
282
+ def fresh():
283
+ return json.dumps({"phase": "idle", "listen_start": None,
284
+ "question_idx": None, "lesson_idx": 0})
285
+
286
+ def S(s): return json.loads(s)
287
+ def D(d): return json.dumps(d)
288
+
289
+ # ── Évaluation ────────────────────────────────────────────────────────────────
290
+ _SYSTEM_PROMPT = (
291
+ "Tu es un professeur de droit français strict et bienveillant. "
292
+ "Tu évalues les réponses des étudiants et réponds UNIQUEMENT en JSON valide sur une seule ligne.\n\n"
293
+ "# Exemples de comportement attendu :\n\n"
294
+ "Question : Quelles sont les finalités respectives de l'action publique et de l'action civile ?\n"
295
+ "Réponse de l'étudiant : je ne sais pas\n"
296
+ "Éléments attendus : Action publique = application d'une peine (trouble social) / Action civile = réparation du préjudice (dommages-intérêts).\n"
297
+ '{"score": 0, "feedback": "Aucune réponse n\'a été proposée. Pour rappel, l\'action publique vise à sanctionner le trouble social par une peine, tandis que l\'action civile cherche à réparer le préjudice subi."}\n\n'
298
+ "Question : Quelles sont les finalités respectives de l'action publique et de l'action civile ?\n"
299
+ "Réponse de l'étudiant : lacion publique a pour but d'apication de pain laction civil a pour but domager le victime\n"
300
+ "Éléments attendus : Action publique = application d'une peine (trouble social) / Action civile = réparation du préjudice (dommages-intérêts).\n"
301
+ '{"score": 7, "feedback": "Les notions de peine et de dédommagement sont comprises, mais l\'orthographe et la formulation doivent être grandement améliorées."}\n\n'
302
+ "Question : Quel magistrat est compétent pour placer une personne en détention provisoire au cours de l'instruction ?\n"
303
+ "Réponse de l'étudiant : juge dinstrucion\n"
304
+ "Éléments attendus : Le Juge des libertés et de la détention (JLD), sur saisine du juge d'instruction ou du procureur.\n"
305
+ '{"score": 2, "feedback": "Le juge d\'instruction saisit le dossier mais n\'est pas compétent pour décider de la détention provisoire ; cette décision relève exclusivement du Juge des libertés et de la détention (JLD)."}\n\n'
306
+ "Question : Quelle différence majeure existe-t-il entre un OPJ et un APJ ?\n"
307
+ "Réponse de l'étudiant : l apj est t'une police administrative aopg police judiciare\n"
308
+ "Éléments attendus : L'OPJ (Officier) a le pouvoir de prendre des mesures coercitives comme le placement en garde à vue, l'APJ (Agent) le seconde.\n"
309
+ '{"score": 1, "feedback": "C\'est incorrect. L\'APJ et l\'OPJ relèvent tous deux de la police judiciaire. La différence majeure réside dans les pouvoirs coercitifs (comme décider d\'une garde à vue) réservés à l\'OPJ, que l\'APJ seconde."}'
310
+ )
311
+
312
+ def _keyword_fallback(answer, hint, reason=""):
313
+ kw = [w.strip().lower() for w in hint.split(",")]
314
+ hits = sum(1 for k in kw if k in answer.lower())
315
+ score = min(10, round((hits / max(len(kw), 1)) * 10))
316
+ label = f"(Sans IA – {reason} – " if reason else "(Sans IA – "
317
+ return score, f"{label}mots-clés : {hits}/{len(kw)})"
318
+
319
+ def evaluate_answer(question, answer, hint):
320
+ if not _server_ok:
321
+ return _keyword_fallback(answer, hint, reason="serveur indisponible")
322
+
323
+ user_msg = (
324
+ f"Question : {question}\n"
325
+ f"Réponse de l'étudiant : {answer}\n"
326
+ f"Éléments attendus : {hint}\n\n"
327
+ "Évalue cette réponse sur 10 et donne un commentaire bref en français.\n"
328
+ "Réponds UNIQUEMENT avec ce JSON sur une seule ligne, sans rien d'autre :\n"
329
+ '{"score": <entier 0-10>, "feedback": "<commentaire bref>"}'
330
+ )
331
+
332
+ payload = {
333
+ "model": "local",
334
+ "messages": [
335
+ {"role": "system", "content": _SYSTEM_PROMPT},
336
+ {"role": "user", "content": user_msg},
337
+ ],
338
+ "max_tokens": 150,
339
+ "temperature": 0.1,
340
+ "stop": ["\n\n", "###"],
341
+ }
342
+
343
+ try:
344
+ resp = requests.post(CHAT_ENDPOINT, json=payload, timeout=60)
345
+ resp.raise_for_status()
346
+ text = resp.json()["choices"][0]["message"]["content"].strip()
347
+ print(f"[law-app] Réponse modèle : {text}")
348
+ start = text.find("{")
349
+ end = text.rfind("}") + 1
350
+ if start == -1 or end == 0:
351
+ raise ValueError("Pas de JSON dans la réponse")
352
+ data = json.loads(text[start:end])
353
+ return max(0, min(10, int(data["score"]))), data.get("feedback", "")
354
+ except Exception as e:
355
+ print(f"[law-app] Erreur éval : {e}")
356
+ return _keyword_fallback(answer, hint, reason=str(e))
357
+
358
+ # ── Handlers ──────────────────────────────────────────────────────────────────
359
+ def start_listening(ss):
360
+ state = S(ss)
361
+ lesson_idx = state.get("lesson_idx", 0)
362
+ lesson = LESSONS[lesson_idx]
363
+ state.update({"phase": "listen", "listen_start": time.time(), "question_idx": None})
364
+ audio = get_lecture_audio(lesson_idx)
365
+ total = len(LESSONS)
366
+ msg = (
367
+ f"🎧 **{lesson['title']}** ({lesson_idx + 1}/{total})\n\n"
368
+ "Écoutez pendant **15 minutes** complètes, puis cliquez sur **🔄 Vérifier le temps**."
369
+ )
370
+ return (
371
+ audio, msg,
372
+ gr.update(visible=True),
373
+ gr.update(visible=False),
374
+ gr.update(visible=False),
375
+ gr.update(visible=False),
376
+ gr.update(interactive=False, value="⏳ Écoute en cours…"),
377
+ D(state),
378
+ )
379
+
380
+ def check_timer(ss):
381
+ state = S(ss)
382
+ if state["phase"] != "listen" or not state["listen_start"]:
383
+ return gr.update(), "⚠️ Lancez d'abord l'écoute.", ss
384
+ rem = max(0, LISTEN_SECONDS - (time.time() - state["listen_start"]))
385
+ m, s = divmod(int(rem), 60)
386
+ if rem <= 0:
387
+ return (
388
+ gr.update(interactive=True, value="✅ Accéder au Quiz"),
389
+ "✅ Durée atteinte ! Cliquez sur **Accéder au Quiz**.",
390
+ ss,
391
+ )
392
+ return (
393
+ gr.update(interactive=False, value=f"⏳ Encore {m:02d}:{s:02d}"),
394
+ f"🎧 Patientez encore **{m} min {s:02d} s**.",
395
+ ss,
396
+ )
397
+
398
+ def open_quiz(ss):
399
+ state = S(ss)
400
+ elapsed = time.time() - (state["listen_start"] or 0)
401
+ if elapsed < LISTEN_SECONDS:
402
+ m, s = divmod(int(LISTEN_SECONDS - elapsed), 60)
403
+ return (
404
+ gr.update(visible=True), gr.update(visible=False),
405
+ gr.update(visible=False), gr.update(visible=False),
406
+ f"⚠️ Patientez encore **{m} min {s:02d} s**.", "", "", ss,
407
+ )
408
+ lesson_idx = state.get("lesson_idx", 0)
409
+ idx = random.randrange(len(LESSONS[lesson_idx]["questions"]))
410
+ state.update({"phase": "quiz", "question_idx": idx})
411
+ return (
412
+ gr.update(visible=False), gr.update(visible=True),
413
+ gr.update(visible=False), gr.update(visible=False),
414
+ "", LESSONS[lesson_idx]["questions"][idx]["q"], "", D(state),
415
+ )
416
+
417
+ # ── Submit avec désactivation immédiate du bouton ─────────────────────────────
418
+ def submit_answer(answer, ss):
419
+ state = S(ss)
420
+ lesson_idx = state.get("lesson_idx", 0)
421
+
422
+ if state["phase"] != "quiz":
423
+ yield (
424
+ gr.update(visible=True), gr.update(visible=False),
425
+ gr.update(visible=False), gr.update(visible=False),
426
+ "",
427
+ gr.update(value="📤 Soumettre", interactive=True),
428
+ gr.update(interactive=True),
429
+ ss,
430
+ )
431
+ return
432
+
433
+ # Étape 1 : désactiver immédiatement bouton + textbox
434
+ yield (
435
+ gr.update(visible=False), gr.update(visible=True),
436
+ gr.update(visible=False), gr.update(visible=False),
437
+ "",
438
+ gr.update(value="⏳ Réponse soumise — Correction en cours…", interactive=False),
439
+ gr.update(interactive=False),
440
+ ss,
441
+ )
442
+
443
+ # Étape 2 : évaluation IA
444
+ q = LESSONS[lesson_idx]["questions"][state["question_idx"]]
445
+ score, fb = evaluate_answer(q["q"], answer, q["hint"])
446
+ total = len(LESSONS)
447
+ is_last = (lesson_idx >= total - 1)
448
+
449
+ if score >= PASS_SCORE:
450
+ state["phase"] = "pass"
451
+ result = (
452
+ f"## 🎉 Bravo ! Leçon {lesson_idx + 1}/{total} réussie.\n\n"
453
+ f"**Score : {score}/10**\n\n**Commentaire :** {fb}"
454
+ )
455
+ next_label = "🏆 Voir mon certificat" if is_last else f"➡️ {LESSONS[lesson_idx + 1]['title']}"
456
+ else:
457
+ state["phase"] = "fail"
458
+ result = (
459
+ f"## ❌ Score insuffisant : {score}/10\n\n"
460
+ f"**Commentaire :** {fb}\n\n"
461
+ f"Seuil requis : **{PASS_SCORE}/10**. "
462
+ "Veuillez **réécouter la leçon** avant de retenter."
463
+ )
464
+ next_label = "🔄 Réécouter la leçon"
465
+
466
+ # Étape 3 : afficher résultat, réactiver bouton, mettre à jour next_btn
467
+ yield (
468
+ gr.update(visible=False), gr.update(visible=False),
469
+ gr.update(visible=True), gr.update(visible=False),
470
+ result,
471
+ gr.update(value="📤 Soumettre", interactive=True),
472
+ gr.update(value="", interactive=True),
473
+ D(state),
474
+ )
475
+
476
+ def handle_next(ss):
477
+ state = S(ss)
478
+ lesson_idx = state.get("lesson_idx", 0)
479
+ total = len(LESSONS)
480
+
481
+ if state["phase"] == "fail":
482
+ return start_listening(D(state))
483
+
484
+ if lesson_idx >= total - 1:
485
+ state["phase"] = "done"
486
+ cert = (
487
+ "# 🏆 Félicitations !\n\n"
488
+ "Vous avez complété avec succès les **5 leçons** du cours de procédure pénale :\n\n"
489
+ "✅ Leçon 1 — Les principes directeurs et la preuve pénale\n\n"
490
+ "✅ Leçon 2 — La police judiciaire, l'enquête et la garde à vue\n\n"
491
+ "✅ Leçon 3 — L'action publique, l'action civile et la prescription\n\n"
492
+ "✅ Leçon 4 — L'instruction préparatoire et les mesures de sûreté\n\n"
493
+ "✅ Leçon 5 — Les juridictions de jugement et les voies de recours\n\n"
494
+ "---\n\n"
495
+ "*Ce certificat atteste de votre participation et de votre réussite "
496
+ "aux évaluations du cours de procédure pénale.*"
497
+ )
498
+ return (
499
+ None, cert,
500
+ gr.update(visible=False),
501
+ gr.update(visible=False),
502
+ gr.update(visible=False),
503
+ gr.update(visible=True),
504
+ gr.update(interactive=False, value="✅ Terminé"),
505
+ D(state),
506
+ )
507
+ else:
508
+ state["lesson_idx"] = lesson_idx + 1
509
+ state["listen_start"] = None
510
+ return start_listening(D(state))
511
+
512
+ def restart_from_cert(ss):
513
+ return start_listening(fresh())
514
+
515
+ # ── UI ────────────────────────────────────────────────────────────────────────
516
+ CSS = "footer{display:none!important;} #title{text-align:center;}"
517
+
518
+ with gr.Blocks(title="Cours de Droit", css=CSS) as demo:
519
+ session = gr.State(fresh())
520
+
521
+ gr.Markdown(
522
+ "# ⚖️ Cours de Droit — Formation Juridique\n"
523
+ "Écoutez chaque leçon · Répondez à la question · Progressez leçon par leçon",
524
+ elem_id="title",
525
+ )
526
+
527
+ with gr.Column(visible=True) as listen_col:
528
+ gr.Markdown("## 🎧 Étape 1 — Écoute de la leçon")
529
+ listen_msg = gr.Markdown("Cliquez sur **Démarrer** pour commencer.")
530
+ audio_out = gr.Audio(label="Cours audio (français)", autoplay=False, interactive=False)
531
+ timer_info = gr.Markdown("")
532
+ with gr.Row():
533
+ start_btn = gr.Button("▶️ Démarrer l'écoute", variant="primary")
534
+ check_btn = gr.Button("🔄 Vérifier le temps", variant="secondary")
535
+ quiz_btn = gr.Button("✅ Accéder au Quiz", variant="secondary", interactive=False)
536
+
537
+ with gr.Column(visible=False) as quiz_col:
538
+ gr.Markdown("## 📝 Étape 2 — Question d'évaluation")
539
+ question_md = gr.Markdown("")
540
+ answer_box = gr.Textbox(label="Votre réponse", placeholder="Rédigez ici…", lines=5)
541
+ submit_btn = gr.Button("�� Soumettre", variant="primary")
542
+
543
+ with gr.Column(visible=False) as result_col:
544
+ gr.Markdown("## 📊 Résultat")
545
+ result_md = gr.Markdown("")
546
+ next_btn = gr.Button("➡️ Leçon suivante", variant="primary")
547
+
548
+ with gr.Column(visible=False) as cert_col:
549
+ cert_md = gr.Markdown("")
550
+ restart_btn = gr.Button("🔄 Recommencer depuis le début", variant="secondary")
551
+
552
+ # ── Câblage ───────────────────────────────────────────────────────────────
553
+ LISTEN_OUT = [audio_out, listen_msg,
554
+ listen_col, quiz_col, result_col, cert_col,
555
+ quiz_btn, session]
556
+
557
+ start_btn.click(start_listening, [session], LISTEN_OUT)
558
+ check_btn.click(check_timer, [session], [quiz_btn, timer_info, session])
559
+
560
+ QUIZ_OUT = [listen_col, quiz_col, result_col, cert_col,
561
+ timer_info, question_md, answer_box, session]
562
+ quiz_btn.click(open_quiz, [session], QUIZ_OUT)
563
+
564
+ # submit_answer est un générateur → outputs inclut submit_btn et answer_box
565
+ RES_OUT = [listen_col, quiz_col, result_col, cert_col,
566
+ result_md, submit_btn, answer_box, session]
567
+ submit_btn.click(submit_answer, [answer_box, session], RES_OUT)
568
+ answer_box.submit(submit_answer, [answer_box, session], RES_OUT)
569
+
570
+ next_btn.click(handle_next, [session], LISTEN_OUT)
571
+ restart_btn.click(restart_from_cert, [session], LISTEN_OUT)
572
+
573
+ if __name__ == "__main__":
574
+ demo.queue()
575
+ demo.launch(server_name="0.0.0.0", server_port=7860, show_error=True)