diff --git a/README.md b/README.md index 8bb303a..5a650e2 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# 🔗 Comfyui : Bjornulf_custom_nodes v0.25 🔗 +# 🔗 Comfyui : Bjornulf_custom_nodes v0.26 🔗 # ❤️ Coffee : ☕☕☕☕☕ 5/5 @@ -73,6 +73,7 @@ wget --content-disposition -P /workspace/ComfyUI/models/checkpoints "https://civ - **v0.23**: Add a new node: Pause, resume or stop workflow. - **v0.24**: Add a new node: Pause, select input, pick one. - **v0.25**: Two new nodes: Loop Images and Random image. +- **v0.26**: New node : Loop write Text. Also increase nb of inputs allowed for most nodes. (+ update some breaking changes) # 📝 Nodes descriptions @@ -111,6 +112,7 @@ General-purpose loop node. **Description:** Cycle through a list of text inputs. + ## 7 - ♻ Loop Integer ![Loop Integer](screenshots/loop_integer.png) ![Loop Int + Show Text](screenshots/loop_int+show_text.png) @@ -121,6 +123,10 @@ Iterate through a range of integer values, good for `steps` in ksampler, etc... ❗ Don't forget that you can convert ksampler widgets to input by right-clicking the ksampler node : ![Widget to Input](screenshots/widget-to-input.png) +Here is an example of usage with ksampler (Notice that it isn't optimized, but good enough for testing) : +![Widget to Input](screenshots/example_loop_integer.png) + + ## 8 - ♻ Loop Float ![Loop Float](screenshots/loop_float.png) ![Loop Float + Show Text](screenshots/loop_float+show_text.png) @@ -369,4 +375,12 @@ Just take a random image from a list of images. **Description:** Loop over a list of images. Usage example : You have a list of images, and you want to apply the same process to all of them. -Above is an example of the loop images node sending them to an Ipadapter style transfer workflow. (Same seed of course.) \ No newline at end of file +Above is an example of the loop images node sending them to an Ipadapter style transfer workflow. (Same seed of course.) + +### 39 - ♻ Loop (✒ Write Text) + +![pick input](screenshots/loop_write_text.png) + +**Description:** +If you need a quick loop but you don't want something too complex with a loop node, you can use this combined write text + loop. +It will take the same special syntax as the write text node `{blue|red}`, but it will loop over ALL the possibilities instead of taking one at random. \ No newline at end of file diff --git a/__init__.py b/__init__.py index a2823c4..5143d9c 100644 --- a/__init__.py +++ b/__init__.py @@ -44,6 +44,8 @@ from .pause_resume_stop import PauseResume from .pick_input import PickInput from .loop_images import LoopImages from .random_image import RandomImage +from .loop_write_text import LoopWriteText +# from .random_checkpoint import RandomCheckpoint # from .pass_preview_image import PassPreviewImage # from .check_black_image import CheckBlackImage @@ -54,6 +56,8 @@ from .random_image import RandomImage NODE_CLASS_MAPPINGS = { # "Bjornulf_CustomStringType": CustomStringType, "Bjornulf_ollamaLoader": ollamaLoader, + # "Bjoenulf_RandomCheckpoint": RandomCheckpoint, + "Bjornulf_LoopWriteText": LoopWriteText, "Bjornulf_LoopImages": LoopImages, "Bjornulf_RandomImage": RandomImage, # "Bjornulf_PassPreviewImage": PassPreviewImage, @@ -107,6 +111,8 @@ NODE_CLASS_MAPPINGS = { NODE_DISPLAY_NAME_MAPPINGS = { # "Bjornulf_CustomStringType": "!!! CUSTOM STRING TYPE !!!", "Bjornulf_ollamaLoader": "🦙 Ollama (Description)", + # "Bjoenulf_RandomCheckpoint": "🎲 Random Checkpoint", + "Bjornulf_LoopWriteText": "♻ Loop (✒ Write Text)", "Bjornulf_LoopImages": "♻🖼 Loop (Images)", "Bjornulf_RandomImage": "🎲🖼 Random Image", # "Bjornulf_PassPreviewImage": "🖼⮕ Pass Preview Image", diff --git a/combine_texts.py b/combine_texts.py index 4d2767a..fde304c 100644 --- a/combine_texts.py +++ b/combine_texts.py @@ -3,13 +3,13 @@ class CombineTexts: def INPUT_TYPES(cls): return { "required": { - "number_of_inputs": ("INT", {"default": 2, "min": 2, "max": 10, "step": 1}), + "number_of_inputs": ("INT", {"default": 2, "min": 2, "max": 50, "step": 1}), "delimiter": (["newline", "comma", "space", "slash"], {"default": "newline"}), "text_1": ("STRING", {"forceInput": True}), "text_2": ("STRING", {"forceInput": True}), }, "hidden": { - **{f"text_{i}": ("STRING", {"forceInput": True}) for i in range(3, 11)} + **{f"text_{i}": ("STRING", {"forceInput": True}) for i in range(3, 51)} } } diff --git a/loop_combine_texts_by_lines.py b/loop_combine_texts_by_lines.py index e8a1873..7f9b13d 100644 --- a/loop_combine_texts_by_lines.py +++ b/loop_combine_texts_by_lines.py @@ -3,17 +3,17 @@ class CombineTextsByLines: def INPUT_TYPES(cls): return { "required": { - "number_of_inputs": ("INT", {"default": 2, "min": 1, "max": 10, "step": 1}), - "number_of_lines": ("INT", {"default": 3, "min": 1, "max": 20, "step": 1}), + "number_of_inputs": ("INT", {"default": 2, "min": 1, "max": 50, "step": 1}), + "number_of_lines": ("INT", {"default": 3, "min": 1, "max": 50, "step": 1}), } } - RETURN_TYPES = tuple(["STRING"] * 20) # Maximum 20 lines - RETURN_NAMES = tuple([f"line_{i+1}" for i in range(20)]) + RETURN_TYPES = tuple(["STRING"] * 50) # Maximum 50 lines + RETURN_NAMES = tuple([f"line_{i+1}" for i in range(50)]) FUNCTION = "extract_lines" OUTPUT_NODE = True CATEGORY = "text" - OUTPUT_IS_LIST = tuple([True] * 20) # Indicate that all outputs are lists + OUTPUT_IS_LIST = tuple([True] * 50) # Indicate that all outputs are lists def extract_lines(self, number_of_inputs, number_of_lines, **kwargs): grouped_lines = [[] for _ in range(number_of_lines)] @@ -31,8 +31,8 @@ class CombineTextsByLines: # Instead of joining the lines, keep them as a list outputs.append(group) - # Pad the output to always return 20 items - outputs.extend([[] for _ in range(20 - len(outputs))]) + # Pad the output to always return 50 items + outputs.extend([[] for _ in range(50 - len(outputs))]) return tuple(outputs) @@ -42,8 +42,8 @@ class CombineTextsByLines: @classmethod def VALIDATE_INPUTS(cls, number_of_inputs, number_of_lines, **kwargs): - if number_of_lines < 1 or number_of_lines > 20: - return "Number of lines must be between 1 and 20" - if number_of_inputs < 1 or number_of_inputs > 10: - return "Number of inputs must be between 1 and 10" + if number_of_lines < 1 or number_of_lines > 50: + return "Number of lines must be between 1 and 50" + if number_of_inputs < 1 or number_of_inputs > 50: + return "Number of inputs must be between 1 and 50" return True diff --git a/loop_images.py b/loop_images.py index dd55446..9097d65 100644 --- a/loop_images.py +++ b/loop_images.py @@ -3,7 +3,7 @@ class LoopImages: def INPUT_TYPES(cls): return { "required": { - "number_of_images": ("INT", {"default": 2, "min": 1, "max": 10, "step": 1}), + "number_of_images": ("INT", {"default": 2, "min": 1, "max": 30, "step": 1}), }, } diff --git a/loop_texts.py b/loop_texts.py index 80267f8..0599f9b 100644 --- a/loop_texts.py +++ b/loop_texts.py @@ -3,12 +3,12 @@ class LoopTexts: def INPUT_TYPES(cls): return { "required": { - "number_of_inputs": ("INT", {"default": 2, "min": 2, "max": 10, "step": 1}), - "text_1": ("STRING", {"forceInput": "True"}), - "text_2": ("STRING", {"forceInput": "True"}), + "number_of_inputs": ("INT", {"default": 2, "min": 2, "max": 50, "step": 1}), + # "text_1": ("STRING", {"forceInput": "True"}), + # "text_2": ("STRING", {"forceInput": "True"}), }, "hidden": { - **{f"text_{i}": ("STRING", {"forceInput": "True"}) for i in range(3, 11)} + **{f"text_{i}": ("STRING", {"forceInput": "True"}) for i in range(1, 51)} } } diff --git a/loop_write_text.py b/loop_write_text.py new file mode 100644 index 0000000..43f0eb6 --- /dev/null +++ b/loop_write_text.py @@ -0,0 +1,41 @@ +import re +import itertools + +class LoopWriteText: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "text": ("STRING", {"multiline": True}), + }, + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("texts",) + FUNCTION = "loop_write_text" + OUTPUT_NODE = True + OUTPUT_IS_LIST = (True,) + CATEGORY = "Bjornulf" + + def loop_write_text(self, text): + pattern = r'\{([^}]+)\}' + matches = re.findall(pattern, text) + + if not matches: + return ([text],) + + options_list = [opt.split('|') for opt in matches] + combinations = list(itertools.product(*options_list)) + + results = [] + for combo in combinations: + result = text + for i, match in enumerate(matches): + result = result.replace(f"{{{match}}}", combo[i], 1) + results.append(result) + + return (results,) + + @classmethod + def IS_CHANGED(s, text): + return float("nan") # Always re-execute to ensure consistency \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index ef8845b..0802e72 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.25" +version = "0.26" license = {file = "LICENSE"} [project.urls] diff --git a/random_texts.py b/random_texts.py index 30a6902..21e72b6 100644 --- a/random_texts.py +++ b/random_texts.py @@ -7,12 +7,12 @@ class RandomTexts: "required": { "number_of_inputs": ("INT", {"default": 2, "min": 2, "max": 30, "step": 1}), "number_of_random": ("INT", {"default": 1, "min": 1, "max": 30, "step": 1}), - "text_1": ("STRING", {"forceInput": "True"}), - "text_2": ("STRING", {"forceInput": "True"}), + # "text_1": ("STRING", {"forceInput": "True"}), + # "text_2": ("STRING", {"forceInput": "True"}), "seed": ("INT", {"default": "1"}), #Used with control_after_generate, }, "hidden": { - **{f"text_{i}": ("STRING", {"forceInput": "True"}) for i in range(3, 31)} + **{f"text_{i}": ("STRING", {"forceInput": "True"}) for i in range(1, 31)} } } diff --git a/screenshots/example_loop_integer.png b/screenshots/example_loop_integer.png new file mode 100644 index 0000000..71ea67c Binary files /dev/null and b/screenshots/example_loop_integer.png differ diff --git a/screenshots/loop_write_text.png b/screenshots/loop_write_text.png new file mode 100644 index 0000000..c1f2f17 Binary files /dev/null and b/screenshots/loop_write_text.png differ