From 02d278fe6ad451310d9d700fa33a40ec06855ea9 Mon Sep 17 00:00:00 2001 From: Level Pixel Dev Date: Thu, 31 Jul 2025 07:01:10 +0600 Subject: [PATCH] Added Iterators and counters for images from folders or frames from videos --- __init__.py | 1 + node_list.json | 11 ++- nodes/convert/convert_LP.py | 65 +++++++++++++ nodes/io/folder_workers_LP.py | 10 +- nodes/io/iterators_LP.py | 175 ++++++++++++++++++++++++++++++++++ pyproject.toml | 4 +- 6 files changed, 260 insertions(+), 6 deletions(-) create mode 100644 nodes/io/iterators_LP.py diff --git a/__init__.py b/__init__.py index 70b7820..bd4210f 100644 --- a/__init__.py +++ b/__init__.py @@ -7,6 +7,7 @@ node_list = [ "convert.convert_LP", "image.image_utils_LP", "image.inpaint_crop_stitch_LP", + "io.iterators_LP", "io.folder_workers_LP", "io.image_loaders_LP", "io.image_outputs_LP", diff --git a/node_list.json b/node_list.json index 584712f..6a64e3e 100644 --- a/node_list.json +++ b/node_list.json @@ -11,6 +11,8 @@ "Convert Int To Float [LP]":"Convert Int To Float", "Convert Int To Bool [LP]":"Convert Int To Bool", "Convert Bool To Int [LP]":"Convert Bool To Int", + "Convert Combo To Text [LP]":"Convert Combo To Text", + "Convert Any To Text [LP]":"Convert Any To Text", "Image Overlay [LP]":"Image Overlay", "Fast Checker Pattern [LP]":"Fast Checker Pattern", "File Counter [LP]":"Counts the number of files in the specified folder", @@ -67,5 +69,12 @@ "Cropped Aspect Size Parameters [LP]":"Cropped Aspect Size Parameters for Inpaint Crop", "Cropped Forsed Size Parameters [LP]":"Cropped Forsed Size Parameters for Inpaint Crop", "Cropped Ranged Size Parameters [LP]":"Cropped Ranged Size Parameters for Inpaint Crop", - "Cropped Free Size Parameters [LP]":"Cropped Free Size Parameter for Inpaint Crops" + "Cropped Free Size Parameters [LP]":"Cropped Free Size Parameter for Inpaint Crops", + "Get Iterator Data From Videos [LP]": "Get Iterator Data From Videos", + "Get Iterator Data From Image Folders [LP]": "Get Iterator Data From Image Folders", + "Image Data Iterator [LP]": "Image Data Iterator", + "Iterator [LP]": "Iterator", + "Get ComfyUI Folder Path [LP]": "Get ComfyUI Folder Path", + "Get ComfyUI HTTP Folder Path [LP]": "Get ComfyUI HTTP Folder Path", + "Get Filename By Index In Folder [LP]": "Get Filename By Index In Folder" } \ No newline at end of file diff --git a/nodes/convert/convert_LP.py b/nodes/convert/convert_LP.py index 424e963..0fd860f 100644 --- a/nodes/convert/convert_LP.py +++ b/nodes/convert/convert_LP.py @@ -274,6 +274,67 @@ class BoolToInt: return (int_out,) +class ComboToText: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "combo_list": ("COMBO",), + "delimiter": ("STRING", {"default": ", ", "multiline": False}), + "as_json": ("BOOLEAN", {"default": False}), + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("text",) + FUNCTION = "combo_to_text" + CATEGORY = "LevelPixel/Conversion" + + def combo_to_text(self, combo_list, delimiter, as_json): + import json + if as_json: + try: + text = json.dumps(combo_list, ensure_ascii=False, indent=2) + except Exception as e: + text = f"Error: {e}" + else: + lines = [] + for item in combo_list: + if isinstance(item, dict): + line = delimiter.join(f"{k}: {v}" for k, v in item.items()) + else: + line = str(item) + lines.append(line) + text = "\n".join(lines) + return (text,) + +class AnyToText: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "any_value": (any, {"defaultInput": True}), + "as_json": ("BOOLEAN", {"default": False}), + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("text",) + FUNCTION = "any_to_text" + CATEGORY = "LevelPixel/Conversion" + + def any_to_text(self, any_value, as_json): + import json + if as_json: + try: + text = json.dumps(any_value, ensure_ascii=False, indent=2) + except Exception as e: + text = f"Error: {e}" + else: + text = str(any_value) + return (text,) + + NODE_CLASS_MAPPINGS = { "StringToInt|LP": StringToInt, "StringToFloat|LP": StringToFloat, @@ -287,6 +348,8 @@ NODE_CLASS_MAPPINGS = { "IntToFloat|LP": IntToFloat, "IntToBool|LP": IntToBool, "BoolToInt|LP": BoolToInt, + "ComboToText|LP": ComboToText, + "AnyToText|LP": AnyToText, } NODE_DISPLAY_NAME_MAPPINGS = { @@ -302,4 +365,6 @@ NODE_DISPLAY_NAME_MAPPINGS = { "IntToFloat|LP": "Convert Int To Float [LP]", "IntToBool|LP": "Convert Int To Bool [LP]", "BoolToInt|LP": "Convert Bool To Int [LP]", + "ComboToText|LP": "Convert Combo To Text [LP]", + "AnyToText|LP": "Convert Any To Text [LP]", } \ No newline at end of file diff --git a/nodes/io/folder_workers_LP.py b/nodes/io/folder_workers_LP.py index cc36e24..16fbb34 100644 --- a/nodes/io/folder_workers_LP.py +++ b/nodes/io/folder_workers_LP.py @@ -9,6 +9,7 @@ class FileCounter: "required": { "directory_path": ("STRING", {"default": '', "multiline": False}), "patterns": ("STRING", {"default": '*.jpg|*.png|*.jpeg', "multiline": False}), + "rescan_each_queue": ("BOOLEAN", {"default": True}), }, } @@ -20,10 +21,13 @@ class FileCounter: CATEGORY = "LevelPixel/IO" @classmethod - def IS_CHANGED(cls, *v): - return ALWAYS_CHANGED_FLAG + def IS_CHANGED(cls, directory_path, patterns, rescan_each_queue, *v): + if rescan_each_queue == True: + return float("NaN") + else: + return False - def file_counter(self, directory_path, patterns): + def file_counter(self, directory_path, patterns, rescan_each_queue): if not os.path.isdir(directory_path): return (0,) total_int = 0 diff --git a/nodes/io/iterators_LP.py b/nodes/io/iterators_LP.py new file mode 100644 index 0000000..0af190e --- /dev/null +++ b/nodes/io/iterators_LP.py @@ -0,0 +1,175 @@ +import os +import glob +import cv2 + +class GetIteratorDataVideos: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "directory_path": ("STRING", {"default": '', "multiline": False}), + "patterns": ("STRING", {"default": '*.mp4|*.avi|*.mov|*.mkv', "multiline": False}), + "rescan_each_queue": ("BOOLEAN", {"default": True}), + }, + } + + RETURN_TYPES = ("ITERATOR_DATA", "INT", "INT") + RETURN_NAMES = ("iterator_data", "video_count", "frame_count") + FUNCTION = "prepare_iterator_data_from_videos" + CATEGORY = "LevelPixel/Iterators" + + @classmethod + def IS_CHANGED(cls, directory_path, patterns, rescan_each_queue, *v): + if rescan_each_queue == True: + return float("NaN") + else: + return False + + def prepare_iterator_data_from_videos(self, directory_path, patterns, rescan_each_queue): + if not os.path.isdir(directory_path): + return ([], 0, 0) + iterator_data = [] + video_files = [] + for pattern in patterns.split("|"): + video_files.extend(glob.glob(os.path.join(directory_path, pattern))) + video_files = sorted(video_files) + total_frames = 0 + for idx, video_path in enumerate(video_files): + name = os.path.basename(video_path) + try: + cap = cv2.VideoCapture(video_path) + count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) + cap.release() + except Exception: + count = 0 + iterator_data.append({"index": idx, "name": name, "count": count}) + total_frames += count + return (iterator_data, len(video_files), total_frames) + + +class GetIteratorDataImageFolders: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "directory_path": ("STRING", {"default": '', "multiline": False}), + "patterns": ("STRING", {"default": '*.jpg|*.png|*.jpeg', "multiline": False}), + "subfolder": ("STRING", {"default": '', "multiline": False}), + "rescan_each_queue": ("BOOLEAN", {"default": True}), + }, + } + + RETURN_TYPES = ("ITERATOR_DATA", "INT", "INT") + RETURN_NAMES = ("iterator_data", "folder_count", "image_count") + FUNCTION = "prepare_iterator_data_from_image_folders" + CATEGORY = "LevelPixel/Iterators" + + @classmethod + def IS_CHANGED(cls, directory_path, patterns, subfolder, rescan_each_queue, *v): + if rescan_each_queue == True: + return float("NaN") + else: + return False + + def prepare_iterator_data_from_image_folders(self, directory_path, patterns, subfolder, rescan_each_queue): + if not os.path.isdir(directory_path): + return ([], 0, 0) + iterator_data = [] + folders = [f for f in os.listdir(directory_path) if os.path.isdir(os.path.join(directory_path, f))] + total_images = 0 + for idx, folder in enumerate(sorted(folders)): + if subfolder: + search_path = os.path.join(directory_path, folder, subfolder) + else: + search_path = os.path.join(directory_path, folder) + count = 0 + if os.path.isdir(search_path): + for pattern in patterns.split("|"): + count += len(glob.glob(os.path.join(search_path, pattern))) + iterator_data.append({"index": idx, "name": folder, "count": count}) + total_images += count + return (iterator_data, len(folders), total_images) + +class ImageDataIterator: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "iterator_data": ("ITERATOR_DATA",), + "global_index": ("INT", {"default": 0, "min": 0, "control_after_generate": True}), + } + } + + RETURN_TYPES = ("INT", "INT", "STRING", "INT") + RETURN_NAMES = ("item_index", "set_index", "set_name", "global_index") + FUNCTION = "iterate" + CATEGORY = "LevelPixel/Iterators" + + def iterate(self, iterator_data, global_index): + total = 0 + total_frames = sum(item["count"] for item in iterator_data) + if global_index >= total_frames: + raise RuntimeError("Image iteration finished") + for i, item in enumerate(iterator_data): + count = item["count"] + if global_index < total + count: + item_index = global_index - total + set_index = item["index"] + set_name = item["name"] + return (item_index, set_index, set_name, global_index) + total += count + return (0, 0, '', global_index) + +class Iterator: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "index": ("INT", {"default": 0, "min": 0, "control_after_generate": True}), + "limit": ("INT", {"default": 0, "min": 0}), + "mode": ([ + "Greater (index > limit)", + "Less (index < limit)", + "Equal (index == limit)", + "Greater or Equal (index >= limit)", + "Less or Equal (index <= limit)" + ], {"default": "Greater (index > limit)"}), + }, + } + + RETURN_TYPES = ("INT",) + RETURN_NAMES = ("index",) + FUNCTION = "indexate" + CATEGORY = "LevelPixel/Iterators" + + def indexate(self, index, limit, mode): + if mode == "Greater (index > limit)": + if index > limit: + raise RuntimeError("Iteration finished (index > limit)") + elif mode == "Less (index < limit)": + if index < limit: + raise RuntimeError("Iteration finished (index < limit)") + elif mode == "Equal (index == limit)": + if index == limit: + raise RuntimeError("Iteration finished (index == limit)") + elif mode == "Greater or Equal (index >= limit)": + if index >= limit: + raise RuntimeError("Iteration finished (index >= limit)") + elif mode == "Less or Equal (index <= limit)": + if index <= limit: + raise RuntimeError("Iteration finished (index <= limit)") + return (index,) + +NODE_CLASS_MAPPINGS = { + "GetIteratorDataVideos|LP": GetIteratorDataVideos, + "GetIteratorDataImageFolders|LP": GetIteratorDataImageFolders, + "ImageDataIterator|LP": ImageDataIterator, + "Iterator|LP": Iterator, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "GetIteratorDataVideos|LP": "Get Iterator Data From Videos [LP]", + "GetIteratorDataImageFolders|LP": "Get Iterator Data From Image Folders [LP]", + "ImageDataIterator|LP": "Image Data Iterator [LP]", + "Iterator|LP": "Iterator [LP]", +} diff --git a/pyproject.toml b/pyproject.toml index 5296885..1888670 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "comfyui-levelpixel" -description = "Main nodes of the Level Pixel company (levelpixel, LP). In this package you will find easy-to-use atomic nodes that can help automate various processes - memory cleaning automation (RAM free, VRAM free), Lora loading like in Automatic1111, powerful inpaint crop and stitch, pipeline, declaring standard variables, converting variables, google translate, filtering tags by categories, working with file system, counting files in file system, counting objects, counting images. The names of the key nodes in this package are: Inpaint-CropAndStitch, Inpaint Crop, Inpaint Stitch, Resize Image, Resize Mask, Model Unloader, Free memory, Tag Filters, Tag Category Filters, Tag Choice Parser, File counter, Image Loader From Path (with counters), Google Translator, Language Translator, Override Device, Delay, Lora Tag Loader, Pipeline, Resize Masks, Resize Image. Licensed under GNU GPLv3. Repository: https://github.com/LevelPixel/ComfyUI-LevelPixel" -version = "1.2.9" +description = "Main nodes of the Level Pixel company (levelpixel, LP). In this package you will find easy-to-use atomic nodes that can help automate various processes - memory cleaning automation (RAM free, VRAM free), Lora loading like in Automatic1111, powerful inpaint crop and stitch, pipeline, declaring standard variables, converting variables, google translate, filtering tags by categories, working with file system, counting files in file system, counting objects, counting images. The names of the key nodes in this package are: Inpaint-CropAndStitch, Inpaint Crop, Inpaint Stitch, Resize Image, Resize Mask, Model Unloader, Free memory, Tag Filters, Tag Category Filters, Tag Choice Parser, File counter, Image Loader From Path (with counters), Google Translator, Language Translator, Override Device, Delay, Lora Tag Loader, Pipeline, Resize Masks, Resize Image, Iterators. Licensed under GNU GPLv3. Repository: https://github.com/LevelPixel/ComfyUI-LevelPixel" +version = "1.3.0" license = { file = "LICENSE.txt" } dependencies = [ "torch>=2.0.1",