diff --git a/README.md b/README.md index d38ad71..3f33781 100644 --- a/README.md +++ b/README.md @@ -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 : . +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 : . For downloading from civitai (get token here ), 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... \ No newline at end of file +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) \ No newline at end of file diff --git a/__init__.py b/__init__.py index a0ea008..72d575a 100644 --- a/__init__.py +++ b/__init__.py @@ -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" diff --git a/combine_images.py b/combine_images.py new file mode 100644 index 0000000..14a3135 --- /dev/null +++ b/combine_images.py @@ -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 diff --git a/if_else.py b/if_else.py index 6888407..cbdf6f9 100644 --- a/if_else.py +++ b/if_else.py @@ -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") \ No newline at end of file + 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") \ No newline at end of file diff --git a/image_details.py b/image_details.py new file mode 100644 index 0000000..6454a43 --- /dev/null +++ b/image_details.py @@ -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) diff --git a/pyproject.toml b/pyproject.toml index e7009c3..f2a29e3 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.40" +version = "0.41" license = {file = "LICENSE"} [project.urls] diff --git a/random_model_selector.py b/random_model_selector.py index d31c7e5..c5ad110 100644 --- a/random_model_selector.py +++ b/random_model_selector.py @@ -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 \ No newline at end of file diff --git a/screenshots/if1.png b/screenshots/_if1.png similarity index 100% rename from screenshots/if1.png rename to screenshots/_if1.png diff --git a/screenshots/if2.png b/screenshots/_if2.png similarity index 100% rename from screenshots/if2.png rename to screenshots/_if2.png diff --git a/screenshots/if3.png b/screenshots/_if3.png similarity index 100% rename from screenshots/if3.png rename to screenshots/_if3.png diff --git a/screenshots/if4.png b/screenshots/_if4.png similarity index 100% rename from screenshots/if4.png rename to screenshots/_if4.png diff --git a/screenshots/if5.png b/screenshots/_if5.png similarity index 100% rename from screenshots/if5.png rename to screenshots/_if5.png diff --git a/screenshots/combine_images_1.png b/screenshots/combine_images_1.png new file mode 100644 index 0000000..e277ec6 Binary files /dev/null and b/screenshots/combine_images_1.png differ diff --git a/screenshots/combine_images_2.png b/screenshots/combine_images_2.png new file mode 100644 index 0000000..8252746 Binary files /dev/null and b/screenshots/combine_images_2.png differ diff --git a/screenshots/combine_images_3.png b/screenshots/combine_images_3.png new file mode 100644 index 0000000..839ad01 Binary files /dev/null and b/screenshots/combine_images_3.png differ diff --git a/screenshots/if_0.png b/screenshots/if_0.png new file mode 100644 index 0000000..991dd45 Binary files /dev/null and b/screenshots/if_0.png differ diff --git a/screenshots/if_0_1.png b/screenshots/if_0_1.png new file mode 100644 index 0000000..800dfbd Binary files /dev/null and b/screenshots/if_0_1.png differ diff --git a/screenshots/if_1.png b/screenshots/if_1.png new file mode 100644 index 0000000..0448023 Binary files /dev/null and b/screenshots/if_1.png differ diff --git a/screenshots/if_2.png b/screenshots/if_2.png new file mode 100644 index 0000000..1b003d1 Binary files /dev/null and b/screenshots/if_2.png differ diff --git a/screenshots/if_3.png b/screenshots/if_3.png new file mode 100644 index 0000000..f59b7bb Binary files /dev/null and b/screenshots/if_3.png differ diff --git a/screenshots/if_4.png b/screenshots/if_4.png new file mode 100644 index 0000000..614ac4d Binary files /dev/null and b/screenshots/if_4.png differ diff --git a/screenshots/if_5.png b/screenshots/if_5.png new file mode 100644 index 0000000..01f414a Binary files /dev/null and b/screenshots/if_5.png differ diff --git a/screenshots/image_details_1.png b/screenshots/image_details_1.png new file mode 100644 index 0000000..3a94dbf Binary files /dev/null and b/screenshots/image_details_1.png differ diff --git a/web/js/combine_images.js b/web/js/combine_images.js new file mode 100644 index 0000000..95381b8 --- /dev/null +++ b/web/js/combine_images.js @@ -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); + } + } +}); diff --git a/web/js/show_text.js b/web/js/show_text.js index 646cd2f..f8aff0d 100644 --- a/web/js/show_text.js +++ b/web/js/show_text.js @@ -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; diff --git a/write_text_advanced.py b/write_text_advanced.py index 18295ed..f2242d6 100644 --- a/write_text_advanced.py +++ b/write_text_advanced.py @@ -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}), }, }