diff --git a/LICENCE b/LICENCE deleted file mode 100644 index 72a4b7b..0000000 --- a/LICENCE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2023 [VOTRE NOM OU PSEUDO] - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file diff --git a/compress_video_node.py b/compress_video_node.py index 5b660fe..af5911b 100644 --- a/compress_video_node.py +++ b/compress_video_node.py @@ -7,7 +7,6 @@ from PIL import Image import shutil import datetime import platform -# NOUVEL IMPORT OBLIGATOIRE from scipy.io.wavfile import write as write_wav class VideoCompressor: @@ -15,21 +14,29 @@ class VideoCompressor: def INPUT_TYPES(s): return { "required": { + # --- ENTRÉES DOT --- "images": ("IMAGE",), "fps": ("FLOAT", {"default": 24.0, "min": 1.0, "max": 120.0, "step": 0.01}), - "mode": (["2pass", "fast_crf", "gpu"], {"default": "2pass"}), - "target_mb": ("FLOAT", {"min": 0.5, "max": 4000.0, "step": 0.5, "default": 8.0}), + + # --- PARAMÈTRES (WIDGETS) --- + # Si target_mb = 0, on utilise le CRF. Sinon, on vise la taille en Mo. + "target_mb": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 10000.0, "step": 0.5, "display": "number"}), + + # Qualité (si target_mb est à 0) + "crf": ("INT", {"default": 23, "min": 0, "max": 51, "step": 1}), + + # Codec : Liste explicite de chaînes de caractères pour éviter l'index "1" "codec": (["libx264", "libx265", "h264_nvenc", "hevc_nvenc"], {"default": "libx264"}), + + "preset": (["ultrafast","superfast","veryfast","faster","fast","medium","slow","slower","veryslow"], {"default": "fast"}), }, "optional": { - "audio": ("*",), - "crf": ("INT", {"default": 23, "min": 10, "max": 40}), - "preset": (["ultrafast","superfast","veryfast","faster","fast","medium","slow","slower","veryslow"], {"default": "fast"}), + "audio": ("AUDIO",), } } - RETURN_TYPES = ("STRING", "IMAGE", "*") - RETURN_NAMES = ("video_path", "images", "audio") + RETURN_TYPES = ("IMAGE", "AUDIO", "FLOAT", "STRING") + RETURN_NAMES = ("images", "audio", "fps", "video_path") FUNCTION = "process" CATEGORY = "🎥Video Utils" @@ -40,12 +47,22 @@ class VideoCompressor: print(f"--- ERREUR FFmpeg ---\nCommande: {' '.join(e.cmd)}\nErreur: {e.stderr}\n--- FIN ERREUR ---") raise e - def process(self, images, fps, mode, target_mb, codec, audio=None, crf=23, preset="fast"): - gpu_codecs = ["h264_nvenc", "hevc_nvenc"] - if mode == "gpu" and codec not in gpu_codecs: codec = "h264_nvenc" - elif mode in ["fast_crf", "2pass"] and codec in gpu_codecs: codec = "libx264" + def process(self, images, fps, target_mb, codec, crf=23, preset="fast", audio=None): + # --- 1. DÉTECTION DU MODE --- + # Si target_mb > 0, on passe en mode "Taille Cible" (2-pass), sinon mode "Qualité" (CRF) + if target_mb > 0.0: + mode = "target_size" + print(f"🔧 Mode détecté : Taille cible de {target_mb} MB (2-Pass)") + else: + mode = "crf" + print(f"🔧 Mode détecté : Qualité constante (CRF {crf}) - Taille cible ignorée (0)") - temp_dir = os.path.join(folder_paths.get_temp_directory(), "compressor_final") + # Gestion GPU/CPU + gpu_codecs = ["h264_nvenc", "hevc_nvenc"] + is_gpu = codec in gpu_codecs + + # --- 2. PRÉPARATION DOSSIERS --- + temp_dir = os.path.join(folder_paths.get_temp_directory(), "comfy_compressor_v2") shutil.rmtree(temp_dir, ignore_errors=True) os.makedirs(temp_dir, exist_ok=True) @@ -53,82 +70,104 @@ class VideoCompressor: clean_video_path = os.path.join(temp_dir, "clean_video.mp4") pass_log_file = os.path.join(temp_dir, "ffmpeg_passlog") - # --- LOGIQUE AUDIO FINALE ET ROBUSTE --- + # --- 3. GESTION AUDIO (Identique, robuste) --- audio_file_path = None has_audio = False if audio is not None: - # CAS 1: L'entrée est des données audio brutes (un dictionnaire) if isinstance(audio, dict) and 'waveform' in audio and 'sample_rate' in audio: - print("Détection de données audio brutes. Sauvegarde en fichier temporaire...") - waveform = audio['waveform'].cpu().numpy() + waveform = audio['waveform'] sample_rate = audio['sample_rate'] - - # S'assurer que la forme est [samples, channels] + if torch.is_tensor(waveform): waveform = waveform.cpu().numpy() if waveform.ndim == 3: waveform = waveform.squeeze(0) if waveform.shape[0] < waveform.shape[1]: waveform = waveform.T - - # Convertir de float [-1, 1] à int16 pour le fichier .wav waveform_int16 = (waveform * 32767).astype(np.int16) - temp_wav_path = os.path.join(temp_dir, "temp_audio.wav") write_wav(temp_wav_path, sample_rate, waveform_int16) audio_file_path = temp_wav_path has_audio = True - - # CAS 2: L'entrée est un chemin de fichier (string, list, ou tuple) else: - path_candidate = "" - if isinstance(audio, (list, tuple)) and len(audio) > 0: - path_candidate = str(audio[0]) - elif isinstance(audio, str): - path_candidate = audio - + path_candidate = str(audio[0]) if isinstance(audio, (list, tuple)) and len(audio) > 0 else str(audio) if os.path.exists(path_candidate): audio_file_path = path_candidate has_audio = True - else: - print(f"Avertissement: L'entrée audio fournie '{audio}' n'est ni des données brutes, ni un chemin valide.") try: + # --- 4. SAUVEGARDE IMAGES --- + print(f"💾 Écriture des {len(images)} frames...") for i, img in enumerate(images): Image.fromarray((255. * img.cpu().numpy()).astype(np.uint8)).save(frames_path % i) - self._run_ffmpeg(["ffmpeg", "-y", "-framerate", str(fps), "-i", frames_path, "-c:v", "libx264", "-pix_fmt", "yuv420p", clean_video_path]) + # --- 5. CRÉATION VIDÉO SOURCE --- + self._run_ffmpeg([ + "ffmpeg", "-y", "-framerate", str(fps), "-i", frames_path, + "-c:v", "libx264", "-pix_fmt", "yuv420p", clean_video_path + ]) + duration_s = len(images) / float(fps) + if duration_s <= 0.001: duration_s = 1.0 # Sécurité division par zéro + # --- 6. CALCUL BITRATE ET PARAMÈTRES --- 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") - final_video_path = os.path.join(output_dir, f"compressed_{timestamp}.mp4") + final_video_path = os.path.join(output_dir, f"render_{timestamp}.mp4") - if duration_s <= 0: raise ValueError("Durée invalide.") - - # ... Le reste du code utilise 'audio_file_path' qui est maintenant GARANTI d'être un chemin valide ... - - if mode == "2pass": - audio_bitrate_bits = 128000 if has_audio else 0 - video_bitrate_k = int(((target_mb * 1024 * 1024 * 8 / duration_s) - audio_bitrate_bits) / 1000) - if video_bitrate_k < 100: video_bitrate_k = 100 + if mode == "target_size": + # Calcul Bitrate : (Taille_MB * 8192) / durée + # On soustrait l'audio (approx 128k) + audio_bitrate_kbps = 128 if has_audio else 0 + total_bitrate_kbps = (target_mb * 8192) / duration_s + video_bitrate_k = int(total_bitrate_kbps - audio_bitrate_kbps) + + if video_bitrate_k < 50: + print("⚠️ Bitrate calculé trop bas, forcé à 100k.") + video_bitrate_k = 100 - pass1_cmd = ["ffmpeg", "-y", "-i", clean_video_path, "-c:v", codec, "-b:v", f"{video_bitrate_k}k", "-preset", preset, "-pass", "1", "-an", "-f", "mp4", "-passlogfile", pass_log_file, "NUL" if platform.system() == "Windows" else "/dev/null"] - self._run_ffmpeg(pass1_cmd) + # --- ENCODAGE 2 PASSES (Target Size) --- + print(f"⚙️ Compression 2-Passes -> Cible: {target_mb}MB (Bitrate: {video_bitrate_k}k)") + + # Pass 1 + pass1 = [ + "ffmpeg", "-y", "-i", clean_video_path, "-c:v", codec, + "-b:v", f"{video_bitrate_k}k", "-preset", preset, + "-pass", "1", "-an", "-f", "mp4", + "-passlogfile", pass_log_file, + "NUL" if platform.system() == "Windows" else "/dev/null" + ] + self._run_ffmpeg(pass1) + + # Pass 2 + pass2 = ["ffmpeg", "-y", "-i", clean_video_path] + if has_audio: pass2.extend(["-i", audio_file_path]) + pass2.extend([ + "-c:v", codec, "-b:v", f"{video_bitrate_k}k", + "-preset", preset, "-pass", "2", "-passlogfile", pass_log_file + ]) + if has_audio: pass2.extend(["-c:a", "aac", "-b:a", "128k", "-map", "0:v:0", "-map", "1:a:0"]) + pass2.append(final_video_path) + self._run_ffmpeg(pass2) - pass2_cmd = ["ffmpeg", "-y", "-i", clean_video_path] - if has_audio: pass2_cmd.extend(["-i", audio_file_path]) - pass2_cmd.extend(["-c:v", codec, "-b:v", f"{video_bitrate_k}k", "-preset", preset, "-pass", "2", "-passlogfile", pass_log_file]) - if has_audio: pass2_cmd.extend(["-c:a", "aac", "-b:a", "128k", "-map", "0:v:0", "-map", "1:a:0"]) - pass2_cmd.append(final_video_path) - self._run_ffmpeg(pass2_cmd) else: - final_cmd = ["ffmpeg", "-y", "-i", clean_video_path] - if has_audio: final_cmd.extend(["-i", audio_file_path]) - quality_param = ["-crf", str(crf)] if mode == "fast_crf" else ["-cq", str(crf)] - final_cmd.extend(["-c:v", codec, "-preset", preset] + quality_param) - if has_audio: final_cmd.extend(["-c:a", "aac", "-b:a", "128k", "-map", "0:v:0", "-map", "1:a:0"]) - final_cmd.append(final_video_path) - self._run_ffmpeg(final_cmd) + # --- ENCODAGE CRF/CQ (Qualité) --- + print(f"⚙️ Compression Qualité -> CRF: {crf}") + cmd = ["ffmpeg", "-y", "-i", clean_video_path] + if has_audio: cmd.extend(["-i", audio_file_path]) + + # NVENC utilise -cq au lieu de -crf, et nécessite souvent -b:v 0 pour ignorer le bitrate + if is_gpu: + quality_args = ["-cq", str(crf), "-b:v", "0"] + else: + quality_args = ["-crf", str(crf)] + + cmd.extend(["-c:v", codec, "-preset", preset] + quality_args) + + if has_audio: cmd.extend(["-c:a", "aac", "-b:a", "128k", "-map", "0:v:0", "-map", "1:a:0"]) + cmd.append(final_video_path) + self._run_ffmpeg(cmd) - return (final_video_path, images, audio if has_audio else "") + return (images, audio, fps, final_video_path) + finally: - shutil.rmtree(temp_dir, ignore_errors=True) \ No newline at end of file + if os.path.exists(temp_dir): + shutil.rmtree(temp_dir, ignore_errors=True) \ No newline at end of file diff --git a/example_wf/Compress video.json b/example_wf/Compress video.json index 78beb28..6d947f3 100644 --- a/example_wf/Compress video.json +++ b/example_wf/Compress video.json @@ -1 +1 @@ -{"id":"219e2ee3-b5f4-4a9e-9c4c-ed0988e62ddd","revision":0,"last_node_id":27,"last_link_id":42,"nodes":[{"id":12,"type":"LoadVideo","pos":[590.2651977539062,445.7998352050781],"size":[274.080078125,439.4400939941406],"flags":{},"order":0,"mode":0,"inputs":[{"localized_name":"file","name":"file","type":"COMBO","widget":{"name":"file"},"link":null},{"localized_name":"choose file to upload","name":"upload","type":"IMAGEUPLOAD","widget":{"name":"upload"},"link":null}],"outputs":[{"localized_name":"VIDEO","name":"VIDEO","type":"VIDEO","links":[11]}],"properties":{"cnr_id":"comfy-core","ver":"0.3.60","Node name for S&R":"LoadVideo"},"widgets_values":["Bitch elle boit de la bière.mp4","image"]},{"id":17,"type":"SaveVideo","pos":[1811.84033203125,448.95257568359375],"size":[600,898],"flags":{},"order":4,"mode":0,"inputs":[{"localized_name":"video","name":"video","type":"VIDEO","link":15},{"localized_name":"filename_prefix","name":"filename_prefix","type":"STRING","widget":{"name":"filename_prefix"},"link":null},{"localized_name":"format","name":"format","type":"COMBO","widget":{"name":"format"},"link":null},{"localized_name":"codec","name":"codec","type":"COMBO","widget":{"name":"codec"},"link":null}],"outputs":[],"properties":{"cnr_id":"comfy-core","ver":"0.3.59","Node name for S&R":"SaveVideo"},"widgets_values":["video/ComfyUI","auto","auto"]},{"id":16,"type":"CreateVideo","pos":[1508.453125,442.51605224609375],"size":[270,78],"flags":{},"order":3,"mode":0,"inputs":[{"localized_name":"images","name":"images","type":"IMAGE","link":39},{"localized_name":"audio","name":"audio","shape":7,"type":"AUDIO","link":41},{"localized_name":"fps","name":"fps","type":"FLOAT","widget":{"name":"fps"},"link":null}],"outputs":[{"localized_name":"VIDEO","name":"VIDEO","type":"VIDEO","links":[15]}],"properties":{"cnr_id":"comfy-core","ver":"0.3.59","Node name for S&R":"CreateVideo"},"widgets_values":[16]},{"id":27,"type":"VideoCompressor","pos":[1177.84326171875,536.1892700195312],"size":[270,218],"flags":{},"order":2,"mode":0,"inputs":[{"localized_name":"images","name":"images","type":"IMAGE","link":40},{"localized_name":"audio","name":"audio","shape":7,"type":"*","link":38},{"localized_name":"fps","name":"fps","type":"FLOAT","widget":{"name":"fps"},"link":42},{"localized_name":"mode","name":"mode","type":"COMBO","widget":{"name":"mode"},"link":null},{"localized_name":"target_mb","name":"target_mb","type":"FLOAT","widget":{"name":"target_mb"},"link":null},{"localized_name":"codec","name":"codec","type":"COMBO","widget":{"name":"codec"},"link":null},{"localized_name":"crf","name":"crf","shape":7,"type":"INT","widget":{"name":"crf"},"link":null},{"localized_name":"preset","name":"preset","shape":7,"type":"COMBO","widget":{"name":"preset"},"link":null}],"outputs":[{"localized_name":"video_path","name":"video_path","type":"STRING","links":null},{"localized_name":"images","name":"images","type":"IMAGE","links":[39]},{"localized_name":"audio","name":"audio","type":"*","links":[41]}],"properties":{"aux_id":"Verolelb/ComfyUI-VideoCompressor","ver":"dc549d8d5711c1695ca8dbbd6e5dec7ecc8f4efc","Node name for S&R":"VideoCompressor"},"widgets_values":[24,"2pass",8,"libx264",23,"fast"]},{"id":13,"type":"GetVideoComponents","pos":[905.2611694335938,465.8274230957031],"size":[190,70],"flags":{"collapsed":false},"order":1,"mode":0,"inputs":[{"localized_name":"video","name":"video","type":"VIDEO","link":11}],"outputs":[{"localized_name":"images","name":"images","type":"IMAGE","links":[40]},{"localized_name":"audio","name":"audio","type":"AUDIO","links":[38]},{"localized_name":"fps","name":"fps","type":"FLOAT","links":[42]}],"properties":{"cnr_id":"comfy-core","ver":"0.3.59","Node name for S&R":"GetVideoComponents"},"widgets_values":[]}],"links":[[11,12,0,13,0,"VIDEO"],[15,16,0,17,0,"VIDEO"],[38,13,1,27,1,"*"],[39,27,1,16,0,"IMAGE"],[40,13,0,27,0,"IMAGE"],[41,27,2,16,1,"AUDIO"],[42,13,2,27,2,"FLOAT"]],"groups":[],"config":{},"extra":{"ds":{"scale":0.8536167070900924,"offset":[-408.32279921247004,-230.1745309529742]}},"version":0.4} \ No newline at end of file +{"id":"219e2ee3-b5f4-4a9e-9c4c-ed0988e62ddd","revision":0,"last_node_id":33,"last_link_id":60,"nodes":[{"id":32,"type":"VideoCompressor","pos":[1242.6033816614238,508.8625343965848],"size":[210,214],"flags":{},"order":3,"mode":0,"inputs":[{"localized_name":"images","name":"images","type":"IMAGE","link":55},{"localized_name":"audio","name":"audio","shape":7,"type":"AUDIO","link":56},{"localized_name":"fps","name":"fps","type":"FLOAT","widget":{"name":"fps"},"link":57},{"localized_name":"target_mb","name":"target_mb","type":"FLOAT","widget":{"name":"target_mb"},"link":null},{"localized_name":"crf","name":"crf","type":"INT","widget":{"name":"crf"},"link":null},{"localized_name":"codec","name":"codec","type":"COMBO","widget":{"name":"codec"},"link":null},{"localized_name":"preset","name":"preset","type":"COMBO","widget":{"name":"preset"},"link":null}],"outputs":[{"localized_name":"images","name":"images","type":"IMAGE","links":[58]},{"localized_name":"audio","name":"audio","type":"AUDIO","links":[59]},{"localized_name":"fps","name":"fps","type":"FLOAT","links":[60]},{"localized_name":"video_path","name":"video_path","type":"STRING","links":null}],"properties":{"aux_id":"Verolelb/ComfyUI-VideoCompressor","ver":"30b8454f0f011ff8dd1f6b81fca54b808468487e","Node name for S&R":"VideoCompressor"},"widgets_values":[24,0,23,"libx264","fast"],"shape":2},{"id":33,"type":"Label (rgthree)","pos":[1016.8291955753649,620.8622997655148],"size":[219.45703125,12],"flags":{"allow_interaction":true},"order":1,"mode":0,"inputs":[],"outputs":[],"title":"choose your target size or let 0 by default","properties":{"fontSize":12,"fontFamily":"Arial","fontColor":"#ffffff","textAlign":"left","backgroundColor":"transparent","padding":0,"borderRadius":0,"angle":0},"color":"#fff0","bgcolor":"#fff0"},{"id":16,"type":"CreateVideo","pos":[1481.0190534504716,514.118339998011],"size":[210,78],"flags":{},"order":4,"mode":0,"inputs":[{"localized_name":"images","name":"images","type":"IMAGE","link":58},{"localized_name":"audio","name":"audio","shape":7,"type":"AUDIO","link":59},{"localized_name":"fps","name":"fps","type":"FLOAT","widget":{"name":"fps"},"link":60}],"outputs":[{"localized_name":"VIDEO","name":"VIDEO","type":"VIDEO","links":[15]}],"properties":{"cnr_id":"comfy-core","ver":"0.3.59","Node name for S&R":"CreateVideo"},"widgets_values":[16],"shape":2},{"id":17,"type":"SaveVideo","pos":[1713.7049519429584,515.6718968961251],"size":[704,329.5121951219512],"flags":{},"order":5,"mode":0,"inputs":[{"localized_name":"video","name":"video","type":"VIDEO","link":15},{"localized_name":"filename_prefix","name":"filename_prefix","type":"STRING","widget":{"name":"filename_prefix"},"link":null},{"localized_name":"format","name":"format","type":"COMBO","widget":{"name":"format"},"link":null},{"localized_name":"codec","name":"codec","type":"COMBO","widget":{"name":"codec"},"link":null}],"outputs":[],"properties":{"cnr_id":"comfy-core","ver":"0.3.59","Node name for S&R":"SaveVideo"},"widgets_values":["video/ComfyUI","auto","auto"],"shape":2},{"id":13,"type":"GetVideoComponents","pos":[1025.5227246194358,502.6749169443874],"size":[193.530859375,66],"flags":{"collapsed":false},"order":2,"mode":0,"inputs":[{"localized_name":"video","name":"video","type":"VIDEO","link":11}],"outputs":[{"localized_name":"images","name":"images","type":"IMAGE","links":[55]},{"localized_name":"audio","name":"audio","type":"AUDIO","links":[56]},{"localized_name":"fps","name":"fps","type":"FLOAT","links":[57]}],"properties":{"cnr_id":"comfy-core","ver":"0.3.59","Node name for S&R":"GetVideoComponents"},"widgets_values":[],"shape":2},{"id":12,"type":"LoadVideo","pos":[569.174190262957,501.8096446356975],"size":[378.080078125,186.2731993140244],"flags":{},"order":0,"mode":0,"inputs":[{"localized_name":"file","name":"file","type":"COMBO","widget":{"name":"file"},"link":null},{"localized_name":"choose file to upload","name":"upload","type":"IMAGEUPLOAD","widget":{"name":"upload"},"link":null}],"outputs":[{"localized_name":"VIDEO","name":"VIDEO","type":"VIDEO","links":[11]}],"properties":{"cnr_id":"comfy-core","ver":"0.3.60","Node name for S&R":"LoadVideo"},"widgets_values":["MMaudio_00001-audio.mp4","image"],"shape":2}],"links":[[11,12,0,13,0,"VIDEO"],[15,16,0,17,0,"VIDEO"],[55,13,0,32,0,"IMAGE"],[56,13,1,32,1,"AUDIO"],[57,13,2,32,2,"FLOAT"],[58,32,0,16,0,"IMAGE"],[59,32,1,16,1,"AUDIO"],[60,32,2,16,2,"FLOAT"]],"groups":[],"config":{},"extra":{"ds":{"scale":0.9389783777991051,"offset":[-342.7614907539266,-234.4700157915137]},"workflowRendererVersion":"LG"},"version":0.4} \ No newline at end of file