""" 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 singleton ───────────────────────────────────────────────────────────── _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 # ── Contenu du cours (droit civil français) ─────────────────────────────────── 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 # 15 minutes PASS_SCORE = 6 # ── Audio cache (partagé entre sessions) ───────────────────────────────────── _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 # ── État de session ──────────────────────────────────────────────────────────── 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) # ── Évaluation Qwen3 ────────────────────────────────────────────────────────── 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": , "feedback": ""}' ) 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." # ── Handlers ────────────────────────────────────────────────────────────────── 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), # listen_group gr.update(visible=False), # quiz_group gr.update(visible=False), # result_group 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()) # ── Interface ───────────────────────────────────────────────────────────────── 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", ) # ── Phase écoute ───────────────────────────────────────────────────────── 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) # ── Phase quiz ─────────────────────────────────────────────────────────── 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") # ── Phase résultat ─────────────────────────────────────────────────────── with gr.Group(visible=False) as result_group: gr.Markdown("## 📊 Résultat") result_md = gr.Markdown("") restart_btn = gr.Button("🔄 Recommencer", variant="secondary") # ── Câblage ─────────────────────────────────────────────────────────────── 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)