ramedde commited on
Commit
00d33d5
·
verified ·
1 Parent(s): c3551ba

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +194 -203
app.py CHANGED
@@ -1,7 +1,7 @@
1
  """
2
- Cours de Droit – Hugging Face Space
 
3
  Compatible avec le Docker HF (gradio 4.44.1 bundled + Python 3.13).
4
- Téléchargement automatique du modèle au démarrage.
5
  """
6
  import os, json, time, tempfile, random, threading, glob
7
  import sys
@@ -40,16 +40,16 @@ try:
40
  except ImportError:
41
  _LLAMA_OK = False
42
 
43
- # ── Téléchargement et chargement du modèle ────────────────────────────────────
44
  _llm = None
45
  _llm_lock = threading.Lock()
46
 
47
  def download_model():
48
  model_path = "/app/model.gguf"
49
  if os.path.exists(model_path):
50
- print("[law-app] Modèle déjà présent.")
51
  return model_path
52
- print("[law-app] Téléchargement du modèle en cours...")
53
  try:
54
  path = hf_hub_download(
55
  repo_id="Qwen/Qwen2.5-0.5B-Instruct-GGUF",
@@ -59,10 +59,10 @@ def download_model():
59
  )
60
  if path != model_path:
61
  os.rename(path, model_path)
62
- print("[law-app] Téléchargement terminé.")
63
  return model_path
64
  except Exception as e:
65
- print(f"[law-app] Erreur téléchargement : {e}")
66
  return None
67
 
68
  def get_llm():
@@ -76,151 +76,152 @@ def get_llm():
76
  return _llm
77
  gguf_files = glob.glob("/app/*.gguf") + glob.glob("*.gguf")
78
  if not gguf_files:
79
- print("[spelling-app] No .gguf file foundAI tips disabled.")
80
  return None
81
  model_path = gguf_files[0]
82
- print(f"[spelling-app] Loading model: {model_path}")
83
  _llm = Llama(model_path=model_path, n_ctx=512, n_threads=2,
84
  n_gpu_layers=int(os.getenv("N_GPU_LAYERS", "-1")), verbose=False)
85
- print("[spelling-app] Model ready.")
86
  return _llm
87
 
