diff --git a/docs/capabilities/gamedev-2d.md b/docs/capabilities/gamedev-2d.md index 2675ede9..d34831a1 100644 --- a/docs/capabilities/gamedev-2d.md +++ b/docs/capabilities/gamedev-2d.md @@ -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) diff --git a/python/functions/ml/comfyui_build_sprite_from_sketch_workflow.md b/python/functions/ml/comfyui_build_sprite_from_sketch_workflow.md new file mode 100644 index 00000000..8a513a71 --- /dev/null +++ b/python/functions/ml/comfyui_build_sprite_from_sketch_workflow.md @@ -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). diff --git a/python/functions/ml/comfyui_build_sprite_from_sketch_workflow.py b/python/functions/ml/comfyui_build_sprite_from_sketch_workflow.py new file mode 100644 index 00000000..cc579409 --- /dev/null +++ b/python/functions/ml/comfyui_build_sprite_from_sketch_workflow.py @@ -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, + ) + )