From 1585e986c10bc61e63e9c2a5e5bd262bbe9dddca Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Sat, 27 Jun 2026 04:20:49 +0200 Subject: [PATCH] =?UTF-8?q?feat(gamedev):=20comfyui=5Fbuild=5Fasset=5Fvari?= =?UTF-8?q?ant=5Fworkflow=20=E2=80=94=20variantes=20img2img=20de=20un=20as?= =?UTF-8?q?set=20existente?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Primer builder gamedev-2d de transformacion (img2img) en vez de generacion (txt2img): parte de un asset ya generado y produce una variante coherente (ice/fire/damaged/golden tier) cambiando material/paleta/estado y conservando silueta, pose y composicion via denoise medio (~0.5). Compone comfyui_build_img2img_workflow + comfyui_inject_lora + ImageScale opcional. Probado e2e en GPU SD1.5: variante ice del goblin del demo pack (prompt_id 5e4a5d3d) — silueta conservada (luminance corr 0.63) + paleta a frio (blueness B-R -1.6 -> +1.9). Subseccion nueva en docs/capabilities y report 0181. --- docs/capabilities/gamedev-2d.md | 13 + .../comfyui_build_asset_variant_workflow.md | 139 ++++++++++ .../comfyui_build_asset_variant_workflow.py | 262 ++++++++++++++++++ 3 files changed, 414 insertions(+) create mode 100644 python/functions/ml/comfyui_build_asset_variant_workflow.md create mode 100644 python/functions/ml/comfyui_build_asset_variant_workflow.py diff --git a/docs/capabilities/gamedev-2d.md b/docs/capabilities/gamedev-2d.md index 7f8e6944..2675ede9 100644 --- a/docs/capabilities/gamedev-2d.md +++ b/docs/capabilities/gamedev-2d.md @@ -65,6 +65,19 @@ VFX (ver `reports/0143`). | `comfyui_build_rune_glyph_workflow_py_ml` | `(glyph, *, glow=True, style="arcane glowing rune", checkpoint="dreamshaper_8…", size=512, seed=0, lora=None, …) -> dict` | UNA **runa / glifo / sigilo mágico** (glifos rúnicos, círculos mágicos, sigilos de invocación, inscripciones brillantes) para hechizos, portales, marcas de conjuro y efectos de magia: símbolo arcano **aislado** sobre fondo uniforme (`{glyph}, {style}, magic symbol, single isolated glyph, centered, glowing on a solid pure black background, occult sigil, arcane inscription, no scenery, game asset…`) → txt2img cuadrado + LoRA estilo opcional. **`glow` elige el camino a alpha**: `glow=True` (defecto) = runa BRILLANTE sobre **NEGRO puro**, **sin Rembg** (recortaría el halo del resplandor), insumo de **`comfyui_matting_luma_to_alpha`** (luma=alpha, **blend aditivo** en el motor — conserva el glow); `glow=False` = runa MATE/grabada sobre fondo plano (el negativo rechaza `glow/neon/bloom`), recorte/inversión por el caller. El negativo rechaza `realistic text/readable words/latin alphabet` (un glifo arcano, **no letras reales**) + fondo texturizado/niebla. **DISTINTO de `status_effect_icon`** (símbolo SÓLIDO de UI, recorte Rembg, legible a 16-32 px en el HUD): la runa es una marca translúcida que **emite luz** e se inscribe en el mundo. Grimorio coherente = mismo `style`/`checkpoint`/`lora`, varía `glyph`/`seed`. ⚠️ luma Rec601 penaliza el rojo → para runas rojas (sigilo demoníaco) pasar `luma_weights` con más peso al rojo + subir `gamma`; runas blancas/azules/doradas van con pesos por defecto. Probado e2e en GPU con SD1.5 — `circular summoning rune` glow seed 11 512×512, círculo de invocación brillante sobre **negro puro** (esquinas luma 0.00, dark 83%, runa 3.4% brillante, max 255) apto luma→alpha (`prompt_id 701d149a`, `reports/0172`). SD1.5. | | `comfyui_build_title_lettering_workflow_py_ml` | `(text, *, letter_style="epic fantasy metallic", checkpoint="juggernaut_xl_v11…", width=1024, height=512, transparent=True, seed=0, lora=None, …) -> dict` | EL texto/logo de **título** de un juego (el nombre del juego o una palabra) renderizado con un **tratamiento de lettering** (metálico, tallado en fuego/piedra/madera, neón, cristal, oro), formato **apaisado** (`width>height`, 1024×512 por defecto), fondo plano recortable a alpha (`the word "{text}" as a game logo, {letter_style} lettering, stylized typography, centered, plain background…`) → txt2img apaisado + LoRA estilo opcional + Rembg (alpha). El **negativo NO rechaza texto** (el lettering es el sujeto) y empuja contra el ruido textual (`extra letters/jumbled text/deformed letters`). El VALOR es el ESTILO del lettering, **NO** la fidelidad tipográfica: ⚠️ la difusión renderiza texto de forma imperfecta — letras de más, deformadas o mal escritas; mitigar con palabras CORTAS en MAYÚSCULA, **re-roll de seeds** (`comfyui_batch_generate`), SDXL > SD1.5 para texto, o pintar el texto real con una fuente en el motor. **Una palabra que es un objeto concreto (DRAGON) → el modelo dibuja el objeto, no las letras** — usar palabras abstractas o reforzar `letter_style`. Marca coherente = mismo `letter_style`/`checkpoint`/`lora`, varía solo `text`. Recorte por **Rembg** (logo sólido), no luma→alpha. Probado e2e en GPU: `DRAGON`/`fire engraved` SD1.5 1024×512 → ilustró dragones rojos (alpha OK, confirma el gotcha de palabra-objeto, `prompt_id 6f3920b7`); `AETHER`/`epic fantasy metallic` SDXL 768×384 → **logo de texto metálico dorado** legible con ortografía imperfecta + alpha (`prompt_id 2a7fe8ba`, `reports/0165`). SD1.5/SDXL. | +## Builders de transformación (`gamedev-2d`, puros — img2img sobre un asset existente) + +A diferencia de los builders de **generación** de arriba (parten de TEXTO, txt2img desde +ruido), estos parten de una **IMAGEN que ya existe** y la transforman. El KSampler arranca +del latente de la imagen base (LoadImage → VAEEncode), no de ruido, así que con `denoise` +medio conserva la estructura del original mientras el prompt reescribe lo pedido. Cubren el +eje que el critic de generación (`reports/0178`) no exploró: derivar de un asset, no inventar +un tipo nuevo. + +| ID | Firma corta | Qué hace | +|---|---|---| +| `comfyui_build_asset_variant_workflow_py_ml` | `(input_image, variant, *, checkpoint="dreamshaper_8…", denoise=0.5, style="game asset", size=512, seed=0, lora=None, …) -> dict` | UNA **variante coherente de un asset 2D ya generado** (img2img): parte del sprite/icono que existe en `input_image` y produce su versión de **otro material/paleta/tier/estado** (`ice element`, `fire element`, `battle-damaged`, `golden tier 2`, `corrupted`) manteniendo **silueta, pose y composición** del original. Compone `comfyui_build_img2img_workflow` (LoadImage → VAEEncode → KSampler con `denoise`) + `comfyui_inject_lora` (estilo opcional) + `ImageScale` opcional (`size` normaliza la base a size×size; `size=None` preserva las dimensiones exactas sin deformar). El prompt es `{variant}, {style}, same composition, same pose, same silhouette, …`. **`denoise` es la palanca**: ~0.3 invisible, **0.45-0.6 recomendado** (cambia material/paleta, conserva forma), ~0.8 deriva la pose y se acerca a txt2img. Set de variantes del MISMO asset = mismo `input_image`/`style`/`seed`, varía solo `variant`. **DISTINTO de los builders txt2img** (`enemy_creature`, `item_icon`…): esos generan un tipo desde cero; éste transforma uno concreto. **NO inyecta Rembg** (img2img preserva el fondo/alpha del original según la base). ⚠️ la imagen base debe existir en `input/` del server (subir con `POST /upload/image`); pura, no valida (usar `comfyui_validate_workflow` antes de enviar); asset NO cuadrado + `size` fijo + `crop="disabled"` deforma → `size=None` o `crop="center"`. Probado e2e en GPU con SD1.5 — variante `ice element, frozen` del goblin `enemy_creature_00001_.png` denoise 0.5 seed 7 512×512 (`prompt_id 5e4a5d3d`): silueta conservada (luminance corr 0.63) + paleta a frío (blueness B−R −1.6→+1.9), `reports/0181`. SD1.5. | + ## Funciones de post-proceso y puente (`gamedev-2d`, CPU) | ID | Firma corta | Qué hace | diff --git a/python/functions/ml/comfyui_build_asset_variant_workflow.md b/python/functions/ml/comfyui_build_asset_variant_workflow.md new file mode 100644 index 00000000..7e076c83 --- /dev/null +++ b/python/functions/ml/comfyui_build_asset_variant_workflow.md @@ -0,0 +1,139 @@ +--- +name: comfyui_build_asset_variant_workflow +kind: function +lang: py +domain: ml +purity: pure +version: 1.0.0 +signature: "def comfyui_build_asset_variant_workflow(input_image: str, variant: str, *, checkpoint: str = \"dreamshaper_8.safetensors\", denoise: float = 0.5, style: str = \"game asset\", size: int | None = 512, seed: int = 0, lora: str | None = None, lora_strength: float = 1.0, upscale_method: str = \"lanczos\", crop: str = \"disabled\", negative: str | None = None, steps: int = 28, cfg: float = 7.0, sampler_name: str = \"dpmpp_2m\", scheduler: str = \"karras\", filename_prefix: str = \"asset_variant\") -> dict" +description: "Construye el dict (API format) del workflow de una VARIANTE img2img de un asset 2D ya generado: parte de una IMAGEN existente (un sprite de enemigo, un icono...) y produce una version coherente que cambia material/paleta/tier/estado (ice element, fire element, battle-damaged, golden tier 2, corrupted) manteniendo la composicion, la pose y la silueta del original. A diferencia de los builders gamedev hermanos (enemy_creature, item_icon...), que parten de TEXTO (txt2img desde ruido), este parte de una imagen via img2img con denoise MEDIO (~0.45-0.6): el KSampler arranca del latente de la imagen base, no de ruido. Normaliza el tamano con un ImageScale opcional (size) o preserva las dimensiones del original (size=None). Compone comfyui_build_img2img_workflow + comfyui_inject_lora (estilo opcional). Pura, sin red ni I/O. class_types verificados contra /object_info (8GB lowvram)." +tags: [comfyui, ml, gamedev-2d, img2img, variant, asset-transform, stable-diffusion, workflow] +uses_functions: [comfyui_build_img2img_workflow_py_ml, comfyui_inject_lora_py_ml] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +params: + - name: input_image + desc: "Nombre del archivo de la imagen base dentro de la carpeta input/ del servidor ComfyUI (un asset YA generado). Lo carga el nodo LoadImage. Subelo antes con POST /upload/image o copialo a ~/ComfyUI/input/. No puede estar vacio." + - name: variant + desc: "Descripcion de la variante a producir (ej. 'ice element, frozen', 'fire element, molten', 'battle-damaged, cracked', 'golden tier 2', 'corrupted shadow'). Reescribe material/paleta/estado del asset manteniendo su composicion. No describe el sujeto desde cero: transforma el que ya existe en input_image. No puede estar vacio." + - name: checkpoint + desc: "Checkpoint del servidor. 'dreamshaper_8.safetensors' (SD1.5, holgado en 8GB lowvram) por defecto. keyword-only." + - name: denoise + desc: "Fuerza de denoising del KSampler (cuanto se aparta del original). ~0.3 apenas cambia; 0.45-0.6 (recomendado) cambia material/paleta conservando silueta/pose; ~0.8 se aleja y empieza a ser casi txt2img. Se clampa a [0.0, 1.0]. keyword-only." + - name: style + desc: "Descriptor de estilo que mantiene coherentes las variantes de un set (ej. 'game asset', 'dark fantasy creature', 'pixel art'). Mismo style + checkpoint + (lora) en todas las variantes del mismo asset. keyword-only." + - name: size + desc: "Lado en px al que se NORMALIZA la imagen base antes de encodearla (inserta un ImageScale a size x size). None = no escala; la variante hereda las dimensiones EXACTAS del original (preserva proporcion sin deformar). 512 por defecto (SD1.5). keyword-only." + - name: seed + desc: "Semilla del KSampler. keyword-only." + - name: lora + desc: "LoRA de estilo opcional en models/loras (ej. 'dark_fantasy_sd15.safetensors'). None = sin LoRA. keyword-only." + - name: lora_strength + desc: "Fuerza del LoRA sobre model y clip. Se clampa a [0.0, 2.0]. keyword-only." + - name: upscale_method + desc: "Metodo del ImageScale ('lanczos', 'bilinear', 'bicubic', 'area', 'nearest-exact'). Solo se usa si size no es None. keyword-only." + - name: crop + desc: "Modo de recorte del ImageScale ('disabled' conserva todo el contenido, 'center' recorta al centro para encajar el ratio). Solo si size no es None. keyword-only." + - name: negative + desc: "Prompt negativo. None usa el negativo por defecto pensado para variantes (conservar pose/composicion, una figura, fondo limpio). keyword-only." + - name: steps + desc: "Pasos de sampling del KSampler. keyword-only." + - name: cfg + desc: "Classifier-free guidance scale. keyword-only." + - name: sampler_name + desc: "Nombre del sampler (ej. 'dpmpp_2m', 'euler'). keyword-only." + - name: scheduler + desc: "Scheduler del sampler (ej. 'karras', 'normal'). keyword-only." + - name: filename_prefix + desc: "Prefijo del archivo de salida en SaveImage. keyword-only." +output: "dict en API format listo para comfyui_submit_workflow: img2img base (parte de input_image) con prompt de variante + ImageScale opcional (normaliza a size) + LoRA opcional. Nodos: CheckpointLoaderSimple '4', LoadImage '10', VAEEncode '11', CLIPTextEncode '6'/'7', KSampler '3' (denoise medio), VAEDecode '8', SaveImage '9', + ImageScale y LoraLoader si aplican." +tested: false +file_path: python/functions/ml/comfyui_build_asset_variant_workflow.py +--- + +Construye el dict (API format) del workflow de una **variante de un asset 2D que ya +existe** (img2img). Builder gamedev hermano de `comfyui_build_enemy_creature_workflow` +e `comfyui_build_item_icon_workflow`, pero con un eje distinto: en vez de generar un +TIPO de asset desde texto, **transforma** una imagen concreta (un sprite ya generado) +en una variante coherente — la version "de hielo", "de fuego", "dañada" o "tier 2 +dorada" — conservando silueta, pose y composición del original. + +## Ejemplo + +```python +import sys, os +sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions")) +from ml.comfyui_build_asset_variant_workflow import comfyui_build_asset_variant_workflow + +# Variante "de hielo" de un sprite de goblin ya generado (subido al input/ del server) +wf = comfyui_build_asset_variant_workflow( + "enemy_creature_00001_.png", # asset existente en el input/ de ComfyUI + "ice element, frozen", # la variante a producir + style="dark fantasy creature, game asset", + denoise=0.5, # medio: cambia material/paleta, conserva silueta + seed=7, +) +# wf parte de una imagen (img2img), NO de ruido: +# "VAEEncode" in {n["class_type"] for n in wf.values()} # True +# "EmptyLatentImage" not in {n["class_type"] for n in wf.values()} # True (no es txt2img) +# wf["10"]["inputs"]["image"] == "enemy_creature_00001_.png" +# wf["3"]["inputs"]["denoise"] == 0.5 +# "ice element, frozen" in wf["6"]["inputs"]["text"] +``` + +El bloque se lanza con el python del venv (`python/.venv/bin/python3`). Nota: `./fn +run` directo no aplica a este builder porque su firma usa `*` (keyword-only) y el +generador de runner de `fn run` no lo soporta — igual que `comfyui_build_img2img_workflow`. +Usa el import de arriba o un heredoc. + +Set de variantes del MISMO asset (mismo `input_image`/`style`/`seed`, distinto `variant`): + +```python +for v in ["ice element, frozen", "fire element, molten", "battle-damaged, cracked", "golden tier 2"]: + wf = comfyui_build_asset_variant_workflow("enemy_creature_00001_.png", v, + style="dark fantasy creature, game asset", + denoise=0.5, seed=7) + # enviar con comfyui_submit_workflow -> familia coherente de variantes +``` + +Para enviar a la GPU: subir la base con `POST /upload/image`, luego +`comfyui_submit_workflow(wf)` + `comfyui_wait_result(prompt_id)` + +`comfyui_fetch_output_image(filename)`. + +## Cuando usarla + +Cuando ya tienes un asset 2D generado y quieres **derivar variantes coherentes** de +él (elemento/material/tier/estado) sin redibujar desde cero: el sprite de hielo del +mismo enemigo, la armadura dorada del mismo personaje, la versión dañada del mismo +prop. Es img2img con denoise medio que conserva la composición original. Para generar +un asset NUEVO desde texto usa los builders txt2img hermanos +(`comfyui_build_enemy_creature_workflow`, `comfyui_build_item_icon_workflow`...); para +ampliar/refinar resolución usa `comfyui_build_upscale_workflow`; para img2img genérico +sin scaffolding de variante usa `comfyui_build_img2img_workflow` directo. + +## Gotchas + +- Es **img2img**, no txt2img: SIEMPRE parte de una imagen (`input_image`), no de ruido + en blanco. Esa imagen debe existir en la carpeta `input/` del servidor ComfyUI + (subir con `POST /upload/image` o copiar a `~/ComfyUI/input/`). Es pura: NO valida + que exista; si no está, ComfyUI rechaza el workflow con HTTP 400 al enviarlo. Valida + antes con `comfyui_validate_workflow`. +- `denoise` es la palanca clave: cerca de 0.0 apenas cambia (variante invisible); + 0.45-0.6 es el rango útil (cambia material/paleta manteniendo silueta); cerca de 0.8 + se aleja del original y deriva la pose/composición (deja de ser variante coherente y + se acerca a un txt2img). Default 0.5. +- `size` reescala la imagen base a `size x size` con un ImageScale ANTES de encodear. + Con `size=512` y un asset cuadrado 512 es no-op de tamaño; con un asset NO cuadrado y + `crop="disabled"` el ImageScale fuerza el ratio cuadrado y puede deformar — pasa + `size=None` para preservar las dimensiones/proporción exactas del original, o + `crop="center"` para recortar al centro en vez de deformar. +- El prompt refuerza "same composition, same pose, same silhouette" además del denoise + medio; aun así, denoise alto o un `variant` que implique cambio de forma (ej. "giant + version") puede alterar la silueta. Para variantes solo de paleta/material, mantén + denoise ≤0.55. +- Asume checkpoint con VAE embebido (VAEEncode/VAEDecode usan el VAE del checkpoint). + Para un VAE externo hay que reconectar esas entradas a mano. +- 8GB lowvram: SD1.5 a 512² va holgado. Si OOM, baja `size` (384) o `denoise`; NO subas + a SDXL en 8GB para esto. diff --git a/python/functions/ml/comfyui_build_asset_variant_workflow.py b/python/functions/ml/comfyui_build_asset_variant_workflow.py new file mode 100644 index 00000000..7cdb4733 --- /dev/null +++ b/python/functions/ml/comfyui_build_asset_variant_workflow.py @@ -0,0 +1,262 @@ +"""Construye el workflow ComfyUI de una VARIANTE de un asset ya generado (img2img). + +A diferencia de los builders gamedev hermanos (enemy_creature, item_icon, +ui_hud...), que parten de TEXTO (txt2img desde ruido), este builder parte de una +IMAGEN que ya existe y produce una variante COHERENTE: cambia paleta, material, +tier o estado del asset manteniendo la composicion, la pose y la silueta del +original. Es el caso real de gamedev: tienes el sprite de un enemigo y quieres su +version "de hielo", "de fuego", "danada en combate" o "tier 2 dorada" sin redibujar +desde cero. + +El mecanismo es img2img con denoise MEDIO: el KSampler parte del latente de la +imagen base (LoadImage -> [ImageScale opcional] -> VAEEncode), no de ruido, asi que +con denoise ~0.45-0.6 conserva la estructura global (silueta/pose) mientras el +prompt de la variante reescribe material y color. Denoise bajo (~0.3) apenas cambia; +alto (~0.8) se aleja del original y empieza a ser casi txt2img. + +Cableado: + + CheckpointLoaderSimple -> [LoraLoader opcional de estilo] -> KSampler + LoadImage -> [ImageScale opcional a size x size] -> VAEEncode -> KSampler.latent + CLIPTextEncode (prompt de variante + "same composition/pose/silhouette") + KSampler (denoise medio) -> VAEDecode -> SaveImage + +Compone: + - comfyui_build_img2img_workflow -> base img2img (LoadImage/VAEEncode/KSampler con denoise) + - comfyui_inject_lora -> LoRA de estilo opcional (consistencia con el set) + +Por que ImageScale opcional y no EmptyLatentImage: en img2img el tamano de salida lo +fija la imagen base (no hay EmptyLatentImage). Para poder NORMALIZAR todos los assets +del set a una resolucion comun (`size`), se inserta un ImageScale entre LoadImage y +VAEEncode que reescala la base antes de encodear. Si size=None, no se escala y la +variante hereda las dimensiones exactas del original (preserva proporcion sin +deformar). Es la diferencia clave con un txt2img: aqui SIEMPRE hay una imagen de +entrada de la que se parte; el prompt no genera en blanco, transforma. + +Por que el prompt empuja "same composition, same pose, same silhouette": el denoise +medio ya conserva la estructura, pero reforzarlo en el texto reduce la deriva de +pose/encuadre y mantiene la variante alineada con el original (lo que se quiere para +un set coherente: misma figura, distinto material/tier). + +class_types/inputs verificados contra /object_info del servidor (8GB lowvram): +CheckpointLoaderSimple, LoadImage, ImageScale, VAEEncode, CLIPTextEncode, KSampler, +VAEDecode, SaveImage, LoraLoader. + +Funcion pura: sin red, sin I/O. No muta dicts de entrada (copia profunda al insertar +ImageScale). NO valida que input_image/checkpoint/lora existan en el servidor (eso +es responsabilidad de comfyui_validate_workflow antes de enviar). Determinista para +los mismos argumentos. +""" +from __future__ import annotations + +import copy +import os +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) + +# Negativo por defecto pensado para variantes de asset: conservar UNA figura entera, +# bien formada, fondo limpio, SIN cambiar la composicion/pose y sin texto/marcas ni +# objetos extra. No filtra material ni paleta (ice/fire/golden/damaged son validos). +_VARIANT_NEGATIVE = ( + "blurry, lowres, deformed, disfigured, bad anatomy, extra limbs, " + "different pose, different composition, different framing, extra objects, " + "duplicate, multiple subjects, text, watermark, signature, logo, " + "cropped, cut off, out of frame, jpeg artifacts" +) + + +def _inject_image_scale( + workflow: dict, *, size: int, upscale_method: str, crop: str +) -> dict: + """Inserta un nodo ImageScale entre LoadImage y VAEEncode para normalizar el tamano. + + Reescala la imagen base a size x size ANTES de encodearla al latente, de modo que + la variante salga a la resolucion deseada en lugar de heredar la del original. + Repunta VAEEncode.pixels a la salida del ImageScale. Pura: trabaja sobre copia. + """ + wf = copy.deepcopy(workflow) + load_id = next( + (nid for nid, n in wf.items() if n.get("class_type") == "LoadImage"), None + ) + vaeencode_id = next( + (nid for nid, n in wf.items() if n.get("class_type") == "VAEEncode"), None + ) + if load_id is None or vaeencode_id is None: + raise ValueError( + "comfyui_build_asset_variant_workflow: no se encontro LoadImage/VAEEncode " + "para insertar ImageScale" + ) + numeric = [int(k) for k in wf.keys() if str(k).isdigit()] + scale_id = str((max(numeric) + 1) if numeric else len(wf) + 1) + # La fuente de pixeles que hoy alimenta el VAEEncode (normalmente LoadImage[0]). + src = wf[vaeencode_id]["inputs"].get("pixels", [load_id, 0]) + wf[scale_id] = { + "class_type": "ImageScale", + "inputs": { + "image": list(src), + "upscale_method": upscale_method, + "width": int(size), + "height": int(size), + "crop": crop, + }, + } + wf[vaeencode_id]["inputs"]["pixels"] = [scale_id, 0] + return wf + + +def comfyui_build_asset_variant_workflow( + input_image: str, + variant: str, + *, + checkpoint: str = "dreamshaper_8.safetensors", + denoise: float = 0.5, + style: str = "game asset", + size: int | None = 512, + seed: int = 0, + lora: str | None = None, + lora_strength: float = 1.0, + upscale_method: str = "lanczos", + crop: str = "disabled", + negative: str | None = None, + steps: int = 28, + cfg: float = 7.0, + sampler_name: str = "dpmpp_2m", + scheduler: str = "karras", + filename_prefix: str = "asset_variant", +) -> dict: + """Construye el dict (API format) de una variante img2img de un asset existente. + + Args: + input_image: nombre del archivo de la imagen base dentro de la carpeta + input/ del servidor ComfyUI (un asset YA generado: un sprite de enemigo, + un icono de objeto...). Lo carga el nodo LoadImage. Subelo antes con + POST /upload/image o copialo a ~/ComfyUI/input/. No puede estar vacio. + variant: descripcion de la variante a producir (ej. "ice element, frozen", + "fire element, molten", "battle-damaged, cracked", "golden tier 2", + "corrupted shadow"). Es lo que reescribe material/paleta/estado del asset + manteniendo su composicion. No puede estar vacio. Es lo que diferencia + este builder de un txt2img: NO describe el sujeto desde cero, transforma + uno que ya existe en la imagen base. + checkpoint: checkpoint del servidor. 'dreamshaper_8.safetensors' (SD1.5, + holgado en 8GB lowvram) por defecto. keyword-only. + denoise: fuerza de denoising del KSampler (cuanto se aparta del original). + ~0.3 apenas cambia; 0.45-0.6 (recomendado) cambia material/paleta + conservando silueta/pose; ~0.8 se aleja y empieza a ser casi txt2img. Se + clampa a [0.0, 1.0]. keyword-only. + style: descriptor de estilo que mantiene coherentes las variantes de un set + (ej. "game asset", "dark fantasy creature", "pixel art"). Pasa el MISMO + style + checkpoint + (lora) a todas las variantes del mismo asset. + keyword-only. + size: lado en px al que se NORMALIZA la imagen base antes de encodearla + (inserta un ImageScale a size x size). None = no escala, la variante + hereda las dimensiones EXACTAS del original (preserva proporcion sin + deformar). 512 por defecto (SD1.5). keyword-only. + seed: semilla del KSampler. keyword-only. + lora: LoRA de estilo opcional en models/loras (ej. + 'dark_fantasy_sd15.safetensors'). None = sin LoRA. keyword-only. + lora_strength: fuerza del LoRA sobre model y clip. Se clampa a [0.0, 2.0]. + keyword-only. + upscale_method: metodo del ImageScale ('lanczos', 'bilinear', 'bicubic', + 'area', 'nearest-exact'). Solo se usa si size no es None. keyword-only. + crop: modo de recorte del ImageScale ('disabled' conserva todo el contenido, + 'center' recorta al centro para encajar el ratio). Solo si size no es + None. keyword-only. + negative: prompt negativo. None usa el negativo por defecto pensado para + variantes (conservar pose/composicion, una figura, fondo limpio). + keyword-only. + steps, cfg, sampler_name, scheduler, filename_prefix: parametros de + generacion. keyword-only. + + Returns: + dict en API format listo para comfyui_submit_workflow: img2img base (parte de + input_image) con prompt de variante ('{variant}, {style}, same composition, + same pose, same silhouette, ...') + ImageScale opcional (normaliza a size) + + LoRA de estilo opcional. Es UNA variante; un set de variantes del MISMO asset + -> llamar por `variant` con el mismo input_image/style/checkpoint/seed. + + Raises: + ValueError: si input_image o variant estan vacios, o si la base no tiene + LoadImage/VAEEncode donde insertar el ImageScale (propagado por el helper). + """ + from ml.comfyui_build_img2img_workflow import comfyui_build_img2img_workflow + + if not input_image or not input_image.strip(): + raise ValueError( + "comfyui_build_asset_variant_workflow: 'input_image' no puede estar vacio" + ) + if not variant or not variant.strip(): + raise ValueError( + "comfyui_build_asset_variant_workflow: 'variant' no puede estar vacio" + ) + + input_image = input_image.strip() + variant = variant.strip() + denoise = max(0.0, min(1.0, float(denoise))) + lora_strength = max(0.0, min(2.0, float(lora_strength))) + neg = _VARIANT_NEGATIVE if negative is None else negative + + # Prompt de variante: reescribe material/paleta/estado pero refuerza que la + # composicion, pose y silueta del original se conservan (img2img coherente). + positive = ( + f"{variant}, {style}, same composition, same pose, same silhouette, " + "consistent design, high detail" + ) + + wf = comfyui_build_img2img_workflow( + checkpoint, + input_image, + positive, + neg, + denoise=denoise, + steps=steps, + cfg=cfg, + seed=seed, + sampler_name=sampler_name, + scheduler=scheduler, + ) + + # El builder base hardcodea filename_prefix="comfy_img2img"; lo repuntamos. + save_id = next( + (nid for nid, n in wf.items() if n.get("class_type") == "SaveImage"), None + ) + if save_id is not None: + wf[save_id]["inputs"]["filename_prefix"] = filename_prefix + + if size is not None: + wf = _inject_image_scale( + wf, size=size, upscale_method=upscale_method, crop=crop + ) + + if lora: + from ml.comfyui_inject_lora import comfyui_inject_lora + + wf = comfyui_inject_lora( + wf, lora, strength_model=lora_strength, strength_clip=lora_strength + ) + + return wf + + +if __name__ == "__main__": + import json + + wf = comfyui_build_asset_variant_workflow( + "enemy_creature_00001_.png", + "ice element, frozen", + style="dark fantasy creature, game asset", + denoise=0.5, + seed=7, + ) + print( + json.dumps( + { + "nodes": list(wf), + "classes": sorted({n["class_type"] for n in wf.values()}), + "denoise": wf["3"]["inputs"]["denoise"], + "positive": wf["6"]["inputs"]["text"], + "input_image": wf["10"]["inputs"]["image"], + }, + indent=2, + ) + )