From 8c3e436c4dd29ddd2128d855993fdd28cdc8b6b6 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 26 Dec 2025 21:18:34 +0700 Subject: [PATCH] Initial commit --- .gitignore | 2 + README.md | 26 ++++++++++ __init__.py | 3 ++ requirements.txt | 2 + save_webp_sequence.py | 111 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 144 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 __init__.py create mode 100644 requirements.txt create mode 100644 save_webp_sequence.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7a60b85 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +*.pyc diff --git a/README.md b/README.md new file mode 100644 index 0000000..5e88601 --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +# ComfyUI-FrameIO + +High-performance frame input/output nodes for AI video pipelines. + +## Features +- Save batch IMAGE tensors to WebP +- Lossy or lossless WebP +- Skip identical frames via hashing +- STRING_LIST output (list-native) +- Optimized for long AI video generation + +## Included Nodes +- Batch Save Image Sequence (WebP) + +## Installation +```bash +cd ComfyUI/custom_nodes +git clone https://github.com/yourname/ComfyUI-FrameIO.git +``` + +## Recommended Settings +- Quality: 90–95 +- Skip Identical Frames: true + +## License +MIT \ No newline at end of file diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..88c3d41 --- /dev/null +++ b/__init__.py @@ -0,0 +1,3 @@ +from .save_webp_sequence import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS + +__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS'] \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..354fd77 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +pillow +numpy \ No newline at end of file diff --git a/save_webp_sequence.py b/save_webp_sequence.py new file mode 100644 index 0000000..a3bcfc2 --- /dev/null +++ b/save_webp_sequence.py @@ -0,0 +1,111 @@ +import os +import torch +import numpy as np +import hashlib +from PIL import Image + +from comfy.utils import ProgressBar + +def image_tensor_hash(img: torch.Tensor) -> str: + img = img.clamp(0, 1) + arr = img.cpu().numpy() + return hashlib.sha1(arr.tobytes()).hexdigest() + + +class BatchSaveImageSequenceWebP: + CATEGORY = "FrameIO" + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "images": ("IMAGE",), + "path_pattern": ( + "STRING", + {"default": "./frame{:06d}.webp", "multiline": False}, + ), + "start_index": ("INT", {"default": 0, "min": 0, "step": 1}), + "lossless": (("false", "true"), {"default": "false"}), + "quality": ("INT", {"default": 95, "min": 1, "max": 100}), + "skip_identical": (("false", "true"), {"default": "true"}), + "overwrite": (("true", "false"), {"default": "true"}), + } + } + + RETURN_TYPES = ("STRING_LIST",) + RETURN_NAMES = ("paths",) + FUNCTION = "execute" + + def execute( + self, + images, + path_pattern, + start_index, + lossless, + quality, + skip_identical, + overwrite, + ): + overwrite = overwrite == "true" + lossless = lossless == "true" + skip_identical = skip_identical == "true" + + batch_size = images.shape[0] + saved_paths = [] + seen_hashes = {} + + pbar = ProgressBar(batch_size) + + for i in range(batch_size): + index = start_index + i + + try: + path = path_pattern.format(index) + except KeyError: + path = path_pattern.format(i=index) + + os.makedirs(os.path.dirname(path) or ".", exist_ok=True) + + img = images[i] + + if skip_identical: + h = image_tensor_hash(img) + if h in seen_hashes: + saved_paths.append(seen_hashes[h]) + pbar.update(1) + continue + + if os.path.exists(path) and not overwrite: + saved_paths.append(path) + pbar.update(1) + continue + + img = img.clamp(0, 1) + img = (img.cpu().numpy() * 255).astype(np.uint8) + pil_img = Image.fromarray(img, mode="RGB") + + kwargs = {"format": "WEBP", "method": 6} + if lossless: + kwargs["lossless"] = True + else: + kwargs["lossless"] = False + kwargs["quality"] = quality + + pil_img.save(path, **kwargs) + + saved_paths.append(path) + if skip_identical: + seen_hashes[h] = path + + pbar.update(1) + + return (saved_paths,) + + +NODE_CLASS_MAPPINGS = { + "BatchSaveImageSequenceWebP": BatchSaveImageSequenceWebP +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "BatchSaveImageSequenceWebP": "Batch Save Image Sequence (WebP)" +} \ No newline at end of file