Compare commits

...
Author SHA1 Message Date
Meisoft 98e25a8fe2 Fix UnboundLocalError in AutoLoopCalculatorTTSBatch: make raw_chunks initialization mutually exclusive with mode 2026-08-14 09:03:21 +02:00
Meisoft 76bc9498a2 Merge pull request #157 from Meisoftcoltd/feat/project-plan-loader-12461161277134022224
Add ProjectPlanLoader bridge node
2026-07-01 21:14:31 +02:00
Meisoftcoltd e0b60696a7 feat: add ProjectPlanLoader node for split-workflow bridging
- Appended `ProjectPlanLoader` class to `loop.py`.
- Followed codebase conventions by placing imports (`os`, `json`, `folder_paths`) locally inside the `load_plan` method.
- Registered the node's display name ("📂 Project Plan Loader") in `__init__.py`.
2026-07-01 18:22:45 +00:00
Meisoft f687c34039 Merge pull request #156 from Meisoftcoltd/fix-batch-scene-sync-12454531999106626496
fix: synchronize batch loops with scene chunks
2026-07-01 18:18:33 +02:00
Meisoftcoltd 1afb103fd2 fix: synchronize batch loops with scene chunks
- Inject global state rules into DynamicSceneDirector to act as a chunk Calculator.
- Protect IncrementalVideoStitcher from accumulating frames in chunk mode.
- Anchor AudioBatchSelector file selection to global_batch_index instead of loop index.
- Remove erroneous global_batch_index reset from SequentialLoopStart on scene resets.
2026-06-30 23:02:55 +00:00
Meisoft ef4dff94c6 Merge pull request #155 from Meisoftcoltd/jules-177535240431834190-8f54f3bd
Refactor loop flow control to center on global_loop_index
2026-06-30 23:16:36 +02:00
Meisoftcoltd 3c3ebca5b2 Refactor loop flow control to center on global_loop_index
Centralizes flow control using global_loop_index as the single source of truth.
Removes legacy batch reset logic from loaders and passive nodes.
Strips VRAM memory clearing hacks from Trigger node, delegating entirely to dedicated defragmenter.
Updates DynamicSceneDirector and AudioBatchSelector to passive observer logic.
2026-06-30 21:14:53 +00:00
Meisoft ee67c2a105 Merge pull request #154 from Meisoftcoltd/jules-16613116407217706356-cfe1cdee
fix(loop): restore missing else statement in SequentialLoopStart to fix infinite cycle 0
2026-06-30 20:36:30 +02:00
Meisoftcoltd 4a003e7973 fix(loop): restore missing else statement in SequentialLoopStart to fix infinite cycle 0 2026-06-30 18:34:41 +00:00
Meisoft c4290fce8e Merge pull request #153 from Meisoftcoltd/jules-6283429635430324294-7379d2c7
docs: restructure node documentation and update version to v0.8.3
2026-06-30 17:58:18 +02:00
Meisoftcoltd 2475b79f21 docs: refactor READMEs, group nodes by category, and update version to v0.8.3
- Bumped version in `__init__.py` to 0.8.3.
- Removed "What's New" and "Changelog" sections from `README.md` and `README_EN.md` as per user request to avoid confusion with the version change.
- Refactored the Node Breakdown section to group all new and existing nodes into distinct logical categories (Orchestration, Director, Batches, Analysis, Video, Audio, Resolution, Memory).
2026-06-30 15:50:25 +00:00
Meisoft 45e3c670f4 Update pyproject.toml 2026-06-30 17:27:20 +02:00
Meisoft 04efd0e47f Merge pull request #152 from Meisoftcoltd/fix-director-state-machine-17416656692113806519
fix: refactor DynamicSceneDirector state machine for direct_scene
2026-06-30 17:23:37 +02:00
Meisoftcoltd 71e9ea2cb7 fix(loop): rewrite direct_scene state machine for JSON caching
- Replace `direct_scene` in `DynamicSceneDirector` to implement a strict 1 cycle = 1 scene state machine.
- Read plan from cached JSON file after cycle 0 to avoid redundant LLM calls.
- Remove obsolete `global_audio_offset_frames` logic, simplifying the preproduction flow where audio sync is unnecessary.
2026-06-30 15:23:14 +00:00
Meisoft bd28628314 Merge pull request #120 from Meisoftcoltd/fix/remove-unused-os-import-9501258116321239895
🧹 Code Health: Remove unused os import in vram_node.py
2026-06-30 17:02:31 +02:00
Meisoft 41529ff0a0 Merge pull request #4 from ComfyNodePRs/publish
Add Github Action for Publishing to Comfy Registry
2026-06-30 17:00:23 +02:00
Meisoft e3b1722393 Merge pull request #3 from ComfyNodePRs/pyproject
Add pyproject.toml for Custom Node Registry
2026-06-30 16:59:42 +02:00
Meisoft 692cc0daa5 Merge pull request #151 from Meisoftcoltd/fix-stitcher-intermediate-loops-1353872024854991906
fix(stitcher): prevent NoneType return in intermediate loops.
2026-06-30 16:15:34 +02:00
Meisoftcoltd 2bf5719335 fix(stitcher): prevent NoneType return in intermediate loops. 2026-06-30 14:06:39 +00:00
Meisoft d70ec7ef48 Merge pull request #150 from Meisoftcoltd/fix-sequential-loop-trigger-vram-cleanup-9208431539197096861
fix(core): add missing else blocks in trigger and implement art-flow clock in SaveSceneKeyframe
2026-06-30 15:12:26 +02:00
Meisoftcoltd 81cf4df040 fix(core): add missing else blocks in trigger and implement art-flow clock in SaveSceneKeyframe 2026-06-30 12:47:21 +00:00
Meisoft 74f28f2d32 Merge pull request #149 from Meisoftcoltd/refactor-director-persistent-json-15513519010271136475
refactor(director): Persistent JSON state and output unification
2026-06-30 13:10:13 +02:00
async () = process.env.GIT_USERNAME || (await ghUser()).email && (await ghUser()).name || DIE("Missing env.GIT_USERNAME") f37c0c77b3 chore(pyproject): Add pyproject.toml for Custom Node Registry 2026-06-29 18:14:20 +00:00
Meisoftcoltd 5089252bca 🧹 Removed unused os import from vram_node.py 2026-05-14 21:52:55 +00:00
async () = process.env.GIT_USERNAME || (await ghUser()).email && (await ghUser()).name || DIE("Missing env.GIT_USERNAME") 1d2faec867 chore(publish): Add Github Action for Publishing to Comfy Registry 2026-01-28 10:16:32 +00:00
8 changed files with 259 additions and 162 deletions
+28
View File
@@ -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 }}
+39 -25
View File
@@ -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
View File
@@ -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
View File
@@ -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']
+108 -109
View File
@@ -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)
+37
View File
@@ -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
+5
View File
@@ -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
View File
@@ -1,7 +1,6 @@
import torch
import gc
import comfy.model_management as mm
import os
from . import register_node
class AnyType(str):