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:
@@ -44,6 +44,7 @@ VFX (ver `reports/0143`).
|
|||||||
| `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. |
|
| `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. |
|
||||||
| `comfyui_build_prop_object_workflow_py_ml` | `(prop, *, style="game prop, isometric or side view", checkpoint="dreamshaper_8…", size=512, transparent=True, seed=0, lora=None, …) -> dict` | UN prop/objeto de escenario (barril, cofre, antorcha, planta, mueble, roca, fuente, estatua): objeto inanimado aislado a **escala de escena y perspectiva de juego** (iso/lateral), centrado, fondo limpio recortable a alpha (`{prop}, {style}, game asset, single object, centered, plain background, scene prop, world object…`) → txt2img cuadrado + LoRA estilo opcional + Rembg (alpha). **Objeto de MUNDO**, no icono plano de inventario (≠ `item_icon`, que es para una casilla de UI); este puebla el nivel. Atrezzo coherente = mismo `style`/`checkpoint`/`lora`, varía solo `prop`. El negativo excluye personas/criaturas (objeto inanimado). Probado e2e en GPU con SD1.5 (`reports/0155`). SD1.5. |
|
| `comfyui_build_prop_object_workflow_py_ml` | `(prop, *, style="game prop, isometric or side view", checkpoint="dreamshaper_8…", size=512, transparent=True, seed=0, lora=None, …) -> dict` | UN prop/objeto de escenario (barril, cofre, antorcha, planta, mueble, roca, fuente, estatua): objeto inanimado aislado a **escala de escena y perspectiva de juego** (iso/lateral), centrado, fondo limpio recortable a alpha (`{prop}, {style}, game asset, single object, centered, plain background, scene prop, world object…`) → txt2img cuadrado + LoRA estilo opcional + Rembg (alpha). **Objeto de MUNDO**, no icono plano de inventario (≠ `item_icon`, que es para una casilla de UI); este puebla el nivel. Atrezzo coherente = mismo `style`/`checkpoint`/`lora`, varía solo `prop`. El negativo excluye personas/criaturas (objeto inanimado). Probado e2e en GPU con SD1.5 (`reports/0155`). SD1.5. |
|
||||||
|
| `comfyui_build_topdown_sprite_workflow_py_ml` | `(subject, *, direction="south", style="top-down game sprite, RPG", checkpoint="dreamshaper_8…", size=512, transparent=True, seed=0, lora=None, …) -> dict` | UN sprite en **vista CENITAL (top-down)** estilo RPG clásico/roguelike (Zelda, juegos cenitales): personaje/objeto visto **desde arriba**, centrado, fondo limpio recortable a alpha (`{subject}, top-down view, overhead view, {direction} facing, {style}, centered, plain background, game asset…`) → txt2img cuadrado + LoRA estilo opcional + Rembg (alpha). `direction` (south/north/east/west) para el sprite de movimiento: las 4 vistas del MISMO personaje = misma `subject`/`style`/`seed`, varía solo `direction` → montar con `comfyui_build_grid`. **DISTINTO de `sprite_sheet` (vista lateral/frontal de plataformas)**: el negativo por defecto rechaza side/front/3-4/isometric/perspective para forzar la cenital. Con SD1.5 sin LoRA sale picado alto; cenital estricto pide LoRA top-down + cfg alto. Probado e2e en GPU con SD1.5 (`reports/0156`). SD1.5. |
|
||||||
|
|
||||||
## Funciones de post-proceso y puente (`gamedev`, CPU)
|
## Funciones de post-proceso y puente (`gamedev`, CPU)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in New Issue
Block a user