diff --git a/README.md b/README.md index 29c9f7e..99f1371 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# 🔗 Comfyui : Bjornulf_custom_nodes v0.36 🔗 +# 🔗 Comfyui : Bjornulf_custom_nodes v0.37 🔗 # ❤️ Coffee : ☕☕☕☕☕ 5/5 @@ -84,6 +84,7 @@ wget --content-disposition -P /workspace/ComfyUI/models/checkpoints "https://civ - **v0.34**: Two new nodes : Load Images from output folder and Select an Image, Pick. - **v0.35**: Great improvements of the TTS node 31. It will also save the audio file in the "ComfyUI/Bjornulf_TTS/" folder. - Not tested on windows yet - - **v0.36**: Fix random model. +- **v0.37**: New node : Random Load checkpoint (Model Selector). Alternative to the random checkpoint node. (Not preloading all checkpoints in memory, slower to switch between checkpoints, but more outputs to decide where to store your results.) # 📝 Nodes descriptions @@ -460,19 +461,41 @@ It will take the same special syntax as the Advanced write text node `{blue|red} ### 40 - 🎲 Random (Model+Clip+Vae) - aka Checkpoint / Model -![pick input](screenshots/random_checkpoint.png) +![random checkpoint](screenshots/random_checkpoint.png) **Description:** -Just take a trio at random from a load checkpoint node. +Just simply take a trio at random from a load checkpoint node. +Notice that it is using the core Load checkpoint node. It means that all checkpoint will be preloaded in memory. -### 41 - ♻ Loop (Model+Clip+Vae) - aka Checkpoint / Model +Details : +- It will take more VRAM, but it will be faster to switch between checkpoints. +- It can't give you the currently loaded checkpoint name's. + +Check node number 41 before deciding which one to use. + +### 41 - 🎲 Random Load checkpoint (Model Selector) + +![pick input](screenshots/random_load_checkpoint.png) + +**Description:** +This is another way to select a load checkpoint node randomly. +It will not preload all the checkpoints in memory, so it will be slower to switch between checkpoints. +But you can use more outputs to decide where to store your results. (`model_folder` is returning the last folder name of the checkpoint.) +I always store my checkpoints in a folder with the type of the model like `SD1.5`, `SDXL`, etc... So it's a good way for me to recover that information quickly. + +Details : +- Note that compared to node 40, you can't have separate configuration depending of the selected checkpoint. (For example `CLIP Set Last Layer` node set at -2 for a specific model, or a separate vae or clip.) Aka : All models are going to share the exact same workflow. + +Check node number 40 before deciding which one to use. + +### 42 - ♻ Loop (Model+Clip+Vae) - aka Checkpoint / Model ![pick input](screenshots/loop_checkpoint.png) **Description:** Loop over all the trios from several checkpoint node. -### 42 - 📂🖼 Load Images from output folder +### 43 - 📂🖼 Load Images from output folder ![pick input](screenshots/load_images_folder.png) @@ -494,7 +517,7 @@ If you are satisfied with this logic, you can then select all these nodes, right Here is another example of the same thing but excluding the save folder node : ![pick input](screenshots/bjornulf_save_character_group2.png) -### 43 - 🖼🔍 Select an Image, Pick +### 44 - 🖼🔍 Select an Image, Pick ![pick input](screenshots/select_image.png) diff --git a/__init__.py b/__init__.py index fbd3706..4a23f11 100644 --- a/__init__.py +++ b/__init__.py @@ -43,29 +43,27 @@ from .pause_resume_stop import PauseResume from .pick_input import PickInput from .loop_images import LoopImages from .random_image import RandomImage -# from .random_checkpoint import RandomCheckpoint from .loop_model_clip_vae import LoopModelClipVae from .write_text_advanced import WriteTextAdvanced from .loop_write_text import LoopWriteText from .load_images_from_folder import LoadImagesFromSelectedFolder -# from .show import ShowWhatever from .select_image_from_list import SelectImageFromList +from .random_model_selector import RandomModelSelector # from .pass_preview_image import PassPreviewImage # from .check_black_image import CheckBlackImage -# from .clear_vram import ClearVRAM # from .CUSTOM_STRING import CustomStringType NODE_CLASS_MAPPINGS = { # "Bjornulf_CustomStringType": CustomStringType, "Bjornulf_ollamaLoader": ollamaLoader, + "Bjornulf_RandomModelSelector": RandomModelSelector, "Bjornulf_SelectImageFromList": SelectImageFromList, "Bjornulf_WriteText": WriteText, "Bjornulf_LoadImagesFromSelectedFolder": LoadImagesFromSelectedFolder, # "Bjornulf_ShowWhatever": ShowWhatever, "Bjornulf_LoopModelClipVae": LoopModelClipVae, - # "Bjoenulf_RandomCheckpoint": RandomCheckpoint, "Bjornulf_LoopWriteText": LoopWriteText, "Bjornulf_LoopImages": LoopImages, "Bjornulf_RandomImage": RandomImage, @@ -135,6 +133,7 @@ NODE_DISPLAY_NAME_MAPPINGS = { "Bjornulf_RandomLineFromInput": "🎲 Random line from input", "Bjornulf_RandomTexts": "🎲 Random (Texts)", "Bjornulf_RandomModelClipVae": "🎲 Random (Model+Clip+Vae)", + "Bjornulf_RandomModelSelector": "🎲 Random Load checkpoint (Model Selector)", # "Bjornulf_PassPreviewImage": "🖼⮕ Pass Preview Image", "Bjornulf_CharacterDescriptionGenerator": "🧑📝 Character Description Generator", "Bjornulf_GreenScreenToTransparency": "🟩➜▢ Green Screen to Transparency", diff --git a/load_image_alpha.py b/load_image_alpha.py index 9a17f5f..7986c24 100644 --- a/load_image_alpha.py +++ b/load_image_alpha.py @@ -15,11 +15,10 @@ class LoadImageWithTransparency: {"image": (sorted(files), {"image_upload": True})}, } - CATEGORY = "image" - RETURN_TYPES = ("IMAGE", "MASK", "STRING") # Added "STRING" for the image path RETURN_NAMES = ("image", "mask", "image_path") FUNCTION = "load_image_alpha" + CATEGORY = "Bjornulf" def load_image_alpha(self, image): image_path = folder_paths.get_annotated_filepath(image) diff --git a/pyproject.toml b/pyproject.toml index 713a074..29997ab 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.36" +version = "0.37" license = {file = "LICENSE"} [project.urls] diff --git a/random_checkpoint.py b/random_checkpoint.py deleted file mode 100644 index decbd8d..0000000 --- a/random_checkpoint.py +++ /dev/null @@ -1,28 +0,0 @@ -import random - -class RandomCheckpoint: - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "number_of_inputs": ("INT", {"default": 2, "min": 2, "max": 10, "step": 1}), - "model_1": ("MODEL", {"forceInput": True}), - "clip_1": ("CLIP", {"forceInput": True}), - "vae_1": ("VAE", {"forceInput": True}), - "model_2": ("MODEL", {"forceInput": True}), - "clip_2": ("CLIP", {"forceInput": True}), - "vae_2": ("VAE", {"forceInput": True}), - } - } - - RETURN_TYPES = ("MODEL", "CLIP", "VAE") - FUNCTION = "random_select" - - def random_select(self, number_of_inputs, **kwargs): - selected_index = random.randint(1, number_of_inputs) - - selected_model = kwargs[f"model_{selected_index}"] - selected_clip = kwargs[f"clip_{selected_index}"] - selected_vae = kwargs[f"vae_{selected_index}"] - - return (selected_model, selected_clip, selected_vae) diff --git a/random_model_clip_vae.py b/random_model_clip_vae.py index 2d2afe5..c0f6e04 100644 --- a/random_model_clip_vae.py +++ b/random_model_clip_vae.py @@ -24,6 +24,7 @@ class RandomModelClipVae: RETURN_TYPES = ("MODEL", "CLIP", "VAE") FUNCTION = "random_select" + CATEGORY = "Bjornulf" def random_select(self, number_of_inputs, **kwargs): random.seed(kwargs.get('seed', 0)) diff --git a/random_model_selector.py b/random_model_selector.py new file mode 100644 index 0000000..d31c7e5 --- /dev/null +++ b/random_model_selector.py @@ -0,0 +1,56 @@ +import os +import random +from folder_paths import get_filename_list, get_full_path +import comfy.sd + +class RandomModelSelector: + @classmethod + def INPUT_TYPES(cls): + model_list = get_filename_list("checkpoints") + optional_inputs = {} + + for i in range(1, 11): + optional_inputs[f"model_{i}"] = (model_list, {"default": model_list[min(i-1, len(model_list)-1)]}) + + optional_inputs["seed"] = ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}) + + return { + "required": { + "number_of_models": ("INT", {"default": 3, "min": 1, "max": 20, "step": 1}), + }, + "optional": optional_inputs + } + + RETURN_TYPES = ("MODEL", "CLIP", "VAE", "STRING", "STRING", "STRING") + RETURN_NAMES = ("model", "clip", "vae", "model_path", "model_name", "model_folder") + FUNCTION = "random_select_model" + CATEGORY = "Bjornulf" + + def random_select_model(self, number_of_models, seed, **kwargs): + random.seed(seed) + + available_models = [kwargs[f"model_{i}"] for i in range(1, number_of_models + 1) if f"model_{i}" in kwargs] + + if not available_models: + raise ValueError("No models selected") + + selected_model = random.choice(available_models) + + # Extract just the name of the model (no folders and no extensions) + model_name = os.path.splitext(os.path.basename(selected_model))[0] + + # Get the full path of the selected model + model_path = get_full_path("checkpoints", selected_model) + + # Get the folder of the selected model (Hopefully people use that to organize their models...) + model_folder = os.path.basename(os.path.dirname(model_path)) + + # Load the model + loaded_objects = comfy.sd.load_checkpoint_guess_config(model_path) + + # Unpack only the values we need + model = loaded_objects[0] + clip = loaded_objects[1] + vae = loaded_objects[2] + + return (model, clip, vae, model_path, model_name, model_folder) diff --git a/screenshots/random_load_checkpoint.png b/screenshots/random_load_checkpoint.png new file mode 100644 index 0000000..aa8c7b9 Binary files /dev/null and b/screenshots/random_load_checkpoint.png differ diff --git a/web/js/random_model_selector.js b/web/js/random_model_selector.js new file mode 100644 index 0000000..f600964 --- /dev/null +++ b/web/js/random_model_selector.js @@ -0,0 +1,62 @@ +import { app } from "../../../scripts/app.js"; + +app.registerExtension({ + name: "Bjornulf.RandomModelSelector", + async nodeCreated(node) { + if (node.comfyClass === "Bjornulf_RandomModelSelector") { + const updateModelInputs = () => { + const numModelsWidget = node.widgets.find(w => w.name === "number_of_models"); + if (!numModelsWidget) return; + + const numModels = numModelsWidget.value; + const checkpointsList = node.widgets.find(w => w.name === "model_1").options.values; + + // Remove excess model widgets + node.widgets = node.widgets.filter(w => !w.name.startsWith("model_") || parseInt(w.name.split("_")[1]) <= numModels); + + // Add new model widgets if needed + for (let i = 1; i <= numModels; i++) { + const widgetName = `model_${i}`; + if (!node.widgets.find(w => w.name === widgetName)) { + const defaultIndex = Math.min(i - 1, checkpointsList.length - 1); + node.addWidget("combo", widgetName, checkpointsList[defaultIndex], () => {}, { + values: checkpointsList + }); + } + } + + // Reorder widgets + node.widgets.sort((a, b) => { + if (a.name === "number_of_models") return -1; + if (b.name === "number_of_models") return 1; + if (a.name === "seed") return 1; + if (b.name === "seed") return -1; + if (a.name.startsWith("model_") && b.name.startsWith("model_")) { + return parseInt(a.name.split("_")[1]) - parseInt(b.name.split("_")[1]); + } + return a.name.localeCompare(b.name); + }); + + node.setSize(node.computeSize()); + }; + + // Set up number_of_models widget + const numModelsWidget = node.widgets.find(w => w.name === "number_of_models"); + if (numModelsWidget) { + numModelsWidget.callback = () => { + updateModelInputs(); + app.graph.setDirtyCanvas(true); + }; + } + + // Set seed widget to hidden input + const seedWidget = node.widgets.find((w) => w.name === "seed"); + if (seedWidget) { + seedWidget.type = "HIDDEN"; + } + + // Initial update + updateModelInputs(); + } + } +});