88
- # ── Leçons ────────────────────────────────────────────────────────────────────
89
  LESSONS = [
90
  {
91
- "title": "Leçon 1Sources du droit et hiérarchie des normes",
92
  "text": """
93
- Cours de droit civil. Introduction aux sources du droit français.
94
- Première partie : Les sources formelles du droit.
95
- Le droit français repose sur plusieurs sources hiérarchisées.
96
- Au sommet se trouve la Constitution du 4 octobre 1958, loi fondamentale
97
- de la République française. Elle établit les règles d'organisation de
98
- l'État et garantit les droits fondamentaux des citoyens.
99
- Sous la Constitution, on trouve les traités internationaux et le droit
100
- de l'Union européenne. En vertu de l'article 55 de la Constitution,
101
- les traités régulièrement ratifiés ont une autorité supérieure à celle
102
- des lois ordinaires.
103
- Vient ensuite la loi, votée par le Parlement. La loi ordinaire est
104
- adoptée par l'Assemblée nationale et le Sénat selon la procédure
105
- législative prévue par la Constitution. Les lois organiques,
106
- quant à elles, complètent et précisent certaines dispositions
107
- constitutionnelles et bénéficient d'un statut intermédiaire.
108
- Le règlement constitue la quatrième source majeure. Il émane du
109
- pouvoir exécutif : décrets du Président de la République et du
110
- Premier ministre, arrêtés ministériels, préfectoraux et municipaux.
111
- L'article 37 de la Constitution confère au gouvernement un domaine
112
- réglementaire autonome.
113
- Deuxième partie : Les sources matérielles du droit.
114
- La jurisprudence, bien qu'elle ne soit pas officiellement reconnue
115
- comme source formelle, joue un rôle fondamental dans le système
116
- juridique français. Les décisions de la Cour de cassation et du
117
- Conseil d'État interprètent et précisent la portée des textes légaux.
118
- Le Conseil constitutionnel contrôle la constitutionnalité des lois.
119
- La doctrine regroupe l'ensemble des travaux des juristes :
120
- professeurs, avocats, magistrats. Sans force obligatoire, elle influence
121
- néanmoins la jurisprudence et le législateur.
122
- La coutume, source ancienne, est aujourd'hui d'application limitée.
123
- En droit commercial, les usages professionnels conservent une importance certaine.
124
- Troisième partie : Le principe de hiérarchie des normes.
125
- Hans Kelsen, juriste autrichien du vingtième siècle, a théorisé
126
- la pyramide des normes. Chaque norme tire sa validité d'une norme supérieure.
127
- En France, le Conseil constitutionnel assure le respect de cette hiérarchie.
128
  """,
129
  "questions": [
130
- {"q": "Quelle est la source au sommet de la hiérarchie des normes en droit français, et quelle est sa date ?",
131
- "hint": "Constitution, 4 octobre 1958, loi fondamentale"},
132
- {"q": "Qu'est-ce que la pyramide des normes et qui en est le théoricien ?",
133
- "hint": "Hans Kelsen, hiérarchie des normes, validité tirée d'une norme supérieure"},
134
- {"q": "Quel est le rôle de la jurisprudence dans le système juridique français ?",
135
- "hint": "interprète et précise les textes légaux, Cour de cassation, Conseil d'État"},
136
- {"q": "Quelle différence y a-t-il entre une loi ordinaire et une loi organique ?",
137
- "hint": "loi ordinaire votée par le Parlement, loi organique complète la Constitution, statut intermédiaire"},
 
 
 
 
 
 
 
 
138
  ],
139
  },
140
  {
141
- "title": "Leçon 2Le droit des contrats",
142
  "text": """
143
- Cours de droit civil. Le droit des contrats.
144
- Première partie : Définition et conditions de validité.
145
- Le contrat est un accord de volontés destiné à créer, modifier, transmettre
146
- ou éteindre des obligations. Le Code civil, issu de l'ordonnance du 10 février 2016,
147
- définit trois conditions de validité : le consentement des parties, leur capacité
148
- à contracter, et un contenu licite et certain.
149
- Le consentement doit être libre et éclairé. Il est vicié par l'erreur, le dol
150
- ou la violence. L'incapacité concerne les mineurs non émancipés et les majeurs
151
- protégés sous tutelle ou curatelle.
152
- Deuxième partie : Les effets du contrat.
153
- L'exécution obéit au principe de la force obligatoire : le contrat tient lieu
154
- de loi entre les parties. En cas d'inexécution : exécution forcée, résolution
155
- ou dommages et intérêts.
156
- L'effet relatif du contrat signifie que le contrat ne crée d'obligations qu'entre
157
- les parties contractantes. Les tiers ne peuvent en principe ni se prévaloir du contrat,
158
- ni se le voir opposer.
159
- Troisième partie : La formation du contrat.
160
- La formation du contrat passe par une offre et une acceptation. L'offre doit être
161
- précise et ferme. L'acceptation doit être pure et simple. Toute contre-proposition
162
- constitue une nouvelle offre.
163
- Les avant-contrats tels que la promesse unilatérale de vente ou le pacte de
164
- préférence permettent d'organiser la phase précontractuelle et d'encadrer
165
- les négociations entre les parties.
166
  """,
167
  "questions": [
168
- {"q": "Quelles sont les trois conditions de validité d'un contrat selon le Code civil réformé en 2016 ?",
169
- "hint": "consentement, capacité, contenu licite et certain"},
170
- {"q": "Quels sont les trois vices du consentement qui peuvent entraîner la nullité d'un contrat ?",
171
- "hint": "erreur, dol, violence"},
172
- {"q": "Quelles personnes sont frappées d'incapacité contractuelle en droit français ?",
173
- "hint": "mineurs non émancipés, majeurs protégés sous tutelle ou curatelle"},
174
- {"q": "Qu'est-ce que le principe de l'effet relatif du contrat ?",
175
- "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"},
 
 
 
 
 
 
 
 
176
  ],
177
  },
178
  {
179
- "title": "Leçon 3La responsabilité civile",
180
  "text": """
181
- Cours de droit civil. La responsabilité civile.
182
- Première partie : Distinction responsabilité civile et pénale.
183
- La responsabilité civile répare le préjudice causé à autrui, contrairement
184
- à la responsabilité pénale qui sanctionne l'auteur d'une infraction.
185
- La responsabilité civile vise à remettre la victime dans l'état elle
186
- se trouvait avant le dommage, par l'octroi de dommages et intérêts.
187
- Deuxième partie : La responsabilité délictuelle.
188
- La responsabilité délictuelle repose sur trois conditions : faute, dommage
189
- et lien de causalité. L'article 1240 du Code civil dispose que tout fait
190
- de l'homme causant un dommage oblige son auteur à le réparer.
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
- Troisième partie : La responsabilité sans faute.
195
- La responsabilité sans faute couvre trois domaines principaux.
196
- Le fait des choses : on est responsable des dommages causés par les choses
197
- dont on a la garde. Le fait d'autrui : les parents sont responsables des dommages
198
- causés par leurs enfants mineurs ; les commettants répondent des fautes de leurs préposés.
199
- Les produits défectueux : le producteur est responsable du dommage causé
200
- par un défaut de son produit, sans que la victime ait à prouver une faute.
201
- Quatrième partie : La responsabilité contractuelle.
202
- Lorsque le dommage résulte de l'inexécution d'un contrat, la responsabilité
203
- contractuelle s'applique. Elle suppose un contrat valable, une inexécution
204
- et un préjudice. Le débiteur peut s'exonérer en prouvant la force majeure,
205
- c'est-à-dire un événement imprévisible, irrésistible et extérieur.
206
  """,
207
  "questions": [
208
- {"q": "Quelle est la différence entre responsabilité civile et responsabilité pénale ?",
209
- "hint": "civile répare le préjudice, pénale sanctionne l'auteur d'une infraction"},
210
- {"q": "Quelles sont les trois conditions de la responsabilité délictuelle ?",
211
- "hint": "faute, dommage, lien de causalité, article 1240 Code civil"},
212
- {"q": "Quels sont les trois domaines de la responsabilité sans faute ?",
213
- "hint": "fait des choses, fait d'autrui, produits défectueux"},
214
- {"q": "Qu'est-ce que la force majeure en matière de responsabilité contractuelle ?",
215
- "hint": "événement imprévisible, irrésistible et extérieur, exonère le débiteur"},
 
 
 
 
 
 
 
 
216
  ],
217
  },
218
  ]
219
 
220
- LISTEN_SECONDS = 10 # 900 en production (15 min)
221
  PASS_SCORE = 6
222
 
223
- # ── Cache audio par leçon ─────────────────────────────────────────────────────
224
  _audio_cache = {}
225
  _audio_lock = threading.Lock()
226
 
@@ -232,16 +233,16 @@ def get_lecture_audio(lesson_idx):
232
  return _audio_cache[lesson_idx]
233
  try:
234
  text = LESSONS[lesson_idx]["text"]
235
- tts = gTTS(text=text, lang="fr", slow=False)
236
  path = tempfile.mktemp(suffix=".mp3")
237
  tts.save(path)
238
  _audio_cache[lesson_idx] = path
239
  return path
240
  except Exception as e:
241
- print(f"[law-app] gTTS erreur : {e}")
242
  return None
243
 
244
- # ── État ──────────────────────────────────────────────────────────────────────
245
  def fresh():
246
  return json.dumps({"phase": "idle", "listen_start": None,
247
  "question_idx": None, "lesson_idx": 0})
@@ -249,53 +250,49 @@ def fresh():
249
  def S(s): return json.loads(s)
250
  def D(d): return json.dumps(d)
251
 
252
- # ── Évaluation ────────────────────────────────────────────────────────────────
253
  def evaluate_answer(question, answer, hint):
254
  llm = get_llm()
255
  if llm is None:
