diff --git a/.gitignore b/.gitignore index 165059f..7af30ea 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,6 @@ tools/assets/out/ build/ .svelte-kit/ dev-dist/ + +# Sorties du pipeline de génération (filière B) +tools/assets/out/ diff --git a/docs/sessions/03-pipeline-assets.md b/docs/sessions/03-pipeline-assets.md index 07060f4..c9428d0 100644 --- a/docs/sessions/03-pipeline-assets.md +++ b/docs/sessions/03-pipeline-assets.md @@ -52,3 +52,65 @@ du modèle ne le sont pas nécessairement**. Le plan d'assets et les workflows ComfyUI ne sont pas écrits : ils dépendent de choix graphiques qui appartiennent au porteur, et générer du décor avant d'avoir un jeu jouable serait mettre la charrue avant les bœufs. + +--- + +## Addendum — pipeline confronté au ComfyUI réel + +Le porteur a ouvert l'accès à son instance (`127.0.0.1:8188`, ComfyUI 0.27.1, deux GPU +AMD). Le pipeline a donc été exécuté pour de vrai, pas simulé. + +### Reproductibilité prouvée au bit près + +| Exécution | seed | SHA-256 de l'image | +|---|---|---| +| 1 | 3475989105 | `b7007a7a1088cb359a06ddaaff358e92…` | +| 2 | 3475989105 | `b7007a7a1088cb359a06ddaaff358e92…` | + +**Images bit-identiques.** C'est précisément ce que le pack d'origine annonçait sans y +parvenir, et c'est maintenant vérifié plutôt qu'affirmé. Workflow de référence : +`workflows/decor-sdxl.json` (SDXL 1.0, 1024×576, 20 pas, dpmpp_2m/karras). + +### Deux défauts de mon propre client, révélés par le réel + +1. **Le prompt négatif était concaténé au prompt positif** — travers hérité du pack. Sur + SDXL, cela revient à demander au modèle de produire ce qu'on voulait exclure. Il a + désormais son propre nœud. +2. **Les images n'étaient pas rapatriées.** Elles restaient dans le dossier de sortie de + ComfyUI, et le manifeste référençait des chemins inexistants. Ajout de `recuperer()` + via `/view`. + +Un pipeline qu'on n'exécute pas est un pipeline qu'on croit correct. + +### Krea 2 — disponible, mais trois questions à trancher + +Deux choses portent ce nom sur la machine, et elles n'ont rien à voir : + +| | ce que c'est | état | +|---|---|---| +| `krea2_turbo_nvfp4.safetensors` | poids locaux, chargés par `UNETLoader` | **nvfp4 = format FP4 NVIDIA Blackwell**, sur deux GPU AMD : très probablement inexploitable | +| `Krea2ImageNode` | nœud `api_node: True`, catégorie `partner/image/Krea` | **appel à un service tiers payant** | + +Le workflow transmis est au format *interface* (`nodes`, `links`, `widgets_values`) et non +au format API que consomme `/prompt`. Sa conversion bute sur le champ `model`, de type +`COMFY_DYNAMICCOMBO_V3`, dont la forme sérialisée ne se devine pas sans risque. + +**Trois points à trancher par le porteur** avant de câbler Krea 2 : + +1. **Export au format API.** Dans ComfyUI : menu → *Save (API Format)*. C'est le seul + format que `/prompt` accepte, et il lève toute incertitude sur le champ `model`. +2. **Doctrine.** `Krea2ImageNode` envoie les prompts à un serveur tiers. C'est toléré parce + que la génération a lieu **au build**, jamais au runtime (playbook P2) — mais c'est un + écart au « zéro service tiers » qui mérite d'être inscrit, pas subi. +3. **Licence des sorties.** Les conditions d'utilisation de Krea déterminent ce qu'on a le + droit de faire des images produites. Tant qu'elles ne sont pas vérifiées, ces images ne + peuvent pas être publiées sous CC BY-SA 4.0 avec le reste du contenu (ADR 0001). + +En attendant, SDXL 1.0 en local reste le workflow par défaut : aucun coût, aucune donnée +qui sort de la machine, et une reproductibilité démontrée. + +### Réserve sur le rendu + +Le fond produit est graphiquement juste — aplats, angles nets, aucun texte, aucun visage — +mais sa palette tire vers l'orange-rouge, plus chaude que la charte OKI (noir profond, or, +vert, rouge en ponctuation). À reprendre en session 6 si ces fonds sont retenus. diff --git a/tools/assets/comfyui_client.py b/tools/assets/comfyui_client.py index dea6da9..67d2548 100644 --- a/tools/assets/comfyui_client.py +++ b/tools/assets/comfyui_client.py @@ -99,19 +99,44 @@ def attendre(serveur: str, prompt_id: str, delai_max: int = 600) -> dict: raise TimeoutError(f"ComfyUI n'a pas rendu {prompt_id} en {delai_max}s") -def preparer(workflow_base: dict, noeud_seed: str, seed: int, noeud_prompt: str, prompt: str) -> dict: +def preparer(workflow_base: dict, plan: dict, seed: int, prompt: str) -> dict: """ Copie profonde du workflow avant modification. `copy.deepcopy` plutôt que `json.loads(json.dumps(...))` : même résultat, sans aller-retour par une sérialisation qui écraserait tout type non JSON. + + Le prompt négatif va dans SON PROPRE nœud. Le pack d'origine le concaténait au + prompt positif — sur SDXL, cela revient à demander au modèle de produire ce qu'on + voulait exclure. """ workflow = copy.deepcopy(workflow_base) - workflow[noeud_seed]["inputs"]["seed"] = seed - workflow[noeud_prompt]["inputs"]["text"] = prompt + workflow[plan["noeud_seed"]]["inputs"]["seed"] = seed + workflow[plan["noeud_prompt"]]["inputs"]["text"] = prompt + if plan.get("noeud_prompt_negatif"): + workflow[plan["noeud_prompt_negatif"]]["inputs"]["text"] = PROMPT_NEGATIF return workflow +def recuperer(serveur: str, image: dict, destination: Path) -> None: + """ + Rapatrie une image depuis ComfyUI. + + Sans cela, les fichiers restent dans le dossier de sortie de ComfyUI et le manifeste + référence des chemins qui n'existent pas côté projet. + """ + parametres = urllib.parse.urlencode( + { + "filename": image["filename"], + "subfolder": image.get("subfolder", ""), + "type": image.get("type", "output"), + } + ) + url = urllib.parse.urljoin(serveur, f"/view?{parametres}") + with urllib.request.urlopen(url, timeout=60) as reponse: + destination.write_bytes(reponse.read()) + + def generer(args: argparse.Namespace) -> int: workflow_base = json.loads(Path(args.workflow).read_text(encoding="utf-8")) plan = json.loads(Path(args.plan).read_text(encoding="utf-8")) @@ -123,7 +148,7 @@ def generer(args: argparse.Namespace) -> int: for asset in plan["assets"]: cle = f"{asset['id']}:{asset['type']}" seed = seed_deterministe(cle) - prompt = f"{asset['prompt']}. Negative: {PROMPT_NEGATIF}" + prompt = asset["prompt"] print(f"→ {cle} (seed {seed})", flush=True) @@ -131,17 +156,20 @@ def generer(args: argparse.Namespace) -> int: manifeste.append({"cle": cle, "seed": seed, "fichier": None, "sha256": None}) continue - workflow = preparer(workflow_base, plan["noeud_seed"], seed, plan["noeud_prompt"], prompt) + workflow = preparer(workflow_base, plan, seed, prompt) prompt_id = poster(args.serveur, workflow, client_id) resultat = attendre(args.serveur, prompt_id) - fichiers = [ - img["filename"] + images = [ + img for sortie in resultat.get("outputs", {}).values() for img in sortie.get("images", []) + if img.get("type") == "output" ] - for nom in fichiers: + for image in images: + nom = f"{asset['id']}-{asset['type']}-{seed}.png" chemin = SORTIE / nom + recuperer(args.serveur, image, chemin) manifeste.append( { "cle": cle, diff --git a/tools/assets/plan-assets.json b/tools/assets/plan-assets.json new file mode 100644 index 0000000..520dd33 --- /dev/null +++ b/tools/assets/plan-assets.json @@ -0,0 +1,13 @@ +{ + "$comment": "FILIÈRE B — DÉCOR UNIQUEMENT. Aucun de ces assets ne sert à identifier une espèce : la porte de validation refuse qu'un média de filière B porte un usage « épreuve » ou « indice ».", + "noeud_seed": "3", + "noeud_prompt": "6", + "noeud_prompt_negatif": "7", + "assets": [ + { + "id": "lakou", + "type": "fond", + "prompt": "lush caribbean creole garden at dawn, warm saturated colours against a very dark background, afrofuturist illustration, flat graphic shapes, sharp angular composition, gold ochre deep green crimson palette, no people, no text, empty foreground for interface" + } + ] +} diff --git a/tools/assets/workflows/decor-sdxl.json b/tools/assets/workflows/decor-sdxl.json new file mode 100644 index 0000000..995752e --- /dev/null +++ b/tools/assets/workflows/decor-sdxl.json @@ -0,0 +1,16 @@ +{ + "4": { "class_type": "CheckpointLoaderSimple", "inputs": { "ckpt_name": "sd_xl_base_1.0.safetensors" } }, + "5": { "class_type": "EmptyLatentImage", "inputs": { "width": 1024, "height": 576, "batch_size": 1 } }, + "6": { "class_type": "CLIPTextEncode", "inputs": { "text": "", "clip": ["4", 1] } }, + "7": { "class_type": "CLIPTextEncode", "inputs": { "text": "", "clip": ["4", 1] } }, + "3": { + "class_type": "KSampler", + "inputs": { + "seed": 0, "steps": 20, "cfg": 6.0, + "sampler_name": "dpmpp_2m", "scheduler": "karras", "denoise": 1.0, + "model": ["4", 0], "positive": ["6", 0], "negative": ["7", 0], "latent_image": ["5", 0] + } + }, + "8": { "class_type": "VAEDecode", "inputs": { "samples": ["3", 0], "vae": ["4", 2] } }, + "9": { "class_type": "SaveImage", "inputs": { "filename_prefix": "ki-fwi/decor", "images": ["8", 0] } } +}