feat(gamedev-2d): pipeline walk_cycle_oneshot — personaje andando en pixel-art animado

Promueve el caso 1 del report 0217 (animacion de sprites de personaje) a un
pipeline one-shot: de un prompt de personaje a un sprite sheet + GIF/WEBP en loop,
frame-by-frame dirigido por pose (ControlNet OpenPose + seed fija + Rembg) con cada
frame pixelizado a NxN RGBA.

Nuevas funciones reutilizables (issue 0087, crecimiento por composicion):
- comfyui_walk_cycle_oneshot (pipeline): orquesta poses -> generacion -> pixelizado
  -> ensamblado. No-throw, salta frames que fallan. Modo openpose (esqueletos reales)
  con fallback prompt-pose.
- render_openpose_walk_skeletons: dibuja N esqueletos OpenPose COCO-18 del walk cycle
  (el insumo que el report 0217 marco como faltante).
- comfyui_pixelize_sprite_png: PNG existente -> NxN RGBA pixel-art real (compone
  crop_to_content + pixeloe_downscale + comfyui_pixelize_image).
- assemble_animated_sprite: frames RGBA -> sprite sheet horizontal + WEBP/GIF loop.
- comfyui_build_walk_cycle_workflow (pura): grafo API del workflow animado para la UI
  (ControlNet OpenPose -> KSampler xN seed fija -> ImageBatch -> Rembg -> SaveAnimatedWEBP).

Verificado en GPU: GIF/WEBP de caballero andando, 4 frames 32x32 (y 64x64) RGBA con
fondo transparente y 16 colores, identidad de silueta consistente, piernas que cambian.
Metodo de poses usado: OpenPose real (sin fallback). Evidencia en report 0221.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-28 18:14:46 +02:00
parent 36a725ba10
commit 6cc90558d4
10 changed files with 1960 additions and 0 deletions
@@ -0,0 +1,92 @@
---
name: assemble_animated_sprite
kind: function
lang: py
domain: ml
version: "1.0.0"
purity: impure
signature: "def assemble_animated_sprite(frame_paths: list, out_dir: str, *, name: str = \"anim\", fps: int = 8, fmt: str = \"webp\", loop: bool = True, spritesheet: bool = True, pad: int = 0) -> dict"
description: "Ensambla N frames PNG RGBA (p.ej. los frames de un walk cycle ya pixelizados a 32x32 con alpha) en DOS entregables: un sprite sheet horizontal (1 fila x N columnas) PNG RGBA con la transparencia intacta, y una animacion en loop WEBP lossless o GIF animado. Es la pieza de ensamblado final de cualquier animacion de sprite. Salta frames que falten o no abran (aviso en error, no aborta); normaliza tamano al primer frame valido reescalando con NEAREST. Solo PIL. No-throw. Devuelve {ok, spritesheet_path, animation_path, n_frames, frame_size, fmt, error}."
tags: [gamedev-2d, comfyui, sprite, animation]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: frame_paths
desc: "lista de rutas a PNG RGBA en orden de reproduccion; los que falten o no abran se saltan (aviso en error)."
- name: out_dir
desc: "directorio de salida; se crea si no existe. Se escriben '<name>_sheet.png' y '<name>.<ext>' dentro."
- name: name
desc: "nombre base de los ficheros generados (keyword-only, default 'anim')."
- name: fps
desc: "frames por segundo de la animacion; duration_ms = round(1000/max(1,fps)) por frame (keyword-only, default 8)."
- name: fmt
desc: "formato de la animacion: 'webp' (recomendado, lossless, alpha completo) o 'gif' (alpha binario) (keyword-only)."
- name: loop
desc: "si True la animacion se repite indefinidamente (loop=0); si False una sola vez (keyword-only, default True)."
- name: spritesheet
desc: "si True genera tambien el sprite sheet horizontal PNG RGBA (keyword-only, default True)."
- name: pad
desc: "pixeles de separacion transparente entre columnas del sheet (keyword-only, default 0)."
output: "dict con ok (bool, True si se produjo la animacion con >=1 frame valido), spritesheet_path (str, '' si spritesheet=False o fallo), animation_path (str, '' si fallo), n_frames (int, frames validos usados), frame_size ([w,h] del frame normalizado), fmt (str, 'webp'|'gif'), error (str, avisos y/o error; '' si limpio)."
tested: false
tests: []
test_file_path: ""
file_path: "python/functions/ml/assemble_animated_sprite.py"
---
## Ejemplo
```python
import sys, os
sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions"))
from ml.assemble_animated_sprite import assemble_animated_sprite
# Frames de un walk cycle ya pixelizados a 32x32 RGBA (p.ej. salida del pipeline ComfyUI):
frames = [
"/tmp/walk/frame_00.png",
"/tmp/walk/frame_01.png",
"/tmp/walk/frame_02.png",
"/tmp/walk/frame_03.png",
]
res = assemble_animated_sprite(frames, "/tmp/walk_out", name="hero_walk", fps=8, fmt="webp")
# {'ok': True,
# 'spritesheet_path': '/tmp/walk_out/hero_walk_sheet.png',
# 'animation_path': '/tmp/walk_out/hero_walk.webp',
# 'n_frames': 4, 'frame_size': [32, 32], 'fmt': 'webp', 'error': ''}
```
## Cuando usarla
Al final de cualquier pipeline de animacion de sprite, cuando ya tienes los frames
sueltos (pixelizados, con alpha) y necesitas (a) verlos animados en bucle para validar
el ciclo a ojo y (b) un sprite sheet horizontal listo para que un motor de juego lo
trocee por columnas. Tipico despues de generar un walk cycle frame a frame con ComfyUI
y pasarlo por el pixelizado: este es el paso de "juntarlo todo". Usa `fmt="webp"` por
defecto; `fmt="gif"` solo si necesitas compatibilidad con visores que no abren WEBP.
## Gotchas
- **GIF solo tiene alpha binario** (1 bit): cada pixel es opaco o totalmente
transparente, los pixeles con `alpha < 128` se vuelven transparentes y se pierde el
anti-aliasing del borde. **WEBP (lossless) es el formato recomendado** para sprites con
alpha — conserva el canal alpha completo y no ensucia el pixel-art. Usa GIF solo por
compatibilidad.
- Al guardar GIF, PIL **reoptimiza la paleta** y el indice de transparencia puede
cambiar (p.ej. de 255 a 1 al releer): es normal, los pixeles transparentes se
preservan (verificable convirtiendo el frame a RGBA y mirando el canal alpha).
- **Frames que faltan o no abren se SALTAN** (se anota en `error`), no se aborta: la
animacion se monta con los frames validos. Si quedan **0 frames validos**`ok=False`.
- El campo `error` puede venir **no vacio aunque `ok=True`**: ahi van los avisos de
frames saltados. `ok` refleja si se genero la animacion, no la ausencia de avisos.
- El tamano se normaliza al **primer frame valido**; los frames de tamano distinto se
reescalan con **NEAREST** (sin interpolacion, preserva el pixel-art duro), lo que puede
deformarlos si su aspect ratio difiere. Asegurate de que todos los frames ya vienen al
mismo tamano.
- Escribe en disco: crea `out_dir` si no existe; si no hay permiso de escritura, el
fallo del sheet va a `error` como aviso y el de la animacion pone `ok=False`.
- `disposal=2` limpia el lienzo entre frames (transparencia correcta en cada paso); sin
el, los frames se acumularian unos sobre otros.