Refactor LoadVideoWithSourceAudio to inherit from VHS_LoadVideo.

Implement safe audio passthrough in `IncrementalVideoStitcher`.
Update README files to reflect VHS as a mandatory dependency.
This commit is contained in:
Meisoftcoltd
2026-03-28 19:36:51 +00:00
parent 6825022800
commit 1bed6fbcbb
3 changed files with 52 additions and 65 deletions
+2 -2
View File
@@ -30,12 +30,12 @@ El sistema se construye alrededor de tres categorías principales:
6. **🎞️ Incremental Auto-Stitcher (`IncrementalVideoStitcher`)**: Archiva progresivamente los tensores generados en el disco duro y los ensambla de forma segura al final de todos los ciclos.
- **🧠 Cero OOM (¡Nuevo!):** Sustituye las acumulaciones en memoria por guardados temporales en disco (`.pt`), borrando la RAM de inmediato para poder procesar vídeos infinitos sin colapsar el sistema. Al desactivar `INPUT_IS_LIST`, maneja tensores puros eficientemente.
- **🎵 Passthrough de Audio (¡Nuevo!):** Alimenta directamente el audio original hacia el archivo ensamblado en el último ciclo (devolviendo `None` en los ciclos intermedios para ahorrar recursos).
7. **🎥 Load Video + Source Audio (`LoadVideoWithSourceAudio`)**: (¡Nuevo!) Funciona exactamente igual que un cargador de vídeo de VHS (incluyendo la vista previa en la UI y el botón de subida), pero extrae y expone de manera segura la pista de audio original **completa** y sin recortes para asegurar que viaja inalterada a lo largo del proceso secuencial.
7. **🎥 Load Video + Source Audio (`LoadVideoWithSourceAudio`)**: (¡Nuevo!) Este nodo **hereda directamente de la clase original de VHS (`VHS_LoadVideo`)**. Funciona exactamente igual (incluyendo validaciones, vista previa en la UI y el botón de subida), pero extrae y expone de manera segura la pista de audio original **completa** y sin recortes para asegurar que viaja inalterada a lo largo del proceso secuencial.
## Configuración y Uso
### Prerrequisitos
- **VideoHelperSuite (VHS)**: Requerido de forma dinámica para que el nodo `Load Video + Source Audio` funcione y replique su interfaz de manera correcta.
- **VideoHelperSuite (VHS)**: **Obligatorio** para que el nodo `Load Video + Source Audio` funcione. Al heredar de su clase base, si VHS no está instalado en tu entorno de ComfyUI, este nodo no se cargará.
- **FFmpeg**: Debe estar instalado y disponible en el PATH del sistema para manejar procesos subyacentes de vídeo.
- **Torchaudio**: (Generalmente incluido en los entornos ComfyUI) es necesario para extraer la pista de audio fuente original de forma pura.
+2 -2
View File
@@ -30,12 +30,12 @@ The system is built around three major categories:
6. **🎞️ Incremental Auto-Stitcher (`IncrementalVideoStitcher`)**: Progressively archives generated tensors directly to the hard drive and safely assembles them at the end of all cycles.
- **🧠 Zero OOM (New!):** Replaces RAM accumulation with progressive temporary disk saves (`.pt`), clearing system memory immediately to enable infinite video processing without crashing the system. By disabling `INPUT_IS_LIST`, it handles raw tensors efficiently.
- **🎵 Audio Passthrough (New!):** Feeds the original pure audio straight to the assembled output during the final loop iteration (returning `None` during intermediate loops to save resources).
7. **🎥 Load Video + Source Audio (`LoadVideoWithSourceAudio`)**: (New!) Functions exactly like the standard VHS video loader (including UI preview widget and upload button), but extracts and safely exposes the **complete**, uncropped original audio track to ensure it travels unaltered throughout the sequential process.
7. **🎥 Load Video + Source Audio (`LoadVideoWithSourceAudio`)**: (New!) This node **inherits directly from the original VHS class (`VHS_LoadVideo`)**. It functions exactly the same (including validations, UI preview widget, and upload button), but extracts and safely exposes the **complete**, uncropped original audio track to ensure it travels unaltered throughout the sequential process.
## Setup & Usage
### Prerequisites
- **VideoHelperSuite (VHS)**: Dynamically required for the `Load Video + Source Audio` node to function and replicate its UI seamlessly.
- **VideoHelperSuite (VHS)**: **Mandatory** for the `Load Video + Source Audio` node to function. Since it inherits from its base class, if VHS is not installed in your ComfyUI environment, this node will not load.
- **FFmpeg**: Must be installed and available in your system's PATH to manage underlying video operations.
- **Torchaudio**: (Usually included in ComfyUI environments) is required to extract the original pure audio track from the source.
+48 -61
View File
@@ -3,7 +3,6 @@ import torch
import torchaudio
import folder_paths
import nodes
import time
from . import register_node
@register_node
@@ -17,75 +16,65 @@ class WanFrameValidator:
CATEGORY = "🔁 Sequential Batcher/Video"
def validate(self, target_frames):
# Fórmula 4k+1 para WanVideo
k = (target_frames - 1) // 4
corrected_frames = (4 * k) + 1
print(f"🛡️ [Wan Validator] Fotogramas ajustados: {corrected_frames}")
return (max(1, corrected_frames), )
@register_node
class LoadVideoWithSourceAudio:
@classmethod
def INPUT_TYPES(s):
vhs_class = nodes.NODE_CLASS_MAPPINGS.get("VHS_LoadVideo")
if vhs_class:
inputs = vhs_class.INPUT_TYPES()
if "video" in inputs["required"]:
video_in = inputs["required"]["video"]
# 🛠️ FIX: Blindaje contra errores de desempaquetado (Unpacking fix)
if isinstance(video_in, (tuple, list)) and len(video_in) >= 2:
v_type, v_params = video_in
else:
v_type = video_in if isinstance(video_in, str) else "VIDEO"
v_params = {}
# 1. Obtenemos la clase original de VHS para heredar de ella
vhs_load_video_class = nodes.NODE_CLASS_MAPPINGS.get("VHS_LoadVideo")
v_params["video_upload"] = True
inputs["required"]["video"] = (v_type, v_params)
return inputs
return {"required": {"video": ("VIDEO", {"video_upload": True})}}
if vhs_load_video_class:
@register_node
class LoadVideoWithSourceAudio(vhs_load_video_class):
# Heredamos todo de VHS, solo añadimos nuestra salida extra
RETURN_TYPES = vhs_load_video_class.RETURN_TYPES + ("AUDIO",)
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"
# Intentamos heredar los nombres si existen, si no, usamos los por defecto + el nuestro
_base_names = getattr(vhs_load_video_class, "RETURN_NAMES", ("IMAGE", "frame_count", "audio", "video_info"))
RETURN_NAMES = _base_names + ("source_audio",)
@classmethod
def IS_CHANGED(s, video, **kwargs):
# Forzamos refresco de caché para evitar bloqueos en el bucle
return time.time()
FUNCTION = "load_video_with_source_audio"
CATEGORY = "🔁 Sequential Batcher/Video"
@classmethod
def VALIDATE_INPUTS(s, 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_source_audio(self, **kwargs):
# 1. Ejecutamos la función original del padre (VHS) tal cual
vhs_func_name = vhs_load_video_class.FUNCTION
vhs_func = getattr(self, vhs_func_name)
vhs_output = vhs_func(**kwargs)
def load_video_with_audio(self, **kwargs):
vhs_class = nodes.NODE_CLASS_MAPPINGS.get("VHS_LoadVideo")
vhs_instance = vhs_class()
# 2. Gestionamos si VHS devolvió la interfaz de vista previa o solo resultados
if isinstance(vhs_output, dict):
res = list(vhs_output["result"])
ui = vhs_output.get("ui", {})
else:
res = list(vhs_output)
ui = {}
# 🛠️ FIX: Filtro estricto para evitar el error 'force_rate' al llamar a VHS
vhs_keys = ["video", "force_rate", "frame_load_cap", "skip_first_frames",
"select_every_nth", "meta_batch", "vae", "format"]
vhs_kwargs = {k: v for k, v in kwargs.items() if k in vhs_keys}
# 3. Nuestra lógica añadida: Extraer audio fuente de forma segura
raw_video = kwargs.get("video")
video_name = raw_video[0] if isinstance(raw_video, (list, tuple)) else raw_video
vhs_output = vhs_instance.load_video(**vhs_kwargs)
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}")
# Gestionamos si VHS devuelve diccionario (con UI preview) o tupla
if isinstance(vhs_output, dict):
res, ui = vhs_output["result"], vhs_output.get("ui", {})
else:
res, ui = vhs_output, {}
# 4. Añadimos el audio al final de la lista original de salidas
res.append(source_audio)
# Extraer audio original para el montaje final
video_path = folder_paths.get_annotated_filepath(kwargs.get("video"))
source_audio = None
try:
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 al extraer audio: {e}")
# 5. Devolvemos el paquete exactamente en el formato que espera ComfyUI
if isinstance(vhs_output, dict):
return {"ui": ui, "result": tuple(res)}
else:
return tuple(res)
else:
print("⚠️ [Advertencia] VideoHelperSuite no encontrado. LoadVideoWithSourceAudio no funcionará.")
return {"ui": ui, "result": (res[0], res[1], res[2], res[3], source_audio)}
@register_node
class IncrementalVideoStitcher:
@@ -109,26 +98,24 @@ class IncrementalVideoStitcher:
cache_dir = os.path.join(folder_paths.get_temp_directory(), "wan_stitcher_cache")
os.makedirs(cache_dir, exist_ok=True)
# 💾 Guardado progresivo en disco (CPU) para proteger la VRAM
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:
# Ciclos intermedios: Micro-tensor (8x8) para mantener cables activos sin OOM
# Micro-tensor (8x8) para mantener cables activos sin petar la memoria
return (torch.zeros((1, 8, 8, 3)), None)
# 🚀 CICLO FINAL: Ensamblaje total
print(f"📦 [Stitcher] Uniendo {total_loops} lotes de vídeo...")
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) # Limpieza de temporales
try: os.remove(p)
except: pass
final_images = torch.cat(all_tensors, dim=0)
print(f"✅ [Stitcher] Vídeo ensamblado con éxito: {final_images.shape[0]} frames.")
print(f"✅ [Stitcher] Vídeo completado: {final_images.shape[0]} frames.")
return (final_images, audio)