From 3f465aceed7236e3817e7ccd741c326f0965995d Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Fri, 26 Jun 2026 22:31:59 +0200 Subject: [PATCH] =?UTF-8?q?feat(gamedev):=20comfyui=5Fbuild=5Fitem=5Ficon?= =?UTF-8?q?=5Fworkflow=20=E2=80=94=20iconos=20de=20inventario?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Builder puro hermano de pixelart/sprite_sheet/seamless_tile: arma el dict (API format) para iconos de items (espada/pocion/anillo/libro/escudo). txt2img cuadrado + prompt scaffold de icono + LoRA estilo opcional + Rembg (alpha). Compone comfyui_build_txt2img_workflow + comfyui_inject_lora. Test offline 7/7 verde. Generacion real verificada (icono de pocion de salud centrado, RGBA fondo recortado, prompt_id 70b7a52a, 512x512 SD1.5). Fila en docs/capabilities/gamedev-2d.md. Detalle en report 0147. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/capabilities/gamedev-2d.md | 3 +- .../ml/comfyui_build_item_icon_workflow.md | 104 +++++++++ .../ml/comfyui_build_item_icon_workflow.py | 203 ++++++++++++++++++ .../comfyui_build_item_icon_workflow_test.py | 95 ++++++++ 4 files changed, 404 insertions(+), 1 deletion(-) create mode 100644 python/functions/ml/comfyui_build_item_icon_workflow.md create mode 100644 python/functions/ml/comfyui_build_item_icon_workflow.py create mode 100644 python/functions/ml/comfyui_build_item_icon_workflow_test.py diff --git a/docs/capabilities/gamedev-2d.md b/docs/capabilities/gamedev-2d.md index 6d219d62..9f735c8a 100644 --- a/docs/capabilities/gamedev-2d.md +++ b/docs/capabilities/gamedev-2d.md @@ -17,7 +17,7 @@ Filtro: `mcp__registry__fn_search query="" tag="gamedev-2d"`. Documento hermano del grupo `comfyui` (generación genérica de imágenes/video/3D). Diseño del puente: `docs/comfyui-godot-integration.md`. Planes origen: `reports/0135` (pixelart), `reports/0139` (entornos/tiles/iso), `reports/0137` (personajes/sprites), -`reports/0140` (VFX), `reports/0143` (ronda 2b: builders). +`reports/0140` (VFX), `reports/0143` (ronda 2b: builders), `reports/0147` (item icons). ## Builders de workflow 2D (`gamedev-2d`, puros — generación) @@ -34,6 +34,7 @@ VFX (ver `reports/0143`). | `comfyui_build_isometric_workflow_py_ml` | `(positive, negative=…, *, iso_lora="isometric_game_assets_sd15…", grid_image=None, …) -> dict` | Asset iso 2:1: LoRA iso + ControlNet grid opcional. | | `comfyui_build_sprite_sheet_workflow_py_ml` | `(subject, *, ref_image=None, pose_skeleton=None, char_lora=None, transparent=True, …) -> dict` | UN sprite de personaje: IPAdapter-FaceID + LoRA + ControlNet OpenPose (Advanced, end<1) + Rembg. Varias poses → sheet. SD1.5. | | `comfyui_build_vfx_spritesheet_workflow_py_ml` | `(prompt, *, motion_model="mm_sd_v15_v2.ckpt", num_frames=16, closed_loop=True, lora=None, …) -> dict` | N frames AnimateDiff loop sobre negro (insumo de luma→alpha). 8GB: 16f@512² revienta, usar ≤8f@512² o bajar resolución. | +| `comfyui_build_item_icon_workflow_py_ml` | `(item, *, style="game icon, clean, centered", checkpoint="dreamshaper_8…", size=512, transparent=True, lora=None, …) -> dict` | UN icono de item de inventario (espada/poción/anillo/libro/escudo): txt2img cuadrado + prompt scaffold de icono + LoRA estilo opcional + Rembg (alpha). Set coherente = mismo style/checkpoint/lora por item. SD1.5. | ## Funciones de post-proceso y puente (`gamedev`, CPU) diff --git a/python/functions/ml/comfyui_build_item_icon_workflow.md b/python/functions/ml/comfyui_build_item_icon_workflow.md new file mode 100644 index 00000000..1a061ddf --- /dev/null +++ b/python/functions/ml/comfyui_build_item_icon_workflow.md @@ -0,0 +1,104 @@ +--- +name: comfyui_build_item_icon_workflow +kind: function +lang: py +domain: ml +version: "1.0.0" +purity: pure +signature: "def comfyui_build_item_icon_workflow(item: str, *, style: str = \"game icon, clean, centered\", 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 = \"item_icon\") -> dict" +description: "Construye el dict (API format) del workflow de UN icono de item de inventario 2D (espada, pocion, anillo, libro, escudo, gema): sujeto unico centrado, fondo limpio uniforme, estilo consistente entre items, encuadre cuadrado. Compone comfyui_build_txt2img_workflow + comfyui_inject_lora (estilo opcional) + Image Rembg (fondo transparente si transparent). Hermano de comfyui_build_pixelart/sprite_sheet/seamless_tile_workflow. Pura, sin red ni I/O. class_types verificados contra /object_info." +tags: [comfyui, ml, gamedev, gamedev-2d, icon, item, inventory, 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: item + desc: "Nombre del item (ej. 'health potion', 'iron sword', 'gold ring'). Se inserta en un prompt scaffold de icono. No puede estar vacio." + - name: style + desc: "Descriptor de estilo que mantiene consistentes los items de un set (ej. 'game icon, clean, centered', 'hand-painted RPG icon', 'flat vector icon'). Pasa el MISMO style + checkpoint + lora a todos los items 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 = item 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', 'isometric_game_assets_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 (un objeto, fondo limpio, sin texto/foto). 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 ('a {item}, game inventory icon, {style}, centered, single object, plain background, ...') + LoRA de estilo opcional + Image Rembg (si transparent). UN icono; set de items -> llamar por item con mismo style/checkpoint/lora." +tested: true +tests: ["golden transparent: clases CheckpointLoaderSimple/KSampler/VAEDecode/SaveImage/Image Rembg; item + 'game inventory icon' + 'centered' en prompt; SaveImage <- Rembg; transparency True", "edge transparent=False: sin Rembg, SaveImage <- VAEDecode", "edge size: width==height==768 (cuadrado)", "edge style en prompt", "edge lora: LoraLoader presente con strength", "error item vacio -> ValueError", "determinismo"] +test_file_path: "python/functions/ml/comfyui_build_item_icon_workflow_test.py" +file_path: "python/functions/ml/comfyui_build_item_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_item_icon_workflow import comfyui_build_item_icon_workflow + +# Un icono de pocion con fondo transparente (alpha), listo para submit. +wf = comfyui_build_item_icon_workflow( + "health potion", + style="hand-painted RPG icon, glowing red liquid", + transparent=True, + seed=42, +) +# Set coherente: misma firma de estilo para cada item del inventario. +# for it in ["iron sword", "gold ring", "ancient book", "wooden shield"]: +# wf = comfyui_build_item_icon_workflow(it, style="hand-painted RPG icon", +# lora="detail_tweaker_sd15.safetensors", seed=42) +# comfyui_submit_workflow(wf) # -> comfyui_wait_result -> comfyui_fetch_output_image +# Contact-sheet del set: montar los PNG resultantes con comfyui_build_grid. +``` + +O lanzable directo con: `./fn run comfyui_build_item_icon_workflow` (imprime nodos + class_types del ejemplo). + +## Cuando usarla + +Cuando necesites iconos de items de inventario para un juego (RPG, roguelike, +crafting) con look consistente entre items: armas, pociones, anillos, libros, +escudos, gemas, comida. Pasa el MISMO `style` + `checkpoint` + (`lora`) a todos +los items del set para que combinen visualmente; varía solo `item`. `transparent` +recorta el fondo (alpha) listo para el motor. Para un atlas/contact-sheet, genera +cada icono y monta los PNG con `comfyui_build_grid`. + +## Gotchas + +- **El recorte usa Rembg, NO luma-to-alpha**: un item es un sujeto solido con + silueta definida, rembg lo recorta limpio. `comfyui_matting_luma_to_alpha` es + para translucidos sobre negro (humo/fuego/magia) y aplanaria el item — no la + uses para iconos. +- **Coherencia del set = mismos parametros**: si cambias `style`/`checkpoint`/ + `lora`/`seed` entre items, el set deja de combinar. Fija esos y varía solo `item`. +- **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 varios objetos o texto, el negativo por defecto ya empuja a + "single object / plain background / no text"; refuerza `style` con "single item, + isolated" si insiste. +- `transparent=False` deja el item 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_item_icon_workflow.py b/python/functions/ml/comfyui_build_item_icon_workflow.py new file mode 100644 index 00000000..9d820374 --- /dev/null +++ b/python/functions/ml/comfyui_build_item_icon_workflow.py @@ -0,0 +1,203 @@ +"""Construye el workflow ComfyUI de UN icono de item de inventario (API format). + +Iconos de inventario de juego (espada, pocion, anillo, libro, escudo, gema...): +sujeto unico centrado, fondo limpio y uniforme, estilo consistente entre items, +encuadre cuadrado. Es el builder hermano de comfyui_build_pixelart_workflow / +comfyui_build_sprite_sheet_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 icono) ... + -> 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 item es un sujeto SOLIDO con +silueta definida; rembg recorta limpio la silueta. La luma-to-alpha es para +translucidos sobre negro (humo/fuego/magia), donde aplanaria el item. 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. + +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: un solo objeto, fondo limpio, nada de +# fotorrealismo ni texto/marcas que ensucien el sujeto centrado. +_ICON_NEGATIVE = ( + "blurry, lowres, multiple objects, cluttered background, busy background, " + "text, 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 usa comfyui_build_sprite_sheet_workflow: el nodo recorta la + silueta del item 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_item_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_item_icon_workflow( + item: str, + *, + style: str = "game icon, clean, centered", + 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 = "item_icon", +) -> dict: + """Construye el dict (API format) del workflow de un icono de item de inventario. + + Args: + item: nombre del item (ej. "health potion", "iron sword", "gold ring"). + Se inserta en un prompt scaffold de icono. No puede estar vacio. + style: descriptor de estilo que mantiene consistentes los items de un set + (ej. "game icon, clean, centered", "hand-painted RPG icon", "flat + vector icon"). Pasa el MISMO style + checkpoint + (lora) a todos los + items del set 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 item 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', 'isometric_game_assets_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 (un objeto, fondo limpio, sin texto/foto). 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 + LoRA de estilo opcional + Rembg + (si transparent). Es UN icono; un set de items -> llamar por item con el + mismo style/checkpoint/lora y montar con comfyui_build_grid si se quiere. + + Raises: + ValueError: si item 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 item or not item.strip(): + raise ValueError("comfyui_build_item_icon_workflow: 'item' no puede estar vacio") + + lora_strength = max(0.0, min(2.0, float(lora_strength))) + neg = _ICON_NEGATIVE if negative is None else negative + + # Prompt scaffold de icono: sujeto unico, centrado, fondo plano, sin recorte. + positive = ( + f"a {item.strip()}, game inventory icon, {style}, centered, " + "single object, 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_item_icon_workflow( + "health potion", + style="hand-painted RPG icon, glowing red liquid", + 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_item_icon_workflow_test.py b/python/functions/ml/comfyui_build_item_icon_workflow_test.py new file mode 100644 index 00000000..4aef8438 --- /dev/null +++ b/python/functions/ml/comfyui_build_item_icon_workflow_test.py @@ -0,0 +1,95 @@ +"""Tests offline de comfyui_build_item_icon_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_item_icon_workflow import ( # noqa: E402 + comfyui_build_item_icon_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_item_icon_workflow("health potion", 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 item aparece en el prompt positivo, con el scaffold de icono. + pos = next( + n for n in wf.values() + if n["class_type"] == "CLIPTextEncode" and "health potion" in n["inputs"]["text"] + ) + assert "game inventory icon" in pos["inputs"]["text"] + assert "centered" 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_item_icon_workflow("iron sword", 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_item_icon_workflow("gold ring", 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_item_icon_workflow( + "ancient book", style="flat vector icon, minimalist", transparent=False + ) + pos = next( + n for n in wf.values() + if n["class_type"] == "CLIPTextEncode" and "ancient book" in n["inputs"]["text"] + ) + assert "flat vector icon, minimalist" in pos["inputs"]["text"] + + +def test_edge_lora_reflected(): + wf = comfyui_build_item_icon_workflow( + "magic shield", 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_item(): + try: + comfyui_build_item_icon_workflow(" ") + assert False + except ValueError as e: + assert "item" in str(e) + + +def test_determinism(): + a = comfyui_build_item_icon_workflow("emerald gem", lora="detail_tweaker_sd15.safetensors", seed=7) + b = comfyui_build_item_icon_workflow("emerald gem", lora="detail_tweaker_sd15.safetensors", seed=7) + assert a == b