mirror of
https://github.com/justUmen/Bjornulf_custom_nodes.git
synced 2026-03-21 20:52:11 -03:00
0.47
This commit is contained in:
16
README.md
16
README.md
@@ -1,6 +1,6 @@
|
||||
# 🔗 Comfyui : Bjornulf_custom_nodes v0.46 🔗
|
||||
|
||||
A list of 52 custom nodes for Comfyui : Display, manipulate, and edit text, images, videos, and more.
|
||||
A list of 53 custom nodes for Comfyui : Display, manipulate, and edit text, images, videos, and more.
|
||||
You can manage looping operations, generate randomized content, trigger logical conditions, pause and manually control your workflows and even work with external AI tools, like Ollama or Text To Speech.
|
||||
|
||||
# Coffee : ☕☕☕☕☕ 5/5
|
||||
@@ -42,6 +42,7 @@ You can manage looping operations, generate randomized content, trigger logical
|
||||
`38.` [♻🖼 Loop (Images)](#38----loop-images)
|
||||
`39.` [♻ Loop (✒🗔 Advanced Write Text + 🅰️ variables)](#39----loop--advanced-write-text)
|
||||
`42.` [♻ Loop (Model+Clip+Vae) - aka Checkpoint / Model](#42----loop-modelclipvae---aka-checkpoint--model)
|
||||
`53.` [♻ Loop Load checkpoint (Model Selector)](#53)
|
||||
|
||||
## 🎲 Randomization 🎲
|
||||
`3.` [✒🗔 Advanced Write Text (+ 🎲 random selection and 🅰️ variables)](#3----advanced-write-text---random-selection-and-🅰%EF%B8%8F-variables)
|
||||
@@ -234,6 +235,7 @@ cd /where/you/installed/ComfyUI && python main.py
|
||||
- **v0.44**: Allow ollama to have a cusom url in the file `ollama_ip.txt` in the comfyui custom nodes folder. Minor changes, add details/updates to README.
|
||||
- **v0.45**: Add a new node : Text scrambler (Character), change text randomly using the file `scrambler/scrambler_character.json` in the comfyui custom nodes folder.
|
||||
- **v0.46**: ❗ A lot of changes to Video nodes. Save to video is now using FLOAT for fps, not INT. (A lot of other custom nodes do that as well...) Add node to preview video, add node to convert a video path to a list of images. add node to convert a list of images to a temporary video + video_path. add node to synchronize duration of audio with video. (useful for MuseTalk) change TTS node with many new outputs ("audio_path", "full_path", "duration") to reuse with other nodes like MuseTalk, also TTS rename input to "connect_to_workflow", to avoid mistakes sending text to it.
|
||||
- **v0.47**: New node : Loop Load checkpoint (Model Selector).
|
||||
|
||||
# 📝 Nodes descriptions
|
||||
|
||||
@@ -659,6 +661,7 @@ 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.
|
||||
Node 53 is the loop version of this node.
|
||||
|
||||
### 42 - ♻ Loop (Model+Clip+Vae) - aka Checkpoint / Model
|
||||
|
||||
@@ -818,3 +821,14 @@ Here is an example without `Audio Video Sync` node (The duration of the video is
|
||||
Here is an example with `Audio Video Sync` node, notice that it is also convenient to recover the frames per second of the video, and send that to other nodes. :
|
||||
|
||||

|
||||
|
||||
### 53 - ♻ Loop Load checkpoint (Model Selector)
|
||||
|
||||

|
||||
|
||||
**Description:**
|
||||
This is the loop version of node 41. (check there for similar details)
|
||||
It will loop over all the selected checkpoints.
|
||||
|
||||
❗ The big difference with 41 is that checkpoints are preloaded in memory. You can run them all faster all at once.
|
||||
It is a good way to test multiple checkpoints quickly.
|
||||
@@ -55,9 +55,11 @@ from .audio_video_sync import AudioVideoSync
|
||||
from .video_path_to_images import VideoToImagesList
|
||||
from .images_to_video_path import ImagesListToVideo
|
||||
from .video_preview import VideoPreview
|
||||
from .loop_model_selector import LoopModelSelector
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"Bjornulf_ollamaLoader": ollamaLoader,
|
||||
"Bjornulf_LoopModelSelector": LoopModelSelector,
|
||||
"Bjornulf_VideoPreview": VideoPreview,
|
||||
"Bjornulf_ImagesListToVideo": ImagesListToVideo,
|
||||
"Bjornulf_VideoToImagesList": VideoToImagesList,
|
||||
@@ -114,6 +116,7 @@ NODE_CLASS_MAPPINGS = {
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"Bjornulf_WriteText": "✒ Write Text",
|
||||
"Bjornulf_LoopModelSelector": "♻ Loop Load checkpoint (Model Selector)",
|
||||
"Bjornulf_VideoPreview": "📹👁 Video Preview",
|
||||
"Bjornulf_ImagesListToVideo": "🖼➜📹 Images to Video path (tmp video)",
|
||||
"Bjornulf_VideoToImagesList": "📹➜🖼 Video Path to Images",
|
||||
|
||||
67
loop_model_selector.py
Normal file
67
loop_model_selector.py
Normal file
@@ -0,0 +1,67 @@
|
||||
import os
|
||||
from folder_paths import get_filename_list, get_full_path
|
||||
import comfy.sd
|
||||
|
||||
class LoopModelSelector:
|
||||
@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)]})
|
||||
|
||||
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 = "select_models"
|
||||
CATEGORY = "Bjornulf"
|
||||
OUTPUT_IS_LIST = (True, True, True, True, True, True)
|
||||
|
||||
def select_models(self, number_of_models, **kwargs):
|
||||
# Collect available models from kwargs
|
||||
available_models = [
|
||||
kwargs[f"model_{i}"] for i in range(1, number_of_models + 1) if f"model_{i}" in kwargs and kwargs[f"model_{i}"]
|
||||
]
|
||||
|
||||
# Raise an error if no models are available
|
||||
if not available_models:
|
||||
raise ValueError("No models selected")
|
||||
|
||||
models = []
|
||||
clips = []
|
||||
vaes = []
|
||||
model_paths = []
|
||||
model_names = []
|
||||
model_folders = []
|
||||
|
||||
for selected_model in available_models:
|
||||
# Get the model name (without folders or extensions)
|
||||
model_name = os.path.splitext(os.path.basename(selected_model))[0]
|
||||
|
||||
# Get the full path to the selected model
|
||||
model_path = get_full_path("checkpoints", selected_model)
|
||||
|
||||
# Get the folder name where the model is located
|
||||
model_folder = os.path.basename(os.path.dirname(model_path))
|
||||
|
||||
# Load the model using ComfyUI's checkpoint loader
|
||||
loaded_objects = comfy.sd.load_checkpoint_guess_config(model_path)
|
||||
|
||||
# Unpack only the values we need
|
||||
model, clip, vae = loaded_objects[:3]
|
||||
|
||||
models.append(model)
|
||||
clips.append(clip)
|
||||
vaes.append(vae)
|
||||
model_paths.append(model_path)
|
||||
model_names.append(model_name)
|
||||
model_folders.append(model_folder)
|
||||
|
||||
return (models, clips, vaes, model_paths, model_names, model_folders)
|
||||
@@ -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.46"
|
||||
version = "0.47"
|
||||
license = {file = "LICENSE"}
|
||||
|
||||
[project.urls]
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 698 KiB After Width: | Height: | Size: 778 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 712 KiB After Width: | Height: | Size: 793 KiB |
BIN
screenshots/loop_model_selector.png
Normal file
BIN
screenshots/loop_model_selector.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 609 KiB |
98
web/js/loop_model_selector.js
Normal file
98
web/js/loop_model_selector.js
Normal file
@@ -0,0 +1,98 @@
|
||||
import { app } from "../../../scripts/app.js";
|
||||
|
||||
app.registerExtension({
|
||||
name: "Bjornulf.LoopModelSelector",
|
||||
async nodeCreated(node) {
|
||||
if (node.comfyClass === "Bjornulf_LoopModelSelector") {
|
||||
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 integer input
|
||||
const seedWidget = node.widgets.find((w) => w.name === "seed");
|
||||
if (seedWidget) {
|
||||
seedWidget.type = "HIDDEN"; // Hide seed widget after restoring saved state
|
||||
}
|
||||
|
||||
// Handle deserialization
|
||||
const originalOnConfigure = node.onConfigure;
|
||||
node.onConfigure = function(info) {
|
||||
if (originalOnConfigure) {
|
||||
originalOnConfigure.call(this, info);
|
||||
}
|
||||
|
||||
// Restore model widgets based on saved properties
|
||||
const savedProperties = info.properties;
|
||||
if (savedProperties) {
|
||||
Object.keys(savedProperties).forEach(key => {
|
||||
if (key.startsWith("model_")) {
|
||||
const widgetName = key;
|
||||
const widgetValue = savedProperties[key];
|
||||
const existingWidget = node.widgets.find(w => w.name === widgetName);
|
||||
if (existingWidget) {
|
||||
existingWidget.value = widgetValue;
|
||||
} else {
|
||||
node.addWidget("combo", widgetName, widgetValue, () => {}, {
|
||||
values: node.widgets.find(w => w.name === "model_1").options.values
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Ensure seed is a valid integer
|
||||
const seedWidget = node.widgets.find(w => w.name === "seed");
|
||||
if (seedWidget && isNaN(parseInt(seedWidget.value))) {
|
||||
seedWidget.value = 0; // Set a default value if invalid
|
||||
}
|
||||
|
||||
// Update model inputs after restoring saved state
|
||||
updateModelInputs();
|
||||
};
|
||||
|
||||
// Initial update
|
||||
updateModelInputs();
|
||||
}
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user