feat(gamedev): comfyui_build_world_map_workflow — mapa de mundo/nivel ilustrado (lámina cartográfica cenital fantasy, regiones rotuladas, borde ornamental, cuadrado por defecto, hires opcional)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-27 01:19:17 +02:00
parent b45165dbc5
commit 5662a54fa7
4 changed files with 492 additions and 0 deletions
+1
View File
@@ -47,6 +47,7 @@ VFX (ver `reports/0143`).
| `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. | | `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. |
| `comfyui_build_world_map_workflow_py_ml` | `(region, *, map_style="fantasy cartography, aged parchment", checkpoint="juggernaut_xl_v11…", width=768, height=768, hires=False, seed=0, lora=None, …) -> dict` | LA ilustración de la **pantalla de mapa** del juego: una lámina cartográfica en **vista cenital** de un continente/región/reino/mazmorra con aspecto de atlas fantasy (`map of {region}, {map_style}, top-down cartographic view, illustrated game world map, labeled regions, decorative border, compass rose, fantasy atlas, no people…`). **Cuadrado por defecto** (768×768; sube `width` para mundo apaisado, `height` para mazmorra en columna), `hires=False` por defecto (ponlo `True` para detalle fino de costas/relieve). Genera SOLO la ilustración — las marcas interactivas, los iconos pinchables, las rutas y el "estás aquí" los pone el motor SOBRE la lámina; la difusión dibuja labels/ornamentos **DECORATIVOS** pero NO garantiza ortografía ni posiciones usables como datos (el negativo rechaza `photo/3d render/perspective/character/person` para mantener la vista cenital plana). Atlas coherente = mismo `map_style`/`checkpoint`/`lora`, varía solo `region`. Probado e2e en GPU con SD1.5 — reino fantasy 768×768, lámina de pergamino con costas/montañas/regiones + borde ornamental + rosa de los vientos (`prompt_id bf4861fc`, `reports/0167`). SD1.5/SDXL. |
| `comfyui_build_decal_overlay_workflow_py_ml` | `(decal, *, on_black=True, style="grunge decal, high detail", checkpoint="dreamshaper_8…", size=512, seed=0, lora=None, …) -> dict` | UN decal/overlay con alpha para superponer sobre superficies/paredes/sprites con blend mode del motor (sangre, grietas, suciedad, óxido, quemaduras, salpicaduras, arañazos, musgo): textura **aislada sobre fondo PLANO** (`{decal}, {style}, single isolated decal, centered, on a solid pure black background, flat backdrop, sticker, no scenery, texture overlay, game asset…`) → txt2img cuadrado + LoRA estilo opcional. `on_black=True` (defecto) pensado para extraer alpha con **`comfyui_matting_luma_to_alpha`** (luma=alpha, conserva el falloff de translúcidos — la técnica gamedev correcta, ≠ recorte binario). **NO inyecta Rembg** (el matting es luma→alpha de disco, no un nodo): el SaveImage sale directo del VAEDecode. Set coherente = mismo `style`/`checkpoint`/`lora`, varía solo `decal`/`seed`. ⚠️ "grunge" en `style` arrastra fondo gris en SD1.5 → para fondo negro plano usar un `style` sin connotación de fondo + reroll de `seed`; luma Rec601 penaliza el rojo → para sangre roja pasar `luma_weights` con más peso al rojo. Probado e2e en GPU con SD1.5 (`reports/0160`). SD1.5. | | `comfyui_build_decal_overlay_workflow_py_ml` | `(decal, *, on_black=True, style="grunge decal, high detail", checkpoint="dreamshaper_8…", size=512, seed=0, lora=None, …) -> dict` | UN decal/overlay con alpha para superponer sobre superficies/paredes/sprites con blend mode del motor (sangre, grietas, suciedad, óxido, quemaduras, salpicaduras, arañazos, musgo): textura **aislada sobre fondo PLANO** (`{decal}, {style}, single isolated decal, centered, on a solid pure black background, flat backdrop, sticker, no scenery, texture overlay, game asset…`) → txt2img cuadrado + LoRA estilo opcional. `on_black=True` (defecto) pensado para extraer alpha con **`comfyui_matting_luma_to_alpha`** (luma=alpha, conserva el falloff de translúcidos — la técnica gamedev correcta, ≠ recorte binario). **NO inyecta Rembg** (el matting es luma→alpha de disco, no un nodo): el SaveImage sale directo del VAEDecode. Set coherente = mismo `style`/`checkpoint`/`lora`, varía solo `decal`/`seed`. ⚠️ "grunge" en `style` arrastra fondo gris en SD1.5 → para fondo negro plano usar un `style` sin connotación de fondo + reroll de `seed`; luma Rec601 penaliza el rojo → para sangre roja pasar `luma_weights` con más peso al rojo. Probado e2e en GPU con SD1.5 (`reports/0160`). SD1.5. |
| `comfyui_build_projectile_workflow_py_ml` | `(projectile, *, direction="right", glow=False, style="game projectile, side view", checkpoint="dreamshaper_8…", size=512, transparent=True, seed=0, lora=None, …) -> dict` | UN proyectil orientado (flecha, bala, bola de fuego, rayo, misil, hechizo): sprite pequeño con **orientación** (apunta a la derecha por defecto, ángulo 0 — el motor rota el sprite), aislado, listo para instanciar. **`glow` elige el camino a alpha**: `glow=False` (defecto) = proyectil SÓLIDO con silueta → `plain background` + **Rembg** (alpha por recorte, como `item_icon`/`topdown_sprite`); `glow=True` = brillante/mágico → `glowing, on black background` **sin Rembg** (recortaría el halo), insumo de **`comfyui_matting_luma_to_alpha`** que el caller aplica luego (como `vfx_spritesheet`/`decal_overlay`). `glow=True` ignora `transparent`/`rembg_model`; el negativo por defecto NO rechaza "black background". `direction` se inserta como `pointing {direction}` (`""`/None = sin orientación). Set coherente = mismo `style`/`checkpoint`/`lora`, varía solo `projectile`/`seed`. Probado e2e en GPU con SD1.5 — fireball glow sobre negro + luma→alpha RGBA (`reports/0161`). SD1.5. | | `comfyui_build_projectile_workflow_py_ml` | `(projectile, *, direction="right", glow=False, style="game projectile, side view", checkpoint="dreamshaper_8…", size=512, transparent=True, seed=0, lora=None, …) -> dict` | UN proyectil orientado (flecha, bala, bola de fuego, rayo, misil, hechizo): sprite pequeño con **orientación** (apunta a la derecha por defecto, ángulo 0 — el motor rota el sprite), aislado, listo para instanciar. **`glow` elige el camino a alpha**: `glow=False` (defecto) = proyectil SÓLIDO con silueta → `plain background` + **Rembg** (alpha por recorte, como `item_icon`/`topdown_sprite`); `glow=True` = brillante/mágico → `glowing, on black background` **sin Rembg** (recortaría el halo), insumo de **`comfyui_matting_luma_to_alpha`** que el caller aplica luego (como `vfx_spritesheet`/`decal_overlay`). `glow=True` ignora `transparent`/`rembg_model`; el negativo por defecto NO rechaza "black background". `direction` se inserta como `pointing {direction}` (`""`/None = sin orientación). Set coherente = mismo `style`/`checkpoint`/`lora`, varía solo `projectile`/`seed`. Probado e2e en GPU con SD1.5 — fireball glow sobre negro + luma→alpha RGBA (`reports/0161`). SD1.5. |
| `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_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. |
@@ -0,0 +1,122 @@
---
name: comfyui_build_world_map_workflow
kind: function
lang: py
domain: ml
version: "1.0.0"
purity: pure
signature: "def comfyui_build_world_map_workflow(region: str, *, map_style: str = \"fantasy cartography, aged parchment\", checkpoint: str = \"juggernaut_xl_v11.safetensors\", width: int = 768, height: int = 768, hires: bool = False, 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 = \"world_map\") -> dict"
description: "Construye el dict (API format) del workflow de UN mapa de mundo / mapa de nivel ilustrado 2D: lamina cartografica estilo fantasy (continente, region, mazmorra, reino) en vista cenital, con marcas/regiones rotuladas y borde ornamental, para la pantalla de mapa del juego. Genera SOLO la ilustracion (las marcas interactivas, los iconos pinchables y el texto fiable los pone el motor). Cuadrado por defecto (768x768), hires opcional. Compone comfyui_build_hires_fix_workflow (si hires) o comfyui_build_txt2img_workflow + comfyui_inject_lora (estilo opcional). Hermano de comfyui_build_splash_art/parallax_background_workflow. Pura, sin red ni I/O. class_types verificados contra /object_info (8GB lowvram)."
tags: [comfyui, ml, gamedev, gamedev-2d, world-map, level-map, cartography, 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: region
desc: "Descripcion de la region a cartografiar (ej. 'a fantasy kingdom with mountains, forests and a coastline', 'a dark dungeon with branching corridors', 'a desert continent with an oasis and ruins'). Se inserta en un prompt scaffold de mapa cartografico cenital. No puede estar vacio."
- name: map_style
desc: "Tratamiento cartografico que define el look del mapa (ej. 'fantasy cartography, aged parchment', 'hand-drawn ink map', 'old nautical chart', 'painted region map, vibrant'). Pasa el MISMO map_style + checkpoint + lora a varios mapas para coherencia visual del juego. keyword-only."
- name: checkpoint
desc: "Checkpoint del servidor. 'juggernaut_xl_v11.safetensors' (SDXL, mejor detalle de costa/relieve/ornamento) 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. 768 por defecto (cuadrado). Para un mundo apaisado (banda continental ancha) sube width. keyword-only."
- name: height
desc: "Alto del lienzo en px. 768 por defecto (cuadrado). Para un mapa en columna (torre/mazmorra vertical) sube height. keyword-only."
- name: hires
desc: "Si True encadena la 2a pasada de detalle (UltimateSDUpscale + Remacri, re-difusion por tiles) sobre la base (costas/montañas/ornamento mas finos). False (defecto) deja la imagen tal cual sale del VAEDecode: mas rapido para iterar. keyword-only."
- name: seed
desc: "Semilla del KSampler (y de la pasada hires). Misma seed + misma region -> mismo mapa. keyword-only."
- name: lora
desc: "LoRA de estilo opcional en models/loras (ej. 'cartography_sd15.safetensors', 'old_map_xl.safetensors'). None = sin LoRA. Encadena la identidad visual del mapa 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 una lamina cartografica (vista cenital plana, sin perspectiva 3D / foto / personaje en primer plano). 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 -> 768x768 pasa a 1152x1152). 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 el trazado del mapa). 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 (hires-fix si hires, txt2img si no) con prompt scaffold de mapa ('map of {region}, {map_style}, top-down cartographic view, illustrated game world map, labeled regions, decorative border, ...') + LoRA de estilo opcional. UNA lamina; atlas coherente -> llamar por region con mismo map_style/checkpoint/lora. Las marcas interactivas, los iconos pinchables y el texto fiable los pone el motor, no este workflow."
tested: true
tests: ["golden plain: clases CheckpointLoaderSimple/KSampler/VAEDecode/SaveImage; region + 'fantasy cartography, aged parchment' + 'top-down cartographic view' + 'illustrated game world map' + 'labeled regions' + 'decorative border' en prompt; base cuadrada 768x768; sin UltimateSDUpscale (hires=False default); SaveImage <- VAEDecode", "edge hires=True: encadena UltimateSDUpscale/UpscaleModelLoader con upscale_by reflejado; UltimateSDUpscale.batch_size=1", "edge dims: width=1024/height=576 reflejados (mundo apaisado)", "edge map_style en prompt", "edge lora: LoraLoader con strength, KSampler.model <- LoraLoader", "edge lora_strength clamp a [0,2]", "error region vacio -> ValueError", "determinismo"]
test_file_path: "python/functions/ml/comfyui_build_world_map_workflow_test.py"
file_path: "python/functions/ml/comfyui_build_world_map_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_world_map_workflow import comfyui_build_world_map_workflow
# Mapa de mundo ilustrado de un reino fantasy en vista cenital cartografica.
# En 8GB lowvram va holgado con SD1.5 (dreamshaper_8) sin hires; SDXL+hires es mas pesado (puede OOM).
wf = comfyui_build_world_map_workflow(
"a fantasy kingdom with mountains, forests and a coastline",
map_style="fantasy cartography, aged parchment",
checkpoint="dreamshaper_8.safetensors",
hires=False,
seed=7,
)
# Atlas coherente: mismo map_style/checkpoint para varios mapas del juego, varia solo region.
# for r in ["the northern ice provinces", "the central trade kingdoms", "the southern jungles"]:
# wf = comfyui_build_world_map_workflow(r, map_style="fantasy cartography, aged parchment",
# checkpoint="dreamshaper_8.safetensors", seed=7)
# comfyui_submit_workflow(wf) # -> comfyui_wait_result -> comfyui_fetch_output_image
# Las marcas interactivas, los iconos pinchables y el "estas aqui" los compone el motor sobre la lamina.
```
O lanzable directo con: `./fn run comfyui_build_world_map_workflow` (imprime nodos + class_types del ejemplo).
## Cuando usarla
Cuando necesites la ILUSTRACION de la pantalla de mapa del juego: una lamina
cartografica cenital de un continente, una region, un reino o una mazmorra, con el
aspecto de un atlas/pergamino fantasy (regiones rotuladas, borde ornamental, rosa de
los vientos). Pasa el MISMO `map_style` + `checkpoint` + (`lora`) a todos los mapas del
juego para que combinen visualmente; varia solo `region`. Deja `hires=False` para
iterar rapido y ponlo a `True` para una lamina con detalle fino de costas y relieve.
Las marcas de ubicacion interactivas, los iconos pinchables, las rutas y el texto
fiable los pone el motor de juego SOBRE la ilustracion — este builder NO los pinta.
## Gotchas
- **Genera SOLO la ilustracion del mapa, no marcas interactivas ni texto fiable**: la
difusion dibuja marcas/etiquetas DECORATIVAS (parte del look cartografico) pero NO
garantiza ni la ortografia ni posiciones usables como datos. Los puntos de interes
pinchables, las rutas dinamicas y el "estas aqui" los compone el motor sobre la
lamina. Si quieres labels nitidos, mejor que los ponga el motor.
- **Vista cenital plana, no perspectiva 3D**: el negativo por defecto rechaza "photo /
3d render / realistic perspective / vanishing point / first person / character /
person" para mantener la lamina cartografica. Si pasas un `negative` propio sin esos
terminos puedes acabar con una escena 3D en vez de un mapa.
- **Cuadrado por defecto (768x768)**: una pantalla de mapa suele encuadrar la region
completa. Para un mundo apaisado (banda continental ancha) sube `width`; para un mapa
en columna (torre/mazmorra vertical) sube `height`.
- **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), deja `hires=False`, o
reduce `width/height`. Probado e2e en GPU con SD1.5 sin hires (ver report 0166).
- **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 atlas = mismos parametros**: si cambias `map_style`/`checkpoint`/
`lora`/`seed` entre mapas, dejan de combinar. Fija esos y varia solo `region`.
- 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,235 @@
"""Construye el workflow ComfyUI de UN mapa de mundo / mapa de nivel ilustrado (API format).
Mapa de aventura estilo fantasy / cartografia (un continente, una region, una
mazmorra, un reino) en vista cenital cartografica, pensado para la PANTALLA DE MAPA
del juego: la ilustracion que se ve al abrir el mapa del mundo, seleccionar nivel o
consultar la region actual. Es el builder hermano de
comfyui_build_splash_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 del mapa (la lamina
cartografica). Las marcas de ubicacion interactivas, los iconos de ciudad/punto de
interes pinchables, las rutas dinamicas, el "estas aqui" y cualquier texto/label
fiable los compone el motor de juego SOBRE la ilustracion resultante — la difusion
puede dibujar marcas y letras DECORATIVAS (parte del look cartografico) pero NO
garantiza ortografia ni posiciones utilizables como datos. El prompt scaffold empuja a
"top-down cartographic view, illustrated game world map, labeled regions, decorative
border" para lograr el aspecto de mapa; el negativo por defecto rechaza la perspectiva
3D, la foto y los personajes en primer plano para mantener la vista cenital plana.
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 + 2a pasada de detalle por
tiles (UltimateSDUpscale + Remacri); el mapa luce el detalle fino de costas,
relieve y ornamentos que se aprecia al ampliar la lamina.
- comfyui_build_txt2img_workflow (si no hires) -> base txt2img simple.
- comfyui_inject_lora -> LoRA de estilo opcional (cartografia antigua / fantasy /
pergamino / tinta) para fijar la identidad visual del mapa del juego.
Por que cuadrado por defecto (768x768): una pantalla de mapa suele encuadrar una
region completa y la cartografia clasica tiende al formato cuadrado/proporcionado;
768x768 va holgado en 8GB lowvram. Para un mundo apaisado (banda continental ancha)
sube width; para un mapa en columna (torre/mazmorra vertical) sube height.
Por que hires opcional y False por defecto: un mapa se lee a escala media y el
scaffold ya da bastante detalle; hires=True anade detalle real (costas, montañas,
ornamento del borde) a costa de tiempo/VRAM. En 8GB lowvram con SDXL puede ser
pesado: baja a SD1.5 (dreamshaper_8) o deja 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 una lamina cartografica: vista cenital plana,
# ilustrada, SIN la perspectiva 3D / foto / primer plano de personajes que romperian
# el aspecto de mapa. NO rechaza "text": un mapa lleva labels/ornamentos decorativos
# como parte del look (aunque la difusion no garantice su ortografia).
_MAP_NEGATIVE = (
"photo, photograph, 3d render, realistic perspective, vanishing point, "
"first person view, close-up, character, person, people, portrait, "
"blurry, lowres, deformed, jpeg artifacts, low quality, watermark, signature"
)
def comfyui_build_world_map_workflow(
region: str,
*,
map_style: str = "fantasy cartography, aged parchment",
checkpoint: str = "juggernaut_xl_v11.safetensors",
width: int = 768,
height: int = 768,
hires: bool = False,
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 = "world_map",
) -> dict:
"""Construye el dict (API format) del workflow de un mapa de mundo/nivel ilustrado.
Args:
region: descripcion de la region a cartografiar (ej. "a fantasy kingdom with
mountains, forests and a coastline", "a dark dungeon with branching
corridors", "a desert continent with an oasis and ruins"). Se inserta en
un prompt scaffold de mapa cartografico cenital. No puede estar vacio.
map_style: tratamiento cartografico que define el look del mapa (ej. "fantasy
cartography, aged parchment", "hand-drawn ink map", "old nautical chart",
"painted region map, vibrant"). Pasa el MISMO map_style + checkpoint +
(lora) a varios mapas para coherencia visual del juego. keyword-only.
checkpoint: checkpoint del servidor. 'juggernaut_xl_v11.safetensors' (SDXL,
mejor detalle de costa/relieve/ornamento) 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. 768 por defecto (cuadrado). Para un mundo
apaisado (banda continental ancha) sube width. keyword-only.
height: alto del lienzo en px. 768 por defecto (cuadrado). Para un mapa en
columna (torre/mazmorra vertical) sube height. keyword-only.
hires: si True encadena la 2a pasada de detalle (UltimateSDUpscale + Remacri,
re-difusion por tiles) sobre la base para una lamina mas detallada
(costas, montañas, ornamento del borde). False (defecto) deja la imagen
tal cual sale del VAEDecode: mas rapido para iterar. keyword-only.
seed: semilla del KSampler (y de la pasada hires). Misma seed + misma region
-> mismo mapa. keyword-only.
lora: LoRA de estilo opcional en models/loras (ej. 'cartography_sd15.safe-
tensors', 'old_map_xl.safetensors'). None = sin LoRA. Encadena la
identidad visual del mapa 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 una
lamina cartografica (vista cenital plana, sin perspectiva 3D / foto /
personaje en primer plano). 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 ->
768x768 pasa a 1152x1152). 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 el trazado del mapa). 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 (hires-fix si
hires, txt2img si no) con prompt scaffold de mapa ('map of {region},
{map_style}, top-down cartographic view, illustrated game world map, labeled
regions, decorative border, ...') + LoRA de estilo opcional. Es UNA lamina;
un atlas coherente -> llamar por region con el mismo map_style/checkpoint/
(lora). Las marcas interactivas, los iconos pinchables y el texto fiable los
pone el motor, no este workflow.
Raises:
ValueError: si region esta vacio, o si los builders/inyectores que compone no
encuentran los nodos donde enganchar (propagado).
"""
if not region or not region.strip():
raise ValueError(
"comfyui_build_world_map_workflow: 'region' no puede estar vacio"
)
region = region.strip()
lora_strength = max(0.0, min(2.0, float(lora_strength)))
neg = _MAP_NEGATIVE if negative is None else negative
# Prompt scaffold de mapa: lamina cartografica cenital ilustrada con regiones
# rotuladas y borde ornamental. 'top-down cartographic view' fuerza la vista de
# mapa; 'no people' refuerza que es la lamina, no una escena.
positive = (
f"map of {region}, {map_style}, top-down cartographic view, "
"illustrated game world map, labeled regions, decorative border, "
"detailed terrain, compass rose, fantasy atlas, no people, 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_world_map_workflow(
"a fantasy kingdom with mountains, forests and a coastline",
map_style="fantasy cartography, aged parchment",
checkpoint="dreamshaper_8.safetensors",
hires=False,
seed=7,
)
print(
json.dumps(
{
"nodes": list(wf),
"classes": sorted({n["class_type"] for n in wf.values()}),
},
indent=2,
)
)
@@ -0,0 +1,134 @@
"""Tests offline de comfyui_build_world_map_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_world_map_workflow import ( # noqa: E402
comfyui_build_world_map_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_plain_recipe():
wf = comfyui_build_world_map_workflow(
"a fantasy kingdom with mountains, forests and a coastline",
map_style="fantasy cartography, aged parchment",
checkpoint="dreamshaper_8.safetensors",
hires=False,
seed=7,
)
cls = _classes(wf)
# Cadena base txt2img (hires=False por defecto del builder).
assert "CheckpointLoaderSimple" in cls
assert "KSampler" in cls
assert "VAEDecode" in cls
assert "SaveImage" in cls
# Sin 2a pasada cuando hires=False.
assert "UltimateSDUpscale" not in cls
assert "UpscaleModelLoader" not in cls
# La region aparece en el prompt positivo con el scaffold de mapa cenital.
pos = _positive(wf, "a fantasy kingdom with mountains, forests and a coastline")
assert "fantasy cartography, aged parchment" in pos["inputs"]["text"]
assert "top-down cartographic view" in pos["inputs"]["text"]
assert "illustrated game world map" in pos["inputs"]["text"]
assert "labeled regions" in pos["inputs"]["text"]
assert "decorative border" in pos["inputs"]["text"]
# Formato cuadrado por defecto.
latent = _by_class(wf, "EmptyLatentImage")[0]["inputs"]
assert latent["width"] == 768
assert latent["height"] == 768
# SaveImage toma del VAEDecode directamente (sin hires).
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_hires_chains_upscale():
wf = comfyui_build_world_map_workflow(
"a desert continent with an oasis and ruins",
checkpoint="dreamshaper_8.safetensors",
hires=True,
upscale_by=2.0,
seed=3,
)
cls = _classes(wf)
assert "UltimateSDUpscale" in cls
assert "UpscaleModelLoader" in cls
usd = _by_class(wf, "UltimateSDUpscale")[0]["inputs"]
assert usd["upscale_by"] == 2.0
# Regresion: UltimateSDUpscale exige batch_size (input requerido /object_info).
assert usd["batch_size"] == 1
def test_edge_dims_reflected():
# Mundo apaisado: banda continental ancha -> width > height.
wf = comfyui_build_world_map_workflow(
"a long coastal continent", width=1024, height=576, hires=False
)
latent = _by_class(wf, "EmptyLatentImage")[0]["inputs"]
assert latent["width"] == 1024
assert latent["height"] == 576
def test_edge_map_style_in_prompt():
wf = comfyui_build_world_map_workflow(
"a dark dungeon with branching corridors",
map_style="hand-drawn ink map",
hires=False,
)
pos = _positive(wf, "a dark dungeon with branching corridors")
assert "hand-drawn ink map" in pos["inputs"]["text"]
def test_edge_lora_reflected():
wf = comfyui_build_world_map_workflow(
"an archipelago of volcanic islands",
lora="cartography_sd15.safetensors",
lora_strength=0.9,
hires=False,
)
loras = _by_class(wf, "LoraLoader")
assert len(loras) == 1
assert loras[0]["inputs"]["lora_name"] == "cartography_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_world_map_workflow(
"a frozen tundra region", 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_error_empty_region():
with pytest.raises(ValueError):
comfyui_build_world_map_workflow(" ", hires=False)
def test_determinism():
a = comfyui_build_world_map_workflow("a swamp lowland map", hires=False, seed=3)
b = comfyui_build_world_map_workflow("a swamp lowland map", hires=False, seed=3)
assert a == b