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
+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,