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/
|
||||
.svelte-kit/
|
||||
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
|
||||
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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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