diff --git a/__pycache__/image.cpython-312.pyc b/__pycache__/image.cpython-312.pyc index daff61b..78c4211 100644 Binary files a/__pycache__/image.cpython-312.pyc and b/__pycache__/image.cpython-312.pyc differ diff --git a/__pycache__/tools.cpython-312.pyc b/__pycache__/tools.cpython-312.pyc index e3b4b92..1bf099b 100644 Binary files a/__pycache__/tools.cpython-312.pyc and b/__pycache__/tools.cpython-312.pyc differ diff --git a/__pycache__/video.cpython-312.pyc b/__pycache__/video.cpython-312.pyc index 2ae1c11..4be497d 100644 Binary files a/__pycache__/video.cpython-312.pyc and b/__pycache__/video.cpython-312.pyc differ diff --git a/image.py b/image.py index 86e359e..d4ffb98 100644 --- a/image.py +++ b/image.py @@ -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))} diff --git a/switch_node.py b/switch_node.py index b4c248e..dd6f304 100644 --- a/switch_node.py +++ b/switch_node.py @@ -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)) diff --git a/tools.py b/tools.py index 10ed381..67567d6 100644 --- a/tools.py +++ b/tools.py @@ -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)) diff --git a/video.py b/video.py index ba65c09..e802eda 100644 --- a/video.py +++ b/video.py @@ -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)) diff --git a/vram_node.py b/vram_node.py index b92a13e..0df7227 100644 --- a/vram_node.py +++ b/vram_node.py @@ -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))