feat(gamedev): comfyui_build_splash_art_workflow — splash/loading screen key art (apaisado 16:9, cinematográfico, espacio para título)
Builder PURO (dict API format) del grupo gamedev/gamedev-2d, hermano de comfyui_build_card_art_workflow y comfyui_build_parallax_background_workflow. Genera la ilustración grande de una pantalla de portada / loading screen / key art en formato pantalla apaisado 16:9 (~1024x576), composición cinematográfica (wide shot) con aire para superponer el título del juego. Compone comfyui_build_hires_fix_workflow (si hires) o comfyui_build_txt2img_workflow + comfyui_inject_lora (estilo opcional). Genera SOLO la ilustración: el negativo por defecto rechaza text/title/logo/UI/frame para que el motor componga el título encima. - 9 tests offline verde (golden hires, apaisado width>height, batch_size, sin hires, dims/mood/lora reflejados, error scene vacío, determinismo). - .md autosuficiente (Ejemplo + Cuando usarla + Gotchas) + fila en docs/capabilities/gamedev-2d.md. - Probado e2e en GPU 8GB lowvram: 1 splash real (héroe ante castillo oscuro en tormenta), 1024x576 -> 1536x864 (16:9 exacto) tras hires, 54s, SD1.5 dreamshaper_8. Evidencia en reports/0159. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -45,6 +45,7 @@ VFX (ver `reports/0143`).
|
|||||||
| `comfyui_build_enemy_creature_workflow_py_ml` | `(creature, *, variant=None, style="game creature, full body", checkpoint="dreamshaper_8…", size=512, transparent=True, seed=0, lora=None, …) -> dict` | UN enemigo/criatura de juego (goblin, esqueleto, slime, dragón, boss, elemental): figura de **cuerpo entero** centrada, fondo limpio recortable a alpha (`{variant} {creature}, {style}, full body, centered, plain background, game asset…`) → txt2img cuadrado + LoRA estilo opcional + Rembg (alpha). `variant` (ice/fire/elite/corrupted…) se antepone a la criatura para generar la familia del MISMO enemigo (misma `creature`/`seed`/`style`, varía solo `variant`); bestiario coherente = mismo `style`/`checkpoint`/`lora`, varía solo `creature`. El negativo empuja a UNA criatura entera sin recorte. Probado e2e en GPU con SD1.5 (`reports/0154`). SD1.5. |
|
| `comfyui_build_enemy_creature_workflow_py_ml` | `(creature, *, variant=None, style="game creature, full body", checkpoint="dreamshaper_8…", size=512, transparent=True, seed=0, lora=None, …) -> dict` | UN enemigo/criatura de juego (goblin, esqueleto, slime, dragón, boss, elemental): figura de **cuerpo entero** centrada, fondo limpio recortable a alpha (`{variant} {creature}, {style}, full body, centered, plain background, game asset…`) → txt2img cuadrado + LoRA estilo opcional + Rembg (alpha). `variant` (ice/fire/elite/corrupted…) se antepone a la criatura para generar la familia del MISMO enemigo (misma `creature`/`seed`/`style`, varía solo `variant`); bestiario coherente = mismo `style`/`checkpoint`/`lora`, varía solo `creature`. El negativo empuja a UNA criatura entera sin recorte. Probado e2e en GPU con SD1.5 (`reports/0154`). SD1.5. |
|
||||||
| `comfyui_build_prop_object_workflow_py_ml` | `(prop, *, style="game prop, isometric or side view", checkpoint="dreamshaper_8…", size=512, transparent=True, seed=0, lora=None, …) -> dict` | UN prop/objeto de escenario (barril, cofre, antorcha, planta, mueble, roca, fuente, estatua): objeto inanimado aislado a **escala de escena y perspectiva de juego** (iso/lateral), centrado, fondo limpio recortable a alpha (`{prop}, {style}, game asset, single object, centered, plain background, scene prop, world object…`) → txt2img cuadrado + LoRA estilo opcional + Rembg (alpha). **Objeto de MUNDO**, no icono plano de inventario (≠ `item_icon`, que es para una casilla de UI); este puebla el nivel. Atrezzo coherente = mismo `style`/`checkpoint`/`lora`, varía solo `prop`. El negativo excluye personas/criaturas (objeto inanimado). Probado e2e en GPU con SD1.5 (`reports/0155`). SD1.5. |
|
| `comfyui_build_prop_object_workflow_py_ml` | `(prop, *, style="game prop, isometric or side view", checkpoint="dreamshaper_8…", size=512, transparent=True, seed=0, lora=None, …) -> dict` | UN prop/objeto de escenario (barril, cofre, antorcha, planta, mueble, roca, fuente, estatua): objeto inanimado aislado a **escala de escena y perspectiva de juego** (iso/lateral), centrado, fondo limpio recortable a alpha (`{prop}, {style}, game asset, single object, centered, plain background, scene prop, world object…`) → txt2img cuadrado + LoRA estilo opcional + Rembg (alpha). **Objeto de MUNDO**, no icono plano de inventario (≠ `item_icon`, que es para una casilla de UI); este puebla el nivel. Atrezzo coherente = mismo `style`/`checkpoint`/`lora`, varía solo `prop`. El negativo excluye personas/criaturas (objeto inanimado). Probado e2e en GPU con SD1.5 (`reports/0155`). SD1.5. |
|
||||||
| `comfyui_build_topdown_sprite_workflow_py_ml` | `(subject, *, direction="south", style="top-down game sprite, RPG", checkpoint="dreamshaper_8…", size=512, transparent=True, seed=0, lora=None, …) -> dict` | UN sprite en **vista CENITAL (top-down)** estilo RPG clásico/roguelike (Zelda, juegos cenitales): personaje/objeto visto **desde arriba**, centrado, fondo limpio recortable a alpha (`{subject}, top-down view, overhead view, {direction} facing, {style}, centered, plain background, game asset…`) → txt2img cuadrado + LoRA estilo opcional + Rembg (alpha). `direction` (south/north/east/west) para el sprite de movimiento: las 4 vistas del MISMO personaje = misma `subject`/`style`/`seed`, varía solo `direction` → montar con `comfyui_build_grid`. **DISTINTO de `sprite_sheet` (vista lateral/frontal de plataformas)**: el negativo por defecto rechaza side/front/3-4/isometric/perspective para forzar la cenital. Con SD1.5 sin LoRA sale picado alto; cenital estricto pide LoRA top-down + cfg alto. Probado e2e en GPU con SD1.5 (`reports/0156`). SD1.5. |
|
| `comfyui_build_topdown_sprite_workflow_py_ml` | `(subject, *, direction="south", style="top-down game sprite, RPG", checkpoint="dreamshaper_8…", size=512, transparent=True, seed=0, lora=None, …) -> dict` | UN sprite en **vista CENITAL (top-down)** estilo RPG clásico/roguelike (Zelda, juegos cenitales): personaje/objeto visto **desde arriba**, centrado, fondo limpio recortable a alpha (`{subject}, top-down view, overhead view, {direction} facing, {style}, centered, plain background, game asset…`) → txt2img cuadrado + LoRA estilo opcional + Rembg (alpha). `direction` (south/north/east/west) para el sprite de movimiento: las 4 vistas del MISMO personaje = misma `subject`/`style`/`seed`, varía solo `direction` → montar con `comfyui_build_grid`. **DISTINTO de `sprite_sheet` (vista lateral/frontal de plataformas)**: el negativo por defecto rechaza side/front/3-4/isometric/perspective para forzar la cenital. Con SD1.5 sin LoRA sale picado alto; cenital estricto pide LoRA top-down + cfg alto. Probado e2e en GPU con SD1.5 (`reports/0156`). SD1.5. |
|
||||||
|
| `comfyui_build_splash_art_workflow_py_ml` | `(scene, *, mood="epic, cinematic", checkpoint="juggernaut_xl_v11…", width=1024, height=576, hires=True, seed=0, lora=None, …) -> dict` | LA ilustración grande de UN splash / pantalla de carga / key art en formato **pantalla apaisado 16:9** (`width>height`, ~1024×576), composición cinematográfica (`{scene}, {mood}, key art, game splash screen, dramatic lighting, cinematic composition, wide shot, epic scale, atmospheric…`). `hires=True` → 2ª pasada de detalle (`comfyui_build_hires_fix_workflow`) para verse a pantalla completa; si no, txt2img + LoRA estilo opcional. Genera SOLO la ilustración — el título/logo/barra de carga los pone el motor/post (negativo rechaza `text/title/logo/UI/frame/watermark`), dejando aire para superponer el título. Set coherente = mismo `mood`/`checkpoint`/`lora`, varía solo `scene`. Probado e2e en GPU con SD1.5 + hires (1024×576 → 1536×864, 54s, `reports/0159`). SD1.5/SDXL. |
|
||||||
|
|
||||||
## Funciones de post-proceso y puente (`gamedev`, CPU)
|
## Funciones de post-proceso y puente (`gamedev`, CPU)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
---
|
||||||
|
name: comfyui_build_splash_art_workflow
|
||||||
|
kind: function
|
||||||
|
lang: py
|
||||||
|
domain: ml
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: pure
|
||||||
|
signature: "def comfyui_build_splash_art_workflow(scene: str, *, mood: str = \"epic, cinematic\", checkpoint: str = \"juggernaut_xl_v11.safetensors\", width: int = 1024, height: int = 576, hires: bool = True, 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\", upscale_by: float = 1.5, hires_denoise: float = 0.4, upscale_model: str = \"4x_foolhardy_Remacri.pth\", filename_prefix: str = \"splash_art\") -> dict"
|
||||||
|
description: "Construye el dict (API format) del workflow de UN splash art / pantalla de carga / key art 2D: ilustracion grande y dramatica en formato pantalla apaisado 16:9 (~1024x576), composicion cinematografica (wide shot) con espacio para el titulo del juego. Genera SOLO la ilustracion (el titulo/logo/barra de carga NO). Compone comfyui_build_hires_fix_workflow (si hires) o comfyui_build_txt2img_workflow + comfyui_inject_lora (estilo opcional). Hermano de comfyui_build_card_art/parallax_background_workflow. Pura, sin red ni I/O. class_types verificados contra /object_info (8GB lowvram)."
|
||||||
|
tags: [comfyui, ml, gamedev, gamedev-2d, splash, key-art, loading-screen, illustration, workflow]
|
||||||
|
uses_functions: [comfyui_build_txt2img_workflow_py_ml, comfyui_build_hires_fix_workflow_py_ml, comfyui_inject_lora_py_ml]
|
||||||
|
uses_types: []
|
||||||
|
returns: []
|
||||||
|
returns_optional: false
|
||||||
|
error_type: ""
|
||||||
|
imports: []
|
||||||
|
params:
|
||||||
|
- name: scene
|
||||||
|
desc: "Descripcion de la escena del splash (ej. 'a lone hero before a dark castle, storm', 'a spaceship fleet approaching a ringed planet', 'an ancient forest temple bathed in golden light'). Se inserta en un prompt scaffold de key art cinematografico. No puede estar vacio."
|
||||||
|
- name: mood
|
||||||
|
desc: "Atmosfera/tono que tine el splash (ej. 'epic, cinematic', 'dark, ominous', 'bright, hopeful', 'mysterious, ethereal'). Pasa el MISMO mood + checkpoint + lora a varias pantallas para coherencia visual del juego. keyword-only."
|
||||||
|
- name: checkpoint
|
||||||
|
desc: "Checkpoint del servidor. 'juggernaut_xl_v11.safetensors' (SDXL, mejor detalle, ideal para key art) por defecto; en 8GB lowvram con hires puede ser pesado: usa 'dreamshaper_8.safetensors' (SD1.5) y/o hires=False si la GPU se queda corta (OOM). keyword-only."
|
||||||
|
- name: width
|
||||||
|
desc: "Ancho del lienzo en px. Apaisado de pantalla -> width > height. 1024 por defecto. keyword-only."
|
||||||
|
- name: height
|
||||||
|
desc: "Alto del lienzo en px. 576 por defecto (16:9 exacto con width=1024). Para SDXL nativo, 1152x640 o 1344x768 lucen mejor (subir ambos manteniendo ~16:9). keyword-only."
|
||||||
|
- name: hires
|
||||||
|
desc: "Si True encadena la 2a pasada de detalle (UltimateSDUpscale + Remacri, re-difusion por tiles) sobre la base apaisada (key art mas detallado a pantalla completa). False deja la imagen tal cual sale del VAEDecode. keyword-only."
|
||||||
|
- name: seed
|
||||||
|
desc: "Semilla del KSampler (y de la pasada hires). Misma seed + misma scene -> misma ilustracion. keyword-only."
|
||||||
|
- name: lora
|
||||||
|
desc: "LoRA de estilo opcional en models/loras (ej. 'concept_art_sd15.safetensors', 'cinematic_xl.safetensors'). None = sin LoRA. Encadena la identidad visual del juego. 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 key art (ilustracion limpia, sin texto/titulo/logo/UI/marco/watermark). keyword-only."
|
||||||
|
- name: steps
|
||||||
|
desc: "Pasos del KSampler (y de la pasada hires). keyword-only."
|
||||||
|
- name: cfg
|
||||||
|
desc: "CFG del KSampler (y de la pasada hires). keyword-only."
|
||||||
|
- name: sampler_name
|
||||||
|
desc: "Sampler del KSampler. keyword-only."
|
||||||
|
- name: scheduler
|
||||||
|
desc: "Scheduler del KSampler. keyword-only."
|
||||||
|
- name: upscale_by
|
||||||
|
desc: "Factor de ampliacion de la pasada hires sobre la base (1.5 -> 1024x576 pasa a 1536x864). Solo si hires=True. keyword-only."
|
||||||
|
- name: hires_denoise
|
||||||
|
desc: "Fuerza de re-difusion de la pasada hires (0.4 por defecto: anade detalle sin alterar la composicion). Solo si hires=True. keyword-only."
|
||||||
|
- name: upscale_model
|
||||||
|
desc: "Modelo de upscale en models/upscale_models/ que usa la pasada hires ('4x_foolhardy_Remacri.pth'). Solo si hires=True. keyword-only."
|
||||||
|
- name: filename_prefix
|
||||||
|
desc: "Prefijo del PNG en output/. keyword-only."
|
||||||
|
output: "dict en API format listo para comfyui_submit_workflow: base apaisada (hires-fix si hires, txt2img si no) con prompt scaffold de key art ('{scene}, {mood}, key art, game splash screen, dramatic lighting, cinematic composition, wide shot, epic scale, atmospheric, ...') + LoRA de estilo opcional. UNA pantalla; set coherente -> llamar por scene con mismo mood/checkpoint/lora. El titulo/logo/barra de carga los pone el motor/post, no este workflow."
|
||||||
|
tested: true
|
||||||
|
tests: ["golden hires: clases CheckpointLoaderSimple/KSampler/VAEDecode/UltimateSDUpscale/UpscaleModelLoader/SaveImage; scene + 'epic, cinematic' + 'key art' + 'cinematic composition' + 'wide shot' + 'dramatic lighting' en prompt; base apaisada 1024x576 (width>height); UltimateSDUpscale.batch_size=1", "edge hires=False: sin UltimateSDUpscale/UpscaleModelLoader, SaveImage <- VAEDecode", "edge dims: width=1152/height=640 reflejados, sigue apaisado", "edge mood en prompt", "edge lora: LoraLoader con strength, KSampler.model <- LoraLoader", "edge lora_strength clamp a [0,2]", "edge hires upscale_by reflejado en UltimateSDUpscale", "error scene vacio -> ValueError", "determinismo"]
|
||||||
|
test_file_path: "python/functions/ml/comfyui_build_splash_art_workflow_test.py"
|
||||||
|
file_path: "python/functions/ml/comfyui_build_splash_art_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_splash_art_workflow import comfyui_build_splash_art_workflow
|
||||||
|
|
||||||
|
# Splash / key art apaisado 16:9 de un heroe ante un castillo en tormenta, con detalle hires.
|
||||||
|
# En 8GB lowvram va holgado con SD1.5 (dreamshaper_8); SDXL+hires es mas pesado (puede OOM).
|
||||||
|
wf = comfyui_build_splash_art_workflow(
|
||||||
|
"a lone hero before a dark castle, storm",
|
||||||
|
mood="epic, cinematic",
|
||||||
|
checkpoint="dreamshaper_8.safetensors",
|
||||||
|
hires=True,
|
||||||
|
seed=7,
|
||||||
|
)
|
||||||
|
# Set coherente: mismo mood/checkpoint para varias pantallas, varia solo scene.
|
||||||
|
# for s in ["title screen vista", "world map overview", "final boss arena"]:
|
||||||
|
# wf = comfyui_build_splash_art_workflow(s, mood="epic, cinematic",
|
||||||
|
# checkpoint="dreamshaper_8.safetensors", seed=7)
|
||||||
|
# comfyui_submit_workflow(wf) # -> comfyui_wait_result -> comfyui_fetch_output_image
|
||||||
|
# El titulo/logo/barra de carga los compone el motor de juego sobre la ilustracion resultante.
|
||||||
|
```
|
||||||
|
|
||||||
|
O lanzable directo con: `./fn run comfyui_build_splash_art_workflow` (imprime nodos + class_types del ejemplo).
|
||||||
|
|
||||||
|
## Cuando usarla
|
||||||
|
|
||||||
|
Cuando necesites la ILUSTRACION grande de una pantalla de portada, pantalla de carga
|
||||||
|
(loading screen) o key art promocional: una escena dramatica en formato pantalla
|
||||||
|
apaisado (16:9), composicion cinematografica (wide shot, plano general) con aire para
|
||||||
|
que el motor superponga el titulo del juego. Pasa el MISMO `mood` + `checkpoint` +
|
||||||
|
(`lora`) a todas las pantallas del juego para que combinen visualmente; varia solo
|
||||||
|
`scene`. Usa `hires=True` para detalle a pantalla completa; `hires=False` para iterar
|
||||||
|
rapido. El titulo, el logo y la barra de progreso los pone el motor de juego o un paso
|
||||||
|
de post sobre la ilustracion — este builder NO los pinta.
|
||||||
|
|
||||||
|
## Gotchas
|
||||||
|
|
||||||
|
- **Genera SOLO la ilustracion, no el titulo/UI**: el titulo del juego, el logo, la
|
||||||
|
barra de progreso de carga y cualquier texto los compone el motor/post. El prompt
|
||||||
|
scaffold empuja a "key art, cinematic composition, wide shot" y el negativo por
|
||||||
|
defecto rechaza "text / title / logo / UI / frame / watermark". Si quieres el titulo
|
||||||
|
horneado en la imagen, pasa un `negative` propio sin esos terminos y describelo en
|
||||||
|
`scene` (no recomendado: el motor compone el texto mejor y nitido).
|
||||||
|
- **Formato apaisado = `width > height`**: un splash ocupa la pantalla, que es mas
|
||||||
|
ancha que alta. 1024x576 (SD1.5, 16:9 exacto) o 1152x640 / 1344x768 (SDXL nativo). Si
|
||||||
|
pones width<=height pierdes el encuadre de pantalla.
|
||||||
|
- **SDXL + hires es pesado en 8GB lowvram**: el default `juggernaut_xl_v11` con
|
||||||
|
`hires=True` re-difunde por tiles y puede dar OOM o ir muy lento. Si la GPU se queda
|
||||||
|
corta: baja a `checkpoint="dreamshaper_8.safetensors"` (SD1.5), pon `hires=False`, o
|
||||||
|
reduce `width/height`. Probado e2e en GPU con SD1.5 + hires (ver report 0159).
|
||||||
|
- **hires requiere UltimateSDUpscale + Remacri**: si el server responde HTTP 400
|
||||||
|
"node type not found: UltimateSDUpscale", falta el custom node; usa `hires=False`. El
|
||||||
|
`upscale_model` ('4x_foolhardy_Remacri.pth') debe existir en `models/upscale_models/`.
|
||||||
|
- **Coherencia del juego = mismos parametros**: si cambias `mood`/`checkpoint`/`lora`/
|
||||||
|
`seed` entre pantallas, dejan de combinar. Fija esos y varia solo `scene`.
|
||||||
|
- `hires_denoise` alto (>0.6) en la 2a pasada puede deformar la composicion; 0.3-0.45
|
||||||
|
anade detalle sin alterarla.
|
||||||
|
- 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,231 @@
|
|||||||
|
"""Construye el workflow ComfyUI de UN splash art / pantalla de carga / key art (API format).
|
||||||
|
|
||||||
|
Ilustracion grande y dramatica en formato pantalla (apaisado 16:9 ~1024x576),
|
||||||
|
composicion cinematografica con espacio para el titulo del juego: la imagen de
|
||||||
|
portada / loading screen / key art que se ve al arrancar el juego, en una pantalla
|
||||||
|
de carga, o como arte promocional. Es el builder hermano de
|
||||||
|
comfyui_build_card_art_workflow / comfyui_build_parallax_background_workflow: mismo
|
||||||
|
patron (PURO, dict API format) que compone funciones existentes del registry, no
|
||||||
|
reescribe el grafo.
|
||||||
|
|
||||||
|
IMPORTANTE — alcance del builder: genera SOLO la ILUSTRACION apaisada (key art). El
|
||||||
|
titulo del juego, el logo, la barra de progreso de carga y cualquier texto/UI NO los
|
||||||
|
pinta este workflow: son composicion del motor de juego o de un paso de post sobre la
|
||||||
|
ilustracion. El prompt scaffold empuja a "key art, cinematic composition, wide shot"
|
||||||
|
y el negativo por defecto rechaza "text / title / logo / UI / watermark" para
|
||||||
|
mantener la ilustracion limpia, dejando aire para que el motor superponga el titulo.
|
||||||
|
|
||||||
|
Cableado segun los argumentos:
|
||||||
|
|
||||||
|
[hires] CheckpointLoaderSimple -> [LoraLoader si lora] -> KSampler (base) ->
|
||||||
|
VAEDecode -> UpscaleModelLoader + UltimateSDUpscale -> SaveImage
|
||||||
|
[plano] CheckpointLoaderSimple -> [LoraLoader si lora] -> KSampler ->
|
||||||
|
VAEDecode -> SaveImage
|
||||||
|
|
||||||
|
Compone:
|
||||||
|
- comfyui_build_hires_fix_workflow (si hires) -> base apaisada + 2a pasada de
|
||||||
|
detalle por tiles (UltimateSDUpscale + Remacri); el splash luce el detalle fino
|
||||||
|
propio de un key art que se mira a pantalla completa.
|
||||||
|
- comfyui_build_txt2img_workflow (si no hires) -> base txt2img apaisada simple.
|
||||||
|
- comfyui_inject_lora -> LoRA de estilo opcional (fantasy / anime / realista /
|
||||||
|
pintura concept-art) para fijar la identidad visual del juego.
|
||||||
|
|
||||||
|
Por que apaisado (width > height): un splash/loading screen ocupa la pantalla, que
|
||||||
|
es mas ancha que alta; 1024x576 (SD1.5, 16:9 exacto) o 1152x640 / 1344x768 (SDXL
|
||||||
|
nativo) dan el encuadre de pantalla. La composicion es cinematografica (wide shot,
|
||||||
|
plano general) dejando un lado o la parte superior con aire para el titulo.
|
||||||
|
|
||||||
|
Por que hires opcional: un key art se mira grande (pantalla completa), asi que el
|
||||||
|
detalle importa; hires re-difunde la imagen por tiles y anade detalle real. En 8GB
|
||||||
|
lowvram con SDXL puede ser pesado: bajar a SD1.5 (dreamshaper_8) o poner hires=False
|
||||||
|
si la GPU se queda corta (OOM).
|
||||||
|
|
||||||
|
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, UpscaleModelLoader,
|
||||||
|
UltimateSDUpscale).
|
||||||
|
|
||||||
|
Funcion pura: sin red, sin I/O. No muta dicts de entrada (los builders/inyectores
|
||||||
|
que compone trabajan sobre copias). 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 por defecto pensado para key art: ilustracion grande, limpia y dramatica,
|
||||||
|
# SIN texto/titulo/logo/UI ni marcas (que son trabajo del motor/post, no de la
|
||||||
|
# ilustracion). NO rechaza personajes: el heroe/escena suele ser el centro del splash.
|
||||||
|
_SPLASH_NEGATIVE = (
|
||||||
|
"blurry, lowres, deformed, disfigured, bad anatomy, extra limbs, "
|
||||||
|
"extra fingers, mutated hands, ugly, text, title, logo, watermark, "
|
||||||
|
"signature, label, UI, user interface, frame, border, "
|
||||||
|
"cropped, out of frame, jpeg artifacts, low quality"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def comfyui_build_splash_art_workflow(
|
||||||
|
scene: str,
|
||||||
|
*,
|
||||||
|
mood: str = "epic, cinematic",
|
||||||
|
checkpoint: str = "juggernaut_xl_v11.safetensors",
|
||||||
|
width: int = 1024,
|
||||||
|
height: int = 576,
|
||||||
|
hires: bool = True,
|
||||||
|
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",
|
||||||
|
upscale_by: float = 1.5,
|
||||||
|
hires_denoise: float = 0.4,
|
||||||
|
upscale_model: str = "4x_foolhardy_Remacri.pth",
|
||||||
|
filename_prefix: str = "splash_art",
|
||||||
|
) -> dict:
|
||||||
|
"""Construye el dict (API format) del workflow de un splash art / key art apaisado.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
scene: descripcion de la escena del splash (ej. "a lone hero before a dark
|
||||||
|
castle, storm", "a spaceship fleet approaching a ringed planet", "an
|
||||||
|
ancient forest temple bathed in golden light"). Se inserta en un prompt
|
||||||
|
scaffold de key art cinematografico. No puede estar vacio.
|
||||||
|
mood: atmosfera/tono que tine el splash (ej. "epic, cinematic", "dark,
|
||||||
|
ominous", "bright, hopeful", "mysterious, ethereal"). Pasa el MISMO mood
|
||||||
|
+ checkpoint + (lora) a varias pantallas para coherencia visual del
|
||||||
|
juego. keyword-only.
|
||||||
|
checkpoint: checkpoint del servidor. 'juggernaut_xl_v11.safetensors' (SDXL,
|
||||||
|
mejor detalle a alta resolucion, ideal para key art) por defecto; en 8GB
|
||||||
|
lowvram con hires puede ser pesado: si la GPU se queda corta, usa
|
||||||
|
'dreamshaper_8.safetensors' (SD1.5) y/o hires=False. keyword-only.
|
||||||
|
width: ancho del lienzo en px. Apaisado de pantalla -> width > height. 1024
|
||||||
|
por defecto. keyword-only.
|
||||||
|
height: alto del lienzo en px. 576 por defecto (16:9 exacto con width=1024,
|
||||||
|
formato de pantalla). Para SDXL nativo, 1152x640 o 1344x768 lucen mejor
|
||||||
|
(subir ambos manteniendo ~16:9). keyword-only.
|
||||||
|
hires: si True encadena la 2a pasada de detalle (UltimateSDUpscale + Remacri,
|
||||||
|
re-difusion por tiles) sobre la base apaisada para un key art mas
|
||||||
|
detallado a pantalla completa. False deja la imagen tal cual sale del
|
||||||
|
VAEDecode. keyword-only.
|
||||||
|
seed: semilla del KSampler (y de la pasada hires). Misma seed + misma scene
|
||||||
|
-> misma ilustracion. keyword-only.
|
||||||
|
lora: LoRA de estilo opcional en models/loras (ej.
|
||||||
|
'concept_art_sd15.safetensors', 'cinematic_xl.safetensors'). None = sin
|
||||||
|
LoRA. Encadena la identidad visual del juego. 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 key
|
||||||
|
art (ilustracion limpia, sin texto/titulo/logo/UI). keyword-only.
|
||||||
|
steps: pasos del KSampler (y de la pasada hires). keyword-only.
|
||||||
|
cfg: CFG del KSampler (y de la pasada hires). keyword-only.
|
||||||
|
sampler_name: sampler del KSampler. keyword-only.
|
||||||
|
scheduler: scheduler del KSampler. keyword-only.
|
||||||
|
upscale_by: factor de ampliacion de la pasada hires sobre la base (1.5 ->
|
||||||
|
1024x576 pasa a 1536x864). Solo se usa si hires=True. keyword-only.
|
||||||
|
hires_denoise: fuerza de re-difusion de la pasada hires (0.4 por defecto:
|
||||||
|
anade detalle sin alterar la composicion). Solo si hires=True.
|
||||||
|
keyword-only.
|
||||||
|
upscale_model: modelo de upscale en models/upscale_models/ que usa la pasada
|
||||||
|
hires ('4x_foolhardy_Remacri.pth'). Solo si hires=True. keyword-only.
|
||||||
|
filename_prefix: prefijo del PNG generado en output/. keyword-only.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict en API format listo para comfyui_submit_workflow: base apaisada
|
||||||
|
(hires-fix si hires, txt2img si no) con prompt scaffold de key art
|
||||||
|
('{scene}, {mood}, key art, game splash screen, dramatic lighting, cinematic
|
||||||
|
composition, wide shot, ...') + LoRA de estilo opcional. Es UNA pantalla; un
|
||||||
|
set coherente -> llamar por scene con el mismo mood/checkpoint/(lora). El
|
||||||
|
titulo/logo/barra de carga los pone el motor/post, no este workflow.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: si scene esta vacio, o si los builders/inyectores que compone no
|
||||||
|
encuentran los nodos donde enganchar (propagado).
|
||||||
|
"""
|
||||||
|
if not scene or not scene.strip():
|
||||||
|
raise ValueError(
|
||||||
|
"comfyui_build_splash_art_workflow: 'scene' no puede estar vacio"
|
||||||
|
)
|
||||||
|
|
||||||
|
scene = scene.strip()
|
||||||
|
lora_strength = max(0.0, min(2.0, float(lora_strength)))
|
||||||
|
neg = _SPLASH_NEGATIVE if negative is None else negative
|
||||||
|
|
||||||
|
# Prompt scaffold de key art: escena dramatica, composicion cinematografica
|
||||||
|
# apaisada (wide shot), dejando aire para el titulo del juego (que pone el
|
||||||
|
# motor/post). 'epic scale' y 'atmospheric' empujan el look de portada.
|
||||||
|
positive = (
|
||||||
|
f"{scene}, {mood}, key art, game splash screen, dramatic lighting, "
|
||||||
|
"cinematic composition, wide shot, epic scale, atmospheric, "
|
||||||
|
"highly detailed, concept art, high quality"
|
||||||
|
)
|
||||||
|
|
||||||
|
if hires:
|
||||||
|
from ml.comfyui_build_hires_fix_workflow import (
|
||||||
|
comfyui_build_hires_fix_workflow,
|
||||||
|
)
|
||||||
|
|
||||||
|
wf = comfyui_build_hires_fix_workflow(
|
||||||
|
checkpoint,
|
||||||
|
positive,
|
||||||
|
neg,
|
||||||
|
first_pass=(width, height),
|
||||||
|
upscale_by=upscale_by,
|
||||||
|
denoise=hires_denoise,
|
||||||
|
steps=steps,
|
||||||
|
cfg=cfg,
|
||||||
|
seed=seed,
|
||||||
|
upscale_model=upscale_model,
|
||||||
|
sampler_name=sampler_name,
|
||||||
|
scheduler=scheduler,
|
||||||
|
filename_prefix=filename_prefix,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
from ml.comfyui_build_txt2img_workflow import comfyui_build_txt2img_workflow
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
return wf
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import json
|
||||||
|
|
||||||
|
wf = comfyui_build_splash_art_workflow(
|
||||||
|
"a lone hero before a dark castle, storm",
|
||||||
|
mood="epic, cinematic",
|
||||||
|
checkpoint="dreamshaper_8.safetensors",
|
||||||
|
hires=True,
|
||||||
|
seed=7,
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"nodes": list(wf),
|
||||||
|
"classes": sorted({n["class_type"] for n in wf.values()}),
|
||||||
|
},
|
||||||
|
indent=2,
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
"""Tests offline de comfyui_build_splash_art_workflow (estructura del dict, sin GPU)."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
from ml.comfyui_build_splash_art_workflow import ( # noqa: E402
|
||||||
|
comfyui_build_splash_art_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 _positive(wf, needle):
|
||||||
|
return next(
|
||||||
|
n
|
||||||
|
for n in wf.values()
|
||||||
|
if n["class_type"] == "CLIPTextEncode" and needle in n["inputs"]["text"]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_golden_hires_recipe():
|
||||||
|
wf = comfyui_build_splash_art_workflow(
|
||||||
|
"a lone hero before a dark castle, storm",
|
||||||
|
mood="epic, cinematic",
|
||||||
|
checkpoint="dreamshaper_8.safetensors",
|
||||||
|
hires=True,
|
||||||
|
seed=7,
|
||||||
|
)
|
||||||
|
cls = _classes(wf)
|
||||||
|
# Cadena base + 2a pasada de detalle (hires).
|
||||||
|
assert "CheckpointLoaderSimple" in cls
|
||||||
|
assert "KSampler" in cls
|
||||||
|
assert "VAEDecode" in cls
|
||||||
|
assert "UltimateSDUpscale" in cls
|
||||||
|
assert "UpscaleModelLoader" in cls
|
||||||
|
assert "SaveImage" in cls
|
||||||
|
# La scene aparece en el prompt positivo con el scaffold de key art.
|
||||||
|
pos = _positive(wf, "a lone hero before a dark castle, storm")
|
||||||
|
assert "epic, cinematic" in pos["inputs"]["text"]
|
||||||
|
assert "key art" in pos["inputs"]["text"]
|
||||||
|
assert "cinematic composition" in pos["inputs"]["text"]
|
||||||
|
assert "wide shot" in pos["inputs"]["text"]
|
||||||
|
assert "dramatic lighting" in pos["inputs"]["text"]
|
||||||
|
# Formato apaisado de pantalla: la base es width > height (16:9).
|
||||||
|
latent = _by_class(wf, "EmptyLatentImage")[0]["inputs"]
|
||||||
|
assert latent["width"] == 1024
|
||||||
|
assert latent["height"] == 576
|
||||||
|
assert latent["width"] > latent["height"]
|
||||||
|
# Regresion: UltimateSDUpscale exige batch_size (input requerido segun
|
||||||
|
# /object_info). Sin el, el submit con hires=True fallaba con node_errors.
|
||||||
|
usd = _by_class(wf, "UltimateSDUpscale")[0]["inputs"]
|
||||||
|
assert usd["batch_size"] == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_edge_no_hires_plain_txt2img():
|
||||||
|
wf = comfyui_build_splash_art_workflow(
|
||||||
|
"a spaceship fleet approaching a ringed planet",
|
||||||
|
checkpoint="dreamshaper_8.safetensors",
|
||||||
|
hires=False,
|
||||||
|
)
|
||||||
|
cls = _classes(wf)
|
||||||
|
assert "UltimateSDUpscale" not in cls
|
||||||
|
assert "UpscaleModelLoader" not in cls
|
||||||
|
# SaveImage toma del VAEDecode directamente.
|
||||||
|
vd_id = next(nid for nid, n in wf.items() if n["class_type"] == "VAEDecode")
|
||||||
|
save = next(n for n in wf.values() if n["class_type"] == "SaveImage")
|
||||||
|
assert save["inputs"]["images"] == [vd_id, 0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_edge_dims_reflected_landscape():
|
||||||
|
wf = comfyui_build_splash_art_workflow(
|
||||||
|
"an ancient forest temple", width=1152, height=640, hires=False
|
||||||
|
)
|
||||||
|
latent = _by_class(wf, "EmptyLatentImage")[0]["inputs"]
|
||||||
|
assert latent["width"] == 1152
|
||||||
|
assert latent["height"] == 640
|
||||||
|
assert latent["width"] > latent["height"] # sigue siendo apaisado 16:9
|
||||||
|
|
||||||
|
|
||||||
|
def test_edge_mood_in_prompt():
|
||||||
|
wf = comfyui_build_splash_art_workflow(
|
||||||
|
"a derelict station drifting in space", mood="dark, ominous", hires=False
|
||||||
|
)
|
||||||
|
pos = _positive(wf, "a derelict station drifting in space")
|
||||||
|
assert "dark, ominous" in pos["inputs"]["text"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_edge_lora_reflected():
|
||||||
|
wf = comfyui_build_splash_art_workflow(
|
||||||
|
"a knight raising a banner on a hill",
|
||||||
|
lora="concept_art_sd15.safetensors",
|
||||||
|
lora_strength=0.9,
|
||||||
|
hires=False,
|
||||||
|
)
|
||||||
|
loras = _by_class(wf, "LoraLoader")
|
||||||
|
assert len(loras) == 1
|
||||||
|
assert loras[0]["inputs"]["lora_name"] == "concept_art_sd15.safetensors"
|
||||||
|
assert loras[0]["inputs"]["strength_model"] == 0.9
|
||||||
|
# Con lora el KSampler.model ya NO viene del checkpoint directo.
|
||||||
|
ksampler = _by_class(wf, "KSampler")[0]
|
||||||
|
lora_id = next(nid for nid, n in wf.items() if n["class_type"] == "LoraLoader")
|
||||||
|
assert ksampler["inputs"]["model"] == [lora_id, 0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_edge_lora_strength_clamped():
|
||||||
|
wf = comfyui_build_splash_art_workflow(
|
||||||
|
"a volcano erupting at night", lora="x.safetensors", lora_strength=5.0, hires=False
|
||||||
|
)
|
||||||
|
loras = _by_class(wf, "LoraLoader")
|
||||||
|
assert loras[0]["inputs"]["strength_model"] == 2.0 # clamp a [0.0, 2.0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_edge_hires_reflected_in_upscale():
|
||||||
|
# hires=True debe encadenar UltimateSDUpscale con el upscale_by dado.
|
||||||
|
wf = comfyui_build_splash_art_workflow(
|
||||||
|
"a dragon over a burning city", hires=True, upscale_by=2.0, seed=3
|
||||||
|
)
|
||||||
|
usd = _by_class(wf, "UltimateSDUpscale")[0]["inputs"]
|
||||||
|
assert usd["upscale_by"] == 2.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_error_empty_scene():
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
comfyui_build_splash_art_workflow(" ", hires=False)
|
||||||
|
|
||||||
|
|
||||||
|
def test_determinism():
|
||||||
|
a = comfyui_build_splash_art_workflow("a castle siege at dawn", hires=False, seed=3)
|
||||||
|
b = comfyui_build_splash_art_workflow("a castle siege at dawn", hires=False, seed=3)
|
||||||
|
assert a == b
|
||||||
Reference in New Issue
Block a user