256
- kw = [w.strip().lower() for w in hint.split(",")]
257
  hits = sum(1 for k in kw if k in answer.lower())
258
  score = min(10, round((hits / max(len(kw), 1)) * 10))
259
- return score, f"(Sans IAmots-clés : {hits}/{len(kw)})"
260
 
261
  prompt = (
262
  "<|im_start|>system\n"
263
- "Tu es un professeur de droit français strict et bienveillant. "
264
- "Tu évalues les réponses des étudiants et réponds UNIQUEMENT en JSON valide.\n"
265
  "<|im_end|>\n"
266
  "<|im_start|>user\n"
267
- f"Question : {question}\n"
268
- f"Réponse de l'étudiant : {answer}\n"
269
- f"Éléments attendus : {hint}\n\n"
270
- "Évalue cette réponse sur 10 et donne un commentaire bref en français.\n"
271
- "Réponds UNIQUEMENT avec ce JSON sur une seule ligne, sans rien d'autre :\n"
272
- '{"score": <entier 0-10>, "feedback": "<commentaire bref>"}\n'
273
  "<|im_end|>\n"
274
  "<|im_start|>assistant\n"
275
  )
276
  try:
277
- out = llm(
278
- prompt,
279
- max_tokens=150,
280
- temperature=0.1,
281
- stop=["<|im_end|>", "\n\n", "###"],
282
- )
283
  text = out["choices"][0]["text"].strip()
284
- print(f"[law-app] Réponse modèle : {text}")
285
  start = text.find("{")
286
  end = text.rfind("}") + 1
287
  if start == -1 or end == 0:
288
- raise ValueError("Pas de JSON dans la réponse")
289
  data = json.loads(text[start:end])
290
  return max(0, min(10, int(data["score"]))), data.get("feedback", "")
291
  except Exception as e:
292
- print(f"[law-app] Erreur éval : {e}")
293
- kw = [w.strip().lower() for w in hint.split(",")]
294
  hits = sum(1 for k in kw if k in answer.lower())
295
  score = min(10, round((hits / max(len(kw), 1)) * 10))
296
- return score, f"(Erreur modèlemots-clés : {hits}/{len(kw)})"
297
 
298
- # ── Handlers ─────────────────────────────────────────────────────────────────
299
  def start_listening(ss):
300
  state = S(ss)
301
  lesson_idx = state.get("lesson_idx", 0)
@@ -305,7 +302,7 @@ def start_listening(ss):
305
  total = len(LESSONS)
306
  msg = (
307
  f"🎧 **{lesson['title']}** ({lesson_idx + 1}/{total})\n\n"
308
- "Écoutez pendant **15 minutes** complètes, puis cliquez sur **🔄 Vérifier le temps**."
309
  )
310
  return (
311
  audio, msg,
@@ -313,25 +310,25 @@ def start_listening(ss):
313
  gr.update(visible=False),
314
  gr.update(visible=False),
315
  gr.update(visible=False),
316
- gr.update(interactive=False, value="⏳ Écoute en cours…"),
317
  D(state),
318
  )
319
 
320
  def check_timer(ss):
321
  state = S(ss)
322
  if state["phase"] != "listen" or not state["listen_start"]:
323
- return gr.update(), "⚠️ Lancez d'abord l'écoute.", ss
324
  rem = max(0, LISTEN_SECONDS - (time.time() - state["listen_start"]))
325
  m, s = divmod(int(rem), 60)
326
  if rem <= 0:
327
  return (
328
- gr.update(interactive=True, value="✅ Accéder au Quiz"),
329
- "✅ Durée atteinte ! Cliquez sur **Accéder au Quiz**.",
330
  ss,
331
  )
332
  return (
333
- gr.update(interactive=False, value=f"⏳ Encore {m:02d}:{s:02d}"),
334
- f"🎧 Patientez encore **{m} min {s:02d} s**.",
335
  ss,
336
  )
337
 
@@ -343,7 +340,7 @@ def open_quiz(ss):
343
  return (
344
  gr.update(visible=True), gr.update(visible=False),
345
  gr.update(visible=False), gr.update(visible=False),
346
- f"⚠️ Patientez encore **{m} min {s:02d} s**.", "", "", ss,
347
  )
348
  lesson_idx = state.get("lesson_idx", 0)
349
  idx = random.randrange(len(LESSONS[lesson_idx]["questions"]))
@@ -354,7 +351,7 @@ def open_quiz(ss):
354
  "", LESSONS[lesson_idx]["questions"][idx]["q"], "", D(state),
355
  )
356
 
357
- # ── Submit avec désactivation immédiate du bouton ────────────────────────────
358
  def submit_answer(answer, ss):
359
  state = S(ss)
360
  lesson_idx = state.get("lesson_idx", 0)
@@ -364,23 +361,23 @@ def submit_answer(answer, ss):
364
  gr.update(visible=True), gr.update(visible=False),
365
  gr.update(visible=False), gr.update(visible=False),
366
  "",
367
- gr.update(value="📤 Soumettre", interactive=True),
368
  gr.update(interactive=True),
369
  ss,
370
  )
371
  return
372
 
373
- # ── Étape 1 : désactiver immédiatement bouton + textbox ──────────────────
374
  yield (
375
  gr.update(visible=False), gr.update(visible=True),
376
  gr.update(visible=False), gr.update(visible=False),
377
  "",
378
- gr.update(value="⏳ Réponse soumiseCorrection en cours…", interactive=False),
379
  gr.update(interactive=False),
380
  ss,
381
  )
382
 
383
- # ── Étape 2 : évaluation IA (peut prendre quelques secondes) ─────────────
384
  q = LESSONS[lesson_idx]["questions"][state["question_idx"]]
385
  score, fb = evaluate_answer(q["q"], answer, q["hint"])
386
  total = len(LESSONS)
@@ -389,26 +386,26 @@ def submit_answer(answer, ss):
389
  if score >= PASS_SCORE:
390
  state["phase"] = "pass"
391
  result = (
392
- f"## 🎉 Bravo ! Leçon {lesson_idx + 1}/{total} réussie.\n\n"
393
- f"**Score : {score}/10**\n\n**Commentaire :** {fb}"
394
  )
