From a5db690cdf6324c155512123b4f3bfdaade7eeb0 Mon Sep 17 00:00:00 2001 From: Meisoftcoltd <38667978+Meisoftcoltd@users.noreply.github.com> Date: Sun, 5 Apr 2026 09:36:44 +0000 Subject: [PATCH] feat: implement proportional adjustment logic for AutoLoopCalculators - Patches `AutoLoopCalculator` in `video.py` to add base proportional adjustment without VAE constraints. - Patches `AutoLoopCalculatorLTX` in `loop.py` to enforce the strict 8n + 1 frame rule for LTX models, matching its calculation mathematically correctly for proportional chunking. - Fixes `__pycache__` to be excluded correctly. --- loop.py | 90 +++++++++++++++++++++++++++++++++++++++++++++----------- video.py | 24 +++++++++++---- 2 files changed, 91 insertions(+), 23 deletions(-) diff --git a/loop.py b/loop.py index 5bde998..a2f0245 100644 --- a/loop.py +++ b/loop.py @@ -74,43 +74,99 @@ class SequentialLoopStart: @register_node class AutoLoopCalculatorLTX: @classmethod - def INPUT_TYPES(cls): + def INPUT_TYPES(s): return { "required": { - "source_video_frames": ("INT", {"forceInput": True}), - "target_chunk_frames": ("INT", {"default": 81, "min": 9, "max": 257, "step": 8}), + "source_frame_count": ("INT", {"forceInput": True}), + "target_frames_per_loop": ("INT", {"default": 81, "min": 9, "max": 257, "step": 8}), + "select_every_nth": ("INT", {"default": 1, "min": 1, "max": 100}), + "current_loop_index": ("INT", {"forceInput": True}), + }, + "optional": { + "safe_faces_list": ("FACE_CUTS", {"forceInput": True}), } } - RETURN_TYPES = ("INT", "INT") - RETURN_NAMES = ("ltx_chunk_size", "total_loops_estimated") + RETURN_TYPES = ("INT", "INT", "INT") + RETURN_NAMES = ("chunk_frames", "skip_frames", "select_every_nth") FUNCTION = "calculate" CATEGORY = "🔁 Sequential Batcher/Loop" - def calculate(self, source_video_frames, target_chunk_frames): + def calculate(self, source_frame_count, target_frames_per_loop, select_every_nth, current_loop_index, safe_faces_list=None): + # --- Ajuste Proporcional LTX (Regla DiT 8n + 1) --- import math + from . import loop - # 1. Forzar el chunk a la regla estricta de LTX 2.3: 8n + 1 - n = max(1, round((target_chunk_frames - 1) / 8)) - ltx_chunk = (n * 8) + 1 + total_effective_frames = source_frame_count // select_every_nth - # 2. Calcular el avance real (Restamos el fotograma 'ancla' de solapamiento) - advance_per_loop = ltx_chunk - 1 - total_loops = math.ceil(source_video_frames / advance_per_loop) + # Truncar al (8n + 1) más cercano hacia abajo + if total_effective_frames < 9: + safe_effective_frames = total_effective_frames # Too short, bypass + else: + safe_effective_frames = ((total_effective_frames - 1) // 8) * 8 + 1 + + safe_source_frame_count = safe_effective_frames * select_every_nth + loop.global_source_frame_count = safe_source_frame_count + + # Ajuste proporcional forzando regla 8n + 1 + estimated_loops = math.ceil(safe_effective_frames / target_frames_per_loop) + if estimated_loops > 0: + optimal_target = math.ceil(safe_effective_frames / estimated_loops) + # Encontrar el 8n + 1 más cercano hacia arriba para cubrir el total + adjusted_target = ((optimal_target - 1 + 7) // 8) * 8 + 1 + target_frames_per_loop = adjusted_target + print(f" -> ⚖️ Ajuste Proporcional LTX: Target recalculado a {adjusted_target} frames por ciclo.") + # ------------------------------------------------------------- + + loop.global_select_every_nth = select_every_nth # Indicador global para el Stitcher y Sender (LTX mode activo) global global_ltx_mode global_ltx_mode = True + current_pos = loop.global_accumulated_frames + print(f"\n{'='*50}") print(f"📊 [DEBUG] NODO: Auto Loop Calculator (LTX 2.3)") - print(f" -> Frames Totales Origen: {source_video_frames}") - print(f" -> Chunk Solicitado: {target_chunk_frames} | Forzado a LTX (8n+1): {ltx_chunk}") - print(f" -> Avance Real por Bucle: {advance_per_loop} frames") - print(f" -> Total de Bucles Estimados: {total_loops}") + print(f" -> Timeline seguro ajustado a LTX: {current_pos} / {safe_source_frame_count} (Original: {source_frame_count})") + + if current_pos >= safe_source_frame_count: + return (1, current_pos, select_every_nth) + + frames_left = safe_source_frame_count - current_pos + + equitable_target = target_frames_per_loop * select_every_nth + + ideal_cut = current_pos + equitable_target + + if frames_left <= equitable_target: + best_cut = safe_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: + 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})") + else: + print(f" -> ⚖️ Sin caras detectadas. Forzando corte equitativo: {ideal_cut}") + + effective_chunk_frames = math.ceil((best_cut - current_pos) / select_every_nth) + # Forzar a 8n + 1 + effective_chunk_frames = ((effective_chunk_frames - 1) // 8) * 8 + 1 + + if effective_chunk_frames < 9: + effective_chunk_frames = 9 # Mínimo vital LTX + + if current_pos + (effective_chunk_frames * select_every_nth) > safe_source_frame_count: + effective_chunk_frames = (safe_source_frame_count - current_pos) // select_every_nth + + skip_frames = current_pos + + print(f" -> 🚀 Ciclo {current_loop_index}: Solicitando {effective_chunk_frames} frames efectivos (Saltando {skip_frames})") print(f"{'='*50}\n") - return (ltx_chunk, total_loops) + return (effective_chunk_frames, skip_frames, select_every_nth) @register_node class SequentialLoopTrigger: diff --git a/video.py b/video.py index 252e088..a8bbfc0 100644 --- a/video.py +++ b/video.py @@ -212,10 +212,20 @@ class AutoLoopCalculator: CATEGORY = "🔁 Sequential Batcher/Video" def calculate(self, source_frame_count, target_frames_per_loop, select_every_nth, current_loop_index, safe_faces_list=None): + # --- Ajuste Proporcional Base (Sin restricciones VAE) --- import math from . import loop - loop.global_source_frame_count = source_frame_count + total_effective_frames = source_frame_count // select_every_nth + safe_source_frame_count = total_effective_frames * select_every_nth + loop.global_source_frame_count = safe_source_frame_count + + estimated_loops = math.ceil(total_effective_frames / target_frames_per_loop) + if estimated_loops > 0: + target_frames_per_loop = math.ceil(total_effective_frames / estimated_loops) + print(f" -> ⚖️ Ajuste Proporcional: Target recalculado a {target_frames_per_loop} frames por ciclo.") + # ------------------------------------------------------------- + loop.global_select_every_nth = select_every_nth current_pos = loop.global_accumulated_frames @@ -224,18 +234,17 @@ class AutoLoopCalculator: print(f"📊 [DEBUG] NODO: Auto Loop Calculator (Motor Dinámico)") print(f" -> Posición en el timeline original: {current_pos} / {source_frame_count}") - if current_pos >= source_frame_count: + if current_pos >= safe_source_frame_count: return (1, current_pos, select_every_nth) - frames_left = source_frame_count - current_pos + frames_left = safe_source_frame_count - current_pos - total_loops = math.ceil(source_frame_count / (target_frames_per_loop * select_every_nth)) - equitable_target = math.floor(source_frame_count / total_loops) + equitable_target = target_frames_per_loop * select_every_nth ideal_cut = current_pos + equitable_target if frames_left <= equitable_target: - best_cut = source_frame_count + best_cut = safe_source_frame_count print(f" -> 🧮 Absorbiendo resto final: meta fijada en frame {best_cut}") else: best_cut = ideal_cut @@ -248,6 +257,9 @@ class AutoLoopCalculator: print(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: + effective_chunk_frames = (safe_source_frame_count - current_pos) // select_every_nth + skip_frames = current_pos print(f" -> 🚀 Ciclo {current_loop_index}: Solicitando {effective_chunk_frames} frames efectivos (Saltando {skip_frames})")