This commit is contained in:
justumen
2024-10-03 09:28:36 +02:00
parent 2fb74b8a07
commit 0a0da98109
8 changed files with 186 additions and 4 deletions

View File

@@ -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
@@ -643,7 +645,7 @@ 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.
Check node number 41 before deciding which one to use.
### 41 - 🎲 Random Load checkpoint (Model Selector)
@@ -658,7 +660,8 @@ I always store my checkpoints in a folder with the type of the model like `SD1.5
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.
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. :
![audio sync video](screenshots/audio_sync_video_with.png)
### 53 - ♻ Loop Load checkpoint (Model Selector)
![loop model selector](screenshots/loop_model_selector.png)
**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.

View File

@@ -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
View 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)

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 609 KiB

View 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();
}
}
});