feat(gamedev): comfyui_build_topdown_sprite_workflow — sprite vista cenital (top-down RPG, direccion, alpha)

Builder puro (dict API format) del grupo gamedev: sprite de personaje/objeto en
vista cenital (top-down) estilo RPG clasico/roguelike, visto desde arriba,
centrado, fondo limpio recortable a alpha. Argumento direction (south/north/east/
west) para el set de sprites de movimiento. Compone comfyui_build_txt2img_workflow
+ comfyui_inject_lora (estilo opcional) + Image Rembg (alpha). Diferenciado de
comfyui_build_sprite_sheet_workflow (vista lateral/frontal): el negativo por
defecto rechaza side/front/isometric/perspective para forzar la cenital.

Probado e2e en GPU con SD1.5 (8GB lowvram): caballero cenital, fondo transparente
(reports/0157). 10 tests offline verdes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-26 23:49:24 +02:00
parent 0c1d2aa4fc
commit 8fb10fdf8a
4 changed files with 507 additions and 0 deletions
@@ -0,0 +1,121 @@
---
name: comfyui_build_topdown_sprite_workflow
kind: function
lang: py
domain: ml
version: "1.0.0"
purity: pure
signature: "def comfyui_build_topdown_sprite_workflow(subject: str, *, direction: str = \"south\", style: str = \"top-down game sprite, RPG\", 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 = \"topdown_sprite\") -> dict"
description: "Construye el dict (API format) del workflow de UN sprite en VISTA CENITAL (top-down) 2D: personaje/objeto visto desde arriba estilo RPG clasico/roguelike (Zelda, juegos cenitales), centrado, fondo limpio uniforme recortable a alpha, listo para un mapa de tiles top-down. Opcion `direction` (south/north/east/west) para el sprite de movimiento: las 4 vistas = misma subject/style/seed variando solo direction. DISTINTO de comfyui_build_sprite_sheet_workflow (ese es vista LATERAL/frontal de plataformas) — el negativo por defecto rechaza side/front/isometric/perspective para forzar la vista cenital. Compone comfyui_build_txt2img_workflow + comfyui_inject_lora (estilo opcional) + Image Rembg (fondo transparente si transparent). Hermano de comfyui_build_enemy_creature/prop_object/item_icon_workflow. Pura, sin red ni I/O. class_types verificados contra /object_info."
tags: [comfyui, ml, gamedev, gamedev-2d, topdown, top-down, overhead, sprite, rpg, roguelike, 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: subject
desc: "Descripcion del personaje/objeto (ej. 'a knight character', 'a wizard', 'a treasure chest', 'a slime'). Se inserta en un prompt scaffold cenital. No puede estar vacio."
- name: direction
desc: "Direccion de encare para el sprite de movimiento. 'south' (mirando a la camara/abajo, el frame por defecto de un sprite RPG), 'north', 'east', 'west' (tambien diagonales como 'south-east'). Las 4 vistas de caminata del MISMO personaje = misma subject/style/seed variando solo direction. None/'' = encare neutro sin direccion. keyword-only."
- name: style
desc: "Descriptor de estilo que mantiene consistentes los sprites del set (ej. 'top-down game sprite, RPG', 'pixel art top-down', 'Zelda-like overhead sprite', 'roguelike tile character'). Pasa el MISMO style + checkpoint + lora a todos los sprites 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 para colocar sobre un mapa de tiles). False = sprite opaco sobre fondo plano, recortable luego por el caller. keyword-only."
- name: seed
desc: "Semilla del KSampler. Misma seed + misma subject/style -> misma figura; variar solo `direction` da las vistas coherentes de movimiento. keyword-only."
- name: lora
desc: "LoRA de estilo/top-down opcional en models/loras (ej. 'topdown_rpg_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: 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 vista cenital (figura entera vista desde arriba, fondo limpio, sin vistas laterales/frontales/isometricas que delatarian otra perspectiva). 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 cenital ('{subject}, top-down view, overhead view, {direction} facing, {style}, centered, plain background, game asset, ...') + LoRA de estilo opcional + Image Rembg (si transparent). UN sprite; las 4 direcciones de un personaje -> misma subject/style/seed variando `direction`; montar el set con comfyui_build_grid."
tested: true
tests: ["golden transparent: clases CheckpointLoaderSimple/KSampler/VAEDecode/SaveImage/Image Rembg; subject + 'top-down view' + 'overhead view' + 'south facing' + 'centered' + 'game asset' en prompt; SaveImage <- Rembg; transparency True", "edge negativo cenital: rechaza side view/front view/isometric/perspective (diferenciacion del builder lateral)", "edge direction reflejada: north/east/west aparecen como '{d} facing'", "edge direction opcional: sin direction no hay 'facing' pero sigue 'top-down view'", "edge transparent=False: sin Rembg, SaveImage <- VAEDecode", "edge size: width==height==768 (cuadrado)", "edge style en prompt", "edge lora: LoraLoader presente con strength", "error subject vacio -> ValueError", "determinismo"]
test_file_path: "python/functions/ml/comfyui_build_topdown_sprite_workflow_test.py"
file_path: "python/functions/ml/comfyui_build_topdown_sprite_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_topdown_sprite_workflow import comfyui_build_topdown_sprite_workflow
# Un sprite cenital (vista desde arriba) con fondo transparente (alpha), listo para submit.
wf = comfyui_build_topdown_sprite_workflow(
"a knight character",
direction="south",
style="top-down game sprite, RPG",
transparent=True,
seed=5,
)
# Las 4 direcciones de movimiento del MISMO personaje: misma subject/style/seed, cambia direction.
# for d in ["south", "north", "east", "west"]:
# wf = comfyui_build_topdown_sprite_workflow("a knight character", direction=d,
# style="top-down game sprite, RPG", seed=5)
# comfyui_submit_workflow(wf) # -> comfyui_wait_result -> comfyui_fetch_output_image
# Atlas de las 4 direcciones: montar los PNG resultantes con comfyui_build_grid.
```
O lanzable directo con: `./fn run comfyui_build_topdown_sprite_workflow` (imprime nodos + class_types del ejemplo).
## Cuando usarla
Cuando necesites sprites de un juego en VISTA CENITAL (top-down) — RPG clasico tipo
Zelda, roguelike, twin-stick, juego de mapa de tiles donde la camara mira el suelo en
picado: personajes, NPCs, objetos del mapa vistos desde arriba. Usa `direction` para
generar el set de movimiento (south/north/east/west) del mismo personaje fijando
subject/style/seed y variando solo la direccion. Pasa el MISMO `style` + `checkpoint`
+ (`lora`) a todos los sprites del juego para que combinen. `transparent` recorta el
fondo (alpha) listo para colocar sobre el mapa. Para un atlas/contact-sheet de las
direcciones, genera cada vista y monta los PNG con `comfyui_build_grid`.
**Elige este builder y NO `comfyui_build_sprite_sheet_workflow` cuando** el juego es
cenital (cámara desde arriba). El sprite_sheet es para vista LATERAL/frontal (sprite de
plataformas, de perfil o de frente). Aquí la figura se proyecta sobre el suelo y se ve
desde lo alto.
## Gotchas
- **Vista cenital vs lateral**: este builder fuerza "top-down view, overhead view" en el
positivo y rechaza "side view / front view / 3/4 view / isometric / perspective" en el
negativo por defecto. Si el modelo aun saca una vista lateral o frontal, refuerza
`style` con "strict top-down, camera directly overhead, character flat on the ground" y
sube `cfg`. La vista cenital es la mas dificil de imponer a SD1.5; con un `lora`
top-down/RPG sale mucho mas fiable.
- **El recorte usa Rembg, NO luma-to-alpha**: un sprite cenital es un sujeto solido con
silueta definida, rembg lo recorta limpio. `comfyui_matting_luma_to_alpha` es para
translucidos sobre negro (humo/fuego/magia). Si el sprite es un fantasma etereo y
quieres conservar translucidez, pon `transparent=False` y recorta con luma-to-alpha en
un paso aparte.
- **Coherencia del set = mismos parametros**: si cambias `style`/`checkpoint`/`lora`/`seed`
entre direcciones, las 4 vistas dejan de combinar. Fija esos y varia solo `direction`.
- **`direction` se inserta como "{direction} facing"**: `direction="east"` ->
"east facing" en el prompt. Deja `direction=""`/None para un sprite sin direccion fija.
- **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.
- `transparent=False` deja el sprite opaco 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,248 @@
"""Construye el workflow ComfyUI de UN sprite en VISTA CENITAL (top-down) (API format).
Sprite de personaje/objeto visto DESDE ARRIBA, estilo RPG clasico / roguelike
(Zelda, juegos cenitales): la camara mira el suelo en picado y el sujeto se ve
desde lo alto, centrado, sobre fondo limpio uniforme recortable a alpha, listo para
colocar en un mapa de tiles top-down. Opcion de `direction` (south/north/east/west)
para el sprite de movimiento: el conjunto de 4 direcciones de un personaje se obtiene
llamando con la misma `subject`/`style`/`seed` variando solo `direction`, y montando
los PNG con `comfyui_build_grid`.
DISTINTO de comfyui_build_sprite_sheet_workflow: ese es vista LATERAL/frontal (sprite
de plataformas, cuerpo entero de perfil/de frente con OpenPose). Este es vista CENITAL
(personaje proyectado en el suelo, visto desde arriba). El scaffold empuja a
"top-down view, overhead view" y el negativo por defecto rechaza "side view, front
view, isometric, perspective" precisamente para no caer en la vista lateral del builder
hermano.
Es el builder hermano de comfyui_build_enemy_creature_workflow /
comfyui_build_prop_object_workflow / comfyui_build_item_icon_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 top-down) ...
-> VAEDecode -> [Image Rembg opcional] -> SaveImage
Compone:
- comfyui_build_txt2img_workflow -> base txt2img cuadrada
- comfyui_inject_lora -> LoRA de estilo top-down/RPG opcional (consistencia)
- 'Image Rembg (Remove Background)' (helper local) -> fondo transparente (alpha)
Por que Rembg y NO comfyui_matting_luma_to_alpha: un sprite cenital es un sujeto
SOLIDO con silueta definida; rembg recorta limpio dejando alpha. La luma-to-alpha es
para translucidos sobre negro (humo/fuego/magia), donde aplanaria el sprite. Para el
sprite top-down tipico (personaje, objeto del mapa) rembg es lo correcto.
Por que `direction` y no meterlo en `subject`: separar el sujeto de la direccion de
encare deja generar el set de movimiento del MISMO personaje — la misma `subject` +
`style` + `seed` con distintos `direction` (south/north/east/west) da las 4 vistas de
caminata coherentes. Con `direction` vacio el prompt usa solo "facing" neutro.
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 sprites cenitales: UNA figura entera vista desde
# arriba, fondo limpio, sin texto/marcas ni recortes. Rechaza explicitamente las vistas
# que pertenecen al builder LATERAL (side/front/3/4/isometric/perspective) para que la
# vista cenital salga de verdad.
_TOPDOWN_NEGATIVE = (
"blurry, lowres, deformed, disfigured, bad anatomy, extra limbs, mutated, ugly, "
"side view, front view, 3/4 view, isometric, perspective, vanishing point, "
"multiple characters, crowd, text, watermark, signature, logo, "
"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_enemy_creature_workflow / item_icon: el nodo
recorta la silueta del sprite 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_topdown_sprite_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_topdown_sprite_workflow(
subject: str,
*,
direction: str = "south",
style: str = "top-down game sprite, RPG",
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 = "topdown_sprite",
) -> dict:
"""Construye el dict (API format) del workflow de UN sprite en vista cenital (top-down).
Args:
subject: descripcion del personaje/objeto (ej. "a knight character",
"a wizard", "a treasure chest", "a slime"). Se inserta en un prompt
scaffold cenital. No puede estar vacio.
direction: direccion de encare para el sprite de movimiento. Tipico
"south" (mirando hacia la camara/abajo, el frame por defecto de un
sprite RPG), "north", "east", "west" (tambien diagonales como
"south-east" si el motor las usa). Las 4 vistas de caminata del MISMO
personaje = misma subject/style/seed variando solo direction. None/""
= encare neutro sin direccion. keyword-only.
style: descriptor de estilo que mantiene consistentes los sprites del set
(ej. "top-down game sprite, RPG", "pixel art top-down", "Zelda-like
overhead sprite", "roguelike tile character"). Pasa el MISMO style +
checkpoint + (lora) a todos los sprites 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 para colocar sobre un mapa de tiles). Si False deja el
sprite opaco sobre fondo plano, recortable luego por el caller/pipeline.
keyword-only.
seed: semilla del KSampler. Misma seed + misma subject/style -> misma
figura; variar solo `direction` da las vistas coherentes de movimiento.
keyword-only.
lora: LoRA de estilo/top-down opcional en models/loras (ej.
'topdown_rpg_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.
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
vista cenital (una figura entera vista desde arriba, fondo limpio, sin
vistas laterales/frontales/isometricas que delatarian otra perspectiva).
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 cenital ('{subject}, top-down view, overhead view,
{direction} facing, {style}, centered, plain background, game asset, ...') +
LoRA de estilo opcional + Image Rembg (si transparent). Es UN sprite; las 4
direcciones de un personaje -> misma subject/style/seed variando `direction`;
montar el set con comfyui_build_grid si se quiere un atlas/contact-sheet.
Raises:
ValueError: si subject 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 subject or not subject.strip():
raise ValueError(
"comfyui_build_topdown_sprite_workflow: 'subject' no puede estar vacio"
)
subject = subject.strip()
direction = (direction or "").strip()
lora_strength = max(0.0, min(2.0, float(lora_strength)))
neg = _TOPDOWN_NEGATIVE if negative is None else negative
# Prompt scaffold cenital: una figura entera vista desde arriba, centrada, fondo
# plano, lista como asset top-down recortable. La direccion (si se da) fija el
# encare para el sprite de movimiento.
facing = f"{direction} facing, " if direction else ""
positive = (
f"{subject}, top-down view, overhead view, {facing}{style}, "
"centered, plain background, game asset, single sprite, "
"bird's-eye view, full body from above"
)
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_topdown_sprite_workflow(
"a knight character",
direction="south",
style="top-down game sprite, RPG",
transparent=True,
seed=5,
)
print(
json.dumps(
{
"nodes": list(wf),
"classes": sorted({n["class_type"] for n in wf.values()}),
},
indent=2,
)
)
@@ -0,0 +1,137 @@
"""Tests offline de comfyui_build_topdown_sprite_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_topdown_sprite_workflow import ( # noqa: E402
comfyui_build_topdown_sprite_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_topdown_sprite_workflow(
"a knight character", direction="south", transparent=True, seed=5
)
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
# El sujeto + la vista cenital + la direccion aparecen en el prompt positivo.
pos = _pos_with(wf, "a knight character")
txt = pos["inputs"]["text"]
assert "top-down view" in txt
assert "overhead view" in txt
assert "south facing" in txt
assert "centered" in txt
assert "game asset" in txt
# 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_topdown_negative_rejects_side_view():
# Diferenciacion del builder LATERAL (sprite_sheet): el negativo por defecto
# rechaza explicitamente vista lateral/frontal/isometrica.
wf = comfyui_build_topdown_sprite_workflow("a wizard", transparent=False)
neg = next(
n["inputs"]["text"]
for n in wf.values()
if n["class_type"] == "CLIPTextEncode" and "side view" in n["inputs"]["text"]
)
assert "side view" in neg
assert "front view" in neg
assert "isometric" in neg
assert "perspective" in neg
def test_edge_direction_reflected():
for d in ["north", "east", "west"]:
wf = comfyui_build_topdown_sprite_workflow("a knight character", direction=d)
pos = _pos_with(wf, "a knight character")
assert f"{d} facing" in pos["inputs"]["text"]
def test_edge_direction_optional():
# Sin direccion, el prompt no inserta "facing" pero sigue siendo vista cenital.
wf = comfyui_build_topdown_sprite_workflow("a treasure chest", direction="", transparent=False)
pos = _pos_with(wf, "a treasure chest")
txt = pos["inputs"]["text"]
assert "facing" not in txt
assert "top-down view" in txt
def test_edge_opaque_no_rembg():
wf = comfyui_build_topdown_sprite_workflow("a slime", 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_topdown_sprite_workflow("a knight character", size=768)
latent = _by_class(wf, "EmptyLatentImage")[0]["inputs"]
assert latent["width"] == 768
assert latent["height"] == 768 # cuadrado
def test_edge_style_in_prompt():
wf = comfyui_build_topdown_sprite_workflow(
"a knight character", style="pixel art top-down", transparent=False
)
pos = _pos_with(wf, "a knight character")
assert "pixel art top-down" in pos["inputs"]["text"]
def test_edge_lora_reflected():
wf = comfyui_build_topdown_sprite_workflow(
"a knight character", lora="topdown_rpg_sd15.safetensors", lora_strength=0.9
)
loras = _by_class(wf, "LoraLoader")
assert len(loras) == 1
assert loras[0]["inputs"]["lora_name"] == "topdown_rpg_sd15.safetensors"
assert loras[0]["inputs"]["strength_model"] == 0.9
def test_error_empty_subject():
try:
comfyui_build_topdown_sprite_workflow(" ")
assert False
except ValueError as e:
assert "subject" in str(e)
def test_determinism():
a = comfyui_build_topdown_sprite_workflow(
"a knight character", direction="east", lora="topdown_rpg_sd15.safetensors", seed=5
)
b = comfyui_build_topdown_sprite_workflow(
"a knight character", direction="east", lora="topdown_rpg_sd15.safetensors", seed=5
)
assert a == b