feat(gamedev): comfyui_build_achievement_badge_workflow — insignias/medallas/logros

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).
This commit is contained in:
2026-06-27 02:18:12 +02:00
parent fa94f7a235
commit 2a7c77cb56
4 changed files with 557 additions and 0 deletions
+1
View File
@@ -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_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_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_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<height`, ~512×768), composición centrada + iluminación dramática (`{subject}, {card_style}, dramatic lighting, detailed illustration, centered composition, full art…`). `hires=True` → 2ª pasada de detalle (`comfyui_build_hires_fix_workflow`); si no, txt2img + LoRA estilo opcional. Genera SOLO la ilustración — el marco/título/stats los pone el motor/post (negativo rechaza `card frame/border/text/stats/UI`). Set coherente = mismo `card_style`/`checkpoint`/`lora`, varía solo `subject`. Probado e2e en GPU con SD1.5 (`reports/0153`); ⚠️ el path `hires=True` falla hoy por bug del builder `comfyui_build_hires_fix_workflow` (nodo `UltimateSDUpscale` pide `batch_size`) — usar `hires=False` hasta el fix. SD1.5/SDXL. | | `comfyui_build_card_art_workflow_py_ml` | `(subject, *, card_style="fantasy trading card art", checkpoint="juggernaut_xl_v11…", width=512, height=768, hires=True, seed=0, lora=None, …) -> dict` | LA ilustración central de UNA carta coleccionable (TCG): criatura/personaje/hechizo en formato **vertical** de carta (`width<height`, ~512×768), composición centrada + iluminación dramática (`{subject}, {card_style}, dramatic lighting, detailed illustration, centered composition, full art…`). `hires=True` → 2ª pasada de detalle (`comfyui_build_hires_fix_workflow`); si no, txt2img + LoRA estilo opcional. Genera SOLO la ilustración — el marco/título/stats los pone el motor/post (negativo rechaza `card frame/border/text/stats/UI`). Set coherente = mismo `card_style`/`checkpoint`/`lora`, varía solo `subject`. Probado e2e en GPU con SD1.5 (`reports/0153`); ⚠️ el path `hires=True` falla hoy por bug del builder `comfyui_build_hires_fix_workflow` (nodo `UltimateSDUpscale` pide `batch_size`) — usar `hires=False` hasta el fix. SD1.5/SDXL. |
| `comfyui_build_enemy_creature_workflow_py_ml` | `(creature, *, variant=None, style="game creature, full body", checkpoint="dreamshaper_8…", size=512, transparent=True, seed=0, lora=None, …) -> dict` | UN enemigo/criatura de juego (goblin, esqueleto, slime, dragón, boss, elemental): figura de **cuerpo entero** centrada, fondo limpio recortable a alpha (`{variant} {creature}, {style}, full body, centered, plain background, game asset…`) → txt2img cuadrado + LoRA estilo opcional + Rembg (alpha). `variant` (ice/fire/elite/corrupted…) se antepone a la criatura para generar la familia del MISMO enemigo (misma `creature`/`seed`/`style`, varía solo `variant`); bestiario coherente = mismo `style`/`checkpoint`/`lora`, varía solo `creature`. El negativo empuja a UNA criatura entera sin recorte. Probado e2e en GPU con SD1.5 (`reports/0154`). SD1.5. | | `comfyui_build_enemy_creature_workflow_py_ml` | `(creature, *, variant=None, style="game creature, full body", checkpoint="dreamshaper_8…", size=512, transparent=True, seed=0, lora=None, …) -> dict` | UN enemigo/criatura de juego (goblin, esqueleto, slime, dragón, boss, elemental): figura de **cuerpo entero** centrada, fondo limpio recortable a alpha (`{variant} {creature}, {style}, full body, centered, plain background, game asset…`) → txt2img cuadrado + LoRA estilo opcional + Rembg (alpha). `variant` (ice/fire/elite/corrupted…) se antepone a la criatura para generar la familia del MISMO enemigo (misma `creature`/`seed`/`style`, varía solo `variant`); bestiario coherente = mismo `style`/`checkpoint`/`lora`, varía solo `creature`. El negativo empuja a UNA criatura entera sin recorte. Probado e2e en GPU con SD1.5 (`reports/0154`). SD1.5. |
| `comfyui_build_prop_object_workflow_py_ml` | `(prop, *, style="game prop, isometric or side view", checkpoint="dreamshaper_8…", size=512, transparent=True, seed=0, lora=None, …) -> dict` | UN prop/objeto de escenario (barril, cofre, antorcha, planta, mueble, roca, fuente, estatua): objeto inanimado aislado a **escala de escena y perspectiva de juego** (iso/lateral), centrado, fondo limpio recortable a alpha (`{prop}, {style}, game asset, single object, centered, plain background, scene prop, world object…`) → txt2img cuadrado + LoRA estilo opcional + Rembg (alpha). **Objeto de MUNDO**, no icono plano de inventario (≠ `item_icon`, que es para una casilla de UI); este puebla el nivel. Atrezzo coherente = mismo `style`/`checkpoint`/`lora`, varía solo `prop`. El negativo excluye personas/criaturas (objeto inanimado). Probado e2e en GPU con SD1.5 (`reports/0155`). SD1.5. | | `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. |
@@ -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`.
@@ -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,
)
)
@@ -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