feat: update VideoAnalyzerWithAudio UI and preview logic

- Replaced VideoAnalyzerWithAudio class with improved UI handling logic.
- Implemented reference frame preview generation within the node.
- Added IS_CHANGED and VALIDATE_INPUTS class methods for correct caching and validation.
- Output formatting changed to return both UI payload and specific results tuple.
This commit is contained in:
Meisoftcoltd
2026-03-30 14:42:34 +00:00
parent e4d091df59
commit 2cd9b13f4c
2 changed files with 70 additions and 21 deletions
+70 -21
View File
@@ -19,22 +19,47 @@ except ImportError:
class VideoAnalyzerWithAudio:
@classmethod
def INPUT_TYPES(cls):
# 💡 TRUCO: Robamos solo el widget de video (con su botón Upload) de VHS
vhs_class = nodes.NODE_CLASS_MAPPINGS.get("VHS_LoadVideo")
video_input = ("STRING", {"video_upload": True}) # Fallback por defecto
if vhs_class:
inputs = vhs_class.INPUT_TYPES()
req = inputs.get("required", {})
# Añadimos nuestros interruptores al nodo de VHS
req["use_face_detector"] = ("BOOLEAN", {"default": True})
req["blur_threshold"] = ("FLOAT", {"default": 100.0, "min": 0.0, "max": 1000.0, "step": 1.0})
return {"required": req}
return {"required": {"video": ("STRING", {"image_upload": True}), "use_face_detector": ("BOOLEAN", {"default": True}), "blur_threshold": ("FLOAT", {"default": 100.0})}}
vhs_inputs = vhs_class.INPUT_TYPES()
if "video" in vhs_inputs.get("required", {}):
video_input = vhs_inputs["required"]["video"]
RETURN_TYPES = ("STRING", "INT", "AUDIO", "FACE_CUTS")
RETURN_NAMES = ("video_name", "total_frames", "source_audio", "safe_faces_list")
return {
"required": {
"video": video_input,
"reference_frame_idx": ("INT", {"default": 0, "min": 0, "max": 100000, "step": 1}),
"use_face_detector": ("BOOLEAN", {"default": True}),
"blur_threshold": ("FLOAT", {"default": 100.0, "min": 0.0, "max": 1000.0, "step": 1.0}),
}
}
RETURN_TYPES = ("STRING", "INT", "AUDIO", "FACE_CUTS", "IMAGE")
RETURN_NAMES = ("video_name", "total_frames", "source_audio", "safe_faces_list", "reference_frame")
OUTPUT_NODE = True # Obligatorio para que ComfyUI renderice la preview
FUNCTION = "analyze"
CATEGORY = "🔁 Sequential Batcher/Video"
def analyze(self, video, use_face_detector, blur_threshold, **kwargs):
@classmethod
def IS_CHANGED(cls, video, **kwargs):
# Romper el caché si el archivo ha sido modificado en disco
video_name = video[0] if isinstance(video, (list, tuple)) else video
video_path = folder_paths.get_annotated_filepath(video_name)
if os.path.exists(video_path):
return os.path.getmtime(video_path)
return time.time()
@classmethod
def VALIDATE_INPUTS(cls, video, **kwargs):
video_name = video[0] if isinstance(video, (list, tuple)) else video
video_path = folder_paths.get_annotated_filepath(video_name)
if not os.path.exists(video_path):
return f"❌ El archivo de video no existe en: {video_path}"
return True
def analyze(self, video, reference_frame_idx, use_face_detector, blur_threshold, **kwargs):
video_name = video[0] if isinstance(video, (list, tuple)) else video
video_path = folder_paths.get_annotated_filepath(video_name)
@@ -51,31 +76,53 @@ class VideoAnalyzerWithAudio:
except:
print(f" -> ⚠️ Sin audio o error al extraer.")
# 2. Escaneo de Frames y Rostros
# 2. Escaneo de OpenCV (Frames, Rostros y Referencia)
frame_count = 0
safe_faces = []
ref_tensor = torch.zeros((1, 64, 64, 3), dtype=torch.float32) # Tensor negro de seguridad
ui_result = {}
if not HAS_OPENCV and use_face_detector:
if not HAS_OPENCV:
print(" -> ❌ ERROR: OpenCV no está instalado. Ejecuta: pip install opencv-python")
use_face_detector = False
if HAS_OPENCV:
else:
cap = cv2.VideoCapture(video_path)
frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
print(f" -> 🎞️ Total Frames detectados: {frame_count}")
# Extraer el Frame de Referencia específico
safe_ref_idx = min(reference_frame_idx, max(0, frame_count - 1))
cap.set(cv2.CAP_PROP_POS_FRAMES, safe_ref_idx)
ret, ref_frame = cap.read()
if ret:
# Convertir BGR a RGB para ComfyUI
ref_frame_rgb = cv2.cvtColor(ref_frame, cv2.COLOR_BGR2RGB)
# 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})")
# Generar imagen temporal para el Preview de la UI
import random
from PIL import Image
preview_dir = folder_paths.get_temp_directory()
preview_name = f"preview_ref_{random.randint(1000, 9999)}.png"
Image.fromarray(ref_frame_rgb).save(os.path.join(preview_dir, preview_name))
ui_result = {"images": [{"filename": preview_name, "subfolder": "", "type": "temp"}]}
# Escaneo de Rostros
if use_face_detector:
print(f" -> 🤖 Iniciando escaneo de rostros (Umbral de nitidez: {blur_threshold})...")
print(f" -> 🤖 Iniciando escaneo de rostros (Umbral: {blur_threshold})...")
cascade_path = cv2.data.haarcascades + 'haarcascade_frontalface_default.xml'
face_cascade = cv2.CascadeClassifier(cascade_path)
cap.set(cv2.CAP_PROP_POS_FRAMES, 0) # Volver al inicio
idx = 0
while True:
ret, frame = cap.read()
if not ret: break
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
# Varianza de Laplace para descartar motion blur
variance = cv2.Laplacian(gray, cv2.CV_64F).var()
if variance > blur_threshold:
@@ -83,14 +130,16 @@ class VideoAnalyzerWithAudio:
if len(faces) > 0:
safe_faces.append(idx)
idx += 1
cap.release()
print(f" -> ✅ Escaneo completado. Encontrados {len(safe_faces)} frames nítidos con rostros.")
print(f" -> ✅ Encontrados {len(safe_faces)} frames nítidos con rostros.")
else:
cap.release()
print(f" -> 🤖 Escaneo de rostros DESACTIVADO.")
cap.release()
print(f"{'='*50}\n")
return (video_name, frame_count, source_audio, safe_faces)
# Devolvemos el diccionario con la clave 'ui' para que se pinte la preview en el lienzo
return {"ui": ui_result, "result": (video_name, frame_count, source_audio, safe_faces, ref_tensor)}
@register_node
class AutoLoopCalculator: