math / app.py
ramedde's picture
Update app.py
d4aaf07 verified
Raw
History Blame
18.2 kB
"""
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": <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."
# ── 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)