feat(assets): pipeline ComfyUI exécuté en réel, reproductibilité prouvée

Le porteur a ouvert l'accès à son instance ComfyUI : le pipeline a été
exécuté, pas simulé.

Deux exécutions du même plan produisent des images BIT-IDENTIQUES
(seed 3475989105, sha256 b7007a7a…). C'est exactement ce que le pack
d'origine annonçait sans y parvenir, et c'est désormais vérifié.
Workflow de référence : SDXL 1.0, 1024×576, 20 pas, dpmpp_2m/karras.

Deux défauts de mon propre client, révélés par l'exécution réelle :
- le prompt négatif était concaténé au positif — travers hérité du pack.
  Sur SDXL cela revient à demander ce qu'on voulait exclure. Nœud propre.
- les images n'étaient pas rapatriées : elles restaient chez ComfyUI et
  le manifeste pointait vers des chemins inexistants. Ajout de recuperer().

Krea 2 : deux choses distinctes portent ce nom. Les poids locaux
krea2_turbo_nvfp4 sont un build FP4 NVIDIA Blackwell, sur GPU AMD —
probablement inexploitable. Krea2ImageNode est un nœud api_node vers un
service tiers payant. Trois points à trancher avant câblage : export au
format API, écart de doctrine à inscrire, et licence des sorties à
vérifier avant toute publication sous CC BY-SA.

SDXL local reste le défaut : aucun coût, aucune donnée qui sort.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
OKI
2026-07-27 12:03:30 -04:00
co-authored by Claude Opus 5
parent ebb05796c9
commit 0683992384
5 changed files with 130 additions and 8 deletions
+3
View File
@@ -31,3 +31,6 @@ tools/assets/out/
build/ build/
.svelte-kit/ .svelte-kit/
dev-dist/ dev-dist/
# Sorties du pipeline de génération (filière B)
tools/assets/out/
+62
View File
@@ -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 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 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. 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.
+36 -8
View File
@@ -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") 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. Copie profonde du workflow avant modification.
`copy.deepcopy` plutôt que `json.loads(json.dumps(...))` : même résultat, sans `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. 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 = copy.deepcopy(workflow_base)
workflow[noeud_seed]["inputs"]["seed"] = seed workflow[plan["noeud_seed"]]["inputs"]["seed"] = seed
workflow[noeud_prompt]["inputs"]["text"] = prompt workflow[plan["noeud_prompt"]]["inputs"]["text"] = prompt
if plan.get("noeud_prompt_negatif"):
workflow[plan["noeud_prompt_negatif"]]["inputs"]["text"] = PROMPT_NEGATIF
return workflow 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: def generer(args: argparse.Namespace) -> int:
workflow_base = json.loads(Path(args.workflow).read_text(encoding="utf-8")) workflow_base = json.loads(Path(args.workflow).read_text(encoding="utf-8"))
plan = json.loads(Path(args.plan).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"]: for asset in plan["assets"]:
cle = f"{asset['id']}:{asset['type']}" cle = f"{asset['id']}:{asset['type']}"
seed = seed_deterministe(cle) seed = seed_deterministe(cle)
prompt = f"{asset['prompt']}. Negative: {PROMPT_NEGATIF}" prompt = asset["prompt"]
print(f"{cle} (seed {seed})", flush=True) 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}) manifeste.append({"cle": cle, "seed": seed, "fichier": None, "sha256": None})
continue 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) prompt_id = poster(args.serveur, workflow, client_id)
resultat = attendre(args.serveur, prompt_id) resultat = attendre(args.serveur, prompt_id)
fichiers = [ images = [
img["filename"] img
for sortie in resultat.get("outputs", {}).values() for sortie in resultat.get("outputs", {}).values()
for img in sortie.get("images", []) 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 chemin = SORTIE / nom
recuperer(args.serveur, image, chemin)
manifeste.append( manifeste.append(
{ {
"cle": cle, "cle": cle,
+13
View File
@@ -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"
}
]
}
+16
View File
@@ -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] } }
}