395
- next_label = "🏆 Voir mon certificat" if is_last else f"➡️ {LESSONS[lesson_idx + 1]['title']}"
396
  else:
397
  state["phase"] = "fail"
398
  result = (
399
- f"## ❌ Score insuffisant : {score}/10\n\n"
400
- f"**Commentaire :** {fb}\n\n"
401
- f"Seuil requis : **{PASS_SCORE}/10**. "
402
- "Veuillez **réécouter la leçon** avant de retenter."
403
  )
404
- next_label = "🔄 Réécouter la leçon"
405
 
406
- # ── Étape 3 : afficher résultat, réactiver bouton, mettre à jour next_btn
407
  yield (
408
  gr.update(visible=False), gr.update(visible=False),
409
  gr.update(visible=True), gr.update(visible=False),
410
  result,
411
- gr.update(value="📤 Soumettre", interactive=True),
412
  gr.update(value="", interactive=True),
413
  D(state),
414
  )
@@ -423,23 +420,13 @@ def handle_next(ss):
423
 
424
  if lesson_idx >= total - 1:
425
  state["phase"] = "done"
426
- cert = (
427
- "# 🏆 Félicitations !\n\n"
428
- "Vous avez complété avec succès les **3 leçons** du cours de droit civil :\n\n"
429
- "✅ Leçon 1 — Sources du droit et hiérarchie des normes\n\n"
430
- "✅ Leçon 2 — Le droit des contrats\n\n"
431
- "✅ Leçon 3 — La responsabilité civile\n\n"
432
- "---\n\n"
433
- "*Ce certificat atteste de votre participation et de votre réussite "
434
- "aux évaluations du cours de droit civil.*"
435
- )
436
  return (
437
  None, "",
438
  gr.update(visible=False),
439
  gr.update(visible=False),
440
  gr.update(visible=False),
441
  gr.update(visible=True),
442
- gr.update(interactive=False, value="✅ Terminé"),
443
  D(state),
444
  )
445
  else:
@@ -450,44 +437,49 @@ def handle_next(ss):
450
  def restart_from_cert(ss):
451
  return start_listening(fresh())
452
 
453
- # ── UI ────────────────────────────────────────────────────────────────────────
454
- CSS = "footer{display:none!important;} #title{text-align:center;}"
 
 
 
 
 
455
 
456
- with gr.Blocks(title="Cours de Droit", css=CSS) as demo:
457
  session = gr.State(fresh())
458
 
459
  gr.Markdown(
460
- "# ⚖️ Cours de Droit Formation Juridique\n"
461
- "Écoutez chaque leçon · Répondez à la question · Progressez leçon par leçon",
462
  elem_id="title",
463
  )
464
 
465
  with gr.Column(visible=True) as listen_col:
466
- gr.Markdown("## 🎧 Étape 1Écoute de la leçon")
467
- listen_msg = gr.Markdown("Cliquez sur **Démarrer** pour commencer.")
468
- audio_out = gr.Audio(label="Cours audio (français)", autoplay=False, interactive=False)
469
  timer_info = gr.Markdown("")
470
  with gr.Row():
471
- start_btn = gr.Button("▶️ Démarrer l'écoute" + str(_LLAMA_OK), variant="primary")
472
- check_btn = gr.Button("🔄 Vérifier le temps", variant="secondary")
473
- quiz_btn = gr.Button("✅ Accéder au Quiz", variant="secondary", interactive=False)
474
 
475
  with gr.Column(visible=False) as quiz_col:
476
- gr.Markdown("## 📝 Étape 2Question d'évaluation")
477
  question_md = gr.Markdown("")
478
- answer_box = gr.Textbox(label="Votre réponse", placeholder="Rédigez ici…", lines=5)
479
- submit_btn = gr.Button("📤 Soumettre", variant="primary")
480
 
481
  with gr.Column(visible=False) as result_col:
482
- gr.Markdown("## 📊 Résultat")
483
  result_md = gr.Markdown("")
484
- next_btn = gr.Button("➡️ Leçon suivante", variant="primary")
485
 
486
  with gr.Column(visible=False) as cert_col:
487
  cert_md = gr.Markdown("")
488
- restart_btn = gr.Button("🔄 Recommencer depuis le début", variant="secondary")
489
 
490
- # ── Câblage ───────────────────────────────────────────────────────────────
491
  LISTEN_OUT = [audio_out, listen_msg,
492
  listen_col, quiz_col, result_col, cert_col,
493
  quiz_btn, session]
@@ -499,7 +491,6 @@ with gr.Blocks(title="Cours de Droit", css=CSS) as demo:
499
  timer_info, question_md, answer_box, session]
500
  quiz_btn.click(open_quiz, [session], QUIZ_OUT)
501
 
502
- # ── submit_answer est un générateur → outputs inclut submit_btn et answer_box
503
  RES_OUT = [listen_col, quiz_col, result_col, cert_col,
504
  result_md, submit_btn, answer_box, session]
505
  submit_btn.click(submit_answer, [answer_box, session], RES_OUT)
@@ -508,7 +499,7 @@ with gr.Blocks(title="Cours de Droit", css=CSS) as demo:
508
  next_btn.click(handle_next, [session], LISTEN_OUT)
509
  restart_btn.click(restart_from_cert, [session], LISTEN_OUT)
510
 
511
- # ── Préchargement du modèle au démarrage ──────────────────────────────────────
512
  threading.Thread(target=get_llm, daemon=True).start()
513
 
514
  if __name__ == "__main__":
 
1
  """
2
+ دورة القانون – Hugging Face Space
3
+ النسخة العربية الكاملة
4
  Compatible avec le Docker HF (gradio 4.44.1 bundled + Python 3.13).
 
5
  """
6
  import os, json, time, tempfile, random, threading, glob
7
  import sys
 
40
  except ImportError:
41
  _LLAMA_OK = False
42
 
43
+ # ── تحميل النموذج ─────────────────────────────────────────────────────────────
44
  _llm = None
