# -*- coding: utf-8 -*- """ Génère un texte de test via l'API Mistral, en limitant le vocabulaire aux concepts présents dans l'ontologie EduPicto.owx. Objectif : produire un texte simple (aide à la lecture, enfants autistes) qui - n'emploie que des concepts pictogrammables (présents dans l'ontologie) - introduit un personnage nommé puis le reprend par des pronoms (il/elle/ils) → exerce la résolution de coréférence du serveur. CLÉ API (une des deux) : export MISTRAL_API_KEY="..." (variable d'environnement) ou bien fichier ~/.mistral_key (contenant juste la clé) USAGE : python generer_texte.py # thème libre python generer_texte.py "une recette simple" # thème imposé python generer_texte.py --envoyer # génère puis envoie au serveur 5006 """ import os import re import sys import json import requests OWX = "EduPicto.owx" MISTRAL_URL = "https://api.mistral.ai/v1/chat/completions" MODELE = "ministral-14b-2512" # Concepts techniques à exclure de la palette (pas des mots affichables) EXCLURE_PREFIXES = ("contexte_",) EXCLURE_SUFFIXES = ("_demo", "_metier") EXCLURE_CLASSES = {"Univers_Cuisine", "Univers_Justice"} # Étiquettes lisibles par classe d'ontologie LABELS_CLASSES = { "Personne": "Personnages", "Action": "Actions (verbes)", "Aliment": "Aliments", "Lieu": "Lieux", "Objet_Ustensile": "Objets / ustensiles", "Animal": "Animaux", "Vêtement": "Vêtements", "Adjectif": "Adjectifs", "Temps": "Moments", "Durée": "Durées", "Couleur": "Couleurs", "Adverbe": "Adverbes", "Métier": "Métiers", "Evenements": "Événements", "Famille": "Famille", "Forme": "Formes", "Bijoux": "Bijoux", "Appareil_Chauffage": "Chauffage", "Planète": "Planètes", "Matériel": "Matériaux", } def charger_concepts(chemin=OWX): """Extrait {individu: classe} depuis les ClassAssertion de l'ontologie.""" with open(chemin, "r", encoding="utf-8") as f: xml = f.read() assertions = re.findall( r'\s*\s*' r'\s*', xml, ) concepts = {} for cls, ind in assertions: if cls in EXCLURE_CLASSES: continue if ind.startswith(EXCLURE_PREFIXES) or ind.endswith(EXCLURE_SUFFIXES): continue concepts[ind] = cls return concepts def lisible(nom): """avocat_metier -> 'avocat', arrière_grand_mère -> 'arrière-grand-mère'.""" return nom.replace("_", " ").strip() def palette_par_classe(concepts): """Regroupe les concepts par classe, en libellés lisibles.""" groupes = {} for ind, cls in concepts.items(): groupes.setdefault(cls, []).append(lisible(ind)) for cls in groupes: groupes[cls] = sorted(set(groupes[cls])) return groupes def construire_prompt(concepts, theme=None): groupes = palette_par_classe(concepts) # Ordre d'affichage : personnages et actions d'abord (utiles à la coréf) ordre = ["Personne", "Action", "Aliment", "Lieu", "Objet_Ustensile", "Animal", "Vêtement", "Adjectif", "Temps", "Durée", "Couleur", "Adverbe", "Métier", "Evenements", "Famille", "Forme", "Bijoux", "Appareil_Chauffage", "Planète", "Matériel"] lignes = [] for cls in ordre: if cls in groupes: label = LABELS_CLASSES.get(cls, cls) lignes.append(f"- {label} : {', '.join(groupes[cls])}") palette = "\n".join(lignes) theme_txt = f"\nThème souhaité : {theme}." if theme else "" system = ( "Tu écris des textes très simples pour aider des enfants à lire avec des " "pictogrammes. Phrases courtes, une idée par phrase. " "RÈGLES DE FORME STRICTES : " "(1) Conjugue les verbes au PRÉSENT de l'indicatif (ex: « elle prépare », " "JAMAIS « elle préparer »). " "(2) AUCUN formatage : pas de gras, pas d'astérisques (*), pas de listes, " "pas d'emoji. " "(3) AUCUN commentaire, note, parenthèse explicative ni texte entre crochets. " "Réponds UNIQUEMENT par le texte de l'histoire." ) user = f"""Écris un court texte (6 à 10 phrases) en français.{theme_txt} CONTRAINTE STRICTE DE VOCABULAIRE (règle absolue) : Chaque nom, verbe et adjectif que tu écris DOIT figurer dans la palette ci-dessous. - Tu peux conjuguer un verbe et accorder un nom/adjectif, mais le mot de base doit être dans la liste. - INTERDIT d'employer un synonyme ou un mot proche s'il n'est pas listé. Si le concept que tu veux n'existe pas, CHOISIS-EN un autre DANS la liste. - Exemples d'erreurs à NE PAS commettre : écrire "poire" alors que seul "pomme" est listé ; écrire "enfourner" alors que seuls "placer" et "four" sont listés. - Seuls les petits mots de liaison sont libres (le, la, un, des, dans, et, avec, pour, il, elle, ils, elles, son, sa, ses...). - CONJUGUE les verbes au présent (« elle coupe », pas « elle couper »). - AUCUN astérisque, AUCUN gras, AUCUNE note ni explication entre parenthèses/crochets. POUR TESTER LA CORÉFÉRENCE : - Choisis UN seul personnage de la liste, nomme-le au début. - Ensuite reprends-le plusieurs fois par un pronom SINGULIER (il ou elle). - N'utilise "ils"/"elles" QUE si tu as nommé explicitement un groupe ou deux personnes avant (sinon évite le pluriel). PALETTE DE CONCEPTS AUTORISÉS : {palette} AVANT DE RÉPONDRE : relis ton texte et remplace tout nom/verbe/adjectif qui ne serait pas dans la palette par un mot de la palette. Donne uniquement le texte final, sans titre ni explication.""" return system, user def lire_cle(): cle = os.environ.get("MISTRAL_API_KEY") if cle: return cle.strip() chemin = os.path.expanduser("~/.mistral_key") if os.path.exists(chemin): with open(chemin) as f: return f.read().strip() sys.exit( "ERREUR: clé Mistral introuvable.\n" " export MISTRAL_API_KEY=\"...\" ou echo 'sk-...' > ~/.mistral_key" ) def generer(theme=None): concepts = charger_concepts() system, user = construire_prompt(concepts, theme) cle = lire_cle() resp = requests.post( MISTRAL_URL, headers={"Authorization": f"Bearer {cle}", "Content-Type": "application/json"}, json={ "model": MODELE, "temperature": 0.2, "messages": [ {"role": "system", "content": system}, {"role": "user", "content": user}, ], }, timeout=60, ) resp.raise_for_status() texte = resp.json()["choices"][0]["message"]["content"].strip() return nettoyer_sortie(texte) def nettoyer_sortie(texte): """Filet de sécurité : retire le formatage et les notes que le modèle ajoute parfois (gras **, emoji, note finale entre crochets ou après ---).""" # Couper toute note/commentaire en fin de réponse for sep in ["\n---", "\n*[", "\n[Note", "\n(Note", "\nNote :"]: idx = texte.find(sep) if idx != -1: texte = texte[:idx] # Retirer le gras/italique markdown texte = texte.replace("**", "").replace("__", "").replace("*", "") return texte.strip() def envoyer_au_serveur(texte, url="http://localhost:5006/pictogramiser"): r = requests.post(url, json={"texte": texte}, timeout=120) r.raise_for_status() flux = r.json()["flux_de_lecture"] print("\n=== FLUX PICTOS (mot -> id, type) ===") for el in flux: mot = (el.get("mot") or "").strip() if mot: print(f" {mot:20s} -> {el.get('picto')} ({el.get('type')})") if __name__ == "__main__": args = [a for a in sys.argv[1:] if a != "--envoyer"] envoyer = "--envoyer" in sys.argv theme = args[0] if args else None texte = generer(theme) print("=== TEXTE GÉNÉRÉ (Mistral, vocabulaire = ontologie) ===\n") print(texte) if envoyer: envoyer_au_serveur(texte)