Download ComfyUI-JoyCaption/CaptionTools.py from MarkLilly/mis-custom-nodes: direct link, hf CLI and curl.
- Browser
- Download file 6.59 kB
-
https://huggingface.co/MarkLilly/mis-custom-nodes/resolve/main/ComfyUI-JoyCaption/CaptionTools.py
- Command line
-
hf download hf://MarkLilly/mis-custom-nodes/ComfyUI-JoyCaption/CaptionTools.py
-
curl -L -o CaptionTools.py https://huggingface.co/MarkLilly/mis-custom-nodes/resolve/main/ComfyUI-JoyCaption/CaptionTools.py
6.59 kB
| import os | |
| from pathlib import Path | |
| import torch | |
| import numpy as np | |
| from PIL import Image, ImageOps | |
| class ImageBatchPath: | |
| def INPUT_TYPES(s): | |
| return { | |
| "required": {"image_dir": ("STRING", {"default": "", "multiline": True, "placeholder": "Input directory containing images"})}, | |
| "optional": { | |
| "batch_size": ("INT", {"default": 0, "min": 0, "step": 1, "tooltip": "Number of images to load (0 = all images)"}), | |
| "start_from": ("INT", {"default": 1, "min": 1, "step": 1, "tooltip": "Start from Nth image (1 = first image)"}), | |
| "sort_method": (["sequential", "reverse", "random"], {"default": "sequential", "tooltip": "Image loading order: sequential/reverse/random"}) | |
| } | |
| } | |
| RETURN_TYPES = ("IMAGE", "STRING") | |
| RETURN_NAMES = ("IMAGE", "IMAGE_PATH") | |
| OUTPUT_IS_LIST = (True, True) | |
| FUNCTION = "load_image_batch" | |
| CATEGORY = "🧪AILab/📝JoyCaption" | |
| def IS_CHANGED(cls, **kwargs): | |
| if 'sort_method' in kwargs and kwargs['sort_method'] == "random": | |
| return float("NaN") | |
| return hash(frozenset(kwargs)) | |
| def load_image_batch(self, image_dir: str, batch_size: int = 0, start_from: int = 1, sort_method: str = "sequential"): | |
| if not os.path.isdir(image_dir): | |
| raise FileNotFoundError(f"Directory '{image_dir}' cannot be found.") | |
| valid_extensions = ['.jpg', '.jpeg', '.png', '.webp'] | |
| image_files = [f for f in os.listdir(image_dir) if any(f.lower().endswith(ext) for ext in valid_extensions)] | |
| if not image_files: | |
| raise FileNotFoundError(f"No valid images found in '{image_dir}'.") | |
| if sort_method == "sequential": | |
| image_files.sort() | |
| elif sort_method == "reverse": | |
| image_files.sort(reverse=True) | |
| elif sort_method == "random": | |
| import random | |
| random.shuffle(image_files) | |
| start_index = min(start_from - 1, len(image_files) - 1) | |
| image_files = image_files[start_index:] | |
| if batch_size > 0: | |
| image_files = image_files[:batch_size] | |
| images = [] | |
| image_paths = [] | |
| for filename in image_files: | |
| img_path = os.path.join(image_dir, filename) | |
| try: | |
| image = ImageOps.exif_transpose(Image.open(img_path)).convert("RGB") | |
| image = torch.from_numpy(np.array(image).astype(np.float32) / 255.0)[None,] | |
| images.append(image) | |
| image_paths.append(img_path) | |
| except Exception as e: | |
| print(f'\033[91mError loading {filename}: {str(e)}\033[0m') | |
| continue | |
| return (images, image_paths) | |
| class CaptionSaver: | |
| def INPUT_TYPES(cls): | |
| return { | |
| "required": { | |
| "string": ("STRING", {"forceInput": True}), | |
| "image_path": ("STRING", {"forceInput": True}), | |
| }, | |
| "optional": { | |
| "image": ("IMAGE",), | |
| "custom_output_path": ("STRING", {"default": "", "multiline": True, "placeholder": "Custom output directory path. If empty, will use the directory of image_path"}), | |
| "custom_file_name": ("STRING", {"default": "", "multiline": False, "placeholder": "Custom filename (without extension)"}), | |
| "overwrite": ("BOOLEAN", {"default": True, "tooltip": "if true, will overwrite the existing txt file, if false, will add a number to the filename to make it unique"}) | |
| } | |
| } | |
| RETURN_TYPES = () | |
| FUNCTION = "save_caption" | |
| CATEGORY = "🧪AILab/📝JoyCaption" | |
| OUTPUT_NODE = True | |
| def get_unique_filename(self, base_path: Path) -> Path: | |
| """Get a unique filename by adding numbers if file exists.""" | |
| if not base_path.exists(): | |
| return base_path | |
| counter = 1 | |
| while True: | |
| new_path = base_path.parent / f"{base_path.stem}_{counter:02d}{base_path.suffix}" | |
| if not new_path.exists(): | |
| return new_path | |
| counter += 1 | |
| def save_caption(self, string, image_path, image=None, custom_output_path="", custom_file_name="", overwrite=True): | |
| try: | |
| image_path = Path(image_path) | |
| save_dir = Path(custom_output_path.strip()) if custom_output_path.strip() else image_path.parent | |
| save_dir.mkdir(parents=True, exist_ok=True) | |
| try: | |
| base_name = custom_file_name.strip() if custom_file_name.strip() else image_path.stem | |
| txt_path = save_dir / f"{base_name}.txt" | |
| if not overwrite and txt_path.exists(): | |
| txt_path = self.get_unique_filename(txt_path) | |
| base_name = txt_path.stem | |
| with open(txt_path, 'w', encoding='utf-8') as f: | |
| f.write(string) | |
| print(f'\033[93m[{txt_path.name}]:\033[0m {string}') | |
| if image is not None and custom_output_path.strip(): | |
| try: | |
| if isinstance(image, torch.Tensor): | |
| while len(image.shape) > 3: | |
| image = image.squeeze(0) | |
| image = (image.cpu().numpy() * 255).astype(np.uint8) | |
| if image.shape[-1] != 3: | |
| image = np.transpose(image, (1, 2, 0)) | |
| image = Image.fromarray(image) | |
| img_out_path = save_dir / f"{base_name}{image_path.suffix}" | |
| image.save(img_out_path) | |
| except Exception as e: | |
| print(f'\033[91mFailed to copy image: {str(e)}\033[0m') | |
| except Exception as e: | |
| print(f'\033[91mError saving caption: {str(e)}\033[0m') | |
| except Exception as e: | |
| print(f'\033[91mCritical error: {str(e)}\033[0m') | |
| return () | |
| NODE_CLASS_MAPPINGS = { | |
| "ImageBatchPath": ImageBatchPath, | |
| "CaptionSaver": CaptionSaver | |
| } | |
| NODE_DISPLAY_NAME_MAPPINGS = { | |
| "ImageBatchPath": "Image Batch Path 🖼️", | |
| "CaptionSaver": "Caption Saver 📝" | |
| } |