diff --git a/README.md b/README.md index 5e88601..a0d27b7 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,162 @@ # ComfyUI-FrameIO -High-performance frame input/output nodes for AI video pipelines. +High-performance frame input/output nodes for **AI video pipelines** in ComfyUI. -## 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 +ComfyUI-FrameIO focuses on **efficient frame storage, loading, and reuse** for long video generation workflows, solving common problems like massive PNG sizes, duplicate frames, and fragile filename-based loading. -## Included Nodes -- Batch Save Image Sequence (WebP) +--- -## Installation -```bash -cd ComfyUI/custom_nodes -git clone https://github.com/yourname/ComfyUI-FrameIO.git +## ✨ Features + +### 🧩 Frame Saving +- Save batch `IMAGE` tensors as **WebP** +- **Lossy or Lossless WebP** +- **Skip identical frames** using hash-based deduplication +- Massive disk space reduction vs PNG +- Returns **STRING_LIST** (ComfyUI list-native) + +### 📥 Frame Loading +- Pattern-based frame loading +- **STRING_LIST → IMAGE loader** (no filename guessing) +- **Frame range selector** (`start`, `end`, `step`) +- **Auto-detect frame count** +- Safe with duplicate paths (from deduplication) + +### ⚡ Optimized for AI Video +- Designed for **long sequences** +- Works with WebP / PNG / JPG +- Preserves exact frame ordering +- Compatible with FFmpeg / Video Combine nodes + +--- + +## 📦 Included Nodes + +### 🔹 Batch Save Image Sequence (WebP) +Save an image batch efficiently to disk. + +**Inputs** +- `images` (IMAGE) +- `path_pattern` (STRING) +- `start_index` (INT) +- `lossless` (true / false) +- `quality` (INT) +- `skip_identical` (true / false) +- `overwrite` (true / false) + +**Outputs** +- `paths` (STRING_LIST) + +--- + +### 🔹 Batch Load Image Sequence +Load images from disk using a filename pattern. + +**Inputs** +- `path_pattern` (STRING) +- `start_index` (INT) +- `frame_count` (INT) +- `ignore_missing_images` (true / false) + +**Outputs** +- `images` (IMAGE) + +--- + +### 🔹 Batch Load Image List (Advanced) +Load images directly from a `STRING_LIST`. + +**Inputs** +- `paths` (STRING_LIST) +- `start` (INT, optional) +- `end` (INT, optional) +- `step` (INT, optional) + +**Outputs** +- `images` (IMAGE) +- `frame_count` (INT) + +> This is the **recommended loader** when using frame deduplication. + +--- + +## 🧠 Why ComfyUI-FrameIO? + +| Problem | Solution | +|------|------| +| PNG sequences too large | WebP (5–10× smaller) | +| Duplicate frames | Hash-based skip | +| Filename guessing | STRING_LIST loader | +| Partial re-render | Frame range selector | +| Long videos | Optimized IO | + +--- + +## 🎬 Recommended Workflows + +### 🔥 Best Practice (Long Videos) ``` -## Recommended Settings -- Quality: 90–95 -- Skip Identical Frames: true +KSampler +↓ +Batch Save Image Sequence (WebP) +↓ STRING_LIST +Batch Load Image List (Advanced) +↓ IMAGE +Video Combine / FFmpeg -## License -MIT \ No newline at end of file +```` + +### 🎯 Partial Reload +- `start = 300` +- `end = 900` +- `step = 2` + +Perfect for re-encoding or post-processing sections. + +--- + +## ⚙️ Recommended Settings + +| Use Case | Settings | +|-------|---------| +| Final video | Quality 95, Skip Identical = true | +| Iteration | Quality 90 | +| Dataset / archival | Lossless = true | +| Static shots | Skip Identical = true | + +--- + +## 📥 Installation + +```bash +cd ComfyUI/custom_nodes +git clone https://github.com//ComfyUI-FrameIO.git +```` + +Restart ComfyUI. + +Nodes will appear under: + +``` +FrameIO +``` + +--- + +## 🔮 Roadmap (Planned / Ideas) + +* FFmpeg encode node (no frame storage) +* AVIF support +* 10-bit video output +* Keyframe detection +* Frame cache / reuse +* Timeline tools + +Contributions and suggestions welcome. + +--- + +## 📄 License + +MIT diff --git a/__init__.py b/__init__.py index 88c3d41..9aaf2c5 100644 --- a/__init__.py +++ b/__init__.py @@ -1,3 +1,24 @@ -from .save_webp_sequence import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS +from .save_webp_sequence import ( + NODE_CLASS_MAPPINGS as SAVE_MAPPINGS, + NODE_DISPLAY_NAME_MAPPINGS as SAVE_NAMES, +) +from .load_image_sequence import ( + NODE_CLASS_MAPPINGS as SEQ_MAPPINGS, + NODE_DISPLAY_NAME_MAPPINGS as SEQ_NAMES, +) +from .load_image_list import ( + NODE_CLASS_MAPPINGS as LIST_MAPPINGS, + NODE_DISPLAY_NAME_MAPPINGS as LIST_NAMES, +) -__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS'] \ No newline at end of file +NODE_CLASS_MAPPINGS = {} +NODE_CLASS_MAPPINGS.update(SAVE_MAPPINGS) +NODE_CLASS_MAPPINGS.update(SEQ_MAPPINGS) +NODE_CLASS_MAPPINGS.update(LIST_MAPPINGS) + +NODE_DISPLAY_NAME_MAPPINGS = {} +NODE_DISPLAY_NAME_MAPPINGS.update(SAVE_NAMES) +NODE_DISPLAY_NAME_MAPPINGS.update(SEQ_NAMES) +NODE_DISPLAY_NAME_MAPPINGS.update(LIST_NAMES) + +__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"] diff --git a/load_image_list.py b/load_image_list.py new file mode 100644 index 0000000..b466001 --- /dev/null +++ b/load_image_list.py @@ -0,0 +1,77 @@ +import os +import torch +from PIL import Image +import numpy as np + + +def load_image(path: str) -> torch.Tensor: + """ + Load image from disk and convert to ComfyUI IMAGE tensor + Shape: [1, H, W, 3], float32 0..1 + """ + img = Image.open(path).convert("RGB") + arr = np.array(img).astype(np.float32) / 255.0 + return torch.from_numpy(arr)[None,] + + +class BatchLoadImageList: + CATEGORY = "FrameIO" + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "paths": ("STRING_LIST",), + }, + "optional": { + "start": ("INT", {"default": 0, "min": 0}), + "end": ("INT", {"default": -1, "min": -1}), + "step": ("INT", {"default": 1, "min": 1}), + }, + } + + RETURN_TYPES = ("IMAGE", "INT") + RETURN_NAMES = ("images", "frame_count") + FUNCTION = "execute" + + def execute( + self, + paths, + start=0, + end=-1, + step=1, + ): + if not paths: + raise RuntimeError("STRING_LIST is empty") + + total = len(paths) + + # Auto-detect end + if end < 0 or end > total: + end = total + + if start >= end: + raise ValueError("Invalid frame range") + + selected = paths[start:end:step] + + if not selected: + raise RuntimeError("No frames selected after slicing") + + imgs = [] + for path in selected: + if not os.path.exists(path): + raise FileNotFoundError(f"Image does not exist: {path}") + imgs.append(load_image(path)) + + images = torch.cat(imgs, dim=0) + return (images, len(selected)) + + +NODE_CLASS_MAPPINGS = { + "BatchLoadImageList": BatchLoadImageList +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "BatchLoadImageList": "Batch Load Image List (Advanced)" +}