feat(gamedev): comfyui_build_outpaint_asset_workflow — extender el lienzo de un asset (outpaint)
Quinto vertice del eje transform de gamedev-2d. Funcion pura (dict API format) que extiende el lienzo de un asset ya pintado por uno o varios lados y genera contenido coherente mas alla de sus bordes via el nodo nativo ImagePadForOutpaint, que ademas de ampliar el canvas EMITE la mascara feathered de la franja nueva (la genera el grafo, no la recibe el caller — esa es la diferencia con inpaint_asset). Compone comfyui_build_inpaint_workflow (base; su LoadImageMask se elimina y VAEEncodeForInpaint se reconecta a las dos salidas del pad) + comfyui_inject_lora. Probado e2e en GPU con SD1.5: seamless_00004 512x512 extendido right=256 -> 768x512 (prompt_id aa33de05), original conservado (diff 7.2) + franja nueva coherente. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -68,7 +68,7 @@ VFX (ver `reports/0143`).
|
||||
## Builders de transformación (`gamedev-2d`, puros — parten de una imagen/dibujo de entrada)
|
||||
|
||||
A diferencia de los builders de **generación** de arriba (parten de TEXTO, txt2img desde
|
||||
ruido), estos parten de una **imagen de entrada** y la transforman. Tres sub-ejes:
|
||||
ruido), estos parten de una **imagen de entrada** y la transforman. Cuatro sub-ejes:
|
||||
|
||||
- **img2img** (`asset_variant`): parte de un asset **ya pintado**; el KSampler arranca del
|
||||
latente de la imagen base (LoadImage → VAEEncode), no de ruido, así que con `denoise` medio
|
||||
@@ -81,6 +81,10 @@ ruido), estos parten de una **imagen de entrada** y la transforman. Tres sub-eje
|
||||
región editar (blanco) y cuál conservar (negro); el sampler regenera **solo** la zona enmascarada
|
||||
dejando el resto del pixel intacto. Cambia **una parte** (arma, casco, escudo, reparación), no el
|
||||
asset entero.
|
||||
- **outpaint** (`outpaint_asset`): parte de un asset **ya pintado** y **agranda el lienzo** por uno o
|
||||
varios lados; el nodo `ImagePadForOutpaint` extiende el canvas **y genera** la máscara feathered de
|
||||
la franja nueva (no la recibe el caller), y el sampler genera ahí contenido coherente. Cambia el
|
||||
**tamaño** del asset (recortar/extender un fondo o parallax a otra resolución/aspect), no lo de dentro.
|
||||
|
||||
Cubren el eje que el critic de generación (`reports/0178`) no exploró: derivar de un asset o
|
||||
del dibujo del dev, no inventar un tipo nuevo desde texto.
|
||||
@@ -90,6 +94,7 @@ del dibujo del dev, no inventar un tipo nuevo desde texto.
|
||||
| `comfyui_build_asset_variant_workflow_py_ml` | `(input_image, variant, *, checkpoint="dreamshaper_8…", denoise=0.5, style="game asset", size=512, seed=0, lora=None, …) -> dict` | UNA **variante coherente de un asset 2D ya generado** (img2img): parte del sprite/icono que existe en `input_image` y produce su versión de **otro material/paleta/tier/estado** (`ice element`, `fire element`, `battle-damaged`, `golden tier 2`, `corrupted`) manteniendo **silueta, pose y composición** del original. Compone `comfyui_build_img2img_workflow` (LoadImage → VAEEncode → KSampler con `denoise`) + `comfyui_inject_lora` (estilo opcional) + `ImageScale` opcional (`size` normaliza la base a size×size; `size=None` preserva las dimensiones exactas sin deformar). El prompt es `{variant}, {style}, same composition, same pose, same silhouette, …`. **`denoise` es la palanca**: ~0.3 invisible, **0.45-0.6 recomendado** (cambia material/paleta, conserva forma), ~0.8 deriva la pose y se acerca a txt2img. Set de variantes del MISMO asset = mismo `input_image`/`style`/`seed`, varía solo `variant`. **DISTINTO de los builders txt2img** (`enemy_creature`, `item_icon`…): esos generan un tipo desde cero; éste transforma uno concreto. **NO inyecta Rembg** (img2img preserva el fondo/alpha del original según la base). ⚠️ la imagen base debe existir en `input/` del server (subir con `POST /upload/image`); pura, no valida (usar `comfyui_validate_workflow` antes de enviar); asset NO cuadrado + `size` fijo + `crop="disabled"` deforma → `size=None` o `crop="center"`. Probado e2e en GPU con SD1.5 — variante `ice element, frozen` del goblin `enemy_creature_00001_.png` denoise 0.5 seed 7 512×512 (`prompt_id 5e4a5d3d`): silueta conservada (luminance corr 0.63) + paleta a frío (blueness B−R −1.6→+1.9), `reports/0181`. SD1.5. |
|
||||
| `comfyui_build_sprite_from_sketch_workflow_py_ml` | `(sketch_image, subject, *, control_type="lineart", checkpoint="dreamshaper_8…", style="game asset, clean, centered", strength=0.8, size=512, seed=0, lora=None, preprocess=True, controlnet_name=None, …) -> dict` | UN **sprite pintado a partir del BOCETO del dev**, guiado por **ControlNet** (sub-eje sketch→ControlNet, **NO img2img**). Recibe el dibujo tosco que existe en `sketch_image` (boceto/lineart/garabato) + `subject` (qué es), y genera un sprite en estilo de juego que **conserva la forma dibujada**: el dev marca la silueta, la IA pone material/color/acabado. Mecanismo: `txt2img` base (ruido, `EmptyLatentImage`, `denoise 1.0`) cuyo positivo pasa por `ControlNetApply` atado al mapa de líneas del boceto. `control_type` elige el **preprocesador** (`LineArtPreprocessor` / `ScribblePreprocessor` / `CannyEdgePreprocessor`, interpuesto entre el boceto y el ControlNet por un helper) y, por defecto, el **modelo CN emparejado**. Compone `comfyui_build_txt2img_workflow` + `comfyui_inject_controlnet` + `comfyui_inject_lora` (estilo opcional). **`strength` es la palanca**: 0 = ignora el dibujo (txt2img puro), ~0.8 recomendado (respeta forma dejando limpiar a la IA), 1.0 = se ciñe estricto. **DISTINTO de `asset_variant`** (img2img conserva forma+color de una imagen ya pintada) y de los txt2img (`enemy_creature`…, inventan la forma desde texto): éste conserva **solo la forma** del dibujo. ⚠️ el boceto debe existir en `input/` (subir con `POST /upload/image`); pura, no valida (usar `comfyui_validate_workflow` antes de enviar); `preprocess=False` solo si el sketch ya es un lineart limpio. **GOTCHA del server 8GB: solo `canny`/`depth`/`openpose` SD1.5 instalados** — para `lineart`/`scribble` pasa `controlnet_name="control_v11p_sd15_canny_fp16.safetensors"` u usa `control_type="canny"` (out-of-the-box); pendiente humano descargar `control_v11p_sd15_lineart_fp16`/`scribble`. Probado e2e en GPU con SD1.5 — boceto del goblin `enemy_creature_00001_.png` → `CannyEdgePreprocessor` → ControlNet canny, `subject="dark fantasy goblin warrior"` strength 0.85 seed 123 512×512 (`prompt_id ea6fc372`): pose/orejas/hombrera/lanza dentada/espada del dibujo conservadas, repintado en estilo de juego, `reports/0182`. SD1.5. |
|
||||
| `comfyui_build_inpaint_asset_workflow_py_ml` | `(input_image, mask_image, prompt, *, checkpoint="dreamshaper_8…", denoise=1.0, style="game asset", grow_mask=6, size=None, seed=0, lora=None, mode="vae_encode", …) -> dict` | EDITA **solo una región** de un asset 2D ya pintado (**inpaint**, sub-eje propio). Recibe el asset en `input_image` + una **máscara** `mask_image` (BLANCO = editar, NEGRO = conservar) + `prompt` de qué poner ahí, y repinta **únicamente** la zona enmascarada dejando el resto del sprite intacto (cambiar/añadir un arma, quitar un casco, poner un escudo, reparar una zona dañada). Mecanismo (`mode="vae_encode"`): `VAEEncodeForInpaint` codifica el latente respetando la máscara y dilata su borde `grow_mask` px para difuminar la costura; `KSampler` (`denoise` alto) regenera solo esa región con `{prompt}, {style}, seamless blend…`. Compone `comfyui_build_inpaint_workflow` (base) + `comfyui_inject_lora` (estilo opcional); `size` escala imagen **Y** máscara de forma consistente (escalar solo una las desalinea). **`grow_mask` es la palanca de costura** (6-10 px difumina el borde lo/nuevo); `denoise` 1.0 reescribe entero, ~0.5-0.7 repara suave. **DISTINTO de `asset_variant`** (img2img reescribe TODO el asset) y de `sprite_from_sketch` (ControlNet parte de un dibujo de líneas para un sprite nuevo): éste edita **un trozo** delimitado por la máscara. **ERROR-PATH**: si el server no expone `VAEEncodeForInpaint`, pasar `mode="noise_mask"` → degrada a `VAEEncode` + `SetLatentNoiseMask` (+ `GrowMask`); `mask_image` vacío lanza `ValueError`. ⚠️ asset y máscara deben existir en `input/` (subir con `POST /upload/image`) y compartir resolución (o usar `size`); `ImageScale` aquí NO ofrece `lanczos` (válidos `bilinear`/`nearest-exact`/`area`/`bicubic`); pura, no valida. Probado e2e en GPU con SD1.5 — máscara circular (R70) sobre la mano del goblin `enemy_creature_00001_.png`, `prompt="a glowing blue magic orb"` grow_mask 8 denoise 1.0 seed 7 (`prompt_id 88b52c66`): orbe azul en la región, **resto idéntico** (diff medio dentro 40.3 vs fuera 1.97 → ratio 20.4×; 44.6% px cambiados dentro vs 1.7% fuera), `reports/0183`. SD1.5. |
|
||||
| `comfyui_build_outpaint_asset_workflow_py_ml` | `(input_image, prompt, *, left=0, right=0, top=0, bottom=0, feather=40, checkpoint="dreamshaper_8…", denoise=1.0, style="game background", grow_mask=0, seed=0, lora=None, …) -> dict` | EXTIENDE **el lienzo** de un asset 2D ya pintado (**outpaint**, sub-eje propio). Recibe el asset en `input_image` + cuánto extender por cada lado (`left`/`right`/`top`/`bottom` px) + `prompt` de qué generar fuera de los bordes, y **agranda el canvas** generando contenido coherente con el original más allá de sus bordes (recortar/extender un fondo, parallax, card_art o splash a otra resolución/aspect ratio). Mecanismo: el nodo nativo `ImagePadForOutpaint` amplía el lienzo y **EMITE** a la vez la imagen extendida **y** la máscara feathered de la franja nueva (la genera el grafo, **NO** la recibe el caller); `VAEEncodeForInpaint` codifica respetando esa máscara y `KSampler` (`denoise` alto) genera lo nuevo con `{prompt}, {style}, seamless extension…`. Compone `comfyui_build_inpaint_workflow` (base; su `LoadImageMask` se elimina y `VAEEncodeForInpaint` se reconecta a las dos salidas del pad) + `comfyui_inject_lora` (estilo opcional). **`feather` difumina la costura** (40 px por defecto, no debe pasarse de la extensión); `grow_mask` (0 por defecto) dilata adicionalmente el borde si aparece costura dura. **DISTINTO de `inpaint_asset`**: éste **no recibe máscara** (la genera el pad) y cambia el **tamaño** del asset extendiendo hacia fuera, mientras inpaint edita una región **interior** con máscara externa del mismo tamaño. **ERROR-PATH**: `input_image`/`prompt` vacíos o las cuatro extensiones en 0 tras redondear (`left=3`→0) lanzan `ValueError`; si el server no expone `ImagePadForOutpaint`, consultar `/object_info`. ⚠️ el asset debe existir en `input/` (subir con `POST /upload/image`); las extensiones se redondean a múltiplo de 8 (`250→248`); pura, no valida. Probado e2e en GPU con SD1.5 — fondo `seamless_00004_.png` 512×512 extendido `right=256` feather 40 denoise 1.0 seed 7 (`prompt_id aa33de05`): canvas **512→768×512** (+256), original conservado (diff medio 7.2 lejos del borde) + franja nueva con contenido coherente (std 28.9, dist de paleta 28.6), `reports/0185`. SD1.5. |
|
||||
|
||||
## Funciones de post-proceso y puente (`gamedev-2d`, CPU)
|
||||
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
---
|
||||
name: comfyui_build_outpaint_asset_workflow
|
||||
kind: function
|
||||
lang: py
|
||||
domain: ml
|
||||
purity: pure
|
||||
version: 1.0.0
|
||||
signature: "def comfyui_build_outpaint_asset_workflow(input_image: str, prompt: str, *, left: int = 0, right: int = 0, top: int = 0, bottom: int = 0, feather: int = 40, checkpoint: str = \"dreamshaper_8.safetensors\", denoise: float = 1.0, style: str = \"game background\", grow_mask: int = 0, 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 = \"outpaint_asset\") -> dict"
|
||||
description: "Construye el dict (API format) del workflow que EXTIENDE EL LIENZO de un asset 2D ya pintado (outpaint): recibe el asset + cuanto extender por cada lado (left/right/top/bottom px) + un prompt de que generar en la franja nueva, y agranda el canvas generando contenido coherente con el original mas alla de sus bordes. Es el quinto vertice del eje transform de gamedev-2d: distinto de txt2img (enemy_creature/item_icon, inventan la forma desde texto), de img2img (asset_variant, reescribe TODO el asset), de ControlNet (sprite_from_sketch, pinta un sprite desde un boceto) y de inpaint (inpaint_asset, edita una region INTERIOR con una mascara externa del mismo tamano). Mecanismo: el nodo nativo ImagePadForOutpaint amplia el lienzo left/top/right/bottom px, rellena la franja y EMITE la mascara feathered de la zona nueva (la genera el grafo, NO la recibe el caller); VAEEncodeForInpaint codifica respetando esa mascara y el KSampler (denoise alto) genera lo nuevo con '{prompt}, {style}, seamless extension'. feather difumina la costura entre lo viejo y lo nuevo; grow_mask dilata adicionalmente el borde si aparece costura. Compone comfyui_build_inpaint_workflow (base; su LoadImageMask se elimina) + comfyui_inject_lora (estilo opcional). Pura, sin red ni I/O. class_types verificados contra /object_info (8GB lowvram). Probado e2e en GPU con SD1.5: seamless 512x512 extendido right=256 -> 768x512, original conservado + franja nueva coherente."
|
||||
tags: [comfyui, ml, gamedev-2d, outpaint, asset-transform, canvas-extend, stable-diffusion, workflow]
|
||||
uses_functions: [comfyui_build_inpaint_workflow_py_ml, comfyui_inject_lora_py_ml]
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: ""
|
||||
params:
|
||||
- name: input_image
|
||||
desc: "Nombre del archivo del asset a extender dentro de la carpeta input/ del servidor ComfyUI (un fondo, parallax, card_art, splash). Lo carga el nodo LoadImage. Subelo antes con POST /upload/image o copialo a ~/ComfyUI/input/. No puede estar vacio."
|
||||
- name: prompt
|
||||
desc: "Que generar en la franja ampliada ('more forest and trees', 'open sky and clouds', 'continuing stone wall'). Describe la CONTINUACION de la escena hacia los lados extendidos, no un objeto centrado. No puede estar vacio."
|
||||
- name: left
|
||||
desc: "Pixeles a extender por la izquierda. Se redondea al multiplo de 8 mas cercano (step del nodo + SD trabaja en latentes de 8 px) y se clampa a [0, 16384]. keyword-only."
|
||||
- name: right
|
||||
desc: "Pixeles a extender por la derecha (mismo tratamiento que left). keyword-only."
|
||||
- name: top
|
||||
desc: "Pixeles a extender por arriba (mismo tratamiento que left). keyword-only."
|
||||
- name: bottom
|
||||
desc: "Pixeles a extender por abajo (mismo tratamiento que left). Al menos uno de left/right/top/bottom debe ser > 0 tras redondear: sin extension no hay outpaint (para editar una region INTERIOR usa comfyui_build_inpaint_asset_workflow). keyword-only."
|
||||
- name: feather
|
||||
desc: "Pixeles de difuminado del borde entre el asset original y la franja nueva (input 'feathering' del nodo ImagePadForOutpaint). Default 40. Mas alto = transicion mas gradual pero invade mas la imagen original; no debe superar mucho la extension del lado mas pequeno. Se clampa a [0, 16384]. keyword-only."
|
||||
- name: checkpoint
|
||||
desc: "Checkpoint del servidor. 'dreamshaper_8.safetensors' (SD1.5, holgado en 8GB lowvram) por defecto. keyword-only."
|
||||
- name: denoise
|
||||
desc: "Fuerza de denoising del KSampler dentro de la mascara (la franja nueva). En outpaint suele ser 1.0 (por defecto): la zona ampliada parte de relleno y se genera por completo. Se clampa a [0.0, 1.0]. keyword-only."
|
||||
- name: style
|
||||
desc: "Descriptor de estilo que mantiene la franja coherente con el asset y el set ('game background', 'pixel art landscape', 'dark fantasy scenery'). Mismo style + checkpoint + (lora) que el resto del set. keyword-only."
|
||||
- name: grow_mask
|
||||
desc: "Pixeles que se dilata ADEMAS el borde de la mascara hacia dentro de la imagen original (sobre el feathering del pad) para reforzar el blend si aparece costura. Default 0 (el feathering del pad suele bastar). Se clampa a [0, 64] (limite de VAEEncodeForInpaint). keyword-only."
|
||||
- name: seed
|
||||
desc: "Semilla del KSampler. keyword-only."
|
||||
- name: lora
|
||||
desc: "LoRA de estilo opcional en models/loras (ej. 'dark_fantasy_sd15.safetensors'). 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 extension de escena (continuacion sin costura, sin repetir/espejar el original, un solo sujeto). keyword-only."
|
||||
- name: steps
|
||||
desc: "Pasos de sampling del KSampler. keyword-only."
|
||||
- name: cfg
|
||||
desc: "Classifier-free guidance scale. keyword-only."
|
||||
- name: sampler_name
|
||||
desc: "Nombre del sampler (ej. 'dpmpp_2m', 'euler'). keyword-only."
|
||||
- name: scheduler
|
||||
desc: "Scheduler del sampler (ej. 'karras', 'normal'). keyword-only."
|
||||
- name: filename_prefix
|
||||
desc: "Prefijo del archivo de salida en SaveImage. keyword-only."
|
||||
output: "dict en API format listo para comfyui_submit_workflow: outpaint que extiende el lienzo por los lados pedidos y genera lo nuevo con '{prompt}, {style}, seamless extension...', conservando el asset original. Nodos: CheckpointLoaderSimple '4', LoadImage '10', ImagePadForOutpaint (id nuevo, reusa el '12' que libera el LoadImageMask eliminado), VAEEncodeForInpaint '11' (pixels <- pad IMAGE, mask <- pad MASK), CLIPTextEncode '6'/'7', KSampler '3', VAEDecode '8', SaveImage '9' (+ LoraLoader si lora). El LoadImageMask de la base inpaint se elimina: la mascara la GENERA el pad."
|
||||
tested: false
|
||||
file_path: python/functions/ml/comfyui_build_outpaint_asset_workflow.py
|
||||
---
|
||||
|
||||
Construye el dict (API format) del workflow que **extiende el lienzo** de un asset 2D ya
|
||||
pintado (**outpaint**). Quinto vértice del eje **transform** de `gamedev-2d`, junto a
|
||||
`asset_variant` (img2img: reescribe todo), `sprite_from_sketch` (ControlNet: pinta desde un
|
||||
boceto) e `inpaint_asset` (edita una región **interior** con máscara externa). Aquí el dev
|
||||
tiene un asset terminado y necesita **más canvas del que tiene** —recortar/extender un
|
||||
fondo o parallax a otra resolución o aspect ratio, ampliar un card_art o un splash más allá
|
||||
de sus bordes, completar la escena hacia un lado— sin regenerarla entera. El lienzo se
|
||||
agranda por los lados pedidos y se genera contenido **nuevo coherente** con el original en
|
||||
esa franja.
|
||||
|
||||
La clave frente a `inpaint_asset`: **la máscara no la aporta el caller**. El nodo nativo
|
||||
`ImagePadForOutpaint` amplía el lienzo y emite a la vez la imagen extendida **y** la máscara
|
||||
feathered de la zona nueva (blanco = generar, negro = conservar). Por eso `inpaint_asset`
|
||||
recibe `mask_image` y `outpaint_asset` no: la genera 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_outpaint_asset_workflow import comfyui_build_outpaint_asset_workflow
|
||||
from ml.comfyui_submit_workflow import comfyui_submit_workflow
|
||||
from ml.comfyui_wait_result import comfyui_wait_result
|
||||
|
||||
# Extender un fondo 512x512 -> 768x512 generando más escena por la derecha.
|
||||
# (input_image debe estar ya subido al input/ del server; POST /upload/image)
|
||||
wf = comfyui_build_outpaint_asset_workflow(
|
||||
"seamless_00004_.png", # el fondo (512x512, ya en ~/ComfyUI/input/)
|
||||
"more forest floor, grass and foliage extending to the right", # qué generar en lo nuevo
|
||||
right=256, # +256 px por la derecha (múltiplo de 8)
|
||||
feather=40, # difumina la costura 40 px
|
||||
style="game background, top-down 2d, seamless tile",
|
||||
denoise=1.0, # la franja nueva se genera por completo
|
||||
seed=7,
|
||||
)
|
||||
resp = comfyui_submit_workflow(wf, server="127.0.0.1:8188")
|
||||
out = comfyui_wait_result(resp["prompt_id"], server="127.0.0.1:8188")
|
||||
# La imagen sale en ~/ComfyUI/output/outpaint_asset_*.png: 768x512, el original intacto
|
||||
# a la izquierda y la franja nueva coherente a la derecha.
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Úsala cuando tengas un asset 2D **ya pintado** y necesites **más lienzo** del que tiene:
|
||||
adaptar un fondo/parallax a otra resolución o aspect ratio, ampliar un card_art o splash
|
||||
más allá de sus bordes, o completar la escena hacia uno o varios lados. Elige entre los
|
||||
hermanos del eje transform así:
|
||||
|
||||
- **más canvas, generar lo de fuera de los bordes** → este builder (outpaint).
|
||||
- **una región interior, el resto intacto, con máscara propia** → `inpaint_asset`.
|
||||
- **todo el asset, mismo diseño/pose, otro material/tier** → `asset_variant` (img2img).
|
||||
- **un sprite nuevo cuya silueta marca un boceto** → `sprite_from_sketch` (ControlNet).
|
||||
- **un asset de cero desde texto** → `enemy_creature` / `item_icon` (txt2img).
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **No recibe máscara: la genera el grafo.** A diferencia de `inpaint_asset`, NO se pasa
|
||||
`mask_image`. `ImagePadForOutpaint` extiende el lienzo y emite la máscara feathered de la
|
||||
zona nueva. Eso es lo que diferencia outpaint de inpaint.
|
||||
- **Al menos un lado > 0.** Si las cuatro extensiones quedan en 0 tras redondear (p. ej.
|
||||
pasar `left=3` → 0), lanza `ValueError`: sin extensión no hay outpaint. Para editar una
|
||||
región **interior** usa `inpaint_asset`.
|
||||
- **Extensiones a múltiplo de 8.** `left/right/top/bottom` se redondean al múltiplo de 8 más
|
||||
cercano (el nodo declara `step: 8` y SD trabaja en latentes de 8 px). `250 → 248`,
|
||||
`60 → 64`. Pásalos ya redondeados si quieres dimensiones exactas.
|
||||
- **`feather` no debe pasarse de la extensión.** Un `feathering` mayor que la franja añadida
|
||||
invade la imagen original y emborrona parte de lo que querías conservar. Mantenlo por
|
||||
debajo del lado más pequeño que extiendes. Default 40 (el del nodo).
|
||||
- **`grow_mask` es refuerzo opcional.** Por defecto 0: el feathering del pad ya difumina la
|
||||
costura. Súbelo (6–10) solo si aparece una línea dura entre lo viejo y lo nuevo; se clampa
|
||||
a `[0, 64]`.
|
||||
- **`denoise` alto por defecto (1.0).** La franja nueva parte de relleno y se genera entera;
|
||||
bajarlo no tiene el mismo sentido que en inpaint (no hay píxeles originales útiles bajo la
|
||||
zona ampliada salvo el relleno del pad).
|
||||
- **Función pura.** No sube ni valida que `input_image`/`checkpoint`/`lora` existan en el
|
||||
servidor: súbelos antes (`POST /upload/image`) y valida con `comfyui_validate_workflow` si
|
||||
quieres atrapar nombres inexistentes antes de enviar.
|
||||
@@ -0,0 +1,336 @@
|
||||
"""Construye el workflow ComfyUI que EXTIENDE EL LIENZO de un asset (outpaint).
|
||||
|
||||
Es el quinto vertice del eje `transform` del catalogo gamedev-2d. Los otros cuatro
|
||||
parten de:
|
||||
|
||||
- txt2img (enemy_creature, item_icon): inventan la forma desde texto en blanco.
|
||||
- img2img (asset_variant): reescriben TODO el asset conservando silueta/pose.
|
||||
- ControlNet (sprite_from_sketch): pintan un sprite desde la silueta de un boceto.
|
||||
- inpaint (inpaint_asset): repintan SOLO una region marcada por una mascara EXTERNA
|
||||
del mismo tamano que el asset, conservando el resto.
|
||||
|
||||
Este builder cubre el dolor que ninguno resuelve: tienes un asset terminado y necesitas
|
||||
MAS LIENZO del que tiene -- extender un fondo/parallax a otra resolucion o aspect ratio,
|
||||
ampliar un card_art o un splash mas alla de sus bordes, completar la escena hacia un lado
|
||||
sin regenerarla entera. Eso es outpaint: se agranda el canvas por uno o varios lados y se
|
||||
genera contenido NUEVO coherente con el original en esa zona ampliada.
|
||||
|
||||
Diferencia clave con inpaint (su hermano mas cercano):
|
||||
|
||||
- inpaint_asset RECIBE una mascara externa que el caller tiene que pintar, del MISMO
|
||||
tamano que el asset, y edita una region INTERIOR; las dimensiones del asset no cambian.
|
||||
- outpaint_asset NO recibe mascara: el lienzo se agranda y la mascara de "lo nuevo"
|
||||
(la franja anadida) la GENERA el grafo con el nodo nativo `ImagePadForOutpaint`, que
|
||||
a la vez extiende la imagen (relleno) y emite la MASK feathered de la zona ampliada.
|
||||
Las dimensiones del asset CRECEN por los lados extendidos.
|
||||
|
||||
Mecanismo:
|
||||
|
||||
CheckpointLoaderSimple -> [LoraLoader opcional de estilo] -> KSampler
|
||||
LoadImage(asset) -> ImagePadForOutpaint(left/top/right/bottom/feathering) -> (IMAGE, MASK)
|
||||
ImagePadForOutpaint.IMAGE -> VAEEncodeForInpaint.pixels
|
||||
ImagePadForOutpaint.MASK -> VAEEncodeForInpaint.mask
|
||||
CLIPTextEncode(prompt de lo nuevo + estilo + "seamless extension") -> KSampler.positive
|
||||
KSampler(denoise alto) -> VAEDecode -> SaveImage
|
||||
|
||||
`ImagePadForOutpaint` (nodo nativo de ComfyUI) hace lo que en inpaint hacian LoadImage +
|
||||
LoadImageMask juntos: amplia el lienzo `left/top/right/bottom` pixeles, rellena la franja
|
||||
nueva, y emite una MASK donde la zona ampliada es blanca (a regenerar) y la original
|
||||
negra (a conservar), con un borde difuminado de `feathering` pixeles para que la costura
|
||||
entre lo viejo y lo nuevo quede suave. `VAEEncodeForInpaint` codifica ese latente
|
||||
respetando la MASK; el KSampler (denoise alto, 1.0) genera lo nuevo solo en la franja.
|
||||
|
||||
`grow_mask` dilata adicionalmente el borde de la mascara hacia DENTRO de la imagen
|
||||
original (sobre el feathering del pad) para reforzar el blend si aparece costura; por
|
||||
defecto 0 porque el feathering del propio pad ya suele bastar.
|
||||
|
||||
Compone:
|
||||
- comfyui_build_inpaint_workflow -> base inpaint (Checkpoint/LoadImage/VAEEncodeForInpaint/
|
||||
KSampler/VAEDecode/SaveImage). El LoadImageMask de la base NO se usa: outpaint genera
|
||||
su propia mascara con ImagePadForOutpaint, asi que ese nodo se elimina y se reconecta
|
||||
VAEEncodeForInpaint a las dos salidas del pad.
|
||||
- comfyui_inject_lora -> LoRA de estilo opcional (coherencia con el set).
|
||||
|
||||
El unico codigo propio es: el prompt gamedev (extension de escena + estilo + mezcla
|
||||
limpia), la insercion del ImagePadForOutpaint entre LoadImage y VAEEncodeForInpaint, el
|
||||
redondeo de las extensiones al multiplo de 8 que exige el nodo (y SD), y el repunte de
|
||||
grow_mask_by / filename_prefix.
|
||||
|
||||
class_types/inputs verificados contra /object_info del servidor (8GB lowvram):
|
||||
CheckpointLoaderSimple, LoadImage, ImagePadForOutpaint (image, left/top/right/bottom INT
|
||||
step 8, feathering INT default 40; outputs IMAGE+MASK), VAEEncodeForInpaint, CLIPTextEncode,
|
||||
KSampler, VAEDecode, SaveImage, LoraLoader.
|
||||
|
||||
Funcion pura: sin red, sin I/O. No muta dicts de entrada (construye desde cero via la base).
|
||||
NO valida que input_image/checkpoint/lora existan en el servidor (eso es responsabilidad
|
||||
del caller / comfyui_validate_workflow antes de enviar). 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 EXTENSION de lienzo: la franja nueva debe continuar la
|
||||
# escena sin costura, sin repetir/espejar el contenido original ni meter un segundo sujeto,
|
||||
# texto o marcas. NO restringe material/color (el prompt manda en la zona nueva).
|
||||
_OUTPAINT_ASSET_NEGATIVE = (
|
||||
"visible seam, hard border, abrupt edge, halo, duplicate, repeated pattern, "
|
||||
"mirrored, tiling artifact, second subject, blurry, lowres, deformed, "
|
||||
"text, watermark, signature, logo, jpeg artifacts"
|
||||
)
|
||||
|
||||
|
||||
def _new_id(wf: dict) -> str:
|
||||
"""Devuelve un node_id numerico libre (max id existente + 1)."""
|
||||
numeric = [int(k) for k in wf.keys() if str(k).isdigit()]
|
||||
return str((max(numeric) + 1) if numeric else len(wf) + 1)
|
||||
|
||||
|
||||
def _find_class(wf: dict, class_type: str) -> str | None:
|
||||
"""Primer node_id cuyo class_type coincide exactamente, o None."""
|
||||
return next(
|
||||
(nid for nid, n in wf.items() if n.get("class_type") == class_type), None
|
||||
)
|
||||
|
||||
|
||||
def _round8(px: int) -> int:
|
||||
"""Redondea al multiplo de 8 mas cercano (>= 0).
|
||||
|
||||
`ImagePadForOutpaint` declara `step: 8` para left/top/right/bottom y SD trabaja sobre
|
||||
latentes de 8 px, asi que extender un numero no multiplo de 8 produce dimensiones que
|
||||
el VAE tendria que recortar. Se normaliza aqui para que las dims finales sean limpias.
|
||||
"""
|
||||
px = max(0, int(px))
|
||||
return ((px + 4) // 8) * 8
|
||||
|
||||
|
||||
def comfyui_build_outpaint_asset_workflow(
|
||||
input_image: str,
|
||||
prompt: str,
|
||||
*,
|
||||
left: int = 0,
|
||||
right: int = 0,
|
||||
top: int = 0,
|
||||
bottom: int = 0,
|
||||
feather: int = 40,
|
||||
checkpoint: str = "dreamshaper_8.safetensors",
|
||||
denoise: float = 1.0,
|
||||
style: str = "game background",
|
||||
grow_mask: int = 0,
|
||||
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 = "outpaint_asset",
|
||||
) -> dict:
|
||||
"""Construye el dict (API format) de un outpaint que EXTIENDE el lienzo de un asset.
|
||||
|
||||
Agranda el canvas del asset `left/top/right/bottom` pixeles por lado y genera el
|
||||
contenido nuevo de esa franja con `prompt`, conservando el asset original en su sitio.
|
||||
La mascara de "lo nuevo" la genera el grafo (`ImagePadForOutpaint`), no el caller: por
|
||||
eso, a diferencia de `comfyui_build_inpaint_asset_workflow`, NO se pasa `mask_image`.
|
||||
|
||||
Args:
|
||||
input_image: nombre del archivo del asset a extender dentro de la carpeta input/
|
||||
del servidor ComfyUI (un fondo, parallax, card_art, splash...). Lo carga
|
||||
LoadImage. Subelo antes con POST /upload/image o copialo a ~/ComfyUI/input/.
|
||||
No puede estar vacio.
|
||||
prompt: que generar en la franja ampliada ("more forest and trees", "open sky and
|
||||
clouds", "continuing stone wall"). Describe la CONTINUACION de la escena, no un
|
||||
objeto centrado. No puede estar vacio.
|
||||
left: pixeles a extender por la izquierda. Se redondea al multiplo de 8 mas cercano
|
||||
y se clampa a [0, 16384]. keyword-only.
|
||||
right: pixeles a extender por la derecha (mismo tratamiento). keyword-only.
|
||||
top: pixeles a extender por arriba (mismo tratamiento). keyword-only.
|
||||
bottom: pixeles a extender por abajo (mismo tratamiento). keyword-only.
|
||||
Al menos uno de left/right/top/bottom debe ser > 0 tras redondear: sin
|
||||
extension no hay outpaint (para editar una region INTERIOR usa inpaint_asset).
|
||||
feather: pixeles de difuminado del borde entre el asset original y la franja nueva
|
||||
(`feathering` del nodo). Default 40 (el del nodo). Mas alto = transicion mas
|
||||
gradual pero invade mas la imagen original; no debe superar mucho la extension
|
||||
del lado mas pequeno. Se clampa a [0, 16384]. keyword-only.
|
||||
checkpoint: checkpoint del servidor. 'dreamshaper_8.safetensors' (SD1.5, holgado en
|
||||
8GB lowvram) por defecto. keyword-only.
|
||||
denoise: fuerza de denoising del KSampler dentro de la mascara (la franja nueva).
|
||||
En outpaint suele ser 1.0 (por defecto): la zona ampliada parte de relleno y se
|
||||
genera por completo. Se clampa a [0.0, 1.0]. keyword-only.
|
||||
style: descriptor de estilo que mantiene la franja coherente con el asset y el set
|
||||
("game background", "pixel art landscape", "dark fantasy scenery"). Pasa el
|
||||
MISMO style + checkpoint + (lora) que el resto del set. keyword-only.
|
||||
grow_mask: pixeles que se dilata ADEMAS el borde de la mascara hacia dentro de la
|
||||
imagen original (sobre el feathering del pad) para reforzar el blend si aparece
|
||||
costura. Default 0 (el feathering del pad suele bastar). Se clampa a [0, 64]
|
||||
(limite de VAEEncodeForInpaint). keyword-only.
|
||||
seed: semilla del KSampler. keyword-only.
|
||||
lora: LoRA de estilo opcional en models/loras (ej. 'dark_fantasy_sd15.safetensors').
|
||||
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 extension
|
||||
de escena (continuacion sin costura, sin repetir/espejar el original, un solo
|
||||
sujeto). keyword-only.
|
||||
steps, cfg, sampler_name, scheduler, filename_prefix: parametros de generacion.
|
||||
keyword-only.
|
||||
|
||||
Returns:
|
||||
dict en API format listo para comfyui_submit_workflow: outpaint que extiende el
|
||||
lienzo por los lados pedidos y genera lo nuevo con '{prompt}, {style}, seamless
|
||||
extension...', conservando el asset original. Nodos: CheckpointLoaderSimple '4',
|
||||
LoadImage '10', ImagePadForOutpaint (id nuevo), VAEEncodeForInpaint '11',
|
||||
CLIPTextEncode '6'/'7', KSampler '3', VAEDecode '8', SaveImage '9' (+ LoraLoader si
|
||||
lora). El LoadImageMask de la base inpaint se elimina: la mascara la genera el pad.
|
||||
|
||||
Raises:
|
||||
ValueError: si input_image o prompt estan vacios; si las cuatro extensiones quedan
|
||||
en 0 tras redondear (no hay nada que extender); o si la base no tiene los nodos
|
||||
esperados (propagado).
|
||||
"""
|
||||
from ml.comfyui_build_inpaint_workflow import comfyui_build_inpaint_workflow
|
||||
|
||||
if not input_image or not input_image.strip():
|
||||
raise ValueError(
|
||||
"comfyui_build_outpaint_asset_workflow: 'input_image' no puede estar vacio"
|
||||
)
|
||||
if not prompt or not prompt.strip():
|
||||
raise ValueError(
|
||||
"comfyui_build_outpaint_asset_workflow: 'prompt' no puede estar vacio"
|
||||
)
|
||||
|
||||
input_image = input_image.strip()
|
||||
prompt = prompt.strip()
|
||||
|
||||
left = min(16384, _round8(left))
|
||||
right = min(16384, _round8(right))
|
||||
top = min(16384, _round8(top))
|
||||
bottom = min(16384, _round8(bottom))
|
||||
if (left + right + top + bottom) == 0:
|
||||
raise ValueError(
|
||||
"comfyui_build_outpaint_asset_workflow: al menos uno de left/right/top/bottom "
|
||||
"debe ser > 0 (multiplo de 8) para extender el lienzo; sin extension no es "
|
||||
"outpaint. Para editar una region INTERIOR usa "
|
||||
"comfyui_build_inpaint_asset_workflow."
|
||||
)
|
||||
|
||||
feather = max(0, min(16384, int(feather)))
|
||||
denoise = max(0.0, min(1.0, float(denoise)))
|
||||
grow_mask = max(0, min(64, int(grow_mask)))
|
||||
lora_strength = max(0.0, min(2.0, float(lora_strength)))
|
||||
neg = _OUTPAINT_ASSET_NEGATIVE if negative is None else negative
|
||||
|
||||
# Prompt de la franja nueva: pide CONTINUAR la escena (no un objeto centrado) y fundirla
|
||||
# con el original. A diferencia de inpaint_asset no se habla de "esta region" sino de
|
||||
# extender naturalmente hacia los lados ampliados.
|
||||
positive = (
|
||||
f"{prompt}, {style}, seamless extension of the scene, "
|
||||
"natural continuation, consistent lighting, matching art style, high detail"
|
||||
)
|
||||
|
||||
# La base inpaint exige un nombre de mascara; usamos un placeholder porque el
|
||||
# LoadImageMask se elimina justo despues (outpaint genera su mascara con el pad).
|
||||
wf = comfyui_build_inpaint_workflow(
|
||||
checkpoint,
|
||||
input_image,
|
||||
"__outpaint_pad_placeholder__",
|
||||
positive,
|
||||
neg,
|
||||
denoise=denoise,
|
||||
steps=steps,
|
||||
cfg=cfg,
|
||||
seed=seed,
|
||||
sampler_name=sampler_name,
|
||||
scheduler=scheduler,
|
||||
)
|
||||
|
||||
load_id = _find_class(wf, "LoadImage")
|
||||
enc_id = _find_class(wf, "VAEEncodeForInpaint")
|
||||
mask_id = _find_class(wf, "LoadImageMask")
|
||||
if load_id is None or enc_id is None:
|
||||
raise ValueError(
|
||||
"comfyui_build_outpaint_asset_workflow: la base inpaint no expone "
|
||||
"LoadImage/VAEEncodeForInpaint; no se puede armar el outpaint"
|
||||
)
|
||||
|
||||
# Eliminar el LoadImageMask de la base: la mascara la genera ImagePadForOutpaint.
|
||||
if mask_id is not None:
|
||||
del wf[mask_id]
|
||||
|
||||
# Insertar ImagePadForOutpaint entre LoadImage y VAEEncodeForInpaint. El pad extiende
|
||||
# el lienzo y emite (IMAGE extendida [salida 0], MASK de la franja nueva [salida 1]).
|
||||
pad_id = _new_id(wf)
|
||||
wf[pad_id] = {
|
||||
"class_type": "ImagePadForOutpaint",
|
||||
"inputs": {
|
||||
"image": [load_id, 0],
|
||||
"left": left,
|
||||
"top": top,
|
||||
"right": right,
|
||||
"bottom": bottom,
|
||||
"feathering": feather,
|
||||
},
|
||||
}
|
||||
|
||||
# Reconectar VAEEncodeForInpaint a las dos salidas del pad: pixels <- IMAGE, mask <- MASK.
|
||||
enc = wf[enc_id]["inputs"]
|
||||
enc["pixels"] = [pad_id, 0]
|
||||
enc["mask"] = [pad_id, 1]
|
||||
enc["grow_mask_by"] = grow_mask
|
||||
|
||||
save_id = _find_class(wf, "SaveImage")
|
||||
if save_id is not None:
|
||||
wf[save_id]["inputs"]["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_outpaint_asset_workflow(
|
||||
"seamless_00004_.png",
|
||||
"more forest and foliage extending to the right",
|
||||
right=256,
|
||||
feather=40,
|
||||
style="game background, top-down 2d",
|
||||
denoise=1.0,
|
||||
seed=7,
|
||||
)
|
||||
pad_id = next(
|
||||
nid for nid, n in wf.items() if n["class_type"] == "ImagePadForOutpaint"
|
||||
)
|
||||
enc_id = next(
|
||||
nid for nid, n in wf.items() if n["class_type"] == "VAEEncodeForInpaint"
|
||||
)
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"nodes": list(wf),
|
||||
"classes": sorted({n["class_type"] for n in wf.values()}),
|
||||
"has_LoadImageMask": any(
|
||||
n["class_type"] == "LoadImageMask" for n in wf.values()
|
||||
),
|
||||
"pad_inputs": wf[pad_id]["inputs"],
|
||||
"enc_pixels": wf[enc_id]["inputs"]["pixels"],
|
||||
"enc_mask": wf[enc_id]["inputs"]["mask"],
|
||||
"enc_grow_mask_by": wf[enc_id]["inputs"]["grow_mask_by"],
|
||||
"denoise": wf["3"]["inputs"]["denoise"],
|
||||
"positive": wf["6"]["inputs"]["text"],
|
||||
"input_image": wf["10"]["inputs"]["image"],
|
||||
"filename_prefix": wf["9"]["inputs"]["filename_prefix"],
|
||||
},
|
||||
indent=2,
|
||||
)
|
||||
)
|
||||
Reference in New Issue
Block a user