fix: blindar IncrementalVideoStitcher para evitar sobreescritura de frames

- Se introduce la subcarpeta `meisoft_video_cache`.
- Se guardan los lotes con un timestamp en milisegundos para evitar sobreescrituras en caso de que falle el `current_loop_index`.
- Se incorpora `@classmethod def IS_CHANGED` con `time.time()` para obligar a ComfyUI a ejecutar siempre el nodo e ignorar el cache.
- El directorio temporal se limpia correctamente en el ciclo 0 y se destruye tras ensamblar el vídeo completo.
This commit is contained in:
Meisoftcoltd
2026-04-12 14:26:43 +00:00
parent 4c5d15567b
commit 8dc273ff16
+59 -19
View File
@@ -337,41 +337,81 @@ class IncrementalVideoStitcher:
FUNCTION = "stitch"
CATEGORY = "🔁 Sequential Batcher/Video"
# 🚀 EVITA QUE COMFYUI IGNORE EL NODO USANDO CACHÉ
@classmethod
def IS_CHANGED(cls, **kwargs):
import time
return time.time()
def stitch(self, images, audio, current_loop_index):
from . import loop
import os, folder_paths, torch
import os, folder_paths, torch, shutil, time
cache_dir = os.path.join(folder_paths.get_temp_directory(), "wan_stitcher_cache")
os.makedirs(cache_dir, exist_ok=True)
# 1. Definir la subcarpeta temporal dedicada
cache_dir = os.path.join(folder_paths.get_temp_directory(), "meisoft_video_cache")
path = os.path.join(cache_dir, f"batch_{current_loop_index:04d}.pt")
# 2. Si es el primer ciclo, purgar la carpeta de renders anteriores
if current_loop_index == 0:
if os.path.exists(cache_dir):
try:
shutil.rmtree(cache_dir)
except Exception as e:
print(f" -> ⚠️ No se pudo limpiar la caché antigua: {e}")
os.makedirs(cache_dir, exist_ok=True)
print(f"\n🧹 [Stitcher] Ciclo 0 detectado. Subcarpeta temporal limpiada y lista.")
else:
os.makedirs(cache_dir, exist_ok=True)
# 3. Guardar las imágenes en la subcarpeta con un nombre a prueba de errores
# Usamos un timestamp para que se ordenen cronológicamente aunque el índice falle
timestamp = int(time.time() * 1000)
filename = f"batch_{current_loop_index:04d}_{timestamp}.pt"
path = os.path.join(cache_dir, filename)
# Guardamos el tensor en la CPU para no saturar la VRAM
torch.save(images.cpu(), path)
print(f"🎞️ [Stitcher] Lote {current_loop_index} guardado en disco ({images.shape[0]} frames).")
print(f"📦 [Stitcher] Guardando lote temporal en: {filename} ({images.shape[0]} frames).")
# 4. Comprobar si hemos llegado al final
source_total = getattr(loop, 'global_source_frame_count', 1)
is_final_cycle = loop.global_accumulated_frames >= source_total
is_final_cycle = getattr(loop, 'global_accumulated_frames', 0) >= source_total
if is_final_cycle:
print(f" -> 🏁 Generación completa. Liberando tensor total al flujo para procesos finales.")
print(f"📦 [Stitcher] Ensamblando todos los lotes de vídeo...")
print(f" -> 🏁 ¡Último ciclo detectado! Extrayendo todos los frames de la subcarpeta...")
all_tensors = []
for i in range(current_loop_index + 1):
p = os.path.join(cache_dir, f"batch_{i:04d}.pt")
if os.path.exists(p):
all_tensors.append(torch.load(p))
try: os.remove(p)
except: pass
# Listar y ordenar todos los archivos .pt de la subcarpeta cronológicamente
batch_files = sorted([f for f in os.listdir(cache_dir) if f.endswith('.pt')])
if not batch_files:
print(" -> ❌ ERROR: No se encontraron frames en la subcarpeta.")
return (images, audio, True)
for f in batch_files:
p = os.path.join(cache_dir, f)
try:
tensor_batch = torch.load(p)
all_tensors.append(tensor_batch)
print(f" -> 🧩 Añadiendo: {f} ({tensor_batch.shape[0]} frames)")
except Exception as e:
print(f" -> ❌ Error leyendo {f}: {e}")
# Ensamblar el vídeo completo
final_tensor = torch.cat(all_tensors, dim=0)
print(f"✅ [Stitcher] Vídeo completado: {final_tensor.shape[0]} frames ensamblados.")
print(f"✅ [Stitcher] VÍDEO COMPLETADO: {final_tensor.shape[0]} frames ensamblados con éxito.")
# 🚀 Retornamos True al final
# Destruir la subcarpeta temporal para no dejar basura
try:
shutil.rmtree(cache_dir)
print(f"🧹 [Stitcher] Subcarpeta temporal destruida.")
except:
pass
# Retornar el vídeo completo
return (final_tensor, audio, True)
else:
print(f" -> ⏳ Ciclo intermedio. Soltando 1 frame dummy...")
print(f" -> ⏳ Ciclo intermedio. Almacenado de forma segura. Pasando 1 frame dummy al pipeline...")
dummy_frame = images[-1:].clone()
# 🚀 Retornamos False al final
return (dummy_frame, None, False)
@register_node