Initial commit: ComfyUI Video Compressor node

This commit is contained in:
Verolelb
2025-09-26 06:50:54 +02:00
parent 03e9a4ed91
commit c343ee3e1f
8 changed files with 279 additions and 0 deletions
+21
View File
@@ -0,0 +1,21 @@
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.
+92
View File
@@ -0,0 +1,92 @@
\# ComfyUI Video Compressor Node
Un node personnalisé pour ComfyUI qui permet de compresser des fichiers vidéo en utilisant FFmpeg, en visant une taille de fichier cible spécifique.
&nbsp;<!-- Mettez une capture d'écran ici ! -->
\## 🌟 Fonctionnalités
\- \*\*Entrée Vidéo Simple\*\* : Prend en entrée un chemin vers un fichier vidéo (parfait pour être chaîné après un node "Video Combine").
\- \*\*Contrôle de la Taille\*\* : Un slider simple pour définir le poids final souhaité de la vidéo en Mégaoctets (MB).
\- \*\*Choix du Codec\*\* : Choisissez entre `libx264` (H.264, très compatible) et `libx265` (H.265/HEVC, plus efficace).
\- \*\*Sortie Organisée\*\* : Les vidéos compressées sont sauvegardées dans `ComfyUI/output/compressed\_videos/`.
\## ⚙️ Installation
\### 1. Prérequis : FFmpeg
\*\*Ce node nécessite que FFmpeg soit installé sur votre système et accessible depuis le terminal.\*\*
\- \*\*Windows\*\* : Téléchargez depuis \[gyan.dev](https://www.gyan.dev/ffmpeg/builds/) et suivez un tutoriel pour ajouter le dossier `bin` à votre variable d'environnement PATH.
\- \*\*MacOS\*\* : Installez avec Homebrew : `brew install ffmpeg`
\- \*\*Linux\*\* : Utilisez votre gestionnaire de paquets : `sudo apt update \&\& sudo apt install ffmpeg`
Pour vérifier que l'installation a réussi, ouvrez un terminal et tapez `ffmpeg -version`. Vous devriez voir les informations de version s'afficher.
\### 2. Installation du Node
La méthode la plus simple est d'utiliser `git`.
1\. Ouvrez un terminal.
2\. Naviguez jusqu'au répertoire des nodes personnalisés de ComfyUI : `cd ComfyUI/custom\_nodes/`
3\. Clonez ce dépôt : `git clone https://github.com/VOTRE\_NOM\_UTILISATEUR/ComfyUI-VideoCompressor.git` <!-- REMPLACEZ PAR VOTRE URL GITHUB -->
4\. Redémarrez ComfyUI.
Le node devrait maintenant apparaître dans le menu "Add Node" sous la catégorie `🎥Video Utils`.
\## 🚀 Utilisation
1\. Générez une séquence d'images et combinez-les en une vidéo avec un node comme "Video Combine".
2\. Connectez la sortie `video` (le chemin du fichier) de ce node à l'entrée `video` du node `🎬 Compress Video (FFmpeg)`.
3\. Ajustez le slider `target\_mb` à la taille désirée (par ex. 8 MB pour Discord).
4\. Lancez le workflow. La vidéo compressée sera disponible à la sortie `compressed\_video\_path` et sera enregistrée sur votre disque.
\## 📄 Licence
Ce projet est sous licence MIT. Voir le fichier `LICENSE` pour plus de détails.
+14
View File
@@ -0,0 +1,14 @@
# On importe le nom correct de la classe : "VideoCompressorUnified"
from .compress_video_node import VideoCompressorUnified
NODE_CLASS_MAPPINGS = {
# On utilise le même nom ici
"VideoCompressorUnified": VideoCompressorUnified
}
NODE_DISPLAY_NAME_MAPPINGS = {
# Et ici, pour le nom affiché dans le menu
"VideoCompressorUnified": "🎬 Compress Video (Unified)"
}
print("✅ ComfyUI-VideoCompressor: Node unifié chargé.")
Binary file not shown.
Binary file not shown.
+151
View File
@@ -0,0 +1,151 @@
import os
import subprocess
import folder_paths
import torch
import numpy as np
from PIL import Image
import shutil
import datetime
import platform
class VideoCompressorUnified:
"""
Node de compression vidéo unifié :
- Accepte images ou vidéo
- Trois modes : fast_crf, 2pass, gpu
"""
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"mode": (["fast_crf", "2pass", "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"}),
},
"optional": {
"images": ("IMAGE",),
"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"}),
}
}
RETURN_TYPES = ("STRING", "IMAGE")
RETURN_NAMES = ("video_path_out", "images")
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
]
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)
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))
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)
pass_log_file = os.path.join(temp_dir, "ffmpeg_passlog")
try:
if images is not None:
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")
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}")
initial_video_path = video_path_in
output_images = self._load_video_to_tensor(video_path_in, temp_dir)
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.")
# ----- 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
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)
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)
else:
raise ValueError(f"Mode inconnu: {mode}")
return (final_video_path, output_images)
finally:
shutil.rmtree(temp_dir, ignore_errors=True)
File diff suppressed because one or more lines are too long
View File