feat(gamedev): comfyui_build_prop_object_workflow — props/objetos de escenario (objeto de mundo, alpha, perspectiva de juego)

Builder PURO del grupo gamedev: dict API format de un prop/objeto de escenario
(barril, cofre, antorcha, planta, mueble, roca, fuente, estatua). Compone
comfyui_build_txt2img_workflow + comfyui_inject_lora opcional + Image Rembg.
Diferenciado de item_icon: objeto de MUNDO (escala de escena, perspectiva
iso/lateral) vs icono plano de inventario. 10 tests offline verdes; 1 generacion
real en GPU (cofre del tesoro, RGBA 512x512, fondo recortado). reports/0155.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-26 23:36:21 +02:00
parent 2ff111bae4
commit 0c1d2aa4fc
4 changed files with 483 additions and 0 deletions
+1
View File
@@ -43,6 +43,7 @@ VFX (ver `reports/0143`).
| `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. | | `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. |
## Funciones de post-proceso y puente (`gamedev`, CPU) ## Funciones de post-proceso y puente (`gamedev`, CPU)
@@ -0,0 +1,112 @@
---
name: comfyui_build_prop_object_workflow
kind: function
lang: py
domain: ml
version: "1.0.0"
purity: pure
signature: "def comfyui_build_prop_object_workflow(prop: str, *, style: str = \"game prop, isometric or side view\", 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 = \"prop_object\") -> dict"
description: "Construye el dict (API format) del workflow de UN prop/objeto de escenario de juego 2D (barril, cofre, antorcha, planta, mueble, roca, fuente, estatua, decoracion): UN objeto inanimado aislado a escala de escena y perspectiva de juego (isometrica/lateral), centrado, fondo limpio uniforme recortable a alpha, estilo consistente para poblar un nivel. Diferenciado de comfyui_build_item_icon (objeto de MUNDO vs icono plano de inventario). Compone comfyui_build_txt2img_workflow + comfyui_inject_lora (estilo opcional) + Image Rembg (fondo transparente si transparent). Hermano de comfyui_build_enemy_creature/item_icon/ui_hud_workflow. Pura, sin red ni I/O. class_types verificados contra /object_info."
tags: [comfyui, ml, gamedev, gamedev-2d, prop, object, scenery, environment, 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: prop
desc: "Descripcion del objeto de escenario (ej. 'wooden barrel', 'treasure chest', 'lit torch', 'potted plant', 'stone fountain', 'wooden table', 'mossy rock'). Se inserta en un prompt scaffold de prop. No puede estar vacio."
- name: style
desc: "Descriptor de estilo/perspectiva que mantiene consistentes los props del set y los situa en el MUNDO, no en la UI (ej. 'game prop, isometric or side view', 'top-down RPG prop', 'side-scroller platformer prop', 'low poly stylized prop'). Pasa el MISMO style + checkpoint + lora a todos los props del nivel 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 soltar sobre el tilemap). False = objeto opaco sobre fondo plano, recortable luego por el caller. keyword-only."
- name: seed
desc: "Semilla del KSampler. Misma seed + mismo prop/style -> mismo objeto. keyword-only."
- name: lora
desc: "LoRA de estilo opcional en models/loras (ej. 'isometric_game_assets_sd15.safetensors', 'stylized_props_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 props (un objeto inanimado entero, sin personas/criaturas, fondo limpio, sin texto/recorte). 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 prop ('{prop}, {style}, game asset, single object, centered, plain background, scene prop, world object, ...') + LoRA de estilo opcional + Image Rembg (si transparent). UN objeto; para poblar un nivel -> llamar por cada prop con mismo style/checkpoint/lora; contact-sheet del atrezzo -> montar los PNG con comfyui_build_grid."
tested: true
tests: ["golden transparent: clases CheckpointLoaderSimple/KSampler/VAEDecode/SaveImage/Image Rembg; prop + 'single object' + 'centered' + 'game asset' + 'plain background' en prompt; SaveImage <- Rembg; transparency True", "edge transparent=False: sin Rembg, SaveImage <- VAEDecode", "edge size: width==height==768 (cuadrado)", "edge prop al inicio del scaffold", "edge style de mundo en prompt", "edge objeto de mundo (scene prop/world object) y negativo anti-persona/criatura (diferenciado de item_icon)", "edge lora: LoraLoader presente con strength", "edge transparent default True", "error prop vacio -> ValueError", "determinismo"]
test_file_path: "python/functions/ml/comfyui_build_prop_object_workflow_test.py"
file_path: "python/functions/ml/comfyui_build_prop_object_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_prop_object_workflow import comfyui_build_prop_object_workflow
# Un prop de escenario con fondo transparente (alpha), listo para soltar en el nivel.
wf = comfyui_build_prop_object_workflow(
"wooden treasure chest",
style="game prop, isometric or side view",
transparent=True,
seed=7,
)
# Poblar un nivel: variar `prop` con el MISMO style/checkpoint/(lora) para coherencia.
# for p in ["wooden barrel", "treasure chest", "lit torch", "potted plant", "mossy rock"]:
# wf = comfyui_build_prop_object_workflow(p, style="game prop, isometric or side view", seed=7)
# comfyui_submit_workflow(wf) # -> comfyui_wait_result -> comfyui_fetch_output_image
# Contact-sheet del atrezzo: montar los PNG resultantes con comfyui_build_grid.
```
O lanzable directo con: `./fn run comfyui_build_prop_object_workflow` (imprime nodos + class_types del ejemplo).
## Cuando usarla
Cuando necesites atrezzo/decorado de escenario para un juego (RPG, plataformas,
top-down, isometrico): barriles, cofres, antorchas, plantas, muebles, rocas,
fuentes, estatuas, decoracion. A diferencia de `comfyui_build_item_icon_workflow`
(icono PLANO de inventario para una casilla de UI), aqui el objeto es de MUNDO:
conserva la perspectiva del juego (isometrica/lateral) y la escala de la escena para
encajar con los tiles. Pasa el MISMO `style` + `checkpoint` + (`lora`) a todos los
props del nivel para que combinen visualmente; varia solo `prop`. `transparent`
recorta el fondo (alpha) listo para soltar sobre el tilemap. Para un atlas/contact-
sheet del atrezzo, genera cada prop y monta los PNG con `comfyui_build_grid`.
## Gotchas
- **Prop (objeto de mundo) != item icon (icono de inventario)**: si lo que quieres es
un icono cuadrado plano para una casilla de UI, usa
`comfyui_build_item_icon_workflow`. Este builder situa el objeto en la ESCENA
(perspectiva isometrica/lateral, escala de mundo) para poblar el nivel.
- **El recorte usa Rembg, NO luma-to-alpha**: un prop es un objeto solido con silueta
definida, rembg lo recorta limpio. `comfyui_matting_luma_to_alpha` es para
translucidos sobre negro (humo/fuego/magia). Si el prop es etereo o translucido
(cristal magico, llama suelta), 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 props, el atrezzo deja de combinar. Fija esos y varia solo `prop`.
- **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 una persona/criatura o varios objetos, el negativo por defecto ya
empuja a "single object / no characters / no cropped"; refuerza `style` con
"isolated object, single item, no people" si insiste.
- `transparent=False` deja el objeto 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,235 @@
"""Construye el workflow ComfyUI de UN prop/objeto de escenario de juego (API format).
Prop/objeto de mundo (barril, cofre, antorcha, planta, mueble, roca, fuente,
estatua, decoracion...): UN objeto inanimado aislado, a escala de escena y con la
perspectiva del juego (isometrica o lateral), centrado, fondo limpio y uniforme
recortable a alpha, estilo consistente para poblar un nivel. Es el builder hermano
de comfyui_build_item_icon_workflow / comfyui_build_enemy_creature_workflow /
comfyui_build_ui_hud_workflow: mismo patron (PURO, dict API format) que compone
funciones existentes del registry, no reescribe el grafo.
Diferencia con item_icon (clave para no duplicar): un *item icon* es un icono PLANO
de inventario (encuadre cuadrado, sujeto presentado de frente, pensado para una
casilla de UI). Un *prop* es un objeto de MUNDO: se coloca en el escenario, conserva
la perspectiva del juego (isometrica/lateral) y la escala de la escena para encajar
con los tiles y el resto de decorado. Por eso el style por defecto empuja
"game prop, isometric or side view, scene prop" en vez de "game icon, clean".
Cableado:
CheckpointLoaderSimple -> [LoraLoader opcional de estilo] -> KSampler
-> CLIPTextEncode (prompt scaffold de prop) ...
-> VAEDecode -> [Image Rembg opcional] -> SaveImage
Compone:
- comfyui_build_txt2img_workflow -> base txt2img cuadrada
- comfyui_inject_lora -> LoRA de estilo opcional (consistencia del set)
- 'Image Rembg (Remove Background)' (helper local) -> fondo transparente (alpha)
Por que Rembg y NO comfyui_matting_luma_to_alpha: un prop es un objeto SOLIDO con
silueta definida (barril, cofre, mueble); rembg recorta limpio la silueta dejando
alpha, listo para soltar sobre el tilemap. La luma-to-alpha es para translucidos
sobre negro (humo/fuego/magia), donde aplanaria el objeto. Si el prop es etereo o
translucido (cristal magico, llama de antorcha sola) y se quiere conservar la
translucidez, recortar fuera del workflow (transparent=False) y componer con
luma-to-alpha en un paso aparte. Para el atrezzo tipico (barril, cofre, roca,
planta) rembg es lo correcto.
Por que un solo objeto centrado y fondo plano: un prop se inserta como sprite/objeto
suelto en el motor; el scaffold empuja a "single object, centered, plain background,
game asset" y el negativo por defecto rechaza "person, character, creature, multiple
objects, cropped, out of frame" para mantener UN objeto entero, inanimado y
recortable.
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 props/objetos de escenario: UN objeto inanimado
# entero, fondo limpio, sin personas/criaturas, sin texto/marcas ni recortes. No
# filtra ningun tipo de objeto (barril, cofre, planta, roca, fuente... son validos).
_PROP_NEGATIVE = (
"person, people, character, creature, animal, hands, face, "
"multiple objects, cluttered, blurry, lowres, deformed, bad perspective, "
"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 / enemy_creature: el nodo
recorta la silueta del objeto 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_prop_object_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_prop_object_workflow(
prop: str,
*,
style: str = "game prop, isometric or side view",
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 = "prop_object",
) -> dict:
"""Construye el dict (API format) del workflow de un prop/objeto de escenario.
Args:
prop: descripcion del objeto de escenario (ej. "wooden barrel",
"treasure chest", "lit torch", "potted plant", "stone fountain",
"wooden table", "mossy rock"). Se inserta en un prompt scaffold de prop.
No puede estar vacio.
style: descriptor de estilo/perspectiva que mantiene consistentes los props
del set y los situa en el MUNDO, no en la UI (ej. "game prop, isometric
or side view", "top-down RPG prop", "side-scroller platformer prop",
"low poly stylized prop"). Pasa el MISMO style + checkpoint + (lora) a
todos los props del nivel 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 soltar sobre el tilemap). Si False deja el objeto
opaco sobre fondo plano, recortable luego por el caller/pipeline.
keyword-only.
seed: semilla del KSampler. Misma seed + mismo prop/style -> mismo objeto.
keyword-only.
lora: LoRA de estilo opcional en models/loras (ej.
'isometric_game_assets_sd15.safetensors', 'stylized_props_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
props (un objeto inanimado entero, sin personas/criaturas, 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 prop ('{prop}, {style}, game asset, single object,
centered, plain background, ...') + LoRA de estilo opcional + Image Rembg (si
transparent). Es UN objeto; para poblar un nivel -> llamar por cada prop con
el mismo style/checkpoint/(lora). Montar el set con comfyui_build_grid si se
quiere un contact-sheet del atrezzo.
Raises:
ValueError: si prop 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 prop or not prop.strip():
raise ValueError(
"comfyui_build_prop_object_workflow: 'prop' no puede estar vacio"
)
prop = prop.strip()
lora_strength = max(0.0, min(2.0, float(lora_strength)))
neg = _PROP_NEGATIVE if negative is None else negative
# Prompt scaffold de prop: un objeto de mundo entero, centrado, fondo plano,
# listo como asset de juego suelto (sprite/objeto) recortable.
positive = (
f"{prop}, {style}, game asset, single object, centered, plain background, "
"scene prop, world object, no characters, 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_prop_object_workflow(
"wooden treasure chest",
style="game prop, isometric or side view",
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,135 @@
"""Tests offline de comfyui_build_prop_object_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_prop_object_workflow import ( # noqa: E402
comfyui_build_prop_object_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_prop_object_workflow(
"wooden treasure chest", 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
# El prop aparece en el prompt positivo con el scaffold de objeto de mundo.
pos = _pos_with(wf, "wooden treasure chest")
txt = pos["inputs"]["text"]
assert "single object" in txt
assert "centered" in txt
assert "game asset" in txt
assert "plain background" 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_edge_opaque_no_rembg():
wf = comfyui_build_prop_object_workflow("stone fountain", 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_prop_object_workflow("mossy rock", size=768)
latent = _by_class(wf, "EmptyLatentImage")[0]["inputs"]
assert latent["width"] == 768
assert latent["height"] == 768 # cuadrado
def test_edge_prop_at_start():
# El scaffold arranca directamente con el prop.
wf = comfyui_build_prop_object_workflow("potted plant", transparent=False)
pos = _pos_with(wf, "potted plant")
assert pos["inputs"]["text"].startswith("potted plant")
def test_edge_style_in_prompt():
# Estilo de MUNDO reflejado (diferenciado del icono plano de inventario).
wf = comfyui_build_prop_object_workflow(
"wooden barrel", style="top-down RPG prop", transparent=False
)
pos = _pos_with(wf, "wooden barrel")
assert "top-down RPG prop" in pos["inputs"]["text"]
def test_edge_world_object_not_inventory_icon():
# Por defecto el scaffold lo trata como objeto de mundo (scene prop), no icono UI.
wf = comfyui_build_prop_object_workflow("lit torch", transparent=False)
txt = _pos_with(wf, "lit torch")["inputs"]["text"]
assert "scene prop" in txt
assert "world object" in txt
# El negativo por defecto excluye personas/criaturas (objeto inanimado).
neg = next(
n for n in wf.values()
if n["class_type"] == "CLIPTextEncode" and "person" in n["inputs"]["text"]
)
assert "creature" in neg["inputs"]["text"]
def test_edge_lora_reflected():
wf = comfyui_build_prop_object_workflow(
"ornate vase",
lora="isometric_game_assets_sd15.safetensors",
lora_strength=0.9,
)
loras = _by_class(wf, "LoraLoader")
assert len(loras) == 1
assert loras[0]["inputs"]["lora_name"] == "isometric_game_assets_sd15.safetensors"
assert loras[0]["inputs"]["strength_model"] == 0.9
def test_edge_transparent_default_true():
# transparent por defecto True -> Rembg presente sin pasar el flag.
wf = comfyui_build_prop_object_workflow("treasure chest")
assert "Image Rembg (Remove Background)" in _classes(wf)
def test_error_empty_prop():
try:
comfyui_build_prop_object_workflow(" ")
assert False
except ValueError as e:
assert "prop" in str(e)
def test_determinism():
a = comfyui_build_prop_object_workflow(
"wooden barrel", lora="stylized_props_xl.safetensors", seed=7
)
b = comfyui_build_prop_object_workflow(
"wooden barrel", lora="stylized_props_xl.safetensors", seed=7
)
assert a == b