From e0f76497f8ac8bf5188a77aaf12c73aa62614c8a Mon Sep 17 00:00:00 2001 From: Verolelb <115535732+Verolelb@users.noreply.github.com> Date: Fri, 26 Sep 2025 07:42:52 +0200 Subject: [PATCH] Feat: Add 2-pass, CRF & GPU modes with auto-correction --- compress_video_node.py | 89 ++++++++++++++---------------------------- 1 file changed, 29 insertions(+), 60 deletions(-) diff --git a/compress_video_node.py b/compress_video_node.py index 91bb9e4..7b29b9e 100644 --- a/compress_video_node.py +++ b/compress_video_node.py @@ -9,12 +9,7 @@ import datetime import platform class VideoCompressorUnified: - """ - Node de compression vidéo unifié : - - Accepte images ou vidéo - - Trois modes : fast_crf, 2pass, gpu - """ - + # ... (le début de la classe ne change pas) ... @classmethod def INPUT_TYPES(s): return { @@ -28,10 +23,7 @@ class VideoCompressorUnified: "video_path_in": ("STRING", {"forceInput": True}), "fps": ("INT", {"default": 24, "min": 1, "max": 60}), "crf": ("INT", {"default": 23, "min": 10, "max": 40}), - "preset": ([ - "ultrafast","superfast","veryfast","faster", - "fast","medium","slow","slower","veryslow" - ], {"default": "fast"}), + "preset": (["ultrafast","superfast","veryfast","faster","fast","medium","slow","slower","veryslow"], {"default": "fast"}), } } @@ -41,15 +33,10 @@ class VideoCompressorUnified: 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 - ] + command = ["ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries", "stream=r_frame_rate,duration", "-of", "csv=p=0", 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.") + 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]) @@ -58,20 +45,29 @@ class VideoCompressorUnified: 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 - ) + 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) + if not images: return torch.empty(0) return torch.cat(images, 0) 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)) + 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}'.") + 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}'.") + 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) @@ -84,14 +80,12 @@ class VideoCompressorUnified: 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") - 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 - ) + + # --- 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: - if not os.path.exists(video_path_in): - raise FileNotFoundError(f"Fichier vidéo non trouvé: {video_path_in}") + 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) @@ -103,49 +97,24 @@ class VideoCompressorUnified: 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.") + if duration_s <= 0: raise ValueError("Durée de la vidéo nulle ou invalide.") - # ----- Choix du mode ----- if mode == "2pass": - # Calcule le bitrate cible target_size_bits = target_mb * 1024 * 1024 * 8 audio_bitrate_bits = 128 * 1000 video_bitrate_bits = (target_size_bits / duration_s) - audio_bitrate_bits - if video_bitrate_bits < 100 * 1000: - video_bitrate_bits = 100 * 1000 + 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"] - - # Première passe subprocess.run(["ffmpeg"] + common_args + ["-pass", "1", "-an", "-f", "mp4", "-passlogfile", pass_log_file, null_output], check=True, capture_output=True) - # Deuxième passe 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) - 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(["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) elif mode == "gpu": - # Ici on force un CRF rapide avec encodeur 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) - + 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}") - return (final_video_path, output_images) - finally: - shutil.rmtree(temp_dir, ignore_errors=True) + shutil.rmtree(temp_dir, ignore_errors=True) \ No newline at end of file