feat(gamedev): comfyui_build_title_lettering_workflow — texto/logo de título de juego (lettering estilizado, apaisado, alpha)

Builder puro (dict API format) hermano de ui_hud/splash_art: compone
comfyui_build_txt2img_workflow + comfyui_inject_lora + Image Rembg. Renderiza
el nombre del juego/una palabra con un tratamiento de lettering (metálico,
tallado, neón, fuego...), formato apaisado 1024x512, fondo recortable a alpha.

El negativo NO rechaza texto (el lettering es el sujeto). Documentada la
limitación clave: la difusión no garantiza la ortografía exacta del texto
(letras de más/deformadas; una palabra-objeto como DRAGON se ilustra en vez de
escribirse). Mitigaciones: palabras cortas en mayúscula, re-roll de seeds,
SDXL > SD1.5, o pintar el texto real en el motor.

Tests 9/9 verde (offline). Verificado e2e en GPU (8GB lowvram): DRAGON/fire
engraved (SD1.5, prompt_id 6f3920b7) y AETHER/epic fantasy metallic (SDXL,
prompt_id 2a7fe8ba, logo metálico dorado + alpha). Fila en
docs/capabilities/gamedev-2d.md. Report en reports/0165.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Egutierrez
2026-06-27 01:10:55 +02:00
parent dbb040aa12
commit b45165dbc5
4 changed files with 523 additions and 0 deletions
@@ -0,0 +1,133 @@
---
name: comfyui_build_title_lettering_workflow
kind: function
lang: py
domain: ml
version: "1.0.0"
purity: pure
signature: "def comfyui_build_title_lettering_workflow(text: str, *, letter_style: str = \"epic fantasy metallic\", checkpoint: str = \"juggernaut_xl_v11.safetensors\", width: int = 1024, height: int = 512, 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 = \"title_lettering\") -> dict"
description: "Construye el dict (API format) del workflow de UN texto/logo de titulo de juego 2D: el nombre del juego o una palabra renderizada con un TRATAMIENTO de lettering (metalico, fantasy tallado, neon, piedra, fuego, cristal, madera...), formato apaisado, fondo plano recortable a alpha, para usar como logo/titulo en menus, splash o cabecera. El valor del builder es el ESTILO del lettering; la difusion NO garantiza la ortografia exacta del texto (limitacion documentada). Compone comfyui_build_txt2img_workflow + comfyui_inject_lora (estilo opcional) + Image Rembg (fondo transparente si transparent). Hermano de comfyui_build_ui_hud/splash_art_workflow. Pura, sin red ni I/O. class_types verificados contra /object_info (8GB lowvram)."
tags: [comfyui, ml, gamedev, gamedev-2d, title, logo, lettering, typography, text, 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: text
desc: "La palabra o nombre del juego a renderizar como logo (ej. 'DRAGON', 'Mystic Realms', 'VOID'). Se inserta ENTRECOMILLADO en el prompt scaffold para que el modelo lo trate como cadena literal. No puede estar vacio. La difusion no garantiza la ortografia exacta: usa palabras cortas en mayuscula y re-rollea seeds."
- name: letter_style
desc: "Tratamiento visual del lettering, el VALOR real del builder (ej. 'epic fantasy metallic', 'fire engraved', 'neon sci-fi glow', 'carved stone', 'cracked ice crystal', 'wooden carved', 'golden royal'). Pasa el MISMO letter_style + checkpoint + lora a varios titulos/subtitulos para coherencia de marca. keyword-only."
- name: checkpoint
desc: "Checkpoint del servidor. 'juggernaut_xl_v11.safetensors' (SDXL, mejor render de texto y detalle) por defecto; en 8GB lowvram con apaisado grande puede ser pesado: usa 'dreamshaper_8.safetensors' (SD1.5) y/o baja la resolucion. keyword-only."
- name: width
desc: "Ancho del lienzo en px. Logo de titulo -> apaisado (width > height). 1024 por defecto. keyword-only."
- name: height
desc: "Alto del lienzo en px. 512 por defecto (2:1 con width=1024, encuadre tipico de logotipo en una linea). Para palabras muy cortas un cuadrado tambien vale (bajar width). keyword-only."
- name: transparent
desc: "Si True inyecta Image Rembg y el PNG sale con alpha (fondo recortado, listo para superponer en el menu). False = lettering opaco sobre fondo plano, recortable luego por el caller. keyword-only."
- name: seed
desc: "Semilla del KSampler. Re-rollear seeds es la via principal para conseguir que el texto salga legible: misma text/letter_style, varias seeds, elegir el mejor. keyword-only."
- name: lora
desc: "LoRA de estilo opcional en models/loras (ej. un LoRA de logotipos o de tipografia tematica). 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 lettering (una pieza de texto limpia, fondo plano, sin escena/personaje; NO rechaza texto, porque el texto es el sujeto). 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 logo de texto ('the word \"{text}\" as a game logo, {letter_style} lettering, stylized typography, centered, plain background, high detail') + LoRA de estilo opcional + Image Rembg (si transparent). UN logo; titulo + subtitulos coherentes -> llamar por text con mismo letter_style/checkpoint/lora."
tested: true
tests: ["golden transparent: clases CheckpointLoaderSimple/KSampler/VAEDecode/SaveImage/Image Rembg; '\"DRAGON\"' entrecomillado + 'game logo' + 'fire engraved' + 'lettering' + 'stylized typography' en prompt; SaveImage <- Rembg; transparency True", "edge transparent=False: sin Rembg, SaveImage <- VAEDecode", "edge dims: width/height reflejados; default apaisado 1024x512 (width>height)", "edge letter_style en prompt", "edge text entrecomillado en prompt", "edge negative NO rechaza texto como sujeto (tokens text/letters/words ausentes)", "edge lora: LoraLoader presente con strength", "error text vacio -> ValueError", "determinismo"]
test_file_path: "python/functions/ml/comfyui_build_title_lettering_workflow_test.py"
file_path: "python/functions/ml/comfyui_build_title_lettering_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_title_lettering_workflow import comfyui_build_title_lettering_workflow
# El titulo "DRAGON" tallado en fuego, fondo transparente (alpha), listo para submit.
wf = comfyui_build_title_lettering_workflow(
"DRAGON",
letter_style="fire engraved",
checkpoint="dreamshaper_8.safetensors",
transparent=True,
seed=42,
)
# Generacion real (GPU): submit -> wait -> fetch.
# from ml.comfyui_submit_workflow import comfyui_submit_workflow
# from ml.comfyui_wait_result import comfyui_wait_result
# from ml.comfyui_fetch_output_image import comfyui_fetch_output_image
# pid = comfyui_submit_workflow(wf)["prompt_id"]
# out = comfyui_wait_result(pid) # outputs por nodo
# img = out[next(iter(out))]["images"][0] # {filename, subfolder, type}
# comfyui_fetch_output_image(img["filename"], subfolder=img["subfolder"], dest_dir="/tmp")
#
# Re-roll de seeds (la via para que el texto salga legible): misma text/letter_style,
# varias seeds, elegir el mejor -> comfyui_batch_generate(wf, seeds=[1,2,3,4]).
# Logo + subtitulo coherentes: mismo letter_style/checkpoint en ambos.
```
O lanzable directo con: `./fn run comfyui_build_title_lettering_workflow` (imprime nodos + class_types del ejemplo).
## Cuando usarla
Cuando necesites el logo de texto / titulo de un juego (el nombre, una palabra clave,
un subtitulo) con un tratamiento visual concreto para menus, pantalla de inicio,
splash o cabecera: lettering metalico, tallado en piedra/madera, neon, fuego, cristal,
oro. Genera el ESTILO del texto (lo que la difusion reproduce bien) sobre fondo plano
recortable a alpha. Pasa el MISMO `letter_style` + `checkpoint` + (`lora`) a varios
titulos/subtitulos para que la marca combine; varia solo `text`. Para el texto exacto,
re-rollea seeds (`comfyui_batch_generate`) y/o retoca en post.
Eligela frente a sus hermanos por el ROL del asset:
- **splash_art** -> la ilustracion apaisada de portada/loading (sin texto). El titulo
se superpone encima; este builder pinta ESE titulo.
- **ui_hud** -> chrome de la interfaz (botones, marcos, barras), sin texto como sujeto.
- **title_lettering (esta)** -> el TEXTO estilizado en si (el nombre del juego como logo).
## Gotchas
- **La difusion NO garantiza la ortografia exacta del texto (limitacion clave)**: los
modelos SD1.5/SDXL renderizan texto de forma imperfecta. Es habitual que salgan
letras de mas, letras deformadas o palabras parecidas pero mal escritas — mas cuanto
mas larga o rara sea `text`. El VALOR de este builder es el ESTILO del lettering, no
la fidelidad tipografica. Mitigaciones: (1) palabras CORTAS y en MAYUSCULA; (2)
re-rollear varias seeds (`comfyui_batch_generate`) y quedarte con la mejor; (3) subir
a SDXL (juggernaut_xl, default) que escribe algo mejor que SD1.5; (4) retocar/recomponer
las letras en post; o (5) dejar que el motor pinte el texto real con una fuente y usar
esto solo como textura/tratamiento de fondo del logo.
- **El recorte usa Rembg, NO luma-to-alpha**: un logo de texto es una pieza solida con
silueta definida (las letras tratadas); rembg recorta limpio el fondo plano dejando
alpha. `comfyui_matting_luma_to_alpha` es para translucidos sobre negro (humo/fuego/
magia) y se comeria las partes oscuras de un logo metalico/tallado — no la uses aqui.
Si el lettering es un texto NEON brillante sobre negro (no solido), considera generarlo
con `transparent=False` sobre fondo negro y aplicar luma-to-alpha tu mismo.
- **Apaisado por defecto (1024x512, 2:1)**: el titulo es una pieza de texto en una linea,
mas ancha que alta. Para una sola palabra corta un cuadrado tambien vale (baja `width`);
para un logo con texto en dos lineas, sube `height`.
- **Coherencia de marca = mismos parametros**: si cambias `letter_style`/`checkpoint`/
`lora`/`seed` entre el titulo y los subtitulos, dejan de combinar. Fija esos y varia
solo `text`.
- **SDXL pide mas VRAM**: con `checkpoint="juggernaut_xl_v11.safetensors"` (default) y
apaisado grande en 8GB lowvram puede ir justo; si hay OOM baja a `dreamshaper_8`
(SD1.5) y/o reduce `width`/`height`.
- `transparent=False` deja el lettering 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,247 @@
"""Construye el workflow ComfyUI de UN texto/logo de titulo de juego (API format).
Lettering / texto estilizado de titulo: el nombre del juego o una palabra renderizada
con un TRATAMIENTO visual concreto (metalico, fantasy tallado, neon, piedra, fuego,
cristal, madera...) sobre fondo plano, recortable a alpha, para usar como logo o
titulo en menus, splash screen o cabecera. Es el builder hermano de
comfyui_build_ui_hud_workflow / comfyui_build_splash_art_workflow: mismo patron
(PURO, dict API format) que compone funciones existentes del registry, no reescribe
el grafo.
IMPORTANTE — limitacion de la difusion con texto: los modelos de difusion (SD1.5/
SDXL) renderizan texto de forma IMPERFECTA. No hay garantia de que las letras salgan
con la ortografia exacta de `text`: aparecen letras de mas, letras deformadas o
palabras parecidas pero mal escritas, sobre todo con palabras largas o poco comunes.
El VALOR de este builder es el ESTILO / tratamiento del lettering (el aspecto
metalico, tallado, neon, etc.), no la fidelidad tipografica. Para el texto exacto:
re-rollear varias seeds y elegir el mejor, usar palabras CORTAS y en mayuscula, y/o
retocar/recomponer las letras en post (o dejar que el motor pinte el texto real con
una fuente y usar esto solo como textura/tratamiento). Ver Gotchas en el .md.
Cableado:
CheckpointLoaderSimple -> [LoraLoader opcional de estilo] -> KSampler
-> CLIPTextEncode (prompt scaffold de logo de texto) ...
-> VAEDecode -> [Image Rembg opcional] -> SaveImage
Compone:
- comfyui_build_txt2img_workflow -> base txt2img apaisada (logo es mas ancho que alto)
- comfyui_inject_lora -> LoRA de estilo opcional (consistencia de marca)
- 'Image Rembg (Remove Background)' (helper local) -> fondo transparente
Por que Rembg y NO comfyui_matting_luma_to_alpha: un logo de texto es una pieza
SOLIDA con silueta definida (las letras tratadas), rembg recorta limpio el fondo
plano dejando alpha. La luma-to-alpha es para translucidos sobre negro
(humo/fuego/magia) y se comeria las partes oscuras de un logo metalico/tallado. 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): el titulo de un juego es una pieza de texto en
una sola linea (o pocas), mas ancha que alta; 1024x512 (2:1) da el encuadre tipico
de un logotipo. Para palabras muy cortas un cuadrado tambien funciona (bajar width).
El mismo letter_style + checkpoint + (lora) en varios titulos/subtitulos hace que
combinen visualmente: es la clave de una identidad de marca coherente, igual que en
los iconos de inventario o 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 un logo de TEXTO: una pieza de lettering limpia
# y centrada, sin escena ni personaje que ensucien el fondo plano recortable. NO
# rechaza "text/letters/words" (al contrario que ui_hud/status_effect): aqui el texto
# ES el sujeto. Empuja contra el ruido textual tipico de la difusion (letras de mas,
# texto enrevesado, letras deformadas) para mejorar algo la legibilidad.
_LETTERING_NEGATIVE = (
"blurry, lowres, extra letters, jumbled text, deformed letters, "
"misspelled, gibberish, cluttered, busy background, scene, landscape, "
"character, person, face, photo, photorealistic, watermark, signature, "
"cropped, out of frame, jpeg artifacts, low quality, 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_status_effect_icon_workflow:
el nodo recorta el fondo plano dejando el lettering sobre 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_title_lettering_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_title_lettering_workflow(
text: str,
*,
letter_style: str = "epic fantasy metallic",
checkpoint: str = "juggernaut_xl_v11.safetensors",
width: int = 1024,
height: int = 512,
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 = "title_lettering",
) -> dict:
"""Construye el dict (API format) del workflow de un texto/logo de titulo de juego.
Args:
text: la palabra o nombre del juego a renderizar como logo (ej. "DRAGON",
"Mystic Realms", "VOID"). Se inserta ENTRECOMILLADO en el prompt scaffold
para que el modelo lo trate como texto literal. No puede estar vacio.
OJO: la difusion no garantiza la ortografia exacta (ver Gotchas); usa
palabras cortas en mayuscula y re-rollea seeds para el mejor resultado.
letter_style: tratamiento visual del lettering, el VALOR real de este builder
(ej. "epic fantasy metallic", "fire engraved", "neon sci-fi glow",
"carved stone", "cracked ice crystal", "wooden carved", "golden royal").
Pasa el MISMO letter_style + checkpoint + (lora) a varios titulos/subtitulos
para coherencia de marca. keyword-only.
checkpoint: checkpoint del servidor. 'juggernaut_xl_v11.safetensors' (SDXL,
mejor render de texto y detalle, recomendado para lettering) por defecto;
en 8GB lowvram con apaisado grande puede ser pesado: si la GPU se queda
corta usa 'dreamshaper_8.safetensors' (SD1.5) y/o baja la resolucion.
keyword-only.
width: ancho del lienzo en px. Logo de titulo -> apaisado (width > height).
1024 por defecto. keyword-only.
height: alto del lienzo en px. 512 por defecto (2:1 con width=1024, encuadre
tipico de logotipo en una linea). Para palabras muy cortas un cuadrado
tambien vale (bajar width). keyword-only.
transparent: si True inyecta Rembg y el PNG sale con alpha (fondo recortado,
listo para superponer en el menu). Si False deja el lettering opaco sobre
fondo plano, recortable luego por el caller/pipeline. keyword-only.
seed: semilla del KSampler. Re-rollear seeds es la via principal para conseguir
que el texto salga legible: misma text/letter_style, varias seeds, elegir
el mejor. keyword-only.
lora: LoRA de estilo opcional en models/loras (ej. un LoRA de logotipos o de
tipografia tematica). 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
lettering (una pieza de texto limpia, fondo plano, sin escena/personaje;
NO rechaza 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: base txt2img apaisada
con prompt scaffold de logo de texto ('the word "{text}" as a game logo,
{letter_style} lettering, stylized typography, plain background, high detail')
+ LoRA de estilo opcional + Rembg (si transparent). Es UN logo; un set de
titulo + subtitulos -> llamar por text con el mismo letter_style/checkpoint/
lora para coherencia de marca.
Raises:
ValueError: si text 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 text or not text.strip():
raise ValueError(
"comfyui_build_title_lettering_workflow: 'text' no puede estar vacio"
)
lora_strength = max(0.0, min(2.0, float(lora_strength)))
neg = _LETTERING_NEGATIVE if negative is None else negative
# Prompt scaffold de logo de texto: el texto entrecomillado (para que el modelo lo
# trate como cadena literal) con el tratamiento de lettering, centrado, fondo plano,
# recortable. El estilo es lo que el modelo reproduce con fidelidad; el texto, no.
positive = (
f'the word "{text.strip()}" as a game logo, {letter_style} lettering, '
"stylized typography, centered, plain background, high detail"
)
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_title_lettering_workflow(
"DRAGON",
letter_style="fire engraved",
checkpoint="dreamshaper_8.safetensors",
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_title_lettering_workflow.
Verifican que el dict en API format se construye correctamente: clases presentes,
cableado del Rembg, prompt scaffold de logo de texto (text entrecomillado +
letter_style), formato apaisado por defecto, y reflejo de los argumentos
(text, letter_style, width/height, 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_title_lettering_workflow import ( # noqa: E402
comfyui_build_title_lettering_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 _negative_prompt(wf):
ks = next(n for n in wf.values() if n["class_type"] == "KSampler")
neg_id = ks["inputs"]["negative"][0]
return wf[neg_id]["inputs"]["text"]
def test_golden_transparent():
"""Caso feliz: logo transparente -> Rembg cableado, prompt de lettering, clases base."""
wf = comfyui_build_title_lettering_workflow(
"DRAGON", letter_style="fire engraved", 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)
# El texto va ENTRECOMILLADO para que el modelo lo trate como cadena literal.
assert '"DRAGON"' in prompt
assert "game logo" in prompt
assert "fire engraved" in prompt
assert "lettering" in prompt
assert "stylized typography" 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_title_lettering_workflow("VOID", 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_dims_reflected_landscape_default():
"""width/height se reflejan; default es apaisado 1024x512 (logo en una linea)."""
wf = comfyui_build_title_lettering_workflow("QUEST", width=768, height=384)
latent = next(n for n in wf.values() if n["class_type"] == "EmptyLatentImage")
assert latent["inputs"]["width"] == 768
assert latent["inputs"]["height"] == 384
wf_default = comfyui_build_title_lettering_workflow("QUEST")
latent_d = next(n for n in wf_default.values() if n["class_type"] == "EmptyLatentImage")
assert latent_d["inputs"]["width"] == 1024
assert latent_d["inputs"]["height"] == 512
# Apaisado por defecto: el logo es mas ancho que alto.
assert latent_d["inputs"]["width"] > latent_d["inputs"]["height"]
def test_edge_letter_style_reflected():
"""letter_style se inserta en el prompt positivo."""
wf = comfyui_build_title_lettering_workflow("NEON", letter_style="neon sci-fi glow")
assert "neon sci-fi glow" in _positive_prompt(wf)
def test_edge_text_reflected_quoted():
"""text se inserta literal y entrecomillado en el prompt positivo."""
wf = comfyui_build_title_lettering_workflow("Mystic Realms")
assert '"Mystic Realms"' in _positive_prompt(wf)
def test_edge_negative_allows_text():
"""El negativo por defecto NO rechaza texto como sujeto (aqui el texto ES el sujeto).
Los hermanos (ui_hud, status_effect_icon) llevan tokens 'text'/'letters'/'words'
en su negativo para limpiar la silueta; este builder NO, porque el lettering es el
sujeto. Tokeniza el negativo y comprueba que esos tokens sueltos no estan (si que
se permiten frases como 'extra letters'/'jumbled text' que rechazan el ruido).
"""
neg = _negative_prompt(comfyui_build_title_lettering_workflow("LOGO"))
tokens = {t.strip() for t in neg.split(",")}
assert "text" not in tokens
assert "letters" not in tokens
assert "words" not in tokens
def test_edge_lora_injected():
"""lora -> LoraLoader presente con la fuerza dada."""
wf = comfyui_build_title_lettering_workflow(
"EMPIRE", lora="logo_style_sd15.safetensors", lora_strength=0.7
)
loras = [n for n in wf.values() if n["class_type"] == "LoraLoader"]
assert len(loras) == 1
assert loras[0]["inputs"]["lora_name"] == "logo_style_sd15.safetensors"
assert loras[0]["inputs"]["strength_model"] == pytest.approx(0.7)
def test_error_empty_text():
"""text vacio -> ValueError."""
with pytest.raises(ValueError):
comfyui_build_title_lettering_workflow(" ")
def test_determinism():
"""Mismos argumentos -> mismo dict (funcion pura)."""
a = comfyui_build_title_lettering_workflow("ARENA", seed=7)
b = comfyui_build_title_lettering_workflow("ARENA", seed=7)
assert a == b