feat(gamedev): comfyui_build_rune_glyph_workflow — runas/glifos/sigilos mágicos (símbolo arcano aislado sobre negro; glow=True -> luma->alpha conserva resplandor para blend aditivo, sin Rembg; glow=False runa mate; hermano de status_effect_icon/decal_overlay; probado e2e SD1.5 prompt_id 701d149a)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-27 01:55:27 +02:00
parent 1a8093a7be
commit 5a0818ee9c
4 changed files with 493 additions and 0 deletions
@@ -0,0 +1,139 @@
---
name: comfyui_build_rune_glyph_workflow
kind: function
lang: py
domain: ml
version: "1.0.0"
purity: pure
signature: "def comfyui_build_rune_glyph_workflow(glyph: str, *, glow: bool = True, style: str = \"arcane glowing rune\", checkpoint: str = \"dreamshaper_8.safetensors\", size: int = 512, seed: int = 0, lora: str | None = None, lora_strength: float = 1.0, negative: str | None = None, steps: int = 28, cfg: float = 7.0, sampler_name: str = \"dpmpp_2m\", scheduler: str = \"karras\", filename_prefix: str = \"rune_glyph\") -> dict"
description: "Construye el dict (API format) del workflow de UNA runa / glifo / sigilo magico 2D: simbolo arcano aislado — glifos runicos, circulos magicos, sigilos de invocacion, inscripciones brillantes — para hechizos, portales, marcas de conjuro y efectos de magia. Se genera AISLADO sobre fondo uniforme; glow=True (defecto) lo pone BRILLANTE sobre NEGRO puro, pensado para extraer alpha por luminancia con comfyui_matting_luma_to_alpha (conserva el resplandor para blend aditivo en el motor). NO inyecta Rembg (el matting de una runa brillante es luma-to-alpha, no un nodo). Compone comfyui_build_txt2img_workflow + comfyui_inject_lora (estilo arcano opcional). Hermano de comfyui_build_status_effect_icon/decal_overlay_workflow. Pura, sin red ni I/O. class_types verificados contra /object_info."
tags: [comfyui, ml, gamedev, gamedev-2d, rune, glyph, sigil, magic, arcane, glow, alpha, luma, 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: glyph
desc: "Descripcion del simbolo arcano a generar (ej. 'circular summoning rune', 'ancient norse rune', 'magic circle with glyphs', 'demonic sigil', 'elven inscription', 'protective ward symbol', 'fire spell glyph'). Se inserta en un prompt scaffold de runa aislada. No puede estar vacio."
- name: glow
desc: "Si True (defecto) genera la runa BRILLANTE sobre fondo NEGRO puro, pensada para extraer alpha por luminancia con comfyui_matting_luma_to_alpha (conserva el resplandor: blend aditivo en el motor). False = runa MATE/grabada sobre fondo plano, sin resplandor (recorte/inversion por el caller). keyword-only."
- name: style
desc: "Descriptor de estilo arcano que mantiene consistentes las runas de un set (ej. 'arcane glowing rune', 'fiery demonic sigil', 'icy blue magic circle', 'golden holy glyph', 'engraved stone rune'). Pasa el MISMO style + checkpoint + lora a todas las runas de un grimorio 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 a 768/1024). keyword-only."
- name: size
desc: "Lado del cuadrado en px (width = height = size). 512 SD1.5 por defecto. keyword-only."
- name: seed
desc: "Semilla del KSampler. Misma seed + mismos glyph/style -> mismo simbolo; variar seed da variantes del mismo tipo de runa. keyword-only."
- name: lora
desc: "LoRA de estilo arcano opcional en models/loras. 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: negative
desc: "Prompt negativo. None usa el negativo por defecto pensado para una runa aislada (un simbolo arcano centrado, sin escena/objeto/texto legible/marco, fondo plano). 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 runa ('{glyph}, {style}, magic symbol, single isolated glyph, centered, glowing on a solid pure black background | on a plain flat background, occult sigil, game asset, ...') + el negativo refuerza el aislamiento (rechaza escena/objeto/texto legible/marco; con glow=False rechaza el resplandor) + LoRA de estilo opcional. NO lleva Rembg: con glow=True el PNG resultante se convierte a RGBA con comfyui_matting_luma_to_alpha (luma=alpha) en un paso posterior. UNA runa; variar seed da variantes del mismo tipo."
file_path: python/functions/ml/comfyui_build_rune_glyph_workflow.py
tested: true
test_file_path: python/functions/ml/comfyui_build_rune_glyph_workflow_test.py
tests: [test_golden_rune_glow_on_black_recipe, test_edge_glow_toggles_background, test_edge_glyph_and_style_reflected, test_edge_seed_and_size_propagate, test_edge_lora_injected_when_requested, test_edge_lora_strength_clamped, test_error_empty_glyph_raises, test_purity_no_global_mutation]
---
Construye el dict (API format) del workflow de UNA runa / glifo / sigilo magico 2D: un
simbolo arcano aislado (glifos runicos, circulos magicos, sigilos de invocacion,
inscripciones brillantes) para hechizos, portales, marcas de conjuro y efectos de magia.
La pieza se genera AISLADA sobre un fondo uniforme para poder extraer luego el canal
alpha por luminancia. Es el builder hermano de `comfyui_build_status_effect_icon_workflow`
/ `comfyui_build_decal_overlay_workflow`: mismo patron PURO (dict API format) que compone
funciones existentes del registry sin reescribir el grafo.
## Ejemplo
```python
import sys, os
sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions"))
from ml.comfyui_build_rune_glyph_workflow import comfyui_build_rune_glyph_workflow
# Una runa de invocacion brillante sobre fondo negro, lista para submit + luma-to-alpha.
wf = comfyui_build_rune_glyph_workflow(
"circular summoning rune",
glow=True,
style="arcane glowing rune",
seed=11,
)
# Pipeline completo de una runa con alpha:
# r = comfyui_submit_workflow(wf); comfyui_wait_result(r["prompt_id"])
# png = comfyui_fetch_output_image(...) # runa brillante sobre negro
# rgba = comfyui_matting_luma_to_alpha(png, gamma=1.2, black_point=0.05) # luma=alpha
# Un grimorio coherente: mismo style/checkpoint/(lora), variando glyph y seed.
# for g in ["fire spell glyph", "ice ward sigil", "lightning rune"]:
# wf = comfyui_build_rune_glyph_workflow(g, style="arcane glowing rune")
```
O lanzable directo con: `./fn run comfyui_build_rune_glyph_workflow` (imprime nodos + class_types del ejemplo).
## Cuando usarla
Cuando necesites simbolos arcanos para magia: el circulo magico que aparece bajo un
invocador, el glifo de un hechizo elemental, el sigilo grabado en un portal, la
inscripcion brillante de un arma encantada, las runas de proteccion en el suelo. La runa
se genera aislada y, si brilla, se extrae su alpha por luminancia para componerla en el
motor con un blend aditivo (el resplandor "encaja" encima del fondo del juego).
Flujo tipico despues de generar:
1. `comfyui_build_rune_glyph_workflow("circular summoning rune", glow=True)` -> dict.
2. `comfyui_submit_workflow` -> `comfyui_wait_result` -> `comfyui_fetch_output_image`
(PNG de la runa brillante sobre negro).
3. `comfyui_matting_luma_to_alpha(png, gamma=..., black_point=...)` -> PNG RGBA donde la
luminancia ES el alpha: resplandor=opaco, negro=transparente. Listo para blend aditivo.
Pasa el MISMO `style` + `checkpoint` + (`lora`) a todas las runas de un grimorio para que
combinen; varia `seed` y `glyph` para sacar el set (varios sigilos del mismo estilo).
**Elige este builder y NO `comfyui_build_status_effect_icon_workflow` cuando** quieres una
marca arcana translucida que emite luz e se inscribe en el mundo (suelo, portal, arma),
cuyo alpha sale del resplandor. Un icono de estado es un simbolo SOLIDO de UI con silueta
recortable por rembg, pequeño y legible a 16-32 px en el HUD: cosa distinta.
## Gotchas
- **glow=True esta pensado para luma->alpha, no es adorno**: la runa se genera brillante
sobre NEGRO puro precisamente para que `comfyui_matting_luma_to_alpha` mapee el
resplandor a alpha y conserve el halo y el degradado. Si extraes el alpha con un matting
binario (rembg) pierdes el glow y la runa queda con borde duro recortado. Para una runa
MATE/grabada (sin resplandor, alpha por recorte) usa `glow=False`.
- **NO inyecta Rembg a proposito**: a diferencia de los builders de sprite/prop/item/icon,
este NO lleva 'Image Rembg (Remove Background)'. El SaveImage toma directo del VAEDecode
(runa sobre fondo uniforme) y el matting es un paso posterior de disco.
- **Es una funcion pura**: solo arma el dict. La generacion real (GPU) la hacen
`comfyui_submit_workflow` + `comfyui_wait_result` + `comfyui_fetch_output_image`; el
alpha lo hace `comfyui_matting_luma_to_alpha`.
- **El fondo debe quedar NEGRO plano (con glow=True)**: si el style "glowing/arcane" se
derrama en una niebla/textura de fondo, el alpha por luminancia recogera basura. El
positivo fuerza "solid pure black background, flat backdrop, bright lines on black" y el
negativo rechaza fog/smoke clouds + fondo texturizado/de otro color. Si aun asi sale
niebla de fondo, sube `cfg` o haz reroll de `seed`.
- **Una runa NO es texto legible**: el modelo tiende a escribir letras reales si el glyph
suena a palabra. El negativo rechaza "realistic text, readable words, latin alphabet,
sentence" para empujar a un glifo arcano abstracto. Si quieres letras concretas de un
alfabeto rúnico real, descríbelo en el `glyph` y relaja el negativo.
- **luma->alpha y el color del glow**: la luminancia Rec601 pesa el rojo a 0.299, asi que
una runa ROJA (sigilo demoniaco) sale mas transparente que una blanca/azul con los pesos
por defecto. Para runas rojas pasa a `comfyui_matting_luma_to_alpha` unos `luma_weights`
con mas peso al rojo y sube `gamma`. Para runas blancas/doradas/azules (la mayoria de
magia brillante) los pesos por defecto van perfectos.
- **SDXL pide mas VRAM y resolucion**: con `checkpoint="juggernaut_xl_v11.safetensors"`
sube `size` a 768/1024; con dreamshaper_8 (SD1.5) deja 512 (holgado en 8GB lowvram). Si
hay OOM, baja `size` o usa SD1.5.
@@ -0,0 +1,223 @@
"""Construye el workflow ComfyUI de UNA runa / glifo / sigilo magico (API format).
Una runa es un simbolo arcano aislado: glifos runicos, circulos magicos, sigilos
de invocacion, inscripciones brillantes — para hechizos, portales, marcas de
conjuro, efectos de magia. La pieza se genera AISLADA sobre un fondo uniforme
(negro por defecto) para poder extraer luego el canal alpha por luminancia.
La tecnica gamedev correcta para una runa BRILLANTE (glifo que emite luz, circulo
magico incandescente, sigilo neon) es generarla sobre fondo NEGRO y convertir la
luminancia en alpha con `comfyui_matting_luma_to_alpha`: brillante -> opaco,
negro -> transparente. Eso preserva el resplandor (el halo y el degradado del
glow) que un matting binario tipo rembg destruiria. Por eso `glow=True` es el modo
por defecto y el scaffold empuja "glowing on pure black background": el PNG
resultante esta pensado para pasar por luma-to-alpha en un paso aparte y quedar
listo para componer en el motor con un blend mode aditivo. `glow=False` (fondo
plano, sin resplandor) sirve para una runa grabada/mate, recortable luego por el
caller de otra forma.
Es el builder hermano de comfyui_build_status_effect_icon_workflow /
comfyui_build_decal_overlay_workflow: mismo patron (PURO, dict API format) que
compone funciones existentes del registry, no reescribe el grafo. Igual que el
builder de decal, este NO inyecta Rembg: el matting de una runa brillante es
luma-to-alpha (post-proceso de disco), no un nodo del workflow.
DISTINTO de status_effect_icon: un icono de estado es un simbolo SOLIDO de UI con
silueta definida (recorte rembg), pequeño y legible a 16-32 px en el HUD. Una runa
es una marca arcana translucida que emite luz, pensada para inscribirse en el
mundo (suelo, portal, arma) y componerse con blend aditivo — su alpha sale del
resplandor, no de un recorte de silueta.
Cableado:
CheckpointLoaderSimple -> [LoraLoader opcional de estilo arcano] -> KSampler
-> CLIPTextEncode (prompt scaffold de runa aislada) ...
-> VAEDecode -> SaveImage (runa sobre fondo uniforme)
Compone:
- comfyui_build_txt2img_workflow -> base txt2img cuadrada
- comfyui_inject_lora -> LoRA de estilo arcano opcional (consistencia)
Pipeline despues de generar (no en este builder):
comfyui_submit_workflow -> comfyui_wait_result -> comfyui_fetch_output_image
-> comfyui_matting_luma_to_alpha (con glow=True) -> PNG RGBA listo para el motor
class_types/inputs verificados contra /object_info del servidor (8GB lowvram):
CheckpointLoaderSimple, CLIPTextEncode, EmptyLatentImage, KSampler, VAEDecode,
SaveImage, LoraLoader.
Funcion pura: sin red, sin I/O. Determinista para los mismos argumentos.
"""
from __future__ import annotations
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
# Negativo comun a cualquier runa: un solo simbolo arcano limpio y centrado, SIN
# escena/objeto 3D/personaje/profundidad ni marco/borde que delaten una
# composicion. Se rechaza tambien el texto legible / caligrafia de alfabetos
# reales para que el modelo dibuje un glifo arcano y no palabras.
_RUNE_NEGATIVE_COMMON = (
"3d render, object, scene, landscape, character, person, creature, "
"realistic text, readable words, latin alphabet, sentence, paragraph, "
"depth of field, perspective, vignette, frame, border, drop shadow, "
"multiple separate symbols scattered, photo, photorealistic, "
"blurry, low quality, jpeg artifacts, watermark, signature, logo"
)
# Refuerzo de fondo segun glow: con glow=True el fondo DEBE quedar NEGRO puro y
# plano para que luma->alpha mapee el resplandor a alpha sin recoger basura. Sin
# esto, un style "arcane glowing" arrastra una niebla/textura de fondo que arruina
# el alpha. Con glow=False (runa mate) se rechaza el resplandor para una marca
# grabada plana.
_RUNE_BG_NEG_GLOW = (
"textured background, gray background, white background, busy background, "
"background pattern, noisy background, gradient background, fog, smoke clouds"
)
_RUNE_BG_NEG_PLAIN = (
"glow, glowing, neon, bloom, light rays, lens flare, "
"busy background, background pattern, noisy background, gradient background"
)
def comfyui_build_rune_glyph_workflow(
glyph: str,
*,
glow: bool = True,
style: str = "arcane glowing rune",
checkpoint: str = "dreamshaper_8.safetensors",
size: int = 512,
seed: int = 0,
lora: str | None = None,
lora_strength: float = 1.0,
negative: str | None = None,
steps: int = 28,
cfg: float = 7.0,
sampler_name: str = "dpmpp_2m",
scheduler: str = "karras",
filename_prefix: str = "rune_glyph",
) -> dict:
"""Construye el dict (API format) del workflow de UNA runa / glifo / sigilo.
Args:
glyph: descripcion del simbolo arcano a generar (ej. "circular summoning
rune", "ancient norse rune", "magic circle with glyphs", "demonic
sigil", "elven inscription", "protective ward symbol", "fire spell
glyph"). Se inserta en un prompt scaffold de runa aislada. No puede
estar vacio.
glow: si True (defecto) la runa se genera BRILLANTE sobre fondo NEGRO puro,
pensada para extraer el alpha por luminancia con
comfyui_matting_luma_to_alpha (conserva el resplandor para componer con
blend aditivo en el motor). Si False se genera una runa MATE/grabada
sobre fondo plano, sin resplandor (recorte/inversion por el caller).
keyword-only.
style: descriptor de estilo arcano que mantiene consistentes las runas de
un set (ej. "arcane glowing rune", "fiery demonic sigil", "icy blue
magic circle", "golden holy glyph", "engraved stone rune"). Pasa el
MISMO style + checkpoint + (lora) a todas las runas de un grimorio 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 a 768/1024). keyword-only.
size: lado del cuadrado en px (width = height = size). 512 SD1.5 por
defecto. keyword-only.
seed: semilla del KSampler. Misma seed + mismos glyph/style -> mismo
simbolo; variar seed da variantes del mismo tipo de runa. keyword-only.
lora: LoRA de estilo arcano opcional en models/loras. None = sin LoRA.
keyword-only.
lora_strength: fuerza del LoRA sobre model y clip. Se clampa a [0.0, 2.0].
keyword-only.
negative: prompt negativo. None usa el negativo por defecto pensado para
una runa aislada (un simbolo arcano centrado, sin escena/objeto/texto
legible/marco, fondo plano). 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 runa ('{glyph}, {style}, magic symbol,
isolated, glowing on pure black background | plain background, centered,
game asset, ...') + LoRA de estilo opcional. NO lleva Rembg: con glow=True
el PNG resultante se convierte a RGBA con comfyui_matting_luma_to_alpha
(luma=alpha) en un paso posterior. Es UNA runa; un grimorio completo ->
llamar por glyph con el mismo style/checkpoint/lora.
Raises:
ValueError: si glyph esta vacio.
"""
from ml.comfyui_build_txt2img_workflow import comfyui_build_txt2img_workflow
if not glyph or not glyph.strip():
raise ValueError(
"comfyui_build_rune_glyph_workflow: 'glyph' no puede estar vacio"
)
glyph = glyph.strip()
lora_strength = max(0.0, min(2.0, float(lora_strength)))
if negative is None:
bg_neg = _RUNE_BG_NEG_GLOW if glow else _RUNE_BG_NEG_PLAIN
neg = f"{_RUNE_NEGATIVE_COMMON}, {bg_neg}"
else:
neg = negative
# Prompt scaffold de runa aislada: un simbolo arcano plano, centrado, sobre
# fondo UNIFORME. Con glow=True el fondo es NEGRO puro y la runa resplandece
# (-> luma-to-alpha despues, blend aditivo en el motor); con glow=False es una
# marca mate sobre fondo plano. El fondo se refuerza para que el resplandor no
# se derrame en una niebla que arruine el alpha.
bg = (
"glowing on a solid pure black background, flat black backdrop, "
"emissive light, bright lines on black"
if glow
else "on a plain flat background, matte engraved symbol"
)
positive = (
f"{glyph}, {style}, magic symbol, single isolated glyph, centered, {bg}, "
"occult sigil, arcane inscription, no scenery, game asset, "
"high detail, crisp lines"
)
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
)
return wf
if __name__ == "__main__":
import json
wf = comfyui_build_rune_glyph_workflow(
"circular summoning rune",
glow=True,
style="arcane glowing rune",
seed=11,
)
print(
json.dumps(
{
"nodes": list(wf),
"classes": sorted({n["class_type"] for n in wf.values()}),
},
indent=2,
)
)
@@ -0,0 +1,130 @@
"""Tests offline de comfyui_build_rune_glyph_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_rune_glyph_workflow import ( # noqa: E402
comfyui_build_rune_glyph_workflow,
)
import pytest # noqa: E402
def _classes(wf):
return sorted({n["class_type"] for n in wf.values()})
def _id_of(wf, cls):
return next(nid for nid, n in wf.items() if n["class_type"] == cls)
def _pos_with(wf, needle):
return next(
n for n in wf.values()
if n["class_type"] == "CLIPTextEncode" and needle in n["inputs"]["text"]
)
def test_golden_rune_glow_on_black_recipe():
wf = comfyui_build_rune_glyph_workflow(
"circular summoning rune", glow=True, seed=11
)
cls = _classes(wf)
# Cadena base txt2img pura: NO lleva Rembg (el matting es luma-to-alpha aparte).
assert "CheckpointLoaderSimple" in cls
assert "KSampler" in cls
assert "VAEDecode" in cls
assert "SaveImage" in cls
assert "Image Rembg (Remove Background)" not in cls
# El glifo + el aislamiento + el resplandor sobre negro aparecen en el positivo.
pos = _pos_with(wf, "circular summoning rune")
txt = pos["inputs"]["text"]
assert "magic symbol" in txt
assert "isolated" in txt
assert "glowing on a solid pure black background" in txt
assert "centered" in txt
assert "game asset" in txt
# SaveImage toma del VAEDecode directamente (sin recorte intermedio).
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_glow_toggles_background():
# glow=True -> resplandor sobre negro plano (pensado para luma->alpha).
wf_glow = comfyui_build_rune_glyph_workflow("fire spell glyph", glow=True)
pos_glow = _pos_with(wf_glow, "fire spell glyph")["inputs"]["text"]
assert "glowing on a solid pure black background" in pos_glow
assert "plain flat background" not in pos_glow
# glow=False -> runa mate sobre fondo plano, sin resplandor; el negativo
# rechaza el glow para una marca grabada.
wf_plain = comfyui_build_rune_glyph_workflow("fire spell glyph", glow=False)
pos_plain = _pos_with(wf_plain, "fire spell glyph")["inputs"]["text"]
assert "plain flat background" in pos_plain
assert "matte engraved symbol" in pos_plain
assert "glowing on a solid pure black background" not in pos_plain
neg_plain = next(
n["inputs"]["text"]
for n in wf_plain.values()
if n["class_type"] == "CLIPTextEncode"
and "fire spell glyph" not in n["inputs"]["text"]
)
assert "glow" in neg_plain # la runa mate rechaza el resplandor
def test_edge_glyph_and_style_reflected():
# glyph y style se reflejan literalmente en el prompt positivo.
wf = comfyui_build_rune_glyph_workflow(
"demonic sigil", style="fiery demonic sigil, ember red", seed=3
)
txt = _pos_with(wf, "demonic sigil")["inputs"]["text"]
assert "demonic sigil" in txt
assert "fiery demonic sigil, ember red" in txt
def test_edge_seed_and_size_propagate():
wf = comfyui_build_rune_glyph_workflow("magic circle", size=384, seed=99)
ks = next(n for n in wf.values() if n["class_type"] == "KSampler")
assert ks["inputs"]["seed"] == 99
latent = next(n for n in wf.values() if n["class_type"] == "EmptyLatentImage")
assert latent["inputs"]["width"] == 384
assert latent["inputs"]["height"] == 384
def test_edge_lora_injected_when_requested():
# Sin LoRA -> no LoraLoader; con LoRA -> aparece el nodo.
wf_no = comfyui_build_rune_glyph_workflow("ward symbol")
assert "LoraLoader" not in _classes(wf_no)
wf_lora = comfyui_build_rune_glyph_workflow(
"ward symbol", lora="arcane_style_sd15.safetensors", lora_strength=0.8
)
assert "LoraLoader" in _classes(wf_lora)
ll = next(n for n in wf_lora.values() if n["class_type"] == "LoraLoader")
assert ll["inputs"]["strength_model"] == pytest.approx(0.8)
assert ll["inputs"]["strength_clip"] == pytest.approx(0.8)
def test_edge_lora_strength_clamped():
wf = comfyui_build_rune_glyph_workflow(
"rune", lora="x.safetensors", lora_strength=5.0
)
ll = next(n for n in wf.values() if n["class_type"] == "LoraLoader")
assert ll["inputs"]["strength_model"] == pytest.approx(2.0) # clamp a 2.0
def test_error_empty_glyph_raises():
with pytest.raises(ValueError):
comfyui_build_rune_glyph_workflow("")
with pytest.raises(ValueError):
comfyui_build_rune_glyph_workflow(" ")
def test_purity_no_global_mutation():
# Dos llamadas identicas producen dicts equivalentes (determinismo).
a = comfyui_build_rune_glyph_workflow("ancient rune", seed=1)
b = comfyui_build_rune_glyph_workflow("ancient rune", seed=1)
assert _classes(a) == _classes(b)
assert _pos_with(a, "ancient rune")["inputs"]["text"] == _pos_with(
b, "ancient rune"
)["inputs"]["text"]