feat: rename [DEBUG] to [Secuencial Batcher] and add STRING log outputs to all nodes

This commit addresses two primary requests:
1. Replaces all console log prefixes `[DEBUG]` (and variations) with `[Secuencial Batcher]`.
2. Extends the `RETURN_TYPES` and `RETURN_NAMES` of all nodes to output a `log` string containing exactly the text emitted via `print()`. This string is constructed by intercepting `print()` calls inside node methods using a local wrapper and appending the texts to a list, which is then returned joined by newlines.
This commit is contained in:
Meisoftcoltd
2026-04-19 13:33:52 +00:00
parent 7d266f818c
commit ce6041da4a
8 changed files with 217 additions and 165 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
+36 -28
View File
@@ -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
@@ -110,14 +118,14 @@ class SessionImageSender:
stride = getattr(loop, 'global_select_every_nth', 1)
ltx_mode_active = getattr(loop, 'global_ltx_mode', False)
print(f"\n{'='*50}")
print(f"📤 [DEBUG] NODO: Image Sender (Filtro Dinámico)")
print(f" -> Frames recibidos de la IA: {batch_size}")
_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:
print(f" -> 🏁 LOTE FINAL ABSOLUTO DETECTADO. Desactivando recorte de rostros para forzar la salida de TODOS los frames.")
_log(f" -> 🏁 LOTE FINAL ABSOLUTO DETECTADO. Desactivando recorte de rostros para forzar la salida de TODOS los frames.")
detect_faces = False
if detect_faces:
@@ -125,7 +133,7 @@ class SessionImageSender:
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):
@@ -136,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. Aceptando todo el lote para evitar bucles infinitos.")
_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
@@ -172,7 +180,7 @@ class SessionImageSender:
# 🚀 FIX: Compensar los frames destruidos por el VAE en el ciclo final
if is_final_chunk:
print(f" -> 🏁 LOTE FINAL ABSOLUTO DETECTADO. Compensando mermas del VAE (Timeline forzado al 100%).")
_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
@@ -180,8 +188,8 @@ class SessionImageSender:
# 🚀 NUEVO: Detectar si es el ciclo final o un ciclo único
is_final_cycle = loop.global_accumulated_frames >= source_total
print(f" -> ✂️ Tensor truncado a {frames_accepted} frames válidos.")
print(f" -> 📈 Timeline avanzado a {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
@@ -196,11 +204,11 @@ class SessionImageSender:
img = Image.fromarray(np.clip(img_array, 0, 255).astype(np.uint8))
img.save(filepath)
print(f" -> 💾 Keyframe seguro guardado: {filename}")
_log(f" -> 💾 Keyframe seguro guardado: {filename}")
else:
print(f" -> 🚀 Ciclo final/único detectado. Omitiendo guardado de keyframe.")
_log(f" -> 🚀 Ciclo final/único detectado. Omitiendo guardado de keyframe.")
print(f"{'='*50}\n")
_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))}
+8 -4
View File
@@ -17,8 +17,8 @@ class MasterSwitch:
}
}
RETURN_TYPES = (any_type,)
RETURN_NAMES = ("output",)
RETURN_TYPES = (any_type, "STRING")
RETURN_NAMES = ("output", "log")
FUNCTION = "route"
CATEGORY = "🔁 Sequential Batcher/Logic"
@@ -30,7 +30,11 @@ class MasterSwitch:
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,)
return (on_true, "\n".join(log_output))
else:
return (on_false,)
return (on_false, "\n".join(log_output))
+66 -46
View File
@@ -14,12 +14,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
@@ -29,9 +33,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:
@@ -45,8 +49,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
@@ -59,10 +63,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
@@ -102,31 +106,35 @@ 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:
@@ -139,17 +147,21 @@ class ConditionalAudioRouter:
}
}
RETURN_TYPES = ("AUDIO",)
RETURN_NAMES = ("routed_audio",)
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:
print(" -> 🔊 [Audio Router] Ciclo Final: Enviando pista de audio completa al Lip Sync.")
return (audio,)
_log(" -> 🔊 [Audio Router] Ciclo Final: Enviando pista de audio completa al Lip Sync.")
return (audio, "\n".join(log_output))
else:
print(" -> 🔇 [Audio Router] Ciclo Intermedio: Generando micro-silencio para bypasear el Lip Sync...")
_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
@@ -161,7 +173,7 @@ class ConditionalAudioRouter:
dummy_waveform = torch.zeros((1, 1, samples), dtype=torch.float32)
dummy_audio = {"waveform": dummy_waveform, "sample_rate": sample_rate}
return (dummy_audio,)
return (dummy_audio, "\n".join(log_output))
@register_node
class VAESafeFramePadder:
@@ -174,12 +186,16 @@ class VAESafeFramePadder:
}
}
RETURN_TYPES = ("IMAGE", "INT")
RETURN_NAMES = ("PADDED_IMAGES", "frames_added")
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
@@ -197,22 +213,22 @@ class VAESafeFramePadder:
diff = target_frames - n_frames
print(f"\n{'='*50}")
print(f"🛡️ [DEBUG] NODO: VAE Safe Frame Padder")
print(f" -> Frames recibidos: {n_frames}")
print(f" -> Regla aplicada: {rule}")
_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:
print(f" -> 🧱 Tensor incompleto. Clonando el último frame {diff} vez/veces...")
_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
print(f" -> ✅ Tensor final acolchado a: {target_frames} frames.")
_log(f" -> ✅ Tensor final acolchado a: {target_frames} frames.")
else:
print(f" -> ✅ Tensor perfecto. No requiere acolchado.")
_log(f" -> ✅ Tensor perfecto. No requiere acolchado.")
print(f"{'='*50}\n")
return (images, diff)
_log(f"{'='*50}\n")
return (images, diff, "\n".join(log_output))
@register_node
class VHS_Path_Selector:
@@ -226,20 +242,24 @@ class VHS_Path_Selector:
}
# 🔄 Ahora devuelve la cadena y el objeto VHS reordenado
RETURN_TYPES = ("STRING", "VHS_FILENAMES")
RETURN_NAMES = ("file_path", "vhs_filenames")
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:
print("⚠️ [Path Selector] Entrada vhs_filenames vacía o inválida.")
return ("", vhs_filenames)
_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)
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)
@@ -263,9 +283,9 @@ class VHS_Path_Selector:
# Creamos el nuevo paquete VHS [bool, [lista_reordenada]]
reordered_vhs = [vhs_filenames[0], new_paths]
print(f"📂 [Path Selector] Selección: {select_file} -> {result}")
return (str(result), reordered_vhs)
_log(f"📂 [Path Selector] Selección: {select_file} -> {result}")
return (str(result), reordered_vhs, "\n".join(log_output))
except Exception as e:
print(f"❌ [Path Selector] Error crítico: {e}")
return ("", vhs_filenames)
_log(f"❌ [Path Selector] Error crítico: {e}")
return ("", vhs_filenames, "\n".join(log_output))
+85 -69
View File
@@ -53,8 +53,8 @@ class VideoAnalyzerWithAudio:
}
}
RETURN_TYPES = ("*", "INT", "FLOAT", "AUDIO", "FACE_CUTS", "IMAGE")
RETURN_NAMES = ("video_path", "total_frames", "source_fps", "source_audio", "safe_faces_list", "reference_frame")
RETURN_TYPES = ("*", "INT", "FLOAT", "AUDIO", "FACE_CUTS", "IMAGE", "STRING")
RETURN_NAMES = ("video_path", "total_frames", "source_fps", "source_audio", "safe_faces_list", "reference_frame", "log")
OUTPUT_NODE = True
FUNCTION = "analyze"
CATEGORY = "🔁 Sequential Batcher/Video"
@@ -73,24 +73,28 @@ class VideoAnalyzerWithAudio:
return True
def analyze(self, video, reference_frame_idx, use_face_detector, blur_threshold, unload_detector_after_analysis=True, bbox_detector=None, **kwargs):
log_output = []
def _log(msg):
print(msg)
log_output.append(str(msg))
# Resolución unificada de la ruta, tal como hace VHS
if os.path.exists(video):
video_path = video
else:
video_path = folder_paths.get_annotated_filepath(video)
print(f"\n{'='*50}")
print(f"🕵️ [DEBUG] NODO: Video Analyzer (Explorador)")
print(f" -> Archivo resuelto: {video_path}")
_log(f"\n{'='*50}")
_log(f"🕵️ [Secuencial Batcher] NODO: Video Analyzer (Explorador)")
_log(f" -> Archivo resuelto: {video_path}")
# 1. Extracción de Audio Íntegro
source_audio = None
try:
waveform, sample_rate = torchaudio.load(video_path)
source_audio = {"waveform": waveform.unsqueeze(0), "sample_rate": sample_rate}
print(f" -> 🎵 Audio extraído correctamente ({sample_rate}Hz)")
_log(f" -> 🎵 Audio extraído correctamente ({sample_rate}Hz)")
except Exception as e:
print(f" -> ⚠️ Sin audio o error al extraer: {e}")
_log(f" -> ⚠️ Sin audio o error al extraer: {e}")
# 2. Escaneo de OpenCV (Frames, Rostros y Referencia)
frame_count = 0
@@ -100,13 +104,13 @@ class VideoAnalyzerWithAudio:
ui_result = {}
if not HAS_OPENCV:
print(" -> ❌ ERROR: OpenCV no está instalado. Ejecuta: pip install opencv-python")
_log(" -> ❌ ERROR: OpenCV no está instalado. Ejecuta: pip install opencv-python")
else:
cap = cv2.VideoCapture(video_path)
frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
source_fps = float(cap.get(cv2.CAP_PROP_FPS))
print(f" -> 🎞️ Total Frames detectados: {frame_count}")
print(f" -> ⏱️ FPS detectados: {source_fps}")
_log(f" -> 🎞️ Total Frames detectados: {frame_count}")
_log(f" -> ⏱️ FPS detectados: {source_fps}")
# Extraer el Frame de Referencia específico
safe_ref_idx = min(reference_frame_idx, max(0, frame_count - 1))
@@ -119,7 +123,7 @@ class VideoAnalyzerWithAudio:
# Convertir a Tensor (1, H, W, 3)
ref_tensor = torch.from_numpy(ref_frame_rgb).float() / 255.0
ref_tensor = ref_tensor.unsqueeze(0)
print(f" -> 🖼️ Frame de referencia extraído (Índice: {safe_ref_idx})")
_log(f" -> 🖼️ Frame de referencia extraído (Índice: {safe_ref_idx})")
# Generar imagen temporal para el Preview de la UI
import random
@@ -131,11 +135,11 @@ class VideoAnalyzerWithAudio:
# Escaneo de Rostros
if bbox_detector is not None or use_face_detector:
print(f" -> 🤖 Iniciando escaneo de rostros (Umbral: {blur_threshold})...")
_log(f" -> 🤖 Iniciando escaneo de rostros (Umbral: {blur_threshold})...")
if bbox_detector is not None:
print(f" -> ⚡ Usando detector de rostros por GPU (YOLO/ONNX).")
_log(f" -> ⚡ Usando detector de rostros por GPU (YOLO/ONNX).")
else:
print(f" -> 🐢 Usando detector de rostros por CPU (OpenCV).")
_log(f" -> 🐢 Usando detector de rostros por CPU (OpenCV).")
if HAS_OPENCV:
cascade_path = cv2.data.haarcascades + 'haarcascade_frontalface_default.xml'
@@ -143,7 +147,7 @@ class VideoAnalyzerWithAudio:
cap.set(cv2.CAP_PROP_POS_FRAMES, 0) # Volver al inicio
print(f" -> 🤖 Iniciando escaneo de rostros por GPU...")
_log(f" -> 🤖 Iniciando escaneo de rostros por GPU...")
# Silenciamos el logger de ultralytics antes de empezar
logging.getLogger("ultralytics").setLevel(logging.ERROR)
@@ -198,11 +202,11 @@ class VideoAnalyzerWithAudio:
comfy_pbar.update(1)
pbar.close() # Cerramos la barra al terminar
print(f" -> ✅ Encontrados {len(safe_faces)} frames nítidos con rostros.")
_log(f" -> ✅ Encontrados {len(safe_faces)} frames nítidos con rostros.")
# Descarga de VRAM
if unload_detector_after_analysis:
print(f" -> 🧹 Descargando detector de rostros de la VRAM...")
_log(f" -> 🧹 Descargando detector de rostros de la VRAM...")
import gc
import comfy.model_management as mm
mm.unload_all_models()
@@ -210,13 +214,13 @@ class VideoAnalyzerWithAudio:
if torch.cuda.is_available():
torch.cuda.empty_cache()
else:
print(f" -> 🤖 Escaneo de rostros DESACTIVADO.")
_log(f" -> 🤖 Escaneo de rostros DESACTIVADO.")
cap.release()
# Auto-limpieza del detector para liberar VRAM inmediatamente
if kwargs.get("unload_detector", True):
print(" -> 🧹 Descargando modelo del detector de rostros de la VRAM...")
_log(" -> 🧹 Descargando modelo del detector de rostros de la VRAM...")
if bbox_detector is not None:
del bbox_detector
if HAS_OPENCV and 'face_cascade' in locals():
@@ -226,10 +230,10 @@ class VideoAnalyzerWithAudio:
if torch.cuda.is_available():
torch.cuda.empty_cache()
print(f"{'='*50}\n")
_log(f"{'='*50}\n")
# 💡 EL BYPASS A VHS: Pasamos video_path (Ruta Absoluta) en lugar de video_name
return {"ui": ui_result, "result": (video_path, frame_count, source_fps, source_audio, safe_faces, ref_tensor)}
return {"ui": ui_result, "result": (video_path, frame_count, source_fps, source_audio, safe_faces, ref_tensor, "\n".join(log_output))}
@register_node
class AutoLoopCalculator:
@@ -247,12 +251,16 @@ class AutoLoopCalculator:
}
}
RETURN_TYPES = ("INT", "INT", "INT")
RETURN_NAMES = ("chunk_frames", "skip_frames", "select_every_nth")
RETURN_TYPES = ("INT", "INT", "INT", "STRING")
RETURN_NAMES = ("chunk_frames", "skip_frames", "select_every_nth", "log")
FUNCTION = "calculate"
CATEGORY = "🔁 Sequential Batcher/Video"
def calculate(self, source_frame_count, target_frames_per_loop, select_every_nth, current_loop_index, safe_faces_list=None):
log_output = []
def _log(msg):
print(msg)
log_output.append(str(msg))
# --- Ajuste Proporcional Base (Sin restricciones VAE) ---
import math
from . import loop
@@ -267,28 +275,28 @@ class AutoLoopCalculator:
estimated_loops = math.ceil(safe_effective_frames / target_frames_per_loop)
if estimated_loops > 0:
target_frames_per_loop = math.ceil(safe_effective_frames / estimated_loops)
print(f" -> ⚖️ Ajuste Proporcional: Target recalculado a {target_frames_per_loop} frames por ciclo.")
_log(f" -> ⚖️ Ajuste Proporcional: Target recalculado a {target_frames_per_loop} frames por ciclo.")
# --- LOGS MEJORADOS Y TRANSPARENTES ---
print(f" -> 🎞️ Capacidad del video: {potential_effective_frames} frames procesables (Nth: {select_every_nth})")
_log(f" -> 🎞️ Capacidad del video: {potential_effective_frames} frames procesables (Nth: {select_every_nth})")
if effective_loss > 0:
print(f" -> 🛡️ Ajuste VAE: Se usarán {safe_effective_frames} frames (Descarte técnico: {effective_loss} frame/s de proceso)")
_log(f" -> 🛡️ Ajuste VAE: Se usarán {safe_effective_frames} frames (Descarte técnico: {effective_loss} frame/s de proceso)")
else:
print(f" -> ✅ Ajuste VAE: Perfecto. Múltiplo de 4 detectado.") # Manteniendo el mismo string por ahora o podemos omitirlo, lo adapto a que es genérico
_log(f" -> ✅ Ajuste VAE: Perfecto. Múltiplo de 4 detectado.") # Manteniendo el mismo string por ahora o podemos omitirlo, lo adapto a que es genérico
print(f" -> 📊 Timeline final: 0 a {safe_source_frame_count} (de {source_frame_count} totales)")
_log(f" -> 📊 Timeline final: 0 a {safe_source_frame_count} (de {source_frame_count} totales)")
# -------------------------------------------------------------
loop.global_select_every_nth = select_every_nth
current_pos = loop.global_accumulated_frames
print(f"\n{'='*50}")
print(f"📊 [DEBUG] NODO: Auto Loop Calculator (Motor Dinámico)")
print(f" -> Posición en el timeline original: {current_pos} / {source_frame_count}")
_log(f"\n{'='*50}")
_log(f"📊 [Secuencial Batcher] NODO: Auto Loop Calculator (Motor Dinámico)")
_log(f" -> Posición en el timeline original: {current_pos} / {source_frame_count}")
if current_pos >= safe_source_frame_count:
return (1, current_pos, select_every_nth)
return (1, current_pos, select_every_nth, "\n".join(log_output))
frames_left = safe_source_frame_count - current_pos
@@ -298,16 +306,16 @@ class AutoLoopCalculator:
if frames_left <= equitable_target:
best_cut = safe_source_frame_count
print(f" -> 🧮 Absorbiendo resto final: meta fijada en frame {best_cut}")
_log(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:
# Find the safe face closest to the equitable target
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})")
_log(f" -> ✂️ Corte Inteligente proyectado: {best_cut} (Meta equitativa: {ideal_cut})")
else:
print(f" -> ⚖️ Sin caras detectadas. Forzando corte equitativo: {ideal_cut}")
_log(f" -> ⚖️ Sin caras detectadas. Forzando corte equitativo: {ideal_cut}")
effective_chunk_frames = math.ceil((best_cut - current_pos) / select_every_nth)
if current_pos + (effective_chunk_frames * select_every_nth) > safe_source_frame_count:
@@ -315,10 +323,10 @@ class AutoLoopCalculator:
skip_frames = current_pos
print(f" -> 🚀 Ciclo {current_loop_index}: Solicitando {effective_chunk_frames} frames efectivos (Saltando {skip_frames})")
print(f"{'='*50}\n")
_log(f" -> 🚀 Ciclo {current_loop_index}: Solicitando {effective_chunk_frames} frames efectivos (Saltando {skip_frames})")
_log(f"{'='*50}\n")
return (effective_chunk_frames, skip_frames, select_every_nth)
return (effective_chunk_frames, skip_frames, select_every_nth, "\n".join(log_output))
@register_node
class IncrementalVideoStitcher:
@@ -332,8 +340,8 @@ class IncrementalVideoStitcher:
}
}
RETURN_TYPES = ("IMAGE", "AUDIO", "BOOLEAN")
RETURN_NAMES = ("ALL_IMAGES", "AUDIO_OUT", "IS_FINAL_CYCLE")
RETURN_TYPES = ("IMAGE", "AUDIO", "BOOLEAN", "STRING")
RETURN_NAMES = ("ALL_IMAGES", "AUDIO_OUT", "IS_FINAL_CYCLE", "log")
FUNCTION = "stitch"
CATEGORY = "🔁 Sequential Batcher/Video"
@@ -344,6 +352,10 @@ class IncrementalVideoStitcher:
return time.time()
def stitch(self, images, audio, current_loop_index):
log_output = []
def _log(msg):
print(msg)
log_output.append(str(msg))
from . import loop
import os, folder_paths, torch, shutil, time
from PIL import Image
@@ -356,14 +368,14 @@ class IncrementalVideoStitcher:
try: shutil.rmtree(cache_dir)
except Exception as e: pass
os.makedirs(cache_dir, exist_ok=True)
print(f"\n🧹 [Stitcher] Ciclo 0 detectado. Subcarpeta temporal limpiada.")
_log(f"\n🧹 [Stitcher] Ciclo 0 detectado. Subcarpeta temporal limpiada.")
else:
os.makedirs(cache_dir, exist_ok=True)
timestamp = int(time.time() * 1000)
# 🚀 FIX OOM RAM: Guardar frames individualmente como PNG (Ultra Ligero)
print(f"📦 [Stitcher] Guardando {images.shape[0]} frames como PNGs de alta calidad...")
_log(f"📦 [Stitcher] Guardando {images.shape[0]} frames como PNGs de alta calidad...")
for i in range(images.shape[0]):
filename = f"frame_{current_loop_index:04d}_{timestamp}_{i:04d}.png"
path = os.path.join(cache_dir, filename)
@@ -375,20 +387,20 @@ class IncrementalVideoStitcher:
is_final_chunk = getattr(loop, 'global_is_final_chunk', False)
if is_final_chunk or loop.global_accumulated_frames >= source_total:
print(f" -> 🏁 ¡Último ciclo detectado! Ensamblando PNGs desde la caché temporal...")
_log(f" -> 🏁 ¡Último ciclo detectado! Ensamblando PNGs desde la caché temporal...")
png_files = sorted([f for f in os.listdir(cache_dir) if f.endswith('.png')])
if not png_files:
print(" -> ❌ ERROR: No se encontraron frames en la subcarpeta.")
return (images, audio, True)
_log(" -> ❌ ERROR: No se encontraron frames en la subcarpeta.")
return (images, audio, True, "\n".join(log_output))
# 🚀 OPTIMIZACIÓN EXTREMA DE RAM: Pre-asignamos el tensor en lugar de usar torch.cat
first_img = Image.open(os.path.join(cache_dir, png_files[0]))
H, W = first_img.height, first_img.width
total_frames = len(png_files)
print(f" -> 🧩 Reservando bloque continuo en RAM para {total_frames} frames...")
_log(f" -> 🧩 Reservando bloque continuo en RAM para {total_frames} frames...")
final_tensor = torch.empty((total_frames, H, W, 3), dtype=torch.float32, device="cpu")
for i, f in enumerate(png_files):
@@ -396,19 +408,19 @@ class IncrementalVideoStitcher:
img_np = np.array(img).astype(np.float32) / 255.0
final_tensor[i] = torch.from_numpy(img_np)
print(f"✅ [Stitcher] VÍDEO COMPLETADO: {final_tensor.shape[0]} frames ensamblados con éxito.")
_log(f"✅ [Stitcher] VÍDEO COMPLETADO: {final_tensor.shape[0]} frames ensamblados con éxito.")
try:
shutil.rmtree(cache_dir)
print(f"🧹 [Stitcher] Subcarpeta temporal destruida.")
_log(f"🧹 [Stitcher] Subcarpeta temporal destruida.")
except:
pass
return (final_tensor, audio, True)
return (final_tensor, audio, True, "\n".join(log_output))
else:
print(f" -> ⏳ Ciclo intermedio. Frames PNG almacenados de forma segura. Pasando 1 frame dummy...")
_log(f" -> ⏳ Ciclo intermedio. Frames PNG almacenados de forma segura. Pasando 1 frame dummy...")
dummy_frame = images[-1:].clone()
return (dummy_frame, None, False)
return (dummy_frame, None, False, "\n".join(log_output))
@register_node
class AutoLoopCalculatorWan:
@@ -426,12 +438,16 @@ class AutoLoopCalculatorWan:
}
}
RETURN_TYPES = ("INT", "INT", "INT")
RETURN_NAMES = ("chunk_frames", "skip_frames", "select_every_nth")
RETURN_TYPES = ("INT", "INT", "INT", "STRING")
RETURN_NAMES = ("chunk_frames", "skip_frames", "select_every_nth", "log")
FUNCTION = "calculate"
CATEGORY = "🔁 Sequential Batcher/Video"
def calculate(self, source_frame_count, target_frames_per_loop, select_every_nth, current_loop_index, safe_faces_list=None):
log_output = []
def _log(msg):
print(msg)
log_output.append(str(msg))
import math
from . import loop
@@ -456,26 +472,26 @@ class AutoLoopCalculatorWan:
if estimated_loops > 0:
optimal_target = math.ceil(safe_effective_frames / estimated_loops)
adjusted_target = ((optimal_target + 2) // 4) * 4 + 1
print(f" -> ⚖️ Ajuste Proporcional: Target recalculado de {target_frames_per_loop} a {adjusted_target} frames por ciclo (para {estimated_loops} ciclos)")
_log(f" -> ⚖️ Ajuste Proporcional: Target recalculado de {target_frames_per_loop} a {adjusted_target} frames por ciclo (para {estimated_loops} ciclos)")
target_frames_per_loop = adjusted_target
print(f" -> 🎞️ Capacidad del video original: {potential_effective_frames} frames (Nth: {select_every_nth})")
_log(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)")
_log(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 4n+1 detectada.")
_log(f" -> ✅ Ajuste VAE: Perfecto. Regla 4n+1 detectada.")
print(f" -> 📊 Timeline final: 0 a {physical_source_frame_count} (Límite Físico Real)")
_log(f" -> 📊 Timeline final: 0 a {physical_source_frame_count} (Límite Físico Real)")
current_pos = getattr(loop, 'global_accumulated_frames', 0)
print(f"\n{'='*50}")
print(f"📊 [DEBUG] NODO: Auto Loop Calculator (WanVideo 3D VAE)")
print(f" -> Timeline físico ajustado: {current_pos} / {physical_source_frame_count} (Original: {source_frame_count})")
_log(f"\n{'='*50}")
_log(f"📊 [Secuencial Batcher] NODO: Auto Loop Calculator (WanVideo 3D VAE)")
_log(f" -> Timeline físico ajustado: {current_pos} / {physical_source_frame_count} (Original: {source_frame_count})")
# Prevención de desbordamiento de bucle (Usando límite físico)
if current_pos >= physical_source_frame_count:
return (4, current_pos, select_every_nth)
return (4, current_pos, select_every_nth, "\n".join(log_output))
frames_left = physical_source_frame_count - current_pos
@@ -484,7 +500,7 @@ class AutoLoopCalculatorWan:
if frames_left <= equitable_target:
best_cut = physical_source_frame_count
print(f" -> 🧮 Absorbiendo resto final seguro: meta fijada en frame {best_cut}")
_log(f" -> 🧮 Absorbiendo resto final seguro: meta fijada en frame {best_cut}")
else:
best_cut = ideal_cut
if safe_faces_list and len(safe_faces_list) > 0:
@@ -494,9 +510,9 @@ class AutoLoopCalculatorWan:
if safe_chunk_from_face < 4: safe_chunk_from_face = 4
best_cut = current_pos + (safe_chunk_from_face * select_every_nth)
print(f" -> ✂️ Corte Inteligente (Ajustado x4): {best_cut} (Cara original detectada: {closest_face})")
_log(f" -> ✂️ Corte Inteligente (Ajustado x4): {best_cut} (Cara original detectada: {closest_face})")
else:
print(f" -> ⚖️ Sin caras. Forzando corte equitativo x4: {ideal_cut}")
_log(f" -> ⚖️ Sin caras. Forzando corte equitativo x4: {ideal_cut}")
# 4. Cálculo final del chunk consolidado a regla 4n+1
effective_chunk_frames = math.ceil((best_cut - current_pos) / select_every_nth)
@@ -508,14 +524,14 @@ class AutoLoopCalculatorWan:
# Comprobación contra el límite físico para evitar pedir más allá del final
if current_pos + (effective_chunk_frames * select_every_nth) >= physical_source_frame_count:
print(f" -> 🏁 Chunk final detectado. Ajustando a {effective_chunk_frames} frames para mantener regla 4n+1.")
_log(f" -> 🏁 Chunk final detectado. Ajustando a {effective_chunk_frames} frames para mantener regla 4n+1.")
loop.global_is_final_chunk = True
else:
loop.global_is_final_chunk = False
skip_frames = current_pos
print(f" -> 🚀 Ciclo {current_loop_index}: Solicitando {effective_chunk_frames} frames efectivos a VHS (Saltando {skip_frames})")
print(f"{'='*50}\n")
_log(f" -> 🚀 Ciclo {current_loop_index}: Solicitando {effective_chunk_frames} frames efectivos a VHS (Saltando {skip_frames})")
_log(f"{'='*50}\n")
return (effective_chunk_frames, skip_frames, select_every_nth)
return (effective_chunk_frames, skip_frames, select_every_nth, "\n".join(log_output))
+22 -18
View File
@@ -24,14 +24,18 @@ class MeisoftVRAMDefragmenter:
}
}
RETURN_TYPES = (any_type,)
RETURN_NAMES = ("anything",)
RETURN_TYPES = (any_type, "STRING")
RETURN_NAMES = ("anything", "log")
FUNCTION = "defragment"
CATEGORY = "🔁 Sequential Batcher/Tools"
def defragment(self, anything, force_model_unload):
print(f"\n{'='*65}")
print(f"🧹 [DEBUG] NODO: VRAM Defragmenter (Modo Arquitectura Profunda)")
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
@@ -39,30 +43,30 @@ class MeisoftVRAMDefragmenter:
if torch.cuda.is_available():
alloc_before = torch.cuda.memory_allocated()
res_before = torch.cuda.memory_reserved()
print(f" -> 📊 Estado Inicial | Asignada: {format_bytes(alloc_before)} | Reservada (Caché): {format_bytes(res_before)}")
_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")
print(" -> ⚙️ PyTorch Allocator ajustado a expandable_segments:True")
_log(" -> ⚙️ PyTorch Allocator ajustado a expandable_segments:True")
except Exception as e:
print(f" -> ⚠️ Aviso: No se pudo ajustar expandable_segments: {e}")
_log(f" -> ⚠️ Aviso: No se pudo ajustar expandable_segments: {e}")
# --- FASE 2: EVACUACIÓN (Opcional) ---
if force_model_unload:
print(" -> 📦 Trasladando modelos inactivos a la RAM (unload_all_models)...")
_log(" -> 📦 Trasladando modelos inactivos a la RAM (unload_all_models)...")
mm.unload_all_models()
# --- FASE 3: LIMPIEZA INTERNA DE COMFYUI (LA CLAVE) ---
print(" -> 🧹 Destruyendo referencias ocultas en el gestor de ComfyUI...")
_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 ---
print(" -> 🗑️ Forzando Garbage Collector de Python (Doble pasada)...")
_log(" -> 🗑️ Forzando Garbage Collector de Python (Doble pasada)...")
gc.collect()
gc.collect() # Doble pasada para romper referencias circulares rebeldes
@@ -71,7 +75,7 @@ class MeisoftVRAMDefragmenter:
# --- FASE 6: PURGADO PROFUNDO DE PYTORCH ---
if torch.cuda.is_available():
print(" -> 🧱 Desfragmentando Caching Allocator de CUDA...")
_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
@@ -85,11 +89,11 @@ class MeisoftVRAMDefragmenter:
kernel32 = ctypes.windll.kernel32
current_process = kernel32.GetCurrentProcess()
kernel32.SetProcessWorkingSetSize(current_process, -1, -1)
print(" -> 💻 Working Set reducido (RAM del sistema purgada en Windows).")
_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)
print(" -> 💻 Malloc Trim ejecutado (RAM del sistema purgada en Linux/WSL).")
_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
@@ -106,10 +110,10 @@ class MeisoftVRAMDefragmenter:
sign_alloc = "+" if freed_alloc < 0 else "-"
sign_res = "+" if freed_res < 0 else "-"
print(f" -> 📈 Estado Final | Asignada: {format_bytes(alloc_after)} | Reservada (Caché): {format_bytes(res_after)}")
print(f" -> ✨ TOTAL LIBERADO | {sign_alloc}{abs(freed_alloc)/(1024**3):.2f} GB (Asignada) | {sign_res}{abs(freed_res)/(1024**3):.2f} GB (Reservada purgada)")
_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:
print(" -> ✨ Limpieza completada (Modo CPU/Otra arquitectura).")
_log(" -> ✨ Limpieza completada (Modo CPU/Otra arquitectura).")
print(f"{'='*65}\n")
return (anything,)
_log(f"{'='*65}\n")
return (anything, "\n".join(log_output))