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
@@ -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