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:
@@ -0,0 +1,110 @@
|
||||
---
|
||||
name: comfyui_walk_cycle_oneshot
|
||||
kind: pipeline
|
||||
lang: py
|
||||
domain: pipelines
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def comfyui_walk_cycle_oneshot(character: str, *, frames: int = 4, size: int = 32, colors: int = 16, fps: int = 8, checkpoint: str = 'IMG_dreamshaper_8.safetensors', ref_image: str | None = None, server: str = '127.0.0.1:8188', dest_dir: str = '~/ComfyUI/output', seed: int = 0, pose_method: str = 'auto', controlnet_strength: float = 0.7, engine: str = 'pixeloe', palette=None, fmt: str = 'webp', **gen_kwargs) -> dict"
|
||||
description: "Pipeline one-shot: de un prompt de personaje a una animacion de walk cycle en pixel-art (sprite sheet + GIF/WEBP en loop). Genera N frames frame-by-frame dirigidos por pose (ControlNet OpenPose con esqueletos del walk cycle, o fase del paso por prompt como fallback), con seed fija para identidad consistente y Rembg para alpha, y pixeliza cada frame a un grid duro size x size RGBA. Materializa el caso 1 de la investigacion de animacion de sprites (report 0217): personaje = frame-by-frame pose-driven, NUNCA modelos de video. Compone render_openpose_walk_skeletons + comfyui_build_sprite_sheet_workflow + submit/wait/fetch + comfyui_pixelize_sprite_png + assemble_animated_sprite. Impuro: red + GPU + disco. No-throw, salta frames que fallan."
|
||||
tags: [gamedev-2d, comfyui, pixelart, sprite, animation, walk-cycle, controlnet, openpose, launcher]
|
||||
uses_functions: ["render_openpose_walk_skeletons_py_ml", "comfyui_build_sprite_sheet_workflow_py_ml", "comfyui_submit_workflow_py_ml", "comfyui_wait_result_py_ml", "comfyui_fetch_output_image_py_ml", "comfyui_pixelize_sprite_png_py_ml", "assemble_animated_sprite_py_ml"]
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: ["os", "sys"]
|
||||
params:
|
||||
- name: character
|
||||
desc: "Prompt del personaje (ej. 'pixel art knight, full body, side view'). No vacio. La identidad se mantiene entre frames con seed fija."
|
||||
- name: frames
|
||||
desc: "Numero de frames del ciclo (>=2, recomendado 4-8). 4 = las 4 fases canonicas contact-L / passing / contact-R / passing."
|
||||
- name: size
|
||||
desc: "Lado del grid pixel-art final por frame en pixeles (32 sprites pequenos, 64 personajes con mas detalle)."
|
||||
- name: colors
|
||||
desc: "Numero de colores de la paleta libre por frame (cuantizacion MEDIANCUT) cuando palette es None."
|
||||
- name: fps
|
||||
desc: "Cadencia de la animacion en frames por segundo (duration = 1000/fps ms por frame)."
|
||||
- name: checkpoint
|
||||
desc: "Checkpoint SD1.5 (ControlNet OpenPose + IPAdapter-FaceID solo instalados en SD1.5; default 'IMG_dreamshaper_8.safetensors')."
|
||||
- name: ref_image
|
||||
desc: "Imagen de cara de referencia en el input/ del servidor para IPAdapter-FaceID (segunda ancla de identidad). None = solo seed + prompt."
|
||||
- name: server
|
||||
desc: "host:port del servidor ComfyUI (sin esquema). Default 127.0.0.1:8188."
|
||||
- name: dest_dir
|
||||
desc: "Directorio donde guardar frames + sprite sheet + animacion (se expande ~)."
|
||||
- name: seed
|
||||
desc: "Semilla FIJA del KSampler para TODOS los frames (identidad estable entre poses)."
|
||||
- name: pose_method
|
||||
desc: "'openpose' (esqueletos OpenPose -> ControlNet, control exacto), 'prompt' (fase del paso descrita en el prompt, sin esqueletos) o 'auto' (intenta openpose, cae a prompt si el render falla)."
|
||||
- name: controlnet_strength
|
||||
desc: "Fuerza del ControlNet OpenPose (0.7 da buen control sin aplastar el estilo). Solo aplica en modo openpose."
|
||||
- name: engine
|
||||
desc: "Motor de downscale del pixelizado: 'pixeloe' (contrast-aware, conserva silueta) o 'nearest'."
|
||||
- name: palette
|
||||
desc: "None (paleta libre a colors), nombre builtin ('pico-8','nes','game-boy') o lista de hex. Fija ignora colors."
|
||||
- name: fmt
|
||||
desc: "Formato de la animacion: 'webp' (recomendado, alpha real) o 'gif' (alpha binario)."
|
||||
output: "dict {ok, frames:[paths], spritesheet_path, animation_path, size, n_frames, seed, pose_method_used, skipped:[idx], error}. ok=True si se produjo la animacion con >=1 frame. n_frames puede ser < frames si alguno fallo (se salta y se sigue)."
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "python/functions/pipelines/comfyui_walk_cycle_oneshot.py"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```python
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.join("python", "functions"))
|
||||
from pipelines.comfyui_walk_cycle_oneshot import comfyui_walk_cycle_oneshot
|
||||
|
||||
# Requiere el servidor ComfyUI vivo en 127.0.0.1:8188 (GPU).
|
||||
res = comfyui_walk_cycle_oneshot(
|
||||
"pixel art knight, full body, side view",
|
||||
frames=4, size=32, colors=16, fps=8, seed=42,
|
||||
dest_dir="/tmp/comfy_walk_cycle",
|
||||
)
|
||||
print(res["ok"], res["n_frames"], res["animation_path"], res["pose_method_used"])
|
||||
# -> True 4 /tmp/comfy_walk_cycle/walk_cycle.webp openpose
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando quieras una animacion de un personaje en pixel-art (caminar, correr) lista
|
||||
para un juego 2D, en un solo paso: das el prompt del personaje y recibes el sprite
|
||||
sheet + el GIF/WEBP en loop. Es la promocion a pipeline (issue 0087) de la receta
|
||||
del caso 1 del report 0217 — el camino correcto para sprites limpios de personaje
|
||||
con alpha, frente a AnimateDiff o modelos de video (que ensucian el alpha y no
|
||||
clavan la pose). Para una sola pose estatica usa `comfyui_pixelart_real_oneshot`;
|
||||
para varias vistas direccionales (8-way) usa
|
||||
`comfyui_build_directional_sprite_workflow`.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **Server vivo + GPU**: requiere ComfyUI en `server` con la GPU libre. El report
|
||||
recomienda `POST /free` antes de cargas pesadas de modelo. Cada frame reusa el
|
||||
mismo checkpoint, asi que el modelo solo se carga una vez.
|
||||
- **Poses OpenPose**: en modo `openpose` los esqueletos se escriben en el `input/`
|
||||
del servidor (asume server local; para un server remoto haria falta subirlos con
|
||||
`POST /upload/image`). Si el ControlNet no produce variacion de piernas
|
||||
reconocible, usa `pose_method="prompt"`: a 32x32 el detalle de pose se simplifica
|
||||
y la fase del paso por prompt + seed fija da un walk reconocible.
|
||||
- **Identidad**: la `seed` es FIJA para todos los frames — esa es la ancla de
|
||||
identidad. Cambiar la seed entre frames rompe la consistencia del personaje.
|
||||
`ref_image` (IPAdapter-FaceID) es una segunda ancla opcional; sobre un sprite de
|
||||
cuerpo entero pequeno aporta sobre todo paleta/ropa (ver report 0217).
|
||||
- **No-throw, salta frames**: si un frame falla (red, GPU, build) se anade a
|
||||
`skipped` y la animacion se monta con los que queden. ok=False solo si NINGUN
|
||||
frame sale.
|
||||
- **Loop suave**: con `frames=4` el ciclo (contact-L, passing, contact-R, passing)
|
||||
ya cierra el bucle — el frame siguiente al ultimo vuelve a la primera fase.
|
||||
- **WEBP vs GIF**: `fmt="webp"` conserva alpha real (lossless); `fmt="gif"` solo
|
||||
tiene alpha binario (1 bit). Para sprites con transparencia, usa WEBP.
|
||||
|
||||
## Capability growth log
|
||||
|
||||
- v1.0.0 (2026-06-28) — version inicial. Caso 1 del report 0217 promovido a
|
||||
pipeline one-shot: walk cycle pixel-art con poses OpenPose (o fallback prompt),
|
||||
seed fija para identidad, Rembg para alpha, pixelizado a NxN RGBA, sprite sheet +
|
||||
WEBP/GIF en loop.
|
||||
@@ -0,0 +1,362 @@
|
||||
"""comfyui_walk_cycle_oneshot — personaje andando -> sprite pixel-art animado (GIF/WEBP).
|
||||
|
||||
Pipeline one-shot (issue 0087) que materializa el caso 1 de la investigacion de
|
||||
animacion de sprites (report 0217): un ciclo de caminar de personaje se anima
|
||||
**frame-by-frame dirigido por pose**, NO con modelos de video. Por cada fase del
|
||||
paso se genera el MISMO personaje cambiando solo la pose (ControlNet OpenPose +
|
||||
seed fija + IPAdapter opcional), se recorta el fondo a alpha (Rembg) y se pixeliza
|
||||
a un grid duro de `size` x `size` RGBA. Los N frames se ensamblan en un sprite
|
||||
sheet horizontal + una animacion en loop (WEBP/GIF).
|
||||
|
||||
Dos metodos para las poses, seleccionables con `pose_method`:
|
||||
- "openpose" (preferido): se dibujan N esqueletos OpenPose del walk cycle
|
||||
(render_openpose_walk_skeletons) y se alimentan al ControlNet OpenPose, un
|
||||
frame por pose. El timing del paso es exactamente el dibujado.
|
||||
- "prompt" (fallback): sin esqueletos, la fase del paso se describe en el prompt
|
||||
("left leg forward", "passing pose", ...) con seed fija. A 32x32 el detalle de
|
||||
pose se simplifica, asi que un walk de 4 frames reconocible basta. Se usa
|
||||
cuando el ControlNet no da poses utilizables o el caller lo pide.
|
||||
- "auto" (default): intenta "openpose"; si el render de esqueletos falla, cae a
|
||||
"prompt" y lo refleja en pose_method_used.
|
||||
|
||||
La identidad consistente entre frames se ancla con **seed fija** (mismo personaje,
|
||||
misma semilla) + prompt base fijo, y opcionalmente con `ref_image` (IPAdapter-FaceID
|
||||
en comfyui_build_sprite_sheet_workflow).
|
||||
|
||||
Compone funciones del registry, no reescribe su logica:
|
||||
render_openpose_walk_skeletons_py_ml (esqueletos OpenPose del walk cycle)
|
||||
comfyui_build_sprite_sheet_workflow_py_ml(1 frame: identidad + pose + alpha)
|
||||
comfyui_submit_workflow_py_ml (POST /prompt)
|
||||
comfyui_wait_result_py_ml (poll /history)
|
||||
comfyui_fetch_output_image_py_ml (GET /view -> disco)
|
||||
comfyui_pixelize_sprite_png_py_ml (PNG alta-res -> NxN RGBA pixel-art)
|
||||
assemble_animated_sprite_py_ml (frames -> sprite sheet + WEBP/GIF loop)
|
||||
|
||||
Pipeline impuro: red (HTTP a ComfyUI), GPU (generacion), escritura en disco.
|
||||
No-throw: cualquier fallo se captura y viaja en el dict de estado (campo error).
|
||||
Si un frame concreto falla se salta y la animacion se monta con los que haya.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Importa las funciones del registry (mismo arbol python/functions).
|
||||
_FUNCTIONS_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
if _FUNCTIONS_ROOT not in sys.path:
|
||||
sys.path.insert(0, _FUNCTIONS_ROOT)
|
||||
|
||||
from ml.assemble_animated_sprite import assemble_animated_sprite
|
||||
from ml.comfyui_build_sprite_sheet_workflow import comfyui_build_sprite_sheet_workflow
|
||||
from ml.comfyui_fetch_output_image import comfyui_fetch_output_image
|
||||
from ml.comfyui_pixelize_sprite_png import comfyui_pixelize_sprite_png
|
||||
from ml.comfyui_submit_workflow import comfyui_submit_workflow
|
||||
from ml.comfyui_wait_result import comfyui_wait_result
|
||||
from ml.render_openpose_walk_skeletons import render_openpose_walk_skeletons
|
||||
|
||||
# Descriptores de fase del paso para el modo "prompt" (y como apoyo). El ciclo
|
||||
# canonico de 4 fases de un walk lateral: contacto pierna izquierda, paso (piernas
|
||||
# juntas, cuerpo arriba), contacto pierna derecha, paso. Para N != 4 se reparten
|
||||
# ciclicamente sobre estas cuatro.
|
||||
_WALK_PHASES = [
|
||||
"walking, left leg forward, mid stride, dynamic walk pose",
|
||||
"walking, legs together passing position, standing tall",
|
||||
"walking, right leg forward, mid stride, dynamic walk pose",
|
||||
"walking, legs together passing position, standing tall",
|
||||
]
|
||||
|
||||
|
||||
def _phase_for(index: int, total: int) -> str:
|
||||
"""Devuelve el descriptor de fase del paso para el frame `index` de `total`.
|
||||
|
||||
Mapea el frame al ciclo de 4 fases base de forma uniforme, de modo que el
|
||||
primer y el ultimo frame cierren un loop continuo (el frame `total` volveria a
|
||||
la fase del frame 0).
|
||||
"""
|
||||
pos = (index / max(1, total)) * len(_WALK_PHASES)
|
||||
return _WALK_PHASES[int(pos) % len(_WALK_PHASES)]
|
||||
|
||||
|
||||
def comfyui_walk_cycle_oneshot(
|
||||
character: str,
|
||||
*,
|
||||
frames: int = 4,
|
||||
size: int = 32,
|
||||
colors: int = 16,
|
||||
fps: int = 8,
|
||||
checkpoint: str = "IMG_dreamshaper_8.safetensors",
|
||||
ref_image: str | None = None,
|
||||
server: str = "127.0.0.1:8188",
|
||||
dest_dir: str = "~/ComfyUI/output",
|
||||
seed: int = 0,
|
||||
pose_method: str = "auto",
|
||||
comfy_input_dir: str = "~/ComfyUI/input",
|
||||
controlnet_strength: float = 0.7,
|
||||
controlnet_end: float = 0.8,
|
||||
facing: str = "right",
|
||||
engine: str = "pixeloe",
|
||||
palette=None,
|
||||
fmt: str = "webp",
|
||||
negative: str = "blurry, lowres, extra limbs, deformed, multiple characters",
|
||||
width: int = 512,
|
||||
height: int = 768,
|
||||
steps: int = 24,
|
||||
cfg: float = 7.0,
|
||||
comfy_python: str | None = None,
|
||||
wait_timeout: float = 300.0,
|
||||
filename_prefix: str = "walk_cycle",
|
||||
keep_frames: bool = True,
|
||||
**gen_kwargs,
|
||||
) -> dict:
|
||||
"""Genera una animacion de personaje andando en pixel-art, end-to-end.
|
||||
|
||||
Args:
|
||||
character: prompt del personaje ("pixel art knight, full body, side view").
|
||||
No puede estar vacio. La identidad se mantiene entre frames con seed fija.
|
||||
frames: numero de frames del ciclo (>=2, recomendado 4-8). keyword-only.
|
||||
size: lado del grid pixel-art final por frame en pixeles (32 sprites
|
||||
pequenos, 64 personajes con mas detalle). keyword-only.
|
||||
colors: numero de colores de la paleta libre por frame (cuantizacion
|
||||
MEDIANCUT) cuando palette es None. keyword-only.
|
||||
fps: cadencia de la animacion en frames por segundo. keyword-only.
|
||||
checkpoint: checkpoint SD1.5 (ControlNet OpenPose + IPAdapter-FaceID solo
|
||||
instalados en SD1.5; default 'IMG_dreamshaper_8.safetensors'). keyword-only.
|
||||
ref_image: imagen de cara de referencia en el input/ del servidor para
|
||||
IPAdapter-FaceID (segunda ancla de identidad). None usa solo seed +
|
||||
prompt. keyword-only.
|
||||
server: host:port del servidor ComfyUI (sin esquema). keyword-only.
|
||||
dest_dir: directorio donde guardar frames + sprite sheet + animacion.
|
||||
keyword-only.
|
||||
seed: semilla FIJA del KSampler para todos los frames (identidad estable).
|
||||
keyword-only.
|
||||
pose_method: "openpose" (esqueletos OpenPose -> ControlNet, control exacto),
|
||||
"prompt" (fase del paso descrita en el prompt, sin esqueletos), o "auto"
|
||||
(intenta openpose, cae a prompt si el render falla). keyword-only.
|
||||
comfy_input_dir: directorio input/ del servidor ComfyUI donde se escriben
|
||||
los esqueletos para que el ControlNet los lea (server local).
|
||||
keyword-only.
|
||||
controlnet_strength: fuerza del ControlNet OpenPose (0.7 da buen control de
|
||||
la pose sin aplastar el estilo). keyword-only.
|
||||
controlnet_end: fraccion de pasos en que el OpenPose deja de aplicarse
|
||||
(end<1.0 libera los ultimos pasos para pelo/ropa). keyword-only.
|
||||
facing: direccion a la que mira el personaje en los esqueletos ("right" o
|
||||
"left"). keyword-only.
|
||||
engine: motor de downscale del pixelizado ("pixeloe" contrast-aware o
|
||||
"nearest"). keyword-only.
|
||||
palette: None (paleta libre a `colors`), nombre builtin ("pico-8", "nes",
|
||||
"game-boy") o lista de hex. keyword-only.
|
||||
fmt: formato de la animacion ("webp" recomendado para alpha, o "gif").
|
||||
keyword-only.
|
||||
negative: prompt negativo de generacion. keyword-only.
|
||||
width, height: resolucion de generacion por frame (512x768 vertical encuadra
|
||||
cuerpo entero). keyword-only.
|
||||
steps, cfg: parametros del KSampler. keyword-only.
|
||||
comfy_python: interprete con pixeloe para el pixelizado (None autodetecta
|
||||
~/ComfyUI/.venv). keyword-only.
|
||||
wait_timeout: segundos maximos esperando cada frame al server. keyword-only.
|
||||
filename_prefix: prefijo de los archivos de salida. keyword-only.
|
||||
keep_frames: si True conserva los PNG de cada frame pixelizado; si False los
|
||||
borra tras montar la animacion. keyword-only.
|
||||
**gen_kwargs: params extra para comfyui_build_sprite_sheet_workflow
|
||||
(sampler_name, scheduler, char_lora, lora_strength, weight, ...).
|
||||
|
||||
Returns:
|
||||
dict con:
|
||||
- ok (bool): True si se produjo la animacion con >=1 frame.
|
||||
- frames (list[str]): rutas de los PNG pixelizados (size x size RGBA).
|
||||
- spritesheet_path (str): ruta del sprite sheet horizontal.
|
||||
- animation_path (str): ruta de la animacion WEBP/GIF en loop.
|
||||
- size (int): lado real de cada frame pixelizado.
|
||||
- n_frames (int): numero de frames producidos (puede ser < `frames` si
|
||||
alguno fallo y se salto).
|
||||
- seed (int): semilla usada.
|
||||
- pose_method_used (str): "openpose" o "prompt" (refleja el fallback).
|
||||
- skipped (list[int]): indices de frames que fallaron y se saltaron.
|
||||
- error (str): mensaje de error; vacio si todo OK.
|
||||
"""
|
||||
out = {
|
||||
"ok": False, "frames": [], "spritesheet_path": "", "animation_path": "",
|
||||
"size": int(size), "n_frames": 0, "seed": int(seed),
|
||||
"pose_method_used": "", "skipped": [], "error": "",
|
||||
}
|
||||
|
||||
if not character or not character.strip():
|
||||
out["error"] = "character vacio"
|
||||
return out
|
||||
if int(frames) < 2:
|
||||
out["error"] = f"frames debe ser >= 2, recibido {frames!r}"
|
||||
return out
|
||||
if int(size) < 1:
|
||||
out["error"] = f"size debe ser >= 1, recibido {size!r}"
|
||||
return out
|
||||
if pose_method not in ("auto", "openpose", "prompt"):
|
||||
out["error"] = f"pose_method invalido: {pose_method!r}"
|
||||
return out
|
||||
|
||||
n = int(frames)
|
||||
dest = os.path.expanduser(dest_dir)
|
||||
frames_dir = os.path.join(dest, f"{filename_prefix}_frames")
|
||||
try:
|
||||
os.makedirs(frames_dir, exist_ok=True)
|
||||
except OSError as exc:
|
||||
out["error"] = f"no se pudo crear dest_dir {frames_dir!r}: {exc}"
|
||||
return out
|
||||
|
||||
# --- Fase 0: resolver el metodo de poses + esqueletos OpenPose si aplica. ---
|
||||
skeleton_names: list[str | None] = [None] * n
|
||||
method = "prompt" if pose_method == "prompt" else "openpose"
|
||||
if method == "openpose":
|
||||
input_dir = os.path.expanduser(comfy_input_dir)
|
||||
sk = render_openpose_walk_skeletons(
|
||||
input_dir, frames=n, width=int(width), height=int(height),
|
||||
facing=facing, filename_prefix=f"{filename_prefix}_pose",
|
||||
)
|
||||
if sk.get("ok") and sk.get("skeleton_paths"):
|
||||
paths = sk["skeleton_paths"]
|
||||
# El builder espera el basename relativo al input/ del servidor.
|
||||
skeleton_names = [os.path.basename(p) for p in paths][:n]
|
||||
# Si se generaron menos esqueletos que frames, rellena con None.
|
||||
while len(skeleton_names) < n:
|
||||
skeleton_names.append(None)
|
||||
else:
|
||||
# Fallback limpio: no hay esqueletos -> modo prompt.
|
||||
method = "prompt"
|
||||
out["error"] = (
|
||||
f"render de esqueletos OpenPose fallo ({sk.get('error')}); "
|
||||
f"fallback a pose por prompt"
|
||||
)
|
||||
if pose_method == "openpose":
|
||||
# El caller forzo openpose y no hay esqueletos: aun asi seguimos en
|
||||
# prompt para no abortar, pero queda anotado en el error.
|
||||
pass
|
||||
out["pose_method_used"] = method
|
||||
|
||||
# --- Fase 1..N: generar + pixelizar cada frame del ciclo. ---
|
||||
pixel_frames: list[str] = []
|
||||
for i in range(n):
|
||||
pose_skeleton = skeleton_names[i] if method == "openpose" else None
|
||||
# En modo openpose la pose la fija el esqueleto: no se mete la fase en el
|
||||
# prompt para no competir con el ControlNet. En modo prompt, la fase guia.
|
||||
if method == "prompt":
|
||||
subject_i = f"{character}, {_phase_for(i, n)}"
|
||||
else:
|
||||
subject_i = f"{character}, walking"
|
||||
|
||||
try:
|
||||
wf = comfyui_build_sprite_sheet_workflow(
|
||||
subject_i,
|
||||
ref_image=ref_image,
|
||||
pose_skeleton=pose_skeleton,
|
||||
ckpt_name=checkpoint,
|
||||
controlnet_strength=controlnet_strength,
|
||||
controlnet_end=controlnet_end,
|
||||
transparent=True,
|
||||
negative=negative,
|
||||
width=int(width),
|
||||
height=int(height),
|
||||
steps=int(steps),
|
||||
cfg=float(cfg),
|
||||
seed=int(seed), # FIJA: identidad consistente entre frames.
|
||||
filename_prefix=f"{filename_prefix}_f{i}",
|
||||
**gen_kwargs,
|
||||
)
|
||||
except (ValueError, TypeError) as exc:
|
||||
out["skipped"].append(i)
|
||||
if not out["error"]:
|
||||
out["error"] = f"build workflow frame {i} fallo: {exc}"
|
||||
continue
|
||||
|
||||
# submit -> wait -> fetch (alta resolucion RGBA). Cualquier fallo de red/
|
||||
# GPU salta este frame y sigue (error path del DoD).
|
||||
try:
|
||||
sub = comfyui_submit_workflow(wf, server=server)
|
||||
prompt_id = sub["prompt_id"]
|
||||
outputs = comfyui_wait_result(prompt_id, server=server, timeout=wait_timeout)
|
||||
except (RuntimeError, KeyError, OSError, TimeoutError) as exc:
|
||||
out["skipped"].append(i)
|
||||
if not out["error"]:
|
||||
out["error"] = f"frame {i} fallo en submit/wait: {exc}"
|
||||
continue
|
||||
|
||||
img = None
|
||||
for node_out in outputs.values():
|
||||
images = node_out.get("images") if isinstance(node_out, dict) else None
|
||||
if images:
|
||||
img = images[0]
|
||||
break
|
||||
if img is None:
|
||||
out["skipped"].append(i)
|
||||
continue
|
||||
|
||||
fetched = comfyui_fetch_output_image(
|
||||
img["filename"], subfolder=img.get("subfolder", ""),
|
||||
type_=img.get("type", "output"), server=server, dest_dir=frames_dir,
|
||||
)
|
||||
if not fetched.get("ok"):
|
||||
out["skipped"].append(i)
|
||||
if not out["error"]:
|
||||
out["error"] = f"frame {i} fetch fallo: {fetched.get('error')}"
|
||||
continue
|
||||
|
||||
# Pixelizar el PNG alta-res a un grid duro size x size RGBA.
|
||||
frame_px = os.path.join(frames_dir, f"{filename_prefix}_px_{i:02d}.png")
|
||||
px = comfyui_pixelize_sprite_png(
|
||||
fetched["path"], frame_px, size=int(size), colors=int(colors),
|
||||
engine=engine, palette=palette, transparent=True, autocrop=True,
|
||||
comfy_python=comfy_python,
|
||||
)
|
||||
if not px.get("ok"):
|
||||
out["skipped"].append(i)
|
||||
if not out["error"]:
|
||||
out["error"] = f"frame {i} pixelizado fallo: {px.get('error')}"
|
||||
continue
|
||||
|
||||
pixel_frames.append(frame_px)
|
||||
|
||||
if not pixel_frames:
|
||||
if not out["error"]:
|
||||
out["error"] = "ningun frame se genero correctamente"
|
||||
return out
|
||||
|
||||
# --- Ensamblado: sprite sheet horizontal + animacion en loop. ---
|
||||
asm = assemble_animated_sprite(
|
||||
pixel_frames, dest, name=filename_prefix, fps=int(fps), fmt=fmt,
|
||||
loop=True, spritesheet=True,
|
||||
)
|
||||
if not asm.get("ok"):
|
||||
out["frames"] = pixel_frames
|
||||
out["n_frames"] = len(pixel_frames)
|
||||
if not out["error"]:
|
||||
out["error"] = f"ensamblado fallo: {asm.get('error')}"
|
||||
return out
|
||||
|
||||
out["frames"] = pixel_frames
|
||||
out["spritesheet_path"] = asm.get("spritesheet_path", "")
|
||||
out["animation_path"] = asm.get("animation_path", "")
|
||||
out["n_frames"] = len(pixel_frames)
|
||||
# El size real lo confirma el primer frame ensamblado.
|
||||
fs = asm.get("frame_size") or [int(size), int(size)]
|
||||
out["size"] = int(fs[0])
|
||||
|
||||
if not keep_frames:
|
||||
for fp in pixel_frames:
|
||||
try:
|
||||
os.remove(fp)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
out["ok"] = True
|
||||
# El error de fallback de poses (si lo hubo) es informativo, no invalida el ok.
|
||||
return out
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import json
|
||||
|
||||
res = comfyui_walk_cycle_oneshot(
|
||||
"pixel art knight, full body, side view",
|
||||
frames=4, size=32, colors=16, fps=8, seed=42,
|
||||
dest_dir="/tmp/comfy_walk_cycle",
|
||||
)
|
||||
print(json.dumps(res, indent=2))
|
||||
Reference in New Issue
Block a user