Files
Bjornulf_custom_nodes/ffmpeg_images_to_video.py
justumen 10263f2110 0.76
2025-02-27 18:00:12 +01:00

745 lines
44 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 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"]
name_prefix = os.path.splitext(name_prefix)[0]
output_base = os.path.join("output", name_prefix)
existing_files = glob.glob(f"{output_base}_*.{format}")
if existing_files:
max_num = max([int(f.split('_')[-1].split('.')[0]) for f in existing_files])
next_num = max_num + 1
else:
next_num = 1
output_file = f"{output_base}_{next_num:04d}.{format}"
temp_dir = "Bjornulf/temp_images_imgs2video"
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)
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)