diff --git a/README.md b/README.md index eac550e..2467f13 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,7 @@ wget --content-disposition -P /workspace/ComfyUI/models/checkpoints "https://civ - **v0.22**: Allow write text node to use random selection like this {hood|helmet} will randomly choose between hood or helmet. - **v0.23**: Add a new node: Pause, resume or stop workflow. - **v0.24**: Add a new node: Pause, select input, pick one. +- **v0.25**: Two new nodes: Loop Images and Random image. # 📝 Nodes descriptions @@ -348,4 +349,20 @@ You can connect any type of node to the pause node, above is an example with tex **Description:** Automatically pause the workflow, and rings a bell when it does. (play the audio `bell.m4a` file provided) You can then manually select the input you want to use, and resume the workflow with it. -You can connect this node to anything you want, above is an example with IMAGE. But you can pick whatever you want, in the node `input = output`. \ No newline at end of file +You can connect this node to anything you want, above is an example with IMAGE. But you can pick whatever you want, in the node `input = output`. + +### 37 - 🎲🖼 Random Image + +![pick input](screenshots/random_image.png) + +**Description:** +Just take a random image from a list of images. + +### 38 - ♻🖼 Loop (Images) + +![pick input](screenshots/loop_images.png) + +**Description:** +Loop over a list of images. +Usage example : You have a list of images, and you want to apply the same process to all of them. +Above is an example of the loop images node sending them to an Ipadapter style transfer workflow. (Same seed of course.) \ No newline at end of file diff --git a/__init__.py b/__init__.py index e25e0da..a2823c4 100644 --- a/__init__.py +++ b/__init__.py @@ -42,6 +42,9 @@ from .loop_combine_texts_by_lines import CombineTextsByLines from .free_vram_hack import FreeVRAM from .pause_resume_stop import PauseResume from .pick_input import PickInput +from .loop_images import LoopImages +from .random_image import RandomImage + # from .pass_preview_image import PassPreviewImage # from .check_black_image import CheckBlackImage # from .clear_vram import ClearVRAM @@ -51,6 +54,8 @@ from .pick_input import PickInput NODE_CLASS_MAPPINGS = { # "Bjornulf_CustomStringType": CustomStringType, "Bjornulf_ollamaLoader": ollamaLoader, + "Bjornulf_LoopImages": LoopImages, + "Bjornulf_RandomImage": RandomImage, # "Bjornulf_PassPreviewImage": PassPreviewImage, "Bjornulf_PickInput": PickInput, "Bjornulf_PauseResume": PauseResume, @@ -102,6 +107,8 @@ NODE_CLASS_MAPPINGS = { NODE_DISPLAY_NAME_MAPPINGS = { # "Bjornulf_CustomStringType": "!!! CUSTOM STRING TYPE !!!", "Bjornulf_ollamaLoader": "🦙 Ollama (Description)", + "Bjornulf_LoopImages": "♻🖼 Loop (Images)", + "Bjornulf_RandomImage": "🎲🖼 Random Image", # "Bjornulf_PassPreviewImage": "🖼⮕ Pass Preview Image", "Bjornulf_PickInput": "⏸️🔍 Paused. Select input, Pick one", "Bjornulf_PauseResume": "⏸️ Paused. Resume or Stop ?", diff --git a/loop_images.py b/loop_images.py new file mode 100644 index 0000000..dd55446 --- /dev/null +++ b/loop_images.py @@ -0,0 +1,26 @@ +class LoopImages: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "number_of_images": ("INT", {"default": 2, "min": 1, "max": 10, "step": 1}), + }, + } + + RETURN_TYPES = ("IMAGE",) + FUNCTION = "loop_images" + OUTPUT_IS_LIST = (True,) + CATEGORY = "Bjornulf" + + def loop_images(self, number_of_images, **kwargs): + image_list = [] + for i in range(1, number_of_images + 1): + image_key = f"image_{i}" + if image_key in kwargs and kwargs[image_key] is not None: + image_list.append(kwargs[image_key]) + return (image_list,) + + @classmethod + def IS_CHANGED(cls, number_of_images, ** kwargs): + return float("nan") # This will force the node to always update + diff --git a/pyproject.toml b/pyproject.toml index 28bb438..ef8845b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "bjornulf_custom_nodes" description = "Nodes: Ollama, Text to Speech, Combine Texts, Random Texts, Save image for Bjornulf LobeChat, Text with random Seed, Random line from input, Combine images, Image to grayscale (black & white), Remove image Transparency (alpha), Resize Image, ..." -version = "0.24" +version = "0.25" license = {file = "LICENSE"} [project.urls] diff --git a/random_image.py b/random_image.py new file mode 100644 index 0000000..4c2f109 --- /dev/null +++ b/random_image.py @@ -0,0 +1,32 @@ +import random + +class RandomImage: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "number_of_images": ("INT", {"default": 2, "min": 1, "max": 10, "step": 1}), + }, + } + + RETURN_TYPES = ("IMAGE",) + FUNCTION = "random_image" + CATEGORY = "Bjornulf" + + def random_image(self, number_of_images, **kwargs): + valid_images = [] + for i in range(1, number_of_images + 1): + image_key = f"image_{i}" + if image_key in kwargs and kwargs[image_key] is not None: + valid_images.append(kwargs[image_key]) + + if not valid_images: + raise ValueError("No valid images provided") + + random_image = random.choice(valid_images) + return (random_image,) + + @classmethod + def IS_CHANGED(cls, number_of_images, ** kwargs): + return float("nan") # This will force the node to always update + diff --git a/screenshots/loop_images.png b/screenshots/loop_images.png new file mode 100644 index 0000000..cd45e2c Binary files /dev/null and b/screenshots/loop_images.png differ diff --git a/screenshots/random_image.png b/screenshots/random_image.png new file mode 100644 index 0000000..b19718a Binary files /dev/null and b/screenshots/random_image.png differ diff --git a/web/js/loop_images.js b/web/js/loop_images.js new file mode 100644 index 0000000..e74326b --- /dev/null +++ b/web/js/loop_images.js @@ -0,0 +1,52 @@ +import { app } from "../../../scripts/app.js"; + +app.registerExtension({ + name: "Bjornulf.LoopImages", + async nodeCreated(node) { + if (node.comfyClass === "Bjornulf_LoopImages") { + const updateInputs = () => { + const numInputsWidget = node.widgets.find(w => w.name === "number_of_images"); + if (!numInputsWidget) return; + + const numInputs = numInputsWidget.value; + + // Initialize node.inputs if it doesn't exist + if (!node.inputs) { + node.inputs = []; + } + + // Filter existing image inputs + const existingInputs = node.inputs.filter(input => input.name.startsWith('image_')); + + // Determine if we need to add or remove inputs + if (existingInputs.length < numInputs) { + // Add new image inputs if not enough existing + for (let i = existingInputs.length + 1; i <= numInputs; i++) { + const inputName = `image_${i}`; + if (!node.inputs.find(input => input.name === inputName)) { + node.addInput(inputName, "IMAGE"); + } + } + } else { + // Remove excess image inputs if too many + node.inputs = node.inputs.filter(input => !input.name.startsWith('image_') || parseInt(input.name.split('_')[1]) <= numInputs); + } + + node.setSize(node.computeSize()); + }; + + // Move number_of_images to the top initially + const numInputsWidget = node.widgets.find(w => w.name === "number_of_images"); + if (numInputsWidget) { + node.widgets = [numInputsWidget, ...node.widgets.filter(w => w !== numInputsWidget)]; + numInputsWidget.callback = () => { + updateInputs(); + app.graph.setDirtyCanvas(true); + }; + } + + // Delay the initial update to ensure node is fully initialized + setTimeout(updateInputs, 0); + } + } +}); diff --git a/web/js/random_image.js b/web/js/random_image.js new file mode 100644 index 0000000..09b00c3 --- /dev/null +++ b/web/js/random_image.js @@ -0,0 +1,58 @@ +import { app } from "../../../scripts/app.js"; + +app.registerExtension({ + name: "Bjornulf.RandomImage", + async nodeCreated(node) { + if (node.comfyClass === "Bjornulf_RandomImage") { + const updateInputs = () => { + const numInputsWidget = node.widgets.find(w => w.name === "number_of_images"); + if (!numInputsWidget) return; + + const numInputs = numInputsWidget.value; + + // Initialize node.inputs if it doesn't exist + if (!node.inputs) { + node.inputs = []; + } + + // Filter existing image inputs + const existingInputs = node.inputs.filter(input => input.name.startsWith('image_')); + + // Determine if we need to add or remove inputs + if (existingInputs.length < numInputs) { + // Add new image inputs if not enough existing + for (let i = existingInputs.length + 1; i <= numInputs; i++) { + const inputName = `image_${i}`; + if (!node.inputs.find(input => input.name === inputName)) { + node.addInput(inputName, "IMAGE"); + } + } + } else { + // Remove excess image inputs if too many + node.inputs = node.inputs.filter(input => !input.name.startsWith('image_') || parseInt(input.name.split('_')[1]) <= numInputs); + } + + node.setSize(node.computeSize()); + }; + + // Set seed widget to hidden input + const seedWidget = node.widgets.find(w => w.name === "seed"); + if (seedWidget) { + seedWidget.type = "HIDDEN"; + } + + // Move number_of_images to the top initially + const numInputsWidget = node.widgets.find(w => w.name === "number_of_images"); + if (numInputsWidget) { + node.widgets = [numInputsWidget, ...node.widgets.filter(w => w !== numInputsWidget)]; + numInputsWidget.callback = () => { + updateInputs(); + app.graph.setDirtyCanvas(true); + }; + } + + // Delay the initial update to ensure node is fully initialized + setTimeout(updateInputs, 0); + } + } +});