diff --git a/README.md b/README.md index c351f09..43b920b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# πŸ”— Comfyui : Bjornulf_custom_nodes v0.64 πŸ”— +# πŸ”— Comfyui : Bjornulf_custom_nodes v0.65 πŸ”— -A list of 110 custom nodes for Comfyui : Display, manipulate, create and edit text, images, videos, loras, generate characters and more. +A list of 116 custom nodes for Comfyui : Display, manipulate, create and edit text, images, videos, loras, generate characters 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 @@ -36,6 +36,12 @@ Support me and my work : ❀️❀️❀️ ❀️ `67.` [πŸ“βžœβœ¨ Text to Anything](#67----text-to-anything) `68.` [βœ¨βžœπŸ“ Anything to Text](#68----anything-to-text) `75.` [πŸ“βžœπŸ“ Replace text](#75----replace-text) +`15.` [πŸ’Ύ Save Text](#15----save-text) +`111.` [βœ¨βžœπŸ”’ Anything to Int](#) +`112.` [βœ¨βžœπŸ”’ Anything to Float](#) +`113.` [πŸ“πŸ”ͺ Text split in 5](#) +`115.` [πŸ“₯ Load Text From Bjornulf Folder](#) +`116.` [πŸ“₯ Load Text From Path](#) ## πŸ”₯ Text Generator πŸ”₯ `81.` [πŸ”₯πŸ“ Text Generator πŸ“πŸ”₯](#81----text-generator-) @@ -131,6 +137,7 @@ Support me and my work : ❀️❀️❀️ ❀️ ## πŸš€ Load loras πŸš€ `54.` [β™» Loop Lora Selector](#54----loop-lora-selector) `55.` [🎲 Random Lora@ Selector](#55----random-lora-selector) +`114.` [πŸ“₯πŸ‘‘ Load Lora with Path]() ## ☁ Image Creation : API / cloud / remote ☁ `106.` [☁🎨 API Image Generator (FalAI) ☁](#10) @@ -345,6 +352,7 @@ cd /where/you/installed/ComfyUI && python main.py - **0.62**: MASSIVE update, Text Generator nodes. (15 nodes), API nodes generate (civitai / black forest labs / fal.ai), API civit ai download models nodes, lora - **0.63**: delete long file, useless - **0.64**: remove "import wget", added some keywords to text generators. +- **0.65**: ❗Breaking changes : Combine Text inputs are now all optional (PLease remake your nodes, sorry.) Add 6 new nodes : any2int, any2float, load text from folder, load text from path, load lora from path. Also upgraded the Save text node. # πŸ“ Nodes descriptions @@ -491,6 +499,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. If the file already exist, it will add the text at the end of the file. +I recommend you to keep saving them in "Bjornulf/Text" (Which is in the Comfyui folder, next to output), this is where the node 116 `Load text from folder` is looking for text files. ![Save Text](screenshots/save_text.png) @@ -1574,4 +1583,61 @@ Generate an image with the Black Forest Labs API. (flux) **Description:** Generate an image with the Stability API. (sd3) -![api stability](screenshots/api_stability.png) \ No newline at end of file +![api stability](screenshots/api_stability.png) + +#### 111 - βœ¨βžœπŸ”’ Anything to Int + +**Description:** + +Just convert anything to a valid INT. (integer) + +![Anything to Int](screenshots/anything_to_int.png) + +#### 112 - βœ¨βžœπŸ”’ Anything to Float + +**Description:** + +Just convert anything to a valid FLOAT. (floating number) + +![Anything to Float](screenshots/anything_to_float.png) + +#### 113 - πŸ“πŸ”ͺ Text split in 5 + +**Description:** + +Take a single input and split it in 5 with a delimiter (newline by default). +It can also ignore everything on the left side of a `=` symbol if you want to use a "variable type format". + +![Text split in 5](screenshots/split_in_5.png) + +#### 114 - πŸ“₯πŸ‘‘ Load Lora with Path + +**Description:** + +Load a lora by using it's path. + +![load lora with path](screenshots/load_lora_with_path.png) + +Here is a complex practical example using node 113, 114, 112 : +![load lora with path](screenshots/load_lora_with_path_COMPLEX.png) + +#### 115 - πŸ“₯ Load Text From Bjornulf Folder + +**Description:** + +Just select a file from the folder `Bjornulf/Text` folder, it will recover its content. +It is made to be used with node 15 `Save Text`. + +![Load Text](screenshots/load_text_from_Bjornulf.png) + +#### 116 - πŸ“₯ Load Text From Path + +**Description:** + +Just give the path of the file, it will recover its content. + +![Load Text](screenshots/load_text_requirements.png) + +If you want, with `Load Text From Path` you can also recover the elements in "Bjornulf/Text" by just adding it: + +![Load Text](screenshots/load_text_PATH.png) diff --git a/__init__.py b/__init__.py index 60bba4c..45c1eb6 100644 --- a/__init__.py +++ b/__init__.py @@ -78,6 +78,8 @@ from .ollama_system_job import OllamaSystemJobSelector from .speech_to_text import SpeechToText from .text_to_anything import TextToAnything from .anything_to_text import AnythingToText +from .anything_to_int import AnythingToInt +from .anything_to_float import AnythingToFloat from .add_line_numbers import AddLineNumbers from .ffmpeg_convert import ConvertVideo # from .hiresfix import HiResFix @@ -88,9 +90,16 @@ from .API_StableDiffusion import APIGenerateStability from .API_civitai import APIGenerateCivitAI, APIGenerateCivitAIAddLORA, CivitAIModelSelectorPony, CivitAIModelSelectorSD15, CivitAIModelSelectorSDXL, CivitAIModelSelectorFLUX_S, CivitAIModelSelectorFLUX_D, CivitAILoraSelectorSD15, CivitAILoraSelectorSDXL, CivitAILoraSelectorPONY from .API_falAI import APIGenerateFalAI from .latent_resolution_selector import LatentResolutionSelector +from .loader_lora_with_path import LoaderLoraWithPath +from .load_text import LoadTextFromFolder, LoadTextFromPath +from .string_splitter import TextSplitin5 NODE_CLASS_MAPPINGS = { "Bjornulf_LatentResolutionSelector": LatentResolutionSelector, + "Bjornulf_LoaderLoraWithPath": LoaderLoraWithPath, + "Bjornulf_LoadTextFromPath": LoadTextFromPath, + "Bjornulf_LoadTextFromFolder": LoadTextFromFolder, + "Bjornulf_TextSplitin5": TextSplitin5, "Bjornulf_APIGenerateFlux": APIGenerateFlux, "Bjornulf_APIGenerateFalAI": APIGenerateFalAI, "Bjornulf_APIGenerateStability": APIGenerateStability, @@ -134,6 +143,8 @@ NODE_CLASS_MAPPINGS = { "Bjornulf_AddLineNumbers": AddLineNumbers, "Bjornulf_TextToAnything": TextToAnything, "Bjornulf_AnythingToText": AnythingToText, + "Bjornulf_AnythingToInt": AnythingToInt, + "Bjornulf_AnythingToFloat": AnythingToFloat, "Bjornulf_SpeechToText": SpeechToText, "Bjornulf_OllamaConfig": OllamaConfig, "Bjornulf_OllamaSystemPersonaSelector": OllamaSystemPersonaSelector, @@ -211,6 +222,8 @@ NODE_DISPLAY_NAME_MAPPINGS = { # "Bjornulf_ImageBlend": "🎨 Image Blend", # "Bjornulf_APIHiResCivitAI": "🎨➜🎨 API Image hires fix (CivitAI)", # "Bjornulf_CivitAILoraSelector": "lora Civit", + "Bjornulf_LoaderLoraWithPath": "πŸ“₯πŸ‘‘ Load Lora with Path", + "Bjornulf_TextSplitin5": "πŸ“πŸ”ͺ Text split in 5", "Bjornulf_LatentResolutionSelector": "🩷 Empty Latent Selector", "Bjornulf_CivitAIModelSelectorSD15": "πŸ“₯ Load checkpoint SD1.5 (+Download from CivitAi)", "Bjornulf_CivitAIModelSelectorSDXL": "πŸ“₯ Load checkpoint SDXL (+Download from CivitAi)", @@ -255,6 +268,8 @@ NODE_DISPLAY_NAME_MAPPINGS = { "Bjornulf_TextToSpeech": "πŸ“βžœπŸ”Š TTS - Text to Speech", "Bjornulf_TextToAnything": "πŸ“βžœβœ¨ Text to Anything", "Bjornulf_AnythingToText": "βœ¨βžœπŸ“ Anything to Text", + "Bjornulf_AnythingToInt": "βœ¨βžœπŸ”’ Anything to Int", + "Bjornulf_AnythingToFloat": "βœ¨βžœπŸ”’ Anything to Float", "Bjornulf_TextReplace": "πŸ“βžœπŸ“ Replace text", "Bjornulf_AddLineNumbers": "πŸ”’ Add line numbers", "Bjornulf_FFmpegConfig": "βš™πŸ“Ή FFmpeg Configuration πŸ“Ήβš™", @@ -311,7 +326,8 @@ NODE_DISPLAY_NAME_MAPPINGS = { "Bjornulf_SaveImageToFolder": "πŸ’ΎπŸ–ΌπŸ“ Save Image(s) to a folder", "Bjornulf_SaveTmpImage": "πŸ’ΎπŸ–Ό Save Image (tmp_api.png) βš οΈπŸ’£", "Bjornulf_SaveText": "πŸ’Ύ Save Text", - # "Bjornulf_LoadText": "πŸ“₯ Load Text", + "Bjornulf_LoadTextFromPath": "πŸ“₯ Load Text From Path", + "Bjornulf_LoadTextFromFolder": "πŸ“₯ Load Text From Bjornulf Folder", "Bjornulf_CombineTexts": "πŸ”— Combine (Texts)", "Bjornulf_imagesToVideo": "πŸ“Ή images to video (FFmpeg)", "Bjornulf_VideoPingPong": "πŸ“Ή video PingPong", diff --git a/anything_to_float.py b/anything_to_float.py new file mode 100644 index 0000000..9ed31b2 --- /dev/null +++ b/anything_to_float.py @@ -0,0 +1,28 @@ +class Everything(str): + def __ne__(self, __value: object) -> bool: + return False + +class AnythingToFloat: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "anything": (Everything("*"), {"forceInput": True}), + }, + } + + @classmethod + def VALIDATE_INPUTS(s, input_types): + return True + + RETURN_TYPES = ("FLOAT",) + RETURN_NAMES = ("float",) + FUNCTION = "any_to_float" + CATEGORY = "Bjornulf" + + def any_to_float(self, anything): + try: + return (float(anything),) + except (ValueError, TypeError): + # Return 0.0 if conversion fails + return (0.0,) \ No newline at end of file diff --git a/anything_to_int.py b/anything_to_int.py new file mode 100644 index 0000000..c0d72d7 --- /dev/null +++ b/anything_to_int.py @@ -0,0 +1,32 @@ +class Everything(str): + def __ne__(self, __value: object) -> bool: + return False + +class AnythingToInt: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "anything": (Everything("*"), {"forceInput": True}), + }, + } + + @classmethod + def VALIDATE_INPUTS(s, input_types): + return True + + RETURN_TYPES = ("INT",) + RETURN_NAMES = ("integer",) + FUNCTION = "any_to_int" + CATEGORY = "Bjornulf" + + def any_to_int(self, anything): + try: + # Handle string inputs that might be floats + if isinstance(anything, str) and '.' in anything: + return (int(float(anything)),) + # Handle other types + return (int(anything),) + except (ValueError, TypeError): + # Return 0 if conversion fails + return (0,) \ No newline at end of file diff --git a/anything_to_text.py b/anything_to_text.py index a33ec6e..a61b089 100644 --- a/anything_to_text.py +++ b/anything_to_text.py @@ -1,3 +1,6 @@ +class Everything(str): + def __ne__(self, __value: object) -> bool: + return False class AnythingToText: @classmethod def INPUT_TYPES(s): @@ -18,9 +21,4 @@ class AnythingToText: def any_to_text(self, anything): # Convert the input to string representation - return (str(anything),) - -# Keep the Everything class definition as it's needed for type handling -class Everything(str): - def __ne__(self, __value: object) -> bool: - return False \ No newline at end of file + return (str(anything),) \ No newline at end of file diff --git a/combine_texts.py b/combine_texts.py index e76baf8..95d248f 100644 --- a/combine_texts.py +++ b/combine_texts.py @@ -5,11 +5,9 @@ class CombineTexts: "required": { "number_of_inputs": ("INT", {"default": 2, "min": 2, "max": 50, "step": 1}), "delimiter": (["newline", "comma", "space", "slash", "nothing"], {"default": "newline"}), - "text_1": ("STRING", {"forceInput": True}), - "text_2": ("STRING", {"forceInput": True}), }, "hidden": { - **{f"text_{i}": ("STRING", {"forceInput": True}) for i in range(3, 51)} + **{f"text_{i}": ("STRING", {"forceInput": True}) for i in range(1, 51)} } } diff --git a/load_text.py b/load_text.py new file mode 100644 index 0000000..102dd8b --- /dev/null +++ b/load_text.py @@ -0,0 +1,92 @@ +import os + +class LoadTextFromFolder: + @classmethod + def INPUT_TYPES(cls): + """Define input parameters for the node""" + default_dir = "Bjornulf/Text" + available_files = [] + + if os.path.exists(default_dir): + available_files = [f for f in os.listdir(default_dir) + if f.lower().endswith('.txt')] + + if not available_files: + available_files = ["no_files_found"] + + return { + "required": { + "text_file": (available_files, {"default": available_files[0]}), + } + } + + RETURN_TYPES = ("STRING", "STRING", "STRING") + RETURN_NAMES = ("text", "filename", "full_path") + FUNCTION = "load_text" + CATEGORY = "Bjornulf" + + def load_text(self, text_file): + try: + if text_file == "no_files_found": + raise ValueError("No text files found in Bjornulf/Text folder") + + filepath = os.path.join("Bjornulf/Text", text_file) + + # Check if file exists + if not os.path.exists(filepath): + raise ValueError(f"File not found: {filepath}") + + # Get absolute path + full_path = os.path.abspath(filepath) + + # Get just the filename + filename = os.path.basename(filepath) + + # Read text from file + with open(filepath, 'r', encoding='utf-8') as file: + text = file.read() + + return (text, filename, full_path) + + except (OSError, IOError) as e: + raise ValueError(f"Error loading file: {str(e)}") + +class LoadTextFromPath: + @classmethod + def INPUT_TYPES(cls): + """Define input parameters for the node""" + return { + "required": { + "file_path": ("STRING", {"default": "Bjornulf/Text/example.txt"}), + } + } + + RETURN_TYPES = ("STRING", "STRING", "STRING") + RETURN_NAMES = ("text", "filename", "full_path") + FUNCTION = "load_text" + CATEGORY = "Bjornulf" + + def load_text(self, file_path): + try: + # Validate file extension + if not file_path.lower().endswith('.txt'): + raise ValueError("File must be a .txt file") + + # Check if file exists + if not os.path.exists(file_path): + raise ValueError(f"File not found: {file_path}") + + # Get absolute path + full_path = os.path.abspath(file_path) + + # Get just the filename + filename = os.path.basename(file_path) + + # Read text from file + with open(file_path, 'r', encoding='utf-8') as file: + text = file.read() + + return (text, filename, full_path) + + except (OSError, IOError) as e: + raise ValueError(f"Error loading file: {str(e)}") \ No newline at end of file diff --git a/loader_lora_with_path.py b/loader_lora_with_path.py new file mode 100644 index 0000000..2c601a4 --- /dev/null +++ b/loader_lora_with_path.py @@ -0,0 +1,49 @@ +import os +import comfy.sd +import comfy.utils + +class LoaderLoraWithPath: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "model": ("MODEL",), + "clip": ("CLIP",), + "lora_path": ("STRING", {"default": ""}), + "strength_model": ("FLOAT", {"default": 1.0, "min": -100.0, "max": 100.0, "step": 0.01}), + "strength_clip": ("FLOAT", {"default": 1.0, "min": -100.0, "max": 100.0, "step": 0.01}), + } + } + + RETURN_TYPES = ("MODEL", "CLIP") + FUNCTION = "load_lora" # Added this line + CATEGORY = "Bjornulf" + + def load_lora(self, model, clip, lora_path, strength_model, strength_clip): + try: + # Check if path exists + if not os.path.isfile(lora_path): + print(f"Error: Lora file not found at path: {lora_path}") + return (model, clip) + + # Load the Lora file + try: + lora = comfy.utils.load_torch_file(lora_path) + except Exception as e: + print(f"Error loading Lora file: {str(e)}") + return (model, clip) + + # Apply the Lora + model_lora, clip_lora = comfy.sd.load_lora_for_models( + model, + clip, + lora, + strength_model, + strength_clip + ) + + return (model_lora, clip_lora) + + except Exception as e: + print(f"Error in load_lora: {str(e)}") + return (model, clip) \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 639feaa..747a6e1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "bjornulf_custom_nodes" -description = "110 ComfyUI nodes : Display, manipulate, and edit text, images, videos, loras, generate characters and more. Manage looping operations, generate randomized content, use logical conditions and work with external AI tools, like Ollama or Text To Speech." -version = "0.64" +description = "116 ComfyUI nodes : Display, manipulate, and edit text, images, videos, loras, generate characters and more. Manage looping operations, generate randomized content, use logical conditions and work with external AI tools, like Ollama or Text To Speech." +version = "0.65" license = {file = "LICENSE"} [project.urls] diff --git a/save_text.py b/save_text.py index a9af4e2..de7f407 100644 --- a/save_text.py +++ b/save_text.py @@ -6,12 +6,12 @@ class SaveText: return { "required": { "text": ("STRING", {"multiline": True, "forceInput": True}), - "filepath": ("STRING", {"default": "output/this_test.txt"}), + "filepath": ("STRING", {"default": "Bjornulf/Text/example.txt"}), } } - RETURN_TYPES = ("STRING",) - RETURN_NAMES = ("text",) + RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING") + RETURN_NAMES = ("added_text", "complete_text", "filename", "full_path") FUNCTION = "save_text" OUTPUT_NODE = True CATEGORY = "Bjornulf" @@ -27,11 +27,30 @@ class SaveText: if directory and not os.path.exists(directory): os.makedirs(directory) + # Get absolute path + full_path = os.path.abspath(filepath) + # Append text to file with a newline with open(filepath, 'a', encoding='utf-8') as file: file.write(text + '\n') - return {"ui": {"text": text}, "result": (text,)} + # Read complete file content + with open(filepath, 'r', encoding='utf-8') as file: + complete_text = file.read() + + # Get just the filename + filename = os.path.basename(filepath) + + # Return all requested information + return { + "ui": {"text": text}, + "result": ( + text, # added_text + complete_text, # complete_text + filename, # filename + full_path # full_path + ) + } except (OSError, IOError) as e: raise ValueError(f"Error saving file: {str(e)}") \ No newline at end of file diff --git a/screenshots/anything_to_float.png b/screenshots/anything_to_float.png new file mode 100644 index 0000000..523cb88 Binary files /dev/null and b/screenshots/anything_to_float.png differ diff --git a/screenshots/anything_to_int.png b/screenshots/anything_to_int.png new file mode 100644 index 0000000..9513289 Binary files /dev/null and b/screenshots/anything_to_int.png differ diff --git a/screenshots/load_lora_with_path.png b/screenshots/load_lora_with_path.png new file mode 100644 index 0000000..03463b3 Binary files /dev/null and b/screenshots/load_lora_with_path.png differ diff --git a/screenshots/load_lora_with_path_COMPLEX.png b/screenshots/load_lora_with_path_COMPLEX.png new file mode 100644 index 0000000..7f65d97 Binary files /dev/null and b/screenshots/load_lora_with_path_COMPLEX.png differ diff --git a/screenshots/load_text_PATH.png b/screenshots/load_text_PATH.png new file mode 100644 index 0000000..8405884 Binary files /dev/null and b/screenshots/load_text_PATH.png differ diff --git a/screenshots/load_text_from_Bjornulf.png b/screenshots/load_text_from_Bjornulf.png new file mode 100644 index 0000000..f99b6d9 Binary files /dev/null and b/screenshots/load_text_from_Bjornulf.png differ diff --git a/screenshots/load_text_requirements.png b/screenshots/load_text_requirements.png new file mode 100644 index 0000000..adb1bd4 Binary files /dev/null and b/screenshots/load_text_requirements.png differ diff --git a/screenshots/save_text.png b/screenshots/save_text.png index 8091cda..24a8f7a 100644 Binary files a/screenshots/save_text.png and b/screenshots/save_text.png differ diff --git a/screenshots/split_in_5.png b/screenshots/split_in_5.png new file mode 100644 index 0000000..5385c94 Binary files /dev/null and b/screenshots/split_in_5.png differ diff --git a/string_splitter.py b/string_splitter.py new file mode 100644 index 0000000..bde3f7b --- /dev/null +++ b/string_splitter.py @@ -0,0 +1,49 @@ +class TextSplitin5: + DELIMITER_NEWLINE = "\\n" # Literal string "\n" for display + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "input_string": ("STRING", { + "multiline": True, + "forceInput": True + }), + "delimiter": ("STRING", { + "default": s.DELIMITER_NEWLINE, # Show "\n" in widget + "multiline": False + }), + "ignore_before_equals": ("BOOLEAN", { + "default": False + }), + }, + } + + RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING", "STRING") + RETURN_NAMES = ("part1", "part2", "part3", "part4", "part5") + FUNCTION = "split_string" + CATEGORY = "Bjornulf" + + def split_string(self, input_string, delimiter, ignore_before_equals): + # Handle the special case for newline delimiter + actual_delimiter = "\n" if delimiter == self.DELIMITER_NEWLINE else delimiter + + # Split the string using the delimiter + parts = input_string.split(actual_delimiter) + + # Ensure we always return exactly 5 parts + result = [] + for i in range(5): + if i < len(parts): + part = parts[i].strip() + # If ignore_before_equals is True and there's an equals sign + if ignore_before_equals and '=' in part: + # Take only what's after the equals sign and strip whitespace + part = part.split('=', 1)[1].strip() + result.append(part) + else: + # If no more parts, append empty string + result.append("") + + # Convert to tuple and return all 5 parts + return tuple(result) \ No newline at end of file