From 2a7c77cb563fb7d7d25f7a817df0f5d1292adbe3 Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Sat, 27 Jun 2026 02:18:12 +0200 Subject: [PATCH] =?UTF-8?q?feat(gamedev):=20comfyui=5Fbuild=5Fachievement?= =?UTF-8?q?=5Fbadge=5Fworkflow=20=E2=80=94=20insignias/medallas/logros?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nuevo builder del grupo gamedev-2d para insignias de logro / medallas / trofeos de sistemas de achievements/recompensas, con tier metálico (bronce/plata/oro/platino/ diamante) y fondo recortable a alpha. Función pura (dict API format) que compone comfyui_build_txt2img_workflow + comfyui_inject_lora (estilo opcional) + Image Rembg. Hermano de item_icon (objeto de inventario suelto) y skill_tree_node (nodo enmarcado de la rejilla de talentos): aquí el asset es la insignia de logro/recompensa = medalla con cinta + tier. El tier metálico y la forma de medalla/trofeo son la firma. 12 tests offline verdes. Probado e2e en GPU con SD1.5: badge="dragon slayer" tier gold seed 77 256x256 RGBA, medalla circular dorada con emblema centrado y fondo recortado a alpha (esquina α=0, centro α=254; prompt_id 8b8b7ede). --- docs/capabilities/gamedev-2d.md | 1 + ...omfyui_build_achievement_badge_workflow.md | 140 ++++++++++ ...omfyui_build_achievement_badge_workflow.py | 264 ++++++++++++++++++ ...i_build_achievement_badge_workflow_test.py | 152 ++++++++++ 4 files changed, 557 insertions(+) create mode 100644 python/functions/ml/comfyui_build_achievement_badge_workflow.md create mode 100644 python/functions/ml/comfyui_build_achievement_badge_workflow.py create mode 100644 python/functions/ml/comfyui_build_achievement_badge_workflow_test.py diff --git a/docs/capabilities/gamedev-2d.md b/docs/capabilities/gamedev-2d.md index 89a074b4..30a19d26 100644 --- a/docs/capabilities/gamedev-2d.md +++ b/docs/capabilities/gamedev-2d.md @@ -44,6 +44,7 @@ VFX (ver `reports/0143`). | `comfyui_build_dialogue_box_workflow_py_ml` | `(box_style="fantasy RPG dialogue box", *, shape="rounded panel", checkpoint="dreamshaper_8…", width=768, height=256, transparent=True, seed=0, lora=None, …) -> dict` | EL contenedor de diálogo / bocadillo / panel de texto de juego (RPG, visual novel, aventura): marco **apaisado** (`width>height`, 768×256) con borde decorativo y un **interior plano/vacío** reservado para que el motor renderice el texto de la conversación encima → `{box_style}, {shape}, game UI dialogue box frame, ornate border, empty flat interior for text, plain background` + LoRA estilo opcional + Rembg (alpha). **DISTINTO de `ui_hud` (elementos sueltos: botón/barra/icono)**: esto es el panel-contenedor completo. `shape` (rounded panel/scroll parchment/stone tablet/speech bubble…) + set coherente = mismo `box_style`/`shape`/`checkpoint`/`lora`. El interior se mantiene liso (negativo rechaza `busy/decorated interior`); el texto lo pone el motor (negativo empuja a `no text`). Probado e2e en GPU con SD1.5 — `medieval fantasy dialogue box, wood and gold` 768×256 RGBA, panel madera+oro con interior plano y alpha (`reports/0171`). 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_skill_tree_node_workflow_py_ml` | `(skill, *, frame="hexagonal", state="unlocked", ui_style="fantasy skill tree node", checkpoint="dreamshaper_8…", size=256, transparent=True, seed=0, lora=None, …) -> dict` | UN nodo de **árbol de habilidades / talentos** (RPG, ARPG, MOBA, roguelike): el icono de una `skill` **DENTRO de un marco** (`frame`: hexagonal/circular/diamond/shield) que la UI de progresión pinta en la rejilla, con variante de **estado** visual (`state`: `unlocked`=brillante/saturado, `locked`=gris/desaturado) → `{skill} skill icon inside a {frame} {ui_style} frame, {state} (…hint…), centered, plain background, game UI, skill tree talent node…` + LoRA estilo opcional + Rembg (alpha). El **marco** y el **estado** son la firma del asset. **DISTINTO de `item_icon` (objeto suelto sin marco), `status_effect_icon` (símbolo superpuesto sin marco) y `ui_hud` (chrome grande)**: aquí es el nodo enmarcado completo de la pantalla de talentos. Par de un mismo talento = mismo `skill`/`frame`/`ui_style`/`seed`, varía solo `state` (las dos caras de la rejilla). Árbol coherente = mismo `frame`/`ui_style`/`checkpoint`/`lora`, varía `skill`. El texto/coste lo pone el motor (negativo empuja a `no text`). Probado e2e en GPU con SD1.5 — `fireball` hexagonal unlocked 256×256 RGBA, nodo enmarcado brillante centrado (`reports/0173`). SD1.5. | +| `comfyui_build_achievement_badge_workflow_py_ml` | `(badge, *, tier="gold", style="game achievement badge, ornate", checkpoint="dreamshaper_8…", size=256, transparent=True, seed=0, lora=None, …) -> dict` | UNA **insignia / medalla / logro** (achievement, recompensa, rango): un trofeo, una medalla con cinta, un escudo de logro o un badge de rango que el panel de logros pinta al desbloquear un hito, con **`tier` metálico** (`bronze`/`silver`/`gold`/`platinum`/`diamond`) que distingue el grado → `{badge} achievement badge, {tier} tier (…hint metálico…), {style}, medal with ribbon, centered, plain background, game UI reward, trophy emblem…` + LoRA estilo opcional + Rembg (alpha). El **tier metálico** y la forma de **medalla/trofeo con cinta** son la firma del asset. **DISTINTO de `item_icon` (objeto de inventario suelto, sin tier ni cinta), `status_effect_icon` (símbolo de estado superpuesto sin marco) y `skill_tree_node` (nodo enmarcado de la rejilla de talentos con estado unlocked/locked)**: aquí es la insignia de logro/recompensa del panel de achievements. Familia de un mismo logro = mismo `badge`/`style`/`seed`, varía solo `tier` (los grados); set coherente = mismo `style`/`checkpoint`/`lora`, varía `badge`. El nombre/descripción/fecha lo pone el motor (negativo empuja a `no text`). Probado e2e en GPU con SD1.5 — `dragon slayer` tier gold seed 77 256×256 RGBA, medalla circular dorada con emblema centrado y fondo recortado a alpha (esquina α=0, centro α=254; `prompt_id 8b8b7ede`, `reports/0175`). 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_achievement_badge_workflow.md b/python/functions/ml/comfyui_build_achievement_badge_workflow.md new file mode 100644 index 00000000..8a977cbd --- /dev/null +++ b/python/functions/ml/comfyui_build_achievement_badge_workflow.md @@ -0,0 +1,140 @@ +--- +name: comfyui_build_achievement_badge_workflow +kind: function +lang: py +domain: ml +version: "1.0.0" +purity: pure +signature: "def comfyui_build_achievement_badge_workflow(badge: str, *, tier: str = \"gold\", style: str = \"game achievement badge, ornate\", 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 = \"achievement_badge\") -> dict" +description: "Construye el dict (API format) del workflow de UNA insignia / medalla / logro 2D (achievement, recompensa, rango): un trofeo, una medalla con cinta, un escudo de logro, una estrella o un badge de rango que la UI de achievements pinta cuando el jugador desbloquea un hito, con TIER metalico (bronce / plata / oro / platino / diamante) que distingue el grado. Centrado, fondo limpio uniforme, recortable a alpha, estilo consistente entre insignias del set. DISTINTO de item_icon (objeto de inventario suelto, sin tier ni cinta), status_effect_icon (simbolo de estado superpuesto sin marco) y skill_tree_node (nodo enmarcado de la rejilla de talentos con estado unlocked/locked): esto es la INSIGNIA DE LOGRO/RECOMPENSA = trofeo/medalla con cinta + tier. El tier metalico y la forma de medalla/trofeo son la firma del asset. Compone comfyui_build_txt2img_workflow + comfyui_inject_lora (estilo opcional) + Image Rembg (fondo transparente si transparent). Hermano de comfyui_build_item_icon/skill_tree_node_workflow. Pura, sin red ni I/O. class_types verificados contra /object_info." +tags: [comfyui, ml, gamedev, gamedev-2d, ui, achievement, badge, medal, trophy, reward, tier, ribbon, 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: badge + desc: "Nombre / tema del logro que representa la insignia (ej. 'dragon slayer', 'first blood', '100 wins', 'explorer', 'marathon runner', 'boss killer'). Se inserta en un prompt scaffold de insignia. No puede estar vacio." + - name: tier + desc: "Grado del logro: 'bronze', 'silver', 'gold' (por defecto), 'platinum' o 'diamond'. Define el aspecto metalico de la medalla. Genera la familia con el mismo badge/style/seed variando solo el tier para tener las caras coherentes del mismo logro. Cualquier otro valor se inserta literal y la pista metalica cae a la de 'gold'. keyword-only." + - name: style + desc: "Descriptor de estilo que mantiene consistentes las insignias de un set (ej. 'game achievement badge, ornate', 'flat minimal medal', 'pixel art trophy'). Pasa el MISMO style + checkpoint + lora a todas las insignias del set 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: las insignias de logro se muestran a tamano reducido en el panel. keyword-only." + - name: transparent + desc: "Si True inyecta Image Rembg y el PNG sale con alpha (fondo recortado, la silueta de la medalla + cinta). False = insignia opaca sobre fondo plano, recortable luego por el caller. keyword-only." + - name: seed + desc: "Semilla del KSampler. Fija el mismo seed en la familia de tiers del mismo logro para que coincidan en composicion. 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 insignias (una medalla limpia, 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 insignia ('{badge} achievement badge, {tier} tier ({tier hint}), {style}, medal with ribbon, centered, plain background, game UI reward, ...') + LoRA de estilo opcional + Image Rembg (si transparent). UNA insignia; un set de logros -> llamar por badge (y por tier) con mismo style/checkpoint/lora y montar el panel en el motor." +tested: true +test_file_path: python/functions/ml/comfyui_build_achievement_badge_workflow_test.py +tests: [test_golden_transparent, test_edge_transparent_false_no_rembg, test_edge_size_reflected_square, test_edge_tier_gold_default_hint, test_edge_tier_bronze_reflected, test_edge_tier_silver_reflected, test_edge_tier_unknown_falls_back_to_gold_hint, test_edge_style_reflected, test_edge_badge_reflected, test_edge_lora_injected, test_error_empty_badge, test_determinism] +file_path: python/functions/ml/comfyui_build_achievement_badge_workflow.py +--- + +Construye el dict (API format) del workflow de UNA insignia / medalla / logro 2D +(achievement, recompensa, rango): un trofeo, una medalla con cinta, un escudo de logro, +una estrella o un badge de rango que la UI de achievements pinta cuando el jugador +desbloquea un hito, con TIER metalico (bronce / plata / oro / platino / diamante) que +distingue el grado. Centrado, fondo limpio uniforme, recortable a alpha, estilo +consistente entre insignias del mismo set. Compone `comfyui_build_txt2img_workflow` + +`comfyui_inject_lora` (estilo opcional) + `Image Rembg` (fondo transparente si +`transparent`). Hermano de `comfyui_build_item_icon_workflow` / +`comfyui_build_skill_tree_node_workflow`. Pura, sin red ni I/O. class_types verificados +contra `/object_info`. + +## Ejemplo + +```python +import sys, os +sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions")) +from ml.comfyui_build_achievement_badge_workflow import comfyui_build_achievement_badge_workflow + +# Insignia de logro tier oro, medalla con cinta, fondo transparente (alpha). +wf = comfyui_build_achievement_badge_workflow( + "dragon slayer", + tier="gold", + style="game achievement badge, ornate", + transparent=True, + seed=12345, +) +# La familia de un mismo logro (los grados del panel): mismo badge/style/seed, +# solo cambia tier. El motor intercambia el sprite segun el grado conseguido. +# for t in ["bronze", "silver", "gold"]: +# wf = comfyui_build_achievement_badge_workflow("dragon slayer", tier=t, seed=12345) +# comfyui_submit_workflow(wf) # -> comfyui_wait_result -> comfyui_fetch_output_image +# Set completo coherente: misma firma de style/checkpoint por insignia, varia badge. +``` + +O lanzable directo con: `./fn run comfyui_build_achievement_badge_workflow` (imprime nodos + class_types del ejemplo). + +## Cuando usarla + +Cuando necesites las insignias del sistema de logros / recompensas de un juego: cada +achievement desbloqueado se muestra como una medalla, trofeo o escudo con cinta y un +grado metalico (bronce / plata / oro). Genera la **familia** `tier="bronze"` / +`"silver"` / `"gold"` con el mismo `badge`/`style`/`seed` para tener las caras +coherentes del mismo logro; el motor cambia el sprite segun el grado que el jugador +haya conseguido. Pasa el MISMO `style` + `checkpoint` + (`lora`) a todas las insignias +del set para que el panel de logros combine. `transparent` recorta la silueta de la +medalla + cinta (alpha) lista para el motor. + +Eligela frente a sus hermanos por el ROL del asset: +- **item_icon** -> objeto de inventario (espada, pocion): ilustracion de un objeto + suelto que el jugador usa/equipa, SIN tier ni cinta. +- **status_effect_icon** -> simbolo de estado compacto que se superpone al retrato/barra + del personaje (envenenado, aturdido), SIN marco; optimizado para 16-32 px. +- **skill_tree_node** -> nodo ENMARCADO de la pantalla de talentos con estado + unlocked/locked; vive en una rejilla de progresion. +- **achievement_badge (esta)** -> la INSIGNIA DE LOGRO/RECOMPENSA = trofeo/medalla con + cinta + tier (bronce/plata/oro). Se muestra en el panel de logros. + +## Gotchas + +- **El tier y la cinta son la firma**: lo que distingue este builder de + item_icon/skill_tree_node es que el asset es una medalla/trofeo con `tier` metalico y + cinta. El negativo por defecto NO rechaza "ribbon/medal/trophy" (son parte del asset), + pero si rechaza escenas recargadas, multiples badges y fondos sucios. Si el modelo + ignora el tier metalico, refuerza `style` con "{tier} medal, metallic sheen". +- **La familia de tiers debe compartir parametros**: para que los grados del mismo logro + coincidan en composicion, fija el mismo `badge`/`style`/`seed` y varia SOLO `tier`. La + pista metalica del tier (`bronze` -> bronze/copper; `silver` -> chrome/grey; `gold` -> + golden/yellow; `platinum`/`diamond` -> top tier) se anade automaticamente en el prompt. + Un tier desconocido se inserta literal pero la pista cae a la de `gold`. +- **Coherencia del set = mismos parametros**: si cambias `style`/`checkpoint`/`lora` + entre insignias, el panel deja de combinar. Fija esos y varia solo `badge` (y `tier`). +- **El recorte usa Rembg, NO luma-to-alpha**: una insignia es una pieza SOLIDA con + silueta definida (la medalla perfila el borde, la cinta cuelga), rembg la recorta + limpio. `comfyui_matting_luma_to_alpha` es para translucidos sobre negro (humo/fuego/ + runas brillantes) y aplanaria la medalla — no la uses para estas insignias. +- **El texto/nombre lo pone el motor, no la imagen**: el negativo por defecto empuja a + "no text/no letters/no numbers" para que la insignia quede limpia; el nombre del logro, + la descripcion y la fecha los renderiza el juego sobre el badge. +- **SDXL pide mas VRAM y resolucion**: con `checkpoint="juggernaut_xl_v11.safetensors"` + sube `size` a 512; con dreamshaper_8 (SD1.5) deja 256 (holgado en 8GB lowvram). +- `transparent=False` deja la insignia 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`. diff --git a/python/functions/ml/comfyui_build_achievement_badge_workflow.py b/python/functions/ml/comfyui_build_achievement_badge_workflow.py new file mode 100644 index 00000000..6d0766fa --- /dev/null +++ b/python/functions/ml/comfyui_build_achievement_badge_workflow.py @@ -0,0 +1,264 @@ +"""Construye el workflow ComfyUI de UNA insignia / medalla / logro (API format). + +Insignia de logro de juego (achievement, recompensa, rango): un trofeo, una +medalla con cinta, un escudo de logro, una estrella o un badge de rango que la UI +de achievements/recompensas pinta cuando el jugador desbloquea un hito. Tiene +TIER (bronce / plata / oro u otro grado) que distingue el nivel del logro, +centrado, fondo limpio uniforme, recortable a alpha, estilo consistente entre +insignias del mismo set. Es el builder hermano de comfyui_build_item_icon / +comfyui_build_skill_tree_node: mismo patron (PURO, dict API format) que compone +funciones existentes del registry, no reescribe el grafo. + +DISTINTO de item_icon, status_effect_icon y skill_tree_node: + - item_icon -> objeto de inventario (espada, pocion): ilustracion de un + objeto suelto que el jugador usa/equipa, SIN tier, SIN cinta. + - status_effect_icon -> simbolo de estado compacto que se superpone al retrato/barra + del personaje (envenenado, aturdido); SIN marco, 16-32 px. + - skill_tree_node -> nodo ENMARCADO de la pantalla de talentos con estado + unlocked/locked; vive en una rejilla de progresion. + - achievement_badge -> ESTO: la INSIGNIA DE LOGRO/RECOMPENSA = trofeo/medalla con + cinta + tier (bronce/plata/oro). El tier metalico y la forma + de medalla/trofeo son la firma del asset; se muestra en el + panel de logros, no en inventario ni en arbol de talentos. + +Por que importa el tier: un sistema de achievements muestra el MISMO logro en varios +grados (bronce / plata / oro) segun el progreso o la dificultad alcanzada. Generar la +familia (tier="bronze" / "silver" / "gold") con el mismo badge/style/seed da las +caras coherentes del mismo logro; el motor intercambia el sprite segun el tier que el +jugador haya conseguido. + +Cableado: + + CheckpointLoaderSimple -> [LoraLoader opcional de estilo] -> KSampler + -> CLIPTextEncode (prompt scaffold de insignia de logro) ... + -> 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 + +Por que Rembg y NO comfyui_matting_luma_to_alpha: una insignia es una pieza SOLIDA +con silueta definida (la medalla/trofeo perfila el borde, la cinta cuelga); rembg +recorta limpio la silueta dejando alpha. La luma-to-alpha es para translucidos sobre +negro (humo/fuego/runas brillantes) y aplanaria la medalla. 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 style + checkpoint + (lora) en todas las insignias del set hace que el panel +de logros combine visualmente: es la clave de un set coherente, igual que en los +iconos de inventario y los nodos del arbol de talentos. + +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 insignias de logro: una sola medalla/trofeo +# centrado y recortable, sin escena ni personaje ni texto/marcas que ensucien la +# silueta. NO rechaza "ribbon/medal/trophy" (son parte del asset), pero si empuja +# contra escenas recargadas, multiples badges y fondos sucios. +_BADGE_NEGATIVE = ( + "blurry, lowres, busy background, cluttered, multiple badges, multiple medals, " + "detailed scene, landscape, full body character, person, face, text, letters, " + "words, numbers, watermark, signature, photo, photorealistic, realistic, " + "jpeg artifacts, cropped, out of frame, deformed" +) + +# Pistas visuales por tier: refuerzan en el prompt el aspecto metalico de la medalla +# segun su grado. El nombre del tier tambien se inserta literal en el prompt para que +# sea explicito. Un tier desconocido cae a la pista de "gold". +_TIER_HINTS = { + "bronze": "bronze metal, copper tones, dark brown sheen, lowest tier", + "silver": "silver metal, polished chrome, cool grey sheen, mid tier", + "gold": "gold metal, shiny golden, warm yellow sheen, highest tier", + "platinum": "platinum metal, bright white-silver, prestige sheen, top tier", + "diamond": "diamond crystal, brilliant facets, prismatic shine, elite tier", +} + + +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_skill_tree_node_workflow: + el nodo recorta la silueta de la insignia (medalla + cinta) 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_achievement_badge_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_achievement_badge_workflow( + badge: str, + *, + tier: str = "gold", + style: str = "game achievement badge, ornate", + 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 = "achievement_badge", +) -> dict: + """Construye el dict (API format) del workflow de una insignia / medalla / logro. + + Args: + badge: nombre / tema del logro que representa la insignia (ej. "dragon slayer", + "first blood", "100 wins", "explorer", "marathon runner", "boss killer"). + Se inserta en un prompt scaffold de insignia. No puede estar vacio. + tier: grado del logro: "bronze", "silver", "gold" (por defecto), "platinum" o + "diamond". Define el aspecto metalico de la medalla. Genera la familia con + el mismo badge/style/seed variando solo el tier para tener las caras + coherentes del mismo logro. Cualquier otro valor se inserta literal (el + tier hint cae al de "gold"). keyword-only. + style: descriptor de estilo que mantiene consistentes las insignias de un set + (ej. "game achievement badge, ornate", "flat minimal medal", "pixel art + trophy"). Pasa el MISMO style + checkpoint + (lora) a todas las insignias + 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). keyword-only. + size: lado del cuadrado en px (width = height = size). 256 por defecto: las + insignias de logro se muestran a tamano reducido en el panel. keyword-only. + transparent: si True inyecta Rembg y el PNG sale con alpha (fondo recortado, + la silueta de la medalla + cinta). Si False deja la insignia opaca sobre + fondo plano, recortable luego por el caller/pipeline. keyword-only. + seed: semilla del KSampler. Fija el mismo seed en la familia de tiers del mismo + logro para que coincidan en composicion. 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 + insignias (una medalla limpia, 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 insignia ('{badge} achievement badge, {tier} tier + ({tier hint}), {style}, medal with ribbon, centered, plain background, game UI + reward, ...') + LoRA de estilo opcional + Rembg (si transparent). Es UNA + insignia; un set de logros -> llamar por badge (y por tier) con el mismo + style/checkpoint/lora y montar el panel en el motor. + + Raises: + ValueError: si badge 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 badge or not badge.strip(): + raise ValueError( + "comfyui_build_achievement_badge_workflow: 'badge' no puede estar vacio" + ) + + lora_strength = max(0.0, min(2.0, float(lora_strength))) + neg = _BADGE_NEGATIVE if negative is None else negative + + badge_s = badge.strip() + tier_s = (tier or "gold").strip() + style_s = (style or "game achievement badge, ornate").strip() + # Pista de aspecto metalico segun tier; tier desconocido -> aspecto de gold. + tier_hint = _TIER_HINTS.get(tier_s.lower(), _TIER_HINTS["gold"]) + + # Prompt scaffold de insignia de logro: medalla/trofeo con cinta, tier metalico, + # centrado, fondo plano, recortable. badge/tier/style quedan reflejados literalmente. + positive = ( + f"{badge_s} achievement badge, {tier_s} tier ({tier_hint}), {style_s}, " + "medal with ribbon, centered, plain background, game UI reward, " + "trophy emblem, 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_achievement_badge_workflow( + "dragon slayer", + tier="gold", + style="game achievement badge, ornate", + 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_achievement_badge_workflow_test.py b/python/functions/ml/comfyui_build_achievement_badge_workflow_test.py new file mode 100644 index 00000000..273ffb9c --- /dev/null +++ b/python/functions/ml/comfyui_build_achievement_badge_workflow_test.py @@ -0,0 +1,152 @@ +"""Tests offline (sin red, sin GPU) de comfyui_build_achievement_badge_workflow. + +Verifican que el dict en API format se construye correctamente: clases presentes, +cableado del Rembg, prompt scaffold de insignia de logro, y reflejo de los argumentos +(badge, tier, 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_achievement_badge_workflow import ( # noqa: E402 + comfyui_build_achievement_badge_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: insignia transparente -> Rembg cableado, prompt de logro, clases base.""" + wf = comfyui_build_achievement_badge_workflow( + "dragon slayer", tier="gold", 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 "dragon slayer" in prompt + assert "achievement badge" in prompt + assert "gold tier" in prompt + assert "medal with ribbon" in prompt + assert "centered" in prompt + assert "game UI reward" 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_achievement_badge_workflow("first blood", 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 (insignia compacta).""" + wf = comfyui_build_achievement_badge_workflow("explorer", 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_achievement_badge_workflow("explorer") + 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_tier_gold_default_hint(): + """tier='gold' (default) se refleja literal y arrastra la pista metalica de oro.""" + wf = comfyui_build_achievement_badge_workflow("boss killer") + prompt = _positive_prompt(wf) + assert "gold tier" in prompt + assert "gold metal" in prompt + + +def test_edge_tier_bronze_reflected(): + """tier='bronze' se refleja literal y arrastra la pista metalica de bronce.""" + wf = comfyui_build_achievement_badge_workflow("rookie", tier="bronze") + prompt = _positive_prompt(wf) + assert "bronze tier" in prompt + assert "bronze metal" in prompt + + +def test_edge_tier_silver_reflected(): + """tier='silver' se refleja literal y arrastra la pista metalica de plata.""" + wf = comfyui_build_achievement_badge_workflow("veteran", tier="silver") + prompt = _positive_prompt(wf) + assert "silver tier" in prompt + assert "silver metal" in prompt + + +def test_edge_tier_unknown_falls_back_to_gold_hint(): + """tier desconocido se inserta literal pero la pista cae a la de gold.""" + wf = comfyui_build_achievement_badge_workflow("mythic", tier="legendary") + prompt = _positive_prompt(wf) + assert "legendary tier" in prompt + assert "gold metal" in prompt # hint fallback + + +def test_edge_style_reflected(): + """style se inserta en el prompt positivo.""" + wf = comfyui_build_achievement_badge_workflow( + "speedrun", style="pixel art trophy" + ) + assert "pixel art trophy" in _positive_prompt(wf) + + +def test_edge_badge_reflected(): + """badge se inserta literal en el prompt positivo.""" + wf = comfyui_build_achievement_badge_workflow("marathon runner") + assert "marathon runner" in _positive_prompt(wf) + + +def test_edge_lora_injected(): + """lora -> LoraLoader presente con la fuerza dada.""" + wf = comfyui_build_achievement_badge_workflow( + "collector", 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_badge(): + """badge vacio -> ValueError.""" + with pytest.raises(ValueError): + comfyui_build_achievement_badge_workflow(" ") + + +def test_determinism(): + """Mismos argumentos -> mismo dict (funcion pura).""" + a = comfyui_build_achievement_badge_workflow("undefeated", seed=7) + b = comfyui_build_achievement_badge_workflow("undefeated", seed=7) + assert a == b