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
@@ -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,
)
)