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:
2026-06-27 04:31:41 +02:00
parent 1585e986c1
commit 1012355998
3 changed files with 478 additions and 6 deletions
+14 -6
View File
@@ -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 BR 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,
)
)