45
  _llm_lock = threading.Lock()
46
 
47
  def download_model():
48
  model_path = "/app/model.gguf"
49
  if os.path.exists(model_path):
50
+ print("[law-app] النموذج موجود مسبقاً.")
51
  return model_path
52
+ print("[law-app] جارٍ تحميل النموذج...")
53
  try:
54
  path = hf_hub_download(
55
  repo_id="Qwen/Qwen2.5-0.5B-Instruct-GGUF",
 
59
  )
60
  if path != model_path:
61
  os.rename(path, model_path)
62
+ print("[law-app] اكتمل التحميل.")
63
  return model_path
64
  except Exception as e:
65
+ print(f"[law-app] خطأ في التحميل: {e}")
66
  return None
67
 
68
  def get_llm():
 
76
  return _llm
77
  gguf_files = glob.glob("/app/*.gguf") + glob.glob("*.gguf")
78
  if not gguf_files:
79
+ print("[law-app] لم يُعثر على ملف .gguf الذكاء الاصطناعي معطّل.")
80
  return None
81
  model_path = gguf_files[0]
82
+ print(f"[law-app] تحميل النموذج: {model_path}")
83
  _llm = Llama(model_path=model_path, n_ctx=512, n_threads=2,
84
  n_gpu_layers=int(os.getenv("N_GPU_LAYERS", "-1")), verbose=False)
85
+ print("[law-app] النموذج جاهز.")
86
  return _llm
87
 
