feat(gamedev): comfyui_build_asset_variant_workflow — variantes img2img de un asset existente

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.
This commit is contained in:
2026-06-27 04:20:49 +02:00
parent e1f1be02ce
commit 1585e986c1
3 changed files with 414 additions and 0 deletions
+13
View File
@@ -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_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. | | `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 BR 1.6→+1.9), `reports/0181`. SD1.5. |
## Funciones de post-proceso y puente (`gamedev-2d`, CPU) ## Funciones de post-proceso y puente (`gamedev-2d`, CPU)
| ID | Firma corta | Qué hace | | ID | Firma corta | Qué hace |
@@ -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.
@@ -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,
)
)