| 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}")
|
|
|
| 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,
|
| }
|
|
|