This commit is contained in:
justumen
2024-09-22 17:27:56 +02:00
parent 37520a9366
commit 6a9d31022a
26 changed files with 455 additions and 51 deletions

129
README.md
View File

@@ -1,4 +1,4 @@
# 🔗 Comfyui : Bjornulf_custom_nodes v0.39 🔗
# 🔗 Comfyui : Bjornulf_custom_nodes v0.41 🔗
# Coffee : ☕☕☕☕☕ 5/5
@@ -33,7 +33,7 @@ huggingface-cli download comfyanonymous/flux_text_encoders clip_l.safetensors --
huggingface-cli download comfyanonymous/flux_text_encoders t5xxl_fp16.safetensors --local-dir /workspace/ComfyUI/models/clip
huggingface-cli download black-forest-labs/FLUX.1-dev ae.safetensors --local-dir /workspace/ComfyUI/models/vae
```
To use Flux you can just drag and drop in your browser the .json from my github repo : `workflows/FLUX_dev_troll.json`, direct link : <https://github.com/justUmen/ComfyUI-BjornulfNodes/blob/main/workflows/FLUX_dev_troll.json>.
To use Flux you can just drag and drop in your browser comfyui interface the .json from my github repo : `workflows/FLUX_dev_troll.json`, direct link : <https://github.com/justUmen/ComfyUI-BjornulfNodes/blob/main/workflows/FLUX_dev_troll.json>.
For downloading from civitai (get token here <https://civitai.com/user/account>), just copy/paste the link of checkpoint you want to download and use something like that, with your token in URL :
```
@@ -42,11 +42,46 @@ wget --content-disposition -P /workspace/ComfyUI/models/checkpoints "https://civ
```
If you have any issues with this template from Runpod, please let me know, I'm here to help. 😊
# Dependencies
# 🏗 Dependencies (nothing to do for runpod ☁)
## 🪟🐍 Windows : Install dependencies on windows with embedded python (portable version)
First you need to find this python_embedded `python.exe`, then you can right click or shift + right click inside the folder in your file manager to open a terminal there.
This is where I have it, with the command you need :
`H:\ComfyUI_windows_portable\python_embeded> .\python.exe -m pip install pydub ollama`
When you have to install something you can retake the same code and install the dependency you want :
`.\python.exe -m pip install whateveryouwant`
You can then run comfyui.
## 🐧🐍 Linux : Install dependencies (without venv, not recommended)
- `pip install ollama` (you can also install ollama if you want : https://ollama.com/download) - You don't need to really install it if you don't want to use my ollama node. (BUT you need to run `pip install ollama`)
- `pip install pydub` (for TTS node)
## 🐧🐍 Linux : Install dependencies with python virtual environment (venv)
If you want to use a python virtual environment only for comfyUI, which I recommended, you can do that for example (also pre-install pip) :
```
sudo apt-get install python3-venv python3-pip
python3 -m venv /the/path/you/want/venv/bjornulf_comfyui
```
Once you have your environment in this new folder, you can activate it with and install dependencies inside :
```
source /the/path/you/want/venv/bjornulf_comfyui/bin/activate
pip install ollama pydub
```
Then you can start comfyui with this environment (notice that you need to re-activate it each time you want to launch comfyui) :
```
cd /where/you/installed/ComfyUI && python main.py
```
# Nodes menu
1. [👁 Show (Text, Int, Float)](#1----show-text-int-float)
@@ -64,15 +99,15 @@ If you have any issues with this template from Runpod, please let me know, I'm h
13. [📏 Resize Exact](#1314------resize-and-save-exact-name-%EF%B8%8F)
14. [🖼 Save Exact name](#1314------resize-and-save-exact-name-%EF%B8%8F)
15. [💾 Save Text](#15----save-text)
16. [🖼💬 Save image for Bjornulf LobeChat](#16----save-image-for-bjornulf-lobechat-for-my-custom-lobe-chat)
17. [🖼 Save image as `tmp_api.png` Temporary API](#17----save-image-as-tmp_apipng-temporary-api-%EF%B8%8F)
18. [🖼📁 Save image to a chosen folder name](#18----save-image-to-a-chosen-folder-name)
16. [💾🖼💬 Save image for Bjornulf LobeChat](#16-----save-image-for-bjornulf-lobechat-for-my-custom-lobe-chat)
17. [💾🖼 Save image as `tmp_api.png` Temporary API](#17-----save-image-as-tmp_apipng-temporary-api-%EF%B8%8F)
18. [💾🖼📁 Save image to a chosen folder name](#18-----save-image-to-a-chosen-folder-name)
19. [🦙 Ollama](#19----ollama)
20. [📹 Video Ping Pong](#20----video-ping-pong)
21. [📹 Images to Video](#21----images-to-video)
22. [🔲 Remove image Transparency (alpha)](#22----remove-image-transparency-alpha)
23. [🔲 Image to grayscale (black & white)](#23----image-to-grayscale-black--white)
24. [🖼+🖼 Combine images (Background + Overlay)](#24----combine-images-background--overlay)
24. [🖼+🖼 Stack two images (Background + Overlay)](#24----combine-images-background--overlay)
25. [🟩➜▢ Green Screen to Transparency](#25----green-screen-to-transparency)
26. [🎲 Random line from input](#26----random-line-from-input)
27. [♻ Loop (All Lines from input)](#27----loop-all-lines-from-input)
@@ -93,7 +128,7 @@ If you have any issues with this template from Runpod, please let me know, I'm h
42. [♻ Loop (Model+Clip+Vae) - aka Checkpoint / Model](#42----loop-modelclipvae---aka-checkpoint--model)
43. [📂🖼 Load Images from output folder](#43----load-images-from-output-folder)
44. [🖼🔍 Select an Image, Pick](#44----select-an-image-pick)
45. [🔀 If-Else (input == compare_with)](#45----if-else-input--compare_with)
45. [🔀 If-Else (input / compare_with)](#45----if-else-input--compare_with)
# 📝 Changelog
@@ -140,6 +175,7 @@ If you have any issues with this template from Runpod, please let me know, I'm h
- **v0.38**: New node : If-Else logic. (input == compare_with), examples with different latent space size. +fix some deserialization issues.
- **v0.39**: Add variables management to Advanced Write Text node.
- **v0.40**: Add variables management to Loop Advanced Write Text node. Add menu for all nodes to the README.
- **v0.41**: Two new nodes : image details and combine images. Also ❗ Big changes to the If-Else node. (+many minor changes)
# 📝 Nodes descriptions
@@ -272,7 +308,7 @@ Resize an image to exact dimensions. The other node will save the image to the e
**Description:**
Save the given text input to a file. Useful for logging and storing text data.
## 16 - 🖼💬 Save image for Bjornulf LobeChat (❗For my custom [lobe-chat](https://github.com/justUmen/Bjornulf_lobe-chat)❗)
## 16 - 💾🖼💬 Save image for Bjornulf LobeChat (❗For my custom [lobe-chat](https://github.com/justUmen/Bjornulf_lobe-chat)❗)
![Save Bjornulf Lobechat](screenshots/save_bjornulf_lobechat.png)
**Description:**
@@ -282,13 +318,13 @@ The name will start at `api_00001.png`, then `api_00002.png`, etc...
It will also create a link to the last generated image at the location `output/BJORNULF_API_LAST_IMAGE.png`.
This link will be used by my custom lobe-chat to copy the image inside the lobe-chat project.
## 17 - 🖼 Save image as `tmp_api.png` Temporary API ⚠️💣
## 17 - 💾🖼 Save image as `tmp_api.png` Temporary API ⚠️💣
![Save Temporary API](screenshots/save_tmp_api.png)
**Description:**
Save image for short-term use : ./output/tmp_api.png ⚠️💣
## 18 - 🖼📁 Save image to a chosen folder name
## 18 - 💾🖼📁 Save image to a chosen folder name
![Save Temporary API](screenshots/save_image_to_folder.png)
**Description:**
@@ -333,11 +369,11 @@ Convert an image to grayscale (black & white)
Example : I sometimes use it with Ipadapter to disable color influence.
But you can sometimes also want a black and white image...
## 24 - 🖼+🖼 Combine images (Background + Overlay)
![Combine Images](screenshots/combine_background_overlay.png)
## 24 - 🖼+🖼 Stack two images (Background + Overlay)
![Superpose Images](screenshots/combine_background_overlay.png)
**Description:**
Combine two images into a single image : a background and one (or several) transparent overlay. (allow to have a video there, just send all the frames and recombine them after.)
Stack two images into a single image : a background and one (or several) transparent overlay. (allow to have a video there, just send all the frames and recombine them after.)
Update 0.11 : Add option to move vertically and horizontally. (from -50% to 150%)
❗ Warning : For now, `background` is a static image. (I will allow video there later too.)
⚠️ Warning : If you want to directly load the image with transparency, use my node `🖼 Load Image with Transparency ▢` instead of the `Load Image` node.
@@ -563,7 +599,6 @@ Loop over all the trios from several checkpoint node.
**Description:**
Quickly select all images from a folder inside the output folder. (Not recursively.)
So... As you can see from the screenshot the images are split based on their resolution.
It is not a choice I made, it is something that is part of the comfyui environment.
It's also not possible to edit dynamically the number of outputs, so I just picked a number : 4.
The node will separate the images based on their resolution, so with this node you can have 4 different resolutions per folder. (If you have more than that, maybe you should have another folder...)
To avoid error or crash if you have less than 4 resolutions in a folder, the node will just output white tensors. (white square image.)
@@ -578,6 +613,8 @@ 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)
⚠️ If you really want to regroup all the images in one flow, you can use my node 47 `Combine images` to put them all together.
### 44 - 🖼🔍 Select an Image, Pick
![pick input](screenshots/select_image.png)
@@ -589,29 +626,71 @@ Useful in combination with my Load images from folder and preview image nodes.
You can also of course make a group node, like this one, which is the same as the screenshot above :
![pick input](screenshots/select_image_group.png)
### 45 - 🔀 If-Else (input == compare_with)
### 45 - 🔀 If-Else (input / compare_with)
![if else](screenshots/if_0.png)
![if else](screenshots/if1.png)
**Description:**
If the `input` given is equal to the `compare_with` given in the widget, it will forward `send_if_true`, otherwise it will forward `send_if_false`.
If the `input` given is equal to the `compare_with` given in the widget, it will forward `send_if_true`, otherwise it will forward `send_if_false`. (If no `send_if_false` it will return `None`.)
You can forward anything, below is an example of forwarding a different size of latent space depending if it's SDXL or not.
![if else](screenshots/if2.png)
![if else](screenshots/if_0_1.png)
Here is an example of the node with all outputs displayed with Show text nodes :
![if else](screenshots/if_1.png)
`send_if_false` is optional, if not connected, it will be replaced by `None`.
![if else](screenshots/if_2.png)
If-Else are chainables, just connect `output` to `send_if_false`.
⚠️ Always simply test `input` with `compare_with`, and connect the desired value to `send_if_true`. ⚠️
Here a simple example with 2 If-Else nodes (choose between 3 different resolutions). ❗ Notice the same write text node is connected to both If-Else nodes input :
Here a simple example with 2 If-Else nodes (choose between 3 different resolutions).
❗ Notice that the same write text node is connected to both If-Else nodes input :
![if else](screenshots/if3.png)
![if else](screenshots/if_3.png)
Let's take a similar example but let's use my Write loop text node to display all 3 types once :
![if else](screenshots/if4.png)
![if else](screenshots/if_4.png)
If you understood the previous examples, here is a complete example that will create 3 images, landscape, portrait and normal :
If you understood the previous examples, here is a complete example that will create 3 images, landscape, portrait and square :
![if else](screenshots/if5.png)
![if else](screenshots/if_5.png)
Workflow is hidden for simplicity, but is very basic, just connect latent to Ksampler, nothing special.)
You can also connect the same advanced loop write text node with my save folder node to save the images (landscape/portrait/normal) in separate folders, but you do you...
You can also connect the same advanced loop write text node with my save folder node to save the images (landscape/portrait/square) in separate folders, but you do you...
### 46 - 🖼🔍 Image Details
**Description:**
Display the details of an image. (width, height, has_transparency, orientation, type)
`RGBA` is considered as having transparency, `RGB` is not.
`orientation` can be `landscape`, `portrait` or `square`.
![image details](screenshots/image_details_1.png)
### 47 - 🖼🔗 Combine Images
**Description:**
Combine multiple images (A single image or a list of images.)
There are two types of logic to "combine images". With "all_in_one" enabled, it will combine all the images into one tensor.
Otherwise it will send the images one by one. (check examples below) :
This is an example of the "all_in_one" option disabled :
![combine images](screenshots/combine_images_1.png)
But for example, if you want to use my node `select an image, pick`, you need to enable `all_in_one` and the images must all have the same resolution.
![combine images](screenshots/combine_images_2.png)
You can notice that there is no visible difference when you use `all_in_one` with `preview image` node. (this is why I added the `show text` node, not that show text will make it blue, because it's an image/tensor.)
When you use `combine image` node, you can actually also send many images at once, it will combine them all.
Here is an example with `Load images from folder` node, `Image details` node and `Combine images` node. (Of course it can't have `all_in_one` set to True in this situation because the images have different resolutions) :
![combine images](screenshots/combine_images_3.png)

View File

@@ -50,6 +50,8 @@ from .load_images_from_folder import LoadImagesFromSelectedFolder
from .select_image_from_list import SelectImageFromList
from .random_model_selector import RandomModelSelector
from .if_else import IfElse
from .image_details import ImageDetails
from .combine_images import CombineImages
# from .pass_preview_image import PassPreviewImage
# from .check_black_image import CheckBlackImage
@@ -59,6 +61,8 @@ from .if_else import IfElse
NODE_CLASS_MAPPINGS = {
# "Bjornulf_CustomStringType": CustomStringType,
"Bjornulf_ollamaLoader": ollamaLoader,
"Bjornulf_CombineImages": CombineImages,
"Bjornulf_ImageDetails": ImageDetails,
"Bjornulf_IfElse": IfElse,
"Bjornulf_RandomModelSelector": RandomModelSelector,
"Bjornulf_SelectImageFromList": SelectImageFromList,
@@ -149,14 +153,13 @@ NODE_DISPLAY_NAME_MAPPINGS = {
# "Bjornulf_ShowFloat": "👁 Show (Float)",
"Bjornulf_ImageMaskCutter": "🖼✂ Cut Image with Mask",
"Bjornulf_LoadImageWithTransparency": "🖼 Load Image with Transparency ▢",
"Bjornulf_CombineBackgroundOverlay": "🖼+🖼 Combine images (Background+Overlay alpha)",
"Bjornulf_CombineBackgroundOverlay": "🖼+🖼 Stack two images (Background+Overlay alpha)",
"Bjornulf_GrayscaleTransform": "🖼➜🔲 Image to grayscale (black & white)",
"Bjornulf_RemoveTransparency": "▢➜⬛ Remove image Transparency (alpha)",
"Bjornulf_ResizeImage": "📏 Resize Image",
"Bjornulf_SaveImagePath": "🖼 Save Image (exact path, exact name) ⚠️💣",
"Bjornulf_SaveImageToFolder": "🖼📁 Save Image(s) to a folder",
"Bjornulf_SaveTmpImage": "🖼 Save Image (tmp_api.png) ⚠️💣",
# "Bjornulf_SaveApiImage": "🖼 Save Image (./output/api_00001.png...)",
"Bjornulf_SaveImagePath": "💾🖼 Save Image (exact path, exact name) ⚠️💣",
"Bjornulf_SaveImageToFolder": "💾🖼📁 Save Image(s) to a folder",
"Bjornulf_SaveTmpImage": "💾🖼 Save Image (tmp_api.png) ⚠️💣",
"Bjornulf_SaveText": "💾 Save Text",
# "Bjornulf_LoadText": "📥 Load Text",
"Bjornulf_CombineTexts": "🔗 Combine (Texts)",
@@ -169,7 +172,9 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"Bjornulf_PauseResume": "⏸️ Paused. Resume or Stop, Pick 👇",
"Bjornulf_LoadImagesFromSelectedFolder": "📂🖼 Load Images from output folder",
"Bjornulf_SelectImageFromList": "🖼🔍 Select an Image, Pick",
"Bjornulf_IfElse": "🔀 If-Else (input == compare_with)",
"Bjornulf_IfElse": "🔀 If-Else (input / compare_with)",
"Bjornulf_ImageDetails": "🖼🔍 Image Details",
"Bjornulf_CombineImages": "🖼🔗 Combine Images",
}
WEB_DIRECTORY = "./web"

86
combine_images.py Normal file
View File

@@ -0,0 +1,86 @@
import torch
import numpy as np
import logging
class CombineImages:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"number_of_images": ("INT", {"default": 2, "min": 1, "max": 50, "step": 1}),
"all_in_one": ("BOOLEAN", {"default": False}),
"image_1": ("IMAGE",),
},
"hidden": {
**{f"image_{i}": ("IMAGE",) for i in range(2, 51)}
}
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "all_in_one_images"
OUTPUT_NODE = True
CATEGORY = "Bjornulf"
def all_in_one_images(self, number_of_images, all_in_one, ** kwargs):
images = [kwargs[f"image_{i}"] for i in range(1, number_of_images + 1) if f"image_{i}" in kwargs]
for i, img in enumerate(images):
logging.info(f"Image {i+1} shape: {img.shape}, dtype: {img.dtype}, min: {img.min()}, max: {img.max()}")
if all_in_one:
# Check if all images have the same shape
shapes = [img.shape for img in images]
if len(set(shapes)) > 1:
raise ValueError("All images must have the same resolution to use all_in_one. "
f"Found different shapes: {shapes}")
# Convert images to float32 and scale to 0-1 range if necessary
processed_images = []
for img in images:
if isinstance(img, np.ndarray):
if img.dtype == np.uint8:
img = img.astype(np.float32) / 255.0
elif img.dtype == np.bool_:
img = img.astype(np.float32)
elif isinstance(img, torch.Tensor):
if img.dtype == torch.uint8:
img = img.float() / 255.0
elif img.dtype == torch.bool:
img = img.float()
# Ensure the image is 3D (height, width, channels)
if img.ndim == 4:
img = img.squeeze(0)
processed_images.append(img)
# Stack all images along a new dimension
if isinstance(processed_images[0], np.ndarray):
all_in_oned = np.stack(processed_images)
all_in_oned = torch.from_numpy(all_in_oned)
else:
all_in_oned = torch.stack(processed_images)
# Ensure the output is in the format expected by the preview node
# (batch, height, width, channels)
if all_in_oned.ndim == 3:
all_in_oned = all_in_oned.unsqueeze(0)
if all_in_oned.shape[-1] != 3 and all_in_oned.shape[-1] != 4:
all_in_oned = all_in_oned.permute(0, 2, 3, 1)
return (all_in_oned,)
else:
# Return a single tuple containing all images (original behavior)
return (images,)
@classmethod
def IS_CHANGED(cls, **kwargs):
return float("NaN")
@classmethod
def VALIDATE_INPUTS(cls, ** kwargs):
if kwargs['all_in_one']:
cls.OUTPUT_IS_LIST = (False,)
else:
cls.OUTPUT_IS_LIST = (True,)
return True

View File

@@ -7,20 +7,97 @@ class IfElse:
def INPUT_TYPES(cls):
return {
"required": {
"input": ("STRING", {"forceInput": True, "multiline": False}),
"input": (Everything("*"), {"forceInput": True, "multiline": False}),
"input_type": ([
"STRING: input EQUAL TO compare_with",
"STRING: input NOT EQUAL TO compare_with",
"BOOLEAN: input IS TRUE",
"NUMBER: input GREATER THAN compare_with",
"NUMBER: input GREATER OR EQUAL TO compare_with",
"NUMBER: input LESS THAN compare_with",
"NUMBER: input LESS OR EQUAL TO compare_with"
], {"default": "STRING: input EQUAL TO compare_with"}),
"send_if_true": (Everything("*"),),
"send_if_false": (Everything("*"),),
"compare_with": ("STRING", {"multiline": False}),
},
"optional": {
"send_if_false": (Everything("*"),),
}
}
RETURN_TYPES = (Everything("*"),"STRING")
RETURN_NAMES = ("output","true_or_false")
RETURN_TYPES = (Everything("*"), Everything("*"), "STRING", "STRING", "STRING")
RETURN_NAMES = ("output", "rejected", "input_type", "true_or_false", "details")
FUNCTION = "if_else"
CATEGORY = "Bjornulf"
def if_else(self, input, send_if_true, send_if_false, compare_with):
if input == compare_with:
return (send_if_true,"True")
def if_else(self, input, send_if_true, compare_with, input_type, send_if_false=None):
result = False
input_type_str = "STRING"
details = f"input: {input}\ncompare_with: {compare_with}\n"
error_message = ""
# Input validation
if input_type.startswith("NUMBER:"):
try:
float(input)
float(compare_with)
except ValueError:
error_message = "If-Else ERROR: For numeric comparisons, both \"input\" and \"compare_with\" must be valid numbers.\n"
elif input_type == "BOOLEAN: input IS TRUE":
if str(input).lower() not in ("true", "false", "1", "0", "yes", "no", "y", "n", "on", "off"):
error_message = "If-Else ERROR: For boolean check, \"input\" must be a recognizable boolean value.\n"
if error_message:
details = error_message + "\n" + details
details += "\nContinuing with default string comparison."
input_type = "STRING: input EQUAL TO compare_with"
if input_type == "STRING: input EQUAL TO compare_with":
result = str(input) == str(compare_with)
details += f"\nCompared strings: '{input}' == '{compare_with}'"
elif input_type == "STRING: input NOT EQUAL TO compare_with":
result = str(input) != str(compare_with)
details += f"\nCompared strings: '{input}' != '{compare_with}'"
elif input_type == "BOOLEAN: input IS TRUE":
result = str(input).lower() in ("true", "1", "yes", "y", "on")
details += f"\nChecked if '{input}' is considered True"
else: # Numeric comparisons
try:
input_num = float(input)
compare_num = float(compare_with)
if input_type == "NUMBER: input GREATER THAN compare_with":
result = input_num > compare_num
details += f"\nCompared numbers: {input_num} > {compare_num}"
elif input_type == "NUMBER: input GREATER OR EQUAL TO compare_with":
result = input_num >= compare_num
details += f"\nCompared numbers: {input_num} >= {compare_num}"
elif input_type == "NUMBER: input LESS THAN compare_with":
result = input_num < compare_num
details += f"\nCompared numbers: {input_num} < {compare_num}"
elif input_type == "NUMBER: input LESS OR EQUAL TO compare_with":
result = input_num <= compare_num
details += f"\nCompared numbers: {input_num} <= {compare_num}"
input_type_str = "FLOAT" if "." in str(input) else "INT"
except ValueError:
result = str(input) == str(compare_with)
details += f"\nUnexpected error in numeric conversion, compared as strings: '{input}' == '{compare_with}'"
if result:
output = send_if_true
rejected = send_if_false if send_if_false is not None else None
else:
return (send_if_false,"False")
output = send_if_false if send_if_false is not None else None
rejected = send_if_true
result_str = str(result)
details += f"\nResult: {result_str}"
details += f"\nReturned value to {'output' if result else 'rejected'}"
details += f"\n\noutput: {output}"
details += f"\nrejected: {rejected}"
return (output, rejected, input_type_str, result_str, details)
@classmethod
def IS_CHANGED(cls, input, send_if_true, compare_with, input_type, send_if_false=None):
return float("NaN")

98
image_details.py Normal file
View File

@@ -0,0 +1,98 @@
import torch
import numpy as np
from PIL import Image
import io
class ImageDetails:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image_input": ("IMAGE",),
},
}
RETURN_TYPES = ("INT", "INT", "BOOL", "STRING", "STRING", "STRING")
RETURN_NAMES = ("WIDTH", "HEIGHT", "HAS_TRANSPARENCY", "ORIENTATION", "TYPE", "ALL")
FUNCTION = "show_image_details"
OUTPUT_NODE = True
CATEGORY = "Bjornulf"
def show_image_details(self, image_input):
if isinstance(image_input, torch.Tensor):
is_tensor = True
input_type = "tensor"
# Ensure the tensor is on CPU and convert to numpy
image_input = image_input.cpu().numpy()
elif isinstance(image_input, (bytes, bytearray)):
is_tensor = False
input_type = "bytes"
image_input = [image_input] # Wrap single bytes object in a list
else:
is_tensor = False
input_type = "bytes"
all_widths, all_heights, all_transparencies, all_details, all_orientations = [], [], [], [], []
if is_tensor:
# Handle tensor images
if len(image_input.shape) == 5: # (batch, 1, channels, height, width)
image_input = np.squeeze(image_input, axis=1)
batch_size = image_input.shape[0]
for i in range(batch_size):
image = image_input[i]
# Ensure the image is in HxWxC format
if image.shape[0] == 3 or image.shape[0] == 4: # If it's in CxHxW format
image = np.transpose(image, (1, 2, 0)) # Change to HxWxC
# Normalize to 0-255 range if necessary
if image.max() <= 1:
image = (image * 255).astype('uint8')
else:
image = image.astype('uint8')
pil_image = Image.fromarray(image)
self.process_image(pil_image, input_type, all_widths, all_heights, all_transparencies, all_details, all_orientations)
else:
# Handle bytes-like objects
batch_size = len(image_input)
for i in range(batch_size):
pil_image = Image.open(io.BytesIO(image_input[i]))
self.process_image(pil_image, input_type, all_widths, all_heights, all_transparencies, all_details, all_orientations)
# Combine all details into a single string
combined_details = "\n".join(all_details)
# Return the details of the first image, plus the combined details string
return (all_widths[0], all_heights[0], all_transparencies[0], all_orientations[0],
input_type, combined_details)
def process_image(self, pil_image, input_type, all_widths, all_heights, all_transparencies, all_details, all_orientations):
# Get image details
width, height = pil_image.size
has_transparency = pil_image.mode in ('RGBA', 'LA') or \
(pil_image.mode == 'P' and 'transparency' in pil_image.info)
# Determine orientation
if width > height:
orientation = "landscape"
elif height > width:
orientation = "portrait"
else:
orientation = "square"
# Prepare the ALL string
details = f"\nType: {input_type}"
details += f"\nWidth: {width}"
details += f"\nHeight: {height}"
details += f"\nLoaded with transparency: {has_transparency}"
details += f"\nImage Mode: {pil_image.mode}"
details += f"\nOrientation: {orientation}\n"
all_widths.append(width)
all_heights.append(height)
all_transparencies.append(has_transparency)
all_details.append(details)
all_orientations.append(orientation)

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.40"
version = "0.41"
license = {file = "LICENSE"}
[project.urls]

View File

@@ -28,29 +28,32 @@ class RandomModelSelector:
def random_select_model(self, number_of_models, seed, **kwargs):
random.seed(seed)
# 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}"]
]
available_models = [kwargs[f"model_{i}"] for i in range(1, number_of_models + 1) if f"model_{i}" in kwargs]
# Raise an error if no models are available
if not available_models:
raise ValueError("No models selected")
# Randomly select a model
selected_model = random.choice(available_models)
# Extract just the name of the model (no folders and no extensions)
# Get the model name (without folders or extensions)
model_name = os.path.splitext(os.path.basename(selected_model))[0]
# Get the full path of the selected model
# Get the full path to 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...)
# Get the folder name where the model is located
model_folder = os.path.basename(os.path.dirname(model_path))
# Load the model
# 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 = loaded_objects[0]
clip = loaded_objects[1]
vae = loaded_objects[2]
model, clip, vae = loaded_objects[:3]
return (model, clip, vae, model_path, model_name, model_folder)
return model, clip, vae, model_path, model_name, model_folder

View File

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 108 KiB

View File

Before

Width:  |  Height:  |  Size: 195 KiB

After

Width:  |  Height:  |  Size: 195 KiB

View File

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 111 KiB

View File

Before

Width:  |  Height:  |  Size: 203 KiB

After

Width:  |  Height:  |  Size: 203 KiB

View File

Before

Width:  |  Height:  |  Size: 444 KiB

After

Width:  |  Height:  |  Size: 444 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 299 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 853 KiB

BIN
screenshots/if_0.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

BIN
screenshots/if_0_1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

BIN
screenshots/if_1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

BIN
screenshots/if_2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

BIN
screenshots/if_3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

BIN
screenshots/if_4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

BIN
screenshots/if_5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

52
web/js/combine_images.js Normal file
View File

@@ -0,0 +1,52 @@
import { app } from "../../../scripts/app.js";
app.registerExtension({
name: "Bjornulf.CombineImages",
async nodeCreated(node) {
if (node.comfyClass === "Bjornulf_CombineImages") {
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 text 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 text 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 text 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);
}
}
});

View File

@@ -47,6 +47,10 @@ app.registerExtension({
color = '#0096FF'; // Integer
} else if (/^-?\d*\.?\d+$/.test(value)) {
color = 'orange'; // Float
} else if (value.startsWith("If-Else ERROR: ")) {
color = 'red'; // If-Else ERROR lines
} else if (value.startsWith("tensor(")) {
color = '#0096FF'; // Lines starting with "tensor("
}
w.inputEl.style.color = color;

View File

@@ -11,7 +11,7 @@ class WriteTextAdvanced:
"text": ("STRING", {"multiline": True, "lines": 10}),
},
"optional": {
"variables": ("STRING", {"multiline": True, "lines": 5}),
"variables": ("STRING", {"multiline": True, "forceInput": True}),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
},
}