Merge pull request #94 from Meisoftcoltd/feat/scene-cuts-priority-1402347662369726826
Refactor scene cuts priority in loop calculators
This commit is contained in:
@@ -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()
|
||||
@@ -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()
|
||||
-12
@@ -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()
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user