diff --git a/README.md b/README.md index eed5714..04c6557 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/README_EN.md b/README_EN.md index d30db3e..f06bb2c 100644 --- a/README_EN.md +++ b/README_EN.md @@ -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. diff --git a/video.py b/video.py index 565c690..6df9722 100644 --- a/video.py +++ b/video.py @@ -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)