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:
@@ -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/
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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] } }
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user