From 2cd9b13f4c37ecf2f1eb75605e07a4236bc147d1 Mon Sep 17 00:00:00 2001 From: Meisoftcoltd <38667978+Meisoftcoltd@users.noreply.github.com> Date: Mon, 30 Mar 2026 14:42:34 +0000 Subject: [PATCH] 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. --- .../test_batch.cpython-312-pytest-9.0.2.pyc | Bin 3971 -> 0 bytes video.py | 91 ++++++++++++++---- 2 files changed, 70 insertions(+), 21 deletions(-) delete mode 100644 tests/__pycache__/test_batch.cpython-312-pytest-9.0.2.pyc diff --git a/tests/__pycache__/test_batch.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_batch.cpython-312-pytest-9.0.2.pyc deleted file mode 100644 index f534887eba3e05994c7ad848629d5f297a01f4d6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3971 zcmb7GO>7&-6`o!0l1p+akxoRdejD+fiv%CqqjJi%MK5A#LG(C94fLd&YUPwu-|TYv<4A%+ zfbVDCn>RD>efwtq6$%9q{C+e3Ph%0J|1!n>`Rc;XMGr!ckdAaNjS`&A`HUyYb0{Mu zM9#5z6JFcmOZaRqB_x~s69M3SI+&3YGKYACqMNR{&8*ENRH={vow&L!O7Wt&d6F+p30?lOw_z) z&6rfo=5)+hA(Jms&j(}IOMW23V;OB4m*j?cBX5=>4ecX5SHRgME`=IHSB*5L!DKEo zRaEVmrJ$-BS;JD*;e0XKy5)Kq(9TI1eT1<6u_t()yN41Uod+uD9-yKw0QKr3P+!p- z_tDOq*t9NbR&wTQj%0wlp;&kU{w!}he*yx1gwuK5^JiWcbdm9C{tjm~{L7qMxjTFr zrRw>HiH)D`E%PqUIJAe4?*Kkht4|Td{S#Yk8(R#^ah`G-cX^44;p8W~#f)+G61=s{?4cKO|>(#buw4e8_#L_=-0^}+z# z{Z0&0*IrwL+o!Vp?v-uS-c$DIzQYjsjsO)FL3rg?7E0ohNU= zCfdN*Xf%HSivvc3(>=PdFBErgGGc3U*rUn(;6?-xkS6SkJ2B*Yu4t&E=;!z9O_z}TU3@VFGrT3Rwr6()&A9Q zhgpeOPe?97PdM&cLEDgy@00lRu-YTk?B>&rZ|n}YdlJi>&NaI^B#56Vg$Ax<@>Vfs zUnuca-uVQA4)(9i6|^*r*{ElLH`y2C!rSSSm`&fePquhmWHDH?oJXuDl4|6n-4iz@ zr6MUy(dFI^X=I->UxKNvXExwPJI%X2;D?-BuAiIaZkcg12yw|U+g$4bJ8c*^+h=~I z-d!=!boeQY5CdW#Z-rmEw0yf7dAt3HJsZ;7_7NxI5|vant7WjN(txUFa(W@nWLZ^N z<{b-R`66#I!rlsmJ?qE_kX2-du^~%>Y?Kk!c}zy`p_jZUo_g645-+^$k;T53$9Qpc zN8&{%!?riuKt?mNsv3WjN|Oa6Z5dgU$`|ucQ&&5z=8G&9!wxE-gAEgWix?KB{>BjX zxnlvCY-dg*R4Tyhhbh!!7EQ|MOxj^&@;PFqjmhDHWu&RUq0^A-J)D6T8)26njWAJd zd#^?78q1HVYFWMhj`KMQgnm4Yt>i78`U%eGRFcbCRIu0THuE^CPLW(jor1TTn$9Kd zj!Xsi5~KqA-r?B*PBdJvhV9Hoh@Wl8o6fOTPlNb2L~?wb3&FlSB!-a78nk-5Agm)$ zfhCx*mvggTRC>>ud!Bp<2KE**e*tvc!*SeySn+<_`5XnFBmZ;M^>5VoKQHR)`Qq9- ziqrcY_NP6?F7jckGK_*Y)=+==JaE5a*Y5`;`hh z^z7(~$3Lm-*D6SP*3&pMLr2KjP~jPgc(*t3G{RsC9Hd%s$9&b_`WI zhF0`y$C>$H?Tvx@s^b-Oq=p1`qD|CYLETHir|85BrEgP-SCsgsa-pJJSVvvo3a7Y{ r#hJ}$euZwmnEVVyFK9)}3Ky diff --git a/video.py b/video.py index 9c2b5ab..445d88a 100644 --- a/video.py +++ b/video.py @@ -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: