diff --git a/README.md b/README.md index 8d17a8d..1f2f9c7 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,13 @@ Video models produce many frames that can easily exceed 24GB VRAM. 1. **Split**: Use `Latent Batch To List` to turn your video latent into a list of single frames. 2. **Process**: Connect to your KSampler/VAE Decoder. ComfyUI will process Frame 1, then Frame 2, then Frame 3... 3. **Gather**: Use `Latent List To Batch` (or `Image List To Batch` if you decoded first) to reconstruct the full video batch for saving. +4. **Stitch (VHS Combine)**: To stitch heavy video chunks sequentially: + - *Option A (Math)*: `Range` -> `MakeBatch` (attribute: "skip_frames") -> `BatchToList` -> `GetAttributeInt` (name: "skip_frames") connected to `VHS_LoadVideo`. + - *Option B (CSV)*: `LoadCSV` (with a `skip_frames` column) -> `BatchToList` -> `GetAttributeInt`. + - Connect the output of `VHS_VideoCombine` (specifically the `VHS_FILENAMES` output) to the **FFmpeg Video Stitcher** node (`video_paths`). The Stitcher will wait for all chunks to finish and combine them automatically! + +> [!WARNING] +> **OOM Prevention (Memory Warning):** While `BatchToList` solves the data passing issue, you must still manage PyTorch VRAM. It is highly recommended to use the **easy cleanGpuUsed** node (or similar VRAM clearing nodes) after the VAE Decoder in each cycle of the Batch. Without manual VRAM clearing in each iteration, the sequential loop will accumulate garbage in your graphics card and eventually cause an Out of Memory error. --- diff --git a/README_ES.md b/README_ES.md index 6771055..afd856e 100644 --- a/README_ES.md +++ b/README_ES.md @@ -34,6 +34,13 @@ Los modelos de vídeo generan muchos fotogramas que pueden superar fácilmente l 1. **Split (Dividir)**: Usa `Latent Batch To List` para convertir tu latente de vídeo en una lista de fotogramas individuales. 2. **Process (Procesar)**: Conecta a tu KSampler o VAE Decoder. ComfyUI procesará el Fotograma 1, luego el 2, luego el 3... ahorrando memoria. 3. **Gather (Reunir)**: Usa `Latent List To Batch` (o `Image List To Batch` si decodificaste primero) para reconstruir el lote completo de vídeo para guardarlo. +4. **Stitch (Unir con VHS)**: Para unir chunks pesados de vídeo secuencialmente: + - *Opción A (Matemática)*: `Range` -> `MakeBatch` (atributo: "skip_frames") -> `BatchToList` -> `GetAttributeInt` (nombre: "skip_frames") conectado a `VHS_LoadVideo`. + - *Opción B (Por CSV)*: `LoadCSV` (con una columna `skip_frames`) -> `BatchToList` -> `GetAttributeInt`. + - Conecta la salida de `VHS_VideoCombine` (específicamente la salida `VHS_FILENAMES`) al nodo **FFmpeg Video Stitcher** (`video_paths`). ¡El Stitcher esperará a que terminen todos los chunks y los unirá automáticamente! + +> [!WARNING] +> **Prevención OOM (Aviso de Memoria):** Aunque `BatchToList` soluciona el paso de datos, a nivel de VRAM de PyTorch, se debe seguir gestionando la memoria. Es altamente recomendable usar el nodo **easy cleanGpuUsed** (o similar) después del decodificador (VAE) en cada ciclo del Batch. Sin la liberación manual de VRAM en cada iteración, el bucle secuencial acumulará basura en la tarjeta gráfica y eventualmente provocará un error de "Out of Memory". --- diff --git a/__init__.py b/__init__.py index 2315a32..a008bfe 100644 --- a/__init__.py +++ b/__init__.py @@ -12,7 +12,7 @@ def register_node(c, display_name=None): return c -from . import sequence, paths, batch, image, debug, loop +from . import sequence, paths, batch, image, debug, loop, video # Display name mappings with emojis NODE_DISPLAY_NAME_MAPPINGS.update({ @@ -53,6 +53,9 @@ NODE_DISPLAY_NAME_MAPPINGS.update({ "LatentBatchToList": "🎞️ Latent Batch To List", "LatentListToBatch": "🎞️ Latent List To Batch", + # Video + "FFmpegVideoStitcher": "🎞️ FFmpeg Video Stitcher", + # Debug "Stringify": "🧵 Stringify", "Interact": "⌨️ Interact", diff --git a/__pycache__/__init__.cpython-312.pyc b/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 6e206bb..0000000 Binary files a/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/__pycache__/batch.cpython-312.pyc b/__pycache__/batch.cpython-312.pyc deleted file mode 100644 index a8016eb..0000000 Binary files a/__pycache__/batch.cpython-312.pyc and /dev/null differ diff --git a/__pycache__/debug.cpython-312.pyc b/__pycache__/debug.cpython-312.pyc deleted file mode 100644 index bbbbaaf..0000000 Binary files a/__pycache__/debug.cpython-312.pyc and /dev/null differ diff --git a/__pycache__/image.cpython-312.pyc b/__pycache__/image.cpython-312.pyc deleted file mode 100644 index 38c5754..0000000 Binary files a/__pycache__/image.cpython-312.pyc and /dev/null differ diff --git a/__pycache__/loop.cpython-312.pyc b/__pycache__/loop.cpython-312.pyc deleted file mode 100644 index fffe64f..0000000 Binary files a/__pycache__/loop.cpython-312.pyc and /dev/null differ diff --git a/__pycache__/paths.cpython-312.pyc b/__pycache__/paths.cpython-312.pyc deleted file mode 100644 index 0b5e237..0000000 Binary files a/__pycache__/paths.cpython-312.pyc and /dev/null differ diff --git a/__pycache__/sequence.cpython-312.pyc b/__pycache__/sequence.cpython-312.pyc deleted file mode 100644 index 289d03e..0000000 Binary files a/__pycache__/sequence.cpython-312.pyc and /dev/null differ diff --git a/video.py b/video.py new file mode 100644 index 0000000..ebd1c97 --- /dev/null +++ b/video.py @@ -0,0 +1,70 @@ +import os +import shutil +import subprocess +import folder_paths +from . import register_node + +@register_node +class FFmpegVideoStitcher: + """Toma una lista de rutas de vídeo generadas en un Batch secuencial, crea un TXT/CSV y los une con FFmpeg.""" + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + # VHS_FILENAMES proviene del nodo de guardado de vídeo + "video_paths": ("VHS_FILENAMES", ), + "output_filename": ("STRING", {"default": "Video_SCAIL_Completo.mp4"}), + }, + } + + RETURN_TYPES = ("STRING", ) + RETURN_NAMES = ("final_video_path", ) + # Al ser INPUT_IS_LIST = True, ComfyUI acumulará los resultados de todos los + # pasos del Batch y ejecutará esto al final del todo. + INPUT_IS_LIST = True + FUNCTION = "stitch_videos" + CATEGORY = "🔁 Sequential Batcher/Video" + + def stitch_videos(self, video_paths, output_filename): + # Aplanar la lista de rutas, ya que VHS_FILENAMES suele venir anidado + flat_paths = [] + for path_group in video_paths: + for p in path_group: + if isinstance(p, str) and p.endswith(".mp4"): + flat_paths.append(p) + + # Como INPUT_IS_LIST es True, output_filename viene como lista, tomamos el primero + out_name = output_filename[0] if isinstance(output_filename, list) else output_filename + + if not flat_paths: + return ("Error: No se encontraron vídeos para unir en el Batch.", ) + + if not shutil.which("ffmpeg"): + raise FileNotFoundError("FFmpeg is not installed or not found in system PATH. Please install FFmpeg to stitch videos.") + + # 1. Crear el archivo de lista de concatenación en el directorio de salida estándar + output_dir = folder_paths.get_output_directory() + list_file_path = os.path.join(output_dir, "batch_concat_list.txt") + + with open(list_file_path, 'w', encoding='utf-8') as f: + for video in sorted(flat_paths): # Mantener el orden de los chunks + f.write(f"file '{os.path.abspath(video)}'\n") + + # 2. Definir ruta final + final_output = os.path.join(output_dir, out_name) + + # 3. FFmpeg en modo copia (rápido y sin recodificar) + ffmpeg_cmd = [ + "ffmpeg", "-y", "-f", "concat", "-safe", "0", + "-i", list_file_path, "-c", "copy", final_output + ] + + try: + subprocess.run(ffmpeg_cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + print(f"[Sequential Batcher] Vídeo final ensamblado exitosamente: {final_output}") + except subprocess.CalledProcessError as e: + print(f"[Sequential Batcher] Error de FFmpeg: {e.stderr.decode()}") + raise e + + return (final_output, )