diff --git a/README.md b/README.md index 82f7b8e..eed5714 100644 --- a/README.md +++ b/README.md @@ -28,9 +28,9 @@ El sistema se construye alrededor de tres categorías principales: ### 🎞️ Video (Ensamblaje y Validación) 5. **🛡️ Wan Frame Validator (`WanFrameValidator`)**: Valida y corrige el número objetivo de fotogramas para asegurar que encajen en la fórmula `4k+1` requerida por modelos específicos (ej. Wan). 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. - - **🎵 Passthrough de Audio (¡Nuevo!):** Alimenta directamente el audio original hacia el archivo ensamblado en el último ciclo. -7. **🎥 Load Video + Source Audio (`LoadVideoWithSourceAudio`)**: (¡Nuevo!) Funciona exactamente igual que un cargador de vídeo de VHS, 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. + - **🧠 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. ## Configuración y Uso diff --git a/README_EN.md b/README_EN.md index 6379541..d30db3e 100644 --- a/README_EN.md +++ b/README_EN.md @@ -28,9 +28,9 @@ The system is built around three major categories: ### 🎞️ Video (Assembly and Validation) 5. **🛡️ Wan Frame Validator (`WanFrameValidator`)**: Validates and corrects the target number of frames to ensure they fit the `4k+1` formula required by specific models (e.g., Wan). 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. - - **🎵 Audio Passthrough (New!):** Feeds the original pure audio straight to the assembled output during the final loop iteration. -7. **🎥 Load Video + Source Audio (`LoadVideoWithSourceAudio`)**: (New!) Functions exactly like the standard VHS video loader, but extracts and safely exposes the **complete**, uncropped original audio track to ensure it travels unaltered throughout the sequential process. + - **🧠 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. ## Setup & Usage diff --git a/__init__.py b/__init__.py index 261e9cb..1d6fe62 100644 --- a/__init__.py +++ b/__init__.py @@ -20,6 +20,7 @@ NODE_DISPLAY_NAME_MAPPINGS.update({ # Video (Ensamblaje y Validación) "IncrementalVideoStitcher": "🎞️ Incremental Auto-Stitcher", + "LoadVideoWithSourceAudio": "🎥 Load Video + Source Audio", "WanFrameValidator": "🛡️ Wan Frame Validator", # Loop (Orquestación Autónoma) diff --git a/__pycache__/video.cpython-312.pyc b/__pycache__/video.cpython-312.pyc deleted file mode 100644 index 2d136be..0000000 Binary files a/__pycache__/video.cpython-312.pyc and /dev/null differ diff --git a/video.py b/video.py index 31d94ee..1ff8cf5 100644 --- a/video.py +++ b/video.py @@ -60,6 +60,7 @@ class IncrementalVideoStitcher: } } + INPUT_IS_LIST = False RETURN_TYPES = ("IMAGE", "AUDIO") RETURN_NAMES = ("ALL_IMAGES", "AUDIO_OUT") OUTPUT_NODE = True @@ -162,53 +163,69 @@ import nodes class LoadVideoWithSourceAudio: @classmethod def INPUT_TYPES(s): - # Clonar los inputs dinámicamente si VHS está instalado vhs_class = nodes.NODE_CLASS_MAPPINGS.get("VHS_LoadVideo") if vhs_class: - return vhs_class.INPUT_TYPES() + inputs = vhs_class.INPUT_TYPES() + # Forzar la aparición del botón de subida ("Upload") + if "video" in inputs["required"]: + video_tuple = list(inputs["required"]["video"]) + if len(video_tuple) > 1 and isinstance(video_tuple[1], dict): + video_tuple[1]["video_upload"] = True + else: + video_tuple = (video_tuple[0], {"video_upload": True}) + inputs["required"]["video"] = tuple(video_tuple) + return inputs else: - raise Exception("❌ VHS_LoadVideo no encontrado. Instala VideoHelperSuite.") + raise Exception("VHS_LoadVideo no encontrado. Instala VideoHelperSuite.") 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" + FUNCTION = "load_video_with_audio" - # Intentamos mantener el Display Name para la interfaz web a través del Custom Node mapping original de ComfyUI (opcional pero util para que se llame diferente a VHS) - # Normalmente esto se hace en el NODE_DISPLAY_NAME_MAPPINGS global de __init__.py pero la clase será LoadVideoWithSourceAudio + @classmethod + def IS_CHANGED(s, video, **kwargs): + vhs_class = nodes.NODE_CLASS_MAPPINGS.get("VHS_LoadVideo") + if vhs_class and hasattr(vhs_class, "IS_CHANGED"): + return vhs_class.IS_CHANGED(video, **kwargs) + return float("NaN") + + @classmethod + def VALIDATE_INPUTS(s, video, **kwargs): + vhs_class = nodes.NODE_CLASS_MAPPINGS.get("VHS_LoadVideo") + if vhs_class and hasattr(vhs_class, "VALIDATE_INPUTS"): + return vhs_class.VALIDATE_INPUTS(video, **kwargs) + return True def load_video_with_audio(self, **kwargs): vhs_class = nodes.NODE_CLASS_MAPPINGS.get("VHS_LoadVideo") - if not vhs_class: - raise Exception("❌ VHS_LoadVideo no encontrado.") - print(f"\n{'='*50}") - print(f"🎥 [DEBUG] NODO: Load Video + Source Audio") - - # 1. Instanciar el nodo original y ejecutar su lógica - print(f" -> Ejecutando lógica original de VHS...") + # 1. Ejecutar el nodo VHS original vhs_instance = vhs_class() - vhs_result = vhs_instance.load_video(**kwargs) + vhs_output = vhs_instance.load_video(**kwargs) - # vhs_result contiene: (IMAGE, frame_count, audio, video_info) + # 2. Recuperar la interfaz (Preview UI) y los resultados + if isinstance(vhs_output, dict) and "result" in vhs_output: + res = vhs_output["result"] + ui = vhs_output.get("ui", {}) + else: + res = vhs_output + ui = {} - # 2. Lógica nueva: Extraer el audio completo del archivo fuente - video_filename = kwargs.get("video") - # El nombre del archivo puede venir como una ruta completa desde input, o desde un subdirectorio. - # En VHS y ComfyUI, usan get_annotated_filepath (esto lo traemos si hace falta, o simplemente path del input dir) - # VHS lo carga así: - video_path = folder_paths.get_annotated_filepath(video_filename) + # 3. Extraer el audio original completo + video_name = kwargs.get("video") + video_path = folder_paths.get_annotated_filepath(video_name) - print(f" -> 🎵 Extrayendo pista de audio completa (sin cortes) desde: {video_path}") + source_audio = None try: - waveform, sample_rate = torchaudio.load(video_path) - # Retornamos el diccionario en el formato estándar AUDIO de ComfyUI - source_audio = {"waveform": waveform.unsqueeze(0), "sample_rate": sample_rate} - print(f" -> ✅ Audio extraído con éxito. Muestreo: {sample_rate}Hz") + 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" -> ⚠️ Error cargando audio fuente: {e}") - source_audio = None + print(f"⚠️ [LoadVideo] Error cargando source_audio: {e}") - print(f"{'='*50}\n") - # 3. Devolver el resultado original + nuestro audio completo en el 5º puerto - return (vhs_result[0], vhs_result[1], vhs_result[2], vhs_result[3], source_audio) \ No newline at end of file + # 4. Retornar el diccionario con la UI intacta (restaura el reproductor visual) + return { + "ui": ui, + "result": (res[0], res[1], res[2], res[3], source_audio) + } \ No newline at end of file