feat(gamedev): comfyui_build_enemy_creature_workflow — enemigos/criaturas (cuerpo entero, alpha, variantes)

Nuevo builder del grupo gamedev/gamedev-2d para enemigos/criaturas de juego
(goblin, esqueleto, slime, dragon, boss, elemental): figura de cuerpo entero,
centrada, fondo limpio recortable a alpha, estilo consistente entre criaturas
del bestiario. Variantes por nivel/elemento (ice, fire, elite, corrupted) via
el argumento variant, que se antepone a la criatura base.

Funcion pura (dict API format) que compone funciones existentes del registry:
comfyui_build_txt2img_workflow + comfyui_inject_lora (estilo opcional) +
Image Rembg (fondo transparente). Hermano de comfyui_build_item_icon /
ui_hud / sprite_sheet_workflow.

- 8 tests offline verdes (golden/edge/error/determinismo)
- .md autosuficiente (Ejemplo + Cuando usarla + Gotchas)
- fila en docs/capabilities/gamedev-2d.md
- probado e2e en GPU (8GB lowvram, SD1.5): goblin warrior ice cuerpo entero,
  512x512 RGBA con alpha, prompt_id e770d050 (report 0154)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-26 23:29:54 +02:00
parent 03df14df97
commit d7387d9d2c
4 changed files with 471 additions and 0 deletions
+1
View File
@@ -42,6 +42,7 @@ VFX (ver `reports/0143`).
| `comfyui_build_normal_map_workflow_py_ml` | `(image, *, method="normal", strength=1.0, resolution=512, bg_threshold=0.1, filename_prefix="normal_map") -> dict` | Normal/depth map de un sprite existente para iluminación dinámica 2.5D (Godot CanvasItem `normal_map`, Unity sprite normal). `LoadImage → preprocesador controlnet_aux → SaveImage`. `method`: `normal` (default, `BAE-NormalMapPreprocessor`, normal canónico **azul/violeta** usable directo en motor), `normal_midas` (MiDaS, único con `strength``a`, paleta no canónica), `normal_dsine` (DSINE), `depth` (`DepthAnythingV2`, height en gris). `image` debe estar en `input/` de ComfyUI. Coste VRAM ≈0. Probado e2e en GPU (`reports/0150`). | | `comfyui_build_normal_map_workflow_py_ml` | `(image, *, method="normal", strength=1.0, resolution=512, bg_threshold=0.1, filename_prefix="normal_map") -> dict` | Normal/depth map de un sprite existente para iluminación dinámica 2.5D (Godot CanvasItem `normal_map`, Unity sprite normal). `LoadImage → preprocesador controlnet_aux → SaveImage`. `method`: `normal` (default, `BAE-NormalMapPreprocessor`, normal canónico **azul/violeta** usable directo en motor), `normal_midas` (MiDaS, único con `strength``a`, paleta no canónica), `normal_dsine` (DSINE), `depth` (`DepthAnythingV2`, height en gris). `image` debe estar en `input/` de ComfyUI. Coste VRAM ≈0. Probado e2e en GPU (`reports/0150`). |
| `comfyui_build_ui_hud_workflow_py_ml` | `(element, *, ui_style="fantasy game UI", checkpoint="dreamshaper_8…", size=512, transparent=True, lora=None, …) -> dict` | UN elemento de interfaz/HUD de juego (botón, marco/panel, barra de vida/maná/XP, icono de UI, cursor, viñeta de menú): txt2img cuadrado + prompt scaffold de UI (`{element}, {ui_style}, game UI element, centered, clean, plain background…`) + LoRA estilo opcional + Rembg (alpha). HUD coherente = mismo `ui_style`/`checkpoint`/`lora` por pieza, varía solo `element`. El texto/label lo pone el motor (negativo empuja a `no text`). Probado e2e en GPU (`reports/0152`). SD1.5. | | `comfyui_build_ui_hud_workflow_py_ml` | `(element, *, ui_style="fantasy game UI", checkpoint="dreamshaper_8…", size=512, transparent=True, lora=None, …) -> dict` | UN elemento de interfaz/HUD de juego (botón, marco/panel, barra de vida/maná/XP, icono de UI, cursor, viñeta de menú): txt2img cuadrado + prompt scaffold de UI (`{element}, {ui_style}, game UI element, centered, clean, plain background…`) + LoRA estilo opcional + Rembg (alpha). HUD coherente = mismo `ui_style`/`checkpoint`/`lora` por pieza, varía solo `element`. El texto/label lo pone el motor (negativo empuja a `no text`). Probado e2e en GPU (`reports/0152`). SD1.5. |
| `comfyui_build_card_art_workflow_py_ml` | `(subject, *, card_style="fantasy trading card art", checkpoint="juggernaut_xl_v11…", width=512, height=768, hires=True, seed=0, lora=None, …) -> dict` | LA ilustración central de UNA carta coleccionable (TCG): criatura/personaje/hechizo en formato **vertical** de carta (`width<height`, ~512×768), composición centrada + iluminación dramática (`{subject}, {card_style}, dramatic lighting, detailed illustration, centered composition, full art…`). `hires=True` → 2ª pasada de detalle (`comfyui_build_hires_fix_workflow`); si no, txt2img + LoRA estilo opcional. Genera SOLO la ilustración — el marco/título/stats los pone el motor/post (negativo rechaza `card frame/border/text/stats/UI`). Set coherente = mismo `card_style`/`checkpoint`/`lora`, varía solo `subject`. Probado e2e en GPU con SD1.5 (`reports/0153`); ⚠️ el path `hires=True` falla hoy por bug del builder `comfyui_build_hires_fix_workflow` (nodo `UltimateSDUpscale` pide `batch_size`) — usar `hires=False` hasta el fix. SD1.5/SDXL. | | `comfyui_build_card_art_workflow_py_ml` | `(subject, *, card_style="fantasy trading card art", checkpoint="juggernaut_xl_v11…", width=512, height=768, hires=True, seed=0, lora=None, …) -> dict` | LA ilustración central de UNA carta coleccionable (TCG): criatura/personaje/hechizo en formato **vertical** de carta (`width<height`, ~512×768), composición centrada + iluminación dramática (`{subject}, {card_style}, dramatic lighting, detailed illustration, centered composition, full art…`). `hires=True` → 2ª pasada de detalle (`comfyui_build_hires_fix_workflow`); si no, txt2img + LoRA estilo opcional. Genera SOLO la ilustración — el marco/título/stats los pone el motor/post (negativo rechaza `card frame/border/text/stats/UI`). Set coherente = mismo `card_style`/`checkpoint`/`lora`, varía solo `subject`. Probado e2e en GPU con SD1.5 (`reports/0153`); ⚠️ el path `hires=True` falla hoy por bug del builder `comfyui_build_hires_fix_workflow` (nodo `UltimateSDUpscale` pide `batch_size`) — usar `hires=False` hasta el fix. SD1.5/SDXL. |
| `comfyui_build_enemy_creature_workflow_py_ml` | `(creature, *, variant=None, style="game creature, full body", checkpoint="dreamshaper_8…", size=512, transparent=True, seed=0, lora=None, …) -> dict` | UN enemigo/criatura de juego (goblin, esqueleto, slime, dragón, boss, elemental): figura de **cuerpo entero** centrada, fondo limpio recortable a alpha (`{variant} {creature}, {style}, full body, centered, plain background, game asset…`) → txt2img cuadrado + LoRA estilo opcional + Rembg (alpha). `variant` (ice/fire/elite/corrupted…) se antepone a la criatura para generar la familia del MISMO enemigo (misma `creature`/`seed`/`style`, varía solo `variant`); bestiario coherente = mismo `style`/`checkpoint`/`lora`, varía solo `creature`. El negativo empuja a UNA criatura entera sin recorte. Probado e2e en GPU con SD1.5 (`reports/0154`). SD1.5. |
## Funciones de post-proceso y puente (`gamedev`, CPU) ## Funciones de post-proceso y puente (`gamedev`, CPU)
@@ -0,0 +1,115 @@
---
name: comfyui_build_enemy_creature_workflow
kind: function
lang: py
domain: ml
version: "1.0.0"
purity: pure
signature: "def comfyui_build_enemy_creature_workflow(creature: str, *, variant: str | None = None, style: str = \"game creature, full body\", checkpoint: str = \"dreamshaper_8.safetensors\", size: int = 512, transparent: bool = True, seed: int = 0, lora: str | None = None, lora_strength: float = 1.0, rembg_model: str = \"u2net\", negative: str | None = None, steps: int = 28, cfg: float = 7.0, sampler_name: str = \"dpmpp_2m\", scheduler: str = \"karras\", filename_prefix: str = \"enemy_creature\") -> dict"
description: "Construye el dict (API format) del workflow de UN enemigo/criatura de juego 2D (goblin, esqueleto, slime, dragon, boss, elemental): figura de cuerpo entero centrada, fondo limpio uniforme recortable a alpha, estilo consistente entre criaturas del bestiario. Variantes por nivel/elemento (ice, fire, elite, corrupted) via `variant`. Compone comfyui_build_txt2img_workflow + comfyui_inject_lora (estilo opcional) + Image Rembg (fondo transparente si transparent). Hermano de comfyui_build_item_icon/ui_hud/sprite_sheet_workflow. Pura, sin red ni I/O. class_types verificados contra /object_info."
tags: [comfyui, ml, gamedev, gamedev-2d, creature, enemy, monster, bestiary, rembg, workflow]
uses_functions: [comfyui_build_txt2img_workflow_py_ml, comfyui_inject_lora_py_ml]
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: []
params:
- name: creature
desc: "Descripcion de la criatura/enemigo (ej. 'goblin warrior', 'skeleton archer', 'green slime', 'fire dragon', 'orc boss'). Se inserta en un prompt scaffold de criatura. No puede estar vacio."
- name: variant
desc: "Variante por nivel o elemento (ej. 'ice', 'fire', 'corrupted', 'elite', 'shadow'). Se antepone a la criatura ('ice goblin warrior') para generar una familia coherente del MISMO enemigo. None/'' = criatura base. keyword-only."
- name: style
desc: "Descriptor de estilo que mantiene consistentes las criaturas del bestiario (ej. 'game creature, full body', 'dark fantasy creature', 'cartoon RPG monster', 'pixel art creature'). Pasa el MISMO style + checkpoint + lora a todas las criaturas del set para coherencia visual. keyword-only."
- name: checkpoint
desc: "Checkpoint del servidor. 'dreamshaper_8.safetensors' (SD1.5, holgado en 8GB lowvram) por defecto; 'juggernaut_xl_v11.safetensors' para SDXL (mas VRAM, subir size). keyword-only."
- name: size
desc: "Lado del cuadrado en px (width = height = size). 512 SD1.5 por defecto. keyword-only."
- name: transparent
desc: "Si True inyecta Image Rembg y el PNG sale con alpha (fondo recortado, listo como sprite/token). False = criatura opaca sobre fondo plano, recortable luego por el caller. keyword-only."
- name: seed
desc: "Semilla del KSampler. Misma seed + misma criatura/style -> misma figura; variar solo `variant` da variantes coherentes. keyword-only."
- name: lora
desc: "LoRA de estilo/criatura opcional en models/loras (ej. 'dark_fantasy_sd15.safetensors', 'monster_design_xl.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: rembg_model
desc: "Modelo Rembg ('u2net' general, 'isnet-anime' para anime). Solo se usa si transparent=True. keyword-only."
- name: negative
desc: "Prompt negativo. None usa el negativo por defecto pensado para criaturas (una criatura entera, fondo limpio, sin texto/recorte; no filtra ningun tipo ni elemento). keyword-only."
- name: steps
desc: "Pasos del KSampler. keyword-only."
- name: cfg
desc: "CFG del KSampler. keyword-only."
- name: sampler_name
desc: "Sampler del KSampler. keyword-only."
- name: scheduler
desc: "Scheduler del KSampler. keyword-only."
- name: filename_prefix
desc: "Prefijo del PNG en output/. keyword-only."
output: "dict en API format listo para comfyui_submit_workflow: base txt2img cuadrada con prompt scaffold de criatura ('{variant} {creature}, {style}, full body, centered, plain background, game asset, ...') + LoRA de estilo opcional + Image Rembg (si transparent). UNA criatura; un bestiario -> llamar por creature con mismo style/checkpoint/lora; variantes de un enemigo -> misma creature/seed variando `variant`."
tested: true
tests: ["golden transparent: clases CheckpointLoaderSimple/KSampler/VAEDecode/SaveImage/Image Rembg; creature + variant + 'full body' + 'centered' + 'game asset' en prompt; SaveImage <- Rembg; transparency True", "edge transparent=False: sin Rembg, SaveImage <- VAEDecode", "edge size: width==height==768 (cuadrado)", "edge variant opcional: sin variant el prompt arranca con la criatura base", "edge style en prompt", "edge lora: LoraLoader presente con strength", "error creature vacio -> ValueError", "determinismo"]
test_file_path: "python/functions/ml/comfyui_build_enemy_creature_workflow_test.py"
file_path: "python/functions/ml/comfyui_build_enemy_creature_workflow.py"
---
## Ejemplo
```python
import sys, os
sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions"))
from ml.comfyui_build_enemy_creature_workflow import comfyui_build_enemy_creature_workflow
# Un enemigo de cuerpo entero con fondo transparente (alpha), listo para submit.
wf = comfyui_build_enemy_creature_workflow(
"goblin warrior",
variant="ice",
style="dark fantasy creature, full body",
transparent=True,
seed=7,
)
# Variantes coherentes del MISMO enemigo: misma creature/seed/style, cambia variant.
# for v in ["ice", "fire", "corrupted", "elite"]:
# wf = comfyui_build_enemy_creature_workflow("goblin warrior", variant=v,
# style="dark fantasy creature, full body", seed=7)
# comfyui_submit_workflow(wf) # -> comfyui_wait_result -> comfyui_fetch_output_image
# Bestiario completo: variar `creature` con el mismo style/checkpoint/lora.
# Contact-sheet del set: montar los PNG resultantes con comfyui_build_grid.
```
O lanzable directo con: `./fn run comfyui_build_enemy_creature_workflow` (imprime nodos + class_types del ejemplo).
## Cuando usarla
Cuando necesites enemigos/criaturas para un juego (RPG, roguelike, tower defense,
bestiario) con look consistente entre criaturas: goblins, esqueletos, slimes,
dragones, bosses, elementales. Pasa el MISMO `style` + `checkpoint` + (`lora`) a
todas las criaturas del set para que combinen visualmente; varía solo `creature`.
Para las variantes de un enemigo (ice/fire/elite/corrupted) fija `creature` + `seed`
+ `style` y varía solo `variant`. `transparent` recorta el fondo (alpha) listo como
sprite/token para el motor. Para un atlas/contact-sheet, genera cada criatura y
monta los PNG con `comfyui_build_grid`.
## Gotchas
- **El recorte usa Rembg, NO luma-to-alpha**: una criatura es un sujeto solido con
silueta definida, rembg la recorta limpio. `comfyui_matting_luma_to_alpha` es para
translucidos sobre negro (humo/fuego/magia). Si la criatura es un slime translucido
o un fantasma etereo y quieres conservar la translucidez, pon `transparent=False` y
recorta con luma-to-alpha en un paso aparte.
- **Coherencia del bestiario = mismos parametros**: si cambias `style`/`checkpoint`/
`lora`/`seed` entre criaturas, el set deja de combinar. Fija esos y varía solo
`creature` (y `variant` para las variantes de un mismo enemigo).
- **`variant` se antepone a la criatura**: `variant="ice"` + `creature="goblin warrior"`
-> prompt "ice goblin warrior". Deja `variant=None` para la criatura base.
- **SDXL pide mas VRAM y resolucion**: con `checkpoint="juggernaut_xl_v11.safetensors"`
sube `size` a 768/1024; con dreamshaper_8 (SD1.5) deja 512 (holgado en 8GB lowvram).
Si hay OOM, baja `size` o usa SD1.5.
- Si el modelo mete varias criaturas o recorta el cuerpo, el negativo por defecto ya
empuja a "single creature / full body / no cropped / no out of frame"; refuerza
`style` con "full body shot, standing, isolated" si insiste.
- `transparent=False` deja la criatura opaca sobre fondo plano: util si prefieres
recortar fuera del workflow o el motor compone sobre un fondo solido.
- Es una funcion **pura**: solo arma el dict. La generacion real (GPU) la hacen
`comfyui_submit_workflow` + `comfyui_wait_result` + `comfyui_fetch_output_image`.
@@ -0,0 +1,242 @@
"""Construye el workflow ComfyUI de UN enemigo/criatura de juego (API format).
Criatura/enemigo de juego (goblin, esqueleto, slime, dragon, boss, elemental...):
figura de CUERPO ENTERO en pose neutra o de combate, centrada, fondo limpio y
uniforme recortable a alpha, estilo consistente entre criaturas de un mismo
bestiario. Variantes por nivel o elemento (ice, fire, corrupted, elite...) via el
argumento `variant`. Es el builder hermano de comfyui_build_item_icon_workflow /
comfyui_build_ui_hud_workflow / comfyui_build_sprite_sheet_workflow: mismo patron
(PURO, dict API format) que compone funciones existentes del registry, no reescribe
el grafo.
Cableado:
CheckpointLoaderSimple -> [LoraLoader opcional de estilo] -> KSampler
-> CLIPTextEncode (prompt scaffold de criatura) ...
-> VAEDecode -> [Image Rembg opcional] -> SaveImage
Compone:
- comfyui_build_txt2img_workflow -> base txt2img cuadrada
- comfyui_inject_lora -> LoRA de estilo/criatura opcional (consistencia)
- 'Image Rembg (Remove Background)' (helper local) -> fondo transparente (alpha)
Por que Rembg y NO comfyui_matting_luma_to_alpha: una criatura es un sujeto SOLIDO
con silueta definida; rembg recorta limpio la silueta dejando alpha. La
luma-to-alpha es para translucidos sobre negro (humo/fuego/magia), donde aplanaria
la criatura. Si la criatura es un slime translucido o un fantasma etereo y se
quiere conservar la translucidez, recortar fuera del workflow (transparent=False) y
componer con luma-to-alpha en un paso aparte. Para el bestiario tipico (goblin,
esqueleto, dragon) rembg es lo correcto.
Por que cuerpo entero y encuadre cuadrado: un enemigo de juego se muestra completo
(no recortado) para que el motor lo coloque como sprite/token; el scaffold empuja a
"full body, centered, plain background, game asset" y el negativo por defecto
rechaza "cropped / cut off / out of frame / multiple creatures" para mantener UNA
criatura entera y recortable.
Por que `variant` y no meterlo en `creature`: separar la criatura base de su
variante (ice/fire/elite/corrupted) deja generar una familia coherente — el mismo
`creature` + `style` + `seed` con distintos `variant` da el set de variantes del
mismo enemigo. Si variant esta vacio, el prompt usa solo la criatura base.
class_types/inputs verificados contra /object_info del servidor (8GB lowvram):
CheckpointLoaderSimple, CLIPTextEncode, EmptyLatentImage, KSampler, VAEDecode,
SaveImage, LoraLoader, 'Image Rembg (Remove Background)' (transparency BOOLEAN).
Funcion pura: sin red, sin I/O. No muta dicts de entrada (copia profunda en el
helper de rembg). 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 criaturas/enemigos: UNA criatura entera, bien
# formada, fondo limpio, sin texto/marcas ni recortes. No filtra ningun tipo de
# criatura ni elemento (ice, fire, undead... son validos).
_CREATURE_NEGATIVE = (
"blurry, lowres, deformed, disfigured, bad anatomy, extra limbs, "
"extra heads, fused limbs, mutated, ugly, multiple creatures, crowd, "
"text, watermark, signature, logo, photo, photorealistic, "
"cropped, cut off, out of frame, jpeg artifacts"
)
def _inject_rembg(workflow: dict, model: str) -> dict:
"""Inserta 'Image Rembg (Remove Background)' (transparency=True) entre VAEDecode y SaveImage.
Mismo helper que usan comfyui_build_item_icon_workflow / sprite_sheet: el nodo
recorta la silueta de la criatura dejando alpha. Repunta SaveImage.images a la
salida del Rembg.
"""
wf = copy.deepcopy(workflow)
vaedecode_id = next(
(nid for nid, n in wf.items() if n.get("class_type") == "VAEDecode"), None
)
save_id = next((nid for nid, n in wf.items() if n.get("class_type") == "SaveImage"), None)
if vaedecode_id is None or save_id is None:
raise ValueError(
"comfyui_build_enemy_creature_workflow: no se encontro VAEDecode/SaveImage para Rembg"
)
numeric = [int(k) for k in wf.keys() if str(k).isdigit()]
rembg_id = str((max(numeric) + 1) if numeric else len(wf) + 1)
wf[rembg_id] = {
"class_type": "Image Rembg (Remove Background)",
"inputs": {
"images": [vaedecode_id, 0],
"transparency": True,
"model": model,
"post_processing": False,
"only_mask": False,
"alpha_matting": False,
"alpha_matting_foreground_threshold": 240,
"alpha_matting_background_threshold": 10,
"alpha_matting_erode_size": 10,
"background_color": "none",
},
}
wf[save_id]["inputs"]["images"] = [rembg_id, 0]
return wf
def comfyui_build_enemy_creature_workflow(
creature: str,
*,
variant: str | None = None,
style: str = "game creature, full body",
checkpoint: str = "dreamshaper_8.safetensors",
size: int = 512,
transparent: bool = True,
seed: int = 0,
lora: str | None = None,
lora_strength: float = 1.0,
rembg_model: str = "u2net",
negative: str | None = None,
steps: int = 28,
cfg: float = 7.0,
sampler_name: str = "dpmpp_2m",
scheduler: str = "karras",
filename_prefix: str = "enemy_creature",
) -> dict:
"""Construye el dict (API format) del workflow de un enemigo/criatura de juego.
Args:
creature: descripcion de la criatura/enemigo (ej. "goblin warrior",
"skeleton archer", "green slime", "fire dragon", "orc boss"). Se inserta
en un prompt scaffold de criatura. No puede estar vacio.
variant: variante por nivel o elemento (ej. "ice", "fire", "corrupted",
"elite", "shadow"). Se antepone a la criatura ("ice goblin warrior") para
generar una familia coherente del MISMO enemigo. None/"" = criatura base
sin variante. keyword-only.
style: descriptor de estilo que mantiene consistentes las criaturas de un
bestiario (ej. "game creature, full body", "dark fantasy creature",
"cartoon RPG monster", "pixel art creature"). Pasa el MISMO style +
checkpoint + (lora) a todas las criaturas del set para coherencia visual.
keyword-only.
checkpoint: checkpoint del servidor. 'dreamshaper_8.safetensors' (SD1.5,
holgado en 8GB lowvram) por defecto; 'juggernaut_xl_v11.safetensors'
para SDXL (mas VRAM, subir size a 768/1024). keyword-only.
size: lado del cuadrado en px (width = height = size). 512 SD1.5 por
defecto. keyword-only.
transparent: si True inyecta Image Rembg y el PNG sale con alpha (fondo
recortado, listo como sprite/token). Si False deja la criatura opaca
sobre fondo plano, recortable luego por el caller/pipeline. keyword-only.
seed: semilla del KSampler. Misma seed + misma criatura/style -> misma
figura; variar solo `variant` da las variantes coherentes. keyword-only.
lora: LoRA de estilo/criatura opcional en models/loras (ej.
'dark_fantasy_sd15.safetensors', 'monster_design_xl.safetensors'). None =
sin LoRA. keyword-only.
lora_strength: fuerza del LoRA sobre model y clip. Se clampa a [0.0, 2.0].
keyword-only.
rembg_model: modelo Rembg ('u2net' general, 'isnet-anime' para anime). Solo
se usa si transparent=True. keyword-only.
negative: prompt negativo. None usa el negativo por defecto pensado para
criaturas (una criatura entera, fondo limpio, sin texto/recorte).
keyword-only.
steps, cfg, sampler_name, scheduler, filename_prefix: parametros de
generacion. keyword-only.
Returns:
dict en API format listo para comfyui_submit_workflow: txt2img base cuadrada
con prompt scaffold de criatura ('{variant} {creature}, {style}, full body,
centered, plain background, game asset, ...') + LoRA de estilo opcional +
Image Rembg (si transparent). Es UNA criatura; un bestiario -> llamar por
creature con el mismo style/checkpoint/(lora); las variantes de un enemigo ->
misma creature/seed variando `variant`. Montar el set con comfyui_build_grid
si se quiere un contact-sheet.
Raises:
ValueError: si creature esta vacio, o si la base no tiene VAEDecode/SaveImage
donde inyectar el Rembg (propagado por el helper).
"""
from ml.comfyui_build_txt2img_workflow import comfyui_build_txt2img_workflow
if not creature or not creature.strip():
raise ValueError(
"comfyui_build_enemy_creature_workflow: 'creature' no puede estar vacio"
)
creature = creature.strip()
variant = (variant or "").strip()
lora_strength = max(0.0, min(2.0, float(lora_strength)))
neg = _CREATURE_NEGATIVE if negative is None else negative
# Variante (ice/fire/elite...) antepuesta a la criatura base.
subject = f"{variant} {creature}" if variant else creature
# Prompt scaffold de criatura: una criatura entera, centrada, fondo plano,
# listo como asset de juego (sprite/token) recortable.
positive = (
f"{subject}, {style}, full body, centered, plain background, "
"game asset, single creature, full body shot, high detail"
)
wf = comfyui_build_txt2img_workflow(
checkpoint,
positive,
neg,
steps=steps,
cfg=cfg,
width=size,
height=size,
seed=seed,
sampler_name=sampler_name,
scheduler=scheduler,
filename_prefix=filename_prefix,
)
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
)
if transparent:
wf = _inject_rembg(wf, rembg_model)
return wf
if __name__ == "__main__":
import json
wf = comfyui_build_enemy_creature_workflow(
"goblin warrior",
variant="ice",
style="dark fantasy creature, full body",
transparent=True,
seed=7,
)
print(
json.dumps(
{
"nodes": list(wf),
"classes": sorted({n["class_type"] for n in wf.values()}),
},
indent=2,
)
)
@@ -0,0 +1,113 @@
"""Tests offline de comfyui_build_enemy_creature_workflow (estructura del dict, sin GPU)."""
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from ml.comfyui_build_enemy_creature_workflow import ( # noqa: E402
comfyui_build_enemy_creature_workflow,
)
def _classes(wf):
return sorted({n["class_type"] for n in wf.values()})
def _by_class(wf, cls):
return [n for n in wf.values() if n["class_type"] == cls]
def _id_of(wf, cls):
return next(nid for nid, n in wf.items() if n["class_type"] == cls)
def _pos_with(wf, needle):
return next(
n for n in wf.values()
if n["class_type"] == "CLIPTextEncode" and needle in n["inputs"]["text"]
)
def test_golden_transparent_recipe():
wf = comfyui_build_enemy_creature_workflow(
"goblin warrior", variant="ice", transparent=True, seed=7
)
cls = _classes(wf)
# Cadena base txt2img + Rembg para alpha.
assert "CheckpointLoaderSimple" in cls
assert "KSampler" in cls
assert "VAEDecode" in cls
assert "SaveImage" in cls
assert "Image Rembg (Remove Background)" in cls
# La criatura + variante aparecen en el prompt positivo con el scaffold de criatura.
pos = _pos_with(wf, "goblin warrior")
assert "ice" in pos["inputs"]["text"]
assert "full body" in pos["inputs"]["text"]
assert "centered" in pos["inputs"]["text"]
assert "game asset" in pos["inputs"]["text"]
# SaveImage toma la imagen del Rembg (no del VAEDecode).
rembg_id = _id_of(wf, "Image Rembg (Remove Background)")
save = next(n for n in wf.values() if n["class_type"] == "SaveImage")
assert save["inputs"]["images"] == [rembg_id, 0]
assert _by_class(wf, "Image Rembg (Remove Background)")[0]["inputs"]["transparency"] is True
def test_edge_opaque_no_rembg():
wf = comfyui_build_enemy_creature_workflow("skeleton archer", transparent=False)
assert "Image Rembg (Remove Background)" not in _classes(wf)
# SaveImage toma del VAEDecode directamente.
vd_id = _id_of(wf, "VAEDecode")
save = next(n for n in wf.values() if n["class_type"] == "SaveImage")
assert save["inputs"]["images"] == [vd_id, 0]
def test_edge_size_reflected():
wf = comfyui_build_enemy_creature_workflow("fire dragon", size=768)
latent = _by_class(wf, "EmptyLatentImage")[0]["inputs"]
assert latent["width"] == 768
assert latent["height"] == 768 # cuadrado
def test_edge_variant_optional():
# Sin variante, la criatura base aparece sin prefijo de variante.
wf = comfyui_build_enemy_creature_workflow("green slime", transparent=False)
pos = _pos_with(wf, "green slime")
txt = pos["inputs"]["text"]
# El scaffold arranca directamente con la criatura (sin variante delante).
assert txt.startswith("green slime")
def test_edge_style_in_prompt():
wf = comfyui_build_enemy_creature_workflow(
"orc boss", style="cartoon RPG monster", transparent=False
)
pos = _pos_with(wf, "orc boss")
assert "cartoon RPG monster" in pos["inputs"]["text"]
def test_edge_lora_reflected():
wf = comfyui_build_enemy_creature_workflow(
"shadow wraith", lora="dark_fantasy_sd15.safetensors", lora_strength=0.9
)
loras = _by_class(wf, "LoraLoader")
assert len(loras) == 1
assert loras[0]["inputs"]["lora_name"] == "dark_fantasy_sd15.safetensors"
assert loras[0]["inputs"]["strength_model"] == 0.9
def test_error_empty_creature():
try:
comfyui_build_enemy_creature_workflow(" ")
assert False
except ValueError as e:
assert "creature" in str(e)
def test_determinism():
a = comfyui_build_enemy_creature_workflow(
"fire dragon", variant="elite", lora="monster_design_xl.safetensors", seed=7
)
b = comfyui_build_enemy_creature_workflow(
"fire dragon", variant="elite", lora="monster_design_xl.safetensors", seed=7
)
assert a == b