Update app.py
Browse files
app.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
"""
|
| 2 |
-
|
|
|
|
| 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 |
-
# ──
|
| 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,10 +59,10 @@ def download_model():
|
|
| 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]
|
| 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("[
|
| 80 |
return None
|
| 81 |
model_path = gguf_files[0]
|
| 82 |
-
print(f"[
|
| 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("[
|
| 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 |
-
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 |
-
{
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
{
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
],
|
| 139 |
},
|
| 140 |
{
|
| 141 |
-
"title": "
|
| 142 |
"text": """
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 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 |
-
{
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
{
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 176 |
],
|
| 177 |
},
|
| 178 |
{
|
| 179 |
-
"title": "
|
| 180 |
"text": """
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 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 |
-
{
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
{
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 216 |
],
|
| 217 |
},
|
| 218 |
]
|
| 219 |
|
| 220 |
-
LISTEN_SECONDS = 10 # 900
|
| 221 |
PASS_SCORE = 6
|
| 222 |
|
| 223 |
-
# ──
|
| 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="
|
| 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]
|
| 242 |
return None
|
| 243 |
|
| 244 |
-
# ──
|
| 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 |
-
# ──
|
| 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"(
|
| 260 |
|
| 261 |
prompt = (
|
| 262 |
"<|im_start|>system\n"
|
| 263 |
-
"
|
| 264 |
-
"
|
| 265 |
"<|im_end|>\n"
|
| 266 |
"<|im_start|>user\n"
|
| 267 |
-
f"
|
| 268 |
-
f"
|
| 269 |
-
f"
|
| 270 |
-
"
|
| 271 |
-
"
|
| 272 |
-
'{"score": <
|
| 273 |
"<|im_end|>\n"
|
| 274 |
"<|im_start|>assistant\n"
|
| 275 |
)
|
| 276 |
try:
|
| 277 |
-
out = llm(
|
| 278 |
-
|
| 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]
|
| 285 |
start = text.find("{")
|
| 286 |
end = text.rfind("}") + 1
|
| 287 |
if start == -1 or end == 0:
|
| 288 |
-
raise ValueError("
|
| 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]
|
| 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"(
|
| 297 |
|
| 298 |
-
# ──
|
| 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 |
-
"
|
| 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="⏳
|
| 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(), "⚠️
|
| 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="✅
|
| 329 |
-
"✅
|
| 330 |
ss,
|
| 331 |
)
|
| 332 |
return (
|
| 333 |
-
gr.update(interactive=False, value=f"⏳
|
| 334 |
-
f"🎧
|
| 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"⚠️
|
| 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 |
-
# ──
|
| 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="📤
|
| 368 |
gr.update(interactive=True),
|
| 369 |
ss,
|
| 370 |
)
|
| 371 |
return
|
| 372 |
|
| 373 |
-
# ──
|
| 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="⏳
|
| 379 |
gr.update(interactive=False),
|
| 380 |
ss,
|
| 381 |
)
|
| 382 |
|
| 383 |
-
# ──
|
| 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"## 🎉
|
| 393 |
-
f"**
|
| 394 |
)
|
| 395 |
-
next_label = "🏆
|
| 396 |
else:
|
| 397 |
state["phase"] = "fail"
|
| 398 |
result = (
|
| 399 |
-
f"## ❌
|
| 400 |
-
f"**
|
| 401 |
-
f"
|
| 402 |
-
"
|
| 403 |
)
|
| 404 |
-
next_label = "🔄
|
| 405 |
|
| 406 |
-
# ──
|
| 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="📤
|
| 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="✅
|
| 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 |
-
# ──
|
| 454 |
-
CSS = "
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 455 |
|
| 456 |
-
with gr.Blocks(title="
|
| 457 |
session = gr.State(fresh())
|
| 458 |
|
| 459 |
gr.Markdown(
|
| 460 |
-
"# ⚖️
|
| 461 |
-
"
|
| 462 |
elem_id="title",
|
| 463 |
)
|
| 464 |
|
| 465 |
with gr.Column(visible=True) as listen_col:
|
| 466 |
-
gr.Markdown("## 🎧
|
| 467 |
-
listen_msg = gr.Markdown("
|
| 468 |
-
audio_out = gr.Audio(label="
|
| 469 |
timer_info = gr.Markdown("")
|
| 470 |
with gr.Row():
|
| 471 |
-
start_btn = gr.Button("▶️
|
| 472 |
-
check_btn = gr.Button("🔄
|
| 473 |
-
quiz_btn = gr.Button("✅
|
| 474 |
|
| 475 |
with gr.Column(visible=False) as quiz_col:
|
| 476 |
-
gr.Markdown("## 📝
|
| 477 |
question_md = gr.Markdown("")
|
| 478 |
-
answer_box = gr.Textbox(label="
|
| 479 |
-
submit_btn = gr.Button("📤
|
| 480 |
|
| 481 |
with gr.Column(visible=False) as result_col:
|
| 482 |
-
gr.Markdown("## 📊
|
| 483 |
result_md = gr.Markdown("")
|
| 484 |
-
next_btn = gr.Button("➡️
|
| 485 |
|
| 486 |
with gr.Column(visible=False) as cert_col:
|
| 487 |
cert_md = gr.Markdown("")
|
| 488 |
-
restart_btn = gr.Button("🔄
|
| 489 |
|
| 490 |
-
# ──
|
| 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 |
-
# ──
|
| 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__":
|