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/
.svelte-kit/
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
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.
+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")
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,
+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] } }
}