From dc549d8d5711c1695ca8dbbd6e5dec7ecc8f4efc Mon Sep 17 00:00:00 2001 From: Verolelb <115535732+Verolelb@users.noreply.github.com> Date: Fri, 26 Sep 2025 14:08:45 +0200 Subject: [PATCH] Fix: Reworked 2-pass mode for precise file size --- __pycache__/__init__.cpython-312.pyc | Bin 0 -> 475 bytes .../compress_video_node.cpython-312.pyc | Bin 0 -> 8638 bytes compress_video_node.py | 95 ++++++++++-------- 3 files changed, 53 insertions(+), 42 deletions(-) create mode 100644 __pycache__/__init__.cpython-312.pyc create mode 100644 __pycache__/compress_video_node.cpython-312.pyc diff --git a/__pycache__/__init__.cpython-312.pyc b/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0fff8f454861752c699c0431e5d9f54088489ecd GIT binary patch literal 475 zcmX@j%ge<81nZ=)WgKT@V0aATzyLFp@!0^#n9h*GkiwY5kjogw$jFewl**XN7{!#r zl)?<8nVA?;SyEV515E${MutjOO}3XHo+jfhv9QdP)O_ds+=8Oi;^O?G(7epF%+!=3 zknyFmK;pyvzBLLkc?Ga)1r4Yw%~B(vibr!=K?>3;Lp^oTOtVt(%TGyFCv%1P)UBn602LSSujcot` literal 0 HcmV?d00001 diff --git a/__pycache__/compress_video_node.cpython-312.pyc b/__pycache__/compress_video_node.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..48135c68cabff3431470d7307534a47d9295b389 GIT binary patch literal 8638 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 ITo&#B02elaIRF3v literal 0 HcmV?d00001 diff --git a/compress_video_node.py b/compress_video_node.py index 7b29b9e..0ba95ba 100644 --- a/compress_video_node.py +++ b/compress_video_node.py @@ -9,12 +9,12 @@ import datetime import platform class VideoCompressorUnified: - # ... (le dĂ©but de la classe ne change pas) ... + # ... (INPUT_TYPES, etc. ne changent pas) ... @classmethod def INPUT_TYPES(s): return { "required": { - "mode": (["fast_crf", "2pass", "gpu"], {"default": "2pass"}), + "mode": (["2pass", "fast_crf", "gpu"], {"default": "2pass"}), "target_mb": ("FLOAT", {"min": 0.5, "max": 2000.0, "step": 0.5, "default": 8.0}), "codec": (["libx264", "libx265", "h264_nvenc", "hevc_nvenc"], {"default": "libx264"}), }, @@ -32,72 +32,77 @@ class VideoCompressorUnified: FUNCTION = "process" CATEGORY = "đŸŽ„Video Utils" - def _get_video_info(self, video_path): - command = ["ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries", "stream=r_frame_rate,duration", "-of", "csv=p=0", video_path] + def _get_video_duration(self, video_path): + command = ["ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", video_path] result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=True) - parts = result.stdout.strip().split(',') - if len(parts) < 2: raise ValueError("Impossible d'analyser la vidĂ©o.") - fps_fraction = parts[0].split('/') - fps = float(fps_fraction[0]) / float(fps_fraction[1]) if len(fps_fraction) == 2 else 30.0 - duration = float(parts[1]) - return {"duration": duration, "fps": fps} - - def _load_video_to_tensor(self, video_path, temp_dir): - frames_dir = os.path.join(temp_dir, "loaded_frames") - os.makedirs(frames_dir, exist_ok=True) - subprocess.run(["ffmpeg", "-i", video_path, os.path.join(frames_dir, "frame_%05d.png")], check=True, capture_output=True) - frame_files = sorted([os.path.join(frames_dir, f) for f in os.listdir(frames_dir) if f.endswith('.png')]) - images = [torch.from_numpy(np.array(Image.open(f).convert("RGB")).astype(np.float32) / 255.0).unsqueeze(0) for f in frame_files] - if not images: return torch.empty(0) - return torch.cat(images, 0) + return float(result.stdout) def process(self, mode, target_mb, codec, images=None, video_path_in=None, fps=24, crf=23, preset="fast"): if images is None and video_path_in is None: return ("", torch.empty(0)) + # ... (La logique de correction de codec reste la mĂȘme) ... gpu_codecs = ["h264_nvenc", "hevc_nvenc"] cpu_codecs = ["libx264", "libx265"] original_codec = codec - if mode == "gpu": if codec not in gpu_codecs: codec = "h264_nvenc" - print(f"Avertissement : Le mode 'gpu' a Ă©tĂ© sĂ©lectionnĂ© avec un codec CPU ({original_codec}). Correction automatique vers '{codec}'.") + print(f"Avertissement: Mode 'gpu' sĂ©lectionnĂ© avec un codec CPU ({original_codec}). Correction auto vers '{codec}'.") elif mode in ["fast_crf", "2pass"]: if codec in gpu_codecs: codec = "libx264" - print(f"Avertissement : Le mode '{mode}' a Ă©tĂ© sĂ©lectionnĂ© avec un codec GPU ({original_codec}). Correction automatique vers '{codec}'.") - + print(f"Avertissement: Mode '{mode}' sĂ©lectionnĂ© avec un codec GPU ({original_codec}). Correction auto vers '{codec}'.") + temp_dir = os.path.join(folder_paths.get_temp_directory(), "compressor_unified") shutil.rmtree(temp_dir, ignore_errors=True) os.makedirs(temp_dir, exist_ok=True) - + + frames_path_pattern = os.path.join(temp_dir, "frame_%05d.png") pass_log_file = os.path.join(temp_dir, "ffmpeg_passlog") + output_images = None + duration_s = 0 try: + # --- ÉTAPE 1: Standardiser l'entrĂ©e en une sĂ©quence de frames --- if images is not None: + print("Mode: Input depuis le tenseur d'images.") output_images = images for i, img_tensor in enumerate(images): - Image.fromarray((255. * img_tensor.cpu().numpy()).astype(np.uint8)).save(os.path.join(temp_dir, f"frame_{i:05d}.png")) - initial_video_path = os.path.join(temp_dir, "initial_from_images.mp4") - - # --- LA CORRECTION EST ICI --- - # "yuv4s20p" a Ă©tĂ© remplacĂ© par "yuv420p" - subprocess.run(["ffmpeg", "-y", "-framerate", str(fps), "-i", os.path.join(temp_dir, "frame_%05d.png"), "-c:v", "libx264", "-pix_fmt", "yuv420p", initial_video_path], check=True, capture_output=True) - else: + Image.fromarray((255. * img_tensor.cpu().numpy()).astype(np.uint8)).save(frames_path_pattern % i) + duration_s = len(images) / fps + else: # video_path_in est utilisĂ© + print("Mode: Input depuis un fichier vidĂ©o. Extraction des frames pour nettoyage...") if not os.path.exists(video_path_in): raise FileNotFoundError(f"Fichier vidĂ©o non trouvĂ©: {video_path_in}") - initial_video_path = video_path_in - output_images = self._load_video_to_tensor(video_path_in, temp_dir) + subprocess.run(["ffmpeg", "-i", video_path_in, "-vsync", "vfr", frames_path_pattern], check=True, capture_output=True) + + frame_files = sorted([f for f in os.listdir(temp_dir) if f.startswith('frame_') and f.endswith('.png')]) + images_list = [torch.from_numpy(np.array(Image.open(os.path.join(temp_dir, f)).convert("RGB")).astype(np.float32) / 255.0).unsqueeze(0) for f in frame_files] + if not images_list: return ("", torch.empty(0)) + output_images = torch.cat(images_list, 0) + # On utilise la durĂ©e rĂ©elle de la vidĂ©o d'origine pour la prĂ©cision + duration_s = self._get_video_duration(video_path_in) + # --- ÉTAPE 2: CrĂ©er une vidĂ©o source "propre", SANS AUDIO --- + print("CrĂ©ation d'une source vidĂ©o propre (sans audio)...") + clean_video_source_path = os.path.join(temp_dir, "clean_source.mp4") + subprocess.run([ + "ffmpeg", "-y", "-framerate", str(fps), "-i", frames_path_pattern, + "-c:v", "libx264", "-pix_fmt", "yuv420p", "-an", # -an = No Audio + clean_video_source_path + ], check=True, capture_output=True) + + # --- ÉTAPE 3: Compresser la vidĂ©o PROPRE en utilisant le mode choisi --- output_dir = os.path.join(folder_paths.get_output_directory(), "compressed_videos") os.makedirs(output_dir, exist_ok=True) timestamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S") filename = f"compressed_{timestamp}.mp4" final_video_path = os.path.join(output_dir, filename) - info = self._get_video_info(initial_video_path) - duration_s = info["duration"] if duration_s <= 0: raise ValueError("DurĂ©e de la vidĂ©o nulle ou invalide.") + + # La commande de base utilise maintenant la source propre + base_ffmpeg_cmd = ["ffmpeg", "-y", "-i", clean_video_source_path] if mode == "2pass": target_size_bits = target_mb * 1024 * 1024 * 8 @@ -105,16 +110,22 @@ class VideoCompressorUnified: video_bitrate_bits = (target_size_bits / duration_s) - audio_bitrate_bits if video_bitrate_bits < 100 * 1000: video_bitrate_bits = 100 * 1000 video_bitrate_k = int(video_bitrate_bits / 1000) + null_output = "NUL" if platform.system() == "Windows" else "/dev/null" - common_args = ["-y", "-i", initial_video_path, "-c:v", codec, "-b:v", f"{video_bitrate_k}k", "-preset", preset, "-threads", "0"] - subprocess.run(["ffmpeg"] + common_args + ["-pass", "1", "-an", "-f", "mp4", "-passlogfile", pass_log_file, null_output], check=True, capture_output=True) - subprocess.run(["ffmpeg"] + common_args + ["-pass", "2", "-c:a", "aac", "-b:a", "128k", "-passlogfile", pass_log_file, final_video_path], check=True, capture_output=True) + common_args = ["-c:v", codec, "-b:v", f"{video_bitrate_k}k", "-preset", preset, "-threads", "0"] + + print("Compression 2-Pass (Passe 1)...") + subprocess.run(base_ffmpeg_cmd + common_args + ["-pass", "1", "-an", "-f", "mp4", "-passlogfile", pass_log_file, null_output], check=True, capture_output=True) + + print("Compression 2-Pass (Passe 2)...") + subprocess.run(base_ffmpeg_cmd + common_args + ["-pass", "2", "-c:a", "aac", "-b:a", "128k", "-passlogfile", pass_log_file, final_video_path], check=True, capture_output=True) + elif mode == "fast_crf": - subprocess.run(["ffmpeg", "-y", "-i", initial_video_path, "-c:v", codec, "-preset", preset, "-crf", str(crf), "-c:a", "aac", "-b:a", "128k", "-threads", "0", final_video_path], check=True, capture_output=True) + subprocess.run(base_ffmpeg_cmd + ["-c:v", codec, "-preset", preset, "-crf", str(crf), "-c:a", "aac", "-b:a", "128k", "-threads", "0", final_video_path], check=True, capture_output=True) + elif mode == "gpu": - subprocess.run(["ffmpeg", "-y", "-i", initial_video_path, "-c:v", codec, "-preset", preset, "-cq", str(crf), "-c:a", "aac", "-b:a", "128k", "-threads", "0", final_video_path], check=True, capture_output=True) - else: - raise ValueError(f"Mode inconnu: {mode}") + subprocess.run(base_ffmpeg_cmd + ["-c:v", codec, "-preset", preset, "-cq", str(crf), "-c:a", "aac", "-b:a", "128k", "-threads", "0", final_video_path], check=True, capture_output=True) + return (final_video_path, output_images) finally: shutil.rmtree(temp_dir, ignore_errors=True) \ No newline at end of file