#!/usr/bin/env python3 import base64 import json import shutil import subprocess import time import urllib.request from pathlib import Path ROOT = Path(__file__).resolve().parent DUEL = ROOT.parents[1] EPREUVE = (DUEL / "EPREUVE-FIGEE.md").read_text(encoding="utf-8") OUT = ROOT / "productions" PROMPTS = ROOT / "prompts-resolus" PROOFS = ROOT / "preuves" FINAL = ROOT / "final" for directory in (OUT, PROMPTS, PROOFS, FINAL): directory.mkdir(parents=True, exist_ok=True) OLLAMA_API = "[adresse locale Ollama retiree]" COMFY_API = "[adresse locale ComfyUI retiree]" COMFY_ROOT = Path("[chemin local ComfyUI retire]") CHECKPOINT = "sdxl_lightning_4step.safetensors" SEED = 81120261351 records = [] def chat(model, role, system, prompt, output_name, images=None, temperature=0.25): body = { "model": model, "stream": False, "messages": [{"role": "system", "content": system}, {"role": "user", "content": prompt}], "options": {"temperature": temperature}, } if images: body["messages"][1]["images"] = images prompt_path = PROMPTS / f"{output_name}.txt" prompt_path.write_text(system + "\n\n" + prompt, encoding="utf-8") started = time.time() request = urllib.request.Request( OLLAMA_API, data=json.dumps(body, ensure_ascii=False).encode("utf-8"), headers={"Content-Type": "application/json"}, ) with urllib.request.urlopen(request, timeout=1200) as response: data = json.load(response) text = data["message"]["content"].strip() + "\n" (OUT / f"{output_name}.md").write_text(text, encoding="utf-8") records.append({ "family": "Petite Eve locale", "model": model, "role": role, "seconds": round(time.time() - started, 3), "output": f"productions/{output_name}.md", "chars": len(text), }) return text invariants = chat( "qwen2.5:7b", "invariants", "Tu extrais des contraintes visuelles. Réponse courte et vérifiable, sans inventer de nouvelles scènes.", "Transforme l'épreuve suivante en exactement 12 invariants PASS/FAIL pour une planche muette.\n\n" + EPREUVE, "01-invariants", ) storyboard = chat( "gemma3:12b", "storyboard et prompt", "Tu es une directrice artistique locale. Tu dois préserver les six moments et la continuité.", "Écris un storyboard compact case par case puis un prompt SDXL unique pour générer toute la planche 3x2. Aucun texte ne doit apparaître dans l'image.\n\nÉPREUVE:\n" + EPREUVE + "\n\nINVARIANTS:\n" + invariants, "02-storyboard", ) review = chat( "qwen3:14b", "revue de continuité", "Tu es une réviseuse locale stricte. Ne réécris pas toute l'histoire : détecte les ambiguïtés et impose des corrections actionnables.", "Liste les risques du prompt puis donne exactement 10 corrections impératives pour maximiser: grille 3x2, continuité, histoire sans texte.\n\nÉPREUVE:\n" + EPREUVE + "\n\nPROPOSITION:\n" + storyboard, "03-revue", ) technical = chat( "qwen2.5-coder:14b", "paramètres ComfyUI", "Tu es une ingénieure ComfyUI locale. Réponds seulement avec des paramètres compatibles avec SDXL-Lightning 4-step.", "Propose les paramètres d'un workflow txt2img unique à 1536x1024 utilisant sdxl_lightning_4step.safetensors. Rubriques exactes: POSITIVE_ADDENDUM, NEGATIVE, STEPS, CFG, SAMPLER, SCHEDULER, SEED. N'ajoute aucun nœud absent du workflow standard.\n\n" + review, "04-technique", ) final_prompt = chat( "phi4:14b", "synthèse finale", "Tu synthétises un prompt SDXL final. Produis uniquement le prompt positif en anglais, sans commentaire, sans Markdown, 700 mots maximum.", "Fusionne l'épreuve, le storyboard, la revue et les paramètres ci-dessous. Exige exactement six cases en grille 3x2, zéro texte, et la même femme adulte athlétique à poitrine généreuse dans toutes ses apparitions. Décris explicitement les six cases dans l'ordre.\n\nÉPREUVE:\n" + EPREUVE + "\n\nSTORYBOARD:\n" + storyboard + "\n\nREVUE:\n" + review + "\n\nTECHNIQUE:\n" + technical, "05-prompt-final", temperature=0.15, ) negative = ( "text, title, letters, numbers, captions, speech bubbles, watermark, signature, logo, extra panels, " "missing panels, merged panels, inset panels, duplicate woman, duplicate crow, inconsistent character, " "changing clothes, changing hairstyle, child, nudity, erotic pose, extra limbs, malformed hands, blurry" ) workflow = { "1": {"class_type": "CheckpointLoaderSimple", "inputs": {"ckpt_name": CHECKPOINT}}, "2": {"class_type": "CLIPTextEncode", "inputs": {"clip": ["1", 1], "text": final_prompt}}, "3": {"class_type": "CLIPTextEncode", "inputs": {"clip": ["1", 1], "text": negative}}, "4": {"class_type": "EmptyLatentImage", "inputs": {"width": 1536, "height": 1024, "batch_size": 1}}, "5": {"class_type": "KSampler", "inputs": { "model": ["1", 0], "positive": ["2", 0], "negative": ["3", 0], "latent_image": ["4", 0], "seed": SEED, "steps": 4, "cfg": 1.0, "sampler_name": "euler", "scheduler": "sgm_uniform", "denoise": 1.0, }}, "6": {"class_type": "VAEDecode", "inputs": {"samples": ["5", 0], "vae": ["1", 2]}}, "7": {"class_type": "SaveImage", "inputs": {"images": ["6", 0], "filename_prefix": "diamant-trop-lourd/essai-1/planche"}}, } (PROOFS / "workflow.json").write_text(json.dumps(workflow, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") started = time.time() request = urllib.request.Request( COMFY_API + "/prompt", data=json.dumps({"prompt": workflow, "client_id": "[identifiant technique retire]"}).encode("utf-8"), headers={"Content-Type": "application/json"}, ) with urllib.request.urlopen(request, timeout=60) as response: queued = json.load(response) prompt_id = queued["prompt_id"] deadline = time.time() + 1200 record = None while time.time() < deadline: with urllib.request.urlopen(COMFY_API + f"/history/{prompt_id}", timeout=30) as response: history = json.load(response) if prompt_id in history: record = history[prompt_id] break time.sleep(2) if record is None: raise SystemExit("Délai ComfyUI dépassé") images = record.get("outputs", {}).get("7", {}).get("images", []) if not images: raise SystemExit("ComfyUI terminé sans image") image_info = images[0] source = COMFY_ROOT / "output" / image_info.get("subfolder", "") / image_info["filename"] destination = FINAL / "planche-minions-essai-1.png" shutil.copy2(source, destination) records.append({ "family": "ComfyUI local", "version": "v0.31.0-9-g2a68ce33", "checkpoint": CHECKPOINT, "seed": SEED, "steps": 4, "cfg": 1.0, "sampler": "euler", "scheduler": "sgm_uniform", "seconds": round(time.time() - started, 3), "output": "final/planche-minions-essai-1.png", "prompt_id": prompt_id, }) (PROOFS / "comfyui-history.json").write_text(json.dumps(record, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") encoded = base64.b64encode(destination.read_bytes()).decode("ascii") vision = chat( "gemma3:12b", "audit visuel", "Tu es une inspectrice visuelle locale. Décris uniquement ce qui est réellement visible; n'accorde aucun point par intention.", "Inspecte cette planche contre l'épreuve. Réponds avec: NOMBRE DE CASES, TEXTE VISIBLE, DESCRIPTION CASE PAR CASE, CONTINUITÉ, CRITÈRES ÉLIMINATOIRES, NOTE PROPOSÉE /100, DÉFAUTS POUR ESSAI 2.\n\n" + EPREUVE, "06-audit-vision", images=[encoded], temperature=0.1, ) judge = chat( "gpt-oss:20b", "verdict local", "Tu es le juge local. Tu appliques d'abord les critères éliminatoires et tu ne vois pas l'image directement: signale cette limite.", "Rends un verdict fondé exclusivement sur l'audit visuel ci-dessous et la grille. Format: LIMITE, ÉLIMINATOIRES, NOTE /100, VERDICT, POINT FORT, TROIS PRIORITÉS ESSAI 2.\n\nÉPREUVE:\n" + EPREUVE + "\n\nAUDIT VISUEL:\n" + vision, "07-verdict-local", temperature=0.1, ) (PROOFS / "execution.json").write_text(json.dumps(records, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") print(json.dumps({"image": str(destination), "records": records, "verdict": judge}, ensure_ascii=False, indent=2))