Files
Meisoftcoltd-comfyui-sequen…/video.py
T
Meisoftcoltd 4fa7effccd feat(video): add loop index skipping logic to WanFrameValidator
- Added `current_loop_index` as a required parameter to `WanFrameValidator`.
- Set `"forceInput": True` to ensure the index parameter appears as a node connection rather than a UI widget.
- Implemented `skip_frames` calculation (`current_loop_index * corrected_frames`) so the video loader knows exactly where to start reading for each batch cycle.
- The `target_frames` parameter remains a UI widget.
2026-03-28 21:22:28 +00:00

166 lines
6.1 KiB
Python

import os
import torch
import torchaudio
import folder_paths
import nodes
import time
from . import register_node
@register_node
class WanFrameValidator:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
# target_frames se queda como un campo numérico en el nodo
"target_frames": ("INT", {"default": 49, "min": 1, "max": 10000}),
# current_loop_index se fuerza como un punto de conexión de entrada (cable)
"current_loop_index": ("INT", {"default": 0, "forceInput": True}),
}
}
RETURN_TYPES = ("INT", "INT")
RETURN_NAMES = ("valid_frames", "skip_frames")
FUNCTION = "validate"
CATEGORY = "🔁 Sequential Batcher/Video"
def validate(self, target_frames, current_loop_index):
# 1. Validar la regla estricta de WanVideo (4k+1)
k = (target_frames - 1) // 4
corrected_frames = max(1, (4 * k) + 1)
# 2. Calcular cuántos frames debe saltar el cargador en este ciclo
skip_frames = current_loop_index * corrected_frames
print(f"🛡️ [Wan Validator] Lote: {corrected_frames} frames | Saltar: {skip_frames} frames")
return (corrected_frames, skip_frames)
@register_node
class LoadVideoWithSourceAudio:
@classmethod
def INPUT_TYPES(cls):
# 1. Obtenemos los inputs de VHS dinámicamente en tiempo de ejecución de la UI
vhs_class = nodes.NODE_CLASS_MAPPINGS.get("VHS_LoadVideo")
if vhs_class:
inputs = vhs_class.INPUT_TYPES()
if "video" in inputs.get("required", {}):
video_in = inputs["required"]["video"]
# Desempaquetado seguro
if isinstance(video_in, (tuple, list)) and len(video_in) >= 2:
v_type, v_params = video_in[0], video_in[1]
else:
v_type = video_in if isinstance(video_in, str) else "VIDEO"
v_params = {}
v_params["video_upload"] = True
inputs["required"]["video"] = (v_type, v_params)
return inputs
return {"required": {"video": ("VIDEO", {"video_upload": True})}}
RETURN_TYPES = ("IMAGE", "INT", "AUDIO", "VHS_VIDEOINFO", "AUDIO")
RETURN_NAMES = ("IMAGE", "frame_count", "audio", "video_info", "source_audio")
FUNCTION = "load_video_with_audio"
CATEGORY = "🔁 Sequential Batcher/Video"
@classmethod
def IS_CHANGED(cls, **kwargs):
return time.time()
@classmethod
def VALIDATE_INPUTS(cls, video, **kwargs):
path = folder_paths.get_annotated_filepath(video)
return True if os.path.exists(path) else f"Archivo no encontrado: {path}"
def load_video_with_audio(self, **kwargs):
vhs_class = nodes.NODE_CLASS_MAPPINGS.get("VHS_LoadVideo")
if not vhs_class:
raise Exception("❌ VideoHelperSuite no está instalado o cargado.")
vhs_instance = vhs_class()
# 2. EL TRUCO MÁGICO: Preguntamos a VHS qué parámetros exactos acepta en esta versión
vhs_inputs = vhs_class.INPUT_TYPES()
allowed_keys = set()
for cat in ["required", "optional", "hidden"]:
if cat in vhs_inputs:
allowed_keys.update(vhs_inputs[cat].keys())
# 3. Filtramos los kwargs estrictamente por lo que VHS nos acaba de decir
vhs_kwargs = {k: v for k, v in kwargs.items() if k in allowed_keys}
# 4. Ejecutamos VHS de forma segura
vhs_output = vhs_instance.load_video(**vhs_kwargs)
# Extraemos retornos
if isinstance(vhs_output, dict):
res = list(vhs_output.get("result", []))
ui = vhs_output.get("ui", {})
else:
res = list(vhs_output)
ui = {}
# 5. Extraer audio original
raw_video = kwargs.get("video")
video_name = raw_video[0] if isinstance(raw_video, (list, tuple)) else raw_video
video_path = folder_paths.get_annotated_filepath(video_name) if video_name else ""
source_audio = None
try:
if os.path.exists(video_path):
waveform, sample_rate = torchaudio.load(video_path)
source_audio = {"waveform": waveform.unsqueeze(0), "sample_rate": sample_rate}
except Exception as e:
print(f"⚠️ [LoadVideo] Error extrayendo source_audio: {e}")
# Añadimos la quinta salida (el source audio)
if len(res) >= 4:
res = [res[0], res[1], res[2], res[3], source_audio]
else:
res.append(source_audio)
return {"ui": ui, "result": tuple(res)}
@register_node
class IncrementalVideoStitcher:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"images": ("IMAGE",),
"audio": ("AUDIO",),
"current_loop_index": ("INT", {"default": 0}),
"total_loops": ("INT", {"default": 1}),
}
}
RETURN_TYPES = ("IMAGE", "AUDIO")
RETURN_NAMES = ("ALL_IMAGES", "AUDIO_OUT")
FUNCTION = "stitch"
CATEGORY = "🔁 Sequential Batcher/Video"
def stitch(self, images, audio, current_loop_index, total_loops):
cache_dir = os.path.join(folder_paths.get_temp_directory(), "wan_stitcher_cache")
os.makedirs(cache_dir, exist_ok=True)
path = os.path.join(cache_dir, f"batch_{current_loop_index:04d}.pt")
torch.save(images.cpu(), path)
print(f"🎞️ [Stitcher] Lote {current_loop_index} guardado en disco.")
if current_loop_index < total_loops - 1:
return (torch.zeros((1, 8, 8, 3)), None)
print(f"📦 [Stitcher] Ensamblando todos los lotes de vídeo...")
all_tensors = []
for i in range(total_loops):
p = os.path.join(cache_dir, f"batch_{i:04d}.pt")
if os.path.exists(p):
all_tensors.append(torch.load(p))
try: os.remove(p)
except: pass
final_images = torch.cat(all_tensors, dim=0)
print(f"✅ [Stitcher] Vídeo completado: {final_images.shape[0]} frames.")
return (final_images, audio)