feat(gamedev): comfyui_build_ui_hud_workflow — elementos de UI/HUD (botones/marcos/barras)

Builder puro hermano de comfyui_build_item_icon_workflow: construye el dict (API
format) del workflow de UN elemento de interfaz/HUD de juego (botón, marco/panel,
barra de vida/maná/XP, icono de UI, cursor, viñeta de menú). Pieza única centrada,
fondo limpio recortable a alpha. Compone comfyui_build_txt2img_workflow +
comfyui_inject_lora + Image Rembg.

Tests offline 7/7 verdes (golden + 4 edge + error + determinismo). Generación real
verificada en GPU (8GB lowvram): ornate health bar frame -> PNG 512x512 RGBA con
alpha recortado (reports/0152). Fila añadida en docs/capabilities/gamedev-2d.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-26 23:14:48 +02:00
parent 4c4eec4b1d
commit 0dd2718c95
4 changed files with 417 additions and 0 deletions
+1
View File
@@ -40,6 +40,7 @@ VFX (ver `reports/0143`).
| `comfyui_build_emote_workflow_py_ml` | `(character, expression, *, ref_face=None, style="character portrait", checkpoint="dreamshaper_8…", size=512, facedetailer=True, lora=None, …) -> dict` | UN emote/expresión facial del MISMO personaje (alegre/triste/enfadado/sorprendido/neutral…) para diálogo, retratos reactivos o emotes de chat: txt2img + prompt scaffold de emote (`portrait of {character}, {expression} expression, emote, clean background`) + FaceDetailer (conserva la expresión); `ref_face` → IPAdapter-FaceID para que varíe SOLO la expresión y el rostro sea el mismo. UNA expresión por llamada; set = mismas claves variando `expression``comfyui_build_grid`. Probado e2e en GPU (`reports/0151`). SD1.5. | | `comfyui_build_emote_workflow_py_ml` | `(character, expression, *, ref_face=None, style="character portrait", checkpoint="dreamshaper_8…", size=512, facedetailer=True, lora=None, …) -> dict` | UN emote/expresión facial del MISMO personaje (alegre/triste/enfadado/sorprendido/neutral…) para diálogo, retratos reactivos o emotes de chat: txt2img + prompt scaffold de emote (`portrait of {character}, {expression} expression, emote, clean background`) + FaceDetailer (conserva la expresión); `ref_face` → IPAdapter-FaceID para que varíe SOLO la expresión y el rostro sea el mismo. UNA expresión por llamada; set = mismas claves variando `expression``comfyui_build_grid`. Probado e2e en GPU (`reports/0151`). SD1.5. |
| `comfyui_build_parallax_background_workflow_py_ml` | `(scene, *, style="game background, side-scroller…", layers=3, checkpoint="dreamshaper_8…", depth_node="DepthAnythingV2Preprocessor", width=1024, height=512, …) -> dict` | Fondo en capas para parallax 2.5D: genera el fondo apaisado (txt2img) + su depth map (`DepthAnythingV2Preprocessor` sobre el VAEDecode), dos SaveImage. El split en N bandas por profundidad es post (GAP: `split_parallax_layers`, aún no creada). Probado e2e en GPU (`reports/0149`). SD1.5. | | `comfyui_build_parallax_background_workflow_py_ml` | `(scene, *, style="game background, side-scroller…", layers=3, checkpoint="dreamshaper_8…", depth_node="DepthAnythingV2Preprocessor", width=1024, height=512, …) -> dict` | Fondo en capas para parallax 2.5D: genera el fondo apaisado (txt2img) + su depth map (`DepthAnythingV2Preprocessor` sobre el VAEDecode), dos SaveImage. El split en N bandas por profundidad es post (GAP: `split_parallax_layers`, aún no creada). Probado e2e en GPU (`reports/0149`). SD1.5. |
| `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. |
## Funciones de post-proceso y puente (`gamedev`, CPU) ## Funciones de post-proceso y puente (`gamedev`, CPU)
@@ -0,0 +1,108 @@
---
name: comfyui_build_ui_hud_workflow
kind: function
lang: py
domain: ml
version: "1.0.0"
purity: pure
signature: "def comfyui_build_ui_hud_workflow(element: str, *, ui_style: str = \"fantasy game UI\", 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 = \"ui_hud\") -> dict"
description: "Construye el dict (API format) del workflow de UN elemento de interfaz de juego (UI/HUD) 2D: botones, marcos/paneles, barras de vida/mana/XP, iconos de UI, cursores, vinhetas de menu. Pieza unica centrada, fondo limpio uniforme, estilo consistente entre elementos del set, recortable a alpha. Compone comfyui_build_txt2img_workflow + comfyui_inject_lora (estilo opcional) + Image Rembg (fondo transparente si transparent). Hermano de comfyui_build_item_icon/pixelart/sprite_sheet_workflow. Pura, sin red ni I/O. class_types verificados contra /object_info."
tags: [comfyui, ml, gamedev, gamedev-2d, ui, hud, button, frame, bar, 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: element
desc: "Nombre del elemento de interfaz (ej. 'health bar', 'wooden button', 'ornate frame', 'magic icon', 'menu cursor', 'mana orb'). Se inserta en un prompt scaffold de UI. No puede estar vacio."
- name: ui_style
desc: "Descriptor de estilo de la interfaz que mantiene consistentes las piezas de un set (ej. 'fantasy game UI', 'sci-fi HUD, neon glow', 'pixel art UI', 'minimal flat UI'). Pasa el MISMO ui_style + checkpoint + lora a todos los elementos del HUD 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). False = elemento opaco sobre fondo plano, recortable luego por el caller. keyword-only."
- name: seed
desc: "Semilla del KSampler. keyword-only."
- name: lora
desc: "LoRA de estilo opcional en models/loras (ej. 'detail_tweaker_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 UI (una pieza, fondo limpio, sin personajes/escena/texto). 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 UI ('{element}, {ui_style}, game UI element, centered, clean, plain background, high detail') + LoRA de estilo opcional + Image Rembg (si transparent). UN elemento; HUD completo -> llamar por element con mismo ui_style/checkpoint/lora."
tested: true
tests: ["golden transparent: clases CheckpointLoaderSimple/KSampler/VAEDecode/SaveImage/Image Rembg; element + 'game UI element' + 'centered' + 'plain background' en prompt; SaveImage <- Rembg; transparency True", "edge transparent=False: sin Rembg, SaveImage <- VAEDecode", "edge size: width==height==768 (cuadrado)", "edge ui_style en prompt", "edge lora: LoraLoader presente con strength", "error element vacio -> ValueError", "determinismo"]
test_file_path: "python/functions/ml/comfyui_build_ui_hud_workflow_test.py"
file_path: "python/functions/ml/comfyui_build_ui_hud_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_ui_hud_workflow import comfyui_build_ui_hud_workflow
# Un marco de barra de vida ornamentado con fondo transparente (alpha), listo para submit.
wf = comfyui_build_ui_hud_workflow(
"ornate health bar frame",
ui_style="fantasy game UI, gold filigree",
transparent=True,
seed=42,
)
# HUD coherente: misma firma de estilo para cada pieza de la interfaz.
# for el in ["wooden button", "ornate frame", "mana orb", "menu cursor"]:
# wf = comfyui_build_ui_hud_workflow(el, ui_style="fantasy game UI",
# lora="detail_tweaker_sd15.safetensors", seed=42)
# comfyui_submit_workflow(wf) # -> comfyui_wait_result -> comfyui_fetch_output_image
# Atlas del HUD: montar los PNG resultantes con comfyui_build_grid.
```
O lanzable directo con: `./fn run comfyui_build_ui_hud_workflow` (imprime nodos + class_types del ejemplo).
## Cuando usarla
Cuando necesites elementos de interfaz/HUD para un juego (RPG, plataformas,
roguelike) con look consistente: botones, marcos/paneles, barras de vida/mana/XP,
iconos de UI, cursores, vinhetas de menu. Pasa el MISMO `ui_style` + `checkpoint` +
(`lora`) a todas las piezas para que combinen visualmente; varia solo `element`.
`transparent` recorta el fondo (alpha) listo para el motor. Para un atlas/contact-sheet,
genera cada elemento y monta los PNG con `comfyui_build_grid`.
## Gotchas
- **El recorte usa Rembg, NO luma-to-alpha**: un elemento de UI es una pieza solida
con silueta definida (boton, marco, barra), rembg lo recorta limpio.
`comfyui_matting_luma_to_alpha` es para translucidos sobre negro (humo/fuego/magia)
y aplanaria la pieza — no la uses para UI.
- **Coherencia del HUD = mismos parametros**: si cambias `ui_style`/`checkpoint`/
`lora`/`seed` entre piezas, el set deja de combinar. Fija esos y varia solo `element`.
- **El texto/label lo pone el motor, no la imagen**: el negativo por defecto empuja
a "no text / no label" para que la silueta quede limpia; el numero de la barra o el
texto del boton se renderizan en el juego sobre la pieza. Si quieres texto horneado,
pasa un `negative` propio sin "text, label".
- **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 el modelo mete personajes o escena, el negativo por defecto ya empuja a "single
element / plain background / no character"; refuerza `ui_style` con "isolated UI
element" si insiste.
- `transparent=False` deja la pieza opaca sobre fondo plano: util si prefieres
recortar fuera del workflow o el motor compone sobre un slot 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,212 @@
"""Construye el workflow ComfyUI de UN elemento de interfaz de juego (UI/HUD) en API format.
Elementos de UI/HUD de juego (botones, marcos/paneles, barras de vida/mana/XP,
iconos de UI, cursores, vinhetas de menu): pieza unica centrada, fondo limpio y
uniforme, estilo consistente entre elementos del mismo set, recortable a alpha. Es
el builder hermano de comfyui_build_item_icon_workflow / comfyui_build_pixelart_workflow
/ comfyui_build_seamless_tile_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 UI) ...
-> VAEDecode -> [Image Rembg opcional] -> SaveImage
Compone:
- comfyui_build_txt2img_workflow -> base txt2img cuadrada
- comfyui_inject_lora -> LoRA de estilo opcional (consistencia)
- 'Image Rembg (Remove Background)' (helper local) -> fondo transparente
Por que Rembg y NO comfyui_matting_luma_to_alpha: un elemento de UI es una pieza
SOLIDA con silueta definida (un boton, un marco, una barra); rembg recorta limpio
la silueta dejando alpha. La luma-to-alpha es para translucidos sobre negro
(humo/fuego/magia) y aplanaria el elemento. Si el caller prefiere recortar fuera
del workflow (transparent=False) deja la imagen opaca sobre fondo plano, recortable
luego por el pipeline o el caller.
El mismo ui_style + checkpoint + (lora) en todas las piezas de un set (botones,
barras, marcos) hace que combinen visualmente: es la clave de un HUD coherente,
igual que en los iconos de inventario.
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 UI/HUD: una sola pieza limpia, sin personajes
# ni escenas ni texto/marcas que ensucien la silueta centrada y recortable.
_UI_NEGATIVE = (
"blurry, lowres, character, person, face, landscape, scene, "
"multiple elements, cluttered, busy background, text, label, watermark, "
"signature, photo, photorealistic, jpeg artifacts, cropped, out of frame, "
"deformed"
)
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 / comfyui_build_sprite_sheet_workflow:
el nodo recorta la silueta del elemento de UI 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_ui_hud_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_ui_hud_workflow(
element: str,
*,
ui_style: str = "fantasy game UI",
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 = "ui_hud",
) -> dict:
"""Construye el dict (API format) del workflow de un elemento de UI/HUD de juego.
Args:
element: nombre del elemento de interfaz (ej. "health bar", "wooden button",
"ornate frame", "magic icon", "menu cursor", "mana orb"). Se inserta en
un prompt scaffold de UI. No puede estar vacio.
ui_style: descriptor de estilo de la interfaz que mantiene consistentes las
piezas de un set (ej. "fantasy game UI", "sci-fi HUD, neon glow",
"pixel art UI", "minimal flat UI"). Pasa el MISMO ui_style + checkpoint +
(lora) a todos los elementos del HUD 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 Rembg y el PNG sale con alpha (fondo
recortado). Si False deja el elemento opaco sobre fondo plano,
recortable luego por el caller/pipeline. keyword-only.
seed: semilla del KSampler. keyword-only.
lora: LoRA de estilo opcional en models/loras (ej.
'detail_tweaker_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
UI (una pieza, fondo limpio, sin personajes/escena/texto). 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 UI ('{element}, {ui_style}, game UI element,
centered, clean, plain background, high detail') + LoRA de estilo opcional +
Rembg (si transparent). Es UN elemento; un HUD completo -> llamar por element
con el mismo ui_style/checkpoint/lora y montar con comfyui_build_grid si se
quiere un atlas.
Raises:
ValueError: si element 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 element or not element.strip():
raise ValueError("comfyui_build_ui_hud_workflow: 'element' no puede estar vacio")
lora_strength = max(0.0, min(2.0, float(lora_strength)))
neg = _UI_NEGATIVE if negative is None else negative
# Prompt scaffold de UI: pieza unica, centrada, fondo plano, recortable.
positive = (
f"{element.strip()}, {ui_style}, game UI element, centered, clean, "
"plain background, 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_ui_hud_workflow(
"ornate health bar frame",
ui_style="fantasy game UI, gold filigree",
transparent=True,
seed=42,
)
print(
json.dumps(
{
"nodes": list(wf),
"classes": sorted({n["class_type"] for n in wf.values()}),
},
indent=2,
)
)
@@ -0,0 +1,96 @@
"""Tests offline de comfyui_build_ui_hud_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_ui_hud_workflow import ( # noqa: E402
comfyui_build_ui_hud_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 test_golden_transparent_recipe():
wf = comfyui_build_ui_hud_workflow("health bar", transparent=True, seed=42)
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 element aparece en el prompt positivo, con el scaffold de UI.
pos = next(
n for n in wf.values()
if n["class_type"] == "CLIPTextEncode" and "health bar" in n["inputs"]["text"]
)
assert "game UI element" in pos["inputs"]["text"]
assert "centered" in pos["inputs"]["text"]
assert "plain background" 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_ui_hud_workflow("wooden button", 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_ui_hud_workflow("mana orb", size=768)
latent = _by_class(wf, "EmptyLatentImage")[0]["inputs"]
assert latent["width"] == 768
assert latent["height"] == 768 # cuadrado
def test_edge_ui_style_in_prompt():
wf = comfyui_build_ui_hud_workflow(
"ornate frame", ui_style="sci-fi HUD, neon glow", transparent=False
)
pos = next(
n for n in wf.values()
if n["class_type"] == "CLIPTextEncode" and "ornate frame" in n["inputs"]["text"]
)
assert "sci-fi HUD, neon glow" in pos["inputs"]["text"]
def test_edge_lora_reflected():
wf = comfyui_build_ui_hud_workflow(
"magic icon", lora="detail_tweaker_sd15.safetensors", lora_strength=0.9
)
loras = _by_class(wf, "LoraLoader")
assert len(loras) == 1
assert loras[0]["inputs"]["lora_name"] == "detail_tweaker_sd15.safetensors"
assert loras[0]["inputs"]["strength_model"] == 0.9
def test_error_empty_element():
try:
comfyui_build_ui_hud_workflow(" ")
assert False
except ValueError as e:
assert "element" in str(e)
def test_determinism():
a = comfyui_build_ui_hud_workflow("menu cursor", lora="detail_tweaker_sd15.safetensors", seed=7)
b = comfyui_build_ui_hud_workflow("menu cursor", lora="detail_tweaker_sd15.safetensors", seed=7)
assert a == b