feat(gamedev): comfyui_build_dialogue_box_workflow — caja de diálogo/bocadillo/panel de texto

Builder del contenedor de diálogo de juego (RPG, visual novel, aventura): marco
apaisado (768x256) con borde decorativo e interior plano reservado para el texto
del motor. Distinto de ui_hud (elementos sueltos): es el panel-contenedor completo.
Compone txt2img + inject_lora + Image Rembg (alpha). Pura, dict API format.
7 tests offline verdes; 1 generación real en GPU (medieval wood+gold, 768x256 RGBA).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-27 01:48:00 +02:00
parent ba302dd793
commit 1a8093a7be
4 changed files with 461 additions and 0 deletions
+1
View File
@@ -41,6 +41,7 @@ VFX (ver `reports/0143`).
| `comfyui_build_parallax_background_workflow_py_ml` | `(scene, *, style="game background, side-scroller…", layers=3, checkpoint="dreamshaper_8…", depth_node="DepthAnythingV2Preprocessor", width=1024, height=512, …) -> dict` | Fondo en capas para parallax 2.5D: genera el fondo apaisado (txt2img) + su depth map (`DepthAnythingV2Preprocessor` sobre el VAEDecode), dos SaveImage. El split en N bandas por profundidad es post (GAP: `split_parallax_layers`, aún no creada). Probado e2e en GPU (`reports/0149`). SD1.5. | | `comfyui_build_parallax_background_workflow_py_ml` | `(scene, *, style="game background, side-scroller…", layers=3, checkpoint="dreamshaper_8…", depth_node="DepthAnythingV2Preprocessor", width=1024, height=512, …) -> dict` | Fondo en capas para parallax 2.5D: genera el fondo apaisado (txt2img) + su depth map (`DepthAnythingV2Preprocessor` sobre el VAEDecode), dos SaveImage. El split en N bandas por profundidad es post (GAP: `split_parallax_layers`, aún no creada). Probado e2e en GPU (`reports/0149`). SD1.5. |
| `comfyui_build_normal_map_workflow_py_ml` | `(image, *, method="normal", strength=1.0, resolution=512, bg_threshold=0.1, filename_prefix="normal_map") -> dict` | Normal/depth map de un sprite existente para iluminación dinámica 2.5D (Godot CanvasItem `normal_map`, Unity sprite normal). `LoadImage → preprocesador controlnet_aux → SaveImage`. `method`: `normal` (default, `BAE-NormalMapPreprocessor`, normal canónico **azul/violeta** usable directo en motor), `normal_midas` (MiDaS, único con `strength``a`, paleta no canónica), `normal_dsine` (DSINE), `depth` (`DepthAnythingV2`, height en gris). `image` debe estar en `input/` de ComfyUI. Coste VRAM ≈0. Probado e2e en GPU (`reports/0150`). | | `comfyui_build_normal_map_workflow_py_ml` | `(image, *, method="normal", strength=1.0, resolution=512, bg_threshold=0.1, filename_prefix="normal_map") -> dict` | Normal/depth map de un sprite existente para iluminación dinámica 2.5D (Godot CanvasItem `normal_map`, Unity sprite normal). `LoadImage → preprocesador controlnet_aux → SaveImage`. `method`: `normal` (default, `BAE-NormalMapPreprocessor`, normal canónico **azul/violeta** usable directo en motor), `normal_midas` (MiDaS, único con `strength``a`, paleta no canónica), `normal_dsine` (DSINE), `depth` (`DepthAnythingV2`, height en gris). `image` debe estar en `input/` de ComfyUI. Coste VRAM ≈0. Probado e2e en GPU (`reports/0150`). |
| `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_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_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_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. |
@@ -0,0 +1,122 @@
---
name: comfyui_build_dialogue_box_workflow
kind: function
lang: py
domain: ml
version: "1.0.0"
purity: pure
signature: "def comfyui_build_dialogue_box_workflow(box_style: str = \"fantasy RPG dialogue box\", *, shape: str = \"rounded panel\", checkpoint: str = \"dreamshaper_8.safetensors\", width: int = 768, height: 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 = \"dialogue_box\") -> dict"
description: "Construye el dict (API format) del workflow de UNA caja de dialogo / bocadillo / panel de texto de juego (RPG, visual novel, aventura): el contenedor de UI apaisado con borde decorativo y un interior plano y vacio reservado para que el motor de juego renderice el texto de la conversacion encima. DISTINTO de comfyui_build_ui_hud_workflow (elementos sueltos: botones/barras/iconos): esto es el contenedor de dialogo completo. Compone comfyui_build_txt2img_workflow + comfyui_inject_lora (estilo opcional) + Image Rembg (fondo transparente si transparent). Hermano de comfyui_build_ui_hud/card_art_workflow. Pura, sin red ni I/O. class_types verificados contra /object_info."
tags: [comfyui, ml, gamedev, gamedev-2d, ui, dialogue, dialogue-box, text-box, panel, 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: box_style
desc: "Descriptor del estilo del contenedor de dialogo (ej. 'fantasy RPG dialogue box', 'medieval fantasy dialogue box, wood and gold', 'sci-fi terminal text box, neon glow', 'visual novel text panel, soft pastel'). Se inserta en un prompt scaffold de caja de dialogo. No puede estar vacio."
- name: shape
desc: "Forma del panel (ej. 'rounded panel', 'rectangular banner', 'scroll parchment', 'stone tablet', 'speech bubble'). Mantiene coherentes las cajas de un juego. 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 width/height). keyword-only."
- name: width
desc: "Ancho del panel en px. Apaisado de caja de dialogo -> width > height. 768 por defecto. keyword-only."
- name: height
desc: "Alto del panel en px. 256 por defecto (panel bajo y ancho). keyword-only."
- name: transparent
desc: "Si True inyecta Image Rembg y el PNG sale con alpha (fondo recortado alrededor del borde). False = panel opaco sobre fondo plano, recortable luego por el caller. keyword-only."
- name: seed
desc: "Semilla del KSampler. keyword-only."
- name: lora
desc: "LoRA de estilo opcional en models/loras (ej. 'detail_tweaker_sd15.safetensors'). 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 la caja de dialogo (panel limpio, interior plano y vacio, sin texto ni personajes/escena). 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 apaisada con prompt scaffold de caja de dialogo ('{box_style}, {shape}, game UI dialogue box frame, ornate border, empty flat interior for text, plain background') + LoRA de estilo opcional + Image Rembg (si transparent). UNA caja; set coherente -> llamar con mismo box_style/shape/checkpoint/lora. El texto de la conversacion lo pone el motor sobre el interior plano, no este workflow."
tested: true
tests: ["golden transparent: clases CheckpointLoaderSimple/KSampler/VAEDecode/SaveImage/Image Rembg; box_style + 'game UI dialogue box frame' + 'ornate border' + 'empty flat interior for text' + 'plain background' en prompt; SaveImage <- Rembg; transparency True", "edge transparent=False: sin Rembg, SaveImage <- VAEDecode", "edge dims apaisadas: width==896 > height==224", "edge shape en prompt", "edge lora: LoraLoader presente con strength", "error box_style vacio -> ValueError", "determinismo"]
test_file_path: "python/functions/ml/comfyui_build_dialogue_box_workflow_test.py"
file_path: "python/functions/ml/comfyui_build_dialogue_box_workflow.py"
---
## Ejemplo
```python
import sys, os
sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions"))
from ml.comfyui_build_dialogue_box_workflow import comfyui_build_dialogue_box_workflow
# Una caja de dialogo medieval de madera y oro, apaisada, con fondo transparente (alpha).
wf = comfyui_build_dialogue_box_workflow(
"medieval fantasy dialogue box, wood and gold",
shape="rounded panel",
transparent=True,
seed=42,
)
# Set de cajas coherentes: misma firma de estilo, varia solo el matiz.
# for st in ["fantasy dialogue box", "sci-fi terminal text box, neon glow",
# "visual novel text panel, soft pastel"]:
# wf = comfyui_build_dialogue_box_workflow(st, shape="rounded panel",
# lora="detail_tweaker_sd15.safetensors", seed=42)
# comfyui_submit_workflow(wf) # -> comfyui_wait_result -> comfyui_fetch_output_image
# El motor de juego renderiza el texto de la conversacion sobre el interior plano.
```
O lanzable directo con: `./fn run comfyui_build_dialogue_box_workflow` (imprime nodos + class_types del ejemplo).
## Cuando usarla
Cuando necesites el MARCO de la caja de texto donde aparece el dialogo de una
conversacion (RPG, visual novel, aventura grafica): un panel apaisado con borde
decorativo y un interior plano reservado para el texto del motor. Usa esto, NO
`comfyui_build_ui_hud_workflow`, cuando lo que quieres es el CONTENEDOR de dialogo
completo (con su borde y su area de texto), no un elemento suelto del HUD (boton,
barra, icono). Pasa el MISMO `box_style` + `shape` + `checkpoint` + (`lora`) a todas
las cajas de un juego para que combinen. `transparent` recorta el fondo (alpha) listo
para superponer en pantalla.
## Gotchas
- **El interior debe quedar PLANO/LISO**: el negativo por defecto rechaza
"busy interior / cluttered / decorated interior / illustration inside" y el scaffold
empuja a "empty flat interior for text" para que el motor escriba el texto encima sin
competir con detalle horneado. Si el modelo decora el interior, refuerza `box_style`
con "simple flat inner area" o pasa un `negative` propio mas estricto.
- **El texto del dialogo lo pone el motor, NO la imagen**: el negativo empuja a
"no text / no letters / no words" para que la caja salga vacia; la conversacion se
renderiza en el juego sobre el panel. Si quieres texto horneado (raro), pasa un
`negative` sin "text, letters, words".
- **Diferencia con ui_hud**: ui_hud = elementos sueltos (botones, barras, iconos,
cursores); este builder = el panel-contenedor de dialogo apaisado. No los confundas:
si quieres un boton o una barra de vida, usa `comfyui_build_ui_hud_workflow`.
- **El recorte usa Rembg, NO luma-to-alpha**: una caja de dialogo es una pieza solida
con silueta definida (panel + borde), rembg la recorta limpio.
`comfyui_matting_luma_to_alpha` es para translucidos sobre negro (humo/fuego/magia)
y aplanaria el panel — no la uses aqui.
- **Apaisado por defecto**: `width=768 > height=256` da la proporcion de panel de
conversacion al pie de la pantalla. Un bocadillo cuadrado -> sube `height`; un banner
muy ancho -> sube `width`.
- **Coherencia del set = mismos parametros**: si cambias `box_style`/`shape`/
`checkpoint`/`lora`/`seed` entre cajas, dejan de combinar. Fija esos y varia solo el
matiz del `box_style`.
- **SDXL pide mas VRAM y resolucion**: con `checkpoint="juggernaut_xl_v11.safetensors"`
sube `width`/`height`; con dreamshaper_8 (SD1.5) deja 768x256 (holgado en 8GB lowvram).
- `transparent=False` deja el panel 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,229 @@
"""Construye el workflow ComfyUI de UNA caja de dialogo / bocadillo / panel de texto en API format.
Caja de dialogo de juego (RPG, visual novel, aventura): el contenedor de UI donde
aparece el texto de conversacion. Marco apaisado con borde decorativo y un interior
plano y liso reservado para que el motor de juego renderice el texto encima. Es el
builder hermano de comfyui_build_ui_hud_workflow / comfyui_build_card_art_workflow:
mismo patron (PURO, dict API format) que compone funciones existentes del registry,
no reescribe el grafo.
IMPORTANTE — diferencia con comfyui_build_ui_hud_workflow: ui_hud genera ELEMENTOS
SUELTOS de interfaz (un boton, una barra de vida, un icono, un cursor). Este builder
genera el CONTENEDOR DE DIALOGO completo: un panel apaisado con borde ornamental y
un area interior intencionalmente plana/vacia donde el motor escribe el texto de la
conversacion. No es un elemento del HUD, es el marco de la caja de texto.
Cableado:
CheckpointLoaderSimple -> [LoraLoader opcional de estilo] -> KSampler
-> CLIPTextEncode (prompt scaffold de caja de dialogo) ...
-> VAEDecode -> [Image Rembg opcional] -> SaveImage
Compone:
- comfyui_build_txt2img_workflow -> base txt2img apaisada (width > height)
- comfyui_inject_lora -> LoRA de estilo opcional (consistencia de UI)
- 'Image Rembg (Remove Background)' (helper local) -> fondo transparente (alpha)
Por que Rembg y NO comfyui_matting_luma_to_alpha: una caja de dialogo es una pieza
SOLIDA con silueta definida (un panel con su borde); rembg recorta limpio el marco
dejando alpha alrededor. La luma-to-alpha es para translucidos sobre negro
(humo/fuego/magia) y aplanaria el panel. 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.
Por que apaisado (width > height): una caja de dialogo ocupa el ancho de la pantalla
y es baja de alto (una/dos lineas de texto). 768x256 (SD1.5) da la proporcion tipica
de panel de conversacion al pie de la pantalla. El interior se mantiene plano para no
competir con el texto del motor.
class_types/inputs verificados contra /object_info del servidor (8GB lowvram) a
traves de los builders que compone: 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 una caja de dialogo: un panel apaisado limpio,
# con el INTERIOR PLANO y VACIO (sin texto, sin imagenes dentro) para que el motor
# escriba el texto encima, sin personajes ni escena que ensucien el contenedor.
_DIALOGUE_NEGATIVE = (
"blurry, lowres, character, person, face, landscape, scene, "
"text, letters, words, label, watermark, signature, "
"busy interior, cluttered, decorated interior, illustration inside, "
"photo, photorealistic, jpeg artifacts, cropped, out of frame, deformed"
)
def _inject_rembg(workflow: dict, model: str) -> dict:
"""Inserta 'Image Rembg (Remove Background)' (transparency=True) entre VAEDecode y SaveImage.
Mismo helper que usan comfyui_build_ui_hud_workflow / comfyui_build_item_icon_workflow:
el nodo recorta la silueta del panel de dialogo dejando alpha alrededor del
borde. 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_dialogue_box_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_dialogue_box_workflow(
box_style: str = "fantasy RPG dialogue box",
*,
shape: str = "rounded panel",
checkpoint: str = "dreamshaper_8.safetensors",
width: int = 768,
height: 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 = "dialogue_box",
) -> dict:
"""Construye el dict (API format) del workflow de una caja de dialogo de juego.
Args:
box_style: descriptor del estilo del contenedor de dialogo (ej. "fantasy RPG
dialogue box", "medieval fantasy dialogue box, wood and gold", "sci-fi
terminal text box, neon glow", "visual novel text panel, soft pastel").
Se inserta en un prompt scaffold de caja de dialogo. No puede estar vacio.
shape: forma del panel (ej. "rounded panel", "rectangular banner", "scroll
parchment", "stone tablet", "speech bubble"). Mantiene coherentes las
cajas de un juego. 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 width/height). keyword-only.
width: ancho del panel en px. Apaisado de caja de dialogo -> width > height.
768 por defecto. keyword-only.
height: alto del panel en px. 256 por defecto (panel bajo y ancho).
keyword-only.
transparent: si True inyecta Rembg y el PNG sale con alpha (fondo recortado
alrededor del borde). Si False deja el panel opaco sobre fondo plano,
recortable luego por el caller/pipeline. keyword-only.
seed: semilla del KSampler. keyword-only.
lora: LoRA de estilo opcional en models/loras (ej.
'detail_tweaker_sd15.safetensors'). 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 la
caja de dialogo (panel limpio, interior plano y vacio, sin texto ni
personajes/escena). 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 apaisada
con prompt scaffold de caja de dialogo ('{box_style}, {shape}, game UI
dialogue box frame, ornate border, empty flat interior for text, plain
background') + LoRA de estilo opcional + Rembg (si transparent). Es UNA caja;
un set coherente -> llamar con el mismo box_style/shape/checkpoint/(lora). El
texto de la conversacion lo renderiza el motor sobre el interior plano, no
este workflow.
Raises:
ValueError: si box_style 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 box_style or not box_style.strip():
raise ValueError(
"comfyui_build_dialogue_box_workflow: 'box_style' no puede estar vacio"
)
lora_strength = max(0.0, min(2.0, float(lora_strength)))
neg = _DIALOGUE_NEGATIVE if negative is None else negative
# Prompt scaffold de caja de dialogo: panel apaisado con borde decorativo y un
# interior PLANO y VACIO reservado para el texto que pone el motor de juego.
positive = (
f"{box_style.strip()}, {shape}, game UI dialogue box frame, "
"ornate border, empty flat interior for text, plain background"
)
wf = comfyui_build_txt2img_workflow(
checkpoint,
positive,
neg,
steps=steps,
cfg=cfg,
width=width,
height=height,
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_dialogue_box_workflow(
"medieval fantasy dialogue box, wood and gold",
shape="rounded panel",
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,109 @@
"""Tests offline de comfyui_build_dialogue_box_workflow (estructura del dict, sin GPU)."""
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from ml.comfyui_build_dialogue_box_workflow import ( # noqa: E402
comfyui_build_dialogue_box_workflow,
)
def _classes(wf):
return sorted({n["class_type"] for n in wf.values()})
def _by_class(wf, cls):
return [n for n in wf.values() if n["class_type"] == cls]
def _id_of(wf, cls):
return next(nid for nid, n in wf.items() if n["class_type"] == cls)
def test_golden_transparent_recipe():
wf = comfyui_build_dialogue_box_workflow(
"medieval fantasy dialogue box, wood and gold", transparent=True, seed=42
)
cls = _classes(wf)
# Cadena base txt2img + Rembg para alpha.
assert "CheckpointLoaderSimple" in cls
assert "KSampler" in cls
assert "VAEDecode" in cls
assert "SaveImage" in cls
assert "Image Rembg (Remove Background)" in cls
# El box_style aparece en el prompt positivo, con el scaffold de caja de dialogo.
pos = next(
n for n in wf.values()
if n["class_type"] == "CLIPTextEncode"
and "medieval fantasy dialogue box" in n["inputs"]["text"]
)
assert "game UI dialogue box frame" in pos["inputs"]["text"]
assert "ornate border" in pos["inputs"]["text"]
assert "empty flat interior for text" in pos["inputs"]["text"]
assert "plain background" in pos["inputs"]["text"]
# SaveImage toma la imagen del Rembg (no del VAEDecode).
rembg_id = _id_of(wf, "Image Rembg (Remove Background)")
save = next(n for n in wf.values() if n["class_type"] == "SaveImage")
assert save["inputs"]["images"] == [rembg_id, 0]
assert _by_class(wf, "Image Rembg (Remove Background)")[0]["inputs"]["transparency"] is True
def test_edge_opaque_no_rembg():
wf = comfyui_build_dialogue_box_workflow("sci-fi text box", transparent=False)
assert "Image Rembg (Remove Background)" not in _classes(wf)
# SaveImage toma del VAEDecode directamente.
vd_id = _id_of(wf, "VAEDecode")
save = next(n for n in wf.values() if n["class_type"] == "SaveImage")
assert save["inputs"]["images"] == [vd_id, 0]
def test_edge_landscape_dims_reflected():
wf = comfyui_build_dialogue_box_workflow("rpg dialogue box", width=896, height=224)
latent = _by_class(wf, "EmptyLatentImage")[0]["inputs"]
assert latent["width"] == 896
assert latent["height"] == 224
# Apaisado: ancho mayor que alto.
assert latent["width"] > latent["height"]
def test_edge_shape_in_prompt():
wf = comfyui_build_dialogue_box_workflow(
"visual novel text panel", shape="scroll parchment", transparent=False
)
pos = next(
n for n in wf.values()
if n["class_type"] == "CLIPTextEncode"
and "visual novel text panel" in n["inputs"]["text"]
)
assert "scroll parchment" in pos["inputs"]["text"]
def test_edge_lora_reflected():
wf = comfyui_build_dialogue_box_workflow(
"fantasy dialogue box",
lora="detail_tweaker_sd15.safetensors",
lora_strength=0.9,
)
loras = _by_class(wf, "LoraLoader")
assert len(loras) == 1
assert loras[0]["inputs"]["lora_name"] == "detail_tweaker_sd15.safetensors"
assert loras[0]["inputs"]["strength_model"] == 0.9
def test_error_empty_box_style():
try:
comfyui_build_dialogue_box_workflow(" ")
assert False
except ValueError as e:
assert "box_style" in str(e)
def test_determinism():
a = comfyui_build_dialogue_box_workflow(
"stone tablet dialogue box", lora="detail_tweaker_sd15.safetensors", seed=7
)
b = comfyui_build_dialogue_box_workflow(
"stone tablet dialogue box", lora="detail_tweaker_sd15.safetensors", seed=7
)
assert a == b