feat(ml): comfyui_build_img2vid_workflow builder img2vid SVD (API format)

Builder puro que construye el dict de un workflow ComfyUI img2vid (Stable Video
Diffusion) en API format a partir de una imagen estatica. Cadena de 7 nodos:
ImageOnlyCheckpointLoader(svd.safetensors, todo-en-uno) + LoadImage ->
SVD_img2vid_Conditioning -> VideoLinearCFGGuidance -> KSampler(denoise 1.0) ->
VAEDecode -> SaveAnimatedWEBP. SVD condiciona por CLIP_VISION de la imagen (sin
prompt de texto); movimiento via motion_bucket_id.

class_type/inputs verificados contra /object_info del servidor vivo. Validacion
estructural con comfyui_validate_workflow: 0 errores. 4 tests verdes. Sin submit
de generacion (GPU en uso por otro agente).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-24 12:02:04 +02:00
parent 3e75d1bf79
commit 11ef8ef6db
4 changed files with 347 additions and 0 deletions
+1
View File
@@ -98,6 +98,7 @@ canónica). El resultado es un `.mp4` vía `CreateVideo → SaveVideo`.
| ID | Firma corta | Qué hace | | ID | Firma corta | Qué hace |
|---|---|---| |---|---|---|
| [comfyui_build_video_workflow_py_ml](../../python/functions/ml/comfyui_build_video_workflow.md) | `build_video_workflow(prompt, *, model='ltx', negative='', width=512, height=320, num_frames=65, steps=20, seed=0, fps=24) -> dict` | Builder txt2video para LTX-Video 2B (`model='ltx'`, 12 nodos LTXV*) o Wan2.1 1.3B (`model='wan'`, UNETLoader+VAELoader+ModelSamplingSD3). Nombres de modelo reales, defaults conservadores 8 GB. **Pura**. | | [comfyui_build_video_workflow_py_ml](../../python/functions/ml/comfyui_build_video_workflow.md) | `build_video_workflow(prompt, *, model='ltx', negative='', width=512, height=320, num_frames=65, steps=20, seed=0, fps=24) -> dict` | Builder txt2video para LTX-Video 2B (`model='ltx'`, 12 nodos LTXV*) o Wan2.1 1.3B (`model='wan'`, UNETLoader+VAELoader+ModelSamplingSD3). Nombres de modelo reales, defaults conservadores 8 GB. **Pura**. |
| [comfyui_build_img2vid_workflow_py_ml](../../python/functions/ml/comfyui_build_img2vid_workflow.md) | `build_img2vid_workflow(image, *, ckpt='svd.safetensors', width=1024, height=576, video_frames=14, motion_bucket_id=127, fps=6, augmentation_level=0.0, steps=20, cfg=2.5, min_cfg=1.0, seed=0, sampler_name='euler', scheduler='karras', filename_prefix='comfy_svd') -> dict` | Builder img2vid (Stable Video Diffusion): anima una imagen estática a clip corto. ImageOnlyCheckpointLoader(`svd.safetensors`, todo-en-uno) + LoadImage → SVD_img2vid_Conditioning → VideoLinearCFGGuidance → KSampler (denoise 1.0) → VAEDecode → SaveAnimatedWEBP. SVD no usa prompt de texto: condiciona por CLIP_VISION de la imagen; movimiento vía `motion_bucket_id`. **Pura**. |
### Imagen → 3D (Hunyuan3D-2 nativo) — dominio `ml` + `pipelines` (tag `img-to-3d`) ### Imagen → 3D (Hunyuan3D-2 nativo) — dominio `ml` + `pipelines` (tag `img-to-3d`)
@@ -0,0 +1,107 @@
---
name: comfyui_build_img2vid_workflow
kind: function
lang: py
domain: ml
version: "1.0.0"
purity: pure
signature: "def comfyui_build_img2vid_workflow(image: str, *, ckpt: str = \"svd.safetensors\", width: int = 1024, height: int = 576, video_frames: int = 14, motion_bucket_id: int = 127, fps: int = 6, augmentation_level: float = 0.0, steps: int = 20, cfg: float = 2.5, min_cfg: float = 1.0, seed: int = 0, sampler_name: str = \"euler\", scheduler: str = \"karras\", filename_prefix: str = \"comfy_svd\") -> dict"
description: "Construye el dict de un workflow ComfyUI img2vid (Stable Video Diffusion) en API format a partir de una imagen estatica. Cadena: ImageOnlyCheckpointLoader(svd.safetensors -> MODEL, CLIP_VISION, VAE) + LoadImage -> SVD_img2vid_Conditioning(positive, negative, latent) -> VideoLinearCFGGuidance -> KSampler(denoise 1.0) -> VAEDecode -> SaveAnimatedWEBP. SVD no usa prompt de texto: el condicionamiento sale de la imagen via CLIP_VISION del checkpoint todo-en-uno. Movimiento via motion_bucket_id y fps. Pura, sin red ni I/O. Hermana de comfyui_build_video_workflow (txt2video LTX/Wan)."
tags: [comfyui, svd, img2vid, video, ml, workflow]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: []
params:
- name: image
desc: "Nombre del archivo de imagen base en la carpeta input/ del servidor ComfyUI (lo que carga LoadImage). Es el frame inicial del que SVD deriva el clip."
- name: ckpt
desc: "Nombre del checkpoint SVD tal como lo ve el servidor. Por defecto 'svd.safetensors' (todo-en-uno: UNet + VAE + CLIP image encoder). keyword-only."
- name: width
desc: "Ancho del video en px (multiplo de 8; SVD base entrena a 1024). keyword-only."
- name: height
desc: "Alto del video en px (multiplo de 8; SVD base entrena a 576). keyword-only."
- name: video_frames
desc: "Numero de frames del clip. svd.safetensors es el modelo de 14 frames; la variante xt llega a 25. keyword-only."
- name: motion_bucket_id
desc: "Intensidad de movimiento (1-255 util; 127 por defecto). Mas alto = mas movimiento. keyword-only."
- name: fps
desc: "Frames por segundo con que se condiciona (SVD_img2vid_Conditioning) y se guarda el clip (SaveAnimatedWEBP, alli como float). keyword-only."
- name: augmentation_level
desc: "Ruido anadido a la imagen base (0.0 = fiel; subirlo da mas libertad/movimiento a costa de fidelidad). keyword-only."
- name: steps
desc: "Pasos de sampling del KSampler. keyword-only."
- name: cfg
desc: "Guidance scale del ultimo frame (VideoLinearCFGGuidance interpola de min_cfg al primero hasta cfg al ultimo). SVD usa cfg baja (~2.5). keyword-only."
- name: min_cfg
desc: "Guidance scale del primer frame para VideoLinearCFGGuidance. keyword-only."
- name: seed
desc: "Semilla del sampler. 0 es determinista; cambiar para variar el clip. keyword-only."
- name: sampler_name
desc: "Algoritmo del KSampler. Por defecto 'euler'. keyword-only."
- name: scheduler
desc: "Scheduler del KSampler. Por defecto 'karras'. keyword-only."
- name: filename_prefix
desc: "Prefijo del archivo de salida (.webp animado de SaveAnimatedWEBP). keyword-only."
output: "dict en API format listo para comfyui_submit_workflow. node_ids string; cada valor con class_type + inputs. Devuelve 7 nodos: ImageOnlyCheckpointLoader, LoadImage, SVD_img2vid_Conditioning, VideoLinearCFGGuidance, KSampler, VAEDecode y SaveAnimatedWEBP. El denoise del KSampler se fija a 1.0 (genera desde el latente condicionado, no es img2img)."
tested: true
tests: ["estructura: 7 nodos SVD presentes + ckpt svd.safetensors + image en LoadImage", "cableado: clip_vision/vae [15,1]/[15,2], cond->KSampler 0/1/2, model post VideoLinearCFGGuidance, denoise 1.0", "params reflejados (width/height/video_frames/motion_bucket_id/fps/augmentation_level/steps/cfg/min_cfg/seed/filename_prefix) + fps float en SaveAnimatedWEBP", "determinismo: misma entrada -> mismo dict (builder puro)"]
test_file_path: "python/functions/ml/tests/test_comfyui_build_img2vid_workflow.py"
file_path: "python/functions/ml/comfyui_build_img2vid_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_img2vid_workflow import comfyui_build_img2vid_workflow
wf = comfyui_build_img2vid_workflow(
"example.png",
width=1024, height=576, video_frames=14,
motion_bucket_id=127, fps=6, steps=20, seed=42,
)
# wf["12"]["class_type"] == "SVD_img2vid_Conditioning"
# wf["30"]["class_type"] == "SaveAnimatedWEBP"
# -> comfyui_submit_workflow(wf) para encolar el clip (necesita GPU)
```
O lanzable directo con: `./fn run comfyui_build_img2vid_workflow` (imprime el JSON del workflow SVD de ejemplo).
## Cuando usarla
Antes de enviar una generacion de video img2vid (animar una imagen estatica) a
ComfyUI: construye aqui el dict del workflow SVD y pasalo a
`comfyui_submit_workflow`. Usala cuando partes de UNA imagen y quieres un clip
corto derivado de ella (SVD no toma prompt de texto). Para texto -> video usa la
hermana `comfyui_build_video_workflow` (LTX/Wan). Verifica el workflow contra el
servidor con `comfyui_validate_workflow` antes de encolar.
## Gotchas
- Es API format (nodos numerados), NO el formato de la UI de ComfyUI. Es lo que
acepta POST /prompt.
- SVD NO usa prompts de texto. El condicionamiento sale de la imagen base via el
CLIP_VISION del checkpoint todo-en-uno; por eso no hay nodos CLIPTextEncode.
- El checkpoint `svd.safetensors` debe existir y ser visible para el servidor
(carpeta de checkpoints o extra_model_paths) o ComfyUI rechaza el workflow con
HTTP 400 al enviarlo. Esta funcion es pura y no valida contra el servidor.
- La imagen `image` debe estar en la carpeta input/ del servidor (subela antes con
el endpoint de upload o el nodo LoadImage de la UI). El validador estructural NO
comprueba la existencia de la imagen (image no es un input de modelo).
- VRAM 8GB: SVD es pesado. Con los defaults (1024x576, 14 frames) el modelo base
puede acercarse al techo de 8GB. Si da OOM, bajar resolucion (768x448) o
video_frames. La generacion real (submit) es un paso posterior con GPU; este
builder solo arma el dict y se valida de forma estructural (offline).
- `svd.safetensors` es el modelo de 14 frames. La variante `svd_xt` admite 25;
con el base, video_frames > 14 puede degradar el clip.
- motion_bucket_id alto = mas movimiento (y mas artefactos). 127 es el centro
recomendado por Stability.
- cfg se mantiene baja (~2.5) y se interpola con VideoLinearCFGGuidance (min_cfg en
el primer frame -> cfg en el ultimo). Subir cfg degrada el video.
- SaveAnimatedWEBP declara `fps` como FLOAT en /object_info: el builder pasa
`float(fps)` para no provocar HTTP 400. El nodo VHS_VideoCombine NO esta instalado
en este servidor; por eso el guardado usa el SaveAnimatedWEBP nativo.
@@ -0,0 +1,152 @@
"""Construye un workflow ComfyUI img2vid (SVD) en "API format" (dict de nodos numerados).
Implementa la plantilla canonica de Stable Video Diffusion de ComfyUI: a partir de
una imagen estatica genera un clip corto de video. El checkpoint `svd.safetensors`
es todo-en-uno (UNet + VAE + CLIP image encoder), cargado con
ImageOnlyCheckpointLoader (da MODEL, CLIP_VISION y VAE de una sola pieza).
Cadena de nodos:
ImageOnlyCheckpointLoader (MODEL, CLIP_VISION, VAE) + LoadImage (imagen base) ->
SVD_img2vid_Conditioning (positive, negative, latent) ->
VideoLinearCFGGuidance (interpola cfg de min_cfg a cfg a lo largo del clip) ->
KSampler (denoise 1.0) -> VAEDecode (secuencia de frames) -> SaveAnimatedWEBP.
A diferencia de los modelos txt2video (LTX/Wan), SVD no usa prompts de texto: el
condicionamiento sale de la imagen via el CLIP_VISION del propio checkpoint. El
movimiento se controla con motion_bucket_id (mas alto = mas movimiento) y fps.
Funcion pura: sin red, sin I/O. Determinista para los mismos argumentos.
"""
def comfyui_build_img2vid_workflow(
image: str,
*,
ckpt: str = "svd.safetensors",
width: int = 1024,
height: int = 576,
video_frames: int = 14,
motion_bucket_id: int = 127,
fps: int = 6,
augmentation_level: float = 0.0,
steps: int = 20,
cfg: float = 2.5,
min_cfg: float = 1.0,
seed: int = 0,
sampler_name: str = "euler",
scheduler: str = "karras",
filename_prefix: str = "comfy_svd",
) -> dict:
"""Construye el dict del workflow img2vid (SVD) para svd.safetensors.
Args:
image: nombre del archivo de imagen base dentro de la carpeta input/ del
servidor ComfyUI (lo que carga el nodo LoadImage). Es el frame inicial
del que SVD deriva el clip.
ckpt: nombre del checkpoint SVD tal como lo ve el servidor. Por defecto
"svd.safetensors" (todo-en-uno: UNet + VAE + CLIP image encoder).
keyword-only.
width: ancho del video en px (multiplo de 8; SVD base entrena a 1024).
keyword-only.
height: alto del video en px (multiplo de 8; SVD base entrena a 576).
keyword-only.
video_frames: numero de frames del clip. svd.safetensors es el modelo de
14 frames; el variante xt llega a 25. keyword-only.
motion_bucket_id: intensidad de movimiento (1-255 util; 127 por defecto).
Mas alto = mas movimiento. keyword-only.
fps: frames por segundo con que se condiciona y se guarda el clip.
keyword-only.
augmentation_level: ruido anadido a la imagen base (0.0 = fiel a la base;
subirlo da mas libertad/movimiento a costa de fidelidad). keyword-only.
steps: pasos de sampling del KSampler. keyword-only.
cfg: guidance scale del ultimo frame (VideoLinearCFGGuidance interpola de
min_cfg al primer frame hasta cfg al ultimo). SVD usa cfg baja (~2.5).
keyword-only.
min_cfg: guidance scale del primer frame para VideoLinearCFGGuidance.
keyword-only.
seed: semilla del sampler (0 = determinista; cambiar para variar el clip).
keyword-only.
sampler_name: algoritmo del KSampler. Por defecto "euler". keyword-only.
scheduler: scheduler del KSampler. Por defecto "karras". keyword-only.
filename_prefix: prefijo del archivo de salida (.webp animado).
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. Devuelve 7 nodos:
ImageOnlyCheckpointLoader, LoadImage, SVD_img2vid_Conditioning,
VideoLinearCFGGuidance, KSampler, VAEDecode y SaveAnimatedWEBP. El denoise
del KSampler se fija a 1.0 (img2vid genera desde latente vacio condicionado,
no es img2img).
"""
return {
"15": {
"class_type": "ImageOnlyCheckpointLoader",
"inputs": {"ckpt_name": ckpt},
},
"23": {
"class_type": "LoadImage",
"inputs": {"image": image},
},
"12": {
"class_type": "SVD_img2vid_Conditioning",
"inputs": {
"clip_vision": ["15", 1],
"init_image": ["23", 0],
"vae": ["15", 2],
"width": width,
"height": height,
"video_frames": video_frames,
"motion_bucket_id": motion_bucket_id,
"fps": fps,
"augmentation_level": augmentation_level,
},
},
"14": {
"class_type": "VideoLinearCFGGuidance",
"inputs": {"model": ["15", 0], "min_cfg": min_cfg},
},
"3": {
"class_type": "KSampler",
"inputs": {
"seed": seed,
"steps": steps,
"cfg": cfg,
"sampler_name": sampler_name,
"scheduler": scheduler,
"denoise": 1.0,
"model": ["14", 0],
"positive": ["12", 0],
"negative": ["12", 1],
"latent_image": ["12", 2],
},
},
"8": {
"class_type": "VAEDecode",
"inputs": {"samples": ["3", 0], "vae": ["15", 2]},
},
"30": {
"class_type": "SaveAnimatedWEBP",
"inputs": {
"images": ["8", 0],
"filename_prefix": filename_prefix,
"fps": float(fps),
"lossless": False,
"quality": 90,
"method": "default",
},
},
}
if __name__ == "__main__":
import json
wf = comfyui_build_img2vid_workflow(
"example.png",
motion_bucket_id=127,
fps=6,
video_frames=14,
seed=42,
)
print(json.dumps(wf, indent=2))
@@ -0,0 +1,87 @@
"""Tests de estructura para comfyui_build_img2vid_workflow (funcion pura, SVD)."""
import os
import sys
sys.path.insert(0, os.path.dirname(__file__))
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", ".."))
from ml.comfyui_build_img2vid_workflow import comfyui_build_img2vid_workflow
from _comfyui_wf_assert import assert_api_format, class_types, node_by_ct
def test_estructura_y_nodos_svd():
wf = comfyui_build_img2vid_workflow("example.png")
assert_api_format(wf)
cts = class_types(wf)
for ct in (
"ImageOnlyCheckpointLoader",
"LoadImage",
"SVD_img2vid_Conditioning",
"VideoLinearCFGGuidance",
"KSampler",
"VAEDecode",
"SaveAnimatedWEBP",
):
assert ct in cts, f"falta {ct} en SVD img2vid"
# El checkpoint SVD es todo-en-uno cargado con el loader image-only.
assert node_by_ct(wf, "ImageOnlyCheckpointLoader")["inputs"]["ckpt_name"] == (
"svd.safetensors"
)
# La imagen base entra por LoadImage.
assert node_by_ct(wf, "LoadImage")["inputs"]["image"] == "example.png"
def test_cableado_de_nodos():
wf = comfyui_build_img2vid_workflow("example.png")
cond = node_by_ct(wf, "SVD_img2vid_Conditioning")["inputs"]
# clip_vision y vae salen del checkpoint image-only (outputs 1 y 2).
assert cond["clip_vision"] == ["15", 1]
assert cond["vae"] == ["15", 2]
assert cond["init_image"] == ["23", 0]
ks = node_by_ct(wf, "KSampler")["inputs"]
# KSampler usa el MODEL post VideoLinearCFGGuidance y los 3 outputs del cond.
assert ks["model"] == ["14", 0]
assert ks["positive"] == ["12", 0]
assert ks["negative"] == ["12", 1]
assert ks["latent_image"] == ["12", 2]
assert ks["denoise"] == 1.0
# VideoLinearCFGGuidance toma el MODEL crudo del checkpoint.
assert node_by_ct(wf, "VideoLinearCFGGuidance")["inputs"]["model"] == ["15", 0]
def test_params_se_reflejan():
wf = comfyui_build_img2vid_workflow(
"fox_front.png",
width=768,
height=448,
video_frames=25,
motion_bucket_id=200,
fps=10,
augmentation_level=0.2,
steps=25,
cfg=3.0,
min_cfg=1.5,
seed=7,
filename_prefix="myclip",
)
cond = node_by_ct(wf, "SVD_img2vid_Conditioning")["inputs"]
assert cond["width"] == 768 and cond["height"] == 448
assert cond["video_frames"] == 25
assert cond["motion_bucket_id"] == 200
assert cond["fps"] == 10
assert cond["augmentation_level"] == 0.2
ks = node_by_ct(wf, "KSampler")["inputs"]
assert ks["steps"] == 25 and ks["cfg"] == 3.0 and ks["seed"] == 7
assert node_by_ct(wf, "VideoLinearCFGGuidance")["inputs"]["min_cfg"] == 1.5
save = node_by_ct(wf, "SaveAnimatedWEBP")["inputs"]
assert save["filename_prefix"] == "myclip"
# SaveAnimatedWEBP declara fps como FLOAT en /object_info.
assert save["fps"] == 10.0
assert isinstance(save["fps"], float)
def test_determinista():
a = comfyui_build_img2vid_workflow("example.png", seed=7)
b = comfyui_build_img2vid_workflow("example.png", seed=7)
assert a == b