Compare commits
73
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ecdda64169 | ||
|
|
8fc697a8c3 | ||
|
|
75ab5d9676 | ||
|
|
cba02bf08f | ||
|
|
8d8e3deb1e | ||
|
|
a875f536da | ||
|
|
12a55c7fd6 | ||
|
|
119626a20f | ||
|
|
b854a90bf9 | ||
|
|
e1181c468c | ||
|
|
e4f53ca75a | ||
|
|
16604f101c | ||
|
|
de0797ddb9 | ||
|
|
771f4bf709 | ||
|
|
d660a6b41b | ||
|
|
0805acb114 | ||
|
|
5983ff65b4 | ||
|
|
d4e507af7c | ||
|
|
863154901b | ||
|
|
6479883f6d | ||
|
|
89a5f767d7 | ||
|
|
fb35df1526 | ||
|
|
2551cd5832 | ||
|
|
3eb5dcb7ae | ||
|
|
ce6041da4a | ||
|
|
7d266f818c | ||
|
|
798d791593 | ||
|
|
41d752577c | ||
|
|
5be95e32ab | ||
|
|
3cf6c13d17 | ||
|
|
7187f783b4 | ||
|
|
10e0affe28 | ||
|
|
444b6c6513 | ||
|
|
490e3322d7 | ||
|
|
8dc273ff16 | ||
|
|
4c5d15567b | ||
|
|
242ff772a5 | ||
|
|
e49056c835 | ||
|
|
0d794fc10e | ||
|
|
55cab6c11b | ||
|
|
74238937dc | ||
|
|
f26a7ef983 | ||
|
|
1b75c40e3e | ||
|
|
5b70ccaf18 | ||
|
|
9e893334a1 | ||
|
|
dea765dbcd | ||
|
|
d06e88a9b2 | ||
|
|
cdc10e72c9 | ||
|
|
7ec112a2cd | ||
|
|
c214b71b76 | ||
|
|
bfa0125a21 | ||
|
|
2731ab0779 | ||
|
|
35975007e7 | ||
|
|
7cc86e6b94 | ||
|
|
63e84c2aa1 | ||
|
|
df18b573d0 | ||
|
|
ecd973b8e5 | ||
|
|
1c90c848e2 | ||
|
|
444dc2c350 | ||
|
|
1e36f9e915 | ||
|
|
1701eeb218 | ||
|
|
3f9c7cbee1 | ||
|
|
ff5643b098 | ||
|
|
e647ac0728 | ||
|
|
6080f61c27 | ||
|
|
9cc66a4c80 | ||
|
|
bd6016f559 | ||
|
|
8a0ceca136 | ||
|
|
608be49841 | ||
|
|
c7c8c93c9c | ||
|
|
a6d8ea88dc | ||
|
|
93d479fb6c | ||
|
|
30670f0602 |
@@ -0,0 +1 @@
|
||||
__pycache__/
|
||||
@@ -1,19 +1,25 @@
|
||||
# ♾️ ComfyUI Sequential Batcher
|
||||
**v1.5.2** | **VRAM-Optimized Generation** | **Recursive Self-Queuing**
|
||||
**v1.6.0** | **VRAM-Optimized Generation** | **Recursive Self-Queuing** | **Perfect Audio Sync**
|
||||
|
||||
Una suite de grado profesional de nodos personalizados para ComfyUI. Diseñada para sortear los límites de VRAM en la generación de vídeo pesado (WanVideo, Hunyuan, etc.) mediante **Auto-Encolado Recursivo** y procesamiento autónomo lote por lote.
|
||||
Una suite de grado profesional de nodos personalizados para ComfyUI. Diseñada para sortear los límites extremos de VRAM en la generación de vídeo pesado (WanVideo, Hunyuan, LTX) mediante **Auto-Encolado Recursivo**, procesamiento autónomo lote por lote y gestión de memoria forense.
|
||||
|
||||
> 🌍 **Read in English:** [README_EN.md](README_EN.md)
|
||||
|
||||
---
|
||||
|
||||
## 🌟 Lo Nuevo en v1.6.0 (Perfect Sync Update)
|
||||
* **🛡️ VAE Safe Frame Padder (Hold Last Frame):** El motor ahora expande dinámicamente los tensores incompletos al final del vídeo. Si faltan frames para cumplir los estrictos requisitos de WanVideo (regla de `4n + 1`) o LTX (8n+1), clona el último fotograma de forma imperceptible. **Resultado: Cero cuelgues del VAE y 100% de sincronización de audio sin micro-cortes.**
|
||||
* **📈 Inversión Matemática a "Expansión":** Los calculadores `AutoLoopCalculatorWan` y `AutoLoopCalculatorLTX` ahora redondean el timeline de forma segura hacia arriba, garantizando fluidez total en ciclos intermedios.
|
||||
|
||||
---
|
||||
|
||||
## ✨ Características Clave
|
||||
|
||||
* **Orquestación Autónoma:** Convierte tu ComfyUI en un motor de renderizado continuo. Pulsa "Queue" una sola vez y el flujo se retroalimentará hasta terminar todo el vídeo.
|
||||
* **Cortes Inteligentes (Smart Chunking):** Analiza el vídeo base y realiza cortes matemáticos evitando separar frames donde los rostros son más nítidos, manteniendo la coherencia de identidad.
|
||||
* **Extracción de Audio Nativa:** Extrae la pista de audio original directamente desde el nodo inicial y la inyecta de vuelta en el ensamblaje final.
|
||||
* **Dual Input Engine:** El nodo explorador acepta tanto vídeos subidos manualmente como rutas inyectadas por cable desde descargadores (ej. YTDLP) comportándose exactamente como VideoHelperSuite.
|
||||
* **Soporte Nativo LTX 2.3:** Se han añadido calculadores matemáticos específicos (`AutoLoopCalculatorLTX` y `ResTool64xLTX`) para garantizar que los lotes de vídeo cumplan con las estrictas reglas de descompresión de arquitectura DiT y el factor de escala latente (resoluciones divisibles por 64 y particiones de fotogramas de `8n + 1`).
|
||||
* **Orquestación Autónoma:** Convierte tu ComfyUI en un motor de renderizado continuo. Pulsa "Queue Prompt" una sola vez y el flujo se retroalimentará modificando semillas e índices hasta terminar todo el vídeo.
|
||||
* **Cortes Inteligentes (Smart Chunking):** Analiza el vídeo base y realiza cortes matemáticos protegiendo los frames donde los rostros son más nítidos, manteniendo la coherencia de identidad (Face Cuts).
|
||||
* **Extracción de Audio Nativa:** Extrae la pista original directamente desde el nodo inicial y la inyecta de vuelta en el ensamblaje final.
|
||||
* **Soporte Arquitecturas DiT (WanVideo & LTX 2.3):** Calculadores matemáticos específicos aseguran que los lotes de vídeo cumplan con las estrictas reglas de descompresión (regla de `4n + 1` para WanVideo, o regla `8n + 1` para LTX).
|
||||
* **Resoluciones Protegidas (Megapixel Shield):** Herramientas matemáticas (`ResTool`) que escalan las dimensiones automáticamente protegiendo el *training floor* de cada modelo base.
|
||||
|
||||
---
|
||||
|
||||
@@ -22,46 +28,56 @@ Una suite de grado profesional de nodos personalizados para ComfyUI. Diseñada p
|
||||
Para que el nodo `VideoAnalyzerWithAudio` pueda desempaquetar contenedores `.mp4` y extraer el audio con `torchaudio`, **FFmpeg es absolutamente obligatorio**.
|
||||
|
||||
* **🐧 Ubuntu / Linux / WSL2:**
|
||||
El script de ComfyUI Manager intentará instalarlo por ti. Si falla, ejecuta:
|
||||
El script intentará instalarlo por ti. Si falla, ejecuta manualmente:
|
||||
```bash
|
||||
sudo apt update && sudo apt install ffmpeg -y
|
||||
```
|
||||
* **🪟 Windows:**
|
||||
Descarga los binarios precompilados de FFmpeg y añade la carpeta bin a tus variables de entorno (PATH).
|
||||
Descarga los binarios precompilados de FFmpeg y añade la carpeta `bin` a tus variables de entorno (PATH).
|
||||
|
||||
## 🧠 La Arquitectura Híbrida (Cómo funciona)
|
||||
El procesamiento de vídeo se divide en roles altamente especializados:
|
||||
### 🚨 Advertencia Crítica sobre el Uso de la VRAM
|
||||
Se **desrecomienda encarecidamente** el uso del argumento `--highvram` al arrancar ComfyUI.
|
||||
|
||||
* **🕵️ Video Analyzer Face detector + Audio (El Explorador):** Escanea el vídeo vía OpenCV o mediante un modelo YOLO/ONNX acelerado por GPU (opcional) para detección precisa de rostros, extrae el audio y escupe un Frame de Referencia visual. Actúa como la puerta de entrada principal.
|
||||
* **📊 Auto Loop Calculator (El Cerebro):** Recibe la biometría del Explorador y calcula las coordenadas de corte asimétricas (chunk_frames, skip_frames) con un margen dinámico del ±10% para evitar minilotes residuales.
|
||||
* **📊 Auto Loop Calculator (WanVideo 3dVAE):** Alternativa para WanVideo que asegura que los chunks de frames sean estrictamente múltiplos de 4, protegiendo al VAE 3D de colapsar.
|
||||
* **🛠️ El Obrero (VHS_LoadVideo):** Liberado de tareas de análisis, este nodo estándar de ComfyUI se limita a extraer los tensores exactos que el Cerebro le ordena.
|
||||
* **🎞️ Incremental Auto-Stitcher (El Ensamblador):** Recoge los lotes renderizados y la pista de audio original, cosiendo el vídeo final de forma progresiva.
|
||||
* **📥 Session Image Receiver** y **📤 Session Image Sender:** Retienen y transfieren el último keyframe válido (frame de referencia) a través de los ciclos para mantener la coherencia temporal de la identidad.
|
||||
* **⏱️ Auto FPS Limiter (El Sincronizador):** Previene errores de VRAM en vídeos de alta tasa de fotogramas (ej. 60 FPS) calculando automáticamente el salto de frames necesario (`select_every_nth`) y ajustando los FPS finales para garantizar que el audio y el movimiento mantengan una sincronización perfecta.
|
||||
* **Resoluciones Protegidas (Megapixel Shield):** Herramientas de resolución matemáticas que escalan las dimensiones para evitar artefactos protegiendo el "training floor" de cada modelo:
|
||||
* **📐 ResTool 8x (SD1.5)**
|
||||
* **📏 ResTool 16x (SDXL)**
|
||||
* **🎞️ ResTool 32x (WanVideo)**
|
||||
* **🎬 ResTool 64x (Hunyuan)**
|
||||
El argumento `--highvram` le da la orden estricta a ComfyUI de mantener los modelos bloqueados en la memoria de video. Esto sabotea directamente el funcionamiento de nuestro `VRAM Defragmenter`, ya que bloquea el comando `mm.unload_all_models()` impidiendo que la gráfica se vacíe entre ciclos. Para que esta suite funcione correctamente con modelos masivos como WanVideo y evite errores de OOM (Out Of Memory), los usuarios deben usar el arranque estándar (o `--normalvram`), permitiendo que el sistema libere dinámicamente la memoria hasta llegar a 0 GB entre lotes.
|
||||
|
||||
## 🔌 Guía de Cableado (Workflow Setup)
|
||||
Para montar el flujo secuencial perfecto, sigue estos pasos:
|
||||
---
|
||||
|
||||
1. **Setup Inicial:** Coloca el 🕵️ Video Analyzer Face detector + Audio al principio. Sube un vídeo o conéctale un cable STRING desde tu descargador favorito. Opcionalmente, conecta un nodo 'ONNX Detection Model Loader' a su entrada `bbox_detector` para escaneo de rostros por GPU.
|
||||
2. **Matemáticas:** Conecta total_frames y safe_faces_list del Explorador hacia el 📊 Auto Loop Calculator (o 📊 Auto Loop Calculator (WanVideo 3dVAE) si usas WanVideo).
|
||||
3. **Extracción:** Añade un nodo VHS_LoadVideo. Haz clic derecho en él -> Convert Widget to Input -> video. Conecta el video_name del Explorador a esta nueva entrada. Alimenta los parámetros de corte desde el Cerebro.
|
||||
4. **Passthrough de Audio:** Lleva el cable source_audio desde el Explorador hasta el puerto audio de tu 🎞️ Incremental Auto-Stitcher (al final del flujo).
|
||||
5. **El Gatillo:** Conecta la salida de tu Stitcher a la entrada del 🚀 Loop Trigger (Auto-Queue).
|
||||
6. **Ejecución:** Conecta el 🏁 Loop Start (Index) al Cerebro y al Stitcher. Dale a "Queue Prompt" UNA SOLA VEZ (sin marcar Auto Queue en la UI). ¡Disfruta de la magia autónoma!
|
||||
## 🧠 La Arquitectura Híbrida (Desglose de Nodos)
|
||||
El procesamiento se divide en roles altamente especializados:
|
||||
|
||||
## 🧹 Auto-Limpieza de VRAM (NUEVO)
|
||||
El nodo `SequentialLoopTrigger` ahora actúa como un gestor de limpieza inteligente. Una vez que detecta que se han procesado todos los frames y la generación del video ha finalizado, automáticamente:
|
||||
1. Obliga a ComfyUI a soltar de memoria todos los modelos pesados (WanVideo, VAE, etc.).
|
||||
2. Fuerza al recolector de basura de Python para eliminar memoria residual en la RAM.
|
||||
3. Vacía profundamente las cachés de PyTorch, soportando múltiples plataformas (CUDA/ROCm para NVIDIA/AMD, y MPS para Apple Silicon).
|
||||
* **🕵️ Video Analyzer Face detector + Audio:** Escanea el vídeo vía OpenCV o mediante un modelo YOLO acelerado por GPU. Extrae rostros, el audio y el Frame de Referencia.
|
||||
* **📊 Auto Loop Calculator (Base / WanVideo / LTX):** Recibe la biometría y calcula las coordenadas de corte (chunk_frames, skip_frames).
|
||||
* **🛡️ VAE Safe Frame Padder:** Intercepta el tensor final de VHS y lo rellena ("acolcha") clonando el último frame si falta material, salvando al VAE de un colapso.
|
||||
* **🎞️ Incremental Auto-Stitcher:** Recoge los lotes renderizados y la pista de audio, cosiendo el vídeo final de forma progresiva en la carpeta temporal.
|
||||
* **📥 Receiver & 📤 Sender:** Retienen y transfieren el último keyframe válido a través de los ciclos para mantener la coherencia temporal.
|
||||
* **⏱️ Auto FPS Limiter:** Reduce los FPS inteligentemente garantizando que el audio y el movimiento mantengan una sincronización perfecta sin romper la VRAM.
|
||||
* **🔀 Master Switch (Evaluación Perezosa):** Amputa físicamente los cables de rutas inactivas en el JSON enviado a ComfyUI. Los nodos pesados que no se necesitan en un ciclo específico ni siquiera se cargan en memoria.
|
||||
* **🧹 VRAM Defragmenter:** Purga forense de memoria (Secuencia Sagrada) que limpia la caché de CUDA/MPS y fuerza al Garbage Collector entre ciclos pesados.
|
||||
|
||||
¡Esto asegura que tu gráfica vuelva a 0GB de uso al terminar, previniendo errores OOM en tus siguientes generaciones!
|
||||
---
|
||||
|
||||
**Changelog v1.5.3:** Se ha introducido un sistema de limpieza profunda automática de VRAM en el nodo `SequentialLoopTrigger`, garantizando que la memoria de la GPU (y RAM) se libere completamente tras la generación de videos pesados en arquitecturas CUDA, ROCm y Mac MPS.
|
||||
**Changelog v1.5.2:** Instalación automatizada de FFmpeg, mejora crítica en el bypass del pre-flight check de ComfyUI para inputs dinámicos en el Analyzer, y exposición detallada de excepciones de torchaudio.
|
||||
## 🔌 Guía de Cableado Rápido (Workflow Setup)
|
||||
|
||||
1. **El Explorador:** Coloca el `Video Analyzer` al principio. Sube un vídeo (o usa una ruta de YTDLP).
|
||||
2. **Matemáticas:** Conecta `total_frames` y `safe_faces_list` hacia tu `Auto Loop Calculator` elegido.
|
||||
3. **Extracción Segura:** Añade el nodo nativo `VHS_LoadVideo`. Conviértele la entrada a *video_name* y conéctala al Analyzer. Conecta su salida `IMAGE` al nuevo nodo `VAE Safe Frame Padder`.
|
||||
4. **Passthrough de Audio:** Lleva el cable `source_audio` desde el Explorador directamente hasta el puerto `audio` de tu `Incremental Auto-Stitcher` (al final del flujo).
|
||||
5. **El Gatillo:** Conecta la salida de tu Stitcher al nodo `Loop Trigger (Auto-Queue)`.
|
||||
6. **Ejecución:** Conecta el `Loop Start (Index)` al Cerebro y al Stitcher.
|
||||
7. **Dispara:** Haz clic en "Queue Prompt" **UNA SOLA VEZ**. ¡Disfruta de la magia autónoma!
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Aceleración por GPU (Escaneo de Rostros)
|
||||
El `VideoAnalyzerWithAudio` tiene un puerto `bbox_detector` para delegar el escaneo facial a la tarjeta gráfica:
|
||||
1. Instala el *Impact Pack*.
|
||||
2. Añade el nodo `UltralyticsDetectorProvider` y selecciona un modelo de rostros (ej. `face_yolov8m.pt`).
|
||||
3. Conecta su salida al puerto del Analyzer.
|
||||
*(El nodo cuenta con auto-descarga: destruirá este modelo de la VRAM al terminar para hacer hueco a la generación).*
|
||||
|
||||
---
|
||||
|
||||
## 📝 Changelog Reciente
|
||||
* **v1.6.0:** Implementación del `VAESafeFramePadder` (Hold Last Frame) y rediseño matemático a modo Expansión. Adiós a los recortes de audio y a los cuelgues en el ciclo final de WanVideo y LTX.
|
||||
* **v1.5.4:** Inyección de la variable de entorno de PyTorch `max_split_size_mb:128` para evitar la micro-fragmentación de VRAM con kernels de Triton (SageAttention). Auto-descarga temprana de modelos YOLO.
|
||||
* **v1.5.3:** "Secuencia Sagrada" en el VRAM Defragmenter (Romper ciclos -> Evacuación Suave -> Limpieza IPC -> Sincronización de hilos).
|
||||
|
||||
+57
-41
@@ -1,67 +1,83 @@
|
||||
# ♾️ ComfyUI Sequential Batcher
|
||||
**v1.5.2** | **VRAM-Optimized Generation** | **Recursive Self-Queuing**
|
||||
**v1.6.0** | **VRAM-Optimized Generation** | **Recursive Self-Queuing** | **Perfect Audio Sync**
|
||||
|
||||
A professional-grade suite of custom nodes for ComfyUI. Designed to bypass VRAM limitations in heavy video generation (WanVideo, Hunyuan, etc.) through **Recursive Self-Queuing** and autonomous, batch-by-batch processing.
|
||||
A professional-grade suite of custom nodes for ComfyUI. Designed to bypass extreme VRAM limits in heavy video generation (WanVideo, Hunyuan, LTX) through **Recursive Auto-Queuing**, autonomous batch-by-batch processing, and forensic memory management.
|
||||
|
||||
> 🌍 **Leer en Español:** [README.md](README.md)
|
||||
> 🇪🇸 **Leer en Español:** [README.md](README.md)
|
||||
|
||||
---
|
||||
|
||||
## 🌟 What's New in v1.6.0 (Perfect Sync Update)
|
||||
* **🛡️ VAE Safe Frame Padder (Hold Last Frame):** The engine now dynamically expands incomplete tensors at the end of the video. If frames are missing to meet the strict requirements of WanVideo (multiples of 4) or LTX (8n+1), it clones the last frame imperceptibly. **Result: Zero VAE crashes and 100% audio sync with no micro-stutters.**
|
||||
* **📈 Mathematical Inversion to "Expansion":** The `AutoLoopCalculatorWan` and `AutoLoopCalculatorLTX` calculators now safely round the timeline upwards, guaranteeing total fluidity in intermediate cycles.
|
||||
|
||||
---
|
||||
|
||||
## ✨ Key Features
|
||||
|
||||
* **Autonomous Orchestration:** Turn your ComfyUI into a continuous rendering engine. Press "Queue" once and the workflow will feed itself until the entire video is finished.
|
||||
* **Smart Chunking:** Analyzes the base video and performs mathematical cuts avoiding the separation of frames where faces are sharpest, thus maintaining identity coherence.
|
||||
* **Native Audio Extraction:** Extracts the original audio track straight from the entry node and seamlessly injects it back into the final stitch.
|
||||
* **Dual Input Engine:** The explorer node accepts both manually uploaded videos and dynamically injected string paths from downloaders (e.g., YTDLP), behaving exactly like VideoHelperSuite.
|
||||
* **Native LTX 2.3 Support:** Specific mathematical calculators (`AutoLoopCalculatorLTX` and `ResTool64xLTX`) have been added to ensure video batches comply with the strict DiT architecture decompression rules and latent downscale factors (resolutions strictly divisible by 64 and frame partitions of `8n + 1`).
|
||||
* **Autonomous Orchestration:** Turn your ComfyUI into a continuous rendering engine. Press "Queue Prompt" just once and the flow will feed itself by modifying seeds and indices until the entire video is finished.
|
||||
* **Smart Chunking:** Analyzes the base video and makes mathematical cuts protecting the frames where faces are sharpest, maintaining identity coherence (Face Cuts).
|
||||
* **Native Audio Extraction:** Extracts the original track directly from the initial node and injects it back into the final assembly.
|
||||
* **DiT Architectures Support (WanVideo & LTX 2.3):** Specific mathematical calculators ensure video batches comply with strict decompression rules (multiples of 4 for WanVideo, or the `8n + 1` rule for LTX).
|
||||
* **Protected Resolutions (Megapixel Shield):** Mathematical tools (`ResTool`) that automatically scale dimensions protecting the *training floor* of each base model.
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ System Requirements (Critical)
|
||||
|
||||
For the `VideoAnalyzerWithAudio` node to unpack `.mp4` containers and extract audio via `torchaudio`, **FFmpeg is absolutely mandatory**.
|
||||
For the `VideoAnalyzerWithAudio` node to unpack `.mp4` containers and extract audio with `torchaudio`, **FFmpeg is absolutely mandatory**.
|
||||
|
||||
* **🐧 Ubuntu / Linux / WSL2:**
|
||||
The ComfyUI Manager script will attempt to install it for you. If it fails, run:
|
||||
The script will attempt to install it for you. If it fails, run manually:
|
||||
```bash
|
||||
sudo apt update && sudo apt install ffmpeg -y
|
||||
```
|
||||
* **🪟 Windows:**
|
||||
Download the pre-compiled binaries from FFmpeg and add the bin folder to your environment variables (PATH).
|
||||
Download the precompiled FFmpeg binaries and add the `bin` folder to your environment variables (PATH).
|
||||
|
||||
## 🧠 The Hybrid Architecture (How it works)
|
||||
Video processing is divided into highly specialized roles:
|
||||
### 🚨 Critical Warning Regarding VRAM Usage
|
||||
The use of the `--highvram` argument when starting ComfyUI is **strongly discouraged**.
|
||||
|
||||
* **🕵️ Video Analyzer Face detector + Audio (The Explorer):** Scans the video via OpenCV or through an optional GPU-accelerated YOLO/ONNX model for precise face detection, extracts the audio and outputs a visual Reference Frame. Acts as the main gateway.
|
||||
* **📊 Auto Loop Calculator (The Brain):** Receives biometrics from the Explorer and calculates asymmetric cut coordinates (chunk_frames, skip_frames) featuring a ±10% dynamic margin to avoid residual micro-batches.
|
||||
* **📊 Auto Loop Calculator (WanVideo 3dVAE):** An alternative for WanVideo that ensures frame chunks are strictly multiples of 4, protecting the 3D VAE from crashing.
|
||||
* **🛠️ The Worker (VHS_LoadVideo):** Freed from analytical tasks, this standard ComfyUI node simply extracts the exact tensors the Brain commands.
|
||||
* **🎞️ Incremental Auto-Stitcher (The Assembler):** Collects the rendered batches and the pristine original audio track, progressively stitching the final video.
|
||||
* **📥 Session Image Receiver** and **📤 Session Image Sender:** Hold and pass the last valid keyframe (reference frame) across iterations to maintain temporal identity coherence.
|
||||
* **⏱️ Auto FPS Limiter (The Synchronizer):** Prevents VRAM OOM errors on high framerate videos (e.g., 60 FPS) by automatically calculating the required frame skip (`select_every_nth`) and adjusting the final FPS to ensure audio and motion maintain perfect synchronization.
|
||||
* **Protected Resolutions (Megapixel Shield):** Mathematical resolution tools that scale dimensions to avoid artifacts by protecting the "training floor" of each model:
|
||||
* **📐 ResTool 8x (SD1.5)**
|
||||
* **📏 ResTool 16x (SDXL)**
|
||||
* **🎞️ ResTool 32x (WanVideo)**
|
||||
* **🎬 ResTool 64x (Hunyuan)**
|
||||
The `--highvram` argument gives ComfyUI the strict command to keep models locked in video memory. This directly sabotages the operation of our `VRAM Defragmenter`, as it blocks the `mm.unload_all_models()` command, preventing the graphics card from emptying between cycles. For this suite to work correctly with massive models like WanVideo and avoid OOM (Out Of Memory) errors, users must use the standard startup (or `--normalvram`), allowing the system to dynamically release memory down to 0 GB between batches.
|
||||
|
||||
## 🔌 Wiring Guide (Workflow Setup)
|
||||
To build the perfect sequential workflow, follow these steps:
|
||||
---
|
||||
|
||||
1. **Initial Setup:** Place the 🕵️ Video Analyzer Face detector + Audio at the beginning. Upload a video or connect a STRING cable to it from your favorite downloader. Optionally, connect an 'ONNX Detection Model Loader' node to its `bbox_detector` input for GPU face scanning.
|
||||
2. **The Math:** Route total_frames and safe_faces_list from the Explorer into the 📊 Auto Loop Calculator (or 📊 Auto Loop Calculator (WanVideo 3dVAE) if using WanVideo).
|
||||
3. **Extraction:** Add a VHS_LoadVideo node. Right-click on it -> Convert Widget to Input -> video. Connect the video_name from the Explorer to this new input. Feed the cutting parameters from the Brain.
|
||||
4. **Audio Passthrough:** Route the source_audio cable from the Explorer all the way to the audio port of your 🎞️ Incremental Auto-Stitcher (at the end of your workflow).
|
||||
5. **The Trigger:** Connect your Stitcher's output to the 🚀 Loop Trigger (Auto-Queue) input.
|
||||
6. **Execution:** Wire the 🏁 Loop Start (Index) to the Brain and the Stitcher. Press "Queue Prompt" ONLY ONCE (do not check Auto Queue in the UI). Sit back and enjoy the autonomous magic!
|
||||
## 🧠 The Hybrid Architecture (Node Breakdown)
|
||||
Processing is divided into highly specialized roles:
|
||||
|
||||
## 🧹 Auto VRAM Cleanup (NEW)
|
||||
The `SequentialLoopTrigger` node now acts as an intelligent cleanup manager. Once it detects that all frames have been processed and video generation has finished, it automatically:
|
||||
1. Forces ComfyUI to release all heavy models from memory (WanVideo, VAE, etc.).
|
||||
2. Triggers Python's garbage collector to eliminate residual memory in RAM.
|
||||
3. Deeply empties PyTorch caches, supporting multiple platforms (CUDA/ROCm for NVIDIA/AMD, and MPS for Apple Silicon).
|
||||
* **🕵️ Video Analyzer Face detector + Audio:** Scans the video via OpenCV or using a GPU-accelerated YOLO model. Extracts faces, audio, and the Reference Frame.
|
||||
* **📊 Auto Loop Calculator (Base / WanVideo / LTX):** Receives the biometrics and calculates cut coordinates (chunk_frames, skip_frames).
|
||||
* **🛡️ VAE Safe Frame Padder:** Intercepts the final VHS tensor and pads it by cloning the last frame if material is missing, saving the VAE from a collapse.
|
||||
* **🎞️ Incremental Auto-Stitcher:** Collects rendered batches and the audio track, progressively stitching the final video in the temporary folder.
|
||||
* **📥 Receiver & 📤 Sender:** Retain and transfer the last valid keyframe across cycles to maintain temporal coherence.
|
||||
* **⏱️ Auto FPS Limiter:** Smartly reduces FPS guaranteeing audio and motion maintain perfect synchronization without breaking VRAM.
|
||||
* **🔀 Master Switch (Lazy Evaluation):** Physically amputates wires from inactive routes in the JSON sent to ComfyUI. Heavy nodes not needed in a specific cycle aren't even loaded into memory.
|
||||
* **🧹 VRAM Defragmenter:** Forensic memory purge (Sacred Sequence) that clears CUDA/MPS cache and forces the Garbage Collector between heavy cycles.
|
||||
|
||||
This ensures your GPU returns to 0GB usage upon completion, preventing OOM errors in your subsequent generations!
|
||||
---
|
||||
|
||||
**Changelog v1.5.3:** Introduced an automatic deep VRAM cleanup system within the `SequentialLoopTrigger` node, ensuring complete release of GPU (and RAM) memory after generating heavy videos across CUDA, ROCm, and Mac MPS architectures.
|
||||
**Changelog v1.5.2:** Automated FFmpeg installation, critical enhancement on ComfyUI pre-flight check bypass for dynamic inputs in the Analyzer, and detailed exposure of torchaudio exceptions.
|
||||
## 🔌 Quick Wiring Guide (Workflow Setup)
|
||||
|
||||
1. **The Explorer:** Place the `Video Analyzer` at the beginning. Upload a video (or use a YTDLP path).
|
||||
2. **Math:** Connect `total_frames` and `safe_faces_list` to your chosen `Auto Loop Calculator`.
|
||||
3. **Safe Extraction:** Add the native `VHS_LoadVideo` node. Convert its input to *video_name* and connect it to the Analyzer. Connect its `IMAGE` output to the new `VAE Safe Frame Padder` node.
|
||||
4. **Audio Passthrough:** Route the `source_audio` wire from the Explorer directly to the `audio` port of your `Incremental Auto-Stitcher` (at the end of the flow).
|
||||
5. **The Trigger:** Connect the output of your Stitcher to the `Loop Trigger (Auto-Queue)` node.
|
||||
6. **Execution:** Connect the `Loop Start (Index)` to the Brain and the Stitcher.
|
||||
7. **Fire:** Click "Queue Prompt" **JUST ONCE**. Enjoy the autonomous magic!
|
||||
|
||||
---
|
||||
|
||||
## 🚀 GPU Acceleration (Face Scanning)
|
||||
The `VideoAnalyzerWithAudio` has a `bbox_detector` port to delegate facial scanning to the graphics card:
|
||||
1. Install the *Impact Pack*.
|
||||
2. Add the `UltralyticsDetectorProvider` node and select a face model (e.g. `face_yolov8m.pt`).
|
||||
3. Connect its output to the Analyzer's port.
|
||||
*(The node features auto-unload: it will destroy this model from VRAM upon completion to make room for generation).*
|
||||
|
||||
---
|
||||
|
||||
## 📝 Recent Changelog
|
||||
* **v1.6.0:** Implementation of `VAESafeFramePadder` (Hold Last Frame) and mathematical redesign to Expansion mode. Goodbye to audio cuts and crashes in the final cycle of WanVideo and LTX.
|
||||
* **v1.5.4:** Injection of the PyTorch environment variable `max_split_size_mb:128` to avoid VRAM micro-fragmentation with Triton kernels (SageAttention). Early YOLO model auto-unload.
|
||||
* **v1.5.3:** "Sacred Sequence" in the VRAM Defragmenter (Break cycles -> Soft Evacuation -> IPC Cleanup -> Thread Synchronization).
|
||||
|
||||
+18
-3
@@ -1,4 +1,6 @@
|
||||
__version__ = "1.5.1"
|
||||
import os
|
||||
|
||||
__version__ = "1.6.0"
|
||||
|
||||
NODE_CLASS_MAPPINGS = {}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {}
|
||||
@@ -9,25 +11,38 @@ def register_node(c, display_name=None):
|
||||
return c
|
||||
|
||||
# Importar tools
|
||||
from . import image, loop, video, tools
|
||||
from . import image, loop, video, tools, vram_node
|
||||
from .switch_node import MasterSwitch, LazySessionCache
|
||||
|
||||
NODE_CLASS_MAPPINGS.update({
|
||||
"MasterSwitch": MasterSwitch,
|
||||
"LazySessionCache": LazySessionCache,
|
||||
})
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update({
|
||||
"MasterSwitch": "🔀 Master Switch",
|
||||
"LazySessionCache": "🗄️ Lazy Session Cache",
|
||||
"MeisoftVRAMDefragmenter": "🧹 VRAM Defragmenter",
|
||||
"SessionImageReceiver": "📥 Session Image Receiver",
|
||||
"SessionImageSender": "📤 Session Image Sender",
|
||||
"IncrementalVideoStitcher": "🎞️ Incremental Auto-Stitcher",
|
||||
"VideoAnalyzerWithAudio": "🕵️ Video Analyzer Face detector + Audio",
|
||||
"VideoAnalyzerFaceDetector": "🕵️ Video Analyzer Face detector + Audio",
|
||||
"VideoAnalyzerSceneDetector": "🎬 Video Analyzer Scene detector",
|
||||
"AutoLoopCalculator": "📊 Auto Loop Calculator",
|
||||
"AutoLoopCalculatorWan": "📊 Auto Loop Calculator (WanVideo 3dVAE)",
|
||||
"AutoLoopCalculatorLTX": "📊 Auto Loop Calculator (LTX 2.3)",
|
||||
"AutoLoopCalculatorTTS": "📊 Auto Loop Calculator (TTS/Texto)",
|
||||
"SequentialLoopStart": "🏁 Loop Start (Index)",
|
||||
"SequentialLoopTrigger": "🚀 Loop Trigger (Auto-Queue)",
|
||||
# Tools
|
||||
"AutoFPSLimiter": "⏱️ Auto FPS Limiter",
|
||||
"VAESafeFramePadder": "🛡️ VAE Safe Frame Padder",
|
||||
"ResTool8x": "📐 ResTool 8x (SD1.5)",
|
||||
"ResTool16x": "📏 ResTool 16x (SDXL)",
|
||||
"ResTool32x": "🎞️ ResTool 32x (WanVideo)",
|
||||
"ResTool64x": "🎬 ResTool 64x (Hunyuan)",
|
||||
"ResTool64xLTX": "🌌 ResTool 64x (LTX 2.3)",
|
||||
"ConditionalAudioRouter": "🎛️ Conditional Audio Router (Bypass)",
|
||||
})
|
||||
|
||||
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']
|
||||
|
||||
@@ -29,8 +29,8 @@ class SessionImageReceiver:
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
RETURN_NAMES = ("current_image",)
|
||||
RETURN_TYPES = ("IMAGE", "STRING")
|
||||
RETURN_NAMES = ("current_image", "log")
|
||||
OUTPUT_NODE = True
|
||||
FUNCTION = "get_image"
|
||||
CATEGORY = "🔁 Sequential Batcher/Image"
|
||||
@@ -40,6 +40,10 @@ class SessionImageReceiver:
|
||||
return time.time()
|
||||
|
||||
def get_image(self, initial_image, current_loop_index):
|
||||
log_output = []
|
||||
def _log(msg):
|
||||
print(msg)
|
||||
log_output.append(str(msg))
|
||||
from . import loop
|
||||
global global_session_image
|
||||
|
||||
@@ -50,23 +54,23 @@ class SessionImageReceiver:
|
||||
accumulated = getattr(loop, 'global_accumulated_frames', 0)
|
||||
source_total = getattr(loop, 'global_source_frame_count', 1)
|
||||
|
||||
print(f"\n{'='*50}")
|
||||
print(f"📥 [DEBUG] NODO: Image Receiver")
|
||||
print(f" -> Ciclo actual: {loop_idx} | Progreso global: {accumulated} / {source_total} frames")
|
||||
_log(f"\n{'='*50}")
|
||||
_log(f"📥 [Secuencial Batcher] NODO: Image Receiver")
|
||||
_log(f" -> Ciclo actual: {loop_idx} | Progreso global: {accumulated} / {source_total} frames")
|
||||
|
||||
if is_first or global_session_image is None:
|
||||
global_session_image = initial_image.clone().cpu()
|
||||
print(f" -> 🆕 Iniciando sesión con la imagen ORIGINAL.")
|
||||
_log(f" -> 🆕 Iniciando sesión con la imagen ORIGINAL.")
|
||||
selected = initial_image
|
||||
else:
|
||||
print(f" -> ♻️ Usando el Keyframe validado y rescatado de la RAM.")
|
||||
_log(f" -> ♻️ Usando el Keyframe validado y rescatado de la RAM.")
|
||||
selected = global_session_image
|
||||
|
||||
print(f" -> 🖼️ Tensor shape: {selected.shape}")
|
||||
print(f"{'='*50}\n")
|
||||
_log(f" -> 🖼️ Tensor shape: {selected.shape}")
|
||||
_log(f"{'='*50}\n")
|
||||
|
||||
ui_image = tensor_to_temp_image(selected, "receiver")
|
||||
return {"ui": {"images": [ui_image]}, "result": (selected, )}
|
||||
return {"ui": {"images": [ui_image]}, "result": (selected, "\n".join(log_output))}
|
||||
|
||||
@register_node
|
||||
class SessionImageSender:
|
||||
@@ -80,8 +84,8 @@ class SessionImageSender:
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
RETURN_NAMES = ("VALIDATED_IMAGES",)
|
||||
RETURN_TYPES = ("IMAGE", "STRING")
|
||||
RETURN_NAMES = ("VALIDATED_IMAGES", "log")
|
||||
OUTPUT_NODE = True
|
||||
FUNCTION = "set_image"
|
||||
CATEGORY = "🔁 Sequential Batcher/Image"
|
||||
@@ -91,6 +95,10 @@ class SessionImageSender:
|
||||
return time.time()
|
||||
|
||||
def set_image(self, generated_images, current_loop_index, detect_faces):
|
||||
log_output = []
|
||||
def _log(msg):
|
||||
print(msg)
|
||||
log_output.append(str(msg))
|
||||
from . import loop
|
||||
import cv2
|
||||
import numpy as np
|
||||
@@ -105,16 +113,27 @@ class SessionImageSender:
|
||||
batch_size = generated_images.shape[0]
|
||||
best_idx = batch_size - 1
|
||||
|
||||
print(f"\n{'='*50}")
|
||||
print(f"📤 [DEBUG] NODO: Image Sender (Filtro Dinámico)")
|
||||
print(f" -> Frames recibidos de la IA: {batch_size}")
|
||||
accumulated = getattr(loop, 'global_accumulated_frames', 0)
|
||||
source_total = getattr(loop, 'global_source_frame_count', 1)
|
||||
stride = getattr(loop, 'global_select_every_nth', 1)
|
||||
ltx_mode_active = getattr(loop, 'global_ltx_mode', False)
|
||||
|
||||
_log(f"\n{'='*50}")
|
||||
_log(f"📤 [Secuencial Batcher] NODO: Image Sender (Filtro Dinámico)")
|
||||
_log(f" -> Frames recibidos de la IA: {batch_size}")
|
||||
|
||||
# 🧠 BYPASS IMPLACABLE: Si al sumar este lote llegamos al final del video, NO RECORTAMOS
|
||||
advance_check = (batch_size - 1) * stride if ltx_mode_active else batch_size * stride
|
||||
if (accumulated + advance_check) >= source_total:
|
||||
_log(f" -> 🏁 LOTE FINAL ABSOLUTO DETECTADO. Desactivando recorte de rostros para forzar la salida de TODOS los frames.")
|
||||
detect_faces = False
|
||||
|
||||
if detect_faces:
|
||||
try:
|
||||
cascade_path = cv2.data.haarcascades + 'haarcascade_frontalface_default.xml'
|
||||
face_cascade = cv2.CascadeClassifier(cascade_path)
|
||||
|
||||
print(f" -> 🕵️ Buscando el último rostro frontal en reversa (Desde frame {batch_size - 1} hasta 0)...")
|
||||
_log(f" -> 🕵️ Buscando el último rostro frontal en reversa (Desde frame {batch_size - 1} hasta 0)...")
|
||||
|
||||
found = False
|
||||
for i in range(batch_size - 1, -1, -1):
|
||||
@@ -125,16 +144,16 @@ class SessionImageSender:
|
||||
if len(faces) > 0:
|
||||
best_idx = i
|
||||
found = True
|
||||
print(f" -> ✅ Rostro detectado en el frame [{i}].")
|
||||
_log(f" -> ✅ Rostro detectado en el frame [{i}].")
|
||||
break
|
||||
|
||||
if not found:
|
||||
print(f" -> ⚠️ ALERTA: No se detectó rostro frontal en NINGÚN frame. Salvando el ciclo usando el frame 0 por defecto.")
|
||||
best_idx = 0
|
||||
_log(f" -> ⚠️ ALERTA: No se detectó rostro frontal en NINGÚN frame. Aceptando todo el lote para evitar bucles infinitos.")
|
||||
best_idx = batch_size - 1
|
||||
except Exception as e:
|
||||
print(f" -> ⚠️ Error en OpenCV al validar: {e}")
|
||||
_log(f" -> ⚠️ Error en OpenCV al validar: {e}")
|
||||
else:
|
||||
print(f" -> ⏩ Detección de rostros desactivada. Usando el último frame absoluto del lote ({batch_size - 1}).")
|
||||
_log(f" -> ⏩ Detección de rostros desactivada. Usando el último frame absoluto del lote ({batch_size - 1}).")
|
||||
best_idx = batch_size - 1
|
||||
|
||||
# 1. TRUNCAR EL TENSOR
|
||||
@@ -156,25 +175,40 @@ class SessionImageSender:
|
||||
# since it will be reused as the start of the next chunk.
|
||||
advanced_original_frames = max(1, (frames_accepted - 1) * stride)
|
||||
|
||||
loop.global_accumulated_frames += advanced_original_frames
|
||||
|
||||
source_total = getattr(loop, 'global_source_frame_count', 1)
|
||||
print(f" -> ✂️ Tensor truncado a {frames_accepted} frames válidos.")
|
||||
print(f" -> 📈 Timeline avanzado a {loop.global_accumulated_frames} / {source_total}")
|
||||
is_final_chunk = getattr(loop, 'global_is_final_chunk', False)
|
||||
|
||||
# 🚀 FIX: Compensar los frames destruidos por el VAE en el ciclo final
|
||||
if is_final_chunk:
|
||||
_log(f" -> 🏁 LOTE FINAL ABSOLUTO DETECTADO. Compensando mermas del VAE (Timeline forzado al 100%).")
|
||||
loop.global_accumulated_frames = source_total
|
||||
else:
|
||||
loop.global_accumulated_frames += advanced_original_frames
|
||||
|
||||
# 🚀 NUEVO: Detectar si es el ciclo final o un ciclo único
|
||||
is_final_cycle = loop.global_accumulated_frames >= source_total
|
||||
|
||||
_log(f" -> ✂️ Tensor truncado a {frames_accepted} frames válidos.")
|
||||
_log(f" -> 📈 Timeline avanzado a {loop.global_accumulated_frames} / {source_total}")
|
||||
|
||||
last_frame = valid_images[-1:].clone().cpu()
|
||||
global_session_image = last_frame
|
||||
|
||||
out_dir = folder_paths.get_output_directory()
|
||||
filename = f"keyframe_{loop_idx:03d}.png"
|
||||
filepath = os.path.join(out_dir, filename)
|
||||
# 🛡️ Solo guardamos el keyframe si habrá un próximo ciclo
|
||||
if not is_final_cycle:
|
||||
out_dir = folder_paths.get_output_directory()
|
||||
filename = f"keyframe_{loop_idx:03d}.png"
|
||||
filepath = os.path.join(out_dir, filename)
|
||||
|
||||
img_array = 255. * last_frame[0].numpy()
|
||||
img = Image.fromarray(np.clip(img_array, 0, 255).astype(np.uint8))
|
||||
img.save(filepath)
|
||||
img_array = 255. * last_frame[0].numpy()
|
||||
img = Image.fromarray(np.clip(img_array, 0, 255).astype(np.uint8))
|
||||
img.save(filepath)
|
||||
|
||||
print(f" -> 💾 Keyframe seguro guardado: {filename}")
|
||||
print(f"{'='*50}\n")
|
||||
_log(f" -> 💾 Keyframe seguro guardado: {filename}")
|
||||
else:
|
||||
_log(f" -> 🚀 Ciclo final/único detectado. Omitiendo guardado de keyframe.")
|
||||
|
||||
_log(f"{'='*50}\n")
|
||||
|
||||
ui_image = tensor_to_temp_image(last_frame, "sender")
|
||||
return {"ui": {"images": [ui_image]}, "result": (valid_images, )}
|
||||
return {"ui": {"images": [ui_image]}, "result": (valid_images, "\n".join(log_output))}
|
||||
|
||||
+2
-1
@@ -21,7 +21,8 @@ def check_and_install_ffmpeg():
|
||||
if sys.platform.startswith("linux"):
|
||||
try:
|
||||
print("⚙️ Intentando instalar FFmpeg automáticamente (Linux/WSL)...")
|
||||
subprocess.check_call("sudo apt-get update && sudo DEBIAN_FRONTEND=noninteractive apt-get install -y ffmpeg", shell=True)
|
||||
subprocess.check_call(["sudo", "apt-get", "update"])
|
||||
subprocess.check_call(["sudo", "DEBIAN_FRONTEND=noninteractive", "apt-get", "install", "-y", "ffmpeg"])
|
||||
print("✅ FFmpeg instalado correctamente.")
|
||||
except Exception as e:
|
||||
print(f"❌ Falló la instalación automática de FFmpeg: {e}")
|
||||
|
||||
@@ -84,6 +84,7 @@ class AutoLoopCalculatorLTX:
|
||||
},
|
||||
"optional": {
|
||||
"safe_faces_list": ("FACE_CUTS", {"forceInput": True}),
|
||||
"scene_cuts_list": ("SCENE_CUTS", {"forceInput": True}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,35 +93,40 @@ class AutoLoopCalculatorLTX:
|
||||
FUNCTION = "calculate"
|
||||
CATEGORY = "🔁 Sequential Batcher/Loop"
|
||||
|
||||
def calculate(self, source_frame_count, target_frames_per_loop, select_every_nth, current_loop_index, safe_faces_list=None):
|
||||
def calculate(self, source_frame_count, target_frames_per_loop, select_every_nth, current_loop_index, safe_faces_list=None, scene_cuts_list=None):
|
||||
# --- Ajuste Proporcional LTX (Regla DiT 8n + 1) ---
|
||||
import math
|
||||
from . import loop
|
||||
|
||||
total_effective_frames = source_frame_count // select_every_nth
|
||||
potential_effective_frames = source_frame_count // select_every_nth
|
||||
|
||||
# Truncar al (8n + 1) más cercano hacia abajo
|
||||
if total_effective_frames < 9:
|
||||
safe_effective_frames = total_effective_frames # Too short, bypass
|
||||
if potential_effective_frames < 9:
|
||||
safe_effective_frames = 9
|
||||
else:
|
||||
safe_effective_frames = ((total_effective_frames - 1) // 8) * 8 + 1
|
||||
safe_effective_frames = ((potential_effective_frames + 6) // 8) * 8 + 1
|
||||
|
||||
safe_source_frame_count = safe_effective_frames * select_every_nth
|
||||
loop.global_source_frame_count = safe_source_frame_count
|
||||
# 🚀 FIX: Conservamos el límite FÍSICO real del vídeo para el timeline global
|
||||
physical_source_frame_count = potential_effective_frames * select_every_nth
|
||||
loop.global_source_frame_count = physical_source_frame_count
|
||||
effective_padding = safe_effective_frames - potential_effective_frames
|
||||
|
||||
# Ajuste proporcional forzando regla 8n + 1
|
||||
# Ajuste proporcional
|
||||
estimated_loops = math.ceil(safe_effective_frames / target_frames_per_loop)
|
||||
if estimated_loops > 0:
|
||||
optimal_target = math.ceil(safe_effective_frames / estimated_loops)
|
||||
# Encontrar el 8n + 1 más cercano hacia arriba para cubrir el total
|
||||
adjusted_target = ((optimal_target - 1 + 7) // 8) * 8 + 1
|
||||
target_frames_per_loop = adjusted_target
|
||||
print(f" -> ⚖️ Ajuste Proporcional LTX: Target recalculado a {adjusted_target} frames por ciclo.")
|
||||
# -------------------------------------------------------------
|
||||
|
||||
print(f" -> 🎞️ Capacidad del video original: {potential_effective_frames} frames (Nth: {select_every_nth})")
|
||||
if effective_padding > 0:
|
||||
print(f" -> 🛡️ Ajuste VAE: Se pedirán {safe_effective_frames} frames (Acolchado técnico: Se rellenarán {effective_padding} frames)")
|
||||
else:
|
||||
print(f" -> ✅ Ajuste VAE: Perfecto. Regla 8n+1 detectada.")
|
||||
|
||||
print(f" -> 📊 Timeline final: 0 a {physical_source_frame_count} (Límite Físico Real)")
|
||||
|
||||
loop.global_select_every_nth = select_every_nth
|
||||
|
||||
# Indicador global para el Stitcher y Sender (LTX mode activo)
|
||||
global global_ltx_mode
|
||||
global_ltx_mode = True
|
||||
|
||||
@@ -128,38 +134,58 @@ class AutoLoopCalculatorLTX:
|
||||
|
||||
print(f"\n{'='*50}")
|
||||
print(f"📊 [DEBUG] NODO: Auto Loop Calculator (LTX 2.3)")
|
||||
print(f" -> Timeline seguro ajustado a LTX: {current_pos} / {safe_source_frame_count} (Original: {source_frame_count})")
|
||||
print(f" -> Timeline físico ajustado a LTX: {current_pos} / {physical_source_frame_count} (Original: {source_frame_count})")
|
||||
|
||||
if current_pos >= safe_source_frame_count:
|
||||
if current_pos >= physical_source_frame_count:
|
||||
return (1, current_pos, select_every_nth)
|
||||
|
||||
frames_left = safe_source_frame_count - current_pos
|
||||
frames_left = physical_source_frame_count - current_pos
|
||||
|
||||
equitable_target = target_frames_per_loop * select_every_nth
|
||||
|
||||
ideal_cut = current_pos + equitable_target
|
||||
|
||||
if frames_left <= equitable_target:
|
||||
best_cut = safe_source_frame_count
|
||||
best_cut = physical_source_frame_count
|
||||
print(f" -> 🧮 Absorbiendo resto final: meta fijada en frame {best_cut}")
|
||||
else:
|
||||
best_cut = ideal_cut
|
||||
if safe_faces_list and len(safe_faces_list) > 0:
|
||||
if scene_cuts_list and len(scene_cuts_list) > 0:
|
||||
future_cuts = [c for c in scene_cuts_list if c > current_pos]
|
||||
if future_cuts:
|
||||
next_cut = min(future_cuts)
|
||||
if next_cut <= ideal_cut:
|
||||
chunk_from_scene = math.ceil((next_cut - current_pos) / select_every_nth)
|
||||
safe_chunk_from_scene = ((chunk_from_scene + 6) // 8) * 8 + 1
|
||||
if safe_chunk_from_scene < 9: safe_chunk_from_scene = 9
|
||||
best_cut = current_pos + (safe_chunk_from_scene * select_every_nth)
|
||||
print(f" -> 🎬 Bucle aislado por escena (Ajustado x8). Cortando en el frame: {best_cut} (Corte real: {next_cut})")
|
||||
else:
|
||||
best_cut = ideal_cut
|
||||
print(f" -> ⚠️ Toma demasiado larga para la VRAM. Cortando por límite técnico en: {best_cut} (El plano real acaba en {next_cut})")
|
||||
else:
|
||||
best_cut = ideal_cut
|
||||
print(f" -> 🎬 No quedan cortes de cámara por delante. Forzando corte final por VRAM en: {ideal_cut}")
|
||||
elif safe_faces_list and len(safe_faces_list) > 0:
|
||||
closest_face = min(safe_faces_list, key=lambda x: abs(x - ideal_cut))
|
||||
best_cut = closest_face
|
||||
print(f" -> ✂️ Corte Inteligente proyectado: {best_cut} (Meta equitativa: {ideal_cut})")
|
||||
chunk_from_face = math.ceil((closest_face - current_pos) / select_every_nth)
|
||||
safe_chunk_from_face = ((chunk_from_face + 6) // 8) * 8 + 1
|
||||
if safe_chunk_from_face < 9: safe_chunk_from_face = 9
|
||||
best_cut = current_pos + (safe_chunk_from_face * select_every_nth)
|
||||
print(f" -> ✂️ Corte Facial Inteligente (Ajustado x8): {best_cut} (Meta equitativa: {ideal_cut})")
|
||||
else:
|
||||
print(f" -> ⚖️ Sin caras detectadas. Forzando corte equitativo: {ideal_cut}")
|
||||
best_cut = ideal_cut
|
||||
print(f" -> ⚖️ Sin detectores conectados. Forzando corte equitativo x8: {ideal_cut}")
|
||||
|
||||
effective_chunk_frames = math.ceil((best_cut - current_pos) / select_every_nth)
|
||||
# Forzar a 8n + 1
|
||||
effective_chunk_frames = ((effective_chunk_frames - 1) // 8) * 8 + 1
|
||||
effective_chunk_frames = ((effective_chunk_frames + 6) // 8) * 8 + 1
|
||||
|
||||
if effective_chunk_frames < 9:
|
||||
effective_chunk_frames = 9 # Mínimo vital LTX
|
||||
effective_chunk_frames = 9
|
||||
|
||||
if current_pos + (effective_chunk_frames * select_every_nth) > safe_source_frame_count:
|
||||
effective_chunk_frames = (safe_source_frame_count - current_pos) // select_every_nth
|
||||
if current_pos + (effective_chunk_frames * select_every_nth) >= physical_source_frame_count:
|
||||
print(f" -> 🏁 Chunk final LTX detectado. Ajustando a {effective_chunk_frames} frames para mantener regla 8n+1.")
|
||||
loop.global_is_final_chunk = True
|
||||
else:
|
||||
loop.global_is_final_chunk = False
|
||||
|
||||
skip_frames = current_pos
|
||||
|
||||
@@ -205,16 +231,51 @@ class SequentialLoopTrigger:
|
||||
if not is_final:
|
||||
print(f" -> ⚙️ Preparando Ciclo {next_loop}...")
|
||||
if prompt is not None:
|
||||
# 🧠 HACKER MODE: Estimamos si el ciclo que vamos a encolar es el final
|
||||
estimated_chunk = 50
|
||||
found_calculator = False
|
||||
m_seeds = 0
|
||||
master_switches = []
|
||||
|
||||
for node_id, node_data in prompt.items():
|
||||
class_type = node_data.get("class_type", "")
|
||||
inputs = node_data.get("inputs", {})
|
||||
|
||||
if not found_calculator and "AutoLoopCalculator" in class_type:
|
||||
estimated_chunk = inputs.get("target_frames_per_loop", 50)
|
||||
found_calculator = True
|
||||
|
||||
# Mutación de semillas
|
||||
for key in ["seed", "noise_seed"]:
|
||||
if key in inputs and isinstance(inputs[key], (int, float)):
|
||||
inputs[key] = random.randint(1, 0xffffffff)
|
||||
m_seeds += 1
|
||||
if node_data.get("class_type") == "SequentialLoopStart":
|
||||
|
||||
# Mutación de índice
|
||||
if class_type == "SequentialLoopStart":
|
||||
inputs["loop_idx"] = next_loop
|
||||
inputs["reset_loop"] = False
|
||||
|
||||
# 🔪 Recolectamos Master Switch para cirugía diferida
|
||||
if class_type == "MasterSwitch":
|
||||
master_switches.append(inputs)
|
||||
|
||||
is_next_final = (global_accumulated_frames + estimated_chunk) >= global_source_frame_count
|
||||
|
||||
for inputs in master_switches:
|
||||
print(f" -> 🔀 [Cirugía de Grafo] Mutando Master Switch para Ciclo {next_loop}...")
|
||||
# Sobrescribimos el cable que viene del Stitcher por un booleano estático
|
||||
inputs["is_final_cycle"] = is_next_final
|
||||
|
||||
if is_next_final:
|
||||
if "on_false" in inputs:
|
||||
del inputs["on_false"]
|
||||
print(f" ✂️ Cable 'on_false' eliminado (Ahorro de VRAM en ruta inactiva).")
|
||||
else:
|
||||
if "on_true" in inputs:
|
||||
del inputs["on_true"]
|
||||
print(f" ✂️ Cable 'on_true' eliminado (Ruta pesada desconectada).")
|
||||
|
||||
print(f" -> 🎲 Semillas mutadas: {m_seeds}")
|
||||
|
||||
p = {"prompt": prompt}
|
||||
@@ -227,13 +288,23 @@ class SequentialLoopTrigger:
|
||||
print(f" -> ✅ Ciclo {next_loop} inyectado en la cola.")
|
||||
except Exception as e:
|
||||
print(f" -> ❌ Error HTTP: {e}")
|
||||
|
||||
# Limpieza ligera por ciclo para evitar fragmentación
|
||||
import gc
|
||||
import torch
|
||||
import comfy.model_management as mm
|
||||
gc.collect()
|
||||
if torch.cuda.is_available():
|
||||
torch.cuda.empty_cache()
|
||||
|
||||
else:
|
||||
print(f" -> 🏁 ¡Generación Finalizada! Todos los frames ensamblados.")
|
||||
global_loop_index = 0
|
||||
global_accumulated_frames = 0
|
||||
|
||||
# --- LIMPIEZA EXTREMA DE VRAM AUTOMÁTICA (Multi-Plataforma) ---
|
||||
print(f" -> 🧹 Iniciando vaciado automático de VRAM...")
|
||||
import gc
|
||||
import torch
|
||||
import comfy.model_management as mm
|
||||
try:
|
||||
# 1. Obligamos al motor interno de ComfyUI a soltar los modelos
|
||||
mm.unload_all_models()
|
||||
@@ -248,6 +319,13 @@ class SequentialLoopTrigger:
|
||||
if torch.cuda.is_available():
|
||||
torch.cuda.empty_cache()
|
||||
torch.cuda.ipc_collect()
|
||||
|
||||
# HACK: Forzar la desfragmentación de la memoria caché de PyTorch
|
||||
import ctypes
|
||||
try:
|
||||
ctypes.CDLL('libc.so.6').malloc_trim(0)
|
||||
except Exception:
|
||||
pass
|
||||
elif hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
|
||||
torch.mps.empty_cache()
|
||||
|
||||
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
class AnyType(str):
|
||||
def __ne__(self, __value: object) -> bool:
|
||||
return False
|
||||
|
||||
any_type = AnyType("*")
|
||||
|
||||
class MasterSwitch:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"is_final_cycle": ("BOOLEAN", {"default": False, "forceInput": False}),
|
||||
},
|
||||
"optional": {
|
||||
"on_true": (any_type,),
|
||||
"on_false": (any_type,),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = (any_type, "STRING")
|
||||
RETURN_NAMES = ("output", "log")
|
||||
FUNCTION = "route"
|
||||
CATEGORY = "🔁 Sequential Batcher/Logic"
|
||||
|
||||
# 🔥 Evaluación Perezosa Nativa (Protege el Ciclo 0)
|
||||
def check_lazy_status(self, is_final_cycle, on_true=None, on_false=None):
|
||||
if is_final_cycle:
|
||||
return ["on_true"]
|
||||
else:
|
||||
return ["on_false"]
|
||||
|
||||
def route(self, is_final_cycle, on_true=None, on_false=None):
|
||||
log_output = []
|
||||
def _log(msg):
|
||||
print(msg)
|
||||
log_output.append(str(msg))
|
||||
if is_final_cycle:
|
||||
return (on_true, "\n".join(log_output))
|
||||
else:
|
||||
return (on_false, "\n".join(log_output))
|
||||
|
||||
|
||||
# Diccionario global para persistencia entre ciclos de ejecución
|
||||
GLOBAL_SESSION_CACHE = {}
|
||||
|
||||
class LazySessionCache:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
# Mantenemos el índice como obligatorio para saber en qué ciclo estamos
|
||||
"current_loop_index": ("INT", {"default": 0, "forceInput": True}),
|
||||
},
|
||||
"optional": {
|
||||
# 'value_in' DEBE ser opcional para que la evaluación perezosa funcione
|
||||
"value_in": (any_type, {"forceInput": True}),
|
||||
},
|
||||
"hidden": {
|
||||
"unique_id": "UNIQUE_ID"
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = (any_type, "STRING")
|
||||
RETURN_NAMES = ("value_out", "log")
|
||||
FUNCTION = "execute"
|
||||
CATEGORY = "🔁 Sequential Batcher/Logic"
|
||||
|
||||
# 🔥 LA CLAVE: Aquí es donde ComfyUI decide si "tira del cable" o no
|
||||
def check_lazy_status(self, current_loop_index, **kwargs):
|
||||
# El índice llega como una lista o un int dependiendo del nodo anterior
|
||||
idx = current_loop_index[0] if isinstance(current_loop_index, list) else current_loop_index
|
||||
|
||||
if idx == 0:
|
||||
# Ciclo 0: Obligamos a ComfyUI a ejecutar todo lo que haya atrás
|
||||
return ["value_in"]
|
||||
else:
|
||||
# Ciclos > 0: NO pedimos 'value_in'. ComfyUI NO ejecutará los nodos anteriores.
|
||||
return []
|
||||
|
||||
def execute(self, current_loop_index, value_in=None, unique_id="0"):
|
||||
global GLOBAL_SESSION_CACHE
|
||||
log_output = []
|
||||
def _log(msg):
|
||||
print(msg); log_output.append(str(msg))
|
||||
|
||||
idx = current_loop_index[0] if isinstance(current_loop_index, list) else current_loop_index
|
||||
|
||||
_log(f"🧠 [Cache] Ciclo {idx} | Nodo ID: {unique_id}")
|
||||
|
||||
# --- LÓGICA DE CAPTURA (Ciclo 0) ---
|
||||
if idx == 0:
|
||||
if value_in is not None:
|
||||
GLOBAL_SESSION_CACHE[unique_id] = value_in
|
||||
_log(" -> 💾 Valor capturado y guardado en RAM.")
|
||||
return (value_in, "\n".join(log_output))
|
||||
else:
|
||||
# Si es el ciclo 0 y no hay nada, algo va mal en el flujo
|
||||
raise ValueError(f"❌ Error en LazySessionCache [{unique_id}]: No se recibió valor en el Ciclo 0.")
|
||||
|
||||
# --- LÓGICA DE RECUPERACIÓN (Ciclos > 0) ---
|
||||
else:
|
||||
if unique_id in GLOBAL_SESSION_CACHE:
|
||||
_log(" -> ♻️ Recuperando valor de RAM (Upstream bloqueado con éxito).")
|
||||
return (GLOBAL_SESSION_CACHE[unique_id], "\n".join(log_output))
|
||||
else:
|
||||
# Fallback de seguridad por si el caché se limpió o se saltó el ciclo 0
|
||||
_log(" -> ⚠️ No hay caché. Intentando usar entrada directa.")
|
||||
return (value_in, "\n".join(log_output))
|
||||
@@ -0,0 +1,34 @@
|
||||
import unittest
|
||||
from unittest.mock import patch, MagicMock
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Mock sys.platform to linux for testing check_and_install_ffmpeg
|
||||
@patch("sys.platform", "linux")
|
||||
@patch("shutil.which", return_value=None)
|
||||
class TestInstall(unittest.TestCase):
|
||||
|
||||
@patch("subprocess.check_call")
|
||||
def test_check_and_install_ffmpeg_linux(self, mock_check_call, mock_which):
|
||||
from install import check_and_install_ffmpeg
|
||||
|
||||
check_and_install_ffmpeg()
|
||||
|
||||
# Verify that subprocess.check_call was called twice with the correct lists
|
||||
self.assertEqual(mock_check_call.call_count, 2)
|
||||
|
||||
expected_calls = [
|
||||
unittest.mock.call(["sudo", "apt-get", "update"]),
|
||||
unittest.mock.call(["sudo", "DEBIAN_FRONTEND=noninteractive", "apt-get", "install", "-y", "ffmpeg"])
|
||||
]
|
||||
mock_check_call.assert_has_calls(expected_calls)
|
||||
|
||||
# Verify shell=True was NOT used
|
||||
for call in mock_check_call.call_args_list:
|
||||
args, kwargs = call
|
||||
if "shell" in kwargs:
|
||||
self.assertFalse(kwargs["shell"], "shell should be False if explicitly provided")
|
||||
# If not in kwargs, it defaults to False for subprocess.check_call
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,55 @@
|
||||
import sys
|
||||
import types
|
||||
import pytest
|
||||
from unittest.mock import MagicMock
|
||||
import os
|
||||
|
||||
# Mock dependencies before importing anything that might depend on them
|
||||
mock_torch = types.ModuleType("torch")
|
||||
mock_torch.cuda = MagicMock()
|
||||
mock_torch.cuda.is_available.return_value = False
|
||||
sys.modules["torch"] = mock_torch
|
||||
|
||||
sys.modules["comfy"] = types.ModuleType("comfy")
|
||||
sys.modules["comfy.model_management"] = types.ModuleType("comfy.model_management")
|
||||
|
||||
def get_format_bytes():
|
||||
# Read the file
|
||||
vram_node_path = os.path.join(os.path.dirname(__file__), "..", "vram_node.py")
|
||||
with open(vram_node_path, 'r') as f:
|
||||
code = f.read()
|
||||
|
||||
# Mock 'from . import register_node' by replacing it
|
||||
code = code.replace("from . import register_node", "def register_node(c, *args, **kwargs): return c")
|
||||
|
||||
vram_module = types.ModuleType("vram_node")
|
||||
exec(code, vram_module.__dict__)
|
||||
|
||||
return vram_module.format_bytes
|
||||
|
||||
@pytest.fixture
|
||||
def format_bytes():
|
||||
return get_format_bytes()
|
||||
|
||||
def test_format_bytes_zero(format_bytes):
|
||||
assert format_bytes(0) == "0.00 GB"
|
||||
|
||||
def test_format_bytes_gigabyte(format_bytes):
|
||||
assert format_bytes(1024**3) == "1.00 GB"
|
||||
|
||||
def test_format_bytes_fractional(format_bytes):
|
||||
assert format_bytes(1024**3 * 0.5) == "0.50 GB"
|
||||
assert format_bytes(1024**3 * 2.5) == "2.50 GB"
|
||||
|
||||
def test_format_bytes_small_values(format_bytes):
|
||||
# 1 MB is 1/1024 GB ≈ 0.000976 GB -> 0.00 GB
|
||||
assert format_bytes(1024**2) == "0.00 GB"
|
||||
# 10 MB is 10/1024 GB ≈ 0.009765 GB -> 0.01 GB (due to rounding .2f)
|
||||
assert format_bytes(1024**2 * 10) == "0.01 GB"
|
||||
|
||||
def test_format_bytes_large_values(format_bytes):
|
||||
assert format_bytes(1024**3 * 1024) == "1024.00 GB"
|
||||
assert format_bytes(1024**3 * 123.456) == "123.46 GB"
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__])
|
||||
@@ -1,5 +1,43 @@
|
||||
import math
|
||||
import time
|
||||
import torch
|
||||
from . import register_node
|
||||
from .switch_node import any_type
|
||||
|
||||
@register_node
|
||||
class PrimitiveDelay:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"delay_seconds": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 3600.0, "step": 0.1, "tooltip": "Tiempo de pausa en segundos"}),
|
||||
},
|
||||
"optional": {
|
||||
"any_in": (any_type, {"tooltip": "Conecta aquí el cable que quieres retrasar"}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = (any_type,)
|
||||
RETURN_NAMES = ("* (passthrough)",)
|
||||
FUNCTION = "execute_delay"
|
||||
CATEGORY = "🔁 Sequential Batcher/Tools"
|
||||
|
||||
def execute_delay(self, delay_seconds, any_in=None):
|
||||
log_output = []
|
||||
def _log(msg):
|
||||
print(msg)
|
||||
log_output.append(str(msg))
|
||||
|
||||
_log(f"\n{'='*50}")
|
||||
_log(f"⏱️ [Secuencial Batcher] NODO: Primitive Delay")
|
||||
_log(f" -> Congelando la ejecución durante {delay_seconds} segundos...")
|
||||
|
||||
time.sleep(delay_seconds)
|
||||
|
||||
_log(f" -> ✅ Pausa terminada. Reanudando flujo.")
|
||||
_log(f"{'='*50}\n")
|
||||
|
||||
return (any_in,)
|
||||
|
||||
# 🧠 Clase Base Invisible (El molde matemático por Megapíxeles)
|
||||
class BaseResolutionTool:
|
||||
@@ -13,12 +51,16 @@ class BaseResolutionTool:
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("INT", "INT", "STRING")
|
||||
RETURN_NAMES = ("width", "height", "debug_info")
|
||||
RETURN_TYPES = ("INT", "INT", "STRING", "STRING")
|
||||
RETURN_NAMES = ("width", "height", "debug_info", "log")
|
||||
FUNCTION = "get_resolution"
|
||||
CATEGORY = "🔁 Sequential Batcher/Tools"
|
||||
|
||||
def get_resolution(self, aspect_ratio, base_resolution):
|
||||
log_output = []
|
||||
def _log(msg):
|
||||
print(msg)
|
||||
log_output.append(str(msg))
|
||||
div = self.DIVISOR
|
||||
min_pixels = getattr(self, "MIN_PIXELS", 0)
|
||||
longest_side = int(base_resolution) # Mantenemos el casting por seguridad
|
||||
@@ -28,9 +70,9 @@ class BaseResolutionTool:
|
||||
h_ratio = float(h_str)
|
||||
ratio = w_ratio / h_ratio
|
||||
|
||||
print(f"\n{'='*50}")
|
||||
print(f"🛠️ [DEBUG] NODO: Resolution Tool {div}x")
|
||||
print(f" -> Petición Original: Lado mayor {longest_side}px | Ratio {aspect_ratio}")
|
||||
_log(f"\n{'='*50}")
|
||||
_log(f"🛠️ [Secuencial Batcher] NODO: Resolution Tool {div}x")
|
||||
_log(f" -> Petición Original: Lado mayor {longest_side}px | Ratio {aspect_ratio}")
|
||||
|
||||
# 1. Calcular resolución inicial teórica
|
||||
if w_ratio >= h_ratio:
|
||||
@@ -44,8 +86,8 @@ class BaseResolutionTool:
|
||||
|
||||
# 2. 🛡️ Protección de Suelo por ÁREA TOTAL (Training Floor)
|
||||
if current_pixels < min_pixels:
|
||||
print(f" -> ⚠️ ALERTA: La resolución pedida ({int(current_pixels)} píxeles) es inferior al mínimo vital del modelo ({min_pixels} píxeles).")
|
||||
print(f" -> 🛡️ Escalando proporcionalmente para evitar artefactos...")
|
||||
_log(f" -> ⚠️ ALERTA: La resolución pedida ({int(current_pixels)} píxeles) es inferior al mínimo vital del modelo ({min_pixels} píxeles).")
|
||||
_log(f" -> 🛡️ Escalando proporcionalmente para evitar artefactos...")
|
||||
# Factor de escala basado en área
|
||||
scale_factor = math.sqrt(min_pixels / current_pixels)
|
||||
ideal_w *= scale_factor
|
||||
@@ -58,10 +100,10 @@ class BaseResolutionTool:
|
||||
|
||||
debug_msg = f"{width}x{height} (Div {div})"
|
||||
|
||||
print(f" -> 🎯 Resultado Final Seguro: Ancho {width} | Alto {height} | Píxeles totales: {width*height}")
|
||||
print(f"{'='*50}\n")
|
||||
_log(f" -> 🎯 Resultado Final Seguro: Ancho {width} | Alto {height} | Píxeles totales: {width*height}")
|
||||
_log(f"{'='*50}\n")
|
||||
|
||||
return (width, height, debug_msg)
|
||||
return (width, height, debug_msg, "\n".join(log_output))
|
||||
|
||||
# 📦 Nodos Específicos Blindados por Píxeles
|
||||
@register_node
|
||||
@@ -101,28 +143,186 @@ class AutoFPSLimiter:
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("INT", "FLOAT")
|
||||
RETURN_NAMES = ("select_every_nth", "new_fps")
|
||||
RETURN_TYPES = ("INT", "FLOAT", "STRING")
|
||||
RETURN_NAMES = ("select_every_nth", "new_fps", "log")
|
||||
FUNCTION = "calculate_fps"
|
||||
CATEGORY = "🔁 Sequential Batcher/Tools"
|
||||
|
||||
def calculate_fps(self, source_fps, target_max_fps):
|
||||
log_output = []
|
||||
def _log(msg):
|
||||
print(msg)
|
||||
log_output.append(str(msg))
|
||||
import math
|
||||
print(f"\n{'='*50}")
|
||||
print(f"⏱️ [DEBUG] NODO: Auto FPS Limiter")
|
||||
print(f" -> FPS Originales: {source_fps:.2f} | Meta Máxima: {target_max_fps:.2f}")
|
||||
_log(f"\n{'='*50}")
|
||||
_log(f"⏱️ [Secuencial Batcher] NODO: Auto FPS Limiter")
|
||||
_log(f" -> FPS Originales: {source_fps:.2f} | Meta Máxima: {target_max_fps:.2f}")
|
||||
|
||||
if source_fps <= target_max_fps:
|
||||
nth = 1
|
||||
new_fps = source_fps
|
||||
print(f" -> ✅ Los FPS originales ya están dentro del límite.")
|
||||
_log(f" -> ✅ Los FPS originales ya están dentro del límite.")
|
||||
else:
|
||||
# Calculamos el salto matemático (hacia arriba) para garantizar no superar la meta
|
||||
nth = math.ceil(source_fps / target_max_fps)
|
||||
new_fps = source_fps / nth
|
||||
print(f" -> ✂️ Reducción necesaria. Procesando 1 de cada {nth} frames.")
|
||||
_log(f" -> ✂️ Reducción necesaria. Procesando 1 de cada {nth} frames.")
|
||||
|
||||
print(f" -> 🎯 Resultado: {new_fps:.2f} FPS finales (Nth: {nth})")
|
||||
print(f"{'='*50}\n")
|
||||
_log(f" -> 🎯 Resultado: {new_fps:.2f} FPS finales (Nth: {nth})")
|
||||
_log(f"{'='*50}\n")
|
||||
|
||||
return (nth, new_fps)
|
||||
return (nth, new_fps, "\n".join(log_output))
|
||||
|
||||
@register_node
|
||||
class ConditionalAudioRouter:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"audio": ("AUDIO",),
|
||||
"is_final_cycle": ("BOOLEAN", {"forceInput": True}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("AUDIO", "STRING")
|
||||
RETURN_NAMES = ("routed_audio", "log")
|
||||
FUNCTION = "route"
|
||||
CATEGORY = "🔁 Sequential Batcher/Tools"
|
||||
|
||||
def route(self, audio, is_final_cycle):
|
||||
log_output = []
|
||||
def _log(msg):
|
||||
print(msg)
|
||||
log_output.append(str(msg))
|
||||
if is_final_cycle:
|
||||
_log(" -> 🔊 [Audio Router] Ciclo Final: Enviando pista de audio completa al Lip Sync.")
|
||||
return (audio, "\n".join(log_output))
|
||||
else:
|
||||
_log(" -> 🔇 [Audio Router] Ciclo Intermedio: Generando micro-silencio para bypasear el Lip Sync...")
|
||||
|
||||
# Detectar el sample rate del audio original (estándar de ComfyUI es un dict)
|
||||
sample_rate = 44100
|
||||
if isinstance(audio, dict) and "sample_rate" in audio:
|
||||
sample_rate = audio["sample_rate"]
|
||||
|
||||
# Creamos 0.1 segundos de silencio absoluto (1 canal, mínimos samples)
|
||||
samples = int(sample_rate * 0.1)
|
||||
dummy_waveform = torch.zeros((1, 1, samples), dtype=torch.float32)
|
||||
dummy_audio = {"waveform": dummy_waveform, "sample_rate": sample_rate}
|
||||
|
||||
return (dummy_audio, "\n".join(log_output))
|
||||
|
||||
@register_node
|
||||
class VAESafeFramePadder:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"images": ("IMAGE",),
|
||||
"rule": (["WanVideo (Regla 4n+1)", "LTX (Regla 8n+1)"],),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE", "INT", "STRING")
|
||||
RETURN_NAMES = ("PADDED_IMAGES", "frames_added", "log")
|
||||
FUNCTION = "pad"
|
||||
CATEGORY = "🔁 Sequential Batcher/Tools"
|
||||
|
||||
def pad(self, images, rule):
|
||||
log_output = []
|
||||
def _log(msg):
|
||||
print(msg)
|
||||
log_output.append(str(msg))
|
||||
n_frames = images.shape[0]
|
||||
target_frames = n_frames
|
||||
|
||||
if "WanVideo" in rule:
|
||||
# 🚀 FIX: Regla 4n+1 para WanVideo (Evita la pérdida de 3 frames en el VAE)
|
||||
if n_frames < 5:
|
||||
target_frames = 5
|
||||
else:
|
||||
target_frames = ((n_frames + 2) // 4) * 4 + 1
|
||||
elif "LTX" in rule:
|
||||
if n_frames < 9:
|
||||
target_frames = 9
|
||||
else:
|
||||
target_frames = ((n_frames + 6) // 8) * 8 + 1
|
||||
|
||||
diff = target_frames - n_frames
|
||||
|
||||
_log(f"\n{'='*50}")
|
||||
_log(f"🛡️ [Secuencial Batcher] NODO: VAE Safe Frame Padder")
|
||||
_log(f" -> Frames recibidos: {n_frames}")
|
||||
_log(f" -> Regla aplicada: {rule}")
|
||||
|
||||
if diff > 0:
|
||||
_log(f" -> 🧱 Tensor incompleto. Clonando el último frame {diff} vez/veces...")
|
||||
last_frame = images[-1:] # Extraemos el último frame
|
||||
padding = last_frame.repeat(diff, 1, 1, 1) # Lo multiplicamos
|
||||
images = torch.cat([images, padding], dim=0) # Lo fusionamos al final
|
||||
_log(f" -> ✅ Tensor final acolchado a: {target_frames} frames.")
|
||||
else:
|
||||
_log(f" -> ✅ Tensor perfecto. No requiere acolchado.")
|
||||
|
||||
_log(f"{'='*50}\n")
|
||||
return (images, diff, "\n".join(log_output))
|
||||
|
||||
@register_node
|
||||
class VHS_Path_Selector:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"vhs_filenames": ("VHS_FILENAMES",),
|
||||
"select_file": (["video_with_audio", "video_muted", "png_thumbnail"],),
|
||||
}
|
||||
}
|
||||
|
||||
# 🔄 Ahora devuelve la cadena y el objeto VHS reordenado
|
||||
RETURN_TYPES = ("STRING", "VHS_FILENAMES", "STRING")
|
||||
RETURN_NAMES = ("file_path", "vhs_filenames", "log")
|
||||
FUNCTION = "select"
|
||||
CATEGORY = "🔁 Sequential Batcher/Tools" # 📍 Ubicación corregida
|
||||
|
||||
def select(self, vhs_filenames, select_file):
|
||||
log_output = []
|
||||
def _log(msg):
|
||||
print(msg)
|
||||
log_output.append(str(msg))
|
||||
try:
|
||||
if vhs_filenames is None or not isinstance(vhs_filenames, (list, tuple)) or len(vhs_filenames) < 2:
|
||||
_log("⚠️ [Path Selector] Entrada vhs_filenames vacía o inválida.")
|
||||
return ("", vhs_filenames, "\n".join(log_output))
|
||||
|
||||
paths = vhs_filenames[1]
|
||||
if not isinstance(paths, list) or not paths:
|
||||
return ("", vhs_filenames, "\n".join(log_output))
|
||||
|
||||
# Identificación de archivos
|
||||
audio_path = next((s for s in paths if isinstance(s, str) and s.endswith("-audio.mp4")), None)
|
||||
muted_path = next((s for s in paths if isinstance(s, str) and s.endswith(".mp4") and not s.endswith("-audio.mp4")), None)
|
||||
png_path = next((s for s in paths if isinstance(s, str) and s.endswith(".png")), None)
|
||||
|
||||
# Selección del archivo principal (result)
|
||||
if select_file == "video_with_audio":
|
||||
result = audio_path if audio_path else (muted_path if muted_path else paths[0])
|
||||
elif select_file == "video_muted":
|
||||
result = muted_path if muted_path else (audio_path if audio_path else paths[0])
|
||||
else: # png_thumbnail
|
||||
result = png_path if png_path else paths[0]
|
||||
|
||||
# 🪄 REORDENADO: Ponemos el elegido en la posición 0
|
||||
new_paths = list(paths)
|
||||
if result in new_paths:
|
||||
new_paths.remove(result)
|
||||
new_paths.insert(0, result)
|
||||
|
||||
# Creamos el nuevo paquete VHS [bool, [lista_reordenada]]
|
||||
reordered_vhs = [vhs_filenames[0], new_paths]
|
||||
|
||||
_log(f"📂 [Path Selector] Selección: {select_file} -> {result}")
|
||||
return (str(result), reordered_vhs, "\n".join(log_output))
|
||||
|
||||
except Exception as e:
|
||||
_log(f"❌ [Path Selector] Error crítico: {e}")
|
||||
return ("", vhs_filenames, "\n".join(log_output))
|
||||
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
import torch
|
||||
import gc
|
||||
import comfy.model_management as mm
|
||||
import os
|
||||
from . import register_node
|
||||
|
||||
class AnyType(str):
|
||||
def __ne__(self, __value: object) -> bool:
|
||||
return False
|
||||
|
||||
any_type = AnyType("*")
|
||||
|
||||
def format_bytes(bytes_val):
|
||||
return f"{bytes_val / (1024 ** 3):.2f} GB"
|
||||
|
||||
@register_node
|
||||
class MeisoftVRAMDefragmenter:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"anything": (any_type,),
|
||||
"force_model_unload": ("BOOLEAN", {"default": False}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = (any_type, "STRING")
|
||||
RETURN_NAMES = ("anything", "log")
|
||||
FUNCTION = "defragment"
|
||||
CATEGORY = "🔁 Sequential Batcher/Tools"
|
||||
|
||||
def defragment(self, anything, force_model_unload):
|
||||
log_output = []
|
||||
def _log(msg):
|
||||
print(msg)
|
||||
log_output.append(str(msg))
|
||||
_log(f"\n{'='*65}")
|
||||
_log(f"🧹 [Secuencial Batcher] NODO: VRAM Defragmenter (Modo Arquitectura Profunda)")
|
||||
|
||||
# --- ESTADÍSTICAS ANTES ---
|
||||
alloc_before = 0
|
||||
res_before = 0
|
||||
if torch.cuda.is_available():
|
||||
alloc_before = torch.cuda.memory_allocated()
|
||||
res_before = torch.cuda.memory_reserved()
|
||||
_log(f" -> 📊 Estado Inicial | Asignada: {format_bytes(alloc_before)} | Reservada (Caché): {format_bytes(res_before)}")
|
||||
|
||||
# --- FASE 1: CONFIGURACIÓN DINÁMICA DEL ALLOCATOR ---
|
||||
try:
|
||||
# Permite que la reserva de VRAM crezca en bloque elástico
|
||||
torch.cuda.memory._set_allocator_settings("expandable_segments:True")
|
||||
_log(" -> ⚙️ PyTorch Allocator ajustado a expandable_segments:True")
|
||||
except Exception as e:
|
||||
_log(f" -> ⚠️ Aviso: No se pudo ajustar expandable_segments: {e}")
|
||||
|
||||
# --- FASE 2: EVACUACIÓN (Opcional) ---
|
||||
if force_model_unload:
|
||||
_log(" -> 📦 Trasladando modelos inactivos a la RAM (unload_all_models)...")
|
||||
mm.unload_all_models()
|
||||
|
||||
# --- FASE 3: LIMPIEZA INTERNA DE COMFYUI (LA CLAVE) ---
|
||||
_log(" -> 🧹 Destruyendo referencias ocultas en el gestor de ComfyUI...")
|
||||
if hasattr(mm, "cleanup_models"):
|
||||
mm.cleanup_models()
|
||||
if hasattr(mm, "cleanup_models_gc"):
|
||||
mm.cleanup_models_gc()
|
||||
|
||||
# --- FASE 4: ROMPER REFERENCIAS PYTHON ---
|
||||
_log(" -> 🗑️ Forzando Garbage Collector de Python (Doble pasada)...")
|
||||
gc.collect()
|
||||
gc.collect() # Doble pasada para romper referencias circulares rebeldes
|
||||
|
||||
# --- FASE 5: EVACUACIÓN SUAVE ---
|
||||
mm.soft_empty_cache()
|
||||
|
||||
# --- FASE 6: PURGADO PROFUNDO DE PYTORCH ---
|
||||
if torch.cuda.is_available():
|
||||
_log(" -> 🧱 Desfragmentando Caching Allocator de CUDA...")
|
||||
torch.cuda.ipc_collect()
|
||||
torch.cuda.synchronize() # 🚀 OPTIMIZACIÓN: Esperar a que terminen los kernels pendientes PRIMERO
|
||||
torch.cuda.empty_cache() # 🚀 Luego vaciamos la caché de forma segura
|
||||
|
||||
# --- FASE 7: LIBERACIÓN DE MEMORIA DEL SISTEMA (Multi-Plataforma) ---
|
||||
import platform
|
||||
try:
|
||||
import ctypes
|
||||
if platform.system() == "Windows":
|
||||
# API de Windows: Reduce el 'Working Set' del proceso actual al mínimo
|
||||
kernel32 = ctypes.windll.kernel32
|
||||
current_process = kernel32.GetCurrentProcess()
|
||||
kernel32.SetProcessWorkingSetSize(current_process, -1, -1)
|
||||
_log(" -> 💻 Working Set reducido (RAM del sistema purgada en Windows).")
|
||||
else:
|
||||
# API de Linux/WSL: Libera la memoria de la librería C
|
||||
ctypes.CDLL('libc.so.6').malloc_trim(0)
|
||||
_log(" -> 💻 Malloc Trim ejecutado (RAM del sistema purgada en Linux/WSL).")
|
||||
except Exception as e:
|
||||
# Si algo falla a nivel de SO, fallamos silenciosamente sin romper el nodo
|
||||
pass
|
||||
|
||||
# --- ESTADÍSTICAS DESPUÉS ---
|
||||
if torch.cuda.is_available():
|
||||
alloc_after = torch.cuda.memory_allocated()
|
||||
res_after = torch.cuda.memory_reserved()
|
||||
|
||||
# Cálculos de liberación
|
||||
freed_alloc = alloc_before - alloc_after
|
||||
freed_res = res_before - res_after
|
||||
|
||||
sign_alloc = "+" if freed_alloc < 0 else "-"
|
||||
sign_res = "+" if freed_res < 0 else "-"
|
||||
|
||||
_log(f" -> 📈 Estado Final | Asignada: {format_bytes(alloc_after)} | Reservada (Caché): {format_bytes(res_after)}")
|
||||
_log(f" -> ✨ TOTAL LIBERADO | {sign_alloc}{abs(freed_alloc)/(1024**3):.2f} GB (Asignada) | {sign_res}{abs(freed_res)/(1024**3):.2f} GB (Reservada purgada)")
|
||||
else:
|
||||
_log(" -> ✨ Limpieza completada (Modo CPU/Otra arquitectura).")
|
||||
|
||||
_log(f"{'='*65}\n")
|
||||
return (anything, "\n".join(log_output))
|
||||
Reference in New Issue
Block a user