From 5a0818ee9c0e97a53af1dbb9ce419f9be0c0d7b7 Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Sat, 27 Jun 2026 01:55:27 +0200 Subject: [PATCH] =?UTF-8?q?feat(gamedev):=20comfyui=5Fbuild=5Frune=5Fglyph?= =?UTF-8?q?=5Fworkflow=20=E2=80=94=20runas/glifos/sigilos=20m=C3=A1gicos?= =?UTF-8?q?=20(s=C3=ADmbolo=20arcano=20aislado=20sobre=20negro;=20glow=3DT?= =?UTF-8?q?rue=20->=20luma->alpha=20conserva=20resplandor=20para=20blend?= =?UTF-8?q?=20aditivo,=20sin=20Rembg;=20glow=3DFalse=20runa=20mate;=20herm?= =?UTF-8?q?ano=20de=20status=5Feffect=5Ficon/decal=5Foverlay;=20probado=20?= =?UTF-8?q?e2e=20SD1.5=20prompt=5Fid=20701d149a)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/capabilities/gamedev-2d.md | 1 + .../ml/comfyui_build_rune_glyph_workflow.md | 139 +++++++++++ .../ml/comfyui_build_rune_glyph_workflow.py | 223 ++++++++++++++++++ .../comfyui_build_rune_glyph_workflow_test.py | 130 ++++++++++ 4 files changed, 493 insertions(+) create mode 100644 python/functions/ml/comfyui_build_rune_glyph_workflow.md create mode 100644 python/functions/ml/comfyui_build_rune_glyph_workflow.py create mode 100644 python/functions/ml/comfyui_build_rune_glyph_workflow_test.py diff --git a/docs/capabilities/gamedev-2d.md b/docs/capabilities/gamedev-2d.md index 3fc2c3d7..c2b326cd 100644 --- a/docs/capabilities/gamedev-2d.md +++ b/docs/capabilities/gamedev-2d.md @@ -55,6 +55,7 @@ VFX (ver `reports/0143`). | `comfyui_build_structure_workflow_py_ml` | `(structure, *, view="isometric", style="game building", checkpoint="dreamshaper_8…", size=512, transparent=True, seed=0, lora=None, …) -> dict` | UN edificio/estructura de escenario (casa, torre, castillo, tienda, posada, ruina, muralla, puente, templo, faro): UN **building COMPLETO** y centrado a perspectiva de juego (`{view} view`, iso por defecto), fondo limpio recortable a alpha (`{structure}, {view} view, {style}, full building, complete structure, single building, centered, plain background, game asset, architecture…`) → txt2img cuadrado + LoRA estilo/iso opcional + Rembg (alpha). **EDIFICACIÓN grande que ocupa varios tiles y define el escenario**, no un objeto pequeño suelto (≠ `prop_object`, que es atrezzo que se deja sobre un tile); el negativo rechaza `small object / single item / prop / furniture`. `view` fija la perspectiva del mapa (iso/side/front/top-down/¾); LoRA iso fija mejor el ángulo 2:1. Set coherente = mismo `view`/`style`/`checkpoint`/`lora`, varía solo `structure`. Probado e2e en GPU con SD1.5 — `medieval blacksmith shop` iso 512×512 RGBA, edificio centrado recortado a alpha (centroide 0.54/0.53, `reports/0164`). SD1.5. | | `comfyui_build_foliage_set_workflow_py_ml` | `(plant, *, view="side", style="game foliage, stylized", checkpoint="dreamshaper_8…", size=512, transparent=True, seed=0, lora=None, …) -> dict` | UN elemento de **vegetación/foliage** de escenario (árbol, arbusto, hierba alta, flores, helecho, hongo, cactus, tronco caído, juncos, hiedra): UN elemento de **naturaleza ORGÁNICA AISLADO** y centrado a perspectiva de juego (`{view} view`, `side` por defecto), fondo limpio recortable a alpha (`{plant}, {view} view, {style}, single plant element, centered, plain background, game nature asset, natural vegetation, organic, isolated plant…`) → txt2img cuadrado + LoRA estilo opcional + Rembg (alpha). **Vegetación que viste el terreno**, distinta del **objeto MANUFACTURADO** suelto (≠ `prop_object`: barril/cofre/mueble) y del **EDIFICIO** (≠ `structure`: casa/torre); el negativo rechaza `building / manmade object / barrel / furniture / person` y `multiple plants / dense forest / jungle / landscape` (UN elemento, no un bosque) + `pot / planter / vase` (planta en maceta = `prop_object`). Recorte por **Rembg** (planta opaca de silueta definida), no luma→alpha. Set coherente = mismo `view`/`style`/`checkpoint`/`lora`, varía solo `plant`. ⚠️ **dos gotchas reales SD1.5+Rembg**: (1) **plantas grandes (árbol) tienden a PAISAJE** (cielo+campo) en lugar de fondo plano → re-roll de seeds buscando fondo uniforme (`comfyui_batch_generate`); (2) **follaje verde claro sobre fondo claro → Rembg se come las hojas** y deja solo tronco/ramas → preferir elementos de **silueta compacta y color saturado** (hongo, arbusto denso) o `transparent=False` + matting manual. Probado e2e en GPU con SD1.5 — golden `a glowing mushroom` seed 7 512×512 RGBA, hongo centrado recortado a alpha limpio (centroide 0.51/0.58, opaco 19%, `prompt_id 8fb65a51`); evidencia del gotcha del roble en `reports/0170`. SD1.5. | | `comfyui_build_particle_texture_workflow_py_ml` | `(particle, *, soft=True, style="particle texture, soft glow", checkpoint="dreamshaper_8…", size=256, seed=0, lora=None, …) -> dict` | UNA textura de **partícula individual** reutilizable (chispa, humo, polvo, destello/flare, gota, copo, hoja, círculo de energía) — el "ladrillo" que el sistema de partículas del motor (Godot `GPUParticles2D`, Unity VFX Graph) instancia a **miles** y anima (spawn/fade/color over lifetime). Aislada y centrada **sobre fondo NEGRO** (`{particle} particle, {style}, isolated on pure black background, edges, single element, for game particle system…`) → txt2img cuadrado + LoRA estilo opcional. **`soft` controla el borde**: `soft=True` (defecto) → `soft glow, feathered edges` (humo/destello/gota); `soft=False` → `crisp sharp edges, high contrast` (chispa/copo/hoja). **NO inyecta Rembg** (rompería el falloff translúcido): insumo de **`comfyui_matting_luma_to_alpha`** (luma=alpha, additive blend en el motor). **`size` por defecto pequeño (256)** porque se replica a miles. **DISTINTO de `vfx_spritesheet`** (ese es la SECUENCIA animada de un efecto; esto es UNA textura estática reutilizable) **y de `decal_overlay`** (ése es una mancha de desgaste estática para superponer; éste es un emisor de partículas). ⚠️ el `style` por defecto trae "soft glow" → si pides `soft=False` para algo nítido, usa un `style` sin connotación suave. Probado e2e en GPU con SD1.5 — `spark` 256×256 sobre negro plano (dark 85%) + luma→alpha RGBA con falloff preservado (`reports/0163`). SD1.5. | +| `comfyui_build_rune_glyph_workflow_py_ml` | `(glyph, *, glow=True, style="arcane glowing rune", checkpoint="dreamshaper_8…", size=512, seed=0, lora=None, …) -> dict` | UNA **runa / glifo / sigilo mágico** (glifos rúnicos, círculos mágicos, sigilos de invocación, inscripciones brillantes) para hechizos, portales, marcas de conjuro y efectos de magia: símbolo arcano **aislado** sobre fondo uniforme (`{glyph}, {style}, magic symbol, single isolated glyph, centered, glowing on a solid pure black background, occult sigil, arcane inscription, no scenery, game asset…`) → txt2img cuadrado + LoRA estilo opcional. **`glow` elige el camino a alpha**: `glow=True` (defecto) = runa BRILLANTE sobre **NEGRO puro**, **sin Rembg** (recortaría el halo del resplandor), insumo de **`comfyui_matting_luma_to_alpha`** (luma=alpha, **blend aditivo** en el motor — conserva el glow); `glow=False` = runa MATE/grabada sobre fondo plano (el negativo rechaza `glow/neon/bloom`), recorte/inversión por el caller. El negativo rechaza `realistic text/readable words/latin alphabet` (un glifo arcano, **no letras reales**) + fondo texturizado/niebla. **DISTINTO de `status_effect_icon`** (símbolo SÓLIDO de UI, recorte Rembg, legible a 16-32 px en el HUD): la runa es una marca translúcida que **emite luz** e se inscribe en el mundo. Grimorio coherente = mismo `style`/`checkpoint`/`lora`, varía `glyph`/`seed`. ⚠️ luma Rec601 penaliza el rojo → para runas rojas (sigilo demoníaco) pasar `luma_weights` con más peso al rojo + subir `gamma`; runas blancas/azules/doradas van con pesos por defecto. Probado e2e en GPU con SD1.5 — `circular summoning rune` glow seed 11 512×512, círculo de invocación brillante sobre **negro puro** (esquinas luma 0.00, dark 83%, runa 3.4% brillante, max 255) apto luma→alpha (`prompt_id 701d149a`, `reports/0172`). SD1.5. | | `comfyui_build_title_lettering_workflow_py_ml` | `(text, *, letter_style="epic fantasy metallic", checkpoint="juggernaut_xl_v11…", width=1024, height=512, transparent=True, seed=0, lora=None, …) -> dict` | EL texto/logo de **título** de un juego (el nombre del juego o una palabra) renderizado con un **tratamiento de lettering** (metálico, tallado en fuego/piedra/madera, neón, cristal, oro), formato **apaisado** (`width>height`, 1024×512 por defecto), fondo plano recortable a alpha (`the word "{text}" as a game logo, {letter_style} lettering, stylized typography, centered, plain background…`) → txt2img apaisado + LoRA estilo opcional + Rembg (alpha). El **negativo NO rechaza texto** (el lettering es el sujeto) y empuja contra el ruido textual (`extra letters/jumbled text/deformed letters`). El VALOR es el ESTILO del lettering, **NO** la fidelidad tipográfica: ⚠️ la difusión renderiza texto de forma imperfecta — letras de más, deformadas o mal escritas; mitigar con palabras CORTAS en MAYÚSCULA, **re-roll de seeds** (`comfyui_batch_generate`), SDXL > SD1.5 para texto, o pintar el texto real con una fuente en el motor. **Una palabra que es un objeto concreto (DRAGON) → el modelo dibuja el objeto, no las letras** — usar palabras abstractas o reforzar `letter_style`. Marca coherente = mismo `letter_style`/`checkpoint`/`lora`, varía solo `text`. Recorte por **Rembg** (logo sólido), no luma→alpha. Probado e2e en GPU: `DRAGON`/`fire engraved` SD1.5 1024×512 → ilustró dragones rojos (alpha OK, confirma el gotcha de palabra-objeto, `prompt_id 6f3920b7`); `AETHER`/`epic fantasy metallic` SDXL 768×384 → **logo de texto metálico dorado** legible con ortografía imperfecta + alpha (`prompt_id 2a7fe8ba`, `reports/0165`). SD1.5/SDXL. | ## Funciones de post-proceso y puente (`gamedev`, CPU) diff --git a/python/functions/ml/comfyui_build_rune_glyph_workflow.md b/python/functions/ml/comfyui_build_rune_glyph_workflow.md new file mode 100644 index 00000000..c7e15316 --- /dev/null +++ b/python/functions/ml/comfyui_build_rune_glyph_workflow.md @@ -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. diff --git a/python/functions/ml/comfyui_build_rune_glyph_workflow.py b/python/functions/ml/comfyui_build_rune_glyph_workflow.py new file mode 100644 index 00000000..c6758189 --- /dev/null +++ b/python/functions/ml/comfyui_build_rune_glyph_workflow.py @@ -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, + ) + ) diff --git a/python/functions/ml/comfyui_build_rune_glyph_workflow_test.py b/python/functions/ml/comfyui_build_rune_glyph_workflow_test.py new file mode 100644 index 00000000..759aefe9 --- /dev/null +++ b/python/functions/ml/comfyui_build_rune_glyph_workflow_test.py @@ -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"]