88
+ # ── الدروس ────────────────────────────────────────────────────────────────────
89
  LESSONS = [
90
  {
91
+ "title": "الدرس الأولمصادر القانون والتدرج الهرمي للقواعد",
92
  "text": """
93
+ دورة القانون المدني. مقدمة في مصادر القانون الجزائري.
94
+ الجزء الأول: المصادر الرسمية للقانون.
95
+ يقوم القانون الجزائري على عدة مصادر متدرجة.
96
+ في القمة توجد الدستور، الذي يُعدّ القانون الأساسي للجمهورية الجزائرية الديمقراطية الشعبية.
97
+ يحدد الدستور قواعد تنظيم الدولة ويضمن الحقوق الأساسية للمواطنين.
98
+ تحت الدستور تأتي المعاهدات الدولية والقانون الدولي.
99
+ بموجب أحكام الدستور، تتمتع المعاهدات المصادَق عليها بسلطة تعلو على القوانين العادية.
100
+ ثم تأتي القوانين التي يصادق عليها البرلمان.
101
+ القانون العادي يُصوَّت عليه في مجلس الشعب الوطني ومجلس الأمة.
102
+ القوانين العضوية تُكمّل وتوضح بعض الأحكام الدستورية وتتمتع بمكانة وسيطة.
103
+ اللوائح والمراسيم تشكّل المصدر الرابع الرئيسي، وتصدر عن السلطة التنفيذية.
104
+ الجزء الثاني: المصادر المادية للقانون.
105
+ الاجتهاد القضائي يلعب دوراً جوهرياً في النظام القانوني الجزائري.
106
+ قرارات المحكمة العليا ومجلس الدولة تفسر وتوضح نطاق النصوص القانونية.
107
+ الفقه القانوني يضم مجموع أعمال علماء القانون من أساتذة ومحامين وقضاة.
108
+ العرف مصدر قديم يحتفظ بأهمية معينة في المعاملات التجارية وبعض المسائل الأسرية.
109
+ الجزء الثالث: مبدأ التدرج الهرمي للقواعد.
110
+ نظرية هرم القواعد القانونية وضعها الفقيه النمساوي هانس كلسن في القرن العشرين.
111
+ كل قاعدة تستمد مشروعيتها من قاعدة أعلى منها.
112
+ في الجزائر، تتولى المحكمة الدستورية السهر على احترام هذا التدرج.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
  """,
114
  "questions": [
115
+ {
116
+ "q": "ما هو المصدر الذي يقع في قمة هرم القواعد القانونية في الجزائر؟",
117
+ "hint": "الدستور، القانون الأساسي، الجمهورية الجزائرية الديمقراطية الشعبية"
118
+ },
119
+ {
120
+ "q": "ما هي نظرية هرم القواعد القانونية ومن هو مؤسسها؟",
121
+ "hint": "هانس كلسن، التدرج الهرمي، كل قاعدة تستمد مشروعيتها من قاعدة أعلى"
122
+ },
123
+ {
124
+ "q": "ما هو دور الاجتهاد القضائي في النظام القانوني الجزائري؟",
125
+ "hint": "يفسر ويوضح النصوص القانونية، المحكمة العليا، مجلس الدولة"
126
+ },
127
+ {
128
+ "q": "ما الفرق بين القانون العادي والقانون العضوي؟",
129
+ "hint": "القانون العادي يُصوَّت عليه في البرلمان، القانون العضوي يكمل الدستور، مكانة وسيطة"
130
+ },
131
  ],
132
  },
133
  {
134
+ "title": "الدرس الثانيقانون العقود",
135
  "text": """
136
+ دورة القانون المدني. قانون العقود.
137
+ الجزء الأول: تعريف شروط صحة العقد.
138
+ العقد هو اتفاق بين إرادتين يهدف إلى إنشاء التزامات أو تعديلها أو نقلها أو إنهائها.
139
+ يشترط القانون المدني الجزائري ثلاثة شروط لصحة العقد:
140
+ أولاً: التراضي، أي توافق إرادة الطرفين.
141
+ ثانياً: الأهلية، إذ يجب أن يكون كل طرف أهلاً للتعاقد.
142
+ ثالثاً: المحل والسبب، فيجب أن يكون موضوع العقد مشروعاً ومعيناً أو قابلاً للتعيين.
143
+ الرضا يجب أن يكون حراً وخالياً من العيوب.
144
+ يُعيب الرضا كلٌّ من: الغلط، والتدليس، والإكراه، والغبن.
145
+ عدم الأهلية تمسّ القاصرين وفاقدي الأهلية والمحجور عليهم.
146
+ الجزء الثاني: آثار العقد.
147
+ تنفيذ العقد يخضع لمبدأ القوة الملزمة، إذ يسري العقد بين الطرفين سريان القانون.
148
+ في حالة الإخلال بالعقد: يجوز للمتضرر المطالبة بالتنفيذ العيني أو فسخ العقد أو التعويض.
149
+ مبدأ نسبية أثر العقد يعني أن العقد لا يُنشئ التزامات إلا بين أطرافه المتعاقدة.
150
+ الغير لا يستطيع في الأصل الاستفادة من العقد ولا الاحتجاج به عليه.
151
+ الجزء الثالث: تكوين العقد.
152
+ يتكون العقد بالإيجاب والقبول.
153
+ الإيجاب يجب أن يكون محدداً وباتاً، والقبول يجب أن يكون مطابقاً للإيجاب.
154
+ كل رد مغاير يُعدّ إيجاباً جديداً.
155
+ العقود التمهيدية كالوعد بالبيع وحق الأفضلية تُنظّم مرحلة ما قبل التعاقد.
 
 
 
156
  """,
157
  "questions": [
158
+ {
159
+ "q": "ما هي الشروط الثلاثة لصحة العقد وفق القانون المدني الجزائري؟",
160
+ "hint": "التراضي، الأهلية، المحل والسبب المشروع"
161
+ },
162
+ {
163
+ "q": "ما هي عيوب الإرادة التي قد تؤدي إلى بطلان العقد؟",
164
+ "hint": "الغلط، التدليس، الإكراه، الغبن"
165
+ },
166
+ {
167
+ "q": "من هم الأشخاص الذين تمسّهم حالات عدم الأهلية في القانون الجزائري؟",
168
+ "hint": "القاصرون، فاقدو الأهلية، المحجور عليهم"
169
+ },
170
+ {
171
+ "q": "ما هو مبدأ نسبية أثر العقد؟",
172
+ "hint": "العقد ��ا يُنشئ التزامات إلا بين الأطراف المتعاقدة، الغير لا يستطيع الاستفادة منه"
173
+ },
174
  ],
175
  },
176
  {
177
+ "title": "الدرس الثالثالمسؤولية المدنية",
178
  "text": """
179
+ دورة القانون المدني. المسؤولية المدنية.
180
+ الجزء الأول: التمييز بين المسؤولية المدنية والجنائية.
181
+ المسؤولية المدنية تهدف إلى إصلاح الضرر الذي لحق بالغير.
182
+ أما المسؤولية الجنائية فتهدف إلى معاقبة مرتكب الجريمة.
183
+ المسؤولية المدنية تسعى إلى إعادة المضرور إلى الحالة التي كان عليها قبل وقوع الضرر بمنحه التعويض.
184
+ الجزء الثاني: المسؤولية التقصيرية.
185
+ تقوم المسؤولية التقصيرية على ثلاثة أركان: الخطأ والضرر والعلاقة السببية.
186
+ تنص المادة 124 من القانون المدني الجزائري على أن كل فعل أيًّا كان يرتكبه الشخص بخطئه سبّب ضرراً للغير يُلزم من كان سبباً في حدوثه بالتعويض.
187
+ الخطأ قد يكون عمدياً أو غير عمدي.
188
+ الضرر يجب أن يكون محققاً ومباشراً وشخصياً.
189
+ على المضرور إثبات العلاقة السببية بين الخطأ والضرر.
190
+ الجزء الثالث: المسؤولية بدون خطأ.
191
+ تشمل المسؤولية بدون خطأ ثلاثة مجالات رئيسية.
192
+ مسؤولية حارس الشيء: يُسأل الحارس عن الأضرار التي تسببها الأشياء الموضوعة تحت حراسته.
193
+ مسؤولية عن فعل الغير: يكون الآباء مسؤولين عن الأضرار التي يتسبب فيها أبناؤهم القاصرون.
194
+ مسؤولية المنتج: يُسأل المنتج عن الضرر الناجم عن عيب في منتجه دون حاجة المضرور لإثبات الخطأ.
195
+ الجزء الرابع: المسؤولية العقدية.
196
+ حين ينشأ الضرر عن عدم تنفيذ عقد، تطبّق أحكام المسؤولية العقدية.
197
+ تستلزم هذه المسؤولية وجود عقد صحيح وإخلالاً بالتزاماته وضرراً ناجماً عن ذلك.
198
+ يستطيع المدين أن يُعفى من المسؤولية بإثبات القوة القاهرة، وهي حادث لا يمكن توقعه ولا دفعه وخارج عن إرادته.
 
 
 
 
 
199
  """,
200
  "questions": [
201
+ {
202
+ "q": "ما الفرق بين المسؤولية المدنية والمسؤولية الجنائية؟",
203
+ "hint": "المدنية تُصلح الضرر وتمنح التعويض، الجنائية تعاقب مرتكب الجريمة"
204
+ },
205
+ {
206
+ "q": "ما هي الأركان الثلاثة للمسؤولية التقصيرية؟",
207
+ "hint": "الخطأ، الضرر، العلاقة السببية، المادة 124 القانون المدني"
208
+ },
209
+ {
210
+ "q": "ما هي المجالات الثلاثة للمسؤولية بدون خطأ؟",
211
+ "hint": "مسؤولية حارس الشيء، مسؤولية عن فعل الغير، مسؤولية المنتج"
212
+ },
213
+ {
214
+ "q": "ما هي القوة القاهرة في إطار المسؤولية العقدية؟",
215
+ "hint": "حادث لا يمكن توقعه ولا دفعه وخارج عن إرادة المدين، يُعفيه من المسؤولية"
216
+ },
217
  ],
218
  },
219
  ]
220
 
221
+ LISTEN_SECONDS = 10 # 900 في بيئة الإنتاج (15 دقيقة)
222
  PASS_SCORE = 6
223
 
224
+ # ── ذاكرة تخزين الصوت ────────────────────────────────────────────────────────
225
  _audio_cache = {}
226
  _audio_lock = threading.Lock()
227
 
 
233
  return _audio_cache[lesson_idx]
234
  try:
