""" دورة القانون – Hugging Face Space النسخة العربية الكاملة Compatible avec le Docker HF (gradio 4.44.1 bundled + Python 3.13). """ import os, json, time, tempfile, random, threading, glob import sys try: import pyaudioop sys.modules["audioop"] = pyaudioop except ImportError: pass import gradio as gr try: import gradio_client.utils as _gcu import gradio.blocks as _gb _orig = _gcu.json_schema_to_python_type def _safe(schema): try: return _orig(schema) except Exception: return "Any" _gcu.json_schema_to_python_type = _safe _gb.client_utils.json_schema_to_python_type = _safe except Exception: pass try: from llama_cpp import Llama _LLAMA_AVAILABLE = True except ImportError: _LLAMA_AVAILABLE = False from gtts import gTTS from huggingface_hub import hf_hub_download try: from llama_cpp import Llama _LLAMA_OK = True except ImportError: _LLAMA_OK = False # ── تحميل النموذج ───────────────────────────────────────────────────────────── _llm = None _llm_lock = threading.Lock() def download_model(): model_path = "/app/model.gguf" if os.path.exists(model_path): print("[law-app] النموذج موجود مسبقاً.") return model_path print("[law-app] جارٍ تحميل النموذج...") try: path = hf_hub_download( repo_id="Qwen/Qwen2.5-0.5B-Instruct-GGUF", filename="qwen2.5-0.5b-instruct-q4_k_m.gguf", local_dir="/app", local_dir_use_symlinks=False, ) if path != model_path: os.rename(path, model_path) print("[law-app] اكتمل التحميل.") return model_path except Exception as e: print(f"[law-app] خطأ في التحميل: {e}") return None def get_llm(): global _llm if _llm is not None: return _llm if not _LLAMA_AVAILABLE: return None with _llm_lock: if _llm is not None: return _llm gguf_files = glob.glob("/app/*.gguf") + glob.glob("*.gguf") if not gguf_files: print("[law-app] لم يُعثر على ملف .gguf – الذكاء الاصطناعي معطّل.") return None model_path = gguf_files[0] print(f"[law-app] تحميل النموذج: {model_path}") _llm = Llama(model_path=model_path, n_ctx=512, n_threads=2, n_gpu_layers=int(os.getenv("N_GPU_LAYERS", "-1")), verbose=False) print("[law-app] النموذج جاهز.") return _llm LESSONS = [ { "title": "الدرس الأول — الأعداد من 0 إلى 999 (علب الحلوى السحرية)", "text": """ مرحباً بك يا بطل! تخيل أن لدينا ثلاث علب نضع فيها قطع الحلوى السحرية: 1. علبة الوحدات (و): نضع فيها حبات الحلوى الفردية (من 1 إلى 9). 2. علبة العشرات (ع): نضع فيها علباً صغيرة، كل علبة بها 10 حبات. 3. علبة المئات (م): نضع فيها علباً كبيرة، كل علبة بها 100 حبة. أولاً: كيف نقرأ ونكتب العدد؟ (الصفحة 17) لو كان لدينا العدد (659): - ننظر للعلبة الكبيرة أولاً (المئات): لدينا 6 علب من فئة 100، فنقول: "ستمائة". - ثم ننظر لعلبة الوحدات: لدينا 9 حبات فردية، فنقول: "وتسعة". - ثم علبة العشرات: لدينا 5 علب من فئة 10، فنقول: "وخمسون". نقرأ العدد كاملاً هكذا: "سِتُّمِائَةٍ وَتِسْعَةٌ وَخَمْسُونَ". ثانياً: كيف نفكك العدد؟ (الصفحة 17) التفكيك يعني أن نفتح العلب ونرى كم حبة حلوى بداخلها بالكامل: - التفكيك بالجمع: نأخذ كل منزلة ونكتب قيمتها: 659 = 600 + 50 + 9 - التفكيك بالضرب والجمع: نكتب كم علبة لدينا في كل منزلة: 659 = (6 × 100) + (5 × 10) + 9 """, "questions": [ {"q": "اكتب العدد (أربعمائة واثنان وثلاثون) بالأرقام ثم فككه تفكيكاً بالجمع.", "hint": "العدد يكتب 432، وتفكيكه هو: 432 = 400 + 30 + 2 (انظر الصفحة 17)."}, {"q": "ما هي مرتبة الرقم 5 في العدد 527؟ وكم حبة حلوى يمثل؟", "hint": "مرتبته هي المئات (م)، ويمثل 500 حبة حلوى سحرية (انظر الصفحة 17)."}, {"q": "افتح علب العدد 854 وفككه بطريقة الضرب والجمع.", "hint": "التفكيك هو: 854 = (8 × 100) + (5 × 10) + 4 (انظر الصفحة 25)."} ] }, { "title": "الدرس الثاني — الجمع والطرح (استراتيجيات الحساب الذكي والضفدع القفاز)", "text": """ مرحبا يا ذكي! هناك طرق ممتعة تساعدك على الجمع والطرح بسرعة في ذهنك: أولاً: الجمع بالتفكيك والإتمام (الصفحة 14) لو أردنا حساب: 54 + 7 - العدد 54 قريب جداً من الـ 60 (يحتاج فقط إلى 6 خطوات ليصل إليها). - نذهب للـ 7 ونأخذ منها 6 خطوات، فتبقى الـ 7 عبارة عن (6 + 1). - الآن نجمع بسهولة: 54 + 6 = 60، ثم نضيف الـ 1 المتبقي: 60 + 1 = 61. - إذن: 54 + 7 = 61. ثانياً: الطرح بالشريط العددي وقفزة الضفدع (الصفحة 14) لو أردنا حساب: 54 - 7 - تخيل أنك ضفدع يقف عند الرقم 54، وتريد القفز للخلف 7 خطوات. - أولاً، اقفز قفزة سهلة للخلف بـ 4 خطوات لتصل للرقم المستدير السهل 50. - بقيت لك 3 خطوات لتكمل قفزاتك السبع؛ اقفزها للخلف من الـ 50 لتصل إلى 47. - إذن: 54 - 7 = 47. ثالثاً: الجمع العمودي بالاحتفاظ (الصفحة 28) لو أردنا حساب: 191 + 547 - نرتب الأرقام بدقة: الوحدات تحت الوحدات، العشرات تحت العشرات، والمئات تحت المئات. - نجمع الوحدات: 1 + 7 = 8. - نجمع العشرات: 9 + 4 = 13. لا نكتب 13 كاملة! نكتب 3 ونضع الـ 1 (كضيف) فوق المئات. - نجمع المئات مع الضيف: 1 (الضيف) + 1 + 5 = 7. - النتيجة النهائية هي: 738. """, "questions": [ {"q": "احسب بذهنك مستعملاً التفكيك والإتمام إلى العشرة الموالية: 59 + 3.", "hint": "نأخذ 1 من الـ 3 لنتمم 59 إلى 60، ويبقى 2. النتيجة هي 60 + 2 = 62 (انظر الصفحة 14)."}, {"q": "باستخدام قفزة الضفدع للخلف على الشريط العددي، احسب: 92 - 23.", "hint": "اطرح 20 خطوة لتصل إلى 72، ثم اطرح 3 خطوات لتصل إلى 69. النتيجة هي 69 (انظر الصفحة 20)."}, {"q": "أوجد نتيجة العملية التالية عمودياً مع الانتباه للضيف المحتفظ به: 325 + 12.", "hint": "نرتب الوحدات والعشرات بدقة: 5+2=7 في الوحدات، 2+1=3 في العشرات، و3 في المئات. النتيجة هي 337 (انظر الصفحة 24)."} ] }, { "title": "الدرس الثالث — الهندسة (خريطة الكنز والقطار المستقيم)", "text": """ مرحباً بك يا بطل الهندسة! اليوم سنتعلم درسين رائعين في الفضاء والرسم: أولاً: تحديد المواقع على المرصوفة (خريطة الكنز) (الصفحة 16) المرصوفة هي شبكة مربعات تشبه خريطة الكنز السري. - الأسطر مرقمة بأرقام (1، 2، 3...) والأعمدة معلمة بحروف (أ، ب، ج...). - لتحديد مكان أي حيوان أو كنز، ننظر للعمود والسطر اللذين يلتقي عندهما. - نكتب الموقع هكذا: (العمود ، السطر). - مثلاً: إذا كان "الأسد" يقع في العمود (أ) والسطر (3)، نكتب موقعه: (أ ، 3). ثانياً: الاستقامية ورسم الخطوط (الصفحة 31) - هل تعرف كيف تسير سكة القطار؟ تسير في خط مستقيم تماماً. - لمعرفة هل النقاط مرسومة في استقامية واحدة أم لا، نضع حافة مسطرتنا عليها. - إذا لامست حافة المسطرة جميع النقاط في نفس الوقت، واستطعنا رسم خط مستقيم يجمعها كلها، نقول: "هذه النقط في استقامية واحدة". """, "questions": [ {"q": "إذا كان الغزال يقع في العمود (ج) والسطر (2)، كيف نكتب موقع خانته؟", "hint": "نكتب موقعه داخل قوسين هكذا: (ج ، 2) (انظر الصفحة 16)."}, {"q": "كيف نتأكد من أن النقط المرسومة على كراسك تقع في استقامية واحدة؟", "hint": "نضع حافة المسطرة عليها؛ فإذا لامست حافة المسطرة كل النقط معاً، فهي في استقامية واحدة (انظر الصفحة 31)."}, {"q": "إذا تحرك رائد خطوة واحدة لليمين وخطوة للأعلى من الخانة (ب ، 4)، فما هي خانته الجديدة؟", "hint": "العمود ب لليمين يصبح ج، والسطر 4 للأعلى يصبح 3. الخانة الجديدة هي (ج ، 3) (انظر الصفحة 16)."} ] } , { "title": "الدرس الثالث — مقارنة أطوال (سباق النملات الذكيات)", "text": """ مرحباً بك يا بطل! في هذا الدرس سنتعلم كيف نقارن بين الأطوال [Page 15]. تخيل أن لدينا ثلاث نملات ذكيات يتسابقن للوصول إلى قطعة سكر: - النملة الأولى مشت في طريق أخضر متعرج. - النملة الثانية مشت في طريق بنفسجي منحنٍ. - النملة الثالثة مشت في طريق برتقالي منكسر. كيف نعرف أي طريق هو الأقصر لتفوز النملة بالجائزة؟ 1. المسطرة الصلبة لا تكفي: لأن الطرق ليست مستقيمة تماماً! 2. الحل السحري: نستخدم "خيطاً" أو "شريطاً من الورق المقوى". 3. طريقة القياس: - نضع الخيط فوق الطريق المتعرج بدقة من البداية إلى النهاية. - نقطع الخيط أو نضع علامة عليه عند نهاية الطريق. - نفرد الخيط ليصبح مستقيماً، ثم نقيس طوله بالمسطرة المدرجة! - نفعل نفس الشيء مع بقية الطرق، والطريق الذي يعطينا أقصر خيط هو الطريق الأقصر للفوز. تعلمتُ: لمقارنة أطوال صغيرة أو منحنية، نستعمل خيطاً أو شريطاً من الورق، ثم نقيسه بالمسطرة. """, "questions": [ {"q": "إذا كان لدينا طريقان متعرجان، كيف نقارن بين طوليهما بدون مسطرة مباشرة؟", "hint": "نستخدم خيطاً نضعه فوق كل طريق، ثم نقارن طولي الخيطين لنرى أيهما أطول (انظر الصفحة 15)."}, {"q": "مستعيناً بالمسطرة، رتب المسارات التالية تصاعدياً (من الأقصر للأطول): 1500m، 2km 30m، 1km 25m.", "hint": "نعلم أن 1km = 1000m. إذن: الأول هو 1025m، والثاني 1500m، والثالث 2030m (انظر الصفحة 57)."} ] }, { "title": "الدرس الرابع — التنقل على مرصوفة (رحلة رائد ومواقع الحيوانات)", "text": """ أهلاً بك يا مستكشف! اليوم سنتعلم كيف نتحرك على المرصوفة ونشفر المسارات [Page 16]. المرصوفة هي شبكة مربعات تشبه خريطة الألعاب: - الأعمدة معلمة بحروف من الأسفل: (أ، ب، ج، د، هـ...). - الأسطر مرقمة من الجانب: (1، 2، 3، 4، 5، 6...). أولاً: كيف نجد موقع حيوان على المرصوفة؟ لتحديد مكان "الغزالة" أو "الدب"، نقوم بخطوتين: 1. ننزل بأعيننا عمودياً لنرى الحرف المقابل لها (العمود). 2. نذهب بأعيننا أفقياً نحو الجانب لنرى الرقم المقابل لها (السطر). نكتب الموقع داخل قوسين هكذا: (الحرف ، الرقم). مثال: إذا كان الدب يقع في العمود (أ) والسطر (3)، فإن تشفيره هو: (أ ، 3). ثانياً: كيف نشفر مسار التنقل؟ عندما يتحرك رائد من المدرسة إلى المنزل، فإنه يتبع خطوات ملونة. - نرمز للحركة نحو اليمين بسهم: (→) - نرمز للحركة نحو الأسفل بسهم: (↓) تشفير المسلك هو كتابة تتابع هذه الأسهم خطوة بخطوة حتى نصل للنهاية. """, "questions": [ {"q": "إذا كان الفيل يقع في العمود (ج) والسطر (2)، اكتب تشفير موقع الفيل.", "hint": "التشفير يكتب بين قوسين هكذا: (ج ، 2) (انظر الصفحة 16)."}, {"q": "ما هي الحركات التي نرمز لها بالأسهم (→) و (↓) على المرصوفة؟", "hint": "السهم (→) يعني خطوة لليمين على السطر، والسهم (↓) يعني خطوة للأسفل على العمود (انظر الصفحة 16)."} ] }, { "title": "الدرس الخامس — الأعداد من 0 إلى 999 (سباق الأرقام الكبيرة)", "text": """ مرحباً بك في عالم الأرقام الكبيرة! اليوم سنتعلم كيف نقرأ ونكتب الأعداد التي تتكون من 3 أرقام [Page 17]. عندما نرى عدداً مكتوباً على قميص عداء في السباق، مثل: (435): - الرقم 5 يمثل الوحدات (و). - الرقم 3 يمثل العشرات (ع). - الرقم 4 يمثل المئات (م). أولاً: كيف نقرأ هذا العدد الكبير؟ نبدأ دائماً بقراءة المئات أولاً، ثم نقفز لقراءة الوحدات، وأخيراً نقرأ العشرات: - نقرأ 400: "أربعمائة". - نقرأ 5: "وخمسة". - نقرأ 30: "وثلاثون". فيصبح العدد: "أربعمائة وخمسة وثلاثون". ثانياً: انتبه للأفخاخ! - العدد "سبعمائة وثلاثة" يُكتب بالأرقام: (703)، وليس (730) لأن العشرات صفر، وليس (307)! - العدد الذي فيه 5 وحدات و 0 عشرات و 1 مئات هو: (105). تعلمتُ: أكتب الأعداد بالحروف والكلمات من خلال تركيبها بانتظام ومراعاة قيمة كل رقم (وحدات، عشرات، مئات). """, "questions": [ {"q": "اكتب العدد (ثمانمائة وستة وخمسون) بالأرقام.", "hint": "نكتب 8 في المئات، و6 في الوحدات، و5 في العشرات، فيصبح: 856 (انظر الصفحة 17)."}, {"q": "تخيل عدداً يملك 5 وحدات، و9 عشرات، و9 مئات. ما هو هذا العدد؟ وكيف نقرؤه؟", "hint": "العدد هو 995، ونقرؤه: تسعمائة وخمسة وتسعون (انظر الصفحة 17)."} ] }, { "title": "الدرس السادس — الجمع والطرح: العد بإضافة أو طرح 1 أو 10 أو 100", "text": """ مرحباً بك في لعبة الحساب السريع والأنماط العددية! [Page 18]. سنتعلم اليوم كيف نزيد أو ننقص الأرقام بذكاء كبير وفي ثوانٍ معدودة: أولاً: إضافة أو طرح 1 (لعبة الوحدات) عندما نزيد أو ننقص 1، يتغير فقط رقم "الوحدات" في العدد: - مثال للإضافة: 500 ← 501 ← 502... (تغير الرقم الأخير فقط). - مثال للطرح: 500 ← 499 ← 498... ثانياً: إضافة أو طرح 10 (لعبة العشرات) عندما نزيد أو ننقص 10، يتغير فقط رقم "العشرات" في الوسط: - مثال للطرح: 874 ← 864 ← 854... (لاحظ أن العشرات تنقص بـ 1 في كل خطوة: 7، 6، 5). - مثال للإضافة: 365 + 10 = 375. ثالثاً: إضافة أو طرح 100 (لعبة المئات) عندما نزيد أو ننقص 100، يتغير فقط الرقم الأول من اليسار وهو "المئات": - مثال للإضافة: 3 ← 103 ← 203 ← 303... (المئات تزيد بـ 1 في كل مرة). - مثال للطرح: 904 - 100 = 804. تعلمتُ: الحساب السريع والعد الشفهي بإضافة أو طرح 1 أو 10 أو 100 يسهل عليّ حل العمليات الصعبة في ذهني! """, "questions": [ {"q": "أكمل السلسلة التالية بذكاء: 432 ← 422 ← 412 ← ... ", "hint": "نلاحظ أن مرتبة العشرات تنقص بـ 10 في كل مرة، إذن العدد الموالي هو: 402 (انظر الصفحة 18)."}, {"q": "احسب ذهنياً وبسرعة فائقة: 643 + 100.", "hint": "نضيف 1 إلى منزلة المئات فقط (الرقم 6 يصبح 7)، والنتيجة هي: 743 (انظر الصفحة 18)."}, {"q": "ما هو العدد المناسب للفراغ: ... + 100 = 320؟", "hint": "نطرح 100 من الـ 320 (منزلة المئات تنقص بـ 1)، فيكون العدد هو: 220 (انظر الصفحة 18)."} ] } ] LISTEN_SECONDS = 10 # 900 في بيئة الإنتاج (15 دقيقة) PASS_SCORE = 6 # ── ذاكرة تخزين الصوت ──────────────────────────────────────────────────────── _audio_cache = {} _audio_lock = threading.Lock() def get_lecture_audio(lesson_idx): if lesson_idx in _audio_cache and os.path.exists(_audio_cache[lesson_idx]): return _audio_cache[lesson_idx] with _audio_lock: if lesson_idx in _audio_cache and os.path.exists(_audio_cache[lesson_idx]): return _audio_cache[lesson_idx] try: text = LESSONS[lesson_idx]["text"] tts = gTTS(text=text, lang="ar", slow=False) path = tempfile.mktemp(suffix=".mp3") tts.save(path) _audio_cache[lesson_idx] = path return path except Exception as e: print(f"[law-app] خطأ gTTS: {e}") return None # ── الحالة ──────────────────────────────────────────────────────────────────── def fresh(): return json.dumps({"phase": "idle", "listen_start": None, "question_idx": None, "lesson_idx": 0}) def S(s): return json.loads(s) def D(d): return json.dumps(d) # ── التقييم ─────────────────────────────────────────────────────────────────── def evaluate_answer(question, answer, hint): llm = get_llm() if llm is None: kw = [w.strip().lower() for w in hint.split("،")] hits = sum(1 for k in kw if k in answer.lower()) score = min(10, round((hits / max(len(kw), 1)) * 10)) return score, f"(بدون ذكاء اصطناعي – الكلمات المفتاحية: {hits}/{len(kw)})" prompt = ( "<|im_start|>system\n" "أنت أستاذ قانون جزائري صارم ومتفهم. " "تقيّم إجابات الطلاب وتردّ فقط بصيغة JSON صحيحة.\n" "<|im_end|>\n" "<|im_start|>user\n" f"السؤال: {question}\n" f"إجابة الطالب: {answer}\n" f"العناصر المتوقعة: {hint}\n\n" "قيّم هذه الإجابة من 10 وأعطِ تعليقاً موجزاً باللغة العربية.\n" "أجب فقط بهذا JSON في سطر واحد دون أي إضافة:\n" '{"score": <عدد صحيح 0-10>, "feedback": "<تعليق موجز>"}\n' "<|im_end|>\n" "<|im_start|>assistant\n" ) try: out = llm(prompt, max_tokens=150, temperature=0.1, stop=["<|im_end|>", "\n\n", "###"]) text = out["choices"][0]["text"].strip() print(f"[law-app] رد النموذج: {text}") start = text.find("{") end = text.rfind("}") + 1 if start == -1 or end == 0: raise ValueError("لا يوجد JSON في الرد") data = json.loads(text[start:end]) return max(0, min(10, int(data["score"]))), data.get("feedback", "") except Exception as e: print(f"[law-app] خطأ في التقييم: {e}") kw = [w.strip().lower() for w in hint.split("،")] hits = sum(1 for k in kw if k in answer.lower()) score = min(10, round((hits / max(len(kw), 1)) * 10)) return score, f"(خطأ في النموذج – الكلمات المفتاحية: {hits}/{len(kw)})" # ── المعالجات ───────────────────────────────────────────────────────────────── def start_listening(ss): state = S(ss) lesson_idx = state.get("lesson_idx", 0) lesson = LESSONS[lesson_idx] state.update({"phase": "listen", "listen_start": time.time(), "question_idx": None}) audio = get_lecture_audio(lesson_idx) total = len(LESSONS) msg = ( f"🎧 **{lesson['title']}** ({lesson_idx + 1}/{total})\n\n" "استمع لمدة **15 دقيقة** كاملة، ثم انقر على **🔄 التحقق من الوقت**." ) return ( audio, msg, gr.update(visible=True), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), gr.update(interactive=False, value="⏳ جارٍ الاستماع…"), D(state), ) def check_timer(ss): state = S(ss) if state["phase"] != "listen" or not state["listen_start"]: return gr.update(), "⚠️ ابدأ الاستماع أولاً.", ss rem = max(0, LISTEN_SECONDS - (time.time() - state["listen_start"])) m, s = divmod(int(rem), 60) if rem <= 0: return ( gr.update(interactive=True, value="✅ الانتقال إلى الاختبار"), "✅ انتهى الوقت! انقر على **الانتقال إلى الاختبار**.", ss, ) return ( gr.update(interactive=False, value=f"⏳ تبقّى {m:02d}:{s:02d}"), f"🎧 انتظر **{m} دقيقة و{s:02d} ثانية** إضافية.", ss, ) def open_quiz(ss): state = S(ss) elapsed = time.time() - (state["listen_start"] or 0) if elapsed < LISTEN_SECONDS: m, s = divmod(int(LISTEN_SECONDS - elapsed), 60) return ( gr.update(visible=True), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), f"⚠️ انتظر **{m} دقيقة و{s:02d} ثانية** إضافية.", "", "", ss, ) lesson_idx = state.get("lesson_idx", 0) idx = random.randrange(len(LESSONS[lesson_idx]["questions"])) state.update({"phase": "quiz", "question_idx": idx}) return ( gr.update(visible=False), gr.update(visible=True), gr.update(visible=False), gr.update(visible=False), "", LESSONS[lesson_idx]["questions"][idx]["q"], "", D(state), ) # ── تقديم الإجابة مع تعطيل فوري للزر ──────────────────────────────────────── def submit_answer(answer, ss): state = S(ss) lesson_idx = state.get("lesson_idx", 0) if state["phase"] != "quiz": yield ( gr.update(visible=True), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), "", gr.update(value="📤 إرسال الإجابة", interactive=True), gr.update(interactive=True), ss, ) return # ── الخطوة 1: تعطيل الزر فوراً وعرض رسالة الانتظار ───────────────────── yield ( gr.update(visible=False), gr.update(visible=True), gr.update(visible=False), gr.update(visible=False), "", gr.update(value="⏳ تم إرسال الإجابة — جارٍ التصحيح…", interactive=False), gr.update(interactive=False), ss, ) # ── الخطوة 2: التقييم بالذكاء الاصطناعي ───────────────────────────────── q = LESSONS[lesson_idx]["questions"][state["question_idx"]] score, fb = evaluate_answer(q["q"], answer, q["hint"]) total = len(LESSONS) is_last = (lesson_idx >= total - 1) if score >= PASS_SCORE: state["phase"] = "pass" result = ( f"## 🎉 أحسنت! اجتزت الدرس {lesson_idx + 1} من {total}.\n\n" f"**النتيجة: {score}/10**\n\n**التعليق:** {fb}" ) next_label = "🏆 عرض الشهادة" if is_last else f"➡️ {LESSONS[lesson_idx + 1]['title']}" else: state["phase"] = "fail" result = ( f"## ❌ النتيجة غير كافية: {score}/10\n\n" f"**التعليق:** {fb}\n\n" f"الحد الأدنى المطلوب: **{PASS_SCORE}/10**. " "يُرجى **إعادة الاستماع إلى الدرس** قبل المحاولة مجدداً." ) next_label = "🔄 إعادة الاستماع إلى الدرس" # ── الخطوة 3: عرض النتيجة وإعادة تفعيل الزر ───────────────────────────── yield ( gr.update(visible=False), gr.update(visible=False), gr.update(visible=True), gr.update(visible=False), result, gr.update(value="📤 إرسال الإجابة", interactive=True), gr.update(value="", interactive=True), D(state), ) def handle_next(ss): state = S(ss) lesson_idx = state.get("lesson_idx", 0) total = len(LESSONS) if state["phase"] == "fail": return start_listening(D(state)) if lesson_idx >= total - 1: state["phase"] = "done" return ( None, "", gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), gr.update(visible=True), gr.update(interactive=False, value="✅ انتهى"), D(state), ) else: state["lesson_idx"] = lesson_idx + 1 state["listen_start"] = None return start_listening(D(state)) def restart_from_cert(ss): return start_listening(fresh()) # ── واجهة المستخدم ──────────────────────────────────────────────────────────── CSS = """ footer { display: none !important; } #title { text-align: center; } .gradio-container { direction: rtl; font-family: 'Amiri', 'Noto Sans Arabic', 'Segoe UI', sans-serif; } .gr-button { font-family: 'Amiri', 'Noto Sans Arabic', sans-serif; } """ with gr.Blocks(title="دورة القانون", css=CSS) as demo: session = gr.State(fresh()) gr.Markdown( "# ⚖️ دورة القانون — التكوين القانوني\n" "استمع إلى كل درس · أجب عن السؤال · تقدّم درساً بعد درس", elem_id="title", ) with gr.Column(visible=True) as listen_col: gr.Markdown("## 🎧 الخطوة الأولى — الاستماع إلى الدرس") listen_msg = gr.Markdown("انقر على **ابدأ** للبدء.") audio_out = gr.Audio(label="الدرس الصوتي (باللغة العربية)", autoplay=False, interactive=False) timer_info = gr.Markdown("") with gr.Row(): start_btn = gr.Button("▶️ بدء الاستماع", variant="primary") check_btn = gr.Button("🔄 التحقق من الوقت", variant="secondary") quiz_btn = gr.Button("✅ الانتقال إلى الاختبار", variant="secondary", interactive=False) with gr.Column(visible=False) as quiz_col: gr.Markdown("## 📝 الخطوة الثانية — سؤال التقييم") question_md = gr.Markdown("") answer_box = gr.Textbox(label="إجابتك", placeholder="اكتب إجابتك هنا…", lines=5) submit_btn = gr.Button("📤 إرسال الإجابة", variant="primary") with gr.Column(visible=False) as result_col: gr.Markdown("## 📊 النتيجة") result_md = gr.Markdown("") next_btn = gr.Button("➡️ الدرس التالي", variant="primary") with gr.Column(visible=False) as cert_col: cert_md = gr.Markdown("") restart_btn = gr.Button("🔄 إعادة البدء من الأول", variant="secondary") # ── الربط ───────────────────────────────────────────────────────────────── LISTEN_OUT = [audio_out, listen_msg, listen_col, quiz_col, result_col, cert_col, quiz_btn, session] start_btn.click(start_listening, [session], LISTEN_OUT) check_btn.click(check_timer, [session], [quiz_btn, timer_info, session]) QUIZ_OUT = [listen_col, quiz_col, result_col, cert_col, timer_info, question_md, answer_box, session] quiz_btn.click(open_quiz, [session], QUIZ_OUT) RES_OUT = [listen_col, quiz_col, result_col, cert_col, result_md, submit_btn, answer_box, session] submit_btn.click(submit_answer, [answer_box, session], RES_OUT) answer_box.submit(submit_answer, [answer_box, session], RES_OUT) next_btn.click(handle_next, [session], LISTEN_OUT) restart_btn.click(restart_from_cert, [session], LISTEN_OUT) # ── تحميل النموذج مسبقاً عند الإطلاق ──────────────────────────────────────── threading.Thread(target=get_llm, daemon=True).start() if __name__ == "__main__": demo.launch(server_name="0.0.0.0", server_port=7860, show_error=True)