Initial commit: ComfyUI Video Compressor node
This commit is contained in:
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
|
||||
|
||||
<!-- 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
@@ -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.
@@ -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
Reference in New Issue
Block a user