From d660a6b41b915561caa09062341999d9256051b7 Mon Sep 17 00:00:00 2001 From: Meisoftcoltd <38667978+Meisoftcoltd@users.noreply.github.com> Date: Sat, 25 Apr 2026 12:20:48 +0000 Subject: [PATCH] Refactor loop calculators to prioritize scene cuts over face cuts\n\n* Added SCENE_CUTS type to `VideoAnalyzerSceneDetector`.\n* Replaced `"safe_faces_list"` usage in loop calculators with `"scene_cuts_list"` as priority.\n* Removed dead code/scripts: `update_video.py`, `fix_newlines.py`, `fix_tqdm.py`, `add_scenedetector.py`. --- add_scenedetector.py | 149 ------------------------------------------- fix_newlines.py | 12 ---- fix_tqdm.py | 12 ---- loop.py | 32 ++++++++-- update_video.py | 27 -------- video.py | 62 ++++++++++++++---- 6 files changed, 76 insertions(+), 218 deletions(-) delete mode 100644 add_scenedetector.py delete mode 100644 fix_newlines.py delete mode 100644 fix_tqdm.py delete mode 100644 update_video.py diff --git a/add_scenedetector.py b/add_scenedetector.py deleted file mode 100644 index 1af81f8..0000000 --- a/add_scenedetector.py +++ /dev/null @@ -1,149 +0,0 @@ -import re - -def update_node(): - with open('video.py', 'r') as f: - content = f.read() - - scene_detector_code = """ -@register_node -class VideoAnalyzerSceneDetector: - @classmethod - def INPUT_TYPES(cls): - input_dir = folder_paths.get_input_directory() - files = [] - if os.path.exists(input_dir): - files = [f for f in os.listdir(input_dir) if f.split('.')[-1].lower() in ['webm', 'mp4', 'mkv', 'gif', 'mov']] - return { - "required": { - "video": (sorted(files), {"forceInput": False, "video_upload": True}), - "reference_frame_idx": ("INT", {"default": 0, "min": 0, "max": 100000}), - "scene_threshold": ("FLOAT", {"default": 25.0, "min": 5.0, "max": 150.0}), - }, - "optional": { - "current_loop_index": ("INT", {"default": 0, "forceInput": True}), - } - } - - RETURN_TYPES = ("*", "INT", "FLOAT", "AUDIO", "FACE_CUTS", "IMAGE", "STRING") - RETURN_NAMES = ("video_path", "total_frames", "source_fps", "source_audio", "scene_cuts_list", "reference_frame", "log") - OUTPUT_NODE = True - FUNCTION = "analyze" - CATEGORY = "🔁 Sequential Batcher/Video" - - @classmethod - def IS_CHANGED(cls, video, reference_frame_idx, scene_threshold, **kwargs): - if isinstance(video, list): - video_str = "".join(video) - else: - video_str = str(video) - return f"{video_str}_{reference_frame_idx}_{scene_threshold}" - - @classmethod - def VALIDATE_INPUTS(cls, **kwargs): - return True - - def analyze(self, video, reference_frame_idx, scene_threshold, current_loop_index=0, **kwargs): - log_output = [] - def _log(msg): - print(msg) - log_output.append(str(msg)) - - video_path = video if os.path.exists(video) else folder_paths.get_annotated_filepath(video) - cache_key = f"{video_path}_scene" - - # --- LÓGICA DE CACHÉ / RECUPERACIÓN --- - if current_loop_index > 0 and cache_key in VIDEO_ANALYSIS_CACHE: - _log(f"♻️ [Scene Detector] Ciclo {current_loop_index}: Recuperando análisis del caché.") - cached = VIDEO_ANALYSIS_CACHE[cache_key] - frame_count, source_fps, source_audio, scene_cuts = cached - else: - # Análisis completo (Solo Ciclo 0 o primer arranque) - _log(f"🎬 [Scene Detector] Ciclo {current_loop_index}: Iniciando análisis profundo...") - - frame_count = 0 - source_fps = 0.0 - source_audio = None - scene_cuts = [] - - if HAS_OPENCV: - cap = cv2.VideoCapture(video_path) - frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) - source_fps = float(cap.get(cv2.CAP_PROP_FPS)) - - # Extracción de Audio - try: - waveform, sample_rate = torchaudio.load(video_path) - source_audio = {"waveform": waveform.unsqueeze(0), "sample_rate": sample_rate} - _log(f" -> 🎵 Audio extraído correctamente ({sample_rate}Hz)") - except Exception as e: - source_audio = None - _log(f" -> ⚠️ Sin audio o error al extraer: {e}") - - # Detección de cortes - _log(f" -> 🎬 Iniciando escaneo de escenas (Umbral Diferencia: {scene_threshold})...") - prev_gray = None - pbar = tqdm(total=frame_count, desc="🎬 Analizando Escenas") - comfy_pbar = comfy.utils.ProgressBar(frame_count) - - for idx in range(frame_count): - ret, frame = cap.read() - if not ret: break - small = cv2.resize(frame, (128, 128)) - gray = cv2.cvtColor(small, cv2.COLOR_BGR2GRAY) - if prev_gray is not None: - score = np.mean(cv2.absdiff(gray, prev_gray)) - if score > scene_threshold: - scene_cuts.append(idx) - # _log(f" 🎞️ Corte de escena detectado en frame [{idx}] (Puntuación: {score:.2f})") - prev_gray = gray - pbar.update(1) - comfy_pbar.update(1) - pbar.close() - cap.release() - _log(f" -> ✅ Encontrados {len(scene_cuts)} cambios de escena fuertes.") - else: - _log(" -> ❌ ERROR: OpenCV no está instalado. Ejecuta: pip install opencv-python") - - # Guardar en caché - VIDEO_ANALYSIS_CACHE[cache_key] = (frame_count, source_fps, source_audio, scene_cuts) - - # --- EXTRACCIÓN DINÁMICA DEL FRAME DE REFERENCIA (Siempre se ejecuta) --- - ui_result = {} - if HAS_OPENCV: - cap = cv2.VideoCapture(video_path) - safe_ref_idx = min(reference_frame_idx, max(0, frame_count - 1)) if frame_count > 0 else 0 - cap.set(cv2.CAP_PROP_POS_FRAMES, safe_ref_idx) - ret, frame = cap.read() - if ret: - ref_frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) - ref_tensor = torch.from_numpy(ref_frame_rgb).float() / 255.0 - ref_tensor = ref_tensor.unsqueeze(0) - - from PIL import Image - preview_name = f"preview_scene_{uuid.uuid4().hex[:5]}.png" - Image.fromarray(ref_frame_rgb).save(os.path.join(folder_paths.get_temp_directory(), preview_name)) - ui_result = {"images": [{"filename": preview_name, "subfolder": "", "type": "temp"}]} - else: - ref_tensor = torch.zeros((1, 64, 64, 3)) - cap.release() - else: - ref_tensor = torch.zeros((1, 64, 64, 3)) - - _log(f"{'='*50}\\n") - return {"ui": ui_result, "result": (video_path, frame_count, source_fps, source_audio, scene_cuts, ref_tensor, "\\n".join(log_output))} - -""" - - # Insert after VideoAnalyzerFaceDetector class - end_of_class = 'return {"ui": ui_result, "result": (video_path, frame_count, source_fps, source_audio, safe_faces, ref_tensor, "\\n".join(log_output))}' - idx = content.find(end_of_class) - if idx != -1: - insert_idx = idx + len(end_of_class) + 1 - new_content = content[:insert_idx] + "\n" + scene_detector_code + "\n" + content[insert_idx:] - with open('video.py', 'w') as f: - f.write(new_content) - else: - print("Could not find the end of VideoAnalyzerFaceDetector") - -if __name__ == '__main__': - update_node() diff --git a/fix_newlines.py b/fix_newlines.py deleted file mode 100644 index ae75bb3..0000000 --- a/fix_newlines.py +++ /dev/null @@ -1,12 +0,0 @@ -def fix(): - with open('video.py', 'r') as f: - content = f.read() - - content = content.replace('_log(f"{'+'=\'*50}\n")', '_log(f"{'+'=\'*50}\\n")') - content = content.replace('", "\n".join(log_output))}', '", "\\n".join(log_output))}') - - with open('video.py', 'w') as f: - f.write(content) - -if __name__ == '__main__': - fix() diff --git a/fix_tqdm.py b/fix_tqdm.py deleted file mode 100644 index a343abf..0000000 --- a/fix_tqdm.py +++ /dev/null @@ -1,12 +0,0 @@ -def fix(): - with open('video.py', 'r') as f: - content = f.read() - - if 'from tqdm import tqdm' not in content: - content = 'from tqdm import tqdm\n' + content - - with open('video.py', 'w') as f: - f.write(content) - -if __name__ == '__main__': - fix() diff --git a/loop.py b/loop.py index a503c4c..cf5bb88 100644 --- a/loop.py +++ b/loop.py @@ -84,6 +84,7 @@ class AutoLoopCalculatorLTX: }, "optional": { "safe_faces_list": ("FACE_CUTS", {"forceInput": True}), + "scene_cuts_list": ("SCENE_CUTS", {"forceInput": True}), } } @@ -92,7 +93,7 @@ class AutoLoopCalculatorLTX: FUNCTION = "calculate" CATEGORY = "🔁 Sequential Batcher/Loop" - def calculate(self, source_frame_count, target_frames_per_loop, select_every_nth, current_loop_index, safe_faces_list=None): + def calculate(self, source_frame_count, target_frames_per_loop, select_every_nth, current_loop_index, safe_faces_list=None, scene_cuts_list=None): # --- Ajuste Proporcional LTX (Regla DiT 8n + 1) --- import math from . import loop @@ -147,13 +148,32 @@ class AutoLoopCalculatorLTX: best_cut = physical_source_frame_count print(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: + if scene_cuts_list and len(scene_cuts_list) > 0: + future_cuts = [c for c in scene_cuts_list if c > current_pos] + if future_cuts: + next_cut = min(future_cuts) + if next_cut <= ideal_cut: + chunk_from_scene = math.ceil((next_cut - current_pos) / select_every_nth) + safe_chunk_from_scene = ((chunk_from_scene + 6) // 8) * 8 + 1 + if safe_chunk_from_scene < 9: safe_chunk_from_scene = 9 + best_cut = current_pos + (safe_chunk_from_scene * select_every_nth) + print(f" -> 🎬 Bucle aislado por escena (Ajustado x8). Cortando en el frame: {best_cut} (Corte real: {next_cut})") + else: + best_cut = ideal_cut + print(f" -> ⚠️ Toma demasiado larga para la VRAM. Cortando por límite técnico en: {best_cut} (El plano real acaba en {next_cut})") + else: + best_cut = ideal_cut + print(f" -> 🎬 No quedan cortes de cámara por delante. Forzando corte final por VRAM en: {ideal_cut}") + elif safe_faces_list and len(safe_faces_list) > 0: 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})") + chunk_from_face = math.ceil((closest_face - current_pos) / select_every_nth) + safe_chunk_from_face = ((chunk_from_face + 6) // 8) * 8 + 1 + if safe_chunk_from_face < 9: safe_chunk_from_face = 9 + best_cut = current_pos + (safe_chunk_from_face * select_every_nth) + print(f" -> ✂️ Corte Facial Inteligente (Ajustado x8): {best_cut} (Meta equitativa: {ideal_cut})") else: - print(f" -> ⚖️ Sin caras. Forzando corte equitativo: {ideal_cut}") + best_cut = ideal_cut + print(f" -> ⚖️ Sin detectores conectados. Forzando corte equitativo x8: {ideal_cut}") effective_chunk_frames = math.ceil((best_cut - current_pos) / select_every_nth) effective_chunk_frames = ((effective_chunk_frames + 6) // 8) * 8 + 1 diff --git a/update_video.py b/update_video.py deleted file mode 100644 index c7e031a..0000000 --- a/update_video.py +++ /dev/null @@ -1,27 +0,0 @@ -import sys - -def modify_video_py(): - with open('video.py', 'r') as f: - lines = f.readlines() - - # Find the imports section - import_uuid_idx = -1 - for i, line in enumerate(lines): - if line.startswith('import time'): - import_uuid_idx = i + 1 - break - - if import_uuid_idx != -1: - lines.insert(import_uuid_idx, 'import uuid\n') - - # Add cache after imports - for i, line in enumerate(lines): - if line.startswith('try:'): - lines.insert(i, '\n# Caché persistente para evitar re-escaneos pesados de vídeo\nVIDEO_ANALYSIS_CACHE = {}\n\n') - break - - with open('video.py', 'w') as f: - f.writelines(lines) - -if __name__ == '__main__': - modify_video_py() diff --git a/video.py b/video.py index 1acee09..a674062 100644 --- a/video.py +++ b/video.py @@ -260,7 +260,7 @@ class VideoAnalyzerSceneDetector: } } - RETURN_TYPES = ("*", "INT", "FLOAT", "AUDIO", "FACE_CUTS", "IMAGE", "STRING") + RETURN_TYPES = ("*", "INT", "FLOAT", "AUDIO", "SCENE_CUTS", "IMAGE", "STRING") RETURN_NAMES = ("video_path", "total_frames", "source_fps", "source_audio", "scene_cuts_list", "reference_frame", "log") OUTPUT_NODE = True FUNCTION = "analyze" @@ -383,6 +383,7 @@ class AutoLoopCalculator: }, "optional": { "safe_faces_list": ("FACE_CUTS", {"forceInput": True}), + "scene_cuts_list": ("SCENE_CUTS", {"forceInput": True}), } } @@ -391,7 +392,7 @@ class AutoLoopCalculator: 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): + def calculate(self, source_frame_count, target_frames_per_loop, select_every_nth, current_loop_index, safe_faces_list=None, scene_cuts_list=None): log_output = [] def _log(msg): print(msg) @@ -443,14 +444,34 @@ class AutoLoopCalculator: best_cut = safe_source_frame_count _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 + if scene_cuts_list and len(scene_cuts_list) > 0: + # 🎬 LÓGICA DE ESCENAS ESTRICTA (1 Escena = 1 Bucle) + # Buscar el PRIMER corte que venga inmediatamente después de la posición actual + future_cuts = [c for c in scene_cuts_list if c > current_pos] + + if future_cuts: + next_cut = min(future_cuts) + + # Verificamos si esta escena entera cabe en la VRAM + if next_cut <= ideal_cut: + best_cut = next_cut + _log(f" -> 🎬 Bucle aislado por escena. Cortando exacto en el frame: {best_cut}") + else: + best_cut = ideal_cut + _log(f" -> ⚠️ Toma demasiado larga para la VRAM. Cortando por límite técnico en: {best_cut} (El plano real acaba en {next_cut})") + else: + best_cut = ideal_cut + _log(f" -> 🎬 No quedan cortes de cámara por delante. Forzando corte final por VRAM en: {ideal_cut}") + + elif safe_faces_list and len(safe_faces_list) > 0: + # 👤 LÓGICA DE ROSTROS (Fallback si no hay escenas conectadas) closest_face = min(safe_faces_list, key=lambda x: abs(x - ideal_cut)) best_cut = closest_face - _log(f" -> ✂️ Corte Inteligente proyectado: {best_cut} (Meta equitativa: {ideal_cut})") + _log(f" -> ✂️ Corte Facial Inteligente: {best_cut} (Meta: {ideal_cut})") + else: - _log(f" -> ⚖️ Sin caras detectadas. Forzando corte equitativo: {ideal_cut}") + best_cut = ideal_cut + _log(f" -> ⚖️ Sin detectores conectados. 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: @@ -570,6 +591,7 @@ class AutoLoopCalculatorWan: }, "optional": { "safe_faces_list": ("FACE_CUTS", {"forceInput": True}), + "scene_cuts_list": ("SCENE_CUTS", {"forceInput": True}), } } @@ -578,7 +600,7 @@ class AutoLoopCalculatorWan: 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): + def calculate(self, source_frame_count, target_frames_per_loop, select_every_nth, current_loop_index, safe_faces_list=None, scene_cuts_list=None): log_output = [] def _log(msg): print(msg) @@ -637,17 +659,33 @@ class AutoLoopCalculatorWan: best_cut = physical_source_frame_count _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: + if scene_cuts_list and len(scene_cuts_list) > 0: + future_cuts = [c for c in scene_cuts_list if c > current_pos] + if future_cuts: + next_cut = min(future_cuts) + if next_cut <= ideal_cut: + chunk_from_scene = math.ceil((next_cut - current_pos) / select_every_nth) + safe_chunk_from_scene = (chunk_from_scene // 4) * 4 + if safe_chunk_from_scene < 4: safe_chunk_from_scene = 4 + best_cut = current_pos + (safe_chunk_from_scene * select_every_nth) + _log(f" -> 🎬 Bucle aislado por escena (Ajustado x4). Cortando en el frame: {best_cut} (Corte real: {next_cut})") + else: + best_cut = ideal_cut + _log(f" -> ⚠️ Toma demasiado larga para la VRAM. Cortando por límite técnico en: {best_cut} (El plano real acaba en {next_cut})") + else: + best_cut = ideal_cut + _log(f" -> 🎬 No quedan cortes de cámara por delante. Forzando corte final por VRAM en: {ideal_cut}") + elif safe_faces_list and len(safe_faces_list) > 0: closest_face = min(safe_faces_list, key=lambda x: abs(x - ideal_cut)) chunk_from_face = math.ceil((closest_face - current_pos) / select_every_nth) safe_chunk_from_face = (chunk_from_face // 4) * 4 if safe_chunk_from_face < 4: safe_chunk_from_face = 4 best_cut = current_pos + (safe_chunk_from_face * select_every_nth) - _log(f" -> ✂️ Corte Inteligente (Ajustado x4): {best_cut} (Cara original detectada: {closest_face})") + _log(f" -> ✂️ Corte Facial Inteligente (Ajustado x4): {best_cut} (Cara original detectada: {closest_face})") else: - _log(f" -> ⚖️ Sin caras. Forzando corte equitativo x4: {ideal_cut}") + best_cut = ideal_cut + _log(f" -> ⚖️ Sin detectores conectados. 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)