235
  text = LESSONS[lesson_idx]["text"]
236
+ tts = gTTS(text=text, lang="ar", slow=False)
237
  path = tempfile.mktemp(suffix=".mp3")
238
  tts.save(path)
239
  _audio_cache[lesson_idx] = path
240
  return path
241
  except Exception as e:
242
+ print(f"[law-app] خطأ gTTS: {e}")
243
  return None
244
 
245
+ # ── الحالة ────────────────────────────────────────────────────────────────────
246
  def fresh():
247
  return json.dumps({"phase": "idle", "listen_start": None,
248
  "question_idx": None, "lesson_idx": 0})
 
250
  def S(s): return json.loads(s)
251
  def D(d): return json.dumps(d)
252
 
253
+ # ── التقييم ───────────────────────────────────────────────────────────────────
254
  def evaluate_answer(question, answer, hint):
255
  llm = get_llm()
256
  if llm is None:
257
+ kw = [w.strip().lower() for w in hint.split("،")]
258
  hits = sum(1 for k in kw if k in answer.lower())
259
  score = min(10, round((hits / max(len(kw), 1)) * 10))
260
+ return score, f"(بدون ذكاء اصطناعي الكلمات المفتاحية: {hits}/{len(kw)})"
261
 
262
  prompt = (
263
  "<|im_start|>system\n"
264
+ "أنت أستاذ قانون جزائري صارم ومتفهم. "
265
+ "تقيّم إجابات الطلاب وتردّ فقط بصيغة JSON صحيحة.\n"
266
  "<|im_end|>\n"
267
  "<|im_start|>user\n"
268
+ f"السؤال: {question}\n"
269
+ f"إجابة الطالب: {answer}\n"
270
+ f"العناصر المتوقعة: {hint}\n\n"
271
+ "قيّم هذه الإجابة من 10 وأعطِ تعليقاً موجزاً باللغة العربية.\n"
272
+ "أجب فقط بهذا JSON في سطر واحد دون أي إضافة:\n"
273
+ '{"score": <عدد صحيح 0-10>, "feedback": "<تعليق موجز>"}\n'
274
  "<|im_end|>\n"
275
  "<|im_start|>assistant\n"
276
  )
277
  try:
278
+ out = llm(prompt, max_tokens=150, temperature=0.1,
279
+ stop=["<|im_end|>", "\n\n", "###"])
 
 
 
 
280
  text = out["choices"][0]["text"].strip()
281
+ print(f"[law-app] رد النموذج: {text}")
282
  start = text.find("{")
283
  end = text.rfind("}") + 1
284
  if start == -1 or end == 0:
285
+ raise ValueError("لا يوجد JSON في الرد")
286
  data = json.loads(text[start:end])
287
  return max(0, min(10, int(data["score"]))), data.get("feedback", "")
288
  except Exception as e:
289
+ print(f"[law-app] خطأ في التقييم: {e}")
290
+ kw = [w.strip().lower() for w in hint.split("،")]
291
  hits = sum(1 for k in kw if k in answer.lower())
292
  score = min(10, round((hits / max(len(kw), 1)) * 10))
293
+ return score, f"(خطأ في النموذج الكلمات المفتاحية: {hits}/{len(kw)})"
294
 
295
+ # ── المعالجات ─────────────────────────────────────────────────────────────────
296
  def start_listening(ss):
297
  state = S(ss)
298
  lesson_idx = state.get("lesson_idx", 0)
 
302
  total = len(LESSONS)
303
  msg = (
304
  f"🎧 **{lesson['title']}** ({lesson_idx + 1}/{total})\n\n"
305
+ "استمع لمدة **15 دقيقة** كاملة، ثم انقر على **🔄 التحقق من الوقت**."
306
  )
307
  return (
308
  audio, msg,
 
310
  gr.update(visible=False),
311
  gr.update(visible=False),
312
  gr.update(visible=False),
313
+ gr.update(interactive=False, value="⏳ جارٍ الاستماع…"),
314
  D(state),
315
  )
316
 
317
  def check_timer(ss):
318
  state = S(ss)
319
  if state["phase"] != "listen" or not state["listen_start"]:
320
+ return gr.update(), "⚠️ ابدأ الاستماع أولاً.", ss
321
  rem = max(0, LISTEN_SECONDS - (time.time() - state["listen_start"]))
322
  m, s = divmod(int(rem), 60)
323
  if rem <= 0:
324
  return (
325
+ gr.update(interactive=True, value="✅ الانتقال إلى الاختبار"),
326
+ "✅ انتهى الوقت! انقر على **الانتقال إلى الاختبار**.",
327
  ss,
328
  )
329
  return (
330
+ gr.update(interactive=False, value=f"⏳ تبقّى {m:02d}:{s:02d}"),
331
+ f"🎧 انتظر **{m} دقيقة و{s:02d} ثانية** إضافية.",
332
  ss,
333
  )
334
 
 
340
  return (
341
  gr.update(visible=True), gr.update(visible=False),
342
  gr.update(visible=False), gr.update(visible=False),
343
+ f"⚠️ انتظر **{m} دقيقة و{s:02d} ثانية** إضافية.", "", "", ss,
344
  )
345
  lesson_idx = state.get("lesson_idx", 0)
346
  idx = random.randrange(len(LESSONS[lesson_idx]["questions"]))
 
351
  "", LESSONS[lesson_idx]["questions"][idx]["q"], "", D(state),
352
  )
353
 
354
+ # ── تقديم الإجابة مع تعطيل فوري للزر ────────────────────────────────────────
355
  def submit_answer(answer, ss):
356
  state = S(ss)
357
  lesson_idx = state.get("lesson_idx", 0)
 
361
  gr.update(visible=True), gr.update(visible=False),
362
  gr.update(visible=False), gr.update(visible=False),
363
  "",
364
+ gr.update(value="📤 إرسال الإجابة", interactive=True),
365
  gr.update(interactive=True),
366
  ss,
367
  )
368
  return
369
 
