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:
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
@@ -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))
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user