diff --git a/docs/capabilities/gamedev-2d.md b/docs/capabilities/gamedev-2d.md index 98287c3c..291e9523 100644 --- a/docs/capabilities/gamedev-2d.md +++ b/docs/capabilities/gamedev-2d.md @@ -41,6 +41,7 @@ VFX (ver `reports/0143`). | `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_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_status_effect_icon_workflow_py_ml` | `(effect, *, ui_style="game status icon, bold symbol, flat", checkpoint="dreamshaper_8…", size=256, transparent=True, seed=0, lora=None, …) -> dict` | UN icono de estado / buff-debuff (veneno, quemadura, congelación, escudo, regeneración, aturdimiento, velocidad, sangrado, maldición): **símbolo compacto** que se superpone al HUD para indicar un efecto activo, optimizado para **legibilidad a tamaño reducido** (16-32 px) → `{effect} status effect icon, {ui_style}, simple bold symbol, centered, readable at small size, plain background…` + LoRA estilo opcional + Rembg (alpha). **`size` por defecto menor (256, no 512)** porque se muestra pequeño; el negativo rechaza `intricate details/complex/cluttered` para no perder legibilidad. **DISTINTO de `item_icon` (objeto de inventario) y `ui_hud` (chrome grande de interfaz)**: aquí es un símbolo de estado. Barra coherente = mismo `ui_style`/`checkpoint`/`lora`, varía solo `effect` (color habla del tipo). El texto/contador lo pone el motor (negativo empuja a `no text`). Probado e2e en GPU con SD1.5 — `poison` 256×256 RGBA, símbolo verde flat centrado (`reports/0162`). 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 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. | diff --git a/python/functions/ml/comfyui_build_status_effect_icon_workflow.md b/python/functions/ml/comfyui_build_status_effect_icon_workflow.md new file mode 100644 index 00000000..6268c72f --- /dev/null +++ b/python/functions/ml/comfyui_build_status_effect_icon_workflow.md @@ -0,0 +1,124 @@ +--- +name: comfyui_build_status_effect_icon_workflow +kind: function +lang: py +domain: ml +version: "1.0.0" +purity: pure +signature: "def comfyui_build_status_effect_icon_workflow(effect: str, *, ui_style: str = \"game status icon, bold symbol, flat\", checkpoint: str = \"dreamshaper_8.safetensors\", size: int = 256, 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 = \"status_effect_icon\") -> dict" +description: "Construye el dict (API format) del workflow de UN icono de estado / buff-debuff 2D (veneno, quemadura, congelacion, escudo, regeneracion, aturdimiento, velocidad, sangrado, maldicion): simbolo unico audaz y LEGIBLE A TAMANO REDUCIDO, centrado, fondo limpio uniforme, estilo de UI consistente entre estados, recortable a alpha. Tamano por defecto menor (256) por ser iconos compactos del HUD. DISTINTO de item_icon (objeto de inventario) y ui_hud (chrome grande de interfaz): es un simbolo de estado compacto. Compone comfyui_build_txt2img_workflow + comfyui_inject_lora (estilo opcional) + Image Rembg (fondo transparente si transparent). Hermano de comfyui_build_item_icon/ui_hud_workflow. Pura, sin red ni I/O. class_types verificados contra /object_info." +tags: [comfyui, ml, gamedev, gamedev-2d, ui, status, buff, debuff, icon, 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: effect + desc: "Nombre del efecto de estado (ej. 'poison', 'burning', 'frozen', 'shield', 'regeneration', 'stun', 'speed boost', 'bleed', 'curse'). Se inserta en un prompt scaffold de icono de estado. No puede estar vacio." + - name: ui_style + desc: "Descriptor de estilo de UI que mantiene consistentes los iconos de un set (ej. 'game status icon, bold symbol, flat', 'sci-fi HUD status, neon glow', 'pixel art status icon', 'minimal flat status icon'). Pasa el MISMO ui_style + checkpoint + lora a todos los iconos de la barra de estados 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). 256 por defecto: son iconos compactos que se muestran a tamano reducido en el HUD. keyword-only." + - name: transparent + desc: "Si True inyecta Image Rembg y el PNG sale con alpha (fondo recortado). False = simbolo 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 iconos de estado (un simbolo simple legible, fondo limpio, sin escena/personaje/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 icono de estado ('{effect} status effect icon, {ui_style}, simple bold symbol, centered, readable at small size, plain background, ...') + LoRA de estilo opcional + Image Rembg (si transparent). UN icono; barra de estados completa -> llamar por effect con mismo ui_style/checkpoint/lora." +tested: true +tests: ["golden transparent: clases CheckpointLoaderSimple/KSampler/VAEDecode/SaveImage/Image Rembg; effect + 'status effect icon' + 'centered' + 'readable at small size' en prompt; SaveImage <- Rembg; transparency True", "edge transparent=False: sin Rembg, SaveImage <- VAEDecode", "edge size: width==height (cuadrado); default 256", "edge ui_style en prompt", "edge effect en prompt", "edge lora: LoraLoader presente con strength", "error effect vacio -> ValueError", "determinismo"] +test_file_path: "python/functions/ml/comfyui_build_status_effect_icon_workflow_test.py" +file_path: "python/functions/ml/comfyui_build_status_effect_icon_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_status_effect_icon_workflow import comfyui_build_status_effect_icon_workflow + +# Un icono de veneno con fondo transparente (alpha), listo para submit. +wf = comfyui_build_status_effect_icon_workflow( + "poison", + ui_style="game status icon, bold symbol, flat, green", + transparent=True, + seed=42, +) +# Barra de estados coherente: misma firma de estilo para cada efecto. +# for fx in ["burning", "frozen", "shield", "regeneration", "stun", "speed boost"]: +# wf = comfyui_build_status_effect_icon_workflow(fx, ui_style="game status icon, bold symbol, flat", +# lora="detail_tweaker_sd15.safetensors", seed=42) +# comfyui_submit_workflow(wf) # -> comfyui_wait_result -> comfyui_fetch_output_image +# Atlas de estados: montar los PNG resultantes con comfyui_build_grid. +``` + +O lanzable directo con: `./fn run comfyui_build_status_effect_icon_workflow` (imprime nodos + class_types del ejemplo). + +## Cuando usarla + +Cuando necesites iconos de buff/debuff para la barra de estados de un juego (RPG, +roguelike, MOBA): veneno, quemadura, congelacion, escudo, regeneracion, aturdimiento, +velocidad, sangrado, bendicion, maldicion. El icono se pinta encima del retrato o la +barra del personaje para indicar un efecto activo, asi que la prioridad es la +LEGIBILIDAD a tamano reducido (16-32 px): un simbolo audaz y simple, no una +ilustracion recargada. Pasa el MISMO `ui_style` + `checkpoint` + (`lora`) a todos los +estados del set para que combinen visualmente; varia solo `effect`. `transparent` +recorta el fondo (alpha) listo para el motor. Para un atlas, genera cada icono y monta +los PNG con `comfyui_build_grid`. + +Eligela frente a sus hermanos por el ROL del asset: +- **item_icon** -> objeto de inventario (espada, pocion, anillo): ilustracion de un + objeto, no un simbolo de estado. +- **ui_hud** -> chrome grande de la interfaz (botones, marcos, barras de vida): piezas + de layout, no simbolos compactos. +- **status_effect_icon (esta)** -> simbolo de estado compacto que se superpone al HUD, + optimizado para legibilidad a tamano reducido. + +## Gotchas + +- **Legibilidad > detalle**: el icono se muestra a 16-32 px sobre el personaje; un + simbolo recargado se vuelve ilegible. El prompt empuja a "simple bold symbol, + readable at small size" y el `size` por defecto es menor (256, no 512 como + item_icon/ui_hud). El negativo por defecto rechaza "intricate details / complex / + cluttered" justamente por esto. Si el modelo mete una escena, refuerza `ui_style` + con "single bold icon, minimal". +- **El recorte usa Rembg, NO luma-to-alpha**: un icono de estado es un simbolo solido + con silueta definida, rembg lo recorta limpio. `comfyui_matting_luma_to_alpha` es + para translucidos sobre negro (humo/fuego/magia) y aplanaria el simbolo — no la uses + para estos iconos. +- **Coherencia del set = mismos parametros**: si cambias `ui_style`/`checkpoint`/ + `lora`/`seed` entre estados, la barra deja de combinar. Fija esos y varia solo + `effect`. Una convencion util: el color habla del tipo de efecto (verde veneno, rojo + quemadura, azul congelacion) sin sacrificar la consistencia del simbolo. +- **El texto/numero lo pone el motor, no la imagen**: el negativo por defecto empuja a + "no text / no letters" para que la silueta quede limpia; el contador de turnos o el + stack del buff se renderiza en el juego sobre el icono. Si quieres texto horneado, + pasa un `negative` propio sin "text, letters". +- **SDXL pide mas VRAM y resolucion**: con `checkpoint="juggernaut_xl_v11.safetensors"` + sube `size` a 512/768; con dreamshaper_8 (SD1.5) deja 256 (holgado en 8GB lowvram). +- `transparent=False` deja el simbolo opaco 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`. diff --git a/python/functions/ml/comfyui_build_status_effect_icon_workflow.py b/python/functions/ml/comfyui_build_status_effect_icon_workflow.py new file mode 100644 index 00000000..d00e24a7 --- /dev/null +++ b/python/functions/ml/comfyui_build_status_effect_icon_workflow.py @@ -0,0 +1,229 @@ +"""Construye el workflow ComfyUI de UN icono de estado / buff-debuff (API format). + +Iconos de estado de juego (veneno, quemadura, congelacion, escudo, regeneracion, +aturdimiento, velocidad, sangrado, bendicion, maldicion): simbolo unico, audaz y +LEGIBLE A TAMANO REDUCIDO, centrado, fondo limpio y uniforme, estilo de UI +consistente entre estados del mismo set, recortable a alpha. Es el builder hermano +de comfyui_build_item_icon_workflow / comfyui_build_ui_hud_workflow: mismo patron +(PURO, dict API format) que compone funciones existentes del registry, no reescribe +el grafo. + +DISTINTO de item_icon y ui_hud: un icono de estado no es un objeto de inventario +(item_icon) ni un elemento de chrome grande de la interfaz (ui_hud, botones/marcos/ +barras). Es un SIMBOLO compacto que se pinta encima del retrato/barra del personaje +para indicar un efecto activo: por eso el prompt empuja a "simple bold symbol, +readable at small size" y el tamano por defecto es menor (256), porque el icono se +mostrara a 16-32 px en el HUD y un simbolo recargado se vuelve ilegible. + +Cableado: + + CheckpointLoaderSimple -> [LoraLoader opcional de estilo] -> KSampler + -> CLIPTextEncode (prompt scaffold de icono de estado) ... + -> 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 icono de estado es un simbolo +SOLIDO con silueta definida; rembg recorta limpio la silueta dejando alpha. La +luma-to-alpha es para translucidos sobre negro (humo/fuego/magia) y aplanaria el +simbolo. 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 todos los iconos de un set (veneno, +escudo, velocidad...) hace que combinen visualmente: es la clave de una barra de +estados coherente, igual que en los iconos de inventario y los elementos del HUD. + +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 iconos de estado: un solo simbolo limpio y +# legible, sin escena ni personaje ni texto/marcas que ensucien la silueta +# centrada y recortable. Empuja contra el detalle excesivo que arruina la +# legibilidad cuando el icono se muestra a 16-32 px en el HUD. +_STATUS_NEGATIVE = ( + "blurry, lowres, intricate details, complex, busy background, " + "cluttered, multiple symbols, detailed scene, landscape, character, " + "person, face, text, letters, words, watermark, signature, photo, " + "photorealistic, realistic, 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_ui_hud_workflow: + el nodo recorta la silueta del simbolo de estado 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_status_effect_icon_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_status_effect_icon_workflow( + effect: str, + *, + ui_style: str = "game status icon, bold symbol, flat", + checkpoint: str = "dreamshaper_8.safetensors", + size: int = 256, + 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 = "status_effect_icon", +) -> dict: + """Construye el dict (API format) del workflow de un icono de estado / buff-debuff. + + Args: + effect: nombre del efecto de estado (ej. "poison", "burning", "frozen", + "shield", "regeneration", "stun", "speed boost", "bleed", "curse"). + Se inserta en un prompt scaffold de icono de estado. No puede estar + vacio. + ui_style: descriptor de estilo de UI que mantiene consistentes los iconos + de un set (ej. "game status icon, bold symbol, flat", "sci-fi HUD + status, neon glow", "pixel art status icon", "minimal flat status + icon"). Pasa el MISMO ui_style + checkpoint + (lora) a todos los iconos + de la barra de estados 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). keyword-only. + size: lado del cuadrado en px (width = height = size). 256 por defecto: + son iconos compactos que se muestran a tamano reducido en el HUD. + keyword-only. + transparent: si True inyecta Rembg y el PNG sale con alpha (fondo + recortado). Si False deja el simbolo 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 + iconos de estado (un simbolo simple legible, fondo limpio, sin + escena/personaje/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 icono de estado ('{effect} status effect + icon, {ui_style}, simple bold symbol, centered, readable at small size, + plain background, ...') + LoRA de estilo opcional + Rembg (si transparent). + Es UN icono; una barra de estados completa -> llamar por effect con el + mismo ui_style/checkpoint/lora y montar con comfyui_build_grid si se quiere + un atlas. + + Raises: + ValueError: si effect 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 effect or not effect.strip(): + raise ValueError( + "comfyui_build_status_effect_icon_workflow: 'effect' no puede estar vacio" + ) + + lora_strength = max(0.0, min(2.0, float(lora_strength))) + neg = _STATUS_NEGATIVE if negative is None else negative + + # Prompt scaffold de icono de estado: simbolo unico, audaz, centrado, legible + # a tamano reducido, fondo plano, recortable. + positive = ( + f"{effect.strip()} status effect icon, {ui_style}, simple bold symbol, " + "centered, readable at small size, plain background, clean, 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_status_effect_icon_workflow( + "poison", + ui_style="game status icon, bold symbol, flat, green", + transparent=True, + seed=42, + ) + print( + json.dumps( + { + "nodes": list(wf), + "classes": sorted({n["class_type"] for n in wf.values()}), + }, + indent=2, + ) + ) diff --git a/python/functions/ml/comfyui_build_status_effect_icon_workflow_test.py b/python/functions/ml/comfyui_build_status_effect_icon_workflow_test.py new file mode 100644 index 00000000..c1ac2b40 --- /dev/null +++ b/python/functions/ml/comfyui_build_status_effect_icon_workflow_test.py @@ -0,0 +1,116 @@ +"""Tests offline (sin red, sin GPU) de comfyui_build_status_effect_icon_workflow. + +Verifican que el dict en API format se construye correctamente: clases presentes, +cableado del Rembg, prompt scaffold de icono de estado, y reflejo de los argumentos +(effect, ui_style, size, transparent, lora). No tocan el servidor ComfyUI. +""" +import os +import sys + +import pytest + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) + +from ml.comfyui_build_status_effect_icon_workflow import ( # noqa: E402 + comfyui_build_status_effect_icon_workflow, +) + + +def _classes(wf): + return {n["class_type"] for n in wf.values()} + + +def _positive_prompt(wf): + """Texto positivo: el CLIPTextEncode al que apunta KSampler.positive.""" + ks = next(n for n in wf.values() if n["class_type"] == "KSampler") + pos_id = ks["inputs"]["positive"][0] + return wf[pos_id]["inputs"]["text"] + + +def test_golden_transparent(): + """Caso feliz: icono transparente -> Rembg cableado, prompt de estado, clases base.""" + wf = comfyui_build_status_effect_icon_workflow("poison", transparent=True, seed=42) + cls = _classes(wf) + for expected in { + "CheckpointLoaderSimple", + "KSampler", + "VAEDecode", + "SaveImage", + "Image Rembg (Remove Background)", + }: + assert expected in cls, f"falta clase {expected}" + + prompt = _positive_prompt(wf) + assert "poison" in prompt + assert "status effect icon" in prompt + assert "centered" in prompt + assert "readable at small size" in prompt + + # SaveImage debe tomar la imagen del Rembg, no del VAEDecode. + save = next(n for n in wf.values() if n["class_type"] == "SaveImage") + rembg_id = next( + nid for nid, n in wf.items() if n["class_type"] == "Image Rembg (Remove Background)" + ) + assert save["inputs"]["images"][0] == rembg_id + rembg = wf[rembg_id] + assert rembg["inputs"]["transparency"] is True + + +def test_edge_transparent_false_no_rembg(): + """transparent=False -> sin nodo Rembg; SaveImage cuelga del VAEDecode.""" + wf = comfyui_build_status_effect_icon_workflow("shield", transparent=False) + assert "Image Rembg (Remove Background)" not in _classes(wf) + save = next(n for n in wf.values() if n["class_type"] == "SaveImage") + vae_id = next(nid for nid, n in wf.items() if n["class_type"] == "VAEDecode") + assert save["inputs"]["images"][0] == vae_id + + +def test_edge_size_reflected_square(): + """size se refleja como width == height (cuadrado). Default 256 (icono compacto).""" + wf = comfyui_build_status_effect_icon_workflow("frozen", size=128) + latent = next(n for n in wf.values() if n["class_type"] == "EmptyLatentImage") + assert latent["inputs"]["width"] == 128 + assert latent["inputs"]["height"] == 128 + + wf_default = comfyui_build_status_effect_icon_workflow("frozen") + latent_d = next(n for n in wf_default.values() if n["class_type"] == "EmptyLatentImage") + assert latent_d["inputs"]["width"] == 256 + assert latent_d["inputs"]["height"] == 256 + + +def test_edge_ui_style_reflected(): + """ui_style se inserta en el prompt positivo.""" + wf = comfyui_build_status_effect_icon_workflow( + "burning", ui_style="sci-fi HUD status, neon glow" + ) + assert "sci-fi HUD status, neon glow" in _positive_prompt(wf) + + +def test_edge_effect_reflected(): + """effect se inserta literal en el prompt positivo.""" + wf = comfyui_build_status_effect_icon_workflow("regeneration") + assert "regeneration" in _positive_prompt(wf) + + +def test_edge_lora_injected(): + """lora -> LoraLoader presente con la fuerza dada.""" + wf = comfyui_build_status_effect_icon_workflow( + "speed boost", lora="detail_tweaker_sd15.safetensors", lora_strength=0.8 + ) + loras = [n for n in wf.values() if n["class_type"] == "LoraLoader"] + assert len(loras) == 1 + assert loras[0]["inputs"]["lora_name"] == "detail_tweaker_sd15.safetensors" + assert loras[0]["inputs"]["strength_model"] == pytest.approx(0.8) + + +def test_error_empty_effect(): + """effect vacio -> ValueError.""" + with pytest.raises(ValueError): + comfyui_build_status_effect_icon_workflow(" ") + + +def test_determinism(): + """Mismos argumentos -> mismo dict (funcion pura).""" + a = comfyui_build_status_effect_icon_workflow("stun", seed=7) + b = comfyui_build_status_effect_icon_workflow("stun", seed=7) + assert a == b