commit 643bf5d843ce93e6a1a616347e8b0a7438fb48f0 Author: justumen Date: Sat Jul 6 12:05:16 2024 +0200 first commit diff --git a/CUSTOM_STRING.py.txt b/CUSTOM_STRING.py.txt new file mode 100644 index 0000000..a643868 --- /dev/null +++ b/CUSTOM_STRING.py.txt @@ -0,0 +1,11 @@ +class CustomStringType: + @classmethod + def INPUT_TYPES(s): + return {"required": {"value": ("STRING", {"multiline": True})}} + + RETURN_TYPES = ("CUSTOM_STRING",) + FUNCTION = "passthrough" + CATEGORY = "Bjornulf" + + def passthrough(self, value): + return (value,) \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..8d1c8b6 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ + diff --git a/SaveText/SaveText.txt b/SaveText/SaveText.txt new file mode 100644 index 0000000..c67368e --- /dev/null +++ b/SaveText/SaveText.txt @@ -0,0 +1 @@ +saluts \ No newline at end of file diff --git a/SaveText/SaveText_001.txt b/SaveText/SaveText_001.txt new file mode 100644 index 0000000..5688a8f --- /dev/null +++ b/SaveText/SaveText_001.txt @@ -0,0 +1 @@ +encule \ No newline at end of file diff --git a/SaveText/SaveText_002.txt b/SaveText/SaveText_002.txt new file mode 100644 index 0000000..d47fd5e --- /dev/null +++ b/SaveText/SaveText_002.txt @@ -0,0 +1 @@ +connard \ No newline at end of file diff --git a/SaveText/SaveText_003.txt b/SaveText/SaveText_003.txt new file mode 100644 index 0000000..e69de29 diff --git a/SaveText/SaveText_004.txt b/SaveText/SaveText_004.txt new file mode 100644 index 0000000..e69de29 diff --git a/SaveText/SaveText_005.txt b/SaveText/SaveText_005.txt new file mode 100644 index 0000000..e69de29 diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..a49a6ff --- /dev/null +++ b/__init__.py @@ -0,0 +1,86 @@ +from .create_video import imgs2vid +from .write_text import WriteText +from .write_image_environment import WriteImageEnvironment +from .write_image_characters import WriteImageCharacters +from .write_image_character import WriteImageCharacter +from .write_image_allinone import WriteImageAllInOne +from .combine_texts import CombineTexts +from .loop_texts import LoopTexts +from .random_texts import RandomTexts +from .random_model_clip_vae import RandomModelClipVae +from .video_pingpong import VideoPingPong +from .loop_float import LoopFloat +from .loop_integer import LoopInteger +from .loop_basic_batch import LoopBasicBatch +from .loop_samplers import LoopSamplers +from .loop_schedulers import LoopSchedulers +from .ollama import ollamaLoader +from .show_text import ShowText +from .show_int import ShowInt +from .show_float import ShowFloat +from .save_text import SaveText +from .save_tmp_image import SaveTmpImage +from .save_api_image import SaveApiImage +from .loop_my_combos_samplers_schedulers import LoopCombosSamplersSchedulers + +# from .CUSTOM_STRING import CustomStringType + +NODE_CLASS_MAPPINGS = { + # "Bjornulf_CustomStringType": CustomStringType, + "Bjornulf_ollamaLoader": ollamaLoader, + "Bjornulf_WriteText": WriteText, + # "Bjornulf_WriteImageEnvironment": WriteImageEnvironment, + # "Bjornulf_WriteImageCharacters": WriteImageCharacters, + # "Bjornulf_WriteImageCharacter": WriteImageCharacter, + # "Bjornulf_WriteImageAllInOne": WriteImageAllInOne, + "Bjornulf_ShowText": ShowText, + "Bjornulf_ShowInt": ShowInt, + "Bjornulf_ShowFloat": ShowFloat, + "Bjornulf_SaveText": SaveText, + "Bjornulf_SaveTmpImage": SaveTmpImage, + "Bjornulf_SaveApiImage": SaveApiImage, + "Bjornulf_CombineTexts": CombineTexts, + "Bjornulf_LoopTexts": LoopTexts, + "Bjornulf_RandomTexts": RandomTexts, + "Bjornulf_RandomModelClipVae": RandomModelClipVae, + "Bjornulf_imgs2vid": imgs2vid, + "Bjornulf_VideoPingPong": VideoPingPong, + "Bjornulf_LoopFloat": LoopFloat, + "Bjornulf_LoopInteger": LoopInteger, + "Bjornulf_LoopBasicBatch": LoopBasicBatch, + "Bjornulf_LoopSamplers": LoopSamplers, + "Bjornulf_LoopSchedulers": LoopSchedulers, + "Bjornulf_LoopCombosSamplersSchedulers": LoopCombosSamplersSchedulers, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + # "Bjornulf_CustomStringType": "!!! CUSTOM STRING TYPE !!!", + "Bjornulf_ollamaLoader": "πŸ¦™ Ollama (Description)", + "Bjornulf_ShowText": "πŸ‘ Show (Text)", + "Bjornulf_ShowInt": "πŸ‘ Show (Int)", + "Bjornulf_ShowFloat": "πŸ‘ Show (Float)", + "Bjornulf_SaveTmpImage": "πŸ–Ό Save Image (tmp_api.png)", + "Bjornulf_SaveApiImage": "πŸ–Ό Save Image (API_IMAGES/00001.png...)", + "Bjornulf_SaveText": "πŸ’Ύ Save Text", #Make SaveCharacter, SaveLocation, SaveCamera, SaveAction, SaveClothes, SaveEmotion... + "Bjornulf_LoadText": "πŸ“₯ Load Text", #Make LoadCharacter, LoadLocation, LoadCamera, LoadAction, LoadClothes, LoadEmotion... + "Bjornulf_WriteText": "βœ’ Write Text", + # "Bjornulf_WriteImageEnvironment": "βœ’ Write Image Environment", + # "Bjornulf_WriteImageCharacters": "βœ’ Write Image Characters", + # "Bjornulf_WriteImageCharacter": "βœ’ Write Image Character", + # "Bjornulf_WriteImageAllInOne": "βœ’ Write Image All-in-one", + "Bjornulf_CombineTexts": "πŸ”— Combine (Texts)", + "Bjornulf_LoopTexts": "β™» Loop (Texts)", + "Bjornulf_RandomTexts": "🎲 Random (Texts)", + "Bjornulf_RandomModelClipVae": "🎲 Random (Model+Clip+Vae)", + "Bjornulf_imgs2vid": "πŸ“Ή imgs2vid (FFmpeg)", + "Bjornulf_VideoPingPong": "πŸ“Ή video PingPong", + "Bjornulf_LoopFloat": "β™» Loop (Float)", + "Bjornulf_LoopInteger": "β™» Loop (Integer)", + "Bjornulf_LoopBasicBatch": "β™» Loop", + "Bjornulf_LoopSamplers": "β™» Loop (All Samplers)", + "Bjornulf_LoopSchedulers": "β™» Loop (All Schedulers)", + "Bjornulf_LoopCombosSamplersSchedulers": "β™» Loop (My combos Samplerβš”Scheduler)", +} + +WEB_DIRECTORY = "./web" +__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS', 'WEB_DIRECTORY'] \ No newline at end of file diff --git a/__pycache__/CUSTOM_STRING.cpython-311.pyc b/__pycache__/CUSTOM_STRING.cpython-311.pyc new file mode 100644 index 0000000..2893bde Binary files /dev/null and b/__pycache__/CUSTOM_STRING.cpython-311.pyc differ diff --git a/__pycache__/__init__.cpython-311.pyc b/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..5adefea Binary files /dev/null and b/__pycache__/__init__.cpython-311.pyc differ diff --git a/__pycache__/combine_texts.cpython-311.pyc b/__pycache__/combine_texts.cpython-311.pyc new file mode 100644 index 0000000..d057813 Binary files /dev/null and b/__pycache__/combine_texts.cpython-311.pyc differ diff --git a/__pycache__/convert_16to4_channels.cpython-311.pyc b/__pycache__/convert_16to4_channels.cpython-311.pyc new file mode 100644 index 0000000..3d18a14 Binary files /dev/null and b/__pycache__/convert_16to4_channels.cpython-311.pyc differ diff --git a/__pycache__/convert_16to8_channels.cpython-311.pyc b/__pycache__/convert_16to8_channels.cpython-311.pyc new file mode 100644 index 0000000..dc7016b Binary files /dev/null and b/__pycache__/convert_16to8_channels.cpython-311.pyc differ diff --git a/__pycache__/create_video.cpython-311.pyc b/__pycache__/create_video.cpython-311.pyc new file mode 100644 index 0000000..21cd95b Binary files /dev/null and b/__pycache__/create_video.cpython-311.pyc differ diff --git a/__pycache__/loop_basic_batch.cpython-311.pyc b/__pycache__/loop_basic_batch.cpython-311.pyc new file mode 100644 index 0000000..c4cf0aa Binary files /dev/null and b/__pycache__/loop_basic_batch.cpython-311.pyc differ diff --git a/__pycache__/loop_float.cpython-311.pyc b/__pycache__/loop_float.cpython-311.pyc new file mode 100644 index 0000000..f42329f Binary files /dev/null and b/__pycache__/loop_float.cpython-311.pyc differ diff --git a/__pycache__/loop_integer.cpython-311.pyc b/__pycache__/loop_integer.cpython-311.pyc new file mode 100644 index 0000000..46e4bae Binary files /dev/null and b/__pycache__/loop_integer.cpython-311.pyc differ diff --git a/__pycache__/loop_my_combos_samplers_schedulers.cpython-311.pyc b/__pycache__/loop_my_combos_samplers_schedulers.cpython-311.pyc new file mode 100644 index 0000000..88cc6f7 Binary files /dev/null and b/__pycache__/loop_my_combos_samplers_schedulers.cpython-311.pyc differ diff --git a/__pycache__/loop_samplers.cpython-311.pyc b/__pycache__/loop_samplers.cpython-311.pyc new file mode 100644 index 0000000..46eb606 Binary files /dev/null and b/__pycache__/loop_samplers.cpython-311.pyc differ diff --git a/__pycache__/loop_schedulers.cpython-311.pyc b/__pycache__/loop_schedulers.cpython-311.pyc new file mode 100644 index 0000000..a63a758 Binary files /dev/null and b/__pycache__/loop_schedulers.cpython-311.pyc differ diff --git a/__pycache__/loop_texts.cpython-311.pyc b/__pycache__/loop_texts.cpython-311.pyc new file mode 100644 index 0000000..eb131ae Binary files /dev/null and b/__pycache__/loop_texts.cpython-311.pyc differ diff --git a/__pycache__/ollama.cpython-311.pyc b/__pycache__/ollama.cpython-311.pyc new file mode 100644 index 0000000..048de21 Binary files /dev/null and b/__pycache__/ollama.cpython-311.pyc differ diff --git a/__pycache__/random_model_clip_vae.cpython-311.pyc b/__pycache__/random_model_clip_vae.cpython-311.pyc new file mode 100644 index 0000000..d0319a1 Binary files /dev/null and b/__pycache__/random_model_clip_vae.cpython-311.pyc differ diff --git a/__pycache__/random_texts.cpython-311.pyc b/__pycache__/random_texts.cpython-311.pyc new file mode 100644 index 0000000..48ac842 Binary files /dev/null and b/__pycache__/random_texts.cpython-311.pyc differ diff --git a/__pycache__/save_api_image.cpython-311.pyc b/__pycache__/save_api_image.cpython-311.pyc new file mode 100644 index 0000000..8092ad8 Binary files /dev/null and b/__pycache__/save_api_image.cpython-311.pyc differ diff --git a/__pycache__/save_image.cpython-311.pyc b/__pycache__/save_image.cpython-311.pyc new file mode 100644 index 0000000..e27676b Binary files /dev/null and b/__pycache__/save_image.cpython-311.pyc differ diff --git a/__pycache__/save_text.cpython-311.pyc b/__pycache__/save_text.cpython-311.pyc new file mode 100644 index 0000000..3e8e58f Binary files /dev/null and b/__pycache__/save_text.cpython-311.pyc differ diff --git a/__pycache__/save_tmp_image.cpython-311.pyc b/__pycache__/save_tmp_image.cpython-311.pyc new file mode 100644 index 0000000..c3ef804 Binary files /dev/null and b/__pycache__/save_tmp_image.cpython-311.pyc differ diff --git a/__pycache__/show_float.cpython-311.pyc b/__pycache__/show_float.cpython-311.pyc new file mode 100644 index 0000000..aeb7dbe Binary files /dev/null and b/__pycache__/show_float.cpython-311.pyc differ diff --git a/__pycache__/show_int.cpython-311.pyc b/__pycache__/show_int.cpython-311.pyc new file mode 100644 index 0000000..f328154 Binary files /dev/null and b/__pycache__/show_int.cpython-311.pyc differ diff --git a/__pycache__/show_text.cpython-311.pyc b/__pycache__/show_text.cpython-311.pyc new file mode 100644 index 0000000..2b632c3 Binary files /dev/null and b/__pycache__/show_text.cpython-311.pyc differ diff --git a/__pycache__/video_pingpong.cpython-311.pyc b/__pycache__/video_pingpong.cpython-311.pyc new file mode 100644 index 0000000..75661b1 Binary files /dev/null and b/__pycache__/video_pingpong.cpython-311.pyc differ diff --git a/__pycache__/write_image_allinone.cpython-311.pyc b/__pycache__/write_image_allinone.cpython-311.pyc new file mode 100644 index 0000000..0e09b40 Binary files /dev/null and b/__pycache__/write_image_allinone.cpython-311.pyc differ diff --git a/__pycache__/write_image_character.cpython-311.pyc b/__pycache__/write_image_character.cpython-311.pyc new file mode 100644 index 0000000..445707c Binary files /dev/null and b/__pycache__/write_image_character.cpython-311.pyc differ diff --git a/__pycache__/write_image_characters.cpython-311.pyc b/__pycache__/write_image_characters.cpython-311.pyc new file mode 100644 index 0000000..4960fb4 Binary files /dev/null and b/__pycache__/write_image_characters.cpython-311.pyc differ diff --git a/__pycache__/write_image_environment.cpython-311.pyc b/__pycache__/write_image_environment.cpython-311.pyc new file mode 100644 index 0000000..3b09cf8 Binary files /dev/null and b/__pycache__/write_image_environment.cpython-311.pyc differ diff --git a/__pycache__/write_text.cpython-311.pyc b/__pycache__/write_text.cpython-311.pyc new file mode 100644 index 0000000..79a0cde Binary files /dev/null and b/__pycache__/write_text.cpython-311.pyc differ diff --git a/__pycache__/write_texts.cpython-311.pyc b/__pycache__/write_texts.cpython-311.pyc new file mode 100644 index 0000000..458349b Binary files /dev/null and b/__pycache__/write_texts.cpython-311.pyc differ diff --git a/combine_texts.py b/combine_texts.py new file mode 100644 index 0000000..0e83a4a --- /dev/null +++ b/combine_texts.py @@ -0,0 +1,46 @@ +class CombineTexts: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "number_of_inputs": ("INT", {"default": 2, "min": 2, "max": 10, "step": 1}), + "delimiter": (["newline", "comma", "space", "test"], {"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)} + } + } + + RETURN_TYPES = ("STRING",) + FUNCTION = "combine_texts" + OUTPUT_IS_LIST = (False,) + CATEGORY = "Bjornulf" + + def combine_texts(self, number_of_inputs, delimiter, **kwargs): + def flatten(item): + if isinstance(item, str): + return item + elif isinstance(item, list): + return self.get_delimiter(delimiter).join(map(flatten, item)) + else: + return str(item) + + combined_text = self.get_delimiter(delimiter).join([ + flatten(kwargs[f"text_{i}"]) + for i in range(1, number_of_inputs + 1) + if f"text_{i}" in kwargs + ]) + return (combined_text,) + + @staticmethod + def get_delimiter(delimiter): + if delimiter == "newline": + return "\n" + elif delimiter == "comma": + return "," + elif delimiter == "space": + return " " + else: + return "\n" \ No newline at end of file diff --git a/create_video.py b/create_video.py new file mode 100644 index 0000000..791d66a --- /dev/null +++ b/create_video.py @@ -0,0 +1,91 @@ +import os +import numpy as np +import torch +import subprocess +from PIL import Image + +class imgs2vid: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "images": ("IMAGE",), + "fps": ("INT", {"default": 30, "min": 1, "max": 60}), + "video_name_NO_format": ("STRING", {"default": "output"}), + "format": (["mp4", "webm"],), + "audio_path": ("STRING", {"default": "/home/umen/6sec.wav"}), # New audio input + }, + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("comment",) + FUNCTION = "create_video" + OUTPUT_NODE = True + CATEGORY = "Bjornulf" + + def create_video(self, images, fps, video_name_NO_format, format, audio_path): + # Remove any existing extension + video_name_NO_format = os.path.splitext(video_name_NO_format)[0] + # Add the correct extension + output_file = f"{video_name_NO_format}.{format}" + temp_dir = "temp_images" + os.makedirs(temp_dir, exist_ok=True) + # Ensure the output directory exists + os.makedirs(os.path.dirname(output_file) if os.path.dirname(output_file) else ".", exist_ok=True) + + # Save the tensor images as PNG files + for i, img_tensor in enumerate(images): + img = Image.fromarray((img_tensor.cpu().numpy() * 255).astype(np.uint8)) + if format == "webm": + img = img.convert("RGBA") # Ensure alpha channel for WebM + img.save(os.path.join(temp_dir, f"frame_{i:04d}.png")) + + # Construct the FFmpeg command based on the selected format + if format == "mp4": + ffmpeg_cmd = [ + "ffmpeg", + "-y", + "-framerate", str(fps), + "-i", os.path.join(temp_dir, "frame_%04d.png"), + "-i", str(audio_path), + "-crf", "19", + "-c:v", "libx264", + "-pix_fmt", "yuv420p", + output_file + ] + comment = "MP4 format: Widely compatible, efficient compression, no transparency support." + elif format == "webm": + ffmpeg_cmd = [ + "ffmpeg", + "-y", + "-framerate", str(fps), + "-i", os.path.join(temp_dir, "frame_%04d.png"), + "-i", str(audio_path), + "-crf", "19", + "-c:v", "libvpx", + "-b:v", "1M", # Set video bitrate + "-auto-alt-ref", "0", # Disable auto alt ref + "-c:a", "libvorbis", + "-pix_fmt", "yuva420p", + "-shortest", + output_file + ] + comment = "WebM format: Supports transparency, open format, smaller file size, but less compatible than MP4." + + # Run FFmpeg + try: + subprocess.run(ffmpeg_cmd, check=True) + print(f"Video created successfully: {output_file}") + except subprocess.CalledProcessError as e: + print(f"Error creating video: {e}") + finally: + # Clean up temporary files + for file in os.listdir(temp_dir): + os.remove(os.path.join(temp_dir, file)) + os.rmdir(temp_dir) + + return (comment,) + +# Example usage +# images = [torch.rand(256, 256, 3) for _ in range(10)] # Replace with actual image tensors +# imgs2vid().create_video(images, 30, "output", "webm", "/home/ diff --git a/loop_basic_batch.py b/loop_basic_batch.py new file mode 100644 index 0000000..6b37969 --- /dev/null +++ b/loop_basic_batch.py @@ -0,0 +1,21 @@ +class LoopBasicBatch: + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "nb_loops": ("INT", {"default": 1, "min": 0, "max": 1000, "step": 1}), + }, + } + + RETURN_TYPES = ("INT",) + OUTPUT_IS_LIST = (True, False) + FUNCTION = "create_loop_basic_batch" + CATEGORY = "Bjornulf" + + def create_loop_basic_batch(self, nb_loops): + range_values = list() + while nb_loops > 0: + range_values.append(1) + nb_loops -= 1 + return (range_values,) \ No newline at end of file diff --git a/loop_float.py b/loop_float.py new file mode 100644 index 0000000..13ff9f3 --- /dev/null +++ b/loop_float.py @@ -0,0 +1,24 @@ +class LoopFloat: + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "from_this": ("FLOAT", {"default": 0.00, "min": 0.00, "max": 1000.00, "step": 0.01}), + "to_that": ("FLOAT", {"default": 10.00, "min": 0.00, "max": 1000.00, "step": 0.01}), + "jump": ("FLOAT", {"default": 1.00, "min": 0.00, "max": 1000.00, "step": 0.01}), + }, + } + + RETURN_TYPES = ("FLOAT",) + OUTPUT_IS_LIST = (True, False) + FUNCTION = "create_loop_float" + CATEGORY = "Bjornulf" + + def create_loop_float(self, from_this, to_that, jump): + range_values = [] + current_value = from_this + while current_value <= to_that: + range_values.append(round(current_value, 2)) # Round to two decimal places + current_value += jump + return (range_values,) diff --git a/loop_integer.py b/loop_integer.py new file mode 100644 index 0000000..a34b459 --- /dev/null +++ b/loop_integer.py @@ -0,0 +1,32 @@ +# class AnyType(str): +# def __ne__(self, __value: object) -> bool: +# return False +# any_type = AnyType("*") + +class LoopInteger: + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "from_this": ("INT", {"default": 0, "min": 0, "max": 1000, "step": 1}), + "to_that": ("INT", {"default": 10, "min": 0, "max": 1000, "step": 1}), + "jump": ("INT", {"default": 1, "min": 0, "max": 1000, "step": 1}), + }, + # "optional": { + # "nb_loops": (any_type,) + # } + } + + RETURN_TYPES = ("INT",) + OUTPUT_IS_LIST = (True, False) + FUNCTION = "create_loop_integer" + CATEGORY = "Bjornulf" + + def create_loop_integer(self, from_this, to_that, jump): + range_values = list() + current_value = from_this + while current_value <= to_that: + range_values.append(current_value) + current_value += jump + return (range_values,) \ No newline at end of file diff --git a/loop_my_combos_samplers_schedulers.py b/loop_my_combos_samplers_schedulers.py new file mode 100644 index 0000000..fbc3660 --- /dev/null +++ b/loop_my_combos_samplers_schedulers.py @@ -0,0 +1,82 @@ +import comfy + +class LoopCombosSamplersSchedulers: + combinations = [ + "sgm_uniform/euler", "sgm_uniform/dpm_2", "sgm_uniform/dpmpp_2m", "sgm_uniform/lcm", + "sgm_uniform/ddim", "sgm_uniform/uni_pc", + + "normal/ddim", "normal/uni_pc", "normal/euler", "normal/heunpp2", "normal/dpm_2", + + "ddim_uniform/euler", "ddim_uniform/dpm_2", "ddim_uniform/lcm", "ddim_uniform/uni_pc", + + "simple/euler", "simple/heun", "simple/heunpp2", "simple/dpmpp_2m", "simple/lcm", "simple/ipndm", "simple/uni_pc", + + "exponential/dpm_adaptive" + ] + # "normal/uni_pc_bh2", "ddim_uniform/uni_pc_bh2", "simple/uni_pc_bh2", "sgm_uniform/uni_pc_bh2" + @classmethod + def INPUT_TYPES(cls): + # Generate the list of combinations from specified pairs + combi_list = ["ALL 6 COMBINATIONS (sgm_uniform)", "ALL 5 COMBINATIONS (normal)", "ALL 5 COMBINATIONS (ddim_uniform)", "ALL 7 COMBINATIONS (simple)"] + cls.combinations + return { + "required": { + "combination": (combi_list,), + } + } + + RETURN_TYPES = (comfy.samplers.KSampler.SAMPLERS, comfy.samplers.KSampler.SCHEDULERS,) + RETURN_NAMES = ("sampler_name", "scheduler",) + OUTPUT_IS_LIST = (True, False) + FUNCTION = "create_loop_combination" + CATEGORY = "Bjornulf" + + def create_loop_combination(self, combination): + if combination == "ALL 6 COMBINATIONS (sgm_uniform)": + return (["euler", "dpm_2", "dpmpp_2m", "lcm", "ddim", "uni_pc"], "sgm_uniform",) #, "uni_pc_bh2" uni_pc_bh2 is too similar to exist.... + elif combination == "ALL 5 COMBINATIONS (normal)": + return (["ddim", "uni_pc", "euler", "heunpp2", "dpm_2"], "normal",) #, "uni_pc_bh2" + elif combination == "ALL 5 COMBINATIONS (ddim_uniform)": + return (["euler", "dpm_2", "lcm", "ddim", "uni_pc"], "ddim_uniform",) #, "uni_pc_bh2" + elif combination == "ALL 7 COMBINATIONS (simple)": + return (["euler", "heun", "heunpp2", "dpmpp_2m", "lcm", "ipndm", "uni_pc"], "simple",) #, "uni_pc_bh2" + else: + # Split the input and output the selected sampler and scheduler + scheduler, sampler = combination.split("/") + # return [(sampler, scheduler,)] + return ([sampler], scheduler,) + +# + ("exponential", "dpm_adaptive") + +# TESTED GOOD WITH SD3, modelsampling 5 / CFG 3 / 28 steps + +# sgm_uniform + euler +# sgm_uniform + dpm_2 +# sgm_uniform + dpmpp_2m +# sgm_uniform + lcm +# sgm_uniform + ddim +# sgm_uniform + uni_pc +# sgm_uniform + uni_pc_bh2 + +# normal + ddim +# normal + uni_pc +# normal + uni_pc_bh2 +# normal + euler +# normal + heunpp2 +# normal + dpm_2 + +# ddim_uniform + euler +# ddim_uniform + dpm_2 +# ddim_uniform + lcm +# ddim_uniform + uni_pc +# ddim_uniform + uni_pc_bh2 + +# simple + euler +# simple + heun +# simple + heunpp2 +# simple + dpmpp_2m +# simple + lcm +# simple + ipndm +# simple + uni_pc +# simple + uni_pc_bh2 + +# exponential + dpm_adaptive \ No newline at end of file diff --git a/loop_samplers.py b/loop_samplers.py new file mode 100644 index 0000000..87b8d22 --- /dev/null +++ b/loop_samplers.py @@ -0,0 +1,23 @@ +import comfy + +class LoopSamplers: + @classmethod + def INPUT_TYPES(cls): + samplers = ["ALL SAMPLERS"] + list(comfy.samplers.KSampler.SAMPLERS) + return { + "required": { + "sampler_name": (samplers,), + } + } + + RETURN_TYPES = (comfy.samplers.KSampler.SAMPLERS,) + RETURN_NAMES = ("sampler_name",) + OUTPUT_IS_LIST = (True,) + FUNCTION = "create_loop_sampler" + CATEGORY = "Bjornulf" + + def create_loop_sampler(self, sampler_name): + if sampler_name == "ALL SAMPLERS": + return (list(comfy.samplers.KSampler.SAMPLERS),) + else: + return ([sampler_name],) \ No newline at end of file diff --git a/loop_schedulers.py b/loop_schedulers.py new file mode 100644 index 0000000..1ade307 --- /dev/null +++ b/loop_schedulers.py @@ -0,0 +1,23 @@ +import comfy + +class LoopSchedulers: + @classmethod + def INPUT_TYPES(cls): + schedulers = ["ALL SCHEDULERS"] + list(comfy.samplers.KSampler.SCHEDULERS) + return { + "required": { + "scheduler": (schedulers,), + } + } + + RETURN_TYPES = (comfy.samplers.KSampler.SCHEDULERS,) + RETURN_NAMES = ("scheduler",) + OUTPUT_IS_LIST = (True,) + FUNCTION = "create_loop_scheduler" + CATEGORY = "Bjornulf" + + def create_loop_scheduler(self, scheduler): + if scheduler == "ALL SCHEDULERS": + return (list(comfy.samplers.KSampler.SCHEDULERS),) + else: + return ([scheduler],) \ No newline at end of file diff --git a/loop_texts.py b/loop_texts.py new file mode 100644 index 0000000..80267f8 --- /dev/null +++ b/loop_texts.py @@ -0,0 +1,22 @@ +class LoopTexts: + @classmethod + 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"}), + }, + "hidden": { + **{f"text_{i}": ("STRING", {"forceInput": "True"}) for i in range(3, 11)} + } + } + + RETURN_TYPES = ("STRING",) + FUNCTION = "loop_texts" + OUTPUT_IS_LIST = (True,) + CATEGORY = "Bjornulf" + + def loop_texts(self, number_of_inputs, **kwargs): + text_list = [kwargs[f"text_{i}"] for i in range(1, number_of_inputs + 1) if f"text_{i}" in kwargs] + return (text_list,) diff --git a/ollama.py b/ollama.py new file mode 100644 index 0000000..22901e1 --- /dev/null +++ b/ollama.py @@ -0,0 +1,28 @@ +import ollama +from ollama import Client # pip install ollama + +class ollamaLoader: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "user_prompt": ("STRING", {"multiline": True}), + # "selected_model": ((), {}), + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("ollama_response",) + FUNCTION = "connect_2_ollama" + # INPUT_NODE = True # Changed from OUTPUT_NODE to INPUT_NODE + CATEGORY = "Bjornulf" + + # @classmethod + def connect_2_ollama(self, user_prompt): + keep_alive = 0 + list_models=ollama.list() #{'models': [{'name': 'dolphin-llama3:latest', 'model': 'dolphin-llama3:latest', 'modified_at': '2024-04-24T06:56:57.498527412+02:00', 'size': 4661235994, 'digest': '613f068e29f863bb900e568f920401b42678efca873d7a7c87b0d6ef4945fadd', 'details': {'parent_model': '', 'format': 'gguf', 'family': 'llama', 'families': ['llama'], 'parameter_size': '8B', 'quantization_level': 'Q4_0'}}]} + print(list_models) + client = Client(host="http://127.0.0.1:11434") + response = client.generate(model="dolphin-llama3", system="I will give you an object, animal, person or landscape, just create details about it : colors, size, clothes, eyes and other physical details or features in 1 sentence.", prompt=user_prompt, keep_alive=str(keep_alive) + "m") + print("Ollama response : ", response['response']) + return (response['response'],) diff --git a/random_checkpoint.py b/random_checkpoint.py new file mode 100644 index 0000000..decbd8d --- /dev/null +++ b/random_checkpoint.py @@ -0,0 +1,28 @@ +import random + +class RandomCheckpoint: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "number_of_inputs": ("INT", {"default": 2, "min": 2, "max": 10, "step": 1}), + "model_1": ("MODEL", {"forceInput": True}), + "clip_1": ("CLIP", {"forceInput": True}), + "vae_1": ("VAE", {"forceInput": True}), + "model_2": ("MODEL", {"forceInput": True}), + "clip_2": ("CLIP", {"forceInput": True}), + "vae_2": ("VAE", {"forceInput": True}), + } + } + + RETURN_TYPES = ("MODEL", "CLIP", "VAE") + FUNCTION = "random_select" + + def random_select(self, number_of_inputs, **kwargs): + selected_index = random.randint(1, number_of_inputs) + + selected_model = kwargs[f"model_{selected_index}"] + selected_clip = kwargs[f"clip_{selected_index}"] + selected_vae = kwargs[f"vae_{selected_index}"] + + return (selected_model, selected_clip, selected_vae) diff --git a/random_model_clip_vae.py b/random_model_clip_vae.py new file mode 100644 index 0000000..869df78 --- /dev/null +++ b/random_model_clip_vae.py @@ -0,0 +1,33 @@ +import random + +class RandomModelClipVae: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "number_of_inputs": ("INT", {"default": 2, "min": 2, "max": 10, "step": 1}), + "model_1": ("MODEL", {"forceInput": True}), + "clip_1": ("CLIP", {"forceInput": True}), + "vae_1": ("VAE", {"forceInput": True}), + "model_2": ("MODEL", {"forceInput": True}), + "clip_2": ("CLIP", {"forceInput": True}), + "vae_2": ("VAE", {"forceInput": True}), + }, + "hidden": { + **{f"model_{i}": ("MODEL", {"forceInput": True}) for i in range(3, 11)}, + **{f"clip_{i}": ("CLIP", {"forceInput": True}) for i in range(3, 11)}, + **{f"vae_{i}": ("VAE", {"forceInput": True}) for i in range(3, 11)} + } + } + + RETURN_TYPES = ("MODEL", "CLIP", "VAE") + FUNCTION = "random_select" + + def random_select(self, number_of_inputs, **kwargs): + selected_index = random.randint(1, number_of_inputs) + + selected_model = kwargs[f"model_{selected_index}"] + selected_clip = kwargs[f"clip_{selected_index}"] + selected_vae = kwargs[f"vae_{selected_index}"] + + return (selected_model, selected_clip, selected_vae) diff --git a/random_texts.py b/random_texts.py new file mode 100644 index 0000000..30a6902 --- /dev/null +++ b/random_texts.py @@ -0,0 +1,27 @@ +import random + +class RandomTexts: + @classmethod + def INPUT_TYPES(cls): + return { + "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"}), + "seed": ("INT", {"default": "1"}), #Used with control_after_generate, + }, + "hidden": { + **{f"text_{i}": ("STRING", {"forceInput": "True"}) for i in range(3, 31)} + } + } + + RETURN_TYPES = ("STRING",) + FUNCTION = "random_texts" + OUTPUT_IS_LIST = (False,) + CATEGORY = "Bjornulf" + + def random_texts(self, number_of_inputs, number_of_random, **kwargs): + texts = [kwargs[f"text_{i}"] for i in range(1, number_of_inputs + 1) if f"text_{i}" in kwargs] + random_texts = random.sample(texts, min(number_of_random, len(texts))) + return (random_texts,) \ No newline at end of file diff --git a/save_api_image.py b/save_api_image.py new file mode 100644 index 0000000..4df46a9 --- /dev/null +++ b/save_api_image.py @@ -0,0 +1,47 @@ +import os +import numpy as np +from PIL import Image + +class SaveApiImage: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "image": ("IMAGE", {"forceInput": True}), + } + } + + FUNCTION = "save_api_image" + RETURN_TYPES = () + OUTPUT_NODE = True + CATEGORY = "Bjornulf" + + def save_api_image(self, image): + # Ensure the output directory exists + os.makedirs("./output/", exist_ok=True) + + # Convert the image from ComfyUI format to PIL Image + i = 255. * image.cpu().numpy() + if i.ndim > 3: + i = np.squeeze(i) + if i.ndim == 2: + i = i[:, :, np.newaxis] + + img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8)) + + # Determine the next available filename + counter = 1 + while True: + filename = f"./output/api_{counter:05d}.png" + if not os.path.exists(filename): + break + counter += 1 + + # Save the image with the determined filename + img.save(filename, format="PNG") + + # Write the number of the last image to a text file with leading zeroes + with open("./output/api_next_image.txt", "w") as f: + f.write(f"api_{counter+1:05d}.png") + + return () diff --git a/save_text.py b/save_text.py new file mode 100644 index 0000000..4e4d23f --- /dev/null +++ b/save_text.py @@ -0,0 +1,37 @@ +import os + +class SaveText: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "text": ("STRING", {"multiline": True, "forceInput": True}), + "filename": ("STRING", {"default": "001.txt"}) + } + } + + # INPUT_IS_LIST = True + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("text",) + FUNCTION = "save_text" + OUTPUT_NODE = True + CATEGORY = "Bjornulf" + # OUTPUT_IS_LIST = (True,) + + def save_text(self, text, filename): + directory = "custom_nodes/Bjornulf_custom_nodes/SaveText/" + if not os.path.exists(directory): + os.makedirs(directory) + + base, ext = os.path.splitext(filename) + counter = 1 + new_filename = os.path.join(directory, filename) + + while os.path.exists(new_filename): + new_filename = os.path.join(directory, f"{base}_{counter:03d}{ext}") + counter += 1 + + with open(new_filename, 'w') as file: + file.write(text) + + return {"ui": {"text": text}, "result": (text,)} \ No newline at end of file diff --git a/save_tmp_image.py b/save_tmp_image.py new file mode 100644 index 0000000..9838dde --- /dev/null +++ b/save_tmp_image.py @@ -0,0 +1,38 @@ +import os +import numpy as np +from PIL import Image + +class SaveTmpImage: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "image": ("IMAGE", {"forceInput": True}), + } + } + + FUNCTION = "save_image" + RETURN_TYPES = () + OUTPUT_NODE = True + CATEGORY = "Bjornulf" + + def save_image(self, image): + # Ensure the output directory exists + # os.makedirs("./output", exist_ok=True) + + # Convert the image from ComfyUI format to PIL Image + # Assuming the first two dimensions are extra, and we need to keep the last two + i = 255. * image.cpu().numpy() + # Reshape the image if it's not in the expected format, remove any leading dimensions of size 1 + if i.ndim > 3: + i = np.squeeze(i) + # Ensure the image is 3D (height, width, channels) + if i.ndim == 2: + i = i[:, :, np.newaxis] # Add a channel dimension if it's missing + + img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8)) + + # Save the image, overwriting if it exists + img.save("./output/tmp_api.png", format="PNG") + + return () diff --git a/show_float.py b/show_float.py new file mode 100644 index 0000000..fc42d2e --- /dev/null +++ b/show_float.py @@ -0,0 +1,18 @@ +class ShowFloat: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "float_value": ("FLOAT", {"forceInput": True}), + }, + } + + INPUT_IS_LIST = True + RETURN_TYPES = () + FUNCTION = "show_float" + OUTPUT_NODE = True + INPUT_IS_LIST = (True,) + CATEGORY = "Bjornulf" + + def show_float(self, float_value): + return {"ui": {"text": float_value}} diff --git a/show_int.py b/show_int.py new file mode 100644 index 0000000..8ba1735 --- /dev/null +++ b/show_int.py @@ -0,0 +1,18 @@ +class ShowInt: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "int_value": ("INT", {"forceInput": True}), + }, + } + + INPUT_IS_LIST = True + RETURN_TYPES = () + FUNCTION = "show_int" + OUTPUT_NODE = True + INPUT_IS_LIST = (True,) + CATEGORY = "Bjornulf" + + def show_int(self, int_value): + return {"ui": {"text": int_value}} diff --git a/show_text.py b/show_text.py new file mode 100644 index 0000000..32662c5 --- /dev/null +++ b/show_text.py @@ -0,0 +1,18 @@ +class ShowText: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "text_value": ("STRING", {"forceInput": True}), + }, + } + + INPUT_IS_LIST = True + RETURN_TYPES = () + FUNCTION = "show_text" + OUTPUT_NODE = True + INPUT_IS_LIST = (True,) + CATEGORY = "Bjornulf" + + def show_text(self, text_value): + return {"ui": {"text": text_value}} diff --git a/video_pingpong.py b/video_pingpong.py new file mode 100644 index 0000000..0b7f3c3 --- /dev/null +++ b/video_pingpong.py @@ -0,0 +1,23 @@ +import torch + +class VideoPingPong: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "images": ("IMAGE",), + }, + } + + RETURN_TYPES = ("IMAGE",) + FUNCTION = "pingpong_images" + CATEGORY = "Bjornulf" + + def pingpong_images(self, images): + if isinstance(images, torch.Tensor): + reversed_images = torch.flip(images, [0]) + combined_images = torch.cat((images, reversed_images[1:]), dim=0) + else: + reversed_images = images[::-1] + combined_images = images + reversed_images[1:] + return (combined_images,) \ No newline at end of file diff --git a/web/js/BJORNULF_TYPES.js.txt b/web/js/BJORNULF_TYPES.js.txt new file mode 100644 index 0000000..db6345c --- /dev/null +++ b/web/js/BJORNULF_TYPES.js.txt @@ -0,0 +1,29 @@ +import { app } from "../../../scripts/app.js"; + +app.registerExtension({ + name: "Bjornulf.CustomBjornulfType", + async beforeRegisterNodeDef(nodeType, nodeData, app) { + if (nodeData.name === "Bjornulf_WriteImageCharacters") { + const onNodeCreated = nodeType.prototype.onNodeCreated; + nodeType.prototype.onNodeCreated = function () { + onNodeCreated?.apply(this, arguments); + const myInput = this.inputs.find(input => input.name === "BJORNULF_CHARACTER"); + if (myInput) { + myInput.type = "BJORNULF_CHARACTER"; + } + }; + } + else if (nodeData.name === "Bjornulf_WriteImageCharacter") { + + } + }, + async setup(app) { + app.registerCustomNodeType("BJORNULF_CHARACTER", (value) => { + return { + type: "BJORNULF_CHARACTER", + data: { value: value || "" }, + name: "BJORNULF_CHARACTER" + }; + }); + } +}); \ No newline at end of file diff --git a/web/js/CUSTOM_STRING.js.txt b/web/js/CUSTOM_STRING.js.txt new file mode 100644 index 0000000..5202e8b --- /dev/null +++ b/web/js/CUSTOM_STRING.js.txt @@ -0,0 +1,52 @@ +import { app } from "../../../scripts/app.js"; + +app.registerExtension({ + name: "Bjornulf.CustomStringType", + async beforeRegisterNodeDef(nodeType, nodeData, app) { + if (nodeData.name === "Bjornulf_WriteImageAllInOne") { + const onNodeCreated = nodeType.prototype.onNodeCreated; + nodeType.prototype.onNodeCreated = function () { + onNodeCreated?.apply(this, arguments); + const locationInput = this.inputs.find(input => input.name === "location"); + if (locationInput) { + locationInput.type = "CUSTOM_STRING"; + } + }; + } + }, + async setup(app) { + app.registerCustomNodeType("CUSTOM_STRING", (value) => { + return { + type: "CustomStringType", + data: { value: value || "" }, + name: "CustomStringType" + }; + }); + } +}); + + + // Override the default onConnectionCreated method + const originalOnConnectionCreated = LGraphCanvas.prototype.onConnectionCreated; + LGraphCanvas.prototype.onConnectionCreated = function(connection, e, node_for_click) { + if (node_for_click && node_for_click.type === "WriteImageAllInOne" && connection.targetInput.name === "location") { + // Check if the connected node is not already a CustomString + if (connection.origin_node.type !== "CustomString") { + // Create a new CustomString node + const customStringNode = LiteGraph.createNode("CustomString"); + // Position the new node + customStringNode.pos = [connection.origin_node.pos[0] + 200, connection.origin_node.pos[1]]; + this.graph.add(customStringNode); + + // Connect the new CustomString node + connection.origin_node.connect(connection.origin_slot, customStringNode, 0); + customStringNode.connect(0, node_for_click, connection.target_slot); + + // Remove the original connection + connection.origin_node.disconnectOutput(connection.origin_slot, node_for_click); + + return true; // Prevent the original connection + } + } + return originalOnConnectionCreated.apply(this, arguments); + }; \ No newline at end of file diff --git a/web/js/combine_texts.js b/web/js/combine_texts.js new file mode 100644 index 0000000..f44b1af --- /dev/null +++ b/web/js/combine_texts.js @@ -0,0 +1,52 @@ +import { app } from "../../../scripts/app.js"; + +app.registerExtension({ + name: "Bjornulf.CombineTexts", + async nodeCreated(node) { + if (node.comfyClass === "Bjornulf_CombineTexts") { + const updateInputs = () => { + const numInputsWidget = node.widgets.find(w => w.name === "number_of_inputs"); + 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('text_')); + + // 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 = `text_${i}`; + if (!node.inputs.find(input => input.name === inputName)) { + node.addInput(inputName, "STRING"); + } + } + } else { + // Remove excess text inputs if too many + node.inputs = node.inputs.filter(input => !input.name.startsWith('text_') || parseInt(input.name.split('_')[1]) <= numInputs); + } + + node.setSize(node.computeSize()); + }; + + // Move number_of_inputs to the top initially + const numInputsWidget = node.widgets.find(w => w.name === "number_of_inputs"); + 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/loop_texts.js b/web/js/loop_texts.js new file mode 100644 index 0000000..931d53a --- /dev/null +++ b/web/js/loop_texts.js @@ -0,0 +1,52 @@ +import { app } from "../../../scripts/app.js"; + +app.registerExtension({ + name: "Bjornulf.LoopTexts", + async nodeCreated(node) { + if (node.comfyClass === "Bjornulf_LoopTexts") { + const updateInputs = () => { + const numInputsWidget = node.widgets.find(w => w.name === "number_of_inputs"); + 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('text_')); + + // 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 = `text_${i}`; + if (!node.inputs.find(input => input.name === inputName)) { + node.addInput(inputName, "STRING"); + } + } + } else { + // Remove excess text inputs if too many + node.inputs = node.inputs.filter(input => !input.name.startsWith('text_') || parseInt(input.name.split('_')[1]) <= numInputs); + } + + node.setSize(node.computeSize()); + }; + + // Move number_of_inputs to the top initially + const numInputsWidget = node.widgets.find(w => w.name === "number_of_inputs"); + 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/random_model_clip_vae.js b/web/js/random_model_clip_vae.js new file mode 100644 index 0000000..5924c5a --- /dev/null +++ b/web/js/random_model_clip_vae.js @@ -0,0 +1,65 @@ +import { app } from "../../../scripts/app.js"; + +app.registerExtension({ + name: "Bjornulf.RandomModelClipVae", + async nodeCreated(node) { + if (node.comfyClass === "Bjornulf_RandomModelClipVae") { + const updateInputs = () => { + const numInputsWidget = node.widgets.find(w => w.name === "number_of_inputs"); + if (!numInputsWidget) return; + + const numInputs = numInputsWidget.value; + + // Initialize node.inputs if it doesn't exist + if (!node.inputs) { + node.inputs = []; + } + + // Filter existing model, clip, and vae inputs + const existingModelInputs = node.inputs.filter(input => input.name.startsWith('model_')); + const existingClipInputs = node.inputs.filter(input => input.name.startsWith('clip_')); + const existingVaeInputs = node.inputs.filter(input => input.name.startsWith('vae_')); + + // Determine if we need to add or remove inputs + if (existingModelInputs.length < numInputs || existingClipInputs.length < numInputs || existingVaeInputs.length < numInputs) { + // Add new model, clip, and vae inputs if not enough existing + for (let i = Math.max(existingModelInputs.length, existingClipInputs.length, existingVaeInputs.length) + 1; i <= numInputs; i++) { + const modelInputName = `model_${i}`; + const clipInputName = `clip_${i}`; + const vaeInputName = `vae_${i}`; + if (!node.inputs.find(input => input.name === modelInputName)) { + node.addInput(modelInputName, "MODEL"); + } + if (!node.inputs.find(input => input.name === clipInputName)) { + node.addInput(clipInputName, "CLIP"); + } + if (!node.inputs.find(input => input.name === vaeInputName)) { + node.addInput(vaeInputName, "VAE"); + } + } + } else { + // Remove excess model, clip, and vae inputs if too many + node.inputs = node.inputs.filter(input => + (!input.name.startsWith('model_') && !input.name.startsWith('clip_') && !input.name.startsWith('vae_')) || + (parseInt(input.name.split('_')[1]) <= numInputs) + ); + } + + node.setSize(node.computeSize()); + }; + + // Move number_of_inputs to the top initially + const numInputsWidget = node.widgets.find(w => w.name === "number_of_inputs"); + 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); + } + } +}); \ No newline at end of file diff --git a/web/js/random_texts.js b/web/js/random_texts.js new file mode 100644 index 0000000..e5718cd --- /dev/null +++ b/web/js/random_texts.js @@ -0,0 +1,58 @@ +import { app } from "../../../scripts/app.js"; + +app.registerExtension({ + name: "Bjornulf.RandomTexts", + async nodeCreated(node) { + if (node.comfyClass === "Bjornulf_RandomTexts") { + const updateInputs = () => { + const numInputsWidget = node.widgets.find(w => w.name === "number_of_inputs"); + 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('text_')); + + // 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 = `text_${i}`; + if (!node.inputs.find(input => input.name === inputName)) { + node.addInput(inputName, "STRING"); + } + } + } else { + // Remove excess text inputs if too many + node.inputs = node.inputs.filter(input => !input.name.startsWith('text_') || parseInt(input.name.split('_')[1]) <= numInputs); + } + + node.setSize(node.computeSize()); + }; + + // Set seed widget to hidden input + const seedWidget = node.widgets.find(w => w.name === "seed"); + if (seedWidget) { + seedWidget.type = "HIDDEN"; + } + + // Move number_of_inputs to the top initially + const numInputsWidget = node.widgets.find(w => w.name === "number_of_inputs"); + 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_float.js b/web/js/show_float.js new file mode 100644 index 0000000..471b8f1 --- /dev/null +++ b/web/js/show_float.js @@ -0,0 +1,76 @@ +import { app } from "../../../scripts/app.js"; +import { ComfyWidgets } from "../../../scripts/widgets.js"; + +// Styles for the text area +const textAreaStyles = { + readOnly: true, + opacity: 1, + padding: '10px', + border: '1px solid #ccc', + borderRadius: '5px', + backgroundColor: '#222', + color: 'Lime', + fontFamily: 'Arial, sans-serif', + fontSize: '14px', + lineHeight: '1.4', + resize: 'vertical', + overflowY: 'auto', +}; + +// Displays input text on a node +app.registerExtension({ + name: "Bjornulf.ShowFloat", + async beforeRegisterNodeDef(nodeType, nodeData, app) { + if (nodeData.name === "Bjornulf_ShowFloat") { + function createStyledTextArea(text) { + const widget = ComfyWidgets["STRING"](this, "text", ["STRING", { multiline: true }], app).widget; + widget.inputEl.readOnly = true; + const textArea = widget.inputEl; + + Object.assign(textArea.style, textAreaStyles); + textArea.classList.add('bjornulf-show-text'); + widget.value = text; + return widget; + } + + function populate(text) { + if (this.widgets) { + for (let i = 1; i < this.widgets.length; i++) { + this.widgets[i].onRemove?.(); + } + this.widgets.length = 1; + } + + const v = Array.isArray(text) ? text : [text]; + for (const list of v) { + if (list) { + createStyledTextArea.call(this, list); + } + } + + requestAnimationFrame(() => { + const sz = this.computeSize(); + if (sz[0] < this.size[0]) sz[0] = this.size[0]; + if (sz[1] < this.size[1]) sz[1] = this.size[1]; + this.onResize?.(sz); + app.graph.setDirtyCanvas(true, false); + }); + } + + // When the node is executed we will be sent the input text, display this in the widget + const onExecuted = nodeType.prototype.onExecuted; + nodeType.prototype.onExecuted = function (message) { + onExecuted?.apply(this, arguments); + populate.call(this, message.text); + }; + + const onConfigure = nodeType.prototype.onConfigure; + nodeType.prototype.onConfigure = function () { + onConfigure?.apply(this, arguments); + if (this.widgets_values?.length) { + populate.call(this, this.widgets_values); + } + }; + } + }, +}); \ No newline at end of file diff --git a/web/js/show_int.js b/web/js/show_int.js new file mode 100644 index 0000000..6234708 --- /dev/null +++ b/web/js/show_int.js @@ -0,0 +1,76 @@ +import { app } from "../../../scripts/app.js"; +import { ComfyWidgets } from "../../../scripts/widgets.js"; + +// Styles for the text area +const textAreaStyles = { + readOnly: true, + opacity: 1, + padding: '10px', + border: '1px solid #ccc', + borderRadius: '5px', + backgroundColor: '#222', + color: 'Lime', + fontFamily: 'Arial, sans-serif', + fontSize: '14px', + lineHeight: '1.4', + resize: 'vertical', + overflowY: 'auto', +}; + +// Displays input text on a node +app.registerExtension({ + name: "Bjornulf.ShowInt", + async beforeRegisterNodeDef(nodeType, nodeData, app) { + if (nodeData.name === "Bjornulf_ShowInt") { + function createStyledTextArea(text) { + const widget = ComfyWidgets["STRING"](this, "text", ["STRING", { multiline: true }], app).widget; + widget.inputEl.readOnly = true; + const textArea = widget.inputEl; + + Object.assign(textArea.style, textAreaStyles); + textArea.classList.add('bjornulf-show-text'); + widget.value = text; + return widget; + } + + function populate(text) { + if (this.widgets) { + for (let i = 1; i < this.widgets.length; i++) { + this.widgets[i].onRemove?.(); + } + this.widgets.length = 1; + } + + const v = Array.isArray(text) ? text : [text]; + for (const list of v) { + if (list) { + createStyledTextArea.call(this, list); + } + } + + requestAnimationFrame(() => { + const sz = this.computeSize(); + if (sz[0] < this.size[0]) sz[0] = this.size[0]; + if (sz[1] < this.size[1]) sz[1] = this.size[1]; + this.onResize?.(sz); + app.graph.setDirtyCanvas(true, false); + }); + } + + // When the node is executed we will be sent the input text, display this in the widget + const onExecuted = nodeType.prototype.onExecuted; + nodeType.prototype.onExecuted = function (message) { + onExecuted?.apply(this, arguments); + populate.call(this, message.text); + }; + + const onConfigure = nodeType.prototype.onConfigure; + nodeType.prototype.onConfigure = function () { + onConfigure?.apply(this, arguments); + if (this.widgets_values?.length) { + populate.call(this, this.widgets_values); + } + }; + } + }, +}); \ No newline at end of file diff --git a/web/js/show_text.js b/web/js/show_text.js new file mode 100644 index 0000000..2f2a01a --- /dev/null +++ b/web/js/show_text.js @@ -0,0 +1,76 @@ +import { app } from "../../../scripts/app.js"; +import { ComfyWidgets } from "../../../scripts/widgets.js"; + +// Styles for the text area +const textAreaStyles = { + readOnly: true, + opacity: 1, + padding: '10px', + border: '1px solid #ccc', + borderRadius: '5px', + backgroundColor: '#222', + color: 'Lime', + fontFamily: 'Arial, sans-serif', + fontSize: '14px', + lineHeight: '1.4', + resize: 'vertical', + overflowY: 'auto', +}; + +// Displays input text on a node +app.registerExtension({ + name: "Bjornulf.ShowText", + async beforeRegisterNodeDef(nodeType, nodeData, app) { + if (nodeData.name === "Bjornulf_ShowText") { + function createStyledTextArea(text) { + const widget = ComfyWidgets["STRING"](this, "text", ["STRING", { multiline: true }], app).widget; + widget.inputEl.readOnly = true; + const textArea = widget.inputEl; + + Object.assign(textArea.style, textAreaStyles); + textArea.classList.add('bjornulf-show-text'); + widget.value = text; + return widget; + } + + function populate(text) { + if (this.widgets) { + for (let i = 1; i < this.widgets.length; i++) { + this.widgets[i].onRemove?.(); + } + this.widgets.length = 1; + } + + const v = Array.isArray(text) ? text : [text]; + for (const list of v) { + if (list) { + createStyledTextArea.call(this, list); + } + } + + requestAnimationFrame(() => { + const sz = this.computeSize(); + if (sz[0] < this.size[0]) sz[0] = this.size[0]; + if (sz[1] < this.size[1]) sz[1] = this.size[1]; + this.onResize?.(sz); + app.graph.setDirtyCanvas(true, false); + }); + } + + // When the node is executed we will be sent the input text, display this in the widget + const onExecuted = nodeType.prototype.onExecuted; + nodeType.prototype.onExecuted = function (message) { + onExecuted?.apply(this, arguments); + populate.call(this, message.text); + }; + + const onConfigure = nodeType.prototype.onConfigure; + nodeType.prototype.onConfigure = function () { + onConfigure?.apply(this, arguments); + if (this.widgets_values?.length) { + populate.call(this, this.widgets_values); + } + }; + } + }, +}); \ No newline at end of file diff --git a/web/js/write_image_characters.js b/web/js/write_image_characters.js new file mode 100644 index 0000000..0f3a0cd --- /dev/null +++ b/web/js/write_image_characters.js @@ -0,0 +1,53 @@ +import { app } from "../../../scripts/app.js"; + +app.registerExtension({ + name: "Bjornulf.WriteImageCharacters", + async nodeCreated(node) { + if (node.comfyClass === "Bjornulf_WriteImageCharacters") { + const updateInputs = () => { + const numInputsWidget = node.widgets.find(w => w.name === "number_of_characters"); + 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('character_')); + + // 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 = `character_${i}`; + if (!node.inputs.find(input => input.name === inputName)) { + // node.addInput(inputName, "STRING"); + node.addInput(inputName, "BJORNULF_CHARACTER"); + } + } + } else { + // Remove excess text inputs if too many + node.inputs = node.inputs.filter(input => !input.name.startsWith('character_') || parseInt(input.name.split('_')[1]) <= numInputs); + } + + node.setSize(node.computeSize()); + }; + + // Move number_of_inputs to the top initially + const numInputsWidget = node.widgets.find(w => w.name === "number_of_characters"); + 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/write_image_allinone.py b/write_image_allinone.py new file mode 100644 index 0000000..17dbeeb --- /dev/null +++ b/write_image_allinone.py @@ -0,0 +1,84 @@ +class WriteImageAllInOne: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "art_style": (["none", "drawing", "digital art", "photography"],), + "location": ("STRING", {"forceInput": "True"}), + "lighting": ("STRING", {"multiline": True}), + "camera_angle": ("STRING", {"multiline": True}), + }, + "optional": { + "other": ("STRING", {"multiline": True},), + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("text",) + FUNCTION = "write_image_allinone" + OUTPUT_NODE = True + CATEGORY = "Bjornulf" + + def write_image_allinone(self, art_style, location, lighting, camera_angle, other=""): + text = f"Art Style: {art_style}\n\n" + text += f"Location:\n{location}\n\n" + text += f"Lighting:\n{lighting}\n\n" + text += f"Camera Angle:\n{camera_angle}\n\n" + if other: + text += f"Other:\n{other}\n\n" + return (text,) + + @classmethod + def CREATE_CONNECTED_NODES(cls): + return [ + { + "class_type": "PrimitiveNode", + "inputs": { + "value": "drawing", + "label": "Art Style", + "type": "combo", + "options": ["drawing", "digital art", "photography"] + }, + "output": ["art_style"] + }, + { + "class_type": "PrimitiveNode", + "inputs": { + "value": "", + "label": "Location", + "type": "text", + "multiline": True + }, + "output": ["location"] + }, + { + "class_type": "PrimitiveNode", + "inputs": { + "value": "", + "label": "Lighting", + "type": "text", + "multiline": True + }, + "output": ["lighting"] + }, + { + "class_type": "PrimitiveNode", + "inputs": { + "value": "", + "label": "Camera Angle", + "type": "text", + "multiline": True + }, + "output": ["camera_angle"] + }, + { + "class_type": "PrimitiveNode", + "inputs": { + "value": "", + "label": "Other", + "type": "text", + "multiline": True + }, + "output": ["other"] + } + ] \ No newline at end of file diff --git a/write_image_character.py b/write_image_character.py new file mode 100644 index 0000000..2f28d81 --- /dev/null +++ b/write_image_character.py @@ -0,0 +1,22 @@ +class WriteImageCharacter: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "description": ("STRING", {"multiline": True}), + "action": ("STRING", {"multiline": False}), + "emotion": ("STRING", {"multiline": False}), + "clothes": ("STRING", {"multiline": False}), + }, + } + + # RETURN_TYPES = ("STRING",) + RETURN_TYPES = ("BJORNULF_CHARACTER",) + RETURN_NAMES = ("character_details",) + FUNCTION = "write_image_character" + OUTPUT_NODE = True + CATEGORY = "Bjornulf" + + def write_image_character(self, description, action, emotion, clothes): + text = f"{description}, {action}, {emotion}, {clothes}" + return (text,) \ No newline at end of file diff --git a/write_image_characters.py b/write_image_characters.py new file mode 100644 index 0000000..c6d9342 --- /dev/null +++ b/write_image_characters.py @@ -0,0 +1,32 @@ +class WriteImageCharacters: + @classmethod + def INPUT_TYPES(cls): + hidden_inputs = {} + for i in range(2, 6): # Notice the range starts at 2 and ends at 6 to include 5 + hidden_inputs.update({ + f"character_{i}": ("BJORNULF_CHARACTER", {"forceInput": True}), + # f"character_{i}": ("STRING", {"forceInput": True}), + }) + return { + "required": { + "number_of_characters": ("INT", {"default": 1, "min": 1, "max": 5, "step": 1}), + "character_1": ("BJORNULF_CHARACTER", {"forceInput": True}), + # "character_1": ("STRING", {"forceInput": True}), + }, + "optional": { + "other": ("STRING", {"multiline": True, "forceInput": True}), + }, + "hidden": hidden_inputs + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("text",) + FUNCTION = "write_image_characters" + OUTPUT_NODE = True + CATEGORY = "Bjornulf" + + def write_image_characters(self, number_of_characters, other="", **kwargs): + text = f"Other: {other}\n" + for i in range(1, number_of_characters + 1): + text += f"{kwargs.get(f'character_{i}', '')}\n" + return (text,) diff --git a/write_image_environment.py b/write_image_environment.py new file mode 100644 index 0000000..8b9bedb --- /dev/null +++ b/write_image_environment.py @@ -0,0 +1,27 @@ +class WriteImageEnvironment: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "art_style": (["drawing", "digital art", "photography"],), + "location": ("STRING", {"multiline": True}), + "lighting": ("STRING", {"multiline": True}), + "camera_angle": ("STRING", {"multiline": True}), + }, + "optional": { + "other": ("STRING", {"multiline": True, "forceInput": True},), + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("text",) + FUNCTION = "write_image_environment" + OUTPUT_NODE = True + CATEGORY = "Bjornulf" + + def write_image_environment(self, art_style, location, lighting, camera_angle, **kwargs): + text = f"Art Style: {art_style}\n\n" + text += f"Location:\n{location}\n\n" + text += f"Lighting:\n{lighting}\n\n" + text += f"Camera Angle:\n{camera_angle}\n\n" + return (text,) \ No newline at end of file diff --git a/write_text.py b/write_text.py new file mode 100644 index 0000000..a9860da --- /dev/null +++ b/write_text.py @@ -0,0 +1,19 @@ +class WriteText: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "text": ("STRING", {"multiline": True}), + } + } + + # INPUT_IS_LIST = True + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("text",) + FUNCTION = "write_text" + OUTPUT_NODE = True + OUTPUT_IS_LIST = (False,) + CATEGORY = "Bjornulf" + + def write_text(self, text): + return {"ui": {"text": text}, "result": (text,)}