feat(gamedev): comfyui_build_sprite_from_sketch_workflow — boceto→sprite vía ControlNet
Tercer eje del catálogo gamedev-2d: partir del DIBUJO del dev. Recibe un boceto/lineart + un prompt de qué es y construye un workflow txt2img guiado por ControlNet (lineart/scribble/canny) que pinta el sprite conservando la forma dibujada. Distinto de los builders txt2img (inventan la forma desde texto) y de asset_variant img2img (reescribe una imagen ya pintada conservando forma+color): aquí el dev marca la silueta y la IA pone material/color/acabado, conservando solo la forma. Función pura (API format). Compone comfyui_build_txt2img_workflow + comfyui_inject_controlnet + comfyui_inject_lora; el único código propio es el helper que interpone el preprocesador (LineArt/Scribble/Canny) entre el boceto y el ControlNet, análogo a _inject_image_scale del hermano asset_variant. control_type selecciona preprocesador y modelo CN emparejado; controlnet_name y preprocess dan override para degradar al modelo disponible. Gotcha documentado: el server 8GB solo tiene modelos CN SD1.5 canny/depth/openpose — para lineart/scribble usar override a canny o control_type=canny (pendiente humano descargar los modelos lineart/scribble dedicados). Verificación: tests offline verdes (cableado txt2img guiado, 3 control_types, clamps, errores). E2E real GPU SD1.5: boceto del goblin → CannyEdgePreprocessor → ControlNet canny → sprite que respeta pose/orejas/hombrera/lanza/espada del dibujo (prompt_id ea6fc372, edge corr 0.545, luminance corr -0.19 confirmando repintado). Report en reports/0182. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -65,18 +65,26 @@ VFX (ver `reports/0143`).
|
||||
| `comfyui_build_rune_glyph_workflow_py_ml` | `(glyph, *, glow=True, style="arcane glowing rune", checkpoint="dreamshaper_8…", size=512, seed=0, lora=None, …) -> dict` | UNA **runa / glifo / sigilo mágico** (glifos rúnicos, círculos mágicos, sigilos de invocación, inscripciones brillantes) para hechizos, portales, marcas de conjuro y efectos de magia: símbolo arcano **aislado** sobre fondo uniforme (`{glyph}, {style}, magic symbol, single isolated glyph, centered, glowing on a solid pure black background, occult sigil, arcane inscription, no scenery, game asset…`) → txt2img cuadrado + LoRA estilo opcional. **`glow` elige el camino a alpha**: `glow=True` (defecto) = runa BRILLANTE sobre **NEGRO puro**, **sin Rembg** (recortaría el halo del resplandor), insumo de **`comfyui_matting_luma_to_alpha`** (luma=alpha, **blend aditivo** en el motor — conserva el glow); `glow=False` = runa MATE/grabada sobre fondo plano (el negativo rechaza `glow/neon/bloom`), recorte/inversión por el caller. El negativo rechaza `realistic text/readable words/latin alphabet` (un glifo arcano, **no letras reales**) + fondo texturizado/niebla. **DISTINTO de `status_effect_icon`** (símbolo SÓLIDO de UI, recorte Rembg, legible a 16-32 px en el HUD): la runa es una marca translúcida que **emite luz** e se inscribe en el mundo. Grimorio coherente = mismo `style`/`checkpoint`/`lora`, varía `glyph`/`seed`. ⚠️ luma Rec601 penaliza el rojo → para runas rojas (sigilo demoníaco) pasar `luma_weights` con más peso al rojo + subir `gamma`; runas blancas/azules/doradas van con pesos por defecto. Probado e2e en GPU con SD1.5 — `circular summoning rune` glow seed 11 512×512, círculo de invocación brillante sobre **negro puro** (esquinas luma 0.00, dark 83%, runa 3.4% brillante, max 255) apto luma→alpha (`prompt_id 701d149a`, `reports/0172`). SD1.5. |
|
||||
| `comfyui_build_title_lettering_workflow_py_ml` | `(text, *, letter_style="epic fantasy metallic", checkpoint="juggernaut_xl_v11…", width=1024, height=512, transparent=True, seed=0, lora=None, …) -> dict` | EL texto/logo de **título** de un juego (el nombre del juego o una palabra) renderizado con un **tratamiento de lettering** (metálico, tallado en fuego/piedra/madera, neón, cristal, oro), formato **apaisado** (`width>height`, 1024×512 por defecto), fondo plano recortable a alpha (`the word "{text}" as a game logo, {letter_style} lettering, stylized typography, centered, plain background…`) → txt2img apaisado + LoRA estilo opcional + Rembg (alpha). El **negativo NO rechaza texto** (el lettering es el sujeto) y empuja contra el ruido textual (`extra letters/jumbled text/deformed letters`). El VALOR es el ESTILO del lettering, **NO** la fidelidad tipográfica: ⚠️ la difusión renderiza texto de forma imperfecta — letras de más, deformadas o mal escritas; mitigar con palabras CORTAS en MAYÚSCULA, **re-roll de seeds** (`comfyui_batch_generate`), SDXL > SD1.5 para texto, o pintar el texto real con una fuente en el motor. **Una palabra que es un objeto concreto (DRAGON) → el modelo dibuja el objeto, no las letras** — usar palabras abstractas o reforzar `letter_style`. Marca coherente = mismo `letter_style`/`checkpoint`/`lora`, varía solo `text`. Recorte por **Rembg** (logo sólido), no luma→alpha. Probado e2e en GPU: `DRAGON`/`fire engraved` SD1.5 1024×512 → ilustró dragones rojos (alpha OK, confirma el gotcha de palabra-objeto, `prompt_id 6f3920b7`); `AETHER`/`epic fantasy metallic` SDXL 768×384 → **logo de texto metálico dorado** legible con ortografía imperfecta + alpha (`prompt_id 2a7fe8ba`, `reports/0165`). SD1.5/SDXL. |
|
||||
|
||||
## Builders de transformación (`gamedev-2d`, puros — img2img sobre un asset existente)
|
||||
## 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 que ya existe** y la transforman. El KSampler arranca
|
||||
del latente de la imagen base (LoadImage → VAEEncode), no de ruido, así que con `denoise`
|
||||
medio conserva la estructura del original mientras el prompt reescribe lo pedido. Cubren el
|
||||
eje que el critic de generación (`reports/0178`) no exploró: derivar de un asset, no inventar
|
||||
un tipo nuevo.
|
||||
ruido), estos parten de una **imagen de entrada** y la transforman. Dos 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
|
||||
conserva la estructura mientras el prompt reescribe material/paleta/tier. Conserva forma **y**
|
||||
color del original.
|
||||
- **sketch→ControlNet** (`sprite_from_sketch`): parte del **dibujo tosco** del dev (boceto,
|
||||
lineart, garabato); es `txt2img` (arranca de ruido) pero condicionado por un ControlNet atado
|
||||
al mapa de líneas del dibujo. Conserva solo la **forma**; la IA pone material/color/acabado.
|
||||
|
||||
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.
|
||||
|
||||
| ID | Firma corta | Qué hace |
|
||||
|---|---|---|
|
||||
| `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. |
|
||||
|
||||
## Funciones de post-proceso y puente (`gamedev-2d`, CPU)
|
||||
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
---
|
||||
name: comfyui_build_sprite_from_sketch_workflow
|
||||
kind: function
|
||||
lang: py
|
||||
domain: ml
|
||||
purity: pure
|
||||
version: 1.0.0
|
||||
signature: "def comfyui_build_sprite_from_sketch_workflow(sketch_image: str, subject: str, *, control_type: str = \"lineart\", checkpoint: str = \"dreamshaper_8.safetensors\", style: str = \"game asset, clean, centered\", strength: float = 0.8, size: int = 512, seed: int = 0, lora: str | None = None, lora_strength: float = 1.0, preprocess: bool = True, controlnet_name: str | None = None, negative: str | None = None, steps: int = 28, cfg: float = 7.0, sampler_name: str = \"dpmpp_2m\", scheduler: str = \"karras\", filename_prefix: str = \"sprite_from_sketch\") -> dict"
|
||||
description: "Construye el dict (API format) del workflow ComfyUI que PINTA un sprite a partir del BOCETO del dev guiado por ControlNet: recibe el dibujo tosco (boceto, lineart o garabato) + un prompt de QUE es, y genera un sprite en estilo de juego que CONSERVA la forma dibujada. Es el tercer eje del catalogo gamedev-2d, distinto de txt2img (enemy_creature, item_icon: inventan la forma desde texto en blanco) y de img2img (asset_variant: reescribe una imagen YA pintada): aqui el dev marca la silueta con su dibujo y la IA pone material/color/acabado. Mecanismo: txt2img base (ruido, denoise 1.0) condicionado por un ControlNet atado al mapa de lineas del boceto. control_type elige el preprocesador (LineArtPreprocessor / ScribblePreprocessor / CannyEdgePreprocessor) y, por defecto, el modelo CN emparejado. Compone comfyui_build_txt2img_workflow + comfyui_inject_controlnet + comfyui_inject_lora (estilo opcional); el unico codigo propio es el helper que interpone el preprocesador entre el boceto y el ControlNet. Pura, sin red ni I/O. class_types verificados contra /object_info (8GB lowvram)."
|
||||
tags: [comfyui, ml, gamedev-2d, controlnet, sketch, lineart, scribble, canny, sprite, asset-transform, stable-diffusion, workflow]
|
||||
uses_functions: [comfyui_build_txt2img_workflow_py_ml, comfyui_inject_controlnet_py_ml, comfyui_inject_lora_py_ml]
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: ""
|
||||
params:
|
||||
- name: sketch_image
|
||||
desc: "Nombre del archivo del boceto dentro de la carpeta input/ del servidor ComfyUI (lo carga el nodo LoadImage). Subelo antes con POST /upload/image o copialo a ~/ComfyUI/input/. Puede ser un boceto a lapiz, un lineart limpio o un garabato; el preprocesador lo normaliza a un mapa de lineas. No puede estar vacio."
|
||||
- name: subject
|
||||
desc: "Descripcion de QUE representa el boceto, para que la IA sepa que pintar sobre la forma (ej. 'armored knight', 'treasure chest', 'fire goblin'). Es el prompt positivo: NO inventa la silueta (eso lo fija el ControlNet), pone material, color y acabado. No puede estar vacio."
|
||||
- name: control_type
|
||||
desc: "Preprocesador y modelo ControlNet a usar. 'lineart' (default, LineArtPreprocessor) para dibujos a lapiz/tinta; 'scribble' (ScribblePreprocessor) para garabatos sueltos con mas libertad a la IA; 'canny' (CannyEdgePreprocessor) para bocetos con bordes nitidos y es el unico cuyo modelo CN dedicado esta instalado en el server 8GB actual (ver Gotchas). keyword-only."
|
||||
- name: checkpoint
|
||||
desc: "Checkpoint del servidor. 'dreamshaper_8.safetensors' (SD1.5, holgado en 8GB lowvram) por defecto. keyword-only."
|
||||
- name: style
|
||||
desc: "Descriptor de estilo que mantiene coherentes los sprites de un set (ej. 'game asset, clean, centered', 'dark fantasy creature', 'pixel art'). Mismo style + checkpoint + (lora) para todos los sprites del mismo juego. keyword-only."
|
||||
- name: strength
|
||||
desc: "Fuerza del ControlNet (cuanto respeta el boceto). 0.0 = ignora el dibujo (txt2img puro); 1.0 = se ciñe estrictamente a las lineas; 0.8 (recomendado) deja algo de margen a la IA. Se clampa a [0.0, 2.0]. keyword-only."
|
||||
- name: size
|
||||
desc: "Lado en px del sprite generado (EmptyLatentImage size x size). El preprocesador normaliza el boceto a esta resolucion. 512 por defecto (SD1.5). 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: preprocess
|
||||
desc: "Si True (default), inserta el preprocesador del control_type entre el boceto y el ControlNet (recomendado: el boceto crudo casi nunca es un mapa de control limpio). Si False, el boceto se pasa DIRECTO al ControlNet como mapa de lineas (util solo cuando sketch_image YA es un lineart blanco-sobre-negro limpio). keyword-only."
|
||||
- name: controlnet_name
|
||||
desc: "Override explicito del modelo ControlNet en models/controlnet. None = usa el modelo emparejado al control_type. Pasa 'control_v11p_sd15_canny_fp16.safetensors' aqui para degradar lineart/scribble al modelo canny disponible en el server actual (ver Gotchas). keyword-only."
|
||||
- name: negative
|
||||
desc: "Prompt negativo. None usa el negativo por defecto pensado para sprites (una figura, fondo limpio, sin texto). 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: txt2img base (CheckpointLoaderSimple '4', EmptyLatentImage '5', CLIPTextEncode '6'/'7', KSampler '3' denoise 1.0, VAEDecode '8', SaveImage '9') + rama ControlNet (LoadImage del boceto -> [Preprocessor del control_type si preprocess] -> ControlNetApply -> KSampler.positive, con ControlNetLoader del modelo CN) + LoraLoader si lora. Es UN sprite; varios objetos del mismo set -> llamar por subject/sketch_image con el mismo style/checkpoint/(lora)."
|
||||
tested: false
|
||||
file_path: python/functions/ml/comfyui_build_sprite_from_sketch_workflow.py
|
||||
---
|
||||
|
||||
Construye el dict (API format) del workflow ComfyUI que **pinta un sprite a partir del
|
||||
dibujo del dev**, guiado por ControlNet. Es el **tercer eje** del catálogo `gamedev-2d`:
|
||||
|
||||
| Eje | Builders | Punto de partida |
|
||||
|---|---|---|
|
||||
| **txt2img** | `enemy_creature`, `item_icon`, `ui_hud`... | TEXTO en blanco — la IA inventa la forma desde ruido |
|
||||
| **img2img** | `asset_variant` | una imagen YA pintada — la reescribe (material/tier) conservando composición |
|
||||
| **sketch→ControlNet** | **este builder** | el DIBUJO TOSCO del dev — lo pinta conservando la silueta dibujada |
|
||||
|
||||
El dolor real que cubre: el dev hace un boceto/lineart de un personaje u objeto y quiere que
|
||||
la IA lo pinte en estilo de juego **manteniendo la forma**. El dev marca la silueta; la IA
|
||||
pone material, color y acabado.
|
||||
|
||||
Mecanismo: es `txt2img` (arranca de ruido, `EmptyLatentImage`, `denoise 1.0`) pero su
|
||||
condicionamiento positivo pasa por un `ControlNetApply` atado al mapa de líneas extraído del
|
||||
boceto. El resultado tiene la **silueta y estructura del dibujo**, pintadas con el prompt de
|
||||
`subject` + `style`.
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```python
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.join("python", "functions"))
|
||||
from ml.comfyui_build_sprite_from_sketch_workflow import comfyui_build_sprite_from_sketch_workflow
|
||||
|
||||
# Boceto 'knight_sketch.png' ya subido al input/ del servidor (POST /upload/image).
|
||||
wf = comfyui_build_sprite_from_sketch_workflow(
|
||||
"knight_sketch.png",
|
||||
"armored knight, fantasy hero",
|
||||
control_type="canny", # canny = preproc + modelo, todo instalado hoy
|
||||
style="game asset, clean, centered, painted",
|
||||
strength=0.85,
|
||||
size=512,
|
||||
seed=123,
|
||||
)
|
||||
# wf -> comfyui_submit_workflow(wf) -> comfyui_wait_result(prompt_id)
|
||||
# El sprite resultante conserva la silueta del boceto, repintada en estilo de juego.
|
||||
```
|
||||
|
||||
Probado end-to-end (27/06/2026): boceto del goblin → `CannyEdgePreprocessor` → ControlNet
|
||||
canny → sprite que respeta pose, orejas, hombrera, lanza dentada y espada del dibujo.
|
||||
`prompt_id ea6fc372-0467-4b4e-9927-069419633eab`, salida `sprite_from_sketch_test_00001_.png`.
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
- Cuando el dev TIENE un dibujo (boceto, lineart, garabato) y quiere que la IA lo pinte
|
||||
respetando la forma — no inventar desde cero (`txt2img`) ni reescribir un asset ya pintado
|
||||
(`img2img` / `asset_variant`).
|
||||
- Para iterar diseño: dibujas la silueta a mano, la IA prueba 5 acabados distintos (varía
|
||||
`seed`/`style`/`lora`) sobre la MISMA forma.
|
||||
- Elige `control_type`: `lineart` para dibujos a lápiz/tinta, `scribble` para garabatos sueltos
|
||||
(más libertad), `canny` para bocetos con bordes nítidos (y único 100% instalado hoy).
|
||||
- Sube el set de un juego usando el mismo `style` + `checkpoint` + (`lora`) para coherencia.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **El server 8GB lowvram actual SOLO tiene los modelos ControlNet SD1.5 `canny` / `depth` /
|
||||
`openpose`.** Los modelos dedicados `control_v11p_sd15_lineart_fp16` y
|
||||
`control_v11p_sd15_scribble_fp16` NO están instalados (pendiente humano descargarlos a
|
||||
`~/ComfyUI/models/controlnet/`). Mientras tanto, para `control_type="lineart"`/`"scribble"`:
|
||||
o pasa `controlnet_name="control_v11p_sd15_canny_fp16.safetensors"` como override (el modelo
|
||||
canny tolera mapas de líneas de cualquier preprocesador, con algo menos de fidelidad que el
|
||||
dedicado), o usa `control_type="canny"` que funciona out-of-the-box. Los **preprocesadores**
|
||||
(LineArt/Scribble/Canny) SÍ están todos instalados (custom node `comfyui_controlnet_aux`).
|
||||
- La función es **pura**: NO consulta el servidor ni valida que `sketch_image`, `checkpoint`,
|
||||
`controlnet_name` o `lora` existan. Si el modelo CN falta, ComfyUI rechaza el workflow con
|
||||
HTTP 400 al enviarlo (`comfyui_submit_workflow` propaga el detalle por nodo). Valida con
|
||||
`/object_info` antes de enviar a un server desconocido.
|
||||
- `preprocess=False` solo tiene sentido si `sketch_image` YA es un mapa de líneas limpio
|
||||
(blanco-sobre-negro). Con un boceto a lápiz crudo y `preprocess=False`, el ControlNet recibe
|
||||
una foto, no líneas, y el resultado se degrada.
|
||||
- `strength` alto (~1.0) se ciñe tanto al boceto que puede arrastrar artefactos del dibujo;
|
||||
~0.8 deja a la IA limpiar. `strength=0` desactiva el ControlNet (vuelve a txt2img puro).
|
||||
- Es `txt2img` guiado (genera desde ruido respetando líneas), NO `img2img`: el sprite NO hereda
|
||||
los colores del boceto, solo su FORMA. Para conservar también los colores de una imagen ya
|
||||
pintada, usa `comfyui_build_asset_variant_workflow` (img2img).
|
||||
@@ -0,0 +1,331 @@
|
||||
"""Construye el workflow ComfyUI que PINTA un sprite a partir del BOCETO del dev (ControlNet).
|
||||
|
||||
Eje distinto al de los builders gamedev hermanos:
|
||||
|
||||
- txt2img (enemy_creature, item_icon, ui_hud...) -> parte de TEXTO en blanco: la IA
|
||||
inventa la forma desde ruido. No hay dibujo de partida.
|
||||
- img2img (asset_variant) -> parte de una imagen YA PINTADA y la reescribe (material,
|
||||
paleta, tier) conservando la composicion via denoise medio.
|
||||
- sketch -> ControlNet (ESTE builder) -> parte del DIBUJO TOSCO del dev (un boceto,
|
||||
un lineart a lapiz, un garabato) y lo PINTA en estilo de juego RESPETANDO las lineas
|
||||
dibujadas. El dev marca la forma; la IA pone el material, el color y el acabado.
|
||||
|
||||
Es txt2img guiado por ControlNet: el KSampler arranca de ruido (EmptyLatentImage, denoise
|
||||
1.0, como txt2img) pero su condicionamiento positivo pasa por un ControlNetApply que lo
|
||||
ata al mapa de lineas extraido del boceto. El resultado tiene la SILUETA y la estructura
|
||||
del dibujo del dev, pintadas con el prompt de `subject` + `style`.
|
||||
|
||||
Cableado:
|
||||
|
||||
CheckpointLoaderSimple -> [LoraLoader opcional] -> KSampler.model
|
||||
EmptyLatentImage ----------------------------------> KSampler.latent (ruido, txt2img)
|
||||
CLIPTextEncode(positive) -> ControlNetApply.conditioning
|
||||
LoadImage(boceto) -> [Preprocessor lineart/scribble/canny] -> ControlNetApply.image
|
||||
ControlNetLoader(modelo CN) -> ControlNetApply.control_net
|
||||
ControlNetApply -> KSampler.positive
|
||||
KSampler -> VAEDecode -> SaveImage
|
||||
|
||||
Compone (registry-first, sin reescribir nada):
|
||||
- comfyui_build_txt2img_workflow -> base txt2img (ruido + KSampler denoise 1.0)
|
||||
- comfyui_inject_controlnet -> rama LoadImage + ControlNetLoader + ControlNetApply
|
||||
repuntando KSampler.positive
|
||||
- comfyui_inject_lora -> LoRA de estilo opcional (consistencia con el set)
|
||||
|
||||
El unico codigo propio es `_inject_preprocessor`: inserta el nodo preprocesador
|
||||
(LineArtPreprocessor / ScribblePreprocessor / CannyEdgePreprocessor) entre el LoadImage
|
||||
del boceto y el ControlNetApply.image, de modo que el ControlNet reciba un MAPA DE LINEAS
|
||||
limpio en lugar del boceto crudo. Es el analogo a `_inject_image_scale` del builder
|
||||
hermano comfyui_build_asset_variant_workflow (mismo patron de helper privado especifico
|
||||
del builder).
|
||||
|
||||
`control_type` selecciona el preprocesador y, por defecto, el modelo ControlNet emparejado:
|
||||
|
||||
control_type preprocesador (extrae lineas) modelo CN ideal
|
||||
----------- ----------------------------- ---------------------------------------
|
||||
lineart LineArtPreprocessor control_v11p_sd15_lineart_fp16.safetensors
|
||||
scribble ScribblePreprocessor control_v11p_sd15_scribble_fp16.safetensors
|
||||
canny CannyEdgePreprocessor control_v11p_sd15_canny_fp16.safetensors
|
||||
|
||||
`canny` se elige cuando el boceto tiene bordes nitidos; `lineart` para dibujos a lapiz/tinta;
|
||||
`scribble` para garabatos sueltos donde se quiere que la IA tenga mas libertad de interpretacion.
|
||||
|
||||
GOTCHA del servidor 8GB lowvram actual: SOLO estan instalados los modelos ControlNet SD1.5
|
||||
canny / depth / openpose. Los modelos dedicados control_v11p_sd15_lineart_fp16 y
|
||||
control_v11p_sd15_scribble_fp16 NO estan presentes (pendiente humano descargarlos). Mientras
|
||||
tanto: usa `control_type="canny"` (preprocesador + modelo, todo instalado), o pasa
|
||||
`controlnet_name="control_v11p_sd15_canny_fp16.safetensors"` como override para lineart/scribble
|
||||
(el modelo canny tolera mapas de lineas de cualquier preprocesador, con algo menos de fidelidad
|
||||
que el modelo dedicado). Los PREPROCESADORES (LineArt/Scribble/Canny) SI estan todos instalados
|
||||
(custom node comfyui_controlnet_aux).
|
||||
|
||||
class_types/inputs verificados contra /object_info del servidor (8GB lowvram):
|
||||
CheckpointLoaderSimple, EmptyLatentImage, CLIPTextEncode, KSampler, VAEDecode, SaveImage,
|
||||
LoadImage, ControlNetLoader, ControlNetApply, LineArtPreprocessor, ScribblePreprocessor,
|
||||
CannyEdgePreprocessor, LoraLoader.
|
||||
|
||||
Funcion pura: sin red, sin I/O. No muta dicts de entrada (copia profunda al insertar el
|
||||
preprocesador). NO valida que sketch_image/checkpoint/controlnet_name/lora existan en el
|
||||
servidor (eso es responsabilidad de 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__), ".."))
|
||||
|
||||
# Mapa control_type -> (clase del preprocesador, modelo ControlNet ideal emparejado).
|
||||
# El preprocesador extrae el mapa de lineas del boceto; el modelo lo aplica al
|
||||
# condicionamiento. Ver el GOTCHA del modulo: en el server 8GB solo canny tiene
|
||||
# ambos lados instalados; lineart/scribble requieren override del modelo o descarga.
|
||||
_CONTROL_MAP = {
|
||||
"lineart": ("LineArtPreprocessor", "control_v11p_sd15_lineart_fp16.safetensors"),
|
||||
"scribble": ("ScribblePreprocessor", "control_v11p_sd15_scribble_fp16.safetensors"),
|
||||
"canny": ("CannyEdgePreprocessor", "control_v11p_sd15_canny_fp16.safetensors"),
|
||||
}
|
||||
|
||||
# Negativo por defecto para un sprite pintado desde boceto: UNA figura entera, bien
|
||||
# formada, fondo limpio, sin texto/marcas ni objetos extra. El ControlNet ya impone la
|
||||
# forma; el negativo solo limpia ruido tipico de SD1.5.
|
||||
_SKETCH_NEGATIVE = (
|
||||
"blurry, lowres, deformed, disfigured, bad anatomy, extra limbs, "
|
||||
"duplicate, multiple subjects, text, watermark, signature, logo, "
|
||||
"cluttered background, cropped, cut off, out of frame, jpeg artifacts"
|
||||
)
|
||||
|
||||
|
||||
def _inject_preprocessor(workflow: dict, *, control_type: str, resolution: int) -> dict:
|
||||
"""Inserta un nodo preprocesador entre el LoadImage del boceto y el ControlNetApply.
|
||||
|
||||
Tras comfyui_inject_controlnet, el ControlNetApply recibe la imagen directamente del
|
||||
LoadImage del boceto. Este helper interpone el preprocesador correspondiente al
|
||||
control_type (LineArt/Scribble/Canny), que convierte el boceto crudo en un mapa de
|
||||
lineas limpio, y repunta ControlNetApply.image a la salida del preprocesador.
|
||||
|
||||
Pura: trabaja sobre una copia profunda. Determinista.
|
||||
|
||||
Raises:
|
||||
ValueError: si no se encuentra ControlNetApply, o si el ControlNetApply no recibe
|
||||
su imagen de un LoadImage (cableado inesperado).
|
||||
"""
|
||||
wf = copy.deepcopy(workflow)
|
||||
|
||||
apply_id = next(
|
||||
(nid for nid, n in wf.items() if n.get("class_type") == "ControlNetApply"), None
|
||||
)
|
||||
if apply_id is None:
|
||||
raise ValueError(
|
||||
"comfyui_build_sprite_from_sketch_workflow: no se encontro ControlNetApply "
|
||||
"para insertar el preprocesador"
|
||||
)
|
||||
|
||||
# Fuente de la imagen de control que hoy alimenta el ControlNetApply (el LoadImage).
|
||||
src = wf[apply_id]["inputs"].get("image")
|
||||
if not (isinstance(src, list) and len(src) == 2):
|
||||
raise ValueError(
|
||||
"comfyui_build_sprite_from_sketch_workflow: ControlNetApply.image no apunta a "
|
||||
"un nodo (cableado inesperado del boceto)"
|
||||
)
|
||||
|
||||
preproc_class = _CONTROL_MAP[control_type][0]
|
||||
inputs: dict = {"image": list(src), "resolution": int(resolution)}
|
||||
if control_type == "lineart":
|
||||
inputs["coarse"] = "disable"
|
||||
elif control_type == "canny":
|
||||
inputs["low_threshold"] = 100
|
||||
inputs["high_threshold"] = 200
|
||||
# scribble no tiene parametros extra mas alla de resolution.
|
||||
|
||||
numeric = [int(k) for k in wf.keys() if str(k).isdigit()]
|
||||
preproc_id = str((max(numeric) + 1) if numeric else len(wf) + 1)
|
||||
wf[preproc_id] = {"class_type": preproc_class, "inputs": inputs}
|
||||
|
||||
# Repunta el ControlNetApply para que reciba el MAPA DE LINEAS preprocesado.
|
||||
wf[apply_id]["inputs"]["image"] = [preproc_id, 0]
|
||||
return wf
|
||||
|
||||
|
||||
def comfyui_build_sprite_from_sketch_workflow(
|
||||
sketch_image: str,
|
||||
subject: str,
|
||||
*,
|
||||
control_type: str = "lineart",
|
||||
checkpoint: str = "dreamshaper_8.safetensors",
|
||||
style: str = "game asset, clean, centered",
|
||||
strength: float = 0.8,
|
||||
size: int = 512,
|
||||
seed: int = 0,
|
||||
lora: str | None = None,
|
||||
lora_strength: float = 1.0,
|
||||
preprocess: bool = True,
|
||||
controlnet_name: str | None = None,
|
||||
negative: str | None = None,
|
||||
steps: int = 28,
|
||||
cfg: float = 7.0,
|
||||
sampler_name: str = "dpmpp_2m",
|
||||
scheduler: str = "karras",
|
||||
filename_prefix: str = "sprite_from_sketch",
|
||||
) -> dict:
|
||||
"""Construye el dict (API format) del workflow boceto->sprite guiado por ControlNet.
|
||||
|
||||
A partir del DIBUJO TOSCO del dev (boceto, lineart, garabato) + un prompt de QUE es,
|
||||
genera un sprite pintado en estilo de juego que CONSERVA la forma dibujada. Es txt2img
|
||||
(arranca de ruido) condicionado por un ControlNet atado al mapa de lineas del boceto.
|
||||
|
||||
Args:
|
||||
sketch_image: nombre del archivo del boceto dentro de la carpeta input/ del servidor
|
||||
ComfyUI (lo carga el nodo LoadImage). Subelo antes con POST /upload/image o
|
||||
copialo a ~/ComfyUI/input/. Puede ser un boceto a lapiz, un lineart limpio o un
|
||||
garabato; el preprocesador lo normaliza a un mapa de lineas. No puede estar vacio.
|
||||
subject: descripcion de QUE representa el boceto, para que la IA sepa que pintar
|
||||
sobre la forma (ej. "armored knight", "treasure chest", "fire goblin"). Es el
|
||||
prompt positivo: NO inventa la silueta (eso lo fija el ControlNet), pone material,
|
||||
color y acabado. No puede estar vacio.
|
||||
control_type: preprocesador y modelo ControlNet a usar. keyword-only.
|
||||
- "lineart" (default): LineArtPreprocessor. Para dibujos a lapiz/tinta.
|
||||
- "scribble": ScribblePreprocessor. Para garabatos sueltos; mas libertad a la IA.
|
||||
- "canny": CannyEdgePreprocessor. Para bocetos con bordes nitidos. Es el unico
|
||||
cuyo modelo CN dedicado esta instalado en el server 8GB actual (ver GOTCHA del
|
||||
modulo); funciona out-of-the-box.
|
||||
checkpoint: checkpoint del servidor. 'dreamshaper_8.safetensors' (SD1.5, holgado en
|
||||
8GB lowvram) por defecto. keyword-only.
|
||||
style: descriptor de estilo que mantiene coherentes los sprites de un set (ej.
|
||||
"game asset, clean, centered", "dark fantasy creature", "pixel art"). Mismo style
|
||||
+ checkpoint + (lora) para todos los sprites del mismo juego. keyword-only.
|
||||
strength: fuerza del ControlNet (cuanto respeta el boceto). 0.0 = ignora el dibujo
|
||||
(txt2img puro); 1.0 = se ciñe estrictamente a las lineas. 0.8 (recomendado) deja
|
||||
algo de margen a la IA para interpretar. Se clampa a [0.0, 2.0]. keyword-only.
|
||||
size: lado en px del sprite generado (EmptyLatentImage size x size). El preprocesador
|
||||
normaliza el boceto a esta resolucion. 512 por defecto (SD1.5). 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.
|
||||
preprocess: si True (default), inserta el preprocesador del control_type entre el
|
||||
boceto y el ControlNet (recomendado: el boceto crudo casi nunca es un mapa de
|
||||
control limpio). Si False, el boceto se pasa DIRECTO al ControlNet como mapa de
|
||||
lineas (util solo cuando sketch_image YA es un lineart blanco-sobre-negro limpio).
|
||||
keyword-only.
|
||||
controlnet_name: override explicito del modelo ControlNet en models/controlnet. None
|
||||
= usa el modelo emparejado al control_type segun _CONTROL_MAP. Pasa aqui
|
||||
'control_v11p_sd15_canny_fp16.safetensors' para degradar lineart/scribble al
|
||||
modelo canny disponible (ver GOTCHA del modulo). keyword-only.
|
||||
negative: prompt negativo. None usa el negativo por defecto pensado para sprites
|
||||
(una figura, fondo limpio, sin texto). keyword-only.
|
||||
steps, cfg, sampler_name, scheduler, filename_prefix: parametros de generacion.
|
||||
keyword-only.
|
||||
|
||||
Returns:
|
||||
dict en API format listo para comfyui_submit_workflow: txt2img base + rama ControlNet
|
||||
(LoadImage del boceto -> [Preprocessor si preprocess] -> ControlNetApply -> KSampler.
|
||||
positive) + LoRA de estilo opcional. Es UN sprite; varios objetos del mismo set ->
|
||||
llamar por `subject`/`sketch_image` con el mismo style/checkpoint/(lora).
|
||||
|
||||
Raises:
|
||||
ValueError: si sketch_image o subject estan vacios, o si control_type no es uno de
|
||||
'lineart' / 'scribble' / 'canny'.
|
||||
"""
|
||||
from ml.comfyui_build_txt2img_workflow import comfyui_build_txt2img_workflow
|
||||
from ml.comfyui_inject_controlnet import comfyui_inject_controlnet
|
||||
|
||||
if not sketch_image or not sketch_image.strip():
|
||||
raise ValueError(
|
||||
"comfyui_build_sprite_from_sketch_workflow: 'sketch_image' no puede estar vacio "
|
||||
"(el ControlNet necesita el boceto en input/)."
|
||||
)
|
||||
if not subject or not subject.strip():
|
||||
raise ValueError(
|
||||
"comfyui_build_sprite_from_sketch_workflow: 'subject' no puede estar vacio "
|
||||
"(la IA necesita saber QUE pintar sobre la forma del boceto)."
|
||||
)
|
||||
if control_type not in _CONTROL_MAP:
|
||||
raise ValueError(
|
||||
"comfyui_build_sprite_from_sketch_workflow: control_type "
|
||||
f"'{control_type}' invalido; usa uno de {sorted(_CONTROL_MAP)}."
|
||||
)
|
||||
|
||||
sketch_image = sketch_image.strip()
|
||||
subject = subject.strip()
|
||||
strength = max(0.0, min(2.0, float(strength)))
|
||||
lora_strength = max(0.0, min(2.0, float(lora_strength)))
|
||||
neg = _SKETCH_NEGATIVE if negative is None else negative
|
||||
cn_model = controlnet_name or _CONTROL_MAP[control_type][1]
|
||||
|
||||
# Prompt positivo: describe el sujeto y el estilo; la SILUETA la impone el ControlNet,
|
||||
# asi que aqui solo se empuja material/acabado y "single subject, full body".
|
||||
positive = (
|
||||
f"{subject}, {style}, single subject, full body, "
|
||||
"painted, polished, high detail, consistent design"
|
||||
)
|
||||
|
||||
wf = comfyui_build_txt2img_workflow(
|
||||
checkpoint,
|
||||
positive,
|
||||
neg,
|
||||
steps=steps,
|
||||
cfg=cfg,
|
||||
width=size,
|
||||
height=size,
|
||||
seed=seed,
|
||||
sampler_name=sampler_name,
|
||||
scheduler=scheduler,
|
||||
filename_prefix=filename_prefix,
|
||||
)
|
||||
|
||||
# Rama ControlNet: LoadImage(boceto) + ControlNetLoader + ControlNetApply, repuntando
|
||||
# KSampler.positive. Reutiliza el inyector encadenable del registry.
|
||||
wf = comfyui_inject_controlnet(wf, sketch_image, cn_model, strength=strength)
|
||||
|
||||
# Interpone el preprocesador del control_type entre el boceto y el ControlNet.
|
||||
if preprocess:
|
||||
wf = _inject_preprocessor(wf, control_type=control_type, resolution=size)
|
||||
|
||||
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_sprite_from_sketch_workflow(
|
||||
"knight_sketch.png",
|
||||
"armored knight, fantasy hero",
|
||||
control_type="lineart",
|
||||
style="game asset, clean, centered",
|
||||
strength=0.8,
|
||||
seed=7,
|
||||
)
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"nodes": list(wf),
|
||||
"classes": sorted({n["class_type"] for n in wf.values()}),
|
||||
"control_type": "lineart",
|
||||
"cn_model": next(
|
||||
n["inputs"]["control_net_name"]
|
||||
for n in wf.values()
|
||||
if n["class_type"] == "ControlNetLoader"
|
||||
),
|
||||
"preprocessor": next(
|
||||
n["class_type"]
|
||||
for n in wf.values()
|
||||
if n["class_type"].endswith("Preprocessor")
|
||||
),
|
||||
"strength": next(
|
||||
n["inputs"]["strength"]
|
||||
for n in wf.values()
|
||||
if n["class_type"] == "ControlNetApply"
|
||||
),
|
||||
"positive": wf["6"]["inputs"]["text"],
|
||||
},
|
||||
indent=2,
|
||||
)
|
||||
)
|
||||
Reference in New Issue
Block a user