import comfy, folder_paths, io, struct, subprocess, os, random, sys, time from PIL import Image import numpy as np from server import PromptServer, BinaryEventTypes from imageio_ffmpeg import get_ffmpeg_exe SPECIAL_ID = 12345 VIDEO_ID = 12346 FFMPEG_PATH = get_ffmpeg_exe() class SwarmSaveAnimationWS: methods = {"default": 4, "fastest": 0, "slowest": 6} @classmethod def INPUT_TYPES(s): return { "required": { "images": ("IMAGE", ), "fps": ("FLOAT", {"default": 6.0, "min": 0.01, "max": 1000.0, "step": 0.01}), "lossless": ("BOOLEAN", {"default": True}), "quality": ("INT", {"default": 80, "min": 0, "max": 100}), "method": (list(s.methods.keys()),), "format": (["webp", "gif", "gif-hd", "h264-mp4", "h265-mp4", "webm", "prores"],), }, } CATEGORY = "SwarmUI/video" RETURN_TYPES = () FUNCTION = "save_images" OUTPUT_NODE = True def save_images(self, images, fps, lossless, quality, method, format): method = self.methods.get(method) if images.shape[0] == 0: return { } if images.shape[0] == 1: pbar = comfy.utils.ProgressBar(SPECIAL_ID) i = 255.0 * images[0].cpu().numpy() img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8)) pbar.update_absolute(0, SPECIAL_ID, ("PNG", img, None)) return { } out_img = io.BytesIO() if format in ["webp", "gif"]: if format == "webp": type_num = 3 else: type_num = 4 pil_images = [] for image in images: i = 255. * image.cpu().numpy() img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8)) pil_images.append(img) pil_images[0].save(out_img, save_all=True, duration=int(1000.0 / fps), append_images=pil_images[1 : len(pil_images)], lossless=lossless, quality=quality, method=method, format=format.upper(), loop=0) else: i = 255. * images.cpu().numpy() raw_images = np.clip(i, 0, 255).astype(np.uint8) args = [FFMPEG_PATH, "-v", "error", "-f", "rawvideo", "-pix_fmt", "rgb24", "-s", f"{len(raw_images[0][0])}x{len(raw_images[0])}", "-r", str(fps), "-i", "-", "-n" ] if format == "h264-mp4": args += ["-c:v", "libx264", "-pix_fmt", "yuv420p", "-crf", "19"] ext = "mp4" type_num = 5 elif format == "h265-mp4": args += ["-c:v", "libx265", "-pix_fmt", "yuv420p"] ext = "mp4" type_num = 5 elif format == "webm": args += ["-pix_fmt", "yuv420p", "-crf", "23"] ext = "webm" type_num = 6 elif format == "prores": args += ["-c:v", "prores_ks", "-profile:v", "3", "-pix_fmt", "yuv422p10le"] ext = "mov" type_num = 7 elif format == "gif-hd": args += ["-filter_complex", "split=2 [a][b]; [a] palettegen [pal]; [b] [pal] paletteuse"] ext = "gif" type_num = 4 path = folder_paths.get_save_image_path("swarm_tmp_", folder_paths.get_temp_directory())[0] rand = '%016x' % random.getrandbits(64) file = os.path.join(path, f"swarm_tmp_{rand}.{ext}") result = subprocess.run(args + [file], input=raw_images.tobytes(), capture_output=True) if result.returncode != 0: print(f"ffmpeg failed with return code {result.returncode}", file=sys.stderr) f_out = result.stdout.decode("utf-8").strip() f_err = result.stderr.decode("utf-8").strip() if f_out: print("ffmpeg out: " + f_out, file=sys.stderr) if f_err: print("ffmpeg error: " + f_err, file=sys.stderr) raise Exception(f"ffmpeg failed: {f_err}") # TODO: Is there a way to get ffmpeg to operate entirely in memory? with open(file, "rb") as f: out_img.write(f.read()) os.remove(file) out = io.BytesIO() header = struct.pack(">I", type_num) out.write(header) out.write(out_img.getvalue()) out.seek(0) preview_bytes = out.getvalue() server = PromptServer.instance server.send_sync("progress", {"value": 12346, "max": 12346}, sid=server.client_id) server.send_sync(BinaryEventTypes.PREVIEW_IMAGE, preview_bytes, sid=server.client_id) return { } @classmethod def IS_CHANGED(s, images, fps, lossless, quality, method, format): return time.time() NODE_CLASS_MAPPINGS = { "SwarmSaveAnimationWS": SwarmSaveAnimationWS, }