mirror of
https://github.com/justUmen/Bjornulf_custom_nodes.git
synced 2026-03-21 12:42:11 -03:00
784 lines
45 KiB
Python
784 lines
45 KiB
Python
import os
|
||
import numpy as np
|
||
import torch
|
||
import subprocess
|
||
import json
|
||
from PIL import Image
|
||
import soundfile as sf
|
||
import glob
|
||
import logging
|
||
|
||
class imagesToVideo:
|
||
@classmethod
|
||
def INPUT_TYPES(cls):
|
||
return {
|
||
"required": {
|
||
"images": ("IMAGE",),
|
||
"fps": ("FLOAT", {"default": 24, "min": 1, "max": 120}),
|
||
"name_prefix": ("STRING", {"default": "imgs2video/me"}),
|
||
"use_python_ffmpeg": ("BOOLEAN", {"default": False}),
|
||
},
|
||
"optional": {
|
||
"audio": ("AUDIO",),
|
||
"audio_path": ("STRING", {"forceInput": True}),
|
||
"FFMPEG_CONFIG_JSON": ("STRING", {"forceInput": True}),
|
||
},
|
||
}
|
||
|
||
RETURN_TYPES = ("STRING", "STRING","STRING",)
|
||
RETURN_NAMES = ("comment", "ffmpeg_command", "video_path",)
|
||
FUNCTION = "image_to_video"
|
||
OUTPUT_NODE = True
|
||
CATEGORY = "Bjornulf"
|
||
|
||
def parse_ffmpeg_config(self, config_json):
|
||
if not config_json:
|
||
return None
|
||
try:
|
||
return json.loads(config_json)
|
||
except json.JSONDecodeError:
|
||
print("Error parsing FFmpeg config JSON")
|
||
return None
|
||
|
||
def run_ffmpeg_python(self, ffmpeg_cmd, output_file, ffmpeg_path):
|
||
try:
|
||
import ffmpeg
|
||
except ImportError:
|
||
logging.error("ffmpeg-python library not installed")
|
||
return False, "ffmpeg-python library not installed"
|
||
|
||
try:
|
||
# Find frame rate
|
||
idx_fr = ffmpeg_cmd.index('-framerate')
|
||
fps = ffmpeg_cmd[idx_fr + 1]
|
||
|
||
# Find all input indices
|
||
idx_inputs = [i for i, x in enumerate(ffmpeg_cmd) if x == '-i']
|
||
if not idx_inputs:
|
||
return False, "Error: No input found"
|
||
|
||
# First input is the image sequence
|
||
image_sequence = ffmpeg_cmd[idx_inputs[0] + 1]
|
||
|
||
# Second input (if present) is the audio file
|
||
audio_file = ffmpeg_cmd[idx_inputs[1] + 1] if len(idx_inputs) > 1 else None
|
||
|
||
# Determine position after the last input
|
||
idx_after = idx_inputs[-1] + 2
|
||
|
||
# Check for video filter
|
||
filter_graph = None
|
||
output_options_start = idx_after
|
||
if idx_after < len(ffmpeg_cmd) - 1 and ffmpeg_cmd[idx_after] == '-vf':
|
||
filter_graph = ffmpeg_cmd[idx_after + 1]
|
||
output_options_start = idx_after + 2
|
||
|
||
# Extract output options (everything between last input/filter and output file)
|
||
output_options = ffmpeg_cmd[output_options_start:-1]
|
||
if len(output_options) % 2 != 0:
|
||
return False, "Error: Output options have odd number of elements"
|
||
|
||
# Convert output options to a dictionary, preserving colons
|
||
options = {}
|
||
for i in range(0, len(output_options), 2):
|
||
key = output_options[i].lstrip('-') # Remove '-' but keep ':'
|
||
value = output_options[i + 1]
|
||
options[key] = value
|
||
|
||
# Add filter graph to options if present
|
||
if filter_graph:
|
||
options['vf'] = filter_graph
|
||
|
||
# Create video input
|
||
video_input = ffmpeg.input(image_sequence, framerate=fps)
|
||
video_stream = video_input.video
|
||
|
||
# Create audio input if present
|
||
audio_stream = None
|
||
if audio_file:
|
||
audio_input = ffmpeg.input(audio_file)
|
||
audio_stream = audio_input.audio
|
||
|
||
# Construct output
|
||
if audio_stream:
|
||
output = ffmpeg.output(video_stream, audio_stream, output_file, **options)
|
||
else:
|
||
output = ffmpeg.output(video_stream, output_file, **options)
|
||
|
||
# Execute FFmpeg command
|
||
output.run(cmd=ffmpeg_path, overwrite_output=True)
|
||
logging.debug(f"FFmpeg-python executed successfully for {output_file}")
|
||
return True, "Success"
|
||
|
||
except ffmpeg.Error as e:
|
||
error_message = "Unknown FFmpeg error"
|
||
if hasattr(e, 'stderr') and e.stderr is not None:
|
||
try:
|
||
error_message = e.stderr.decode(errors='replace')
|
||
except Exception as decode_err:
|
||
error_message = f"Could not decode stderr: {decode_err}"
|
||
logging.error(f"FFmpeg-python failed: {error_message}\nCommand: {' '.join(ffmpeg_cmd)}")
|
||
return False, f"FFmpeg error: {error_message}\nCommand: {' '.join(ffmpeg_cmd)}"
|
||
except Exception as e:
|
||
logging.error(f"Unexpected error in FFmpeg-python: {str(e)}")
|
||
return False, f"Error: {str(e)}"
|
||
|
||
def get_next_filename(self, output_base, format="mp4"):
|
||
"""
|
||
Determines the next filename in a sequence with 4-digit numbering (0001, 0002, etc.).
|
||
|
||
Args:
|
||
output_base (str): The base path and prefix (e.g., 'output/imgs2video/me')
|
||
format (str): The file extension (e.g., 'mp4')
|
||
|
||
Returns:
|
||
str: The next filename in the sequence (e.g., 'output/imgs2video/me_0001.mp4')
|
||
"""
|
||
# Ensure output_base is clean
|
||
output_base = output_base.rstrip(os.sep)
|
||
|
||
# Pattern to match files with 4-digit numbers: e.g., 'output/imgs2video/me_0001.mp4'
|
||
pattern = f"{output_base}_[0-9][0-9][0-9][0-9].{format}"
|
||
|
||
# Get all files matching the pattern
|
||
existing_files = glob.glob(pattern)
|
||
|
||
# Extract numbers from filenames
|
||
numbers = []
|
||
for filepath in existing_files:
|
||
# Get the filename from the full path
|
||
filename = os.path.basename(filepath)
|
||
# Extract the 4-digit number between '_' and '.'
|
||
number_part = filename.split('_')[-1].split('.')[0]
|
||
# Verify it's a 4-digit number
|
||
if number_part.isdigit() and len(number_part) == 4:
|
||
numbers.append(int(number_part))
|
||
|
||
# Determine the next number
|
||
if numbers:
|
||
next_num = max(numbers) + 1
|
||
else:
|
||
next_num = 1
|
||
|
||
# Construct the next filename with zero-padding to 4 digits
|
||
next_filename = f"{output_base}_{next_num:04d}.{format}"
|
||
|
||
return next_filename
|
||
|
||
def image_to_video(self, images, fps, name_prefix, use_python_ffmpeg=False, audio=None, audio_path=None, FFMPEG_CONFIG_JSON=None):
|
||
ffmpeg_config = self.parse_ffmpeg_config(FFMPEG_CONFIG_JSON)
|
||
|
||
format = "mp4"
|
||
if ffmpeg_config and ffmpeg_config["output"]["container_format"] != "None":
|
||
format = ffmpeg_config["output"]["container_format"]
|
||
|
||
# Remove any extension from name_prefix and create output_base
|
||
name_prefix = os.path.splitext(name_prefix)[0]
|
||
output_base = os.path.join("output", name_prefix)
|
||
|
||
# Get the next filename using the corrected function
|
||
output_file = self.get_next_filename(output_base, format)
|
||
|
||
# Clean up and prepare temporary directory
|
||
temp_dir = os.path.join("Bjornulf", "temp_images_imgs2video") # Use os.path.join for cross-platform compatibility
|
||
if os.path.exists(temp_dir) and os.path.isdir(temp_dir):
|
||
for file in os.listdir(temp_dir):
|
||
os.remove(os.path.join(temp_dir, file))
|
||
os.rmdir(temp_dir)
|
||
|
||
# Create necessary directories
|
||
os.makedirs(temp_dir, exist_ok=True)
|
||
os.makedirs(os.path.dirname(output_file) if os.path.dirname(output_file) else ".", exist_ok=True)
|
||
|
||
for i, img_tensor in enumerate(images):
|
||
img = Image.fromarray((img_tensor.cpu().numpy() * 255).astype(np.uint8))
|
||
if format == "webm":
|
||
img = img.convert("RGBA")
|
||
img.save(os.path.join(temp_dir, f"frame_{i:04d}.png"))
|
||
|
||
# Handle audio from either AUDIO type or audio_path
|
||
temp_audio_file = None
|
||
# Always use audio if either audio or audio_path is provided
|
||
# logging.info(f"audio : {audio}")
|
||
# logging.info(f"audio_path : {audio_path}")
|
||
audio_enabled = (audio is not None) or (audio_path is not None and os.path.exists(audio_path))
|
||
# logging.info(f"audio_enabled : {audio_enabled}")
|
||
|
||
if audio_enabled:
|
||
if audio is not None:
|
||
# Process AUDIO type input
|
||
temp_audio_file = os.path.join(temp_dir, "temp_audio.wav")
|
||
waveform = audio['waveform'].squeeze().numpy()
|
||
sample_rate = audio['sample_rate']
|
||
sf.write(temp_audio_file, waveform, sample_rate)
|
||
elif audio_path and os.path.exists(audio_path):
|
||
# Use provided audio path directly
|
||
temp_audio_file = audio_path
|
||
|
||
ffmpeg_path = "ffmpeg"
|
||
if ffmpeg_config and ffmpeg_config["ffmpeg"]["path"]:
|
||
ffmpeg_path = ffmpeg_config["ffmpeg"]["path"]
|
||
|
||
ffmpeg_cmd = [
|
||
ffmpeg_path,
|
||
"-y",
|
||
"-framerate", str(fps),
|
||
"-i", os.path.join(temp_dir, "frame_%04d.png"),
|
||
]
|
||
|
||
# logging.info(f"temp_audio_file : {temp_audio_file}")
|
||
if temp_audio_file:
|
||
ffmpeg_cmd.extend(["-i", temp_audio_file])
|
||
|
||
if ffmpeg_config and format == "webm" and ffmpeg_config["video"]["force_transparency_webm"]:
|
||
ffmpeg_cmd.extend([
|
||
"-vf", "scale=iw:ih,format=rgba,split[s0][s1];[s0]lutrgb=r=0:g=0:b=0:a=0[transparent];[transparent][s1]overlay"
|
||
])
|
||
|
||
if ffmpeg_config:
|
||
if ffmpeg_config["video"]["codec"] != "None":
|
||
ffmpeg_cmd.extend(["-c:v", ffmpeg_config["video"]["codec"]])
|
||
|
||
if ffmpeg_config["video"]["preset"] != "None":
|
||
ffmpeg_cmd.extend(["-preset", ffmpeg_config["video"]["preset"]])
|
||
|
||
if ffmpeg_config["video"]["bitrate"]:
|
||
ffmpeg_cmd.extend(["-b:v", ffmpeg_config["video"]["bitrate"]])
|
||
|
||
if ffmpeg_config["video"]["crf"]:
|
||
if "nvenc" in (ffmpeg_config["video"]["codec"] or ""):
|
||
ffmpeg_cmd.extend(["-cq", str(ffmpeg_config["video"]["crf"])])
|
||
else:
|
||
ffmpeg_cmd.extend(["-crf", str(ffmpeg_config["video"]["crf"])])
|
||
|
||
if ffmpeg_config["video"]["pixel_format"] != "None":
|
||
ffmpeg_cmd.extend(["-pix_fmt", ffmpeg_config["video"]["pixel_format"]])
|
||
|
||
if ffmpeg_config["video"]["resolution"]:
|
||
scale_filter = f"scale={ffmpeg_config['video']['resolution']['width']}:{ffmpeg_config['video']['resolution']['height']}"
|
||
if format == "webm" and ffmpeg_config["video"]["force_transparency_webm"]:
|
||
current_filter_idx = ffmpeg_cmd.index("-vf") + 1
|
||
current_filter = ffmpeg_cmd[current_filter_idx]
|
||
ffmpeg_cmd[current_filter_idx] = scale_filter + "," + current_filter
|
||
else:
|
||
ffmpeg_cmd.extend(["-vf", scale_filter])
|
||
|
||
if ffmpeg_config["video"]["fps"]["enabled"]:
|
||
ffmpeg_cmd.extend(["-r", str(ffmpeg_config["video"]["fps"]["force_fps"])])
|
||
|
||
# if not ffmpeg_config["audio"]["enabled"]:
|
||
# ffmpeg_cmd.extend(["-an"])
|
||
# elif ffmpeg_config["audio"]["codec"] != "None" and temp_audio_file:
|
||
# ffmpeg_config["audio"]["codec"] != "None" and
|
||
#Need codec ????
|
||
if temp_audio_file:
|
||
# Check if we have ffmpeg_config with audio codec settings
|
||
if ffmpeg_config and "audio" in ffmpeg_config and ffmpeg_config["audio"]["codec"] != "None":
|
||
ffmpeg_cmd.extend(["-c:a", ffmpeg_config["audio"]["codec"]])
|
||
if "bitrate" in ffmpeg_config["audio"] and ffmpeg_config["audio"]["bitrate"]:
|
||
ffmpeg_cmd.extend(["-b:a", ffmpeg_config["audio"]["bitrate"]])
|
||
else:
|
||
# Use default audio codec based on format if no specific codec is set
|
||
if format == "mp4":
|
||
ffmpeg_cmd.extend(["-c:a", "aac"])
|
||
elif format == "webm":
|
||
ffmpeg_cmd.extend(["-c:a", "libvorbis"])
|
||
else:
|
||
ffmpeg_cmd.extend(["-an"]) # No audio
|
||
else:
|
||
if format == "mp4":
|
||
ffmpeg_cmd.extend([
|
||
"-c:v", "libx264",
|
||
"-preset", "medium",
|
||
"-crf", "19",
|
||
"-pix_fmt", "yuv420p"
|
||
])
|
||
if temp_audio_file:
|
||
ffmpeg_cmd.extend(["-c:a", "aac"])
|
||
elif format == "webm":
|
||
ffmpeg_cmd.extend([
|
||
"-c:v", "libvpx-vp9",
|
||
"-crf", "19",
|
||
# "-b:v", "0",
|
||
"-pix_fmt", "yuva420p"
|
||
])
|
||
if temp_audio_file:
|
||
ffmpeg_cmd.extend(["-c:a", "libvorbis"])
|
||
|
||
ffmpeg_cmd.append(output_file)
|
||
|
||
try:
|
||
if use_python_ffmpeg:
|
||
success, message = self.run_ffmpeg_python(ffmpeg_cmd, output_file, ffmpeg_path)
|
||
comment = f"Python FFmpeg: {message}" if not success else f"Video created successfully with {'custom' if ffmpeg_config else 'default'} settings (Python FFmpeg)"
|
||
else:
|
||
subprocess.run(ffmpeg_cmd, check=True)
|
||
comment = f"Video created successfully with {'custom' if ffmpeg_config else 'default'} FFmpeg settings"
|
||
|
||
print(f"Video created successfully: {output_file}")
|
||
except subprocess.CalledProcessError as e:
|
||
print(f"Error creating video: {e}")
|
||
comment = f"Error creating video: {e}"
|
||
finally:
|
||
# Only remove temp_audio_file if it was created here (not if it's an external path)
|
||
if temp_audio_file and audio_path != temp_audio_file:
|
||
print("Temporary files not removed for debugging purposes.")
|
||
|
||
# Generate configuration report
|
||
comment_lines = []
|
||
comment_lines.append("📽 Video Generation Configuration Report 📽\n")
|
||
|
||
# Quick format overview based on selected format
|
||
if format.lower() == "mp4":
|
||
comment_lines.append("MP4 FORMAT OVERVIEW:")
|
||
comment_lines.append("✅ Advantages: Universal compatibility, excellent streaming support")
|
||
comment_lines.append("❌ Drawbacks: No transparency support, less efficient than newer formats")
|
||
comment_lines.append("🏆 Best for: General distribution, web streaming, maximum device compatibility\n")
|
||
elif format.lower() == "webm":
|
||
comment_lines.append("WEBM FORMAT OVERVIEW:")
|
||
comment_lines.append("✅ Advantages: Better compression efficiency, transparency support, open format")
|
||
comment_lines.append("❌ Drawbacks: Limited compatibility on older devices/iOS, slower encoding")
|
||
comment_lines.append("🏆 Best for: Web delivery, animations with transparency, modern browsers\n")
|
||
elif format.lower() == "mov":
|
||
comment_lines.append("MOV FORMAT OVERVIEW:")
|
||
comment_lines.append("✅ Advantages: Professional codec support, good for editing workflows, Apple ecosystem")
|
||
comment_lines.append("❌ Drawbacks: Larger file sizes, less web-friendly")
|
||
comment_lines.append("🏆 Best for: Professional workflows, Mac/iOS delivery, intermediate editing files\n")
|
||
elif format.lower() == "mkv":
|
||
comment_lines.append("MKV FORMAT OVERVIEW:")
|
||
comment_lines.append("✅ Advantages: Superior flexibility, supports all codecs, multiple audio/subtitle tracks")
|
||
comment_lines.append("❌ Drawbacks: Not viewable in browsers, limited device support")
|
||
comment_lines.append("🏆 Best for: Archiving, local playback, advanced feature support\n")
|
||
elif format.lower() == "gif":
|
||
comment_lines.append("GIF FORMAT OVERVIEW:")
|
||
comment_lines.append("✅ Advantages: Universal compatibility, simple animation support")
|
||
comment_lines.append("❌ Drawbacks: Extremely inefficient compression, limited to 256 colors, no audio")
|
||
comment_lines.append("🏆 Best for: Simple animations, maximum compatibility\n")
|
||
|
||
# Basic parameters section
|
||
comment_lines.append("=== Core Parameters ===")
|
||
comment_lines.append(f"• FPS: {fps} ({24 if fps == 24 else 'custom'} fps)")
|
||
if fps == 24:
|
||
comment_lines.append(" ℹ️ 24 fps is the cinema standard, offering a classic film look")
|
||
elif fps == 30:
|
||
comment_lines.append(" ℹ️ 30 fps provides smoother motion for general video content")
|
||
elif fps == 60:
|
||
comment_lines.append(" ℹ️ 60 fps delivers very smooth motion ideal for gaming/sports")
|
||
elif fps > 60:
|
||
comment_lines.append(" ℹ️ High frame rate (>60 fps) used for slow-motion effects")
|
||
comment_lines.append(f" 📊 Valid range: 1-120 fps (Higher values increase file size significantly)")
|
||
|
||
comment_lines.append(f"• Output Naming: '{name_prefix}'")
|
||
comment_lines.append(f" 📁 Full path: {output_file}")
|
||
|
||
comment_lines.append(f"• Execution Mode: {'Python ffmpeg' if use_python_ffmpeg else 'System FFmpeg'}")
|
||
if use_python_ffmpeg:
|
||
comment_lines.append(" ℹ️ Python FFmpeg: Integrated library approach with cleaner error handling")
|
||
comment_lines.append(" ⚠️ May have fewer codec options than system FFmpeg")
|
||
comment_lines.append(" 💡 For next improvement: Switch to system FFmpeg for access to more codecs and options")
|
||
else:
|
||
comment_lines.append(" ℹ️ System FFmpeg: Direct shell access with full codec/options support")
|
||
comment_lines.append(" ⚠️ Requires FFmpeg to be installed and in system PATH")
|
||
|
||
# Video configuration section
|
||
comment_lines.append("\n=== Video Encoding Configuration ===")
|
||
if ffmpeg_config:
|
||
comment_lines.append("🔧 Custom Configuration Active")
|
||
v = ffmpeg_config.get('video', {})
|
||
|
||
default_codec = "libx264"
|
||
# Codec information
|
||
if format.lower() == "webm":
|
||
default_codec = "libvpx-vp9"
|
||
|
||
codec = v.get('codec', default_codec)
|
||
comment_lines.append(f"• Codec: {codec}")
|
||
|
||
if "264" in codec:
|
||
comment_lines.append(" ℹ️ H.264/AVC: Universal compatibility, good balance of quality and size")
|
||
comment_lines.append(" ⭐ Quality: 8/10 | Compatibility: 10/10 | Encoding Speed: 7/10")
|
||
comment_lines.append(" 💡 For next improvement: Consider H.265/HEVC for 20-30% better compression at same quality")
|
||
elif "265" in codec in codec.lower():
|
||
comment_lines.append(" ℹ️ H.265/HEVC: Better compression than H.264, but slower encoding")
|
||
comment_lines.append(" ⭐ Quality: 9/10 | Compatibility: 6/10 | Encoding Speed: 5/10")
|
||
comment_lines.append(" ⚠️ Limited browser/device support, best for archiving")
|
||
comment_lines.append(" 💡 For next improvement: Try AV1 for even better compression")
|
||
elif "vp9" in codec.lower():
|
||
comment_lines.append(" ℹ️ VP9: Google's open codec with excellent quality-to-size ratio")
|
||
comment_lines.append(" ⭐ Quality: 9/10 | Compatibility: 7/10 | Encoding Speed: 4/10")
|
||
comment_lines.append(" ✅ Good support in modern browsers, especially Chrome")
|
||
comment_lines.append(" 💡 For next improvement: Consider AV1 for 20% better compression or faster encoding preset")
|
||
elif "av1" in codec.lower():
|
||
comment_lines.append(" ℹ️ AV1: Next-gen open codec with superior compression")
|
||
comment_lines.append(" ⭐ Quality: 10/10 | Compatibility: 5/10 | Encoding Speed: 2/10")
|
||
comment_lines.append(" ⚠️ Very slow encoding, requires modern hardware")
|
||
comment_lines.append(" 💡 For next improvement: Use SVT-AV1 encoder for faster processing")
|
||
|
||
# Quality parameters
|
||
crf = v.get('crf', 19)
|
||
comment_lines.append(f"• Quality: CRF {crf}")
|
||
|
||
if crf != 'N/A':
|
||
if 0 <= int(crf) <= 14:
|
||
comment_lines.append(" ℹ️ Very High Quality (CRF 0-14): Nearly lossless, very large files")
|
||
comment_lines.append(" ⭐ Visual Quality: 9-10/10 | File Size: Very Large")
|
||
elif 15 <= int(crf) <= 19:
|
||
comment_lines.append(" ℹ️ High Quality (CRF 15-19): Visually transparent, good for archiving")
|
||
comment_lines.append(" ⭐ Visual Quality: 8-9/10 | File Size: Large")
|
||
comment_lines.append(" 💡 For next improvement: Lower CRF to 17 for even better quality")
|
||
elif 20 <= int(crf) <= 24:
|
||
comment_lines.append(" ℹ️ Balanced Quality (CRF 20-24): Good for general distribution")
|
||
comment_lines.append(" ⭐ Visual Quality: 7-8/10 | File Size: Moderate")
|
||
comment_lines.append(" 💡 For next improvement: Lower CRF to 18 for higher quality or switch to H.265 at same CRF")
|
||
elif 25 <= int(crf) <= 30:
|
||
comment_lines.append(" ℹ️ Reduced Quality (CRF 25-30): Noticeable compression artifacts")
|
||
comment_lines.append(" ⭐ Visual Quality: 5-6/10 | File Size: Small")
|
||
comment_lines.append(" 💡 For next improvement: Lower CRF to 22 for better quality-size balance")
|
||
else:
|
||
comment_lines.append(" ℹ️ Low Quality (CRF 31+): Heavy compression, significant artifacts")
|
||
comment_lines.append(" ⭐ Visual Quality: <5/10 | File Size: Very Small")
|
||
comment_lines.append(" 💡 For next improvement: Use CRF 28 for better quality with minimal size increase")
|
||
|
||
comment_lines.append(" ⚠️ Cannot combine CRF with static bitrate settings")
|
||
|
||
# Encoding speed/preset
|
||
preset = v.get('preset', 'medium')
|
||
comment_lines.append(f"• Performance: {preset} preset")
|
||
|
||
if preset == 'ultrafast':
|
||
comment_lines.append(" ℹ️ Ultrafast: Maximum encoding speed, largest file size")
|
||
comment_lines.append(" ⏱️ Speed: 10/10 | Efficiency: 3/10 | Use case: Live streaming")
|
||
comment_lines.append(" 💡 For next improvement: Try 'superfast' for 30% better compression with minimal speed loss")
|
||
elif preset == 'superfast' or preset == 'veryfast':
|
||
comment_lines.append(" ℹ️ Very Fast: Quick encoding, larger file sizes")
|
||
comment_lines.append(" ⏱️ Speed: 8-9/10 | Efficiency: 4-5/10 | Use case: Quick exports")
|
||
comment_lines.append(" 💡 For next improvement: Try 'slower' preset for better compression")
|
||
elif preset == 'faster' or preset == 'fast':
|
||
comment_lines.append(" ℹ️ Fast: Good balance of speed and compression")
|
||
comment_lines.append(" ⏱️ Speed: 6-7/10 | Efficiency: 6-7/10 | Use case: General purpose")
|
||
comment_lines.append(" 💡 For next improvement: Consider 'veryslow' preset for better compression")
|
||
elif preset == 'medium':
|
||
comment_lines.append(" ℹ️ Medium: Default preset, balanced speed/compression")
|
||
comment_lines.append(" ⏱️ Speed: 5/10 | Efficiency: 7/10 | Use case: Standard encoding")
|
||
comment_lines.append(" 💡 For next improvement: Try 'veryslow' preset for 15-20% better compression")
|
||
elif preset == 'slow' or preset == 'slower':
|
||
comment_lines.append(" ℹ️ Slow: Better compression, slower encoding")
|
||
comment_lines.append(" ⏱️ Speed: 3-4/10 | Efficiency: 8-9/10 | Use case: Distribution/archiving")
|
||
comment_lines.append(" 💡 For next improvement: Try 'veryslow' for archival quality or reduce CRF slightly")
|
||
elif preset == 'veryslow' or preset == 'placebo':
|
||
comment_lines.append(" ℹ️ Very Slow: Maximum compression, extremely slow encoding")
|
||
comment_lines.append(" ⏱️ Speed: 1-2/10 | Efficiency: 9-10/10 | Use case: Final archiving")
|
||
|
||
# Bitrate information
|
||
bitrate = v.get('bitrate', 'Auto')
|
||
comment_lines.append(f"• Bitrate: {bitrate}")
|
||
|
||
if bitrate == 'Auto':
|
||
comment_lines.append(" ℹ️ Auto Bitrate: Determined by CRF value (recommended)")
|
||
else:
|
||
comment_lines.append(f" ℹ️ Fixed Bitrate: {bitrate}")
|
||
comment_lines.append(" ⚠️ Fixed bitrate overrides quality-based settings (CRF)")
|
||
|
||
# Rough bitrate quality indicators
|
||
if isinstance(bitrate, str):
|
||
bitrate_value = int(''.join(filter(str.isdigit, bitrate)))
|
||
if 'k' in bitrate.lower():
|
||
bitrate_value *= 1000
|
||
|
||
if bitrate_value < 1000000:
|
||
comment_lines.append(" ⭐ Quality: Low (< 1 Mbps) - Suitable for mobile/web previews")
|
||
comment_lines.append(" 💡 For next improvement: Increase to at least 2-3 Mbps for SD content")
|
||
elif 1000000 <= bitrate_value < 5000000:
|
||
comment_lines.append(" ⭐ Quality: Medium (1-5 Mbps) - Standard web video")
|
||
comment_lines.append(" 💡 For next improvement: Use 5-8 Mbps for higher quality HD content")
|
||
elif 5000000 <= bitrate_value < 10000000:
|
||
comment_lines.append(" ⭐ Quality: High (5-10 Mbps) - HD streaming")
|
||
comment_lines.append(" 💡 For next improvement: Consider two-pass encoding for consistent quality")
|
||
elif 10000000 <= bitrate_value < 20000000:
|
||
comment_lines.append(" ⭐ Quality: Very High (10-20 Mbps) - Full HD premium content")
|
||
comment_lines.append(" 💡 For next improvement: Switch to CRF-based encoding for more efficient sizing")
|
||
else:
|
||
comment_lines.append(" ⭐ Quality: Ultra High (20+ Mbps) - 4K/professional use")
|
||
|
||
# Pixel format details
|
||
pixel_format = v.get('pixel_format', 'yuv420p/yuva420p')
|
||
comment_lines.append(f"• Pixel Format: {pixel_format}")
|
||
|
||
if '420' in pixel_format:
|
||
comment_lines.append(" ℹ️ YUV 4:2:0: Standard chroma subsampling, best compatibility")
|
||
comment_lines.append(" ✅ Recommended for most content")
|
||
comment_lines.append(" 💡 For next improvement: Consider 4:2:2 for professional content or chroma keying")
|
||
elif '422' in pixel_format:
|
||
comment_lines.append(" ℹ️ YUV 4:2:2: Better color accuracy, larger files")
|
||
comment_lines.append(" ✅ Good for professional content/chroma keying")
|
||
comment_lines.append(" 💡 For next improvement: Use 4:4:4 for graphic design work or precision color grading")
|
||
elif '444' in pixel_format:
|
||
comment_lines.append(" ℹ️ YUV 4:4:4: Full chroma resolution, largest files")
|
||
comment_lines.append(" ✅ Best for high-end professional work")
|
||
|
||
if 'a' in pixel_format:
|
||
comment_lines.append(" ℹ️ Alpha channel support active (transparency)")
|
||
comment_lines.append(" ⚠️ Only supported in WebM (VP8/VP9) and some MOV containers")
|
||
comment_lines.append(" 💡 For next improvement: Use VP9 for better quality transparency")
|
||
|
||
# Resolution information
|
||
if v.get('resolution'):
|
||
width = v['resolution']['width']
|
||
height = v['resolution']['height']
|
||
comment_lines.append(f"• Resolution: {width}x{height}")
|
||
|
||
# Add resolution category information
|
||
if width >= 3840 or height >= 2160:
|
||
comment_lines.append(" ℹ️ 4K Ultra HD (3840×2160 or higher)")
|
||
comment_lines.append(" ⚠️ Very large files, may require powerful hardware to play")
|
||
comment_lines.append(" 💡 For next improvement: Try 1440p (2560×1440) for better balance of quality and size")
|
||
elif width >= 1920 or height >= 1080:
|
||
comment_lines.append(" ℹ️ Full HD (1920×1080)")
|
||
comment_lines.append(" ✅ Standard for high-quality video")
|
||
comment_lines.append(" 💡 For next improvement: Consider H.265/HEVC codec for better compression at this resolution")
|
||
elif width >= 1280 or height >= 720:
|
||
comment_lines.append(" ℹ️ HD (1280×720)")
|
||
comment_lines.append(" ✅ Good balance of quality and file size")
|
||
comment_lines.append(" 💡 For next improvement: Upgrade to 1080p for higher quality or lower CRF")
|
||
elif width >= 854 or height >= 480:
|
||
comment_lines.append(" ℹ️ SD (854×480 or similar)")
|
||
comment_lines.append(" ✅ Suitable for mobile devices or low bandwidth")
|
||
comment_lines.append(" 💡 For next improvement: Increase to 720p for better viewing experience")
|
||
else:
|
||
comment_lines.append(" ℹ️ Low Resolution (< 480p)")
|
||
comment_lines.append(" ⚠️ May appear pixelated on modern displays")
|
||
comment_lines.append(" 💡 For next improvement: Increase to at least 480p for acceptable quality")
|
||
|
||
# Container format detailed information
|
||
comment_lines.append(f"• Container: {format.upper()}")
|
||
else:
|
||
comment_lines.append(f"🔄 Default {format.upper()} Configuration:")
|
||
comment_lines.append("• Codec: " + ("libx264" if format == "mp4" else "libvpx-vp9"))
|
||
comment_lines.append("• CRF: 19")
|
||
comment_lines.append("• Preset: medium" + (" (slow for VP9)" if format == "webm" else ""))
|
||
comment_lines.append(" 💡 For next improvement: Lower CRF to 16-18 for better quality")
|
||
|
||
# Container format information
|
||
comment_lines.append("\n=== Container Format Details ===")
|
||
if format.lower() == "mp4":
|
||
comment_lines.append("• MP4 (.mp4)")
|
||
comment_lines.append(" ℹ️ Universal compatibility with nearly all devices and platforms")
|
||
comment_lines.append(" ✅ Excellent for web, mobile, and general distribution")
|
||
comment_lines.append(" ✅ Supports H.264, H.265, AAC audio")
|
||
comment_lines.append(" ❌ Limited support for transparency")
|
||
comment_lines.append(" ⭐ Compatibility: 10/10 | Flexibility: 7/10")
|
||
comment_lines.append(" 💡 For next improvement: Consider H.265 in MP4 for 30% smaller files")
|
||
elif format.lower() == "webm":
|
||
comment_lines.append("• WebM (.webm)")
|
||
comment_lines.append(" ℹ️ Open format optimized for web delivery")
|
||
comment_lines.append(" ✅ Excellent support in modern browsers")
|
||
comment_lines.append(" ✅ Native support for transparency (alpha channel)")
|
||
comment_lines.append(" ✅ Supports VP8/VP9 video, Vorbis/Opus audio")
|
||
comment_lines.append(" ❌ Limited support on older devices/iOS")
|
||
comment_lines.append(" ⭐ Compatibility: 7/10 | Web Performance: 9/10")
|
||
comment_lines.append(" 💡 For next improvement: Try AV1 in WebM for better quality/size ratio")
|
||
elif format.lower() == "mov":
|
||
comment_lines.append("• QuickTime (.mov)")
|
||
comment_lines.append(" ℹ️ Apple's native container format")
|
||
comment_lines.append(" ✅ Excellent for macOS/iOS ecosystem")
|
||
comment_lines.append(" ✅ Good support for professional codecs (ProRes, DNxHD)")
|
||
comment_lines.append(" ✅ Can support transparency")
|
||
comment_lines.append(" ❌ Less compatible outside Apple ecosystem")
|
||
comment_lines.append(" ⭐ Compatibility: 6/10 | Professional Use: 8/10")
|
||
comment_lines.append(" 💡 For next improvement: Use ProRes 422 for editing workflows or H.264 for delivery")
|
||
elif format.lower() == "mkv":
|
||
comment_lines.append("• Matroska (.mkv)")
|
||
comment_lines.append(" ℹ️ Highly flexible open container format")
|
||
comment_lines.append(" ✅ Supports virtually all codecs and features")
|
||
comment_lines.append(" ✅ Excellent for archiving and local playback")
|
||
comment_lines.append(" ✅ Supports multiple audio/subtitle tracks")
|
||
comment_lines.append(" ❌ Not natively supported in browsers or some devices")
|
||
comment_lines.append(" ⚠️ Cannot be viewed directly in web browsers")
|
||
comment_lines.append(" ⭐ Compatibility: 5/10 | Flexibility: 10/10")
|
||
comment_lines.append(" 💡 For next improvement: Use H.265 or AV1 codec inside MKV for best archival quality")
|
||
elif format.lower() == "gif":
|
||
comment_lines.append("• GIF (.gif)")
|
||
comment_lines.append(" ℹ️ Simple animated image format")
|
||
comment_lines.append(" ✅ Universal compatibility across all platforms")
|
||
comment_lines.append(" ✅ Supports basic transparency")
|
||
comment_lines.append(" ❌ Limited to 256 colors, no audio")
|
||
comment_lines.append(" ❌ Very inefficient compression (large files)")
|
||
comment_lines.append(" ⭐ Compatibility: 10/10 | Quality: 2/10")
|
||
comment_lines.append(" 💡 For next improvement: Use WebM or MP4 with autoplay for much better quality/size")
|
||
|
||
# Audio configuration section
|
||
comment_lines.append("\n=== Audio Configuration ===")
|
||
if audio_enabled:
|
||
comment_lines.append(f"• Audio Source: {'Direct input' if audio else 'External file'}")
|
||
if ffmpeg_config and ffmpeg_config.get('audio'):
|
||
a = ffmpeg_config['audio']
|
||
codec = a.get('codec', 'AAC/Vorbis')
|
||
comment_lines.append(f"• Codec: {codec}")
|
||
|
||
if 'aac' in codec.lower():
|
||
comment_lines.append(" ℹ️ AAC: High-quality lossy compression, excellent compatibility")
|
||
comment_lines.append(" ⭐ Quality: 8/10 | Compatibility: 10/10")
|
||
comment_lines.append(" 💡 For next improvement: Use higher bitrate (192+ kbps) or switch to Opus for better quality")
|
||
elif 'opus' in codec.lower():
|
||
comment_lines.append(" ℹ️ Opus: Modern codec with superior quality at low bitrates")
|
||
comment_lines.append(" ⭐ Quality: 9/10 | Compatibility: 7/10")
|
||
comment_lines.append(" 💡 For next improvement: Fine-tune VBR settings or increase bitrate by 10-20%")
|
||
elif 'vorbis' in codec.lower():
|
||
comment_lines.append(" ℹ️ Vorbis: Open audio codec, good quality-to-size ratio")
|
||
comment_lines.append(" ⭐ Quality: 7/10 | Compatibility: 8/10")
|
||
comment_lines.append(" 💡 For next improvement: Switch to Opus for better quality at same bitrate")
|
||
elif 'mp3' in codec.lower():
|
||
comment_lines.append(" ℹ️ MP3: Widely compatible but older codec technology")
|
||
comment_lines.append(" ⭐ Quality: 6/10 | Compatibility: 10/10")
|
||
comment_lines.append(" 💡 For next improvement: Switch to AAC for better quality at same bitrate")
|
||
elif 'flac' in codec.lower() or 'alac' in codec.lower():
|
||
comment_lines.append(" ℹ️ FLAC/ALAC: Lossless audio compression")
|
||
comment_lines.append(" ⭐ Quality: 10/10 | Compatibility: 6/10 | File Size: Large")
|
||
|
||
bitrate = a.get('bitrate', 'Default')
|
||
comment_lines.append(f"• Bitrate: {bitrate}")
|
||
|
||
# Audio bitrate quality indicators
|
||
if bitrate != 'Default':
|
||
if isinstance(bitrate, str):
|
||
bitrate_value = int(''.join(filter(str.isdigit, bitrate)))
|
||
if 'k' in bitrate.lower():
|
||
bitrate_value *= 1000
|
||
|
||
if bitrate_value < 96000:
|
||
comment_lines.append(" ℹ️ Low Bitrate (<96 kbps): Basic audio quality")
|
||
comment_lines.append(" ⭐ Quality: 4/10 | Use case: Voice/basic audio")
|
||
comment_lines.append(" 💡 For next improvement: Increase to at least 128 kbps for music or 96 kbps for speech")
|
||
elif 96000 <= bitrate_value < 128000:
|
||
comment_lines.append(" ℹ️ Standard Bitrate (96-128 kbps): Acceptable quality")
|
||
comment_lines.append(" ⭐ Quality: 6/10 | Use case: General purpose")
|
||
comment_lines.append(" 💡 For next improvement: Use 160-192 kbps for better music quality")
|
||
elif 128000 <= bitrate_value < 192000:
|
||
comment_lines.append(" ℹ️ Good Bitrate (128-192 kbps): Good quality")
|
||
comment_lines.append(" ⭐ Quality: 7/10 | Use case: Music/general media")
|
||
comment_lines.append(" 💡 For next improvement: Use 192-256 kbps for higher quality music")
|
||
elif 192000 <= bitrate_value < 256000:
|
||
comment_lines.append(" ℹ️ High Bitrate (192-256 kbps): Near transparent")
|
||
comment_lines.append(" ⭐ Quality: 8/10 | Use case: Music distribution")
|
||
comment_lines.append(" 💡 For next improvement: Consider VBR encoding for more efficient size/quality")
|
||
else:
|
||
comment_lines.append(" ℹ️ Very High Bitrate (256+ kbps): Transparent quality")
|
||
comment_lines.append(" ⭐ Quality: 9-10/10 | Use case: Archiving/professional")
|
||
else:
|
||
comment_lines.append(" ℹ️ Default bitrate selected based on codec")
|
||
comment_lines.append(" ✅ Typically 128-192 kbps for lossy formats")
|
||
comment_lines.append(" 💡 For next improvement: Specify 192-256 kbps for music content")
|
||
else:
|
||
comment_lines.append("• Codec: " + ("AAC" if format == "mp4" else "Vorbis"))
|
||
if format == "mp4":
|
||
comment_lines.append(" ℹ️ AAC: Standard audio codec for MP4 with excellent quality")
|
||
comment_lines.append(" ⭐ Quality: 8/10 at default bitrate (128-192 kbps)")
|
||
comment_lines.append(" 💡 For next improvement: Set explicit bitrate of 192 kbps for better quality")
|
||
else:
|
||
comment_lines.append(" ℹ️ Vorbis: Open audio codec with good compression efficiency")
|
||
comment_lines.append(" ⭐ Quality: 7/10 at default bitrate (128 kbps)")
|
||
comment_lines.append(" 💡 For next improvement: Switch to Opus codec for better quality at same bitrate")
|
||
else:
|
||
comment_lines.append("• Audio: Disabled")
|
||
comment_lines.append(" ℹ️ No audio track will be included in the output file")
|
||
comment_lines.append(" ✅ Results in smaller file size")
|
||
comment_lines.append(" 💡 For next improvement: Add audio if applicable to content")
|
||
|
||
# Advanced features with detailed explanations
|
||
comment_lines.append("\n=== Advanced Features ===")
|
||
|
||
# Transparency handling
|
||
transparency_enabled = format == 'webm' and ffmpeg_config and ffmpeg_config['video'].get('force_transparency_webm', False)
|
||
comment_lines.append(f"• Transparency Handling: {'Enabled' if transparency_enabled else 'Disabled'}")
|
||
|
||
if transparency_enabled:
|
||
comment_lines.append(" ℹ️ Alpha channel (transparency) will be preserved")
|
||
comment_lines.append(" ✅ WebM with VP9 codec provides excellent transparency support")
|
||
comment_lines.append(" ⚠️ Requires 'yuva420p' pixel format")
|
||
comment_lines.append(" ⚠️ Increases file size by approximately 33%")
|
||
comment_lines.append(" 💡 For next improvement: Ensure original content has high-quality alpha channel")
|
||
else:
|
||
if format == 'webm':
|
||
comment_lines.append(" ℹ️ Transparency can be enabled for WebM format")
|
||
comment_lines.append(" 💡 For next improvement: Set 'force_transparency_webm: True' in ffmpeg_config to enable")
|
||
elif format == 'mov':
|
||
comment_lines.append(" ℹ️ MOV format can support transparency with certain codecs")
|
||
comment_lines.append(" 💡 For next improvement: Use ProRes 4444 or PNG codec for transparency in MOV")
|
||
elif format == 'gif':
|
||
comment_lines.append(" ℹ️ GIF supports basic binary transparency (on/off)")
|
||
comment_lines.append(" 💡 For next improvement: Use WebM for smooth alpha transparency")
|
||
else:
|
||
comment_lines.append(" ℹ️ Selected format does not support transparency")
|
||
comment_lines.append(" 💡 For next improvement: Use WebM format for web-compatible transparency")
|
||
|
||
# Temp frames information
|
||
comment_lines.append(f"• Temp Frames: {len(images)} images @ {temp_dir}")
|
||
comment_lines.append(f" ℹ️ Processing {len(images)} individual frames")
|
||
if len(images) > 1000:
|
||
comment_lines.append(" ⚠️ Large frame count (>1000): May require significant processing time")
|
||
comment_lines.append(f" 💡 Estimated size: ~{len(images) * 0.2:.1f}MB temporary storage")
|
||
comment_lines.append(f" 🗂️ Temporary directory: {temp_dir}")
|
||
|
||
# Execution status
|
||
try:
|
||
# [Existing FFmpeg execution code...]
|
||
comment_lines.append("\n=== Execution Status ===")
|
||
comment_lines.append("✅ Success: Video created")
|
||
comment_lines.append(f" 📁 Output: {output_file}")
|
||
# comment_lines.append(f" 📊 Final file size: {"[Will be calculated after processing]"}")
|
||
|
||
# Add estimated output quality based on settings
|
||
if ffmpeg_config and ffmpeg_config.get('video'):
|
||
v = ffmpeg_config['video']
|
||
crf = v.get('crf')
|
||
preset = v.get('preset', 'medium')
|
||
codec = v.get('codec', '')
|
||
|
||
quality_score = 0
|
||
# Base quality on CRF
|
||
if crf is not None:
|
||
if 0 <= int(crf) <= 14:
|
||
quality_score = 9.5
|
||
elif 15 <= int(crf) <= 19:
|
||
quality_score = 8.5
|
||
elif 20 <= int(crf) <= 24:
|
||
quality_score = 7.5
|
||
elif 25 <= int(crf) <= 30:
|
||
quality_score = 5.5
|
||
else:
|
||
quality_score = 4.0
|
||
|
||
# Adjust for codec
|
||
if "265" in codec or "hevc" in codec.lower() or "av1" in codec.lower():
|
||
quality_score += 0.5
|
||
elif "vp9" in codec.lower():
|
||
quality_score += 0.3
|
||
elif "nvenc" in codec.lower():
|
||
quality_score -= 0.5
|
||
|
||
# Adjust for preset
|
||
if preset in ['veryslow', 'placebo']:
|
||
quality_score += 0.5
|
||
elif preset in ['ultrafast', 'superfast']:
|
||
quality_score -= 0.5
|
||
|
||
# Cap at 10
|
||
quality_score = min(10, quality_score)
|
||
|
||
comment_lines.append(f" ⭐ Estimated quality: {quality_score:.1f}/10")
|
||
else:
|
||
comment_lines.append(f" ⭐ For estimated quality x/10, connect FFMPEG Configuration node")
|
||
|
||
except Exception as e:
|
||
comment_lines.append("\n=== Execution Status ===")
|
||
comment_lines.append(f"❌ Error: {str(e)}")
|
||
comment_lines.append(" ⚠️ See log for detailed error information")
|
||
comment_lines.append(" 💡 Common issues:")
|
||
comment_lines.append(" - FFmpeg not installed or not in PATH")
|
||
comment_lines.append(" - Insufficient disk space")
|
||
comment_lines.append(" - Incompatible codec/container combination")
|
||
comment_lines.append(" - Invalid parameter values")
|
||
|
||
return ("\n".join(comment_lines), " ".join(ffmpeg_cmd), output_file)
|
||
|
||
# return (comment, " ".join(ffmpeg_cmd), output_file) |