Files
2026-01-20 19:37:33 +08:00

175 lines
6.8 KiB
Python

import os
import subprocess
import numpy as np
import folder_paths
import tempfile
import soundfile as sf
from comfy.utils import ProgressBar
import time
import json
import datetime
from PIL import Image, ExifTags
from PIL.PngImagePlugin import PngInfo
try:
import imageio_ffmpeg
ffmpeg_path = imageio_ffmpeg.get_ffmpeg_exe()
except ImportError:
ffmpeg_path = "ffmpeg"
class PainterVideoCombine:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"images": ("IMAGE", ),
"frame_rate": ("FLOAT", {"default": 24, "min": 1, "max": 240, "step": 0.1, "display": "number"}),
"format": (["video/h264-mp4", "video/webm", "image/gif"],),
"filename_prefix": ("STRING", {"default": "Painter_Video"}),
},
"optional": {
"audio": ("AUDIO",),
},
"hidden": {
"prompt": "PROMPT",
"extra_pnginfo": "EXTRA_PNGINFO",
"unique_id": "UNIQUE_ID"
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("filename",)
OUTPUT_NODE = True
CATEGORY = "Painter/Video"
FUNCTION = "combine_video"
def combine_video(self, images, frame_rate, format, filename_prefix="Painter", audio=None,
prompt=None, extra_pnginfo=None, unique_id=None):
pbar = ProgressBar(len(images))
output_dir = folder_paths.get_output_directory()
full_output_folder, filename, counter, subfolder, _ = folder_paths.get_save_image_path(
filename_prefix, output_dir, images[0].shape[1], images[0].shape[0]
)
ext = "mp4" if "mp4" in format else ("webm" if "webm" in format else "gif")
file_name = f"{filename}_{counter:05}_.{ext}"
file_path = os.path.join(full_output_folder, file_name)
# Build metadata
video_metadata = {}
if prompt is not None:
video_metadata["prompt"] = json.dumps(prompt)
if extra_pnginfo is not None:
for x in extra_pnginfo:
video_metadata[x] = extra_pnginfo[x]
# Data validation
images_np = images.cpu().numpy()
if not np.isfinite(images_np).all():
images_np = np.nan_to_num(images_np, nan=0.0, posinf=1.0, neginf=0.0)
images_np = np.clip(images_np, 0, 1)
images_np = (images_np * 255).astype(np.uint8)
n, h, w, c = images_np.shape
w, h = (w // 2) * 2, (h // 2) * 2
# Build FFmpeg arguments
args = [
ffmpeg_path, "-y",
"-f", "rawvideo", "-pix_fmt", "rgb24", "-s", f"{w}x{h}", "-r", str(frame_rate), "-i", "-"
]
audio_temp_path = None
if audio is not None:
try:
wav_tensor = audio['waveform']
wav_data = wav_tensor[0].cpu().numpy().transpose() if len(wav_tensor.shape) == 3 else wav_tensor.cpu().numpy().transpose()
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as temp_audio:
sf.write(temp_audio.name, wav_data, audio['sample_rate'], format='WAV')
audio_temp_path = temp_audio.name
args += ["-i", audio_temp_path]
except Exception as e:
print(f"Warning: Audio processing failed: {e}")
if ext == "mp4":
args += ["-c:v", "libx264", "-pix_fmt", "yuv420p", "-crf", "18", "-preset", "faster"]
if audio_temp_path:
args += ["-c:a", "aac", "-shortest"]
elif ext == "webm":
args += ["-c:v", "libvpx-vp9", "-crf", "30", "-b:v", "0"]
if audio_temp_path:
args += ["-c:a", "libvorbis", "-shortest"]
else:
args += ["-vf", "split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse"]
# Add metadata if available
metadata_path = None
if video_metadata:
try:
metadata = json.dumps(video_metadata)
metadata = metadata.replace("\\", "\\\\")
metadata = metadata.replace(";", "\\;")
metadata = metadata.replace("#", "\\#")
metadata = metadata.replace("=", "\\=")
metadata = metadata.replace("\n", "\\\n")
metadata = "comment=" + metadata
metadata_path = os.path.join(tempfile.gettempdir(), f"painter_metadata_{unique_id}.txt")
with open(metadata_path, "w", encoding="utf-8") as f:
f.write(";FFMETADATA1\n")
f.write(metadata)
# Insert metadata input into ffmpeg args
args = args[:1] + ["-i", metadata_path] + args[1:] + ["-metadata", "creation_time=now"]
except Exception as e:
print(f"Warning: Metadata processing failed: {e}")
args.append(file_path)
# Core fix: Use temp file to avoid pipe deadlock
with tempfile.NamedTemporaryFile(mode='wb', suffix='.raw', delete=False) as temp_video:
temp_video_path = temp_video.name
# Write all frames to temp file
for i, frame in enumerate(images_np):
temp_video.write(frame[:h, :w, :].tobytes())
pbar.update(1)
temp_video.flush()
os.fsync(temp_video.fileno())
try:
# Read data from temp file
with open(temp_video_path, 'rb') as f:
process = subprocess.Popen(
args,
stdin=f,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
stdout, stderr = process.communicate(timeout=60)
if process.returncode != 0:
error_msg = stderr.decode('utf-8', errors='ignore')
raise RuntimeError(f"FFmpeg failed:\n{error_msg}")
# Print concise info only on success
print(f"Video created!: {n} frames, {w}x{h}, {frame_rate}fps -> {file_name}")
finally:
# Clean up temp files
if os.path.exists(temp_video_path):
os.remove(temp_video_path)
if audio_temp_path and os.path.exists(audio_temp_path):
os.remove(audio_temp_path)
if metadata_path and os.path.exists(metadata_path):
os.remove(metadata_path)
return {
"ui": {"painter_output": [{"filename": file_name, "subfolder": subfolder, "type": "output"}]},
"result": (file_name,)
}