diff --git a/docs/capabilities/gamedev-2d.md b/docs/capabilities/gamedev-2d.md index ed561056..3fc2c3d7 100644 --- a/docs/capabilities/gamedev-2d.md +++ b/docs/capabilities/gamedev-2d.md @@ -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_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_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_card_art_workflow_py_ml` | `(subject, *, card_style="fantasy trading card art", checkpoint="juggernaut_xl_v11…", width=512, height=768, hires=True, seed=0, lora=None, …) -> dict` | LA ilustración central de UNA carta coleccionable (TCG): criatura/personaje/hechizo en formato **vertical** de carta (`width dict` | UN enemigo/criatura de juego (goblin, esqueleto, slime, dragón, boss, elemental): figura de **cuerpo entero** centrada, fondo limpio recortable a alpha (`{variant} {creature}, {style}, full body, centered, plain background, game asset…`) → txt2img cuadrado + LoRA estilo opcional + Rembg (alpha). `variant` (ice/fire/elite/corrupted…) se antepone a la criatura para generar la familia del MISMO enemigo (misma `creature`/`seed`/`style`, varía solo `variant`); bestiario coherente = mismo `style`/`checkpoint`/`lora`, varía solo `creature`. El negativo empuja a UNA criatura entera sin recorte. Probado e2e en GPU con SD1.5 (`reports/0154`). SD1.5. | diff --git a/python/functions/ml/comfyui_build_dialogue_box_workflow.md b/python/functions/ml/comfyui_build_dialogue_box_workflow.md new file mode 100644 index 00000000..c8b5fc9d --- /dev/null +++ b/python/functions/ml/comfyui_build_dialogue_box_workflow.md @@ -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`. diff --git a/python/functions/ml/comfyui_build_dialogue_box_workflow.py b/python/functions/ml/comfyui_build_dialogue_box_workflow.py new file mode 100644 index 00000000..fdbf14ac --- /dev/null +++ b/python/functions/ml/comfyui_build_dialogue_box_workflow.py @@ -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, + ) + ) diff --git a/python/functions/ml/comfyui_build_dialogue_box_workflow_test.py b/python/functions/ml/comfyui_build_dialogue_box_workflow_test.py new file mode 100644 index 00000000..e0dbcd66 --- /dev/null +++ b/python/functions/ml/comfyui_build_dialogue_box_workflow_test.py @@ -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