From ce6041da4abec23dcf69eabe1593e9b7308e1f58 Mon Sep 17 00:00:00 2001 From: Meisoftcoltd <38667978+Meisoftcoltd@users.noreply.github.com> Date: Sun, 19 Apr 2026 13:33:52 +0000 Subject: [PATCH] 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. --- __pycache__/image.cpython-312.pyc | Bin 9350 -> 10119 bytes __pycache__/tools.cpython-312.pyc | Bin 11712 -> 13278 bytes __pycache__/video.cpython-312.pyc | Bin 24073 -> 25202 bytes image.py | 64 +++++++------ switch_node.py | 12 ++- tools.py | 112 +++++++++++++--------- video.py | 154 +++++++++++++++++------------- vram_node.py | 40 ++++---- 8 files changed, 217 insertions(+), 165 deletions(-) diff --git a/__pycache__/image.cpython-312.pyc b/__pycache__/image.cpython-312.pyc index daff61b0392e5a823f32975b91b34b5f56d75af2..78c42115198cc721da494fcfb341d89127bee45d 100644 GIT binary patch delta 4050 zcmbtXYiv_T7M^?ky0-6)o!E{YzZ2(S69{y-E$={@5E2$rUZn*J#(12E;5W9CP3Qd#jnSi&A__mJ{h*bZjCvk|=6YnGw8E|6TAA2f(}LkH84{xjAnh<{D;s-EhN(Gn$nwMsL|*lzp4>_5RI%0v~c8f!wMZf%0e$ zT;!4~yI$=-M~0VL#kMl$ERbI!+H0DkZc-jg=ZgB+8zq@Dy%uviJN|{)w5e-bDie>TvfW)&v6h&8<0bI@EO;c= z&xPLGvm`byioONW_euOp=+fu|u|tj!18g?Vdb>zx`8W7?!Q+P#@u@^QJ{C&_Uy9}8 zqY3%e$HqpEJAywm1@NM2onLkl06#RhX1VxMi2gz--GAP;mwZ)F)z=x&$;p^3WDdM zAgY$_1z`v4gsu!+*`luNneUwU-HodUPpSu^YIInYMhe0xZWfz&JQS^}ZFfN&TynNv z9C%FM4i?12kDQ(P0peM;bY30!!_c)MwR5W~Z7T@diQhSW{>+@rro(TGF8loRcQ${% z`R=j%y|1a8jxJb^slqY*TM_98=z4eHkhERjF}ruk)`^FuHcdcx{MP%@L5|ypAJ_GE zZY4(K&|8o6>>cduIlQYcI=HWA_pU>7H&Gle8ncO&LLl$Ndn^N9nh$BS!meVu*yf`QbIFsmKme0ZgHXjf~8jss8&L5f%O?5R-=lTzMFBm!l>3P9Mol?!8&$RO+BxX=H2jASYRsw|Z3^xvR7QHD)Dd z;VXL}xwV=sX*<&ekTg@uAL1lf?WAIgI0#T3u_7_Kf#NOq6<F$AsSi-BVHH0s$3x;>w;F#3 z>_Q!kRd}J8q_w)Hku16xF1seQVm+l5A&~n?*{T)Djf#hb;76X?(2fEK6wi6j9QWSe zOTD+ax4;?AQScTQ+3+K3@+@XDO6RH2jsJ_9$a{$|nKGYh>cMXM={0!L?+w+mjyN?x zOo3ibGWoVZeg&tUFY8~W-PiCJ&h_m_Y4;d`DM|$h>-hAUti$Yr9CzW?WYZ|uRCJ7_ zGN)sy=vX#7JeDS%_~=x6eCiZ_)wS;V-pmA{P1#r)W`Yw5c_Nk#!bB=~q-R$!e0Xdk zks3=Ug2NekIz|i#uX%36hBo|_JH{Qyn>?2r*o}IUgc50&$)Tbg<{zFHtTEa>O<|Zq z96#s1U}ejAp1vn2Kzzr$DH@SS=*u|@j0qvyE72j^d!K@rLL-F+3csX4tE^1FSJ8Bu za;<1~Y&uaCPA9U26u>iblIq9tQQr+MT`vB?x20}^${4+3v#sK8Y&3;8L8TE(|uD^B|@^g@qs|tODNPb0QLd?TL7dz{E{b_ z-}~6?T;?04e*9se-SgOLSDmNS*tnX`EW|SEsY!JbsSc%JJ&(T%%)Ro^(B)gU0r$oyjlku5Xm=6+-dCu9$E*BOAab?m*XQR3zJDSBsa~~>tEo5D zv(pQyX?6UZdQMU6&lkiQ{O928%-2r;uX1YJ&iUs1&c3|(s3~~Gu6A_K>+U!8=j)#6 zf#_xgFk29(mey^~_dO8T{7tkjifg_U*Qj0n3t~T!EbD>Wzu2^Kp=smoZ3|7i^L30F zc3quOyLPBM_N&r?f^d-SP1n`j;`)6H>-VW$LwDoi3geVc6@(c6wE4yiBbb|RyKbA* z`rZd(AA3Ju5Kj<~RkZSJTgO$?&4K&2?RjCz8MqR@;#M2i-uBMt?wtMnta{-1{oSvt zn@%iPPO8Gm@4r3A0lV*CfRp;Uhjg!Z&ezXtR7c;Ub=Q~HT|X>qsOCS}0KLDyaQ2#p zn!#UMS_s~2Hogor_tuz)wrcKe)sg;CM3mDsfie^(DZD{p3k8`%mO_rg6ooe_Frs^& z_BK*rN1OhF$Y;tx3uSfy_k}zqD%?e>aK{vLH9m}N2zwb!IQaTQO z__L60FFOx&)E_$(?2NF(sHZYWAxxo@0-LusmZGq2A_J#ViEjCi*xI^n%TKxE+%ll} fjRx?RWu6I=1hQv5j+g5!9KB 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 delta 4675 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 VU{x_BTe!w$0>C66@=#(Q_)oYD|GNMH diff --git a/__pycache__/video.cpython-312.pyc b/__pycache__/video.cpython-312.pyc index 2ae1c11be044fc10ccd9b651f20f4a917bc14df7..4be497da91cda63833190f157b52fece61e45935 100644 GIT binary patch delta 10490 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

)ABjW{DMmRTAB)I=~3tsfROpxZwXMesjrm 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 delta 9698 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 Z!AhZYKUSMQTA9 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))