370
+ # ── الخطوة 1: تعطيل الزر فوراً وعرض رسالة الانتظار ─────────────────────
371
  yield (
372
  gr.update(visible=False), gr.update(visible=True),
373
  gr.update(visible=False), gr.update(visible=False),
374
  "",
375
+ gr.update(value="⏳ تم إرسال الإجابة جارٍ التصحيح…", interactive=False),
376
  gr.update(interactive=False),
377
  ss,
378
  )
379
 
380
+ # ── الخطوة 2: التقييم بالذكاء الاصطناعي ─────────────────────────────────
381
  q = LESSONS[lesson_idx]["questions"][state["question_idx"]]
382
  score, fb = evaluate_answer(q["q"], answer, q["hint"])
383
  total = len(LESSONS)
 
386
  if score >= PASS_SCORE:
387
  state["phase"] = "pass"
388
  result = (
389
+ f"## 🎉 أحسنت! اجتزت الدرس {lesson_idx + 1} من {total}.\n\n"
390
+ f"**النتيجة: {score}/10**\n\n**التعليق:** {fb}"
391
  )
392
+ next_label = "🏆 عرض الشهادة" if is_last else f"➡️ {LESSONS[lesson_idx + 1]['title']}"
393
  else:
394
  state["phase"] = "fail"
395
  result = (
396
+ f"## ❌ النتيجة غير كافية: {score}/10\n\n"
397
+ f"**التعليق:** {fb}\n\n"
398
+ f"الحد الأدنى المطلوب: **{PASS_SCORE}/10**. "
399
+ "يُرجى **إعادة الاستماع إلى الدرس** قبل المحاولة مجدداً."
400
  )
401
+ next_label = "🔄 إعادة الاستماع إلى الدرس"
402
 
403
+ # ── الخطوة 3: عرض النتيجة وإعادة تفعيل الزر────────────────────────────
404
  yield (
405
  gr.update(visible=False), gr.update(visible=False),
406
  gr.update(visible=True), gr.update(visible=False),
407
  result,
408
+ gr.update(value="📤 إرسال الإجابة", interactive=True),
409
  gr.update(value="", interactive=True),
410
  D(state),
411
  )
 
420
 
421
  if lesson_idx >= total - 1:
422
  state["phase"] = "done"
 
 
 
 
 
 
 
 
 
 
423
  return (
424
  None, "",
425
  gr.update(visible=False),
426
  gr.update(visible=False),
427
  gr.update(visible=False),
428
  gr.update(visible=True),
429
+ gr.update(interactive=False, value="✅ انتهى"),
430
  D(state),
431
  )
432
  else:
 
437
  def restart_from_cert(ss):
438
  return start_listening(fresh())
439
 
440
+ # ── واجهة المستخدم ────────────────────────────────────────────────────────────
441
+ CSS = """
442
+ footer { display: none !important; }
443
+ #title { text-align: center; }
444
+ .gradio-container { direction: rtl; font-family: 'Amiri', 'Noto Sans Arabic', 'Segoe UI', sans-serif; }
445
+ .gr-button { font-family: 'Amiri', 'Noto Sans Arabic', sans-serif; }
446
+ """
447
 
448
+ with gr.Blocks(title="دورة القانون", css=CSS) as demo:
449
  session = gr.State(fresh())
450
 
451
  gr.Markdown(
452
+ "# ⚖️ دورة القانونالتكوين القانوني\n"
453
+ "استمع إلى كل درس · أجب عن السؤال · تقدّم درساً بعد درس",
454
  elem_id="title",
455
  )
456
 
457
  with gr.Column(visible=True) as listen_col:
458
+ gr.Markdown("## 🎧 الخطوة الأولىالاستماع إلى الدرس")
459
+ listen_msg = gr.Markdown("انقر على **ابدأ** للبدء.")
460
+ audio_out = gr.Audio(label="الدرس الصوتي (باللغة العربية)", autoplay=False, interactive=False)
461
  timer_info = gr.Markdown("")
462
  with gr.Row():
463
+ start_btn = gr.Button("▶️ بدء الاستماع", variant="primary")
464
+ check_btn = gr.Button("🔄 التحقق من الوقت", variant="secondary")
465
+ quiz_btn = gr.Button("✅ الانتقال إلى الاختبار", variant="secondary", interactive=False)
466
 
467
  with gr.Column(visible=False) as quiz_col:
468
+ gr.Markdown("## 📝 الخطوة الثانيةسؤال التقييم")
469
  question_md = gr.Markdown("")
470
+ answer_box = gr.Textbox(label="إجابتك", placeholder="اكتب إجابتك هنا…", lines=5)
471
+ submit_btn = gr.Button("📤 إرسال الإجابة", variant="primary")
472
 
473
  with gr.Column(visible=False) as result_col:
474
+ gr.Markdown("## 📊 النتيجة")
475
  result_md = gr.Markdown("")
476
+ next_btn = gr.Button("➡️ الدرس التالي", variant="primary")
477
 
478
  with gr.Column(visible=False) as cert_col:
479
  cert_md = gr.Markdown("")
480
+ restart_btn = gr.Button("🔄 إعادة البدء من الأول", variant="secondary")
481
 
482
+ # ── الربط ─────────────────────────────────────────────────────────────────
483
  LISTEN_OUT = [audio_out, listen_msg,
484
  listen_col, quiz_col, result_col, cert_col,
485
  quiz_btn, session]
 
491
  timer_info, question_md, answer_box, session]
492
  quiz_btn.click(open_quiz, [session], QUIZ_OUT)
493
 
 
494
  RES_OUT = [listen_col, quiz_col, result_col, cert_col,
495
  result_md, submit_btn, answer_box, session]
496
  submit_btn.click(submit_answer, [answer_box, session], RES_OUT)
 
499
  next_btn.click(handle_next, [session], LISTEN_OUT)
500
  restart_btn.click(restart_from_cert, [session], LISTEN_OUT)
501
 
502
+ # ── تحميل النموذج مسبقاً عند الإطلاق ───────────────────────���───────────────
503
  threading.Thread(target=get_llm, daemon=True).start()
504
 
505
  if __name__ == "__main__":