feat(gamedev): comfyui_build_skill_tree_node_workflow — nodos de arbol de habilidades/talentos

Builder puro del grupo gamedev-2d: nodo de skill tree (icono de habilidad dentro de
un marco circular/hexagonal/rombo/escudo) con variante de estado visual (unlocked
brillante / locked gris), centrado, recortable a alpha. Diferenciado de item_icon
(objeto suelto sin marco), status_effect_icon (simbolo superpuesto sin marco) y
ui_hud (chrome grande): el marco y el estado son la firma del asset. Compone
comfyui_build_txt2img_workflow + comfyui_inject_lora + Image Rembg, sin reescribir el
grafo. 11 tests offline en verde. Probado e2e SD1.5 8GB lowvram: fireball hexagonal
unlocked 256x256 RGBA, prompt_id cf36b2ea, nodo enmarcado brillante centrado
(reports/0173).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-27 02:03:07 +02:00
parent 5a0818ee9c
commit 0ce1c31fb9
4 changed files with 556 additions and 0 deletions
+1
View File
@@ -43,6 +43,7 @@ VFX (ver `reports/0143`).
| `comfyui_build_ui_hud_workflow_py_ml` | `(element, *, ui_style="fantasy game UI", checkpoint="dreamshaper_8…", size=512, transparent=True, lora=None, …) -> dict` | UN elemento de interfaz/HUD de juego (botón, marco/panel, barra de vida/maná/XP, icono de UI, cursor, viñeta de menú): txt2img cuadrado + prompt scaffold de UI (`{element}, {ui_style}, game UI element, centered, clean, plain background…`) + LoRA estilo opcional + Rembg (alpha). HUD coherente = mismo `ui_style`/`checkpoint`/`lora` por pieza, varía solo `element`. El texto/label lo pone el motor (negativo empuja a `no text`). Probado e2e en GPU (`reports/0152`). SD1.5. |
| `comfyui_build_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_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_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,143 @@
---
name: comfyui_build_skill_tree_node_workflow
kind: function
lang: py
domain: ml
version: "1.0.0"
purity: pure
signature: "def comfyui_build_skill_tree_node_workflow(skill: str, *, frame: str = \"hexagonal\", state: str = \"unlocked\", ui_style: str = \"fantasy skill tree node\", 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 = \"skill_tree_node\") -> dict"
description: "Construye el dict (API format) del workflow de UN nodo de arbol de habilidades / talentos 2D (RPG, ARPG, MOBA, roguelike): el icono de una skill DENTRO de un marco decorativo (circular, hexagonal, rombo, escudo) que la UI de progresion pinta en la rejilla de talentos, con variante de ESTADO visual (desbloqueado brillante / bloqueado gris). Centrado, fondo limpio uniforme, recortable a alpha, estilo consistente entre nodos del arbol. DISTINTO de item_icon (objeto de inventario sin marco), status_effect_icon (simbolo de estado superpuesto sin marco) y ui_hud (chrome grande suelto): esto es el nodo ENMARCADO completo de la pantalla de talentos. El marco y el estado 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_status_effect_icon/ui_hud_workflow. Pura, sin red ni I/O. class_types verificados contra /object_info."
tags: [comfyui, ml, gamedev, gamedev-2d, ui, skill, talent, tree, progression, node, frame, 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: skill
desc: "Nombre de la habilidad/talento que va dentro del nodo (ej. 'fireball', 'double jump', 'critical strike', 'healing aura', 'stealth', 'lightning bolt'). Se inserta en un prompt scaffold de nodo de skill tree. No puede estar vacio."
- name: frame
desc: "Forma del marco del nodo (ej. 'hexagonal', 'circular', 'diamond', 'shield', 'square ornate'). Define la silueta enmarcada que perfila el nodo en la rejilla de talentos. Pasa el MISMO frame a todos los nodos del arbol para coherencia. keyword-only."
- name: state
desc: "Estado visual del nodo: 'unlocked' (disponible/comprado: brillante, saturado, resaltado) o 'locked' (bloqueado: gris, desaturado, atenuado). Genera el par con el mismo skill/frame/ui_style/seed para tener las dos caras del mismo talento. Cualquier otro valor se inserta literal y la pista visual cae a la de 'unlocked'. keyword-only."
- name: ui_style
desc: "Descriptor de estilo de UI que mantiene consistentes los nodos de un arbol (ej. 'fantasy skill tree node', 'sci-fi tech tree, neon', 'dark souls talent node', 'minimal flat skill node'). Pasa el MISMO ui_style + frame + checkpoint + lora a todos los nodos 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: los nodos de talento se muestran a tamano reducido en la rejilla. keyword-only."
- name: transparent
desc: "Si True inyecta Image Rembg y el PNG sale con alpha (fondo recortado, la silueta del marco). False = nodo opaco sobre fondo plano, recortable luego por el caller. keyword-only."
- name: seed
desc: "Semilla del KSampler. Fija el mismo seed en el par unlocked/locked del mismo talento para que coincidan. 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 nodos de skill tree (icono enmarcado limpio, 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 nodo de skill tree ('{skill} skill icon inside a {frame} {ui_style} frame, {state} ({state hint}), centered, plain background, game UI, ...') + LoRA de estilo opcional + Image Rembg (si transparent). UN nodo; un arbol completo -> llamar por skill (y por state) con mismo frame/ui_style/checkpoint/lora y montar la rejilla en el motor."
tested: true
test_file_path: python/functions/ml/comfyui_build_skill_tree_node_workflow_test.py
tests: [test_golden_transparent, test_edge_transparent_false_no_rembg, test_edge_size_reflected_square, test_edge_frame_reflected, test_edge_state_locked_reflected, test_edge_state_unlocked_hint, test_edge_ui_style_reflected, test_edge_skill_reflected, test_edge_lora_injected, test_error_empty_skill, test_determinism]
file_path: python/functions/ml/comfyui_build_skill_tree_node_workflow.py
---
Construye el dict (API format) del workflow de UN nodo de arbol de habilidades /
talentos 2D: el icono de una skill DENTRO de un marco decorativo (circular,
hexagonal, rombo, escudo) que la UI de progresion pinta en la rejilla de talentos,
con variante de ESTADO visual (desbloqueado brillante / bloqueado gris). Centrado,
fondo limpio uniforme, recortable a alpha, estilo de UI consistente entre nodos del
mismo arbol. Compone `comfyui_build_txt2img_workflow` + `comfyui_inject_lora` (estilo
opcional) + `Image Rembg` (fondo transparente si `transparent`). Hermano de
`comfyui_build_status_effect_icon_workflow` / `comfyui_build_ui_hud_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_skill_tree_node_workflow import comfyui_build_skill_tree_node_workflow
# Nodo de skill desbloqueado, marco hexagonal, fondo transparente (alpha).
wf = comfyui_build_skill_tree_node_workflow(
"fireball",
frame="hexagonal",
state="unlocked",
ui_style="fantasy skill tree node",
transparent=True,
seed=12345,
)
# El par de un mismo talento (las dos caras de la rejilla): mismo skill/frame/seed,
# solo cambia state. El motor intercambia el sprite segun el progreso del jugador.
# for st in ["unlocked", "locked"]:
# wf = comfyui_build_skill_tree_node_workflow("fireball", frame="hexagonal",
# state=st, seed=12345)
# comfyui_submit_workflow(wf) # -> comfyui_wait_result -> comfyui_fetch_output_image
# Arbol completo coherente: misma firma de frame/ui_style por nodo, varia skill.
```
O lanzable directo con: `./fn run comfyui_build_skill_tree_node_workflow` (imprime nodos + class_types del ejemplo).
## Cuando usarla
Cuando necesites los nodos de la pantalla de talentos / arbol de habilidades de un
juego (RPG, ARPG, MOBA, roguelike): cada celda de la rejilla de progresion es el icono
de una skill enmarcado, con un aspecto cuando esta disponible/comprado (brillante) y
otro cuando esta bloqueado (gris). Genera el **par** `state="unlocked"` /
`state="locked"` con el mismo `skill`/`frame`/`ui_style`/`seed` para tener las dos
caras del mismo talento; el motor cambia el sprite segun el progreso. Pasa el MISMO
`frame` + `ui_style` + `checkpoint` + (`lora`) a todos los nodos para que la rejilla
combine. `transparent` recorta la silueta del marco (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, SIN marco.
- **status_effect_icon** -> simbolo de estado compacto que se superpone al retrato/barra
del personaje, SIN marco; optimizado para 16-32 px.
- **ui_hud** -> chrome grande/suelto de la interfaz (botones, barras, marcos vacios);
no es el nodo de talento completo.
- **skill_tree_node (esta)** -> el nodo ENMARCADO completo de la pantalla de talentos =
icono de skill + marco + estado (unlocked/locked).
## Gotchas
- **El marco y el estado son la firma**: lo que distingue este builder de
item_icon/status_effect_icon es que el icono va DENTRO de un `frame` y arrastra un
`state`. El negativo por defecto NO rechaza "frame/border" (el marco es parte del
asset), pero si rechaza escenas recargadas, multiples nodos y fondos sucios. Si el
modelo ignora el marco, refuerza `ui_style` con "ornate {frame} frame, bordered".
- **El par unlocked/locked debe compartir parametros**: para que las dos caras del
mismo talento coincidan, fija el mismo `skill`/`frame`/`ui_style`/`seed` y varia
SOLO `state`. La pista visual del estado (`unlocked` -> glowing/vibrant; `locked` ->
greyed out/desaturated) se anade automaticamente en el prompt.
- **Coherencia del arbol = mismos parametros**: si cambias `frame`/`ui_style`/
`checkpoint`/`lora` entre nodos, la rejilla deja de combinar. Fija esos y varia solo
`skill` (y `state`).
- **El recorte usa Rembg, NO luma-to-alpha**: un nodo es una pieza SOLIDA con silueta
definida (el marco perfila el borde), rembg lo recorta limpio. `comfyui_matting_luma_to_alpha`
es para translucidos sobre negro (humo/fuego/runas brillantes) y aplanaria el marco
— no la uses para estos nodos.
- **El texto/nombre lo pone el motor, no la imagen**: el negativo por defecto empuja a
"no text/no letters/no numbers" para que el nodo quede limpio; el nombre del talento,
el coste y los rangos los renderiza el juego sobre el nodo.
- **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 el nodo 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`.
@@ -0,0 +1,270 @@
"""Construye el workflow ComfyUI de UN nodo de arbol de habilidades / talentos (API format).
Nodo de skill tree de juego (RPG, ARPG, MOBA, roguelike): el icono de una
habilidad/talento DENTRO de un marco decorativo (circular, hexagonal, rombo,
escudo) que la UI de progresion pinta en la rejilla de talentos, con variante de
ESTADO visual (desbloqueado = brillante/iluminado / bloqueado = gris/apagado).
Centrado, fondo limpio uniforme, recortable a alpha, estilo de UI consistente entre
nodos del mismo arbol. Es el builder hermano de comfyui_build_status_effect_icon /
comfyui_build_ui_hud_workflow: mismo patron (PURO, dict API format) que compone
funciones existentes del registry, no reescribe el grafo.
DISTINTO de item_icon, status_effect_icon y ui_hud:
- item_icon -> objeto de inventario (espada, pocion): ilustracion de un
objeto suelto, SIN marco.
- status_effect_icon-> simbolo de estado compacto que se superpone al retrato/barra
del personaje, SIN marco; optimizado para 16-32 px.
- ui_hud -> chrome grande/suelto de la interfaz (botones, barras, marcos
vacios); no es el nodo de talento completo.
- skill_tree_node -> ESTO: el nodo ENMARCADO de la pantalla de talentos = icono de
skill + marco + estado (unlocked/locked). El marco y el estado
son la firma del asset.
Por que importa el estado: una pantalla de talentos muestra el MISMO nodo en dos
apariencias — disponible/comprado (brillante, saturado, resaltado) y bloqueado (gris,
desaturado, atenuado). Generar el par (state="unlocked" / state="locked") con el mismo
skill/frame/ui_style/seed da las dos caras coherentes del mismo talento; el motor
intercambia el sprite segun el progreso del jugador.
Cableado:
CheckpointLoaderSimple -> [LoraLoader opcional de estilo] -> KSampler
-> CLIPTextEncode (prompt scaffold de nodo de skill tree) ...
-> VAEDecode -> [Image Rembg opcional] -> SaveImage
Compone:
- comfyui_build_txt2img_workflow -> base txt2img cuadrada
- comfyui_inject_lora -> LoRA de estilo opcional (consistencia del arbol)
- 'Image Rembg (Remove Background)' (helper local) -> fondo transparente
Por que Rembg y NO comfyui_matting_luma_to_alpha: un nodo de skill tree es una pieza
SOLIDA con silueta definida (el marco perfila el borde); rembg recorta limpio la
silueta del marco dejando alpha. La luma-to-alpha es para translucidos sobre negro
(humo/fuego/runas brillantes) y aplanaria el marco. 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 frame + ui_style + checkpoint + (lora) en todos los nodos del arbol hace que
la rejilla de talentos combine visualmente: es la clave de un skill tree coherente,
igual que en los iconos de inventario y los elementos del HUD.
class_types/inputs verificados contra /object_info del servidor (8GB lowvram):
CheckpointLoaderSimple, CLIPTextEncode, EmptyLatentImage, KSampler, VAEDecode,
SaveImage, LoraLoader, 'Image Rembg (Remove Background)' (transparency BOOLEAN).
Funcion pura: sin red, sin I/O. No muta dicts de entrada (copia profunda en el
helper de rembg). Determinista para los mismos argumentos.
"""
from __future__ import annotations
import copy
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
# Negativo por defecto pensado para nodos de skill tree: un icono enmarcado limpio,
# centrado y recortable, sin escena ni personaje ni texto/marcas que ensucien la
# silueta del marco. NO rechaza "frame/border" (el marco es parte del asset), pero si
# empuja contra escenas recargadas, multiples nodos y fondos sucios.
_SKILL_TREE_NEGATIVE = (
"blurry, lowres, busy background, cluttered, multiple nodes, multiple icons, "
"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 estado: refuerzan en el prompt el aspecto del nodo segun este
# disponible/comprado (unlocked) o bloqueado (locked) en la pantalla de talentos. El
# nombre del estado tambien se inserta literal en el prompt para que sea explicito.
_STATE_HINTS = {
"unlocked": "glowing, illuminated, vibrant, saturated, highlighted, active",
"locked": "greyed out, desaturated, dimmed, dark, muted, inactive",
}
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_status_effect_icon_workflow / comfyui_build_ui_hud_workflow:
el nodo recorta la silueta del nodo enmarcado 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_skill_tree_node_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_skill_tree_node_workflow(
skill: str,
*,
frame: str = "hexagonal",
state: str = "unlocked",
ui_style: str = "fantasy skill tree node",
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 = "skill_tree_node",
) -> dict:
"""Construye el dict (API format) del workflow de un nodo de arbol de habilidades.
Args:
skill: nombre de la habilidad/talento que va dentro del nodo (ej. "fireball",
"double jump", "critical strike", "healing aura", "stealth", "lightning
bolt"). Se inserta en un prompt scaffold de nodo de skill tree. No puede
estar vacio.
frame: forma del marco del nodo (ej. "hexagonal", "circular", "diamond",
"shield", "square ornate"). Define la silueta enmarcada que perfila el
nodo en la rejilla de talentos. Pasa el MISMO frame a todos los nodos del
arbol para coherencia. keyword-only.
state: estado visual del nodo: "unlocked" (disponible/comprado: brillante,
saturado, resaltado) o "locked" (bloqueado: gris, desaturado, atenuado).
Genera el par con el mismo skill/frame/ui_style/seed para tener las dos
caras del mismo talento. Cualquier otro valor se inserta literal (el state
hint cae al de "unlocked"). keyword-only.
ui_style: descriptor de estilo de UI que mantiene consistentes los nodos de un
arbol (ej. "fantasy skill tree node", "sci-fi tech tree, neon", "dark
souls talent node", "minimal flat skill node"). Pasa el MISMO ui_style +
frame + checkpoint + (lora) a todos los nodos 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: los
nodos de talento se muestran a tamano reducido en la rejilla. keyword-only.
transparent: si True inyecta Rembg y el PNG sale con alpha (fondo recortado,
la silueta del marco). Si False deja el nodo opaco sobre fondo plano,
recortable luego por el caller/pipeline. keyword-only.
seed: semilla del KSampler. Fija el mismo seed en el par unlocked/locked del
mismo talento para que coincidan. 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 nodos
de skill tree (icono enmarcado limpio, 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 nodo de skill tree ('{skill} skill icon inside a {frame}
{ui_style} frame, {state} ({state hint}), centered, plain background, game
UI, ...') + LoRA de estilo opcional + Rembg (si transparent). Es UN nodo; un
arbol completo -> llamar por skill (y por state) con el mismo frame/ui_style/
checkpoint/lora y montar la rejilla en el motor.
Raises:
ValueError: si skill 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 skill or not skill.strip():
raise ValueError(
"comfyui_build_skill_tree_node_workflow: 'skill' no puede estar vacio"
)
lora_strength = max(0.0, min(2.0, float(lora_strength)))
neg = _SKILL_TREE_NEGATIVE if negative is None else negative
skill_s = skill.strip()
frame_s = (frame or "hexagonal").strip()
state_s = (state or "unlocked").strip()
ui_style_s = (ui_style or "fantasy skill tree node").strip()
# Pista de aspecto segun estado; estado desconocido -> aspecto de unlocked.
state_hint = _STATE_HINTS.get(state_s.lower(), _STATE_HINTS["unlocked"])
# Prompt scaffold de nodo de skill tree: icono de skill DENTRO de un marco, con el
# estado (desbloqueado brillante / bloqueado gris), centrado, fondo plano,
# recortable. skill/frame/state/ui_style quedan reflejados literalmente.
positive = (
f"{skill_s} skill icon inside a {frame_s} {ui_style_s} frame, "
f"{state_s} ({state_hint}), centered, plain background, game UI, "
"skill tree talent node, 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_skill_tree_node_workflow(
"fireball",
frame="hexagonal",
state="unlocked",
ui_style="fantasy skill tree node",
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,142 @@
"""Tests offline (sin red, sin GPU) de comfyui_build_skill_tree_node_workflow.
Verifican que el dict en API format se construye correctamente: clases presentes,
cableado del Rembg, prompt scaffold de nodo de skill tree, y reflejo de los argumentos
(skill, frame, state, ui_style, size, transparent, lora). No tocan el servidor ComfyUI.
"""
import os
import sys
import pytest
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from ml.comfyui_build_skill_tree_node_workflow import ( # noqa: E402
comfyui_build_skill_tree_node_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: nodo transparente -> Rembg cableado, prompt de skill tree, clases base."""
wf = comfyui_build_skill_tree_node_workflow(
"fireball", frame="hexagonal", state="unlocked", 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 "fireball" in prompt
assert "skill icon inside" in prompt
assert "hexagonal" in prompt
assert "unlocked" in prompt
assert "centered" in prompt
assert "skill tree talent node" 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_skill_tree_node_workflow("double jump", 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 (nodo compacto)."""
wf = comfyui_build_skill_tree_node_workflow("stealth", 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_skill_tree_node_workflow("stealth")
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_frame_reflected():
"""frame se inserta literal en el prompt positivo."""
wf = comfyui_build_skill_tree_node_workflow("critical strike", frame="diamond")
assert "diamond" in _positive_prompt(wf)
def test_edge_state_locked_reflected():
"""state='locked' se refleja literal y arrastra la pista visual de bloqueado."""
wf = comfyui_build_skill_tree_node_workflow("healing aura", state="locked")
prompt = _positive_prompt(wf)
assert "locked" in prompt
assert "greyed out" in prompt
def test_edge_state_unlocked_hint():
"""state='unlocked' (default) arrastra la pista visual de brillante."""
wf = comfyui_build_skill_tree_node_workflow("lightning bolt")
prompt = _positive_prompt(wf)
assert "unlocked" in prompt
assert "glowing" in prompt
def test_edge_ui_style_reflected():
"""ui_style se inserta en el prompt positivo."""
wf = comfyui_build_skill_tree_node_workflow(
"overcharge", ui_style="sci-fi tech tree, neon"
)
assert "sci-fi tech tree, neon" in _positive_prompt(wf)
def test_edge_skill_reflected():
"""skill se inserta literal en el prompt positivo."""
wf = comfyui_build_skill_tree_node_workflow("backstab")
assert "backstab" in _positive_prompt(wf)
def test_edge_lora_injected():
"""lora -> LoraLoader presente con la fuerza dada."""
wf = comfyui_build_skill_tree_node_workflow(
"whirlwind", 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_skill():
"""skill vacio -> ValueError."""
with pytest.raises(ValueError):
comfyui_build_skill_tree_node_workflow(" ")
def test_determinism():
"""Mismos argumentos -> mismo dict (funcion pura)."""
a = comfyui_build_skill_tree_node_workflow("dash", seed=7)
b = comfyui_build_skill_tree_node_workflow("dash", seed=7)
assert a == b