| """ |
| Cours de Droit – Hugging Face Space |
| =================================== |
| Workflow: |
| 1. L'étudiant écoute le cours audio (français, gTTS) pendant 15 minutes. |
| 2. Après 15 min une question aléatoire apparaît (réponse ouverte). |
| 3. Qwen3-0.6B-Q4_K_M (llama-cpp) évalue la réponse de 0 à 10. |
| 4. Score ≥ 6 → réussi. Score < 6 → retour au début, réécoute obligatoire. |
| Multi-utilisateur sécurisé via gr.State (JSON par session). |
| Compatible gradio 5.x + Python 3.13. |
| """ |
|
|
| import os, glob, json, time, tempfile, random, threading |
| import gradio as gr |
| from gtts import gTTS |
|
|
| try: |
| from llama_cpp import Llama |
| _LLAMA_OK = True |
| except ImportError: |
| _LLAMA_OK = False |
|
|
| |
| _llm = None |
| _llm_lock = threading.Lock() |
|
|
| def get_llm(): |
| global _llm |
| if _llm is not None: |
| return _llm |
| if not _LLAMA_OK: |
| return None |
| with _llm_lock: |
| if _llm is not None: |
| return _llm |
| paths = glob.glob("/app/*.gguf") + glob.glob("*.gguf") |
| if not paths: |
| print("[law-app] Aucun fichier .gguf trouvé – évaluation IA désactivée.") |
| return None |
| print(f"[law-app] Chargement du modèle : {paths[0]}") |
| _llm = Llama( |
| model_path=paths[0], |
| n_ctx=1024, |
| n_threads=int(os.getenv("N_THREADS", "2")), |
| n_gpu_layers=int(os.getenv("N_GPU_LAYERS", "-1")), |
| verbose=False, |
| ) |
| print("[law-app] Modèle prêt.") |
| return _llm |
|
|
| |
| LECTURE_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, par ses décisions, contrôle la |
| constitutionnalité des lois. |
| |
| La doctrine regroupe l'ensemble des travaux des juristes : |
| professeurs, avocats, magistrats. Elle propose des analyses, |
| des systématisations et des critiques du droit positif. |
| Sans force obligatoire, elle influence néanmoins la jurisprudence |
| et le législateur. |
| |
| La coutume, source ancienne, est aujourd'hui d'application limitée. |
| Elle consiste en une pratique ancienne, constante et répétée, |
| acceptée comme obligatoire par la population. 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. Selon cette théorie, chaque norme tire |
| sa validité d'une norme supérieure. En France, le Conseil |
| constitutionnel assure le respect de cette hiérarchie en contrôlant |
| la conformité des lois à la Constitution. |
| |
| Le contrôle de légalité permet quant à lui de vérifier que les |
| actes administratifs sont conformes aux lois qui leur sont |
| supérieures. Tout acte contraire à une norme supérieure est |
| susceptible d'être annulé ou écarté. |
| |
| Quatrième partie : Le droit des contrats. |
| |
| Le contrat est un accord de volontés entre deux ou plusieurs |
| personnes destiné à créer, modifier, transmettre ou éteindre |
| des obligations. Le Code civil, dans sa rédaction issue de |
| l'ordonnance du 10 février 2016, définit les conditions de |
| validité du contrat : le consentement des parties, leur capacité |
| à contracter, et un contenu licite et certain. |
| |
| Le consentement doit être libre et éclairé. Il est vicié lorsqu'il |
| a été donné par erreur, obtenu par dol ou arraché par violence. |
| Dans ces cas, le contrat peut être annulé à la demande de la |
| partie dont le consentement a été vicié. |
| |
| La capacité contractuelle est la règle. L'incapacité est |
| l'exception et concerne principalement les mineurs non émancipés |
| et les majeurs protégés sous tutelle ou curatelle. |
| |
| L'exécution du contrat obéit au principe de la force obligatoire : |
| le contrat tient lieu de loi entre les parties qui l'ont conclu. |
| En cas d'inexécution, la partie lésée dispose de plusieurs recours : |
| l'exécution forcée, la résolution du contrat ou des dommages et intérêts. |
| |
| Cinquième partie : La responsabilité civile. |
| |
| La responsabilité civile a pour fonction de réparer le préjudice |
| causé à autrui. Elle se distingue de la responsabilité pénale |
| qui vise à sanctionner l'auteur d'une infraction. |
| |
| La responsabilité délictuelle repose sur trois conditions : |
| une faute, un dommage et un lien de causalité entre les deux. |
| L'article 1240 du Code civil dispose que tout fait quelconque |
| de l'homme qui cause à autrui un dommage oblige son auteur |
| à le réparer. |
| |
| La responsabilité sans faute, dite objective, est reconnue |
| dans certains cas : responsabilité du fait des choses, |
| responsabilité du fait d'autrui, responsabilité des produits |
| défectueux. L'évolution jurisprudentielle et législative tend |
| à élargir ces hypothèses pour mieux protéger les victimes. |
| |
| Conclusion : Le droit est un système dynamique en constante évolution. |
| La maîtrise de ses sources et de ses principes fondamentaux est |
| indispensable à tout juriste. La rigueur dans l'analyse textuelle, |
| combinée à la connaissance de la jurisprudence, permet d'appliquer |
| le droit de manière correcte et juste. |
| """ |
|
|
| 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": "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": "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": "Quels sont les trois vices du consentement qui peuvent entraîner la nullité d'un contrat ?", |
| "hint": "erreur, dol, violence", |
| }, |
| { |
| "q": "Quelles sont les trois conditions nécessaires pour engager la responsabilité délictuelle ?", |
| "hint": "faute, dommage, lien de causalité, article 1240 Code civil", |
| }, |
| { |
| "q": "Expliquez la différence entre la responsabilité civile et la responsabilité pénale.", |
| "hint": "civile répare le préjudice, pénale sanctionne l'auteur d'une infraction", |
| }, |
| { |
| "q": "Quel est le rôle de la jurisprudence dans le système juridique français ?", |
| "hint": "interprète et précise la portée des textes légaux, Cour de cassation, Conseil d'État", |
| }, |
| { |
| "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", |
| }, |
| ] |
|
|
| LISTEN_SECONDS = 15 * 60 |
| PASS_SCORE = 6 |
|
|
| |
| _audio_cache = None |
| _audio_lock = threading.Lock() |
|
|
| def get_lecture_audio(): |
| global _audio_cache |
| if _audio_cache and os.path.exists(_audio_cache): |
| return _audio_cache |
| with _audio_lock: |
| if _audio_cache and os.path.exists(_audio_cache): |
| return _audio_cache |
| try: |
| tts = gTTS(text=LECTURE_TEXT, lang="fr", slow=False) |
| path = tempfile.mktemp(suffix=".mp3") |
| tts.save(path) |
| _audio_cache = 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, |
| }) |
|
|
| def S(s): return json.loads(s) |
| def D(d): return json.dumps(d) |
|
|
| |
| def evaluate_answer(question: str, answer: str, hint: str): |
| llm = get_llm() |
| if llm is None: |
| keywords = [w.strip().lower() for w in hint.split(",")] |
| ans_low = answer.lower() |
| hits = sum(1 for k in keywords if k in ans_low) |
| score = min(10, round((hits / max(len(keywords), 1)) * 10)) |
| return score, f"(Évaluation sans IA – mots-clés trouvés : {hits}/{len(keywords)})" |
|
|
| prompt = ( |
| f"Tu es un professeur de droit strict mais juste.\n" |
| f"Question : {question}\n" |
| f"Réponse de l'étudiant : {answer}\n" |
| f"Éléments attendus : {hint}\n\n" |
| "Évalue la réponse de 0 à 10. Réponds UNIQUEMENT avec un JSON sur une seule ligne, " |
| 'sans texte avant ni après : {"score": <entier 0-10>, "feedback": "<commentaire bref en français>"}' |
| ) |
| try: |
| out = llm(prompt, max_tokens=200, temperature=0.1, stop=["###", "\n\n"]) |
| text = out["choices"][0]["text"].strip() |
| data = json.loads(text[text.find("{"):text.rfind("}")+1]) |
| return max(0, min(10, int(data["score"]))), data.get("feedback", "") |
| except Exception as e: |
| print(f"[law-app] Erreur évaluation : {e}") |
| return 0, "Erreur lors de l'évaluation. Veuillez réessayer." |
|
|
| |
|
|
| def start_listening(ss): |
| state = S(ss) |
| state["phase"] = "listen" |
| state["listen_start"] = time.time() |
| audio = get_lecture_audio() |
| msg = ( |
| "🎧 **Cours en cours d'écoute…**\n\n" |
| "Vous devez écouter pendant **15 minutes** complètes avant d'accéder au quiz.\n\n" |
| "Cliquez sur **🔄 Vérifier le temps** pour voir combien il reste." |
| ) |
| return ( |
| audio, |
| msg, |
| gr.update(visible=True), |
| 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 state["listen_start"] is None: |
| return gr.update(), "⚠️ Lancez d'abord l'écoute.", ss |
| elapsed = time.time() - state["listen_start"] |
| remaining = max(0, LISTEN_SECONDS - elapsed) |
| m, s = divmod(int(remaining), 60) |
| if remaining <= 0: |
| return ( |
| gr.update(interactive=True, value="✅ Accéder au Quiz"), |
| "✅ Durée d'écoute 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), |
| f"⚠️ Patientez encore **{m} min {s:02d} s**.", |
| "", "", ss, |
| ) |
| idx = random.randrange(len(QUESTIONS)) |
| state["phase"] = "quiz" |
| state["question_idx"] = idx |
| return ( |
| gr.update(visible=False), |
| gr.update(visible=True), |
| gr.update(visible=False), |
| "", |
| QUESTIONS[idx]["q"], |
| "", |
| D(state), |
| ) |
|
|
|
|
| def submit_answer(answer, ss): |
| state = S(ss) |
| if state["phase"] != "quiz": |
| return gr.update(visible=True), gr.update(visible=False), "", "", ss |
|
|
| idx = state["question_idx"] |
| q = QUESTIONS[idx] |
| score, fb = evaluate_answer(q["q"], answer, q["hint"]) |
|
|
| if score >= PASS_SCORE: |
| state["phase"] = "pass" |
| result = ( |
| f"## 🎉 Félicitations ! Vous avez réussi.\n\n" |
| f"**Score : {score}/10**\n\n" |
| f"**Commentaire :** {fb}\n\n" |
| f"---\n*Vous maîtrisez bien ce cours de droit civil.*" |
| ) |
| btn = "🔄 Recommencer" |
| else: |
| state["phase"] = "fail" |
| result = ( |
| f"## ❌ Score insuffisant : {score}/10\n\n" |
| f"**Commentaire :** {fb}\n\n" |
| f"---\n" |
| f"Seuil requis : **{PASS_SCORE}/10**.\n\n" |
| f"⬇️ Veuillez **réécouter l'intégralité du cours** (15 minutes) " |
| f"avant de tenter à nouveau le quiz." |
| ) |
| btn = "🔄 Réécouter le cours" |
|
|
| return ( |
| gr.update(visible=False), |
| gr.update(visible=True), |
| result, |
| btn, |
| D(state), |
| ) |
|
|
|
|
| def restart(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 le cours en français · Répondez à une question · Obtenez une note sur 10", |
| elem_id="title", |
| ) |
|
|
| |
| with gr.Group(visible=True) as listen_group: |
| gr.Markdown("## 🎧 Étape 1 — Écoute du cours (15 minutes)") |
| listen_msg = gr.Markdown("Cliquez sur **Démarrer l'écoute** 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", 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.Group(visible=False) as quiz_group: |
| gr.Markdown("## 📝 Étape 2 — Question d'évaluation") |
| question_md = gr.Markdown("") |
| answer_box = gr.Textbox( |
| label="Votre réponse", |
| placeholder="Rédigez votre réponse ici…", |
| lines=5, |
| ) |
| submit_btn = gr.Button("📤 Soumettre la réponse", variant="primary") |
|
|
| |
| with gr.Group(visible=False) as result_group: |
| gr.Markdown("## 📊 Résultat") |
| result_md = gr.Markdown("") |
| restart_btn = gr.Button("🔄 Recommencer", variant="secondary") |
|
|
| |
| START_OUT = [audio_out, listen_msg, listen_group, quiz_group, result_group, quiz_btn, session] |
|
|
| start_btn.click(start_listening, [session], START_OUT) |
| check_btn.click(check_timer, [session], [quiz_btn, timer_info, session]) |
|
|
| QUIZ_OUT = [listen_group, quiz_group, result_group, timer_info, question_md, answer_box, session] |
| quiz_btn.click(open_quiz, [session], QUIZ_OUT) |
|
|
| RESULT_OUT = [quiz_group, result_group, result_md, restart_btn, session] |
| submit_btn.click(submit_answer, [answer_box, session], RESULT_OUT) |
| answer_box.submit(submit_answer, [answer_box, session], RESULT_OUT) |
|
|
| restart_btn.click(restart, [session], START_OUT) |
|
|
| if __name__ == "__main__": |
| demo.launch(server_name="0.0.0.0", server_port=7860) |