feat(gamedev): comfyui_build_inpaint_asset_workflow — editar solo una región de un asset (inpaint)
Cuarto vértice del eje transform de gamedev-2d: editar SOLO una región de un asset 2D ya pintado vía inpaint, conservando el resto del sprite. Completa el eje junto a txt2img (crear de cero), asset_variant (img2img: reescribe todo) y sprite_from_sketch (ControlNet: sprite nuevo desde boceto). Función pura (API format dict) que compone comfyui_build_inpaint_workflow (base) + comfyui_inject_lora (estilo opcional). Recibe asset + máscara (blanco=editar, negro=conservar) + prompt de qué poner; VAEEncodeForInpaint codifica respetando la máscara y dilata el borde grow_mask px para difuminar la costura; el KSampler regenera solo esa zona. mode="noise_mask" degrada a VAEEncode+SetLatentNoiseMask para servidores sin VAEEncodeForInpaint (error path). size escala imagen Y máscara de forma consistente. class_types verificados contra /object_info (8GB lowvram). Probado e2e en GPU con SD1.5: máscara circular sobre la mano del goblin enemy_creature_00001_.png, prompt "a glowing blue magic orb" (prompt_id 88b52c66). Solo la región enmascarada cambió: diff medio dentro 40.3 vs fuera 1.97 (ratio 20.4x), 44.6% px cambiados dentro vs 1.7% fuera. Confirmación visual: orbe azul en la región, resto del goblin idéntico. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
---
|
||||
name: comfyui_build_inpaint_asset_workflow
|
||||
kind: function
|
||||
lang: py
|
||||
domain: ml
|
||||
purity: pure
|
||||
version: 1.0.0
|
||||
signature: "def comfyui_build_inpaint_asset_workflow(input_image: str, mask_image: str, prompt: str, *, checkpoint: str = \"dreamshaper_8.safetensors\", denoise: float = 1.0, style: str = \"game asset\", grow_mask: int = 6, size: int | None = None, seed: int = 0, lora: str | None = None, lora_strength: float = 1.0, mode: str = \"vae_encode\", negative: str | None = None, steps: int = 28, cfg: float = 7.0, sampler_name: str = \"dpmpp_2m\", scheduler: str = \"karras\", img_upscale_method: str = \"bilinear\", mask_upscale_method: str = \"nearest-exact\", crop: str = \"disabled\", filename_prefix: str = \"inpaint_asset\") -> dict"
|
||||
description: "Construye el dict (API format) del workflow que EDITA SOLO UNA REGION de un asset 2D ya pintado (inpaint): recibe el asset + una mascara (blanco = editar, negro = conservar) + un prompt de que poner en esa zona, y repinta UNICAMENTE la region enmascarada dejando el resto del sprite intacto. Es el cuarto 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), y de ControlNet (sprite_from_sketch, pinta un sprite nuevo desde un boceto). Mecanismo (modo vae_encode): VAEEncodeForInpaint codifica el latente respetando la mascara y dilata su borde grow_mask px para difuminar la costura; el KSampler (denoise alto) regenera solo esa zona con '{prompt}, {style}, seamless blend'. Modo noise_mask degrada a VAEEncode + SetLatentNoiseMask (+ GrowMask) para servidores sin VAEEncodeForInpaint. size escala imagen Y mascara de forma consistente (escalar solo una las desalinearia). Compone comfyui_build_inpaint_workflow + comfyui_inject_lora (estilo opcional). Pura, sin red ni I/O. class_types verificados contra /object_info (8GB lowvram)."
|
||||
tags: [comfyui, ml, gamedev-2d, inpaint, asset-transform, mask-edit, 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 editar dentro de la carpeta input/ del servidor ComfyUI (un sprite/icono YA pintado). Lo carga el nodo LoadImage. Subelo antes con POST /upload/image o copialo a ~/ComfyUI/input/. No puede estar vacio."
|
||||
- name: mask_image
|
||||
desc: "Nombre del archivo de la mascara dentro de input/ del servidor. BLANCO = region a editar/repintar, NEGRO = region a conservar. Debe tener la MISMA resolucion que input_image (salvo que pases size, que reescala ambas consistentemente). No puede estar vacio: sin mascara no es inpaint (para reescribir el asset entero usa comfyui_build_asset_variant_workflow)."
|
||||
- name: prompt
|
||||
desc: "Que poner en la region enmascarada ('a golden sword', 'a blue shield', 'empty background', 'intact armor'). Es lo unico que se regenera; el resto del asset se conserva. No puede estar vacio."
|
||||
- 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. En inpaint suele ser alto: 1.0 (por defecto) reescribe la region por completo; <1.0 conserva parte de los pixeles originales (reparacion suave). Se clampa a [0.0, 1.0]. keyword-only."
|
||||
- name: style
|
||||
desc: "Descriptor de estilo que mantiene la region coherente con el asset y el set ('game asset', 'dark fantasy creature', 'pixel art'). Mismo style + checkpoint + (lora) que el resto del set. keyword-only."
|
||||
- name: grow_mask
|
||||
desc: "Pixeles que se dilata el borde de la mascara para difuminar la costura entre lo viejo y lo nuevo (evita bordes duros). Se clampa a [0, 64] (limite del nodo VAEEncodeForInpaint). En modo noise_mask se aplica via GrowMask. keyword-only."
|
||||
- name: size
|
||||
desc: "Lado en px al que se NORMALIZAN imagen Y mascara antes de inpaint. None (por defecto) = no escala: conserva la resolucion nativa del asset y exige que mascara e imagen ya coincidan en tamano (lo recomendado; los assets del set ya salen a 512). Un int reescala AMBAS a size x size de forma consistente. Solo aplica al modo vae_encode. 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: mode
|
||||
desc: "'vae_encode' (por defecto, VAEEncodeForInpaint con grow_mask nativo) o 'noise_mask' (degrada a VAEEncode + SetLatentNoiseMask + GrowMask para servidores sin VAEEncodeForInpaint). El caller decide tras consultar /object_info. keyword-only."
|
||||
- name: negative
|
||||
desc: "Prompt negativo. None usa el negativo por defecto pensado para edicion local (mezcla limpia, sin costuras ni bordes de mascara, una figura). 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: img_upscale_method
|
||||
desc: "Metodo de ImageScale para la imagen cuando size no es None ('bilinear' por defecto; 'lanczos' NO esta disponible en este servidor). keyword-only."
|
||||
- name: mask_upscale_method
|
||||
desc: "Metodo de ImageScale para la mascara cuando size no es None ('nearest-exact' por defecto, preserva bordes nitidos blanco/negro de la mascara). keyword-only."
|
||||
- name: crop
|
||||
desc: "Modo de recorte de ImageScale ('disabled' o 'center'). Solo si size no es None. 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: inpaint que repinta SOLO la region marcada en blanco por la mascara, conservando el resto del asset, con grow_mask para difuminar la costura, escalado consistente opcional (img+mask) y LoRA de estilo opcional. Nodos modo vae_encode: CheckpointLoaderSimple '4', LoadImage '10', LoadImageMask '12', VAEEncodeForInpaint '11', CLIPTextEncode '6'/'7', KSampler '3', VAEDecode '8', SaveImage '9' (+ ImageScale/ImageToMask si size, + LoraLoader si lora). Modo noise_mask sustituye VAEEncodeForInpaint por VAEEncode + SetLatentNoiseMask (+ GrowMask)."
|
||||
tested: false
|
||||
file_path: python/functions/ml/comfyui_build_inpaint_asset_workflow.py
|
||||
---
|
||||
|
||||
Construye el dict (API format) del workflow que **edita SOLO una región** de un asset
|
||||
2D ya pintado (inpaint). Cuarto vértice del eje **transform** de `gamedev-2d`, junto a
|
||||
sus hermanos `comfyui_build_asset_variant_workflow` (img2img: reescribe todo) y
|
||||
`comfyui_build_sprite_from_sketch_workflow` (ControlNet: pinta desde un boceto). Aquí
|
||||
el dev tiene un sprite terminado y quiere cambiar **una parte** —ponerle otra arma,
|
||||
quitarle el casco, añadir un escudo, reparar una zona dañada— dejando el resto del
|
||||
pixel intacto. Una máscara delimita qué se repinta (blanco = editar, negro = conservar)
|
||||
y el sampler solo regenera ahí.
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```python
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions"))
|
||||
from ml.comfyui_build_inpaint_asset_workflow import comfyui_build_inpaint_asset_workflow
|
||||
from ml.comfyui_submit_workflow import comfyui_submit_workflow
|
||||
from ml.comfyui_wait_result import comfyui_wait_result
|
||||
|
||||
# Editar SOLO la mano del goblin: poner un orbe mágico, conservar el resto del sprite.
|
||||
# (input_image y mask_image deben estar ya subidos al input/ del server, misma resolución)
|
||||
wf = comfyui_build_inpaint_asset_workflow(
|
||||
"enemy_creature_00001_.png", # el goblin (512x512, ya en ~/ComfyUI/input/)
|
||||
"goblin_hand_mask.png", # máscara: blanco sobre la mano, negro el resto
|
||||
"a glowing blue magic orb", # qué poner en la región enmascarada
|
||||
style="dark fantasy creature, game asset",
|
||||
grow_mask=8, # difumina la costura 8 px
|
||||
denoise=1.0, # reescribe la región 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/inpaint_asset_*.png: SOLO la mano cambió.
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Úsala cuando tengas un asset 2D **ya pintado** y quieras cambiar **una zona concreta**
|
||||
sin regenerar el resto: cambiar/añadir un objeto que sostiene un personaje, quitar una
|
||||
pieza de equipo, reparar una región dañada o limpiar un fondo detrás del sujeto. Elige
|
||||
entre los hermanos del eje transform así:
|
||||
|
||||
- **una región, el resto intacto** → este builder (inpaint con máscara).
|
||||
- **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
|
||||
|
||||
- **Necesita una máscara real.** `mask_image` es obligatorio (blanco = editar, negro =
|
||||
conservar). Sin máscara no es inpaint: lanza `ValueError`. Para reescribir el asset
|
||||
entero usa `asset_variant`. La máscara y el asset deben tener la **misma resolución**
|
||||
(o pasa `size`, que reescala ambos de forma consistente; escalar solo uno los
|
||||
desalinea y la edición cae en el sitio equivocado).
|
||||
- **`grow_mask` evita costuras.** Sin dilatar el borde de la máscara aparece una línea
|
||||
dura entre lo viejo y lo nuevo. 6–10 px suele bastar; se clampa a `[0, 64]`.
|
||||
- **`denoise` alto por defecto (1.0).** La región se reescribe por completo. Baja a
|
||||
~0.5–0.7 para reparaciones suaves que conserven parte de los píxeles originales bajo
|
||||
la máscara.
|
||||
- **`mode="noise_mask"` es el plan B.** Si el servidor no expone `VAEEncodeForInpaint`
|
||||
(compruébalo con `/object_info`), pásalo: arma el equivalente con `VAEEncode` +
|
||||
`SetLatentNoiseMask` (+ `GrowMask`). El camino por defecto `vae_encode` da mejores
|
||||
bordes y es el recomendado.
|
||||
- **`ImageScale` no ofrece 'lanczos' en este servidor** (8GB lowvram): métodos válidos
|
||||
`nearest-exact`, `bilinear`, `area`, `bicubic`. La máscara se escala con
|
||||
`nearest-exact` por defecto para no difuminar sus bordes blanco/negro.
|
||||
- **Función pura.** No sube ni valida que `input_image`/`mask_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,422 @@
|
||||
"""Construye el workflow ComfyUI que EDITA SOLO UNA REGION de un asset ya pintado (inpaint).
|
||||
|
||||
Es el cuarto vertice del eje `transform` del catalogo gamedev-2d. Los otros tres
|
||||
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.
|
||||
|
||||
Este builder cubre el dolor que ninguno resuelve: tienes un asset terminado y quieres
|
||||
cambiar SOLO una parte -- ponerle otra arma, quitarle el casco, anadirle un escudo,
|
||||
reparar una zona danada -- dejando el RESTO del pixel intacto. Eso es inpaint: una
|
||||
mascara marca que region se repinta (blanco = editar, negro = conservar) y el sampler
|
||||
solo regenera ahi.
|
||||
|
||||
Diferencia clave con los hermanos:
|
||||
|
||||
- vs asset_variant (img2img): variant reescribe el asset ENTERO con denoise medio;
|
||||
aqui solo cambia la region enmascarada y el resto se preserva bit a bit fuera del
|
||||
crecimiento de la mascara.
|
||||
- vs sprite_from_sketch (ControlNet): sketch parte de un dibujo de lineas que guia
|
||||
la forma de un sprite NUEVO; aqui se parte de un asset YA pintado y de una mascara
|
||||
que delimita la zona a editar.
|
||||
|
||||
Mecanismo (modo por defecto `vae_encode`):
|
||||
|
||||
CheckpointLoaderSimple -> [LoraLoader opcional de estilo] -> KSampler
|
||||
LoadImage(asset) -> [ImageScale opcional] ->\
|
||||
LoadImageMask(mask) ----------------------- > VAEEncodeForInpaint(grow_mask_by) -> KSampler.latent
|
||||
CLIPTextEncode(prompt de la region + estilo + "seamless blend") -> KSampler.positive
|
||||
KSampler(denoise alto) -> VAEDecode -> SaveImage
|
||||
|
||||
`VAEEncodeForInpaint` codifica el latente respetando la mascara y dilata sus bordes
|
||||
`grow_mask_by` pixeles para que la costura entre lo viejo y lo nuevo quede difuminada
|
||||
(sin bordes duros visibles). En inpaint el `denoise` suele ser alto (1.0): la region
|
||||
se reescribe por completo a partir del prompt; un denoise menor conserva parte de los
|
||||
pixeles originales bajo la mascara (util para reparaciones suaves).
|
||||
|
||||
Modo de degradacion `noise_mask` (para el error path): si el servidor NO expone
|
||||
`VAEEncodeForInpaint`, el caller -- que SI puede consultar /object_info -- pide
|
||||
`mode="noise_mask"` y el builder arma el camino equivalente con nodos basicos:
|
||||
|
||||
LoadImage -> VAEEncode -> SetLatentNoiseMask(mask) -> KSampler.latent
|
||||
|
||||
opcionalmente dilatando la mascara con `GrowMask(expand=grow_mask)` antes de aplicarla,
|
||||
ya que `SetLatentNoiseMask` no crece la mascara por si mismo.
|
||||
|
||||
Compone:
|
||||
- comfyui_build_inpaint_workflow -> base inpaint (Checkpoint/LoadImage/LoadImageMask/
|
||||
VAEEncodeForInpaint/KSampler/VAEDecode/SaveImage).
|
||||
- comfyui_inject_lora -> LoRA de estilo opcional (coherencia con el set).
|
||||
|
||||
El unico codigo propio es: el prompt gamedev (region + estilo + mezcla limpia), el
|
||||
repunte de `grow_mask_by` y `filename_prefix`, el escalado opcional CONSISTENTE de
|
||||
imagen y mascara (escalar solo una las desalinearia), y la construccion del camino
|
||||
`noise_mask` de degradacion.
|
||||
|
||||
class_types/inputs verificados contra /object_info del servidor (8GB lowvram):
|
||||
CheckpointLoaderSimple, LoadImage, LoadImageMask, ImageScale, ImageToMask,
|
||||
VAEEncode, VAEEncodeForInpaint, SetLatentNoiseMask, GrowMask, CLIPTextEncode,
|
||||
KSampler, VAEDecode, SaveImage, LoraLoader. Nota: ImageScale en este servidor NO
|
||||
ofrece 'lanczos'; los metodos validos son 'nearest-exact', 'bilinear', 'area',
|
||||
'bicubic'.
|
||||
|
||||
Funcion pura: sin red, sin I/O. No muta dicts de entrada (copia profunda al insertar
|
||||
nodos). NO valida que input_image/mask_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 copy
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
|
||||
# Negativo por defecto pensado para edicion local de un asset: la region nueva debe
|
||||
# fundirse con el resto (sin costuras ni bordes de mascara visibles), sin duplicar el
|
||||
# sujeto ni meter texto/marcas. NO restringe material/color (el prompt manda en la zona).
|
||||
_INPAINT_ASSET_NEGATIVE = (
|
||||
"visible seam, visible mask edge, hard border, halo, blurry, lowres, "
|
||||
"deformed, bad anatomy, extra limbs, duplicate, multiple subjects, "
|
||||
"extra objects, text, watermark, signature, logo, jpeg artifacts"
|
||||
)
|
||||
|
||||
_VALID_MODES = ("vae_encode", "noise_mask")
|
||||
|
||||
|
||||
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 _scale_inpaint_to_size(
|
||||
wf: dict, *, size: int, img_method: str, mask_method: str, crop: str
|
||||
) -> dict:
|
||||
"""Escala imagen base Y mascara al mismo size para no desalinearlas.
|
||||
|
||||
Inserta un ImageScale entre el LoadImage del asset y el VAEEncodeForInpaint, y
|
||||
reemplaza el LoadImageMask por LoadImage(mask) -> ImageScale -> ImageToMask(red)
|
||||
de modo que ambos lleguen al nodo de inpaint a size x size con el mismo recorte.
|
||||
Escalar solo la imagen (dejando la mascara a su tamano original) produciria una
|
||||
mascara que ya no cubre la region correcta: por eso aqui se escalan las DOS o
|
||||
ninguna. Pura: trabaja sobre una copia profunda.
|
||||
"""
|
||||
wf = copy.deepcopy(wf)
|
||||
enc_id = _find_class(wf, "VAEEncodeForInpaint")
|
||||
if enc_id is None:
|
||||
raise ValueError(
|
||||
"comfyui_build_inpaint_asset_workflow: no se encontro VAEEncodeForInpaint "
|
||||
"para escalar (size solo aplica al modo vae_encode)"
|
||||
)
|
||||
enc = wf[enc_id]["inputs"]
|
||||
img_src = list(enc.get("pixels", []))
|
||||
mask_link = list(enc.get("mask", []))
|
||||
if not img_src or not mask_link:
|
||||
raise ValueError(
|
||||
"comfyui_build_inpaint_asset_workflow: VAEEncodeForInpaint sin pixels/mask"
|
||||
)
|
||||
|
||||
# Imagen base: LoadImage[0] -> ImageScale -> VAEEncodeForInpaint.pixels
|
||||
scale_img = _new_id(wf)
|
||||
wf[scale_img] = {
|
||||
"class_type": "ImageScale",
|
||||
"inputs": {
|
||||
"image": img_src,
|
||||
"upscale_method": img_method,
|
||||
"width": int(size),
|
||||
"height": int(size),
|
||||
"crop": crop,
|
||||
},
|
||||
}
|
||||
enc["pixels"] = [scale_img, 0]
|
||||
|
||||
# Mascara: el nodo fuente (LoadImageMask) se convierte en LoadImage para poder
|
||||
# escalarla como imagen y volver a MASK con ImageToMask al mismo size.
|
||||
mask_node_id = mask_link[0]
|
||||
mask_node = wf[mask_node_id]
|
||||
mask_file = mask_node["inputs"].get("image", "")
|
||||
mask_node["class_type"] = "LoadImage"
|
||||
mask_node["inputs"] = {"image": mask_file}
|
||||
|
||||
scale_mask = _new_id(wf)
|
||||
wf[scale_mask] = {
|
||||
"class_type": "ImageScale",
|
||||
"inputs": {
|
||||
"image": [mask_node_id, 0],
|
||||
"upscale_method": mask_method,
|
||||
"width": int(size),
|
||||
"height": int(size),
|
||||
"crop": crop,
|
||||
},
|
||||
}
|
||||
to_mask = _new_id(wf)
|
||||
wf[to_mask] = {
|
||||
"class_type": "ImageToMask",
|
||||
"inputs": {"image": [scale_mask, 0], "channel": "red"},
|
||||
}
|
||||
enc["mask"] = [to_mask, 0]
|
||||
return wf
|
||||
|
||||
|
||||
def _to_noise_mask(wf: dict, *, grow_mask: int) -> dict:
|
||||
"""Reemplaza el camino VAEEncodeForInpaint por VAEEncode + SetLatentNoiseMask.
|
||||
|
||||
Degradacion para servidores sin VAEEncodeForInpaint: codifica la imagen con un
|
||||
VAEEncode normal y aplica la mascara al latente con SetLatentNoiseMask, opcional-
|
||||
mente dilatandola antes con GrowMask(expand=grow_mask) porque SetLatentNoiseMask
|
||||
no crece la mascara. Repunta KSampler.latent_image al nuevo SetLatentNoiseMask.
|
||||
Pura: copia profunda.
|
||||
"""
|
||||
wf = copy.deepcopy(wf)
|
||||
enc_id = _find_class(wf, "VAEEncodeForInpaint")
|
||||
if enc_id is None:
|
||||
raise ValueError(
|
||||
"comfyui_build_inpaint_asset_workflow: base sin VAEEncodeForInpaint; "
|
||||
"no se puede degradar a noise_mask"
|
||||
)
|
||||
enc = wf[enc_id]["inputs"]
|
||||
pixels = list(enc["pixels"])
|
||||
vae = list(enc["vae"])
|
||||
mask_link = list(enc["mask"])
|
||||
|
||||
# VAEEncode normal en el mismo node_id (reutiliza el id; cae el grow_mask_by).
|
||||
wf[enc_id] = {
|
||||
"class_type": "VAEEncode",
|
||||
"inputs": {"pixels": pixels, "vae": vae},
|
||||
}
|
||||
|
||||
mask_out = mask_link
|
||||
if grow_mask and int(grow_mask) != 0:
|
||||
grow_id = _new_id(wf)
|
||||
wf[grow_id] = {
|
||||
"class_type": "GrowMask",
|
||||
"inputs": {
|
||||
"mask": mask_link,
|
||||
"expand": int(grow_mask),
|
||||
"tapered_corners": True,
|
||||
},
|
||||
}
|
||||
mask_out = [grow_id, 0]
|
||||
|
||||
setmask = _new_id(wf)
|
||||
wf[setmask] = {
|
||||
"class_type": "SetLatentNoiseMask",
|
||||
"inputs": {"samples": [enc_id, 0], "mask": mask_out},
|
||||
}
|
||||
|
||||
ks_id = _find_class(wf, "KSampler")
|
||||
if ks_id is not None:
|
||||
wf[ks_id]["inputs"]["latent_image"] = [setmask, 0]
|
||||
return wf
|
||||
|
||||
|
||||
def comfyui_build_inpaint_asset_workflow(
|
||||
input_image: str,
|
||||
mask_image: str,
|
||||
prompt: str,
|
||||
*,
|
||||
checkpoint: str = "dreamshaper_8.safetensors",
|
||||
denoise: float = 1.0,
|
||||
style: str = "game asset",
|
||||
grow_mask: int = 6,
|
||||
size: int | None = None,
|
||||
seed: int = 0,
|
||||
lora: str | None = None,
|
||||
lora_strength: float = 1.0,
|
||||
mode: str = "vae_encode",
|
||||
negative: str | None = None,
|
||||
steps: int = 28,
|
||||
cfg: float = 7.0,
|
||||
sampler_name: str = "dpmpp_2m",
|
||||
scheduler: str = "karras",
|
||||
img_upscale_method: str = "bilinear",
|
||||
mask_upscale_method: str = "nearest-exact",
|
||||
crop: str = "disabled",
|
||||
filename_prefix: str = "inpaint_asset",
|
||||
) -> dict:
|
||||
"""Construye el dict (API format) de un inpaint que edita SOLO la region enmascarada.
|
||||
|
||||
Args:
|
||||
input_image: nombre del archivo del asset a editar dentro de la carpeta input/
|
||||
del servidor ComfyUI (un sprite/icono YA pintado). Lo carga LoadImage.
|
||||
Subelo antes con POST /upload/image o copialo a ~/ComfyUI/input/. No puede
|
||||
estar vacio.
|
||||
mask_image: nombre del archivo de la mascara dentro de input/ del servidor.
|
||||
BLANCO = region a editar/repintar, NEGRO = region a conservar. Debe tener
|
||||
la MISMA resolucion que input_image (salvo que pases `size`, que reescala
|
||||
ambas de forma consistente). No puede estar vacio: una edicion sin mascara
|
||||
no es inpaint -- para reescribir el asset entero usa asset_variant.
|
||||
prompt: que poner en la region enmascarada ("a golden sword", "a blue shield",
|
||||
"empty background", "intact armor"). Es lo unico que se regenera; el resto
|
||||
del asset se conserva. No puede estar vacio.
|
||||
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. En inpaint suele
|
||||
ser alto: 1.0 (por defecto) reescribe la region por completo; <1.0 conserva
|
||||
parte de los pixeles originales (reparacion suave). Se clampa a [0.0, 1.0].
|
||||
keyword-only.
|
||||
style: descriptor de estilo que mantiene la region coherente con el asset y el
|
||||
set ("game asset", "dark fantasy creature", "pixel art"). Pasa el MISMO
|
||||
style + checkpoint + (lora) que el resto del set. keyword-only.
|
||||
grow_mask: pixeles que se dilata el borde de la mascara para difuminar la
|
||||
costura entre lo viejo y lo nuevo (evita bordes duros). Se clampa a [0, 64]
|
||||
(limite del nodo VAEEncodeForInpaint). En modo noise_mask se aplica via
|
||||
GrowMask. keyword-only.
|
||||
size: lado en px al que se NORMALIZAN imagen Y mascara antes de inpaint. None
|
||||
(por defecto) = no escala: el inpaint conserva la resolucion nativa del
|
||||
asset y exige que mascara e imagen ya coincidan en tamano (lo recomendado;
|
||||
los assets del set ya salen a 512). Un int reescala AMBAS a size x size de
|
||||
forma consistente. Solo aplica al modo vae_encode. 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.
|
||||
mode: 'vae_encode' (por defecto, usa VAEEncodeForInpaint con grow_mask nativo)
|
||||
o 'noise_mask' (degrada a VAEEncode + SetLatentNoiseMask para servidores sin
|
||||
VAEEncodeForInpaint). El caller decide el modo tras consultar /object_info.
|
||||
keyword-only.
|
||||
negative: prompt negativo. None usa el negativo por defecto pensado para edicion
|
||||
local (mezcla limpia, sin costuras, una figura). keyword-only.
|
||||
steps, cfg, sampler_name, scheduler, filename_prefix: parametros de generacion.
|
||||
keyword-only.
|
||||
img_upscale_method: metodo de ImageScale para la imagen cuando size no es None
|
||||
('bilinear' por defecto; 'lanczos' NO esta disponible en este servidor).
|
||||
keyword-only.
|
||||
mask_upscale_method: metodo de ImageScale para la mascara cuando size no es None
|
||||
('nearest-exact' por defecto, preserva bordes nitidos blanco/negro de la
|
||||
mascara). keyword-only.
|
||||
crop: modo de recorte de ImageScale ('disabled' o 'center'). keyword-only.
|
||||
|
||||
Returns:
|
||||
dict en API format listo para comfyui_submit_workflow: inpaint que repinta SOLO
|
||||
la region marcada en blanco por la mascara con '{prompt}, {style}, seamless
|
||||
blend...', conservando el resto del asset. Con grow_mask para difuminar la
|
||||
costura, escalado consistente opcional y LoRA de estilo opcional.
|
||||
|
||||
Raises:
|
||||
ValueError: si input_image, mask_image o prompt estan vacios; si mode no es
|
||||
'vae_encode' ni 'noise_mask'; o si la base no tiene los nodos esperados
|
||||
(propagado por los helpers).
|
||||
"""
|
||||
from ml.comfyui_build_inpaint_workflow import comfyui_build_inpaint_workflow
|
||||
|
||||
if not input_image or not input_image.strip():
|
||||
raise ValueError(
|
||||
"comfyui_build_inpaint_asset_workflow: 'input_image' no puede estar vacio"
|
||||
)
|
||||
if not mask_image or not mask_image.strip():
|
||||
raise ValueError(
|
||||
"comfyui_build_inpaint_asset_workflow: 'mask_image' no puede estar vacio "
|
||||
"(sin mascara no es inpaint; para reescribir el asset entero usa "
|
||||
"comfyui_build_asset_variant_workflow)"
|
||||
)
|
||||
if not prompt or not prompt.strip():
|
||||
raise ValueError(
|
||||
"comfyui_build_inpaint_asset_workflow: 'prompt' no puede estar vacio"
|
||||
)
|
||||
if mode not in _VALID_MODES:
|
||||
raise ValueError(
|
||||
f"comfyui_build_inpaint_asset_workflow: 'mode' debe ser uno de {_VALID_MODES}, "
|
||||
f"recibido {mode!r}"
|
||||
)
|
||||
|
||||
input_image = input_image.strip()
|
||||
mask_image = mask_image.strip()
|
||||
prompt = prompt.strip()
|
||||
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 = _INPAINT_ASSET_NEGATIVE if negative is None else negative
|
||||
|
||||
# Prompt de la region: describe SOLO lo que va en la zona enmascarada y empuja a
|
||||
# que se funda con el resto (mezcla limpia, iluminacion consistente). A diferencia
|
||||
# de asset_variant NO se pide "same pose/silhouette": aqui solo cambia un trozo.
|
||||
positive = (
|
||||
f"{prompt}, {style}, seamless blend with surroundings, "
|
||||
"consistent lighting, matching art style, high detail"
|
||||
)
|
||||
|
||||
wf = comfyui_build_inpaint_workflow(
|
||||
checkpoint,
|
||||
input_image,
|
||||
mask_image,
|
||||
positive,
|
||||
neg,
|
||||
denoise=denoise,
|
||||
steps=steps,
|
||||
cfg=cfg,
|
||||
seed=seed,
|
||||
sampler_name=sampler_name,
|
||||
scheduler=scheduler,
|
||||
)
|
||||
|
||||
# Repuntar grow_mask_by (el base lo deja en 6) y el filename_prefix (el base lo
|
||||
# deja en 'comfy_inpaint').
|
||||
enc_id = _find_class(wf, "VAEEncodeForInpaint")
|
||||
if enc_id is not None:
|
||||
wf[enc_id]["inputs"]["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 mode == "vae_encode" and size is not None:
|
||||
wf = _scale_inpaint_to_size(
|
||||
wf,
|
||||
size=int(size),
|
||||
img_method=img_upscale_method,
|
||||
mask_method=mask_upscale_method,
|
||||
crop=crop,
|
||||
)
|
||||
|
||||
if mode == "noise_mask":
|
||||
wf = _to_noise_mask(wf, grow_mask=grow_mask)
|
||||
|
||||
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_inpaint_asset_workflow(
|
||||
"enemy_creature_00001_.png",
|
||||
"goblin_hand_mask.png",
|
||||
"a glowing blue magic orb",
|
||||
style="dark fantasy creature, game asset",
|
||||
grow_mask=8,
|
||||
denoise=1.0,
|
||||
seed=7,
|
||||
)
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"nodes": list(wf),
|
||||
"classes": sorted({n["class_type"] for n in wf.values()}),
|
||||
"denoise": wf["3"]["inputs"]["denoise"],
|
||||
"grow_mask_by": wf["11"]["inputs"]["grow_mask_by"],
|
||||
"positive": wf["6"]["inputs"]["text"],
|
||||
"input_image": wf["10"]["inputs"]["image"],
|
||||
"mask": wf["12"]["inputs"]["image"],
|
||||
"filename_prefix": wf["9"]["inputs"]["filename_prefix"],
|
||||
},
|
||||
indent=2,
|
||||
)
|
||||
)
|
||||
Reference in New Issue
Block a user