| """ |
| Cours de Droit – Hugging Face Space |
| Compatible avec le Docker HF (gradio 4.44.1 bundled + Python 3.13). |
| Téléchargement automatique du modèle au démarrage. |
| """ |
| 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] Modèle déjà présent.") |
| return model_path |
| print("[law-app] Téléchargement du modèle en cours...") |
| 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] Téléchargement terminé.") |
| return model_path |
| except Exception as e: |
| print(f"[law-app] Erreur téléchargement : {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("[spelling-app] No .gguf file found – AI tips disabled.") |
| return None |
| model_path = gguf_files[0] |
| print(f"[spelling-app] Loading model: {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("[spelling-app] Model ready.") |
| return _llm |
|
|
| |
| LESSONS = [ |
| { |
| "title": "Leçon 1 — Sources du droit et hiérarchie des normes", |
| "text": """ |
| Cours de droit civil. Introduction aux sources du droit français. |
| Première partie : Les sources formelles du droit. |
| Le droit français repose sur plusieurs sources hiérarchisées. |
| Au sommet se trouve la Constitution du 4 octobre 1958, loi fondamentale |
| de la République française. Elle établit les règles d'organisation de |
| l'État et garantit les droits fondamentaux des citoyens. |
| Sous la Constitution, on trouve les traités internationaux et le droit |
| de l'Union européenne. En vertu de l'article 55 de la Constitution, |
| les traités régulièrement ratifiés ont une autorité supérieure à celle |
| des lois ordinaires. |
| Vient ensuite la loi, votée par le Parlement. La loi ordinaire est |
| adoptée par l'Assemblée nationale et le Sénat selon la procédure |
| législative prévue par la Constitution. Les lois organiques, |
| quant à elles, complètent et précisent certaines dispositions |
| constitutionnelles et bénéficient d'un statut intermédiaire. |
| Le règlement constitue la quatrième source majeure. Il émane du |
| pouvoir exécutif : décrets du Président de la République et du |
| Premier ministre, arrêtés ministériels, préfectoraux et municipaux. |
| L'article 37 de la Constitution confère au gouvernement un domaine |
| réglementaire autonome. |
| Deuxième partie : Les sources matérielles du droit. |
| La jurisprudence, bien qu'elle ne soit pas officiellement reconnue |
| comme source formelle, joue un rôle fondamental dans le système |
| juridique français. Les décisions de la Cour de cassation et du |
| Conseil d'État interprètent et précisent la portée des textes légaux. |
| Le Conseil constitutionnel contrôle la constitutionnalité des lois. |
| La doctrine regroupe l'ensemble des travaux des juristes : |
| professeurs, avocats, magistrats. Sans force obligatoire, elle influence |
| néanmoins la jurisprudence et le législateur. |
| La coutume, source ancienne, est aujourd'hui d'application limitée. |
| En droit commercial, les usages professionnels conservent une importance certaine. |
| Troisième partie : Le principe de hiérarchie des normes. |
| Hans Kelsen, juriste autrichien du vingtième siècle, a théorisé |
| la pyramide des normes. Chaque norme tire sa validité d'une norme supérieure. |
| En France, le Conseil constitutionnel assure le respect de cette hiérarchie. |
| """, |
| "questions": [ |
| {"q": "Quelle est la source au sommet de la hiérarchie des normes en droit français, et quelle est sa date ?", |
| "hint": "Constitution, 4 octobre 1958, loi fondamentale"}, |
| {"q": "Qu'est-ce que la pyramide des normes et qui en est le théoricien ?", |
| "hint": "Hans Kelsen, hiérarchie des normes, validité tirée d'une norme supérieure"}, |
| {"q": "Quel est le rôle de la jurisprudence dans le système juridique français ?", |
| "hint": "interprète et précise les textes légaux, Cour de cassation, Conseil d'État"}, |
| {"q": "Quelle différence y a-t-il entre une loi ordinaire et une loi organique ?", |
| "hint": "loi ordinaire votée par le Parlement, loi organique complète la Constitution, statut intermédiaire"}, |
| ], |
| }, |
| { |
| "title": "Leçon 2 — Le droit des contrats", |
| "text": """ |
| Cours de droit civil. Le droit des contrats. |
| Première partie : Définition et conditions de validité. |
| Le contrat est un accord de volontés destiné à créer, modifier, transmettre |
| ou éteindre des obligations. Le Code civil, issu de l'ordonnance du 10 février 2016, |
| définit trois conditions de validité : le consentement des parties, leur capacité |
| à contracter, et un contenu licite et certain. |
| Le consentement doit être libre et éclairé. Il est vicié par l'erreur, le dol |
| ou la violence. L'incapacité concerne les mineurs non émancipés et les majeurs |
| protégés sous tutelle ou curatelle. |
| Deuxième partie : Les effets du contrat. |
| L'exécution obéit au principe de la force obligatoire : le contrat tient lieu |
| de loi entre les parties. En cas d'inexécution : exécution forcée, résolution |
| ou dommages et intérêts. |
| L'effet relatif du contrat signifie que le contrat ne crée d'obligations qu'entre |
| les parties contractantes. Les tiers ne peuvent en principe ni se prévaloir du contrat, |
| ni se le voir opposer. |
| Troisième partie : La formation du contrat. |
| La formation du contrat passe par une offre et une acceptation. L'offre doit être |
| précise et ferme. L'acceptation doit être pure et simple. Toute contre-proposition |
| constitue une nouvelle offre. |
| Les avant-contrats tels que la promesse unilatérale de vente ou le pacte de |
| préférence permettent d'organiser la phase précontractuelle et d'encadrer |
| les négociations entre les parties. |
| """, |
| "questions": [ |
| {"q": "Quelles sont les trois conditions de validité d'un contrat selon le Code civil réformé en 2016 ?", |
| "hint": "consentement, capacité, contenu licite et certain"}, |
| {"q": "Quels sont les trois vices du consentement qui peuvent entraîner la nullité d'un contrat ?", |
| "hint": "erreur, dol, violence"}, |
| {"q": "Quelles personnes sont frappées d'incapacité contractuelle en droit français ?", |
| "hint": "mineurs non émancipés, majeurs protégés sous tutelle ou curatelle"}, |
| {"q": "Qu'est-ce que le principe de l'effet relatif du contrat ?", |
| "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"}, |
| ], |
| }, |
| { |
| "title": "Leçon 3 — La responsabilité civile", |
| "text": """ |
| Cours de droit civil. La responsabilité civile. |
| Première partie : Distinction responsabilité civile et pénale. |
| La responsabilité civile répare le préjudice causé à autrui, contrairement |
| à la responsabilité pénale qui sanctionne l'auteur d'une infraction. |
| La responsabilité civile vise à remettre la victime dans l'état où elle |
| se trouvait avant le dommage, par l'octroi de dommages et intérêts. |
| Deuxième partie : La responsabilité délictuelle. |
| La responsabilité délictuelle repose sur trois conditions : faute, dommage |
| et lien de causalité. L'article 1240 du Code civil dispose que tout fait |
| de l'homme causant un dommage oblige son auteur à le réparer. |
| La faute peut être intentionnelle ou non intentionnelle. Le dommage doit être |
| certain, direct et personnel. Le lien de causalité entre la faute et le dommage |
| doit être prouvé par la victime. |
| Troisième partie : La responsabilité sans faute. |
| La responsabilité sans faute couvre trois domaines principaux. |
| Le fait des choses : on est responsable des dommages causés par les choses |
| dont on a la garde. Le fait d'autrui : les parents sont responsables des dommages |
| causés par leurs enfants mineurs ; les commettants répondent des fautes de leurs préposés. |
| Les produits défectueux : le producteur est responsable du dommage causé |
| par un défaut de son produit, sans que la victime ait à prouver une faute. |
| Quatrième partie : La responsabilité contractuelle. |
| Lorsque le dommage résulte de l'inexécution d'un contrat, la responsabilité |
| contractuelle s'applique. Elle suppose un contrat valable, une inexécution |
| et un préjudice. Le débiteur peut s'exonérer en prouvant la force majeure, |
| c'est-à-dire un événement imprévisible, irrésistible et extérieur. |
| """, |
| "questions": [ |
| {"q": "Quelle est la différence entre responsabilité civile et responsabilité pénale ?", |
| "hint": "civile répare le préjudice, pénale sanctionne l'auteur d'une infraction"}, |
| {"q": "Quelles sont les trois conditions de la responsabilité délictuelle ?", |
| "hint": "faute, dommage, lien de causalité, article 1240 Code civil"}, |
| {"q": "Quels sont les trois domaines de la responsabilité sans faute ?", |
| "hint": "fait des choses, fait d'autrui, produits défectueux"}, |
| {"q": "Qu'est-ce que la force majeure en matière de responsabilité contractuelle ?", |
| "hint": "événement imprévisible, irrésistible et extérieur, exonère le débiteur"}, |
| ], |
| }, |
| ] |
|
|
| LISTEN_SECONDS = 10 |
| 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="fr", 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 erreur : {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"(Sans IA – mots-clés : {hits}/{len(kw)})" |
|
|
| prompt = ( |
| "<|im_start|>system\n" |
| "Tu es un professeur de droit français strict et bienveillant. " |
| "Tu évalues les réponses des étudiants et réponds UNIQUEMENT en JSON valide.\n" |
| "<|im_end|>\n" |
| "<|im_start|>user\n" |
| f"Question : {question}\n" |
| f"Réponse de l'étudiant : {answer}\n" |
| f"Éléments attendus : {hint}\n\n" |
| "Évalue cette réponse sur 10 et donne un commentaire bref en français.\n" |
| "Réponds UNIQUEMENT avec ce JSON sur une seule ligne, sans rien d'autre :\n" |
| '{"score": <entier 0-10>, "feedback": "<commentaire bref>"}\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] Réponse modèle : {text}") |
| start = text.find("{") |
| end = text.rfind("}") + 1 |
| if start == -1 or end == 0: |
| raise ValueError("Pas de JSON dans la réponse") |
| 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] Erreur éval : {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"(Erreur modèle – mots-clés : {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" |
| "Écoutez pendant **15 minutes** complètes, puis cliquez sur **🔄 Vérifier le temps**." |
| ) |
| return ( |
| audio, msg, |
| gr.update(visible=True), |
| gr.update(visible=False), |
| gr.update(visible=False), |
| gr.update(visible=False), |
| gr.update(interactive=False, value="⏳ Écoute en cours…"), |
| D(state), |
| ) |
|
|
| def check_timer(ss): |
| state = S(ss) |
| if state["phase"] != "listen" or not state["listen_start"]: |
| return gr.update(), "⚠️ Lancez d'abord l'écoute.", 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="✅ Accéder au Quiz"), |
| "✅ Durée atteinte ! Cliquez sur **Accéder au Quiz**.", |
| ss, |
| ) |
| return ( |
| gr.update(interactive=False, value=f"⏳ Encore {m:02d}:{s:02d}"), |
| f"🎧 Patientez encore **{m} min {s:02d} s**.", |
| 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"⚠️ Patientez encore **{m} min {s:02d} s**.", "", "", 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="📤 Soumettre", interactive=True), |
| gr.update(interactive=True), |
| ss, |
| ) |
| return |
|
|
| |
| yield ( |
| gr.update(visible=False), gr.update(visible=True), |
| gr.update(visible=False), gr.update(visible=False), |
| "", |
| gr.update(value="⏳ Réponse soumise — Correction en cours…", interactive=False), |
| gr.update(interactive=False), |
| ss, |
| ) |
|
|
| |
| 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"## 🎉 Bravo ! Leçon {lesson_idx + 1}/{total} réussie.\n\n" |
| f"**Score : {score}/10**\n\n**Commentaire :** {fb}" |
| ) |
| next_label = "🏆 Voir mon certificat" if is_last else f"➡️ {LESSONS[lesson_idx + 1]['title']}" |
| else: |
| state["phase"] = "fail" |
| result = ( |
| f"## ❌ Score insuffisant : {score}/10\n\n" |
| f"**Commentaire :** {fb}\n\n" |
| f"Seuil requis : **{PASS_SCORE}/10**. " |
| "Veuillez **réécouter la leçon** avant de retenter." |
| ) |
| next_label = "🔄 Réécouter la leçon" |
|
|
| |
| yield ( |
| gr.update(visible=False), gr.update(visible=False), |
| gr.update(visible=True), gr.update(visible=False), |
| result, |
| gr.update(value="📤 Soumettre", 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" |
| cert = ( |
| "# 🏆 Félicitations !\n\n" |
| "Vous avez complété avec succès les **3 leçons** du cours de droit civil :\n\n" |
| "✅ Leçon 1 — Sources du droit et hiérarchie des normes\n\n" |
| "✅ Leçon 2 — Le droit des contrats\n\n" |
| "✅ Leçon 3 — La responsabilité civile\n\n" |
| "---\n\n" |
| "*Ce certificat atteste de votre participation et de votre réussite " |
| "aux évaluations du cours de droit civil.*" |
| ) |
| return ( |
| None, "", |
| gr.update(visible=False), |
| gr.update(visible=False), |
| gr.update(visible=False), |
| gr.update(visible=True), |
| gr.update(interactive=False, value="✅ Terminé"), |
| 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;}" |
|
|
| with gr.Blocks(title="Cours de Droit", css=CSS) as demo: |
| session = gr.State(fresh()) |
|
|
| gr.Markdown( |
| "# ⚖️ Cours de Droit — Formation Juridique\n" |
| "Écoutez chaque leçon · Répondez à la question · Progressez leçon par leçon", |
| elem_id="title", |
| ) |
|
|
| with gr.Column(visible=True) as listen_col: |
| gr.Markdown("## 🎧 Étape 1 — Écoute de la leçon") |
| listen_msg = gr.Markdown("Cliquez sur **Démarrer** pour commencer.") |
| audio_out = gr.Audio(label="Cours audio (français)", autoplay=False, interactive=False) |
| timer_info = gr.Markdown("") |
| with gr.Row(): |
| start_btn = gr.Button("▶️ Démarrer l'écoute" + str(_LLAMA_OK), variant="primary") |
| check_btn = gr.Button("🔄 Vérifier le temps", variant="secondary") |
| quiz_btn = gr.Button("✅ Accéder au Quiz", variant="secondary", interactive=False) |
|
|
| with gr.Column(visible=False) as quiz_col: |
| gr.Markdown("## 📝 Étape 2 — Question d'évaluation") |
| question_md = gr.Markdown("") |
| answer_box = gr.Textbox(label="Votre réponse", placeholder="Rédigez ici…", lines=5) |
| submit_btn = gr.Button("📤 Soumettre", variant="primary") |
|
|
| with gr.Column(visible=False) as result_col: |
| gr.Markdown("## 📊 Résultat") |
| result_md = gr.Markdown("") |
| next_btn = gr.Button("➡️ Leçon suivante", variant="primary") |
|
|
| with gr.Column(visible=False) as cert_col: |
| cert_md = gr.Markdown("") |
| restart_btn = gr.Button("🔄 Recommencer depuis le début", 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) |