Compare commits
25
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
98e25a8fe2 | ||
|
|
76bc9498a2 | ||
|
|
e0b60696a7 | ||
|
|
f687c34039 | ||
|
|
1afb103fd2 | ||
|
|
ef4dff94c6 | ||
|
|
3c3ebca5b2 | ||
|
|
ee67c2a105 | ||
|
|
4a003e7973 | ||
|
|
c4290fce8e | ||
|
|
2475b79f21 | ||
|
|
45e3c670f4 | ||
|
|
04efd0e47f | ||
|
|
71e9ea2cb7 | ||
|
|
bd28628314 | ||
|
|
41529ff0a0 | ||
|
|
e3b1722393 | ||
|
|
692cc0daa5 | ||
|
|
2bf5719335 | ||
|
|
d70ec7ef48 | ||
|
|
81cf4df040 | ||
|
|
74f28f2d32 | ||
|
|
f37c0c77b3 | ||
|
|
5089252bca | ||
|
|
1d2faec867 |
@@ -0,0 +1,28 @@
|
||||
name: Publish to Comfy registry
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
paths:
|
||||
- "pyproject.toml"
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
publish-node:
|
||||
name: Publish Custom Node to registry
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository_owner == 'Meisoftcoltd' }}
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
- name: Publish Custom Node
|
||||
uses: Comfy-Org/publish-node-action@v1
|
||||
with:
|
||||
## Add your own personal access token to your Github Repository secrets and reference it here.
|
||||
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
|
||||
@@ -1,5 +1,5 @@
|
||||
# ♾️ ComfyUI Sequential Batcher
|
||||
**v1.6.0** | **VRAM-Optimized Generation** | **Recursive Self-Queuing** | **Perfect Audio Sync**
|
||||
**v0.8.3** | **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 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.
|
||||
|
||||
@@ -7,12 +7,6 @@ Una suite de grado profesional de nodos personalizados para ComfyUI. Diseñada p
|
||||
|
||||
---
|
||||
|
||||
## 🌟 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 Prompt" una sola vez y el flujo se retroalimentará modificando semillas e índices hasta terminar todo el vídeo.
|
||||
@@ -25,7 +19,7 @@ Una suite de grado profesional de nodos personalizados para ComfyUI. Diseñada p
|
||||
|
||||
## ⚠️ Requisitos del Sistema (Crítico)
|
||||
|
||||
Para que el nodo `VideoAnalyzerWithAudio` pueda desempaquetar contenedores `.mp4` y extraer el audio con `torchaudio`, **FFmpeg es absolutamente obligatorio**.
|
||||
Para que el nodo `VideoAnalyzerFaceDetector` pueda desempaquetar contenedores `.mp4` y extraer el audio con `torchaudio`, **FFmpeg es absolutamente obligatorio**.
|
||||
|
||||
* **🐧 Ubuntu / Linux / WSL2:**
|
||||
El script intentará instalarlo por ti. Si falla, ejecuta manualmente:
|
||||
@@ -43,16 +37,43 @@ El argumento `--highvram` le da la orden estricta a ComfyUI de mantener los mode
|
||||
---
|
||||
|
||||
## 🧠 La Arquitectura Híbrida (Desglose de Nodos)
|
||||
El procesamiento se divide en roles altamente especializados:
|
||||
|
||||
* **🕵️ 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.
|
||||
El procesamiento se divide en roles altamente especializados, organizados en las siguientes categorías:
|
||||
|
||||
### 🎼 Orquestación
|
||||
* **🏁 Loop Start (Index):** Inicia la secuencia de bucle y mantiene el índice actual.
|
||||
* **🚀 Loop Trigger (Auto-Queue):** Dispara la siguiente iteración de ComfyUI de manera autónoma hasta que se completa la tarea.
|
||||
|
||||
### 🎬 Director / Storyboard
|
||||
* **🎬 Dynamic Scene Director:** Máquina de estados para orquestar metadatos (prompts, duraciones) en la preproducción (Split-Workflow).
|
||||
* **💾 Save Scene Keyframe / 🖼️ Load Scene Keyframe:** Guarda y carga los frames clave de la escena para mantener la continuidad entre flujos separados.
|
||||
|
||||
### 📦 Lotes (Batches)
|
||||
* **📂 Batch Audio Folder Loader:** Carga dinámicamente carpetas enteras de archivos (ej. lotes de audio).
|
||||
* **🎛️ Audio Batch Selector:** Despacha los archivos del lote uno por uno sincronizándose con el bucle.
|
||||
|
||||
### 🔬 Análisis y Cálculos
|
||||
* **🕵️ Video Analyzer Face detector + Audio:** Escanea el vídeo vía OpenCV/YOLO, extrayendo rostros, audio y frame de referencia.
|
||||
* **🎬 Video Analyzer Scene detector:** Detecta cortes de escena en los vídeos.
|
||||
* **📊 Auto Loop Calculators:** Nodos especializados (`Base`, `WanVideo`, `LTX`, `TTS`) que reciben metadatos y calculan matemáticamente las coordenadas de corte exactas (`chunk_frames`, `skip_frames`).
|
||||
|
||||
### 🎞️ Video
|
||||
* **🎞️ Incremental Auto-Stitcher:** Ensambla progresivamente los fragmentos de video renderizados en un único archivo.
|
||||
* **🛡️ VAE Safe Frame Padder:** Rellena ("acolcha") tensores incompletos clonando el último frame para salvar al VAE de un colapso en modelos restrictivos (ej. reglas 4n+1).
|
||||
* **💉 LTXV Single Frame Injector:** Inyecta frames individuales específicos requeridos por flujos de LTX.
|
||||
* **⏱️ Auto FPS Limiter:** Limita de manera inteligente los cuadros por segundo para ahorrar VRAM manteniendo sincronía de audio.
|
||||
|
||||
### 🎵 Audio
|
||||
* **✂️ Precise Audio Slicer:** Corta el audio de forma precisa (a nivel de muestra matemática) en base a los frames de vídeo correspondientes.
|
||||
* **🎛️ Conditional Audio Router (Bypass):** Enruta el audio condicionalmente permitiendo saltar su procesamiento si no es necesario.
|
||||
|
||||
### 📐 Herramientas de Resolución
|
||||
* **ResTool (`8x`, `16x`, `32x`, `64x`, `64xLTX`):** Calculan resoluciones óptimas basadas en límites de VRAM y las reglas geométricas (multiplicadores) de diferentes modelos (SD1.5, SDXL, WanVideo, Hunyuan, LTX).
|
||||
|
||||
### 🧠 Gestión de Memoria / Lógica
|
||||
* **🔀 Master Switch & 🗄️ Lazy Session Cache:** Evaluación perezosa nativa; amputan rutas inactivas para que los nodos pesados ni siquiera se carguen en memoria.
|
||||
* **🧹 VRAM Defragmenter:** Purga forense de memoria (Secuencia Sagrada) que limpia la caché de CUDA/MPS y fuerza al recolector de basura (GC) de Python entre ciclos.
|
||||
* **📥 Session Image Receiver / 📤 Session Image Sender:** Retienen y transfieren imágenes en memoria a través de diferentes ciclos.
|
||||
|
||||
---
|
||||
|
||||
@@ -69,15 +90,8 @@ El procesamiento se divide en roles altamente especializados:
|
||||
---
|
||||
|
||||
## 🚀 Aceleración por GPU (Escaneo de Rostros)
|
||||
El `VideoAnalyzerWithAudio` tiene un puerto `bbox_detector` para delegar el escaneo facial a la tarjeta gráfica:
|
||||
El `VideoAnalyzerFaceDetector` 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).
|
||||
|
||||
+39
-25
@@ -1,5 +1,5 @@
|
||||
# ♾️ ComfyUI Sequential Batcher
|
||||
**v1.6.0** | **VRAM-Optimized Generation** | **Recursive Self-Queuing** | **Perfect Audio Sync**
|
||||
**v0.8.3** | **VRAM-Optimized Generation** | **Recursive Self-Queuing** | **Perfect Audio Sync**
|
||||
|
||||
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.
|
||||
|
||||
@@ -7,12 +7,6 @@ A professional-grade suite of custom nodes for ComfyUI. Designed to bypass extre
|
||||
|
||||
---
|
||||
|
||||
## 🌟 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 Prompt" just once and the flow will feed itself by modifying seeds and indices until the entire video is finished.
|
||||
@@ -25,7 +19,7 @@ A professional-grade suite of custom nodes for ComfyUI. Designed to bypass extre
|
||||
|
||||
## ⚠️ System Requirements (Critical)
|
||||
|
||||
For the `VideoAnalyzerWithAudio` node to unpack `.mp4` containers and extract audio with `torchaudio`, **FFmpeg is absolutely mandatory**.
|
||||
For the `VideoAnalyzerFaceDetector` node to unpack `.mp4` containers and extract audio with `torchaudio`, **FFmpeg is absolutely mandatory**.
|
||||
|
||||
* **🐧 Ubuntu / Linux / WSL2:**
|
||||
The script will attempt to install it for you. If it fails, run manually:
|
||||
@@ -43,16 +37,43 @@ The `--highvram` argument gives ComfyUI the strict command to keep models locked
|
||||
---
|
||||
|
||||
## 🧠 The Hybrid Architecture (Node Breakdown)
|
||||
Processing is divided into highly specialized roles:
|
||||
|
||||
* **🕵️ 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.
|
||||
Processing is divided into highly specialized roles, organized in the following categories:
|
||||
|
||||
### 🎼 Orchestration
|
||||
* **🏁 Loop Start (Index):** Starts the loop sequence and tracks the current index.
|
||||
* **🚀 Loop Trigger (Auto-Queue):** Autonomously triggers the next ComfyUI iteration until the task is complete.
|
||||
|
||||
### 🎬 Director / Storyboard
|
||||
* **🎬 Dynamic Scene Director:** State machine to orchestrate metadata (prompts, durations) in preproduction (Split-Workflow).
|
||||
* **💾 Save Scene Keyframe / 🖼️ Load Scene Keyframe:** Saves and loads the scene's keyframes to maintain continuity across decoupled flows.
|
||||
|
||||
### 📦 Batches
|
||||
* **📂 Batch Audio Folder Loader:** Dynamically loads entire folders of files (e.g. audio batches).
|
||||
* **🎛️ Audio Batch Selector:** Dispatches files from the batch one by one in sync with the loop.
|
||||
|
||||
### 🔬 Analysis and Calculations
|
||||
* **🕵️ Video Analyzer Face detector + Audio:** Scans the video via OpenCV/YOLO, extracting faces, audio, and the reference frame.
|
||||
* **🎬 Video Analyzer Scene detector:** Detects scene cuts in videos.
|
||||
* **📊 Auto Loop Calculators:** Specialized nodes (`Base`, `WanVideo`, `LTX`, `TTS`) that receive metadata and mathematically calculate the exact cut coordinates (`chunk_frames`, `skip_frames`).
|
||||
|
||||
### 🎞️ Video
|
||||
* **🎞️ Incremental Auto-Stitcher:** Progressively stitches rendered video chunks into a single final file.
|
||||
* **🛡️ VAE Safe Frame Padder:** Pads incomplete tensors by cloning the last frame to save the VAE from a collapse in restrictive models (e.g. 4n+1 rules).
|
||||
* **💉 LTXV Single Frame Injector:** Injects specific individual frames required by LTX flows.
|
||||
* **⏱️ Auto FPS Limiter:** Smartly limits frames per second to save VRAM while maintaining audio sync.
|
||||
|
||||
### 🎵 Audio
|
||||
* **✂️ Precise Audio Slicer:** Precisely slices audio (at mathematical sample level) based on the corresponding video frames.
|
||||
* **🎛️ Conditional Audio Router (Bypass):** Conditionally routes audio allowing its processing to be bypassed if not needed.
|
||||
|
||||
### 📐 Resolution Tools
|
||||
* **ResTool (`8x`, `16x`, `32x`, `64x`, `64xLTX`):** Calculate optimal resolutions based on VRAM limits and geometric rules (multipliers) of different models (SD1.5, SDXL, WanVideo, Hunyuan, LTX).
|
||||
|
||||
### 🧠 Memory Management / Logic
|
||||
* **🔀 Master Switch & 🗄️ Lazy Session Cache:** Native lazy evaluation; they cut inactive routes so heavy nodes aren't even loaded into memory.
|
||||
* **🧹 VRAM Defragmenter:** Forensic memory purge (Sacred Sequence) that clears CUDA/MPS cache and forces the Python Garbage Collector (GC) between cycles.
|
||||
* **📥 Session Image Receiver / 📤 Session Image Sender:** Retain and transfer in-memory images across different cycles.
|
||||
|
||||
---
|
||||
|
||||
@@ -69,15 +90,8 @@ Processing is divided into highly specialized roles:
|
||||
---
|
||||
|
||||
## 🚀 GPU Acceleration (Face Scanning)
|
||||
The `VideoAnalyzerWithAudio` has a `bbox_detector` port to delegate facial scanning to the graphics card:
|
||||
The `VideoAnalyzerFaceDetector` 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).
|
||||
|
||||
+3
-2
@@ -1,4 +1,4 @@
|
||||
__version__ = "1.6.0"
|
||||
__version__ = "0.8.3"
|
||||
|
||||
NODE_CLASS_MAPPINGS = {}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {}
|
||||
@@ -48,7 +48,8 @@ NODE_DISPLAY_NAME_MAPPINGS.update({
|
||||
"PreciseAudioSlicer": "✂️ Precise Audio Slicer",
|
||||
"SaveSceneKeyframe": "💾 Save Scene Keyframe",
|
||||
"LoadSceneKeyframe": "🖼️ Load Scene Keyframe",
|
||||
"LTXVSingleFrameInjector": "💉 LTXV Single Frame Injector"
|
||||
"LTXVSingleFrameInjector": "💉 LTXV Single Frame Injector",
|
||||
"ProjectPlanLoader": "📂 Project Plan Loader"
|
||||
})
|
||||
|
||||
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']
|
||||
|
||||
@@ -87,21 +87,13 @@ class SequentialLoopStart:
|
||||
|
||||
is_reset = str(reset_loop).lower() in ['true', '1', 't', 'y']
|
||||
|
||||
# 💡 FIX: Detectar si es un arranque manual del usuario para resetear el lote
|
||||
if loop_idx == 0 and not is_reset:
|
||||
global_is_batch_advancing = False
|
||||
global_batch_index = 0
|
||||
print(" -> 🆕 Arranque manual detectado. Lote (Batch) reiniciado a 0.")
|
||||
|
||||
if is_reset or loop_idx == 0:
|
||||
global_loop_index = 0
|
||||
global_accumulated_frames = 0
|
||||
global_ltx_mode = False
|
||||
print(" -> 🔄 Bucle y Acumulador reiniciados a 0.")
|
||||
# HACK: Mantenemos global_is_batch_advancing = True durante el ciclo 0
|
||||
|
||||
else:
|
||||
global_loop_index = loop_idx
|
||||
# Lo apagamos a partir del ciclo 1
|
||||
global_is_batch_advancing = False
|
||||
|
||||
print(f" -> 📍 Índice actual de bucle: {global_loop_index}")
|
||||
@@ -151,25 +143,11 @@ class AutoLoopCalculatorTTSBatch:
|
||||
mode = split_mode[0] if isinstance(split_mode, list) else split_mode
|
||||
idx = current_loop_index[0] if isinstance(current_loop_index, list) else current_loop_index
|
||||
|
||||
# 💡 FIX: Detección inteligente de nuevos flujos mediante Hash
|
||||
current_hash = hash(str(texts))
|
||||
last_hash = getattr(loop, 'global_last_text_hash', None)
|
||||
# El batch index es extraído directamente de idx para evitar bloqueos
|
||||
current_batch_idx = idx
|
||||
|
||||
if idx == 0:
|
||||
if current_hash != last_hash:
|
||||
_log(" -> 🆕 Nuevo texto detectado. Forzando reinicio del lote (Batch) a 0.")
|
||||
loop.global_batch_index = 0
|
||||
loop.global_is_batch_advancing = False
|
||||
loop.global_last_text_hash = current_hash
|
||||
elif not getattr(loop, 'global_is_batch_advancing', False):
|
||||
loop.global_batch_index = 0
|
||||
|
||||
current_batch_idx = getattr(loop, 'global_batch_index', 0)
|
||||
|
||||
# Seguridad adicional por si el índice se desborda por cachés antiguos
|
||||
if current_batch_idx >= len(texts):
|
||||
current_batch_idx = len(texts) - 1
|
||||
loop.global_batch_index = current_batch_idx
|
||||
|
||||
current_file_text = texts[current_batch_idx]
|
||||
current_name = names[current_batch_idx] if current_batch_idx < len(names) else "unknown"
|
||||
@@ -189,7 +167,7 @@ class AutoLoopCalculatorTTSBatch:
|
||||
if raw_chunks: raw_chunks[-1] += " " + buffer_text
|
||||
else: raw_chunks.append(buffer_text)
|
||||
chunk_type_name = "Frases Optimizadas"
|
||||
|
||||
else:
|
||||
raw_chunks = [p.strip() for p in current_file_text.split('\n') if p.strip()]
|
||||
chunk_type_name = "Párrafos"
|
||||
|
||||
@@ -418,7 +396,7 @@ class SequentialLoopTrigger:
|
||||
if is_final_chunk and has_more_batches:
|
||||
inputs["loop_idx"] = 0
|
||||
inputs["reset_loop"] = True
|
||||
|
||||
else:
|
||||
inputs["loop_idx"] = next_loop
|
||||
inputs["reset_loop"] = False
|
||||
|
||||
@@ -443,10 +421,8 @@ class SequentialLoopTrigger:
|
||||
inputs["is_final_cycle"] = is_next_final
|
||||
|
||||
if is_final_chunk and has_more_batches:
|
||||
loop.global_batch_index = getattr(loop, 'global_batch_index', 0) + 1
|
||||
loop.global_is_batch_advancing = True
|
||||
print(f" -> 📦 Archivo finalizado. Iniciando archivo {loop.global_batch_index + 1} del lote...")
|
||||
|
||||
print(f" -> 📦 Archivo finalizado. Iniciando siguiente archivo del lote...")
|
||||
else:
|
||||
print(f" -> ⚙️ Preparando Ciclo {next_loop}...")
|
||||
|
||||
p = {"prompt": prompt}
|
||||
@@ -459,32 +435,8 @@ class SequentialLoopTrigger:
|
||||
except Exception as e:
|
||||
print(f" -> ❌ Error HTTP: {e}")
|
||||
|
||||
import gc, torch
|
||||
gc.collect()
|
||||
if torch.cuda.is_available(): torch.cuda.empty_cache()
|
||||
|
||||
|
||||
else:
|
||||
print(f" -> 🏁 ¡Generación Finalizada! Todos los archivos del lote completados.")
|
||||
# ... Mantener aquí el código existente de limpieza extrema de VRAM (mm.unload_all_models, malloc_trim, etc.) ...
|
||||
print(f" -> 🧹 Iniciando vaciado automático de VRAM...")
|
||||
import gc
|
||||
import torch
|
||||
import comfy.model_management as mm
|
||||
try:
|
||||
mm.unload_all_models()
|
||||
mm.soft_empty_cache()
|
||||
except Exception as e:
|
||||
pass
|
||||
gc.collect()
|
||||
if torch.cuda.is_available():
|
||||
torch.cuda.empty_cache()
|
||||
torch.cuda.ipc_collect()
|
||||
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()
|
||||
print(f" -> ✨ VRAM liberada con éxito. Gráfica lista para nuevos flujos.")
|
||||
|
||||
print(f"{'='*50}\n")
|
||||
return ()
|
||||
@@ -587,37 +539,22 @@ class AudioBatchSelector:
|
||||
|
||||
idx = current_loop_index[0] if isinstance(current_loop_index, list) else current_loop_index
|
||||
|
||||
current_hash = hash(str(names))
|
||||
last_hash = getattr(loop, 'global_last_audio_hash', None)
|
||||
|
||||
if idx == 0:
|
||||
if current_hash != last_hash:
|
||||
_log(" -> 🆕 Nuevo lote de audios detectado. Reiniciando Batch a 0.")
|
||||
loop.global_batch_index = 0
|
||||
loop.global_is_batch_advancing = False
|
||||
loop.global_last_audio_hash = current_hash
|
||||
elif not getattr(loop, 'global_is_batch_advancing', False):
|
||||
loop.global_batch_index = 0
|
||||
|
||||
# 🧠 FIX: El archivo lo dicta el índice de lote global, NO el índice de ciclo (que es para escenas)
|
||||
current_batch_idx = getattr(loop, 'global_batch_index', 0)
|
||||
|
||||
if current_batch_idx >= len(audios):
|
||||
current_batch_idx = len(audios) - 1
|
||||
loop.global_batch_index = current_batch_idx
|
||||
|
||||
current_audio = audios[current_batch_idx]
|
||||
current_name = names[current_batch_idx]
|
||||
total_audios = len(audios)
|
||||
|
||||
# Control de comunicación con el Trigger final
|
||||
loop.global_has_more_batches = (current_batch_idx < total_audios - 1)
|
||||
|
||||
_log(f"\n{'='*50}")
|
||||
_log(f"🎛️ [Secuencial Batcher] NODO: Audio Batch Selector")
|
||||
_log(f" -> Lote (Batch) Actual: {current_batch_idx + 1} de {total_audios}")
|
||||
_log(f" -> Ciclo interno del video: {idx}")
|
||||
_log(f" -> Archivo activo: {current_name}")
|
||||
_log(f" -> Quedan archivos en el lote: {'Sí' if loop.global_has_more_batches else 'No'}")
|
||||
_log(f"{'='*50}\n")
|
||||
|
||||
return (current_audio, current_name, current_batch_idx, total_audios, "\n".join(log_output))
|
||||
@@ -647,56 +584,58 @@ class DynamicSceneDirector:
|
||||
log_output = []
|
||||
def _log(msg): print(msg); log_output.append(str(msg))
|
||||
from . import loop
|
||||
import os, folder_paths, json, math
|
||||
import os, folder_paths, json, math, re
|
||||
|
||||
idx = current_loop_index[0] if isinstance(current_loop_index, list) else current_loop_index
|
||||
|
||||
_log(f"\n{'='*50}")
|
||||
_log(f"🎬 [Director] Evaluando Ciclo: {idx}")
|
||||
|
||||
try:
|
||||
data = json.loads(agent_json)
|
||||
scenes = data.get("scenes", [])
|
||||
base_output = folder_paths.get_output_directory()
|
||||
safe_audio_name = "".join(c for c in audio_filename if c.isalnum() or c in " _-").strip() or "proyecto"
|
||||
plan_path = os.path.join(base_output, f"{safe_audio_name}_plan.json")
|
||||
|
||||
base_output = folder_paths.get_output_directory()
|
||||
safe_audio_name = "".join(c for c in audio_filename if c.isalnum() or c in " _-").strip() or "proyecto"
|
||||
plan_path = os.path.join(base_output, f"{safe_audio_name}_plan.json")
|
||||
# 1. 🛡️ GESTIÓN DEL JSON (Ignora a Ollama a partir del ciclo 1)
|
||||
if idx == 0:
|
||||
try:
|
||||
# Extracción robusta por si el LLM añade texto extra
|
||||
json_match = re.search(r'\{.*\}', agent_json, re.DOTALL)
|
||||
raw_json = json_match.group() if json_match else agent_json
|
||||
data = json.loads(raw_json)
|
||||
|
||||
with open(plan_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(data, f, indent=2)
|
||||
|
||||
except Exception as e:
|
||||
raise ValueError(f"❌ Error en JSON: {e}")
|
||||
with open(plan_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(data, f, indent=2)
|
||||
_log(f" -> 💾 Plan maestro guardado en: {plan_path}")
|
||||
except Exception as e:
|
||||
raise ValueError(f"❌ Error al interpretar el JSON del Agente: {e}")
|
||||
else:
|
||||
try:
|
||||
with open(plan_path, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
_log(f" -> ♻️ Plan recuperado del disco (Ignorando entrada del Agente).")
|
||||
except Exception as e:
|
||||
raise ValueError(f"❌ Error al leer el JSON guardado en {plan_path}: {e}")
|
||||
|
||||
scenes = data.get("scenes", [])
|
||||
total_scenes = len(scenes)
|
||||
if total_scenes == 0: raise ValueError("❌ JSON sin escenas.")
|
||||
|
||||
# 🧠 NUEVA MÁQUINA DE ESTADOS (Por Escena y Chunks)
|
||||
if idx == 0:
|
||||
loop.global_current_scene_index = 0
|
||||
loop.global_accumulated_frames = 0
|
||||
loop.global_audio_offset_frames = 0
|
||||
loop.global_is_final_chunk = False
|
||||
_log(" -> 🆕 Inicio de Proyecto. Estado reseteado.")
|
||||
|
||||
elif getattr(loop, 'global_is_final_chunk', False):
|
||||
loop.global_audio_offset_frames = getattr(loop, 'global_audio_offset_frames', 0) + getattr(loop, 'global_source_frame_count', 0)
|
||||
loop.global_current_scene_index = getattr(loop, 'global_current_scene_index', 0) + 1
|
||||
loop.global_accumulated_frames = 0
|
||||
loop.global_is_final_chunk = False
|
||||
_log(" -> ⏭️ Escena terminada. Avanzando a la siguiente escena del guion.")
|
||||
|
||||
|
||||
scene_idx = getattr(loop, 'global_current_scene_index', 0)
|
||||
# 2. ⏱️ MÁQUINA DE ESTADOS (1 Ciclo = 1 Escena)
|
||||
# El director es un observador pasivo, scene_idx se deriva directamente de idx
|
||||
scene_idx = idx
|
||||
|
||||
if scene_idx >= total_scenes:
|
||||
scene_idx = total_scenes - 1
|
||||
loop.global_is_absolute_video_final = True
|
||||
else:
|
||||
loop.global_is_absolute_video_final = False
|
||||
|
||||
scene = scenes[scene_idx]
|
||||
|
||||
# 🧠 REGLA APLICADA: Este nodo actúa como "Calculator" del flujo JSON
|
||||
loop.global_step_by_chunk = True
|
||||
loop.global_source_frame_count = total_scenes
|
||||
loop.global_accumulated_frames = scene_idx + 1
|
||||
loop.global_is_final_chunk = (scene_idx + 1) >= total_scenes
|
||||
loop.global_is_absolute_video_final = loop.global_is_final_chunk
|
||||
|
||||
keyframes_dir = os.path.join(base_output, f"{safe_audio_name}_Keyframes")
|
||||
os.makedirs(keyframes_dir, exist_ok=True)
|
||||
|
||||
@@ -707,11 +646,9 @@ class DynamicSceneDirector:
|
||||
duration = scene.get("duration_seconds", 5.0)
|
||||
chunk_frames = math.ceil(duration * fps)
|
||||
|
||||
loop.global_source_frame_count = chunk_frames
|
||||
|
||||
_log(f" -> 🎬 Procesando Escena {scene_idx + 1}/{total_scenes} ({duration}s)")
|
||||
|
||||
_log(f"{'='*50}\n")
|
||||
|
||||
return (flux_prompt, wan_prompt, chunk_frames, image_path, plan_path)
|
||||
|
||||
@register_node
|
||||
@@ -773,7 +710,12 @@ class IncrementalVideoStitcher:
|
||||
stride = getattr(loop, 'global_select_every_nth', 1)
|
||||
ltx_mode = getattr(loop, 'global_ltx_mode', False)
|
||||
advanced_frames = max(1, (frames_accepted - 1) * stride) if ltx_mode else frames_accepted * stride
|
||||
loop.global_accumulated_frames += advanced_frames
|
||||
|
||||
# 🧠 PROTECCIÓN: Solo el Stitcher suma frames si NO estamos en modo Chunk (Director/TTS)
|
||||
is_chunk_mode = getattr(loop, 'global_step_by_chunk', False)
|
||||
if not is_chunk_mode:
|
||||
loop.global_accumulated_frames += advanced_frames
|
||||
loop.global_is_final_chunk = loop.global_accumulated_frames >= getattr(loop, 'global_source_frame_count', 1)
|
||||
|
||||
is_absolute_final = getattr(loop, 'global_is_absolute_video_final', False) and getattr(loop, 'global_is_final_chunk', False)
|
||||
has_more_batches = getattr(loop, 'global_has_more_batches', False)
|
||||
@@ -811,4 +753,61 @@ class IncrementalVideoStitcher:
|
||||
|
||||
return (final_tensor, final_audio, True, not has_more_batches, "\n".join(log_output))
|
||||
|
||||
return (images[-1:].clone(), None, False, False, "\n".join(log_output))
|
||||
# 💡 FIX: Retorno de seguridad para ciclos intermedios
|
||||
# Devolvemos el último frame procesado para que el flujo no rompa
|
||||
return (images[-1:].clone(), None, False, False, "\n".join(log_output))
|
||||
|
||||
@register_node
|
||||
class ProjectPlanLoader:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"audio_filename": ("STRING", {"forceInput": True, "tooltip": "Conectar al 'current_file_name' del Audio Batch Selector"}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING", "INT", "STRING")
|
||||
RETURN_NAMES = ("agent_json", "scene_count", "plan_path")
|
||||
FUNCTION = "load_plan"
|
||||
CATEGORY = "🔁 Sequential Batcher/Director"
|
||||
|
||||
@classmethod
|
||||
def IS_CHANGED(cls, **kwargs):
|
||||
# Queremos que siempre lea el archivo más reciente del disco
|
||||
import time
|
||||
return time.time()
|
||||
|
||||
def load_plan(self, audio_filename):
|
||||
log_output = []
|
||||
def _log(msg): print(msg); log_output.append(str(msg))
|
||||
|
||||
import os
|
||||
import json
|
||||
import folder_paths
|
||||
|
||||
_log(f"\n{'='*50}")
|
||||
_log(f"📂 [Plan Loader] Buscando proyecto para: {audio_filename}")
|
||||
|
||||
# La misma lógica exacta del DynamicSceneDirector
|
||||
base_output = folder_paths.get_output_directory()
|
||||
safe_audio_name = "".join(c for c in audio_filename if c.isalnum() or c in " _-").strip() or "proyecto"
|
||||
plan_path = os.path.join(base_output, f"{safe_audio_name}_plan.json")
|
||||
|
||||
if not os.path.exists(plan_path):
|
||||
raise ValueError(f"❌ Error: No se encontró el plan de proyecto en {plan_path}. Asegúrate de que el Flujo 1 lo haya generado.")
|
||||
|
||||
with open(plan_path, 'r', encoding='utf-8') as f:
|
||||
raw_json = f.read()
|
||||
|
||||
try:
|
||||
data = json.loads(raw_json)
|
||||
scenes = data.get("scenes", [])
|
||||
scene_count = len(scenes)
|
||||
except Exception as e:
|
||||
raise ValueError(f"❌ Error al interpretar el JSON guardado en {plan_path}: {e}")
|
||||
|
||||
_log(f" -> ✅ Plan cargado con éxito. Escenas detectadas: {scene_count}")
|
||||
_log(f"{'='*50}\n")
|
||||
|
||||
return (raw_json, scene_count, plan_path)
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
[project]
|
||||
name = "Sequential-Batcher"
|
||||
description = "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."
|
||||
version = "0.8.3"
|
||||
license = "MIT"
|
||||
# classifiers = [
|
||||
# # For OS-independent nodes (works on all operating systems)
|
||||
# "Operating System :: OS Independent",
|
||||
#
|
||||
# # OR for OS-specific nodes, specify the supported systems:
|
||||
# "Operating System :: Microsoft :: Windows", # Windows specific
|
||||
# "Operating System :: POSIX :: Linux", # Linux specific
|
||||
# "Operating System :: MacOS", # macOS specific
|
||||
#
|
||||
# # GPU Accelerator support. Pick the ones that are supported by your extension.
|
||||
# "Environment :: GPU :: NVIDIA CUDA", # NVIDIA CUDA support
|
||||
# "Environment :: GPU :: AMD ROCm", # AMD ROCm support
|
||||
# "Environment :: GPU :: Intel Arc", # Intel Arc support
|
||||
# "Environment :: NPU :: Huawei Ascend", # Huawei Ascend support
|
||||
# "Environment :: GPU :: Apple Metal", # Apple Metal support
|
||||
# ]
|
||||
|
||||
dependencies = ["torch", "torchaudio", "numpy", "Pillow", "opencv-python", "safetensors"]
|
||||
|
||||
[project.urls]
|
||||
Repository = "https://github.com/Meisoftcoltd/ComfyUI-Sequential-Batcher"
|
||||
# Used by Comfy Registry https://registry.comfy.org
|
||||
Documentation = "https://github.com/Meisoftcoltd/ComfyUI-Sequential-Batcher/wiki"
|
||||
"Bug Tracker" = "https://github.com/Meisoftcoltd/ComfyUI-Sequential-Batcher/issues"
|
||||
|
||||
[tool.comfy]
|
||||
PublisherId = ""
|
||||
DisplayName = "ComfyUI-Sequential-Batcher"
|
||||
Icon = ""
|
||||
includes = []
|
||||
# "requires-comfyui" = ">=1.0.0" # ComfyUI version compatibility
|
||||
|
||||
@@ -465,6 +465,7 @@ class SaveSceneKeyframe:
|
||||
|
||||
import os, numpy as np
|
||||
from PIL import Image
|
||||
from . import loop
|
||||
|
||||
img_array = 255. * image[0].cpu().numpy()
|
||||
img = Image.fromarray(np.clip(img_array, 0, 255).astype(np.uint8))
|
||||
@@ -473,6 +474,10 @@ class SaveSceneKeyframe:
|
||||
os.rmdir(file_path) # Borra la carpeta fantasma creada por WAS
|
||||
os.makedirs(os.path.dirname(file_path), exist_ok=True)
|
||||
img.save(file_path) # Ahora sí, guarda la imagen (sobrescribiendo si existe)
|
||||
|
||||
# 🧠 HACK DE RELOJ (Art Flow): Marca la escena como terminada para que el Director avance.
|
||||
loop.global_is_final_chunk = True
|
||||
|
||||
print(f"💾 [Keyframe Saver] Guardado con éxito en: {file_path}")
|
||||
return (image, file_path)
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import torch
|
||||
import gc
|
||||
import comfy.model_management as mm
|
||||
import os
|
||||
from . import register_node
|
||||
|
||||
class AnyType(str):
|
||||
|
||||
Reference in New Issue
Block a user