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:
@@ -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
|
||||
Reference in New Issue
Block a user