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