diff --git a/docs/capabilities/gamedev-2d.md b/docs/capabilities/gamedev-2d.md index fcdcbffc..26e67a8b 100644 --- a/docs/capabilities/gamedev-2d.md +++ b/docs/capabilities/gamedev-2d.md @@ -109,6 +109,12 @@ ruido), estos parten de una **imagen de entrada** y la transforman. Cuatro sub-e varios lados; el nodo `ImagePadForOutpaint` extiende el canvas **y genera** la máscara feathered de la franja nueva (no la recibe el caller), y el sampler genera ahí contenido coherente. Cambia el **tamaño** del asset (recortar/extender un fondo o parallax a otra resolución/aspect), no lo de dentro. +- **multi-vista 3D / 2.5D** (`directional_sprite`): parte del sprite **frontal** de un personaje y lo + **rota en 3D** (SV3D turntable u Stable Zero123 órbita) para producir N vistas direccionales del MISMO + personaje (8-way N/NE/E/SE/S/SW/W/NW o 4-way). A diferencia de `sprite_sheet` (re-poza con OpenPose 2D, + re-dibuja la silueta → identidad inconsistente entre ángulos), aquí la difusión 3D gira la figura sobre + su eje, así casco/arma/paleta son los mismos en cada dirección (**consistencia rotacional**). Cambia el + **ángulo de cámara**, no la pose ni el material. 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. @@ -118,6 +124,7 @@ del dibujo del dev, no inventar un tipo nuevo desde texto. | `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. | | `comfyui_build_inpaint_asset_workflow_py_ml` | `(input_image, mask_image, prompt, *, checkpoint="dreamshaper_8…", denoise=1.0, style="game asset", grow_mask=6, size=None, seed=0, lora=None, mode="vae_encode", …) -> dict` | EDITA **solo una región** de un asset 2D ya pintado (**inpaint**, sub-eje propio). Recibe el asset en `input_image` + una **máscara** `mask_image` (BLANCO = editar, NEGRO = conservar) + `prompt` de qué poner ahí, y repinta **únicamente** la zona enmascarada dejando el resto del sprite intacto (cambiar/añadir un arma, quitar un casco, poner un escudo, reparar una zona dañada). Mecanismo (`mode="vae_encode"`): `VAEEncodeForInpaint` codifica el latente respetando la máscara y dilata su borde `grow_mask` px para difuminar la costura; `KSampler` (`denoise` alto) regenera solo esa región con `{prompt}, {style}, seamless blend…`. Compone `comfyui_build_inpaint_workflow` (base) + `comfyui_inject_lora` (estilo opcional); `size` escala imagen **Y** máscara de forma consistente (escalar solo una las desalinea). **`grow_mask` es la palanca de costura** (6-10 px difumina el borde lo/nuevo); `denoise` 1.0 reescribe entero, ~0.5-0.7 repara suave. **DISTINTO de `asset_variant`** (img2img reescribe TODO el asset) y de `sprite_from_sketch` (ControlNet parte de un dibujo de líneas para un sprite nuevo): éste edita **un trozo** delimitado por la máscara. **ERROR-PATH**: si el server no expone `VAEEncodeForInpaint`, pasar `mode="noise_mask"` → degrada a `VAEEncode` + `SetLatentNoiseMask` (+ `GrowMask`); `mask_image` vacío lanza `ValueError`. ⚠️ asset y máscara deben existir en `input/` (subir con `POST /upload/image`) y compartir resolución (o usar `size`); `ImageScale` aquí NO ofrece `lanczos` (válidos `bilinear`/`nearest-exact`/`area`/`bicubic`); pura, no valida. Probado e2e en GPU con SD1.5 — máscara circular (R70) sobre la mano del goblin `enemy_creature_00001_.png`, `prompt="a glowing blue magic orb"` grow_mask 8 denoise 1.0 seed 7 (`prompt_id 88b52c66`): orbe azul en la región, **resto idéntico** (diff medio dentro 40.3 vs fuera 1.97 → ratio 20.4×; 44.6% px cambiados dentro vs 1.7% fuera), `reports/0183`. SD1.5. | +| `comfyui_build_directional_sprite_workflow_py_ml` | `(input_image, *, directions=8, model="sv3d", elevation=0.0, size=None, orbit_frames=None, seed=0, ckpt=None, …) -> dict` | UN **sprite MULTI-DIRECCIONAL** del MISMO personaje rotado en 3D (**multi-vista 2.5D**, sub-eje propio): parte de la imagen **frontal** del personaje (fondo limpio, en `input/`) y construye el workflow que genera N vistas direccionales CONSISTENTES (8-way N/NE/E/SE/S/SW/W/NW o 4-way) para top-down/iso/shooter 8-way. `model="sv3d"` (default) = `SV3D_Conditioning` produce un **orbit turntable** de N frames equiespaciados en 360° en una pasada (mejor consistencia, `sv3d_p.safetensors`, nativo 576²); `model="zero123"` = `StableZero123_Conditioning_Batched` da un **batch** de N vistas por azimuth (fallback menor VRAM, `stable_zero123.ckpt`, nativo 256²). `elevation` (~15-30) da picado para cámara cenital; `orbit_frames` (SOLO sv3d) densifica el orbit (21 nativo) para submuestrear; el módulo expone `directional_sprite_view_order(directions)` (frame i = dirección i). **DISTINTO de `sprite_sheet`** (OpenPose 2D re-poza la silueta → identidad inconsistente): aquí la difusión 3D ROTA la figura sobre su eje → casco/arma/paleta idénticos en cada dirección (rotación 3D real, no re-dibujo). Construye, NO genera (el coste GPU es el `submit`); **pura, no valida** (la imagen frontal debe existir en `input/`). Hermana **pura** de `comfyui_generate_views_from_image` (orquestador impuro para recon 3D, 4 cardinales). ⚠️ VRAM RTX 3070 8GB: SV3D es modelo de vídeo, pesa — 8 frames@576² → pico **7145 MiB**; limpiar GPU antes (`POST /free`); OOM → baja `size`/`directions` o cae a zero123, NO matar procesos; `comfyui_wait_result` lanza `TimeoutError` pero el job completa (sondear `/history`). Probado e2e en GPU con SV3D — goblin `enemy_creature_00001_.png` (compuesto sobre blanco 576²) → 8 direcciones elevation 15 seed 7, **8 frames** 576² en 75 s, consistencia rotacional medida (MAE adyacentes 27 < frente↔espalda 29.6, spread de paleta 3.83 = mismo personaje en las 8 vistas; `prompt_id 8b9f75de`, `reports/0187`). SV3D/Zero123. | | `comfyui_build_outpaint_asset_workflow_py_ml` | `(input_image, prompt, *, left=0, right=0, top=0, bottom=0, feather=40, checkpoint="dreamshaper_8…", denoise=1.0, style="game background", grow_mask=0, seed=0, lora=None, …) -> dict` | EXTIENDE **el lienzo** de un asset 2D ya pintado (**outpaint**, sub-eje propio). Recibe el asset en `input_image` + cuánto extender por cada lado (`left`/`right`/`top`/`bottom` px) + `prompt` de qué generar fuera de los bordes, y **agranda el canvas** generando contenido coherente con el original más allá de sus bordes (recortar/extender un fondo, parallax, card_art o splash a otra resolución/aspect ratio). Mecanismo: el nodo nativo `ImagePadForOutpaint` amplía el lienzo y **EMITE** a la vez la imagen extendida **y** la máscara feathered de la franja nueva (la genera el grafo, **NO** la recibe el caller); `VAEEncodeForInpaint` codifica respetando esa máscara y `KSampler` (`denoise` alto) genera lo nuevo con `{prompt}, {style}, seamless extension…`. Compone `comfyui_build_inpaint_workflow` (base; su `LoadImageMask` se elimina y `VAEEncodeForInpaint` se reconecta a las dos salidas del pad) + `comfyui_inject_lora` (estilo opcional). **`feather` difumina la costura** (40 px por defecto, no debe pasarse de la extensión); `grow_mask` (0 por defecto) dilata adicionalmente el borde si aparece costura dura. **DISTINTO de `inpaint_asset`**: éste **no recibe máscara** (la genera el pad) y cambia el **tamaño** del asset extendiendo hacia fuera, mientras inpaint edita una región **interior** con máscara externa del mismo tamaño. **ERROR-PATH**: `input_image`/`prompt` vacíos o las cuatro extensiones en 0 tras redondear (`left=3`→0) lanzan `ValueError`; si el server no expone `ImagePadForOutpaint`, consultar `/object_info`. ⚠️ el asset debe existir en `input/` (subir con `POST /upload/image`); las extensiones se redondean a múltiplo de 8 (`250→248`); pura, no valida. Probado e2e en GPU con SD1.5 — fondo `seamless_00004_.png` 512×512 extendido `right=256` feather 40 denoise 1.0 seed 7 (`prompt_id aa33de05`): canvas **512→768×512** (+256), original conservado (diff medio 7.2 lejos del borde) + franja nueva con contenido coherente (std 28.9, dist de paleta 28.6), `reports/0185`. SD1.5. | ## Funciones de post-proceso y puente (`gamedev-2d`, CPU) @@ -201,9 +208,12 @@ comfyui_export_asset_to_godot(rgba["out_path"], "vfx", PROJ) un pack entero compartiendo checkpoint/style/lora/seed (issue 0087, ver tabla de pipelines arriba). One-shots por-asset individuales (pixelart/sprite/VFX) siguen encadenándose a mano; candidatos a promoción cuando el patrón se repita. -- **Sprite turnaround multi-vista** (orquestar N poses con identidad fija + juez): - el builder `comfyui_build_sprite_sheet_workflow` produce UN frame; la orquestación - multi-pose es pipeline pendiente (plan `reports/0137` T2). +- **Sprite turnaround multi-vista** (N direcciones del mismo personaje con identidad fija): + cubierto por `comfyui_build_directional_sprite_workflow` (rotación 3D SV3D/Zero123, + consistencia rotacional medida — `reports/0187`). Lo que sigue **pendiente** es la + orquestación multi-POSE 2D con juez (re-pozar un personaje en N acciones manteniendo + identidad, distinto de rotarlo): `comfyui_build_sprite_sheet_workflow` produce UN frame; + el pipeline multi-pose con juez sigue pendiente (plan `reports/0137` T2). - **Paletas lospec por red** (`load_lospec_palette`): no incluido. `pixelize` usa paletas fijas embebidas (game-boy/pico-8/nes) o lista de hex, sin HTTP. - **TileSet / SpriteFrames `.tres`**: Godot no los deriva solos; `export_asset_to_godot` diff --git a/python/functions/ml/comfyui_build_directional_sprite_workflow.md b/python/functions/ml/comfyui_build_directional_sprite_workflow.md new file mode 100644 index 00000000..b790424a --- /dev/null +++ b/python/functions/ml/comfyui_build_directional_sprite_workflow.md @@ -0,0 +1,134 @@ +--- +name: comfyui_build_directional_sprite_workflow +kind: function +lang: py +domain: ml +version: "1.0.0" +purity: pure +signature: "def comfyui_build_directional_sprite_workflow(input_image: str, *, directions: int = 8, model: str = \"sv3d\", elevation: float = 0.0, size: int | None = None, orbit_frames: int | None = None, seed: int = 0, ckpt: str | None = None, steps: int = 20, cfg: float | None = None, min_cfg: float = 1.0, sampler_name: str = \"euler\", scheduler: str = \"karras\", filename_prefix: str = \"directional_sprite\") -> dict" +description: "Construye el dict (API format) del workflow ComfyUI de un sprite MULTI-DIRECCIONAL: a partir de UNA imagen frontal de un personaje (fondo limpio) genera N vistas direccionales CONSISTENTES (el mismo personaje rotado en 3D, no re-dibujado) para un juego top-down / isométrico / shooter 8-way. Dos modelos 3D nativos del server: SV3D (orbit turntable en una pasada, mejor consistencia, sv3d_p.safetensors) o Stable Zero123 (batch de vistas por azimuth, fallback de menor VRAM, stable_zero123.ckpt). Es el puente 2.5D que faltaba en gamedev-2d: a diferencia de sprite_sheet (re-poza con OpenPose 2D, identidad inconsistente entre angulos), aqui la difusion 3D ROTA la figura sobre su eje, asi casco/arma/paleta son los mismos en cada direccion. Funcion pura: solo construye el grafo (class_types/inputs verificados contra /object_info); el coste GPU esta al enviar con comfyui_submit_workflow. La imagen frontal debe existir ya en el input/ del servidor." +tags: [comfyui, ml, gamedev-2d, sprite, directional, sv3d, zero123, turntable, multiview, stable-diffusion, workflow] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [] +params: + - name: input_image + desc: "Nombre del archivo de la imagen frontal del personaje dentro del input/ del servidor ComfyUI (lo carga LoadImage). Debe existir ya (subir con POST /upload/image o copiar a ~/ComfyUI/input/). Idealmente personaje centrado, encuadrado entero, mirando a camara, sobre fondo limpio (SV3D/Zero123 rinden mucho mejor sin fondo). No puede estar vacio." + - name: directions + desc: "Numero de direcciones a generar. 8 (default) = 8-way N/NE/E/SE/S/SW/W/NW (shooter 8-direcciones, top-down/iso); 4 = N/E/S/W (RPG clasico). Cualquier N>=1 vale (vistas equiespaciadas a 360/N grados); 4 y 8 reciben nombres cardinales via directional_sprite_view_order. keyword-only." + - name: model + desc: "'sv3d' (default, orbita turntable en una pasada, mejor consistencia angular, checkpoint sv3d_p.safetensors) o 'zero123' (batch de vistas con control de azimuth, fallback de menor VRAM, checkpoint stable_zero123.ckpt, nativo 256px). keyword-only." + - name: elevation + desc: "Elevacion de camara en grados. 0 = orbita en el ecuador (vista lateral pura). Top-down/isometrico suele querer PICADO (mirar al personaje algo desde arriba): subir a ~15-30. keyword-only." + - name: size + desc: "Lado en px de cada vista. None (default) usa la resolucion nativa del modelo (576 sv3d / 256 zero123). Bajarlo reduce VRAM a costa de detalle (p.ej. 320 para SV3D si OOM). keyword-only." + - name: orbit_frames + desc: "SOLO sv3d. Numero de frames del orbit que sintetiza SV3D_Conditioning. None (default) = directions (una imagen por direccion, salida directa). Subirlo (p.ej. 21, el nativo de SV3D) da una orbita mas densa y consistente de la que el caller submuestrea las direcciones (frame mas cercano por azimuth); a mas frames, mas VRAM. Ignorado por zero123. keyword-only." + - name: seed + desc: "Semilla del KSampler (0 = determinista; cambiar para variar la orbita). keyword-only." + - name: ckpt + desc: "Nombre del checkpoint en el servidor. None (default) usa el del modelo (sv3d_p.safetensors / stable_zero123.ckpt). Pasar 'sv3d_u.safetensors' para la orbita uniforme en el ecuador. keyword-only." + - name: steps + desc: "Pasos de sampling del KSampler. keyword-only." + - name: cfg + desc: "Guidance scale. None (default) usa el del modelo (2.5 sv3d / 4.0 zero123). keyword-only." + - name: min_cfg + desc: "SOLO sv3d. CFG del primer frame para VideoLinearCFGGuidance (interpola de min_cfg a cfg a lo largo del orbit). keyword-only." + - name: sampler_name + desc: "Algoritmo del KSampler. Default 'euler'. keyword-only." + - name: scheduler + desc: "Scheduler del KSampler. Default 'karras'. keyword-only." + - name: filename_prefix + desc: "Prefijo del archivo de salida del SaveImage. keyword-only." +output: "dict en API format listo para comfyui_submit_workflow (claves = node_ids string, valores = class_type + inputs). SV3D: ImageOnlyCheckpointLoader + LoadImage + SV3D_Conditioning + VideoLinearCFGGuidance + KSampler + VAEDecode + SaveImage (los N frames del orbit). Zero123: ImageOnlyCheckpointLoader + LoadImage + StableZero123_Conditioning_Batched + KSampler + VAEDecode + SaveImage (un batch de directions vistas). El frame i (i-esima imagen del SaveImage, azimuth creciente desde la frontal) = direccion i de directional_sprite_view_order(directions). El modulo expone ademas directional_sprite_view_order(directions) -> lista de nombres de direccion alineada por indice con los frames." +tested: false +file_path: "python/functions/ml/comfyui_build_directional_sprite_workflow.py" +--- + +## Ejemplo + +```python +import sys, os +sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions")) +from ml.comfyui_build_directional_sprite_workflow import ( + comfyui_build_directional_sprite_workflow, + directional_sprite_view_order, +) +from ml.comfyui_submit_workflow import comfyui_submit_workflow + +# 'goblin_front.png' = sprite frontal del personaje, fondo limpio, ya en input/ del server. +wf = comfyui_build_directional_sprite_workflow( + "goblin_front.png", + directions=8, # 8-way N/NE/E/SE/S/SW/W/NW + model="sv3d", # turntable consistente (default) + elevation=15.0, # algo de picado (top-down/iso) + size=576, + seed=7, +) +# wf["3"]["class_type"] == "SV3D_Conditioning"; wf["7"] == SaveImage (los 8 frames del orbit) +pid = comfyui_submit_workflow(wf)["prompt_id"] +# sondear /history/{pid} -> comfyui_fetch_output_image por frame. +# El frame i corresponde a la direccion i: +print(directional_sprite_view_order(8)) # ['S','SE','E','NE','N','NW','W','SW'] +``` + +Probado end-to-end (27/06/2026): goblin `enemy_creature_00001_.png` (compuesto sobre blanco +576×576) → SV3D `sv3d_p.safetensors` 8 direcciones elevation 15 seed 7. `prompt_id +8b9f75de-9dcb-41f6-ae4c-7d52d34ed238`, 8 frames `dir_sprite_goblin_0000{1..8}_.png`, 75 s, +VRAM pico 7145/8192 MiB. Consistencia rotacional medida: MAE adyacentes 27 (rotación +gradual) < frente↔espalda 29.6 (la espalda difiere más = rotación 3D real), spread de paleta +entre frames 3.83 (identidad de color consistente — el mismo goblin en las 8 vistas). + +## Cuando usarla + +- Cuando necesites un personaje visto desde **varias direcciones de movimiento** con + **identidad consistente**: enemigos de un 8-way shooter, NPCs de un top-down, unidades de + un RTS isométrico, sprite del jugador en un roguelike cenital. +- Tienes **UN sprite frontal** (lo generaste con `enemy_creature`/`topdown_sprite`, o lo + dibujaste) y quieres las otras direcciones SIN re-dibujarlas a mano. +- Elige `directions`: 8 para movimiento diagonal completo, 4 para RPG clásico. +- Sube algo de `elevation` (~15-30) para juegos con cámara picada (iso/top-down); déjalo en + 0 para un turntable lateral puro. +- **No** la uses si quieres re-pozar al personaje (correr, atacar, saltar) en una sola vista + — eso es `comfyui_build_sprite_sheet_workflow` (OpenPose). Esto **rota**, no anima la pose. + +## Gotchas + +- **La imagen frontal debe existir ya en `input/`** del servidor (subir con + `POST /upload/image` o copiar a `~/ComfyUI/input/`). La función es pura y NO valida que + exista ni que el checkpoint esté instalado; valida con `comfyui_validate_workflow` antes + de enviar a un server desconocido. +- **Fondo limpio obligatorio para buen resultado.** SV3D/Zero123 rinden mucho mejor con el + personaje aislado sobre fondo plano (blanco o transparente compuesto sobre blanco). Un + fondo con escena confunde la rotación 3D. Recorta/compón el sprite antes (Rembg o + `Image.alpha_composite` sobre blanco). +- **VRAM (RTX 3070 8GB):** SV3D es un modelo de vídeo; aún en lowvram pesa. Medido: 8 frames + a 576² → pico **7145 MiB**, margen estrecho. Limpia la GPU antes + (`POST /free {"unload_models":true,"free_memory":true}`) — no convive con un juego AAA + abierto (~2.7 GB). Si **OOM**: baja `size` (576→320), baja `directions` (8→4), o cae a + `model="zero123"` (vistas independientes, batch más barato). NO hace falta matar procesos. +- **`comfyui_wait_result` lanza `TimeoutError`** si el orbit no termina dentro del timeout, + pero el job sigue en GPU y completa — recupera los frames sondeando `/history/{prompt_id}` + por su cuenta (envolver el wait en try/except o sondear directamente). El golden tardó 75 s, + pero con `orbit_frames=21` o `size` alto puede irse a minutos. +- **SV3D entrena a 21 frames.** Con `orbit_frames` distinto de 21 (p.ej. el default + `=directions=8`) el orbit es más espaciado pero funciona (probado a 8). Para máxima + fidelidad angular usa `orbit_frames=21` y submuestrea las `directions` vistas (frame más + cercano por azimuth) — a costa de más VRAM y tiempo. +- **Mapeo dirección↔frame:** el frame 0 es la vista frontal (personaje de frente); el frame + i es el azimuth i·360/N. `directional_sprite_view_order(directions)` da la lista de nombres + alineada por índice con los frames. La etiqueta cardinal ("S" = front/mirando abajo) es + convención gamedev top-down; ajústala a tu motor si difiere. +- **Distinto de `comfyui_generate_views_from_image`** (impura): esa orquesta + `/object_info` + submit + wait + fetch para reconstrucción 3D multi-vista (4 cardinales + front/right/back/left). Este builder es el equivalente **puro** orientado a sprite + direccional gamedev, hermano de `comfyui_build_img2vid_workflow` (builder puro frente al + orquestador). Para el flujo completo build→submit→wait→fetch, compón este builder con esas + funciones del registry. + +## Capability growth log + +(sin cambios desde v1.0.0) diff --git a/python/functions/ml/comfyui_build_directional_sprite_workflow.py b/python/functions/ml/comfyui_build_directional_sprite_workflow.py new file mode 100644 index 00000000..fd79265b --- /dev/null +++ b/python/functions/ml/comfyui_build_directional_sprite_workflow.py @@ -0,0 +1,310 @@ +"""Construye el workflow ComfyUI de un sprite MULTI-DIRECCIONAL (vistas 3D consistentes). + +A partir de UNA imagen frontal de un personaje (fondo limpio), construye el dict (API +format) de un workflow que genera N vistas direccionales del MISMO personaje rotando en +3D: las 8 direcciones N/NE/E/SE/S/SW/W/NW de un shooter 8-way / juego top-down / +isométrico, o las 4 direcciones N/E/S/W de un RPG clásico. El valor de este builder es la +CONSISTENCIA ROTACIONAL: es el mismo personaje girado en 3D, no re-dibujado por dirección. + +Es el puente 2.5D que faltaba en el catálogo `gamedev-2d`. Los builders 2D de ese grupo +(`enemy_creature`, `topdown_sprite`, `sprite_sheet`) producen UNA vista; `sprite_sheet` +re-poza con OpenPose 2D (re-dibuja la silueta para cada pose, así que la identidad del +personaje deriva entre ángulos). Aquí, en cambio, un modelo de difusión 3D (SV3D turntable +u Stable Zero123 órbita) rota la figura alrededor de su eje vertical, de modo que el casco, +el arma y la paleta son los mismos en cada dirección. Es la diferencia entre "dibujar al +goblin 8 veces" (inconsistente) y "fotografiar al goblin desde 8 ángulos" (consistente). + +Dos modelos, ambos confirmados en /object_info del server 8GB: + + - SV3D (model="sv3d", default): el nodo `SV3D_Conditioning` produce una ÓRBITA de + `video_frames` vistas equiespaciadas en 360° en una sola pasada (reutiliza la + maquinaria img2vid de Stable Video Diffusion: `ImageOnlyCheckpointLoader` → + `SV3D_Conditioning` → `VideoLinearCFGGuidance` → `KSampler` → `VAEDecode` → `SaveImage`, + los N frames). Mejor consistencia angular; checkpoint `sv3d_p.safetensors` (~9.4 GB en + disco, ~2-4 GB en VRAM porque es un modelo de vídeo en lowvram). Frame 0 = la vista de + entrada; el frame i corresponde al azimuth i·360/N. + - Stable Zero123 (model="zero123"): el nodo `StableZero123_Conditioning_Batched` genera + un BATCH de N vistas a azimuths equiespaciados (control directo del ángulo por vista). + Útil como fallback si SV3D no cabe en VRAM: cada vista es una imagen independiente, así + que se puede bajar `directions` o `size` con más holgura. Checkpoint + `stable_zero123.ckpt`. width/height nativos 256. + +Función PURA: sin red, sin I/O. Solo construye el dict del grafo (los class_types y sus +inputs verificados contra /object_info). El coste GPU está al enviar con +`comfyui_submit_workflow`. La imagen de entrada debe existir ya en el `input/` del servidor +(subir antes con POST /upload/image). Hermana impura: `comfyui_generate_views_from_image` +(orquesta /object_info + submit + wait + fetch para reconstrucción 3D multi-vista, 4 +cardinales); este builder es el equivalente PURO orientado a sprite direccional gamedev, +igual que `comfyui_build_img2vid_workflow` es el builder puro frente al orquestador. +""" +from __future__ import annotations + +# Modelos soportados -> checkpoint por defecto que el nodo carga vía ImageOnlyCheckpointLoader. +_MODEL_CKPT = { + "sv3d": "sv3d_p.safetensors", + "zero123": "stable_zero123.ckpt", +} + +# Resolución nativa por modelo cuando el caller no fija `size`. SV3D entrena a 576²; +# Stable Zero123 a 256². Usar la nativa evita degradación y picos de VRAM innecesarios. +_MODEL_NATIVE_SIZE = {"sv3d": 576, "zero123": 256} + +# CFG por defecto por modelo cuando el caller no lo fija. SV3D usa guía de vídeo baja +# interpolada (VideoLinearCFGGuidance de min_cfg a cfg); Zero123 usa una CFG algo mayor. +_MODEL_DEFAULT_CFG = {"sv3d": 2.5, "zero123": 4.0} + +# Nombres de dirección por nº de direcciones, en el orden del orbit empezando por la vista +# frontal (frame 0 = personaje de frente, mirando a cámara). Convención gamedev top-down: +# "front" = el personaje mirando hacia ABAJO/al jugador (south). Cada paso avanza el azimuth +# 360/N grados. El orden es solo una etiqueta de conveniencia para el caller al ensamblar el +# grid direccional; el grafo no depende de él. +_DIRECTION_NAMES = { + 4: ["S", "E", "N", "W"], + 8: ["S", "SE", "E", "NE", "N", "NW", "W", "SW"], +} + + +def directional_sprite_view_order(directions: int) -> list: + """Lista ordenada de nombres de dirección que produce el orbit, frame 0..N-1. + + El frame i del workflow (i-ésima imagen del SaveImage, en orden de azimuth creciente + desde la vista frontal) corresponde a la dirección devuelta en la posición i. Para + 4 u 8 direcciones devuelve los nombres cardinales canónicos; para cualquier otro N + devuelve etiquetas genéricas por azimuth (``az0``, ``az45``, ...). Pura. + + Args: + directions: número de direcciones del orbit (>= 1). + + Returns: + lista de `directions` nombres de dirección, alineada por índice con los frames. + """ + directions = int(directions) + if directions in _DIRECTION_NAMES: + return list(_DIRECTION_NAMES[directions]) + return [f"az{round(i * 360 / directions)}" for i in range(directions)] + + +def comfyui_build_directional_sprite_workflow( + input_image: str, + *, + directions: int = 8, + model: str = "sv3d", + elevation: float = 0.0, + size: int | None = None, + orbit_frames: int | None = None, + seed: int = 0, + ckpt: str | None = None, + steps: int = 20, + cfg: float | None = None, + min_cfg: float = 1.0, + sampler_name: str = "euler", + scheduler: str = "karras", + filename_prefix: str = "directional_sprite", +) -> dict: + """Construye el dict (API format) del workflow de sprite multi-direccional. + + Recibe el sprite frontal de UN personaje (imagen en el `input/` del servidor, fondo + limpio) y construye un workflow que produce N vistas direccionales CONSISTENTES (el + mismo personaje rotado en 3D), una por dirección de movimiento del juego. + + Args: + input_image: nombre del archivo de imagen frontal del personaje dentro del `input/` + del servidor ComfyUI (lo carga el nodo LoadImage). Debe existir ya (subir con + POST /upload/image o copiar a ~/ComfyUI/input/). Idealmente con fondo limpio y el + personaje centrado y encuadrado entero, mirando a cámara. No puede estar vacío. + directions: número de direcciones a generar. 8 (default) = 8-way N/NE/E/SE/S/SW/W/NW + (shooter 8-direcciones, top-down/iso); 4 = N/E/S/W (RPG clásico). Cualquier N>=1 + es válido (vistas equiespaciadas a 360/N grados); 4 y 8 reciben nombres cardinales + (ver `directional_sprite_view_order`). keyword-only. + model: "sv3d" (default, órbita turntable, mejor consistencia angular) o "zero123" + (batch de vistas con control de azimuth, fallback de menor VRAM). keyword-only. + elevation: elevación de cámara en grados. 0 = órbita en el ecuador (vista lateral + pura). Un juego top-down/isométrico suele querer algo de PICADO (mirar al + personaje desde arriba): subir a ~15-30. keyword-only. + size: lado en px de cada vista. None (default) usa la resolución nativa del modelo + (576 para sv3d, 256 para zero123). Bajarlo reduce VRAM a costa de detalle. + keyword-only. + orbit_frames: SOLO sv3d. Número de frames del orbit que sintetiza SV3D_Conditioning. + None (default) = `directions` (una imagen por dirección, salida directa). Subirlo + (p. ej. 21, el nativo de SV3D) da una órbita más densa y consistente de la que el + caller submuestrea las `directions` vistas (frame más cercano por azimuth); a más + frames, más VRAM. Ignorado por zero123 (su batch es exactamente `directions`). + keyword-only. + seed: semilla del KSampler (0 = determinista; cambiar para variar la órbita). + keyword-only. + ckpt: nombre del checkpoint en el servidor. None (default) usa el del modelo + (`sv3d_p.safetensors` / `stable_zero123.ckpt`). keyword-only. + steps: pasos de sampling del KSampler. keyword-only. + cfg: guidance scale. None (default) usa el del modelo (2.5 sv3d / 4.0 zero123). + keyword-only. + min_cfg: SOLO sv3d. CFG del primer frame para VideoLinearCFGGuidance (interpola de + min_cfg a cfg a lo largo del orbit). keyword-only. + sampler_name: algoritmo del KSampler. Default "euler". keyword-only. + scheduler: scheduler del KSampler. Default "karras". keyword-only. + filename_prefix: prefijo del archivo de salida del SaveImage. keyword-only. + + Returns: + dict en API format listo para `comfyui_submit_workflow`. Las claves son node_ids + (string) y cada valor tiene class_type + inputs. SV3D devuelve 6 nodos + (ImageOnlyCheckpointLoader, LoadImage, SV3D_Conditioning, VideoLinearCFGGuidance, + KSampler, VAEDecode, SaveImage → los N frames del orbit). Zero123 devuelve + ImageOnlyCheckpointLoader + LoadImage + StableZero123_Conditioning_Batched + KSampler + + VAEDecode + SaveImage (un batch de `directions` vistas). El frame i (i-ésima imagen + del SaveImage, azimuth creciente desde la frontal) = dirección i de + `directional_sprite_view_order(directions)`. + + Raises: + ValueError: si input_image está vacío, model no es "sv3d"/"zero123", o directions < 1. + """ + if not input_image or not input_image.strip(): + raise ValueError( + "comfyui_build_directional_sprite_workflow: 'input_image' no puede estar vacío " + "(el modelo 3D necesita la vista frontal en input/)." + ) + if model not in _MODEL_CKPT: + raise ValueError( + "comfyui_build_directional_sprite_workflow: model " + f"'{model}' inválido; usa uno de {sorted(_MODEL_CKPT)}." + ) + directions = int(directions) + if directions < 1: + raise ValueError( + "comfyui_build_directional_sprite_workflow: 'directions' debe ser >= 1 " + f"(recibido {directions}); 4 u 8 son los valores canónicos de gamedev." + ) + + input_image = input_image.strip() + ckpt_name = ckpt or _MODEL_CKPT[model] + px = int(size) if size is not None else _MODEL_NATIVE_SIZE[model] + guidance = float(cfg) if cfg is not None else _MODEL_DEFAULT_CFG[model] + + if model == "sv3d": + return _build_sv3d( + input_image, ckpt_name, directions, elevation, px, orbit_frames, seed, + steps, guidance, min_cfg, sampler_name, scheduler, filename_prefix, + ) + return _build_zero123( + input_image, ckpt_name, directions, elevation, px, seed, + steps, guidance, sampler_name, scheduler, filename_prefix, + ) + + +def _build_sv3d(input_image, ckpt_name, directions, elevation, size, orbit_frames, + seed, steps, cfg, min_cfg, sampler_name, scheduler, filename_prefix) -> dict: + """Workflow SV3D: 1 imagen frontal -> orbit de N vistas en una pasada. + + SV3D reutiliza la cadena img2vid de Stable Video Diffusion: el checkpoint sv3d_p se + carga con ImageOnlyCheckpointLoader (MODEL + CLIP_VISION + VAE), SV3D_Conditioning + produce el conditioning del orbit y la latente, se muestrea como vídeo con guía CFG + lineal y el SaveImage emite los `video_frames` frames del turntable. + """ + video_frames = int(orbit_frames) if orbit_frames else directions + return { + "1": {"class_type": "LoadImage", "inputs": {"image": input_image}}, + "2": {"class_type": "ImageOnlyCheckpointLoader", "inputs": {"ckpt_name": ckpt_name}}, + "3": { + "class_type": "SV3D_Conditioning", + "inputs": { + "clip_vision": ["2", 1], + "init_image": ["1", 0], + "vae": ["2", 2], + "width": size, + "height": size, + "video_frames": video_frames, + "elevation": float(elevation), + }, + }, + "4": { + "class_type": "VideoLinearCFGGuidance", + "inputs": {"model": ["2", 0], "min_cfg": float(min_cfg)}, + }, + "5": { + "class_type": "KSampler", + "inputs": { + "seed": int(seed), + "steps": int(steps), + "cfg": float(cfg), + "sampler_name": sampler_name, + "scheduler": scheduler, + "denoise": 1.0, + "model": ["4", 0], + "positive": ["3", 0], + "negative": ["3", 1], + "latent_image": ["3", 2], + }, + }, + "6": {"class_type": "VAEDecode", "inputs": {"samples": ["5", 0], "vae": ["2", 2]}}, + "7": { + "class_type": "SaveImage", + "inputs": {"images": ["6", 0], "filename_prefix": filename_prefix}, + }, + } + + +def _build_zero123(input_image, ckpt_name, directions, elevation, size, seed, + steps, cfg, sampler_name, scheduler, filename_prefix) -> dict: + """Workflow Stable Zero123: batch de N vistas a azimuths equiespaciados. + + StableZero123_Conditioning_Batched genera `directions` vistas en una pasada, empezando + en azimuth 0 (la frontal) y avanzando 360/N grados por vista. El KSampler las muestrea + como un batch y el SaveImage las emite en orden de azimuth creciente. + """ + increment = 360.0 / directions + return { + "1": {"class_type": "LoadImage", "inputs": {"image": input_image}}, + "2": {"class_type": "ImageOnlyCheckpointLoader", "inputs": {"ckpt_name": ckpt_name}}, + "3": { + "class_type": "StableZero123_Conditioning_Batched", + "inputs": { + "clip_vision": ["2", 1], + "init_image": ["1", 0], + "vae": ["2", 2], + "width": size, + "height": size, + "batch_size": directions, + "elevation": float(elevation), + "azimuth": 0.0, + "elevation_batch_increment": 0.0, + "azimuth_batch_increment": increment, + }, + }, + "4": { + "class_type": "KSampler", + "inputs": { + "seed": int(seed), + "steps": int(steps), + "cfg": float(cfg), + "sampler_name": sampler_name, + "scheduler": scheduler, + "denoise": 1.0, + "model": ["2", 0], + "positive": ["3", 0], + "negative": ["3", 1], + "latent_image": ["3", 2], + }, + }, + "5": {"class_type": "VAEDecode", "inputs": {"samples": ["4", 0], "vae": ["2", 2]}}, + "6": { + "class_type": "SaveImage", + "inputs": {"images": ["5", 0], "filename_prefix": filename_prefix}, + }, + } + + +if __name__ == "__main__": + import json + + wf = comfyui_build_directional_sprite_workflow( + "goblin_front.png", + directions=8, + model="sv3d", + elevation=15.0, + seed=7, + ) + print(json.dumps({ + "nodes": list(wf), + "classes": sorted({n["class_type"] for n in wf.values()}), + "video_frames": wf["3"]["inputs"]["video_frames"], + "size": wf["3"]["inputs"]["width"], + "view_order": directional_sprite_view_order(8), + }, indent=2))