# Developed by Omar - https://github.com/omar92 # https://civitai.com/user/omar92 # discord: Omar92#3374 import io import json import os import random import time from urllib.request import urlopen import numpy as np import requests import torch from PIL import Image, ImageFont, ImageDraw import importlib import comfy.samplers import comfy.sd import comfy.utils import torch.nn as nn MAX_RESOLUTION = 8192 # region INSTALLATION CLEANUP (thanks WAS i got this from you) # Delete legacy nodes legacy_nodes = ['ChatGPT_Omar92.py', 'LatentUpscaleMultiply_Omar92.py', 'StringSuit_Omar92.py'] legacy_nodes_found = [] f_disp = False for f in legacy_nodes: node_path_dir = os.getcwd()+'./custom_nodes/' file = f'{node_path_dir}{f}' if os.path.exists(file): import zipfile if not f_disp: print( '\033[33mQualityOflife Node Suite:\033[0m Found legacy nodes. Archiving legacy nodes...') f_disp = True legacy_nodes_found.append(file) if legacy_nodes_found: from os.path import basename archive = zipfile.ZipFile( f'{node_path_dir}QualityOflife_Backup_{round(time.time())}.zip', "w") for f in legacy_nodes_found: archive.write(f, basename(f)) try: os.remove(f) except OSError: pass archive.close() if f_disp: print('\033[33mQualityOflife Node Suite:\033[0m Legacy cleanup complete.') # endregion # region global PACKAGE_NAME = '\033[33mQualityOfLifeSuit_Omar92:\033[0m' NODE_FILE = os.path.abspath(__file__) SUIT_DIR = (os.path.dirname(os.path.dirname(NODE_FILE)) if os.path.dirname(os.path.dirname(NODE_FILE)) == 'QualityOfLifeSuit_Omar92' or os.path.dirname(os.path.dirname(NODE_FILE)) == 'QualityOfLifeSuit_Omar92-dev' else os.path.dirname(NODE_FILE)) SUIT_DIR = os.path.normpath(os.path.join(SUIT_DIR, '..')) print(f'\033[33mQualityOfLifeSuit_Omar92_DIR:\033[0m {SUIT_DIR}') def enforce_mul_of_64(d): leftover = d % 8 # 8 is the number of pixels per byte if leftover != 0: # if the number of pixels is not a multiple of 8 if (leftover < 4): # if the number of pixels is less than 4 d -= leftover # remove the leftover pixels else: # if the number of pixels is more than 4 d += 8 - leftover # add the leftover pixels return d # endregion # region openAITools def install_openai(): # Helper function to install the OpenAI module if not already installed try: importlib.import_module('openai') except ImportError: import pip pip.main(['install', 'openai']) def get_api_key(): # Helper function to get the API key from the file try: # open config file configPath = os.path.join(SUIT_DIR, "config.json") with open(configPath, 'r') as f: # Open the file and read the API key config = json.load(f) api_key = config["openAI_API_Key"] except: print("Error: OpenAI API key file not found OpenAI features wont work for you") return "" return api_key # Return the API key openAI_models = None #region chatGPTDefaultInitMessages chatGPTDefaultInitMessage_tags = """ First, some basic Stable Diffusion prompting rules for you to better understand the syntax. The parentheses are there for grouping prompt words together, so that we can set uniform weight to multiple words at the same time. Notice the ":1.2" in (masterpiece, best quality, absurdres:1.2), it means that we set the weight of both "masterpiece" and "best quality" to 1.2. The parentheses can also be used to directly increase weight for single word without adding ":WEIGHT". For example, we can type ((masterpiece)), this will increase the weight of "masterpiece" to 1.21. This basic rule is imperative that any parentheses in a set of prompts have purpose, and so they must not be remove at any case. Conversely, when brackets are used in prompts, it means to decrease the weight of a word. For example, by typing "[bird]", we decrease the weight of the word "bird" by 1.1. Now, I've develop a prompt template to use generate character portraits in Stable Diffusion. Here's how it works. Every time user sent you "CHAR prompts", you should give prompts that follow below format: CHAR: [pre-defined prompts], [location], [time], [weather], [gender], [skin color], [photo type], [pose], [camera position], [facial expression], [body feature], [skin feature], [eye color], [outfit], [hair style], [hair color], [accessories], [random prompt], [pre-defined prompts] are always the same, which are "RAW, (masterpiece, best quality, photorealistic, absurdres, 8k:1.2), best lighting, complex pupils, complex textile, detailed background". Don't change anything in [pre-defined prompts], meaning that you SHOULD NOT REMOVE OR MODIFY the parentheses since their purpose is for grouping prompt words together so that we can set uniform weight to them; [location] is the location where character is in, can be either outdoor location or indoor, but need to be specific; [time] refers to the time of day, can be "day", "noon", "night", "evening", "dawn" or "dusk"; [weather] is the weather, for example "windy", "rainy" or "cloudy"; [gender] is either "1boy" or "1girl"; [skin color] is the skin color of the character, could be "dark skin", "yellow skin" or "pale skin"; [photo type] can be "upper body", "full body", "close up", "mid-range", "Headshot", "3/4 shot" or "environmental portrait"; [pose] is the character's pose, for example, "standing", "sitting", "kneeling" or "squatting" ...; [camera position] can be "from top", "from below", "from side", "from front" or "from behind"; [facial expression] is the expression of the character, you should give user a random expression; [body feature] describe how the character's body looks like, for example, it could be "wide hip", "large breasts" or "sexy", try to be creative; [skin feature] is the feature of character's skin. Could be "scar on skin", "dirty skin", "tanned mark", "birthmarks" or other skin features you can think of; [eye color] is the pupil color of the character, it can be of any color as long as the color looks natural on human eyes, so avoid colors like pure red or pure black; [outfit] is what character wears, it should include at least the top wear, bottom wear and footwear, for example, "crop top, shorts, sneakers", the style of outfit can be any, but the [character gender] should be considered; [hair style] is the hairstyle of the character, [character gender] should be taken into account when setting the hairstyle; [hair color] can be of any color, for example, "orange hair", "multi-colored hair"; [accessories] is the accessory the character might wear, can be "chocker", "earrings", "bracelet" or other types of accessory; [random prompt] will test your creativity, put anything here, just remember that you can only use nouns in [random prompt], the number of [random prompt] can be between 1 to 4. For example, you could give "campfire", but you can also give "shooting star, large moon, fallen leaves". Again, be creative with this one. also use gelbooru tags as much as you can if you use gelbooru write "gTags" before it Do not use markdown syntax in prompts, do not use capital letter and keep all prompt words in the same line. Respond with "prompt:" to start prompting with us. """; chatGPTDefaultInitMessage_description = """ act as prompt generator ,i will give you text and you describe an image that match that text in details use gelbooru tags in your description also describe the high quality of the image, answer with one response only """; def get_init_message(isTags=False): if(isTags): return chatGPTDefaultInitMessage_tags else: return chatGPTDefaultInitMessage_description #endregion chatGPTDefaultInitMessages def get_openAI_models(): global openAI_models if (openAI_models != None): return openAI_models install_openai() import openai # Set the API key for the OpenAI module openai.api_key = get_api_key() try: models = openai.Model.list() # Get the list of models except: print("Error: OpenAI API key is invalid OpenAI features wont work for you") return [] openAI_models = [] # Create a list for the chat models for model in models["data"]: # Loop through the models openAI_models.append(model["id"]) # Add the model to the list return openAI_models # Return the list of chat models openAI_gpt_models = None def get_gpt_models(): global openAI_gpt_models if (openAI_gpt_models != None): return openAI_gpt_models models = get_openAI_models() openAI_gpt_models = [] # Create a list for the chat models for model in models: # Loop through the models if ("gpt" in model.lower()): openAI_gpt_models.append(model) return openAI_gpt_models # Return the list of chat models class O_ChatGPT_O: """ this node is based on the openAI GPT-3 API to generate propmpts using the AI """ # Define the input types for the node @classmethod def INPUT_TYPES(cls): return { "required": { # Multiline string input for the prompt "prompt": ("STRING", {"multiline": True}), "model": (get_gpt_models(), {"default": "gpt-3.5-turbo"}), "behaviour": (["tags","description"], {"default": "description"}), }, "optional": { "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), } } RETURN_TYPES = ("STRING",) # Define the return type of the node FUNCTION = "fun" # Define the function name for the node CATEGORY = "O/OpenAI" # Define the category for the node def fun(self, model, prompt,behaviour, seed): install_openai() # Install the OpenAI module if not already installed import openai # Import the OpenAI module # Get the API key from the file api_key = get_api_key() openai.api_key = api_key # Set the API key for the OpenAI module initMessage = ""; if(behaviour == "description"): initMessage = get_init_message(False); else: initMessage = get_init_message(True); # Create a chat completion using the OpenAI module try: completion = openai.ChatCompletion.create( model=model, messages=[ {"role": "user", "content":initMessage}, {"role": "user", "content": prompt} ] ) except: # sometimes it fails first time to connect to server completion = openai.ChatCompletion.create( model=model, messages=[ {"role": "user", "content": initMessage}, {"role": "user", "content": prompt} ] ) # Get the answer from the chat completion answer = completion["choices"][0]["message"]["content"] return (answer,) # Return the answer as a string class O_ChatGPT_medium_O: """ this node is based on the openAI GPT-3 API to generate propmpts using the AI """ # Define the input types for the node @classmethod def INPUT_TYPES(cls): return { "required": { # Multiline string input for the prompt "prompt": ("STRING", {"multiline": True}), "initMsg": ("STRING", {"multiline": True, "default": get_init_message()}), "model": (get_gpt_models(), {"default": "gpt-3.5-turbo"}), }, "optional": { "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), } } RETURN_TYPES = ("STRING",) # Define the return type of the node FUNCTION = "fun" # Define the function name for the node CATEGORY = "O/OpenAI" # Define the category for the node def fun(self, model, prompt, initMsg, seed): install_openai() # Install the OpenAI module if not already installed import openai # Import the OpenAI module # Get the API key from the file api_key = get_api_key() openai.api_key = api_key # Set the API key for the OpenAI module # Create a chat completion using the OpenAI module try: completion = openai.ChatCompletion.create( model=model, messages=[ {"role": "user", "content": initMsg}, {"role": "user", "content": prompt} ] ) except: # sometimes it fails first time to connect to server completion = openai.ChatCompletion.create( model=model, messages=[ {"role": "user", "content": initMsg}, {"role": "user", "content": prompt} ] ) # Get the answer from the chat completion answer = completion["choices"][0]["message"]["content"] return (answer,) # Return the answer as a string # region advanced class load_openAI_O: """ this node will load openAI model """ # Define the input types for the node @classmethod def INPUT_TYPES(cls): return { "required": { } } RETURN_TYPES = ("OPENAI",) # Define the return type of the node FUNCTION = "fun" # Define the function name for the node CATEGORY = "O/OpenAI/Advanced" # Define the category for the node def fun(self): install_openai() # Install the OpenAI module if not already installed import openai # Import the OpenAI module # Get the API key from the file api_key = get_api_key() openai.api_key = api_key # Set the API key for the OpenAI module return ( { "openai": openai, # Return openAI model }, ) # region ChatGPT class openAi_chat_message_O: """ create chat message for openAI chatGPT """ # Define the input types for the node @classmethod def INPUT_TYPES(cls): return { "required": { "role": (["user", "assistant", "system"], {"default": "user"}), "content": ("STRING", {"multiline": True, "default":get_init_message()}), } } # Define the return type of the node RETURN_TYPES = ("OPENAI_CHAT_MESSAGES",) FUNCTION = "fun" # Define the function name for the node # Define the category for the node CATEGORY = "O/OpenAI/Advanced/ChatGPT" def fun(self, role, content): return ( { "messages": [{"role": role, "content": content, }] }, ) class openAi_chat_messages_Combine_O: """ compine chat messages into 1 tuple """ # Define the input types for the node @classmethod def INPUT_TYPES(cls): return { "required": { "message1": ("OPENAI_CHAT_MESSAGES", ), "message2": ("OPENAI_CHAT_MESSAGES", ), } } # Define the return type of the node RETURN_TYPES = ("OPENAI_CHAT_MESSAGES",) FUNCTION = "fun" # Define the function name for the node # Define the category for the node CATEGORY = "O/OpenAI/Advanced/ChatGPT" def fun(self, message1, message2): messages = message1["messages"] + \ message2["messages"] # compine messages return ( { "messages": messages }, ) class openAi_chat_completion_O: """ create chat completion for openAI chatGPT """ # Define the input types for the node @classmethod def INPUT_TYPES(cls): return { "required": { "openai": ("OPENAI", ), # "model": ("STRING", {"multiline": False, "default": "gpt-3.5-turbo"}), "model": (get_gpt_models(), {"default": "gpt-3.5-turbo"}), "messages": ("OPENAI_CHAT_MESSAGES", ), }, "optional": { "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), } } # Define the return type of the node RETURN_TYPES = ("STRING", "OPENAI_CHAT_COMPLETION",) FUNCTION = "fun" # Define the function name for the node OUTPUT_NODE = True # Define the category for the node CATEGORY = "O/OpenAI/Advanced/ChatGPT" def fun(self, openai, model, messages, seed): # Create a chat completion using the OpenAI module openai = openai["openai"] try: completion = openai.ChatCompletion.create( model=model, messages=messages["messages"] ) except: # sometimes it fails first time to connect to server completion = openai.ChatCompletion.create( model=model, messages=messages["messages"] ) # Get the answer from the chat completion content = completion["choices"][0]["message"]["content"] return ( content, # Return the answer as a string completion, # Return the chat completion ) class DebugOpenAIChatMEssages_O: """ Debug OpenAI Chat Messages """ # Define the input types for the node @classmethod def INPUT_TYPES(cls): return { "required": { "messages": ("OPENAI_CHAT_MESSAGES", ), } } # Define the return type of the node RETURN_TYPES = () FUNCTION = "fun" # Define the function name for the node OUTPUT_NODE = True # Define the category for the node CATEGORY = "O/debug/OpenAI/Advanced/ChatGPT" def fun(self, messages): print(f'{PACKAGE_NAME}:OpenAIChatMEssages', messages["messages"]) return () class DebugOpenAIChatCompletion_O: """ Debug OpenAI Chat Completion """ # Define the input types for the node @classmethod def INPUT_TYPES(cls): return { "required": { "completion": ("OPENAI_CHAT_COMPLETION", ), } } # Define the return type of the node RETURN_TYPES = () FUNCTION = "fun" # Define the function name for the node OUTPUT_NODE = True # Define the category for the node CATEGORY = "O/debug/OpenAI/Advanced/ChatGPT" def fun(self, completion): print(f'{PACKAGE_NAME}:OpenAIChatCompletion:', completion) return () # endregion ChatGPT # region Image class openAi_Image_create_O: """ create image using openai """ # Define the input types for the node @classmethod def INPUT_TYPES(cls): return { "required": { "openai": ("OPENAI", ), "prompt": ("STRING", {"multiline": True}), "number": ("INT", {"default": 1, "min": 1, "max": 10, "step": 1}), "size": (["256x256", "512x512", "1024x1024"], {"default": "256x256"}), }, "optional": { "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), } } # Define the return type of the node RETURN_TYPES = ("IMAGE", "MASK") FUNCTION = "fun" # Define the function name for the node OUTPUT_NODE = True # Define the category for the node CATEGORY = "O/OpenAI/Advanced/Image" def fun(self, openai, prompt, number, size, seed): # Create a chat completion using the OpenAI module openai = openai["openai"] prompt = prompt number = 1 imageURL = "" try: imagesURLS = openai.Image.create( prompt=prompt, n=number, size=size ) imageURL = imagesURLS["data"][0]["url"] except Exception as e: print(f'{PACKAGE_NAME}:openAi_Image_create_O:', e) imageURL = "https://i.imgur.com/removed.png" image = requests.get(imageURL).content i = Image.open(io.BytesIO(image)) image = i.convert("RGBA") image = np.array(image).astype(np.float32) / 255.0 # image_np = np.transpose(image_np, (2, 0, 1)) image = torch.from_numpy(image)[None,] if 'A' in i.getbands(): mask = np.array(i.getchannel('A')).astype(np.float32) / 255.0 mask = 1. - torch.from_numpy(mask) else: mask = torch.zeros((64, 64), dtype=torch.float32, device="cpu") return (image, mask) class openAi_Image_Edit_O: """ edit an image using openai """ # Define the input types for the node @classmethod def INPUT_TYPES(cls): return { "required": { "openai": ("OPENAI", ), "image": ("IMAGE",), "prompt": ("STRING", {"multiline": True}), "number": ("INT", {"default": 1, "min": 1, "max": 10, "step": 1}), "size": (["256x256", "512x512", "1024x1024"], {"default": "256x256"}), }, "optional": { "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), } } # Define the return type of the node RETURN_TYPES = ("IMAGE", "MASK") FUNCTION = "fun" # Define the function name for the node OUTPUT_NODE = True # Define the category for the node CATEGORY = "O/OpenAI/Advanced/Image" def fun(self, openai, image, prompt, number, size, seed): # Create a chat completion using the OpenAI module openai = openai["openai"] prompt = prompt number = 1 # Convert PyTorch tensor to NumPy array image = image[0] i = 255. * image.cpu().numpy() img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8)) # Save the image to a BytesIO object as a PNG file with io.BytesIO() as output: img.save(output, format='PNG') binary_image = output.getvalue() # Create a circular mask with alpha 0 in the middle mask = np.zeros((image.shape[0], image.shape[1], 4), dtype=np.uint8) center = (image.shape[1] // 2, image.shape[0] // 2) radius = min(center[0], center[1]) draw = ImageDraw.Draw(Image.fromarray(mask, mode='RGBA')) draw.ellipse((center[0]-radius, center[1]-radius, center[0]+radius, center[1]+radius), fill=(0, 0, 0, 255), outline=(0, 0, 0, 0)) del draw # Save the mask to a BytesIO object as a PNG file with io.BytesIO() as output: Image.fromarray(mask, mode='RGBA').save(output, format='PNG') binary_mask = output.getvalue() imageURL = "" try: imagesURLS = openai.Image.create_edit( image=binary_image, mask=binary_mask, prompt=prompt, n=number, size=size ) imageURL = imagesURLS["data"][0]["url"] except Exception as e: print(f'{PACKAGE_NAME}:openAi_Image_create_O:', e) imageURL = "https://i.imgur.com/removed.png" image = requests.get(imageURL).content i = Image.open(io.BytesIO(image)) image = i.convert("RGBA") image = np.array(image).astype(np.float32) / 255.0 # image_np = np.transpose(image_np, (2, 0, 1)) image = torch.from_numpy(image)[None,] if 'A' in i.getbands(): mask = np.array(i.getchannel('A')).astype(np.float32) / 255.0 mask = 1. - torch.from_numpy(mask) else: mask = torch.zeros( (1, image.shape[2], image.shape[3]), dtype=torch.float32, device="cpu") return (image, mask) class openAi_Image_variation_O: """ edit an image using openai """ # Define the input types for the node @classmethod def INPUT_TYPES(cls): return { "required": { "openai": ("OPENAI", ), "image": ("IMAGE",), "number": ("INT", {"default": 1, "min": 1, "max": 10, "step": 1}), "size": (["256x256", "512x512", "1024x1024"], {"default": "256x256"}), }, "optional": { "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), } } # Define the return type of the node RETURN_TYPES = ("IMAGE", "MASK") FUNCTION = "fun" # Define the function name for the node OUTPUT_NODE = True # Define the category for the node CATEGORY = "O/OpenAI/Advanced/Image" def fun(self, openai, image, number, size, seed): # Create a chat completion using the OpenAI module openai = openai["openai"] number = 1 # Convert PyTorch tensor to NumPy array image = image[0] i = 255. * image.cpu().numpy() img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8)) # Save the image to a BytesIO object as a PNG file with io.BytesIO() as output: img.save(output, format='PNG') binary_image = output.getvalue() imageURL = " " try: imagesURLS = openai.Image.create_variation( image=binary_image, n=number, size=size ) imageURL = imagesURLS["data"][0]["url"] except Exception as e: print(f'{PACKAGE_NAME}:openAi_Image_create_O:', e) imageURL = "https://i.imgur.com/removed.png" image = requests.get(imageURL).content i = Image.open(io.BytesIO(image)) image = i.convert("RGBA") image = np.array(image).astype(np.float32) / 255.0 # image_np = np.transpose(image_np, (2, 0, 1)) image = torch.from_numpy(image)[None,] if 'A' in i.getbands(): mask = np.array(i.getchannel('A')).astype(np.float32) / 255.0 mask = 1. - torch.from_numpy(mask) else: mask = torch.zeros( (1, image.shape[2], image.shape[3]), dtype=torch.float32, device="cpu") return (image, mask) # endregion Image # endregion advanced # endregion openAI # region latentTools class LatentUpscaleFactor_O: """ Upscale the latent code by multiplying the width and height by a factor """ upscale_methods = ["nearest-exact", "bilinear", "area"] crop_methods = ["disabled", "center"] @classmethod def INPUT_TYPES(cls): return { "required": { "samples": ("LATENT",), "upscale_method": (cls.upscale_methods,), "WidthFactor": ("FLOAT", {"default": 1.25, "min": 0.0, "max": 10.0, "step": 0.28125}), "HeightFactor": ("FLOAT", {"default": 1.25, "min": 0.0, "max": 10.0, "step": 0.28125}), "crop": (cls.crop_methods,), } } RETURN_TYPES = ("LATENT",) FUNCTION = "upscale" CATEGORY = "O/latent" def upscale(self, samples, upscale_method, WidthFactor, HeightFactor, crop): s = samples.copy() x = samples["samples"].shape[3] y = samples["samples"].shape[2] new_x = int(x * WidthFactor) new_y = int(y * HeightFactor) if (new_x > MAX_RESOLUTION): new_x = MAX_RESOLUTION if (new_y > MAX_RESOLUTION): new_y = MAX_RESOLUTION print(f'{PACKAGE_NAME}:upscale from ({x*8},{y*8}) to ({new_x*8},{new_y*8})') s["samples"] = comfy.utils.common_upscale( samples["samples"], enforce_mul_of_64( new_x), enforce_mul_of_64(new_y), upscale_method, crop ) return (s,) class LatentUpscaleFactorSimple_O: """ Upscale the latent code by multiplying the width and height by a factor """ upscale_methods = ["nearest-exact", "bilinear", "area"] crop_methods = ["disabled", "center"] @classmethod def INPUT_TYPES(cls): return { "required": { "samples": ("LATENT",), "upscale_method": (cls.upscale_methods,), "factor": ("FLOAT", {"default": 1.25, "min": 0.0, "max": 10.0, "step": 0.28125}), "crop": (cls.crop_methods,), } } RETURN_TYPES = ("LATENT",) FUNCTION = "upscale" CATEGORY = "O/latent" def upscale(self, samples, upscale_method, factor, crop): s = samples.copy() x = samples["samples"].shape[3] y = samples["samples"].shape[2] new_x = int(x * factor) new_y = int(y * factor) if (new_x > MAX_RESOLUTION): new_x = MAX_RESOLUTION if (new_y > MAX_RESOLUTION): new_y = MAX_RESOLUTION print(f'{PACKAGE_NAME}:upscale from ({x*8},{y*8}) to ({new_x*8},{new_y*8})') s["samples"] = comfy.utils.common_upscale( samples["samples"], enforce_mul_of_64( new_x), enforce_mul_of_64(new_y), upscale_method, crop ) return (s,) class SelectLatentImage_O: """ Select a single image from a batch of generated latent images. """ @classmethod def INPUT_TYPES(cls): return { "required": { "samples": ("LATENT",), "index": ("INT", {"default": 0, "min": 0}), } } RETURN_TYPES = ("LATENT",) FUNCTION = "fun" CATEGORY = "O/latent" def fun(self, samples, index): # Get the batch size and number of channels batch_size, num_channels, height, width = samples["samples"].shape # Ensure that the index is within bounds if index >= batch_size: index = batch_size - 1 # Select the specified image selected_image = samples["samples"][index].unsqueeze(0) # Return the selected image return ({"samples": selected_image},) class VAEDecodeParallel_O: def __init__(self, device="cpu"): self.device = device self.device_count = torch.cuda.device_count() if device != "cpu" else 1 self.module = VAEDecodeOriginal(device) self.net = nn.DataParallel(self.module) @classmethod def INPUT_TYPES(cls): return {"required": {"samples": ("LATENT", ), "vae": ("VAE", )}} RETURN_TYPES = ("IMAGE",) FUNCTION = "decode_parallel" CATEGORY = "latent" def decode_parallel(self, vae, samples): batch_size = samples["samples"].shape[0] images = torch.zeros((batch_size, 3, 256, 256)).to(self.device) for i in range(0, batch_size, self.device_count): batch_samples = samples["samples"][i:i + self.device_count].to(self.device) batch_images = self.net(vae, {"samples": batch_samples})[ 0].to(self.device) images[i:i+self.device_count] = batch_images return (images,) class VAEDecodeOriginal: def __init__(self, device="cpu"): self.device = device @classmethod def INPUT_TYPES(s): return {"required": {"samples": ("LATENT", ), "vae": ("VAE", )}} RETURN_TYPES = ("IMAGE",) FUNCTION = "decode" CATEGORY = "latent" def decode(self, vae, samples): return (vae.decode(samples["samples"]), ) # endregion latentTools # region TextTools class seed2String_O: """ This node convert seeds to string // can be used to force the system to read a string again if it got compined with it """ @classmethod def INPUT_TYPES(cls): return {"required": {"seed": ("SEED")}} RETURN_TYPES = ("STRING") FUNCTION = "fun" CATEGORY = "O/utils" def fun(self, seed): return (str(seed)) class saveTextToFile_O: def __init__(self): pass @classmethod def INPUT_TYPES(cls): return { "required": { "text": ("STRING", {"default": '', "multiline": False, "defaultBehavior": "input"}), "filename": ("STRING", {"default": "log.txt", "multiline": False}), }, "optional": { "append": (["true", "false"], {"default": True}) } } OUTPUT_NODE = True RETURN_TYPES = () FUNCTION = "fun" CATEGORY = "O/text" def fun(self, text, filename, append): # append dateTime current_time = time.strftime("%d/%m/%Y %H:%M:%S") # dd/mm/YY H:M:S textToSave = f'{current_time}: \n' # append text in new line textToSave += f' {text} \n\n' self.saveTextToFile(textToSave, filename, append) return (textToSave, ) def saveTextToFile(self, text, filename, append): saveDir = os.path.join(SUIT_DIR, "output") saveFile = os.path.join(saveDir, filename) # Create directory if it does not exist if not os.path.exists(saveDir): os.makedirs(saveDir) # Write to file mode = "a" if append else "w" try: with open(saveFile, mode, encoding="utf-8") as f: f.write(text) except OSError as e: print(f'{PACKAGE_NAME}:error writing to file {saveFile}') fonts = None def loadFonts(): global fonts if (fonts != None): return fonts try: fonts_filepath = os.path.join(SUIT_DIR, "fonts") fonts = [] for file in os.listdir(fonts_filepath): if file.endswith(".ttf") or file.endswith(".otf") or file.endswith(".ttc") or file.endswith(".TTF") or file.endswith(".OTF") or file.endswith(".TTC"): fonts.append(file) except: fonts = [] if (len(fonts) == 0): print(f'{PACKAGE_NAME}:no fonts found in {fonts_filepath}') fonts = ["Arial.ttf"] return fonts class Text2Image_O: """ This node will convert a string to an image """ def __init__(self): self.font_filepath = os.path.join(SUIT_DIR, "fonts") @classmethod def INPUT_TYPES(s): return { "required": { "text": ("STRING", {"multiline": True}), "font": (loadFonts(), {"default": loadFonts()[0], }), "size": ("INT", {"default": 36, "min": 0, "max": 255, "step": 1}), "font_R": ("INT", {"default": 0, "min": 0, "max": 255, "step": 1}), "font_G": ("INT", {"default": 0, "min": 0, "max": 255, "step": 1}), "font_B": ("INT", {"default": 0, "min": 0, "max": 255, "step": 1}), "font_A": ("INT", {"default": 255, "min": 0, "max": 255, "step": 1}), "background_R": ("INT", {"default": 255, "min": 0, "max": 255, "step": 1}), "background_G": ("INT", {"default": 255, "min": 0, "max": 255, "step": 1}), "background_B": ("INT", {"default": 255, "min": 0, "max": 255, "step": 1}), "background_A": ("INT", {"default": 255, "min": 0, "max": 255, "step": 1}), "width": ("INT", {"default": 128, "min": 0, "step": 1}), "height": ("INT", {"default": 128, "min": 0, "step": 1}), "expand": (["true", "false"], {"default": "true"}), "x": ("INT", {"default": 0, "min": -100, "step": 1}), "y": ("INT", {"default": 0, "min": -100, "step": 1}), } } RETURN_TYPES = ("IMAGE",) FUNCTION = "create_image_new" OUTPUT_NODE = False CATEGORY = "O/text" def create_image_new(self, text, font, size, font_R, font_G, font_B, font_A, background_R, background_G, background_B, background_A, width, height, expand, x, y): font_color = (font_R, font_G, font_B, font_A) background_color = (background_R, background_G, background_B, background_A) font_path = os.path.join(self.font_filepath, font) font = ImageFont.truetype(font_path, size) # Initialize the drawing context image = Image.new('RGBA', (1, 1), color=background_color) draw = ImageDraw.Draw(image) # Get the size of the text text_width, text_height = draw.textsize(text, font=font) # Set the dimensions of the image if expand == "true": if width < text_width: width = text_width if height < text_height: height = text_height width = enforce_mul_of_64(width) height = enforce_mul_of_64(height) # Create a new image image = Image.new('RGBA', (width, height), color=background_color) # Initialize the drawing context draw = ImageDraw.Draw(image) # Calculate the position of the text text_x = x - text_width/2 if (text_x < 0): text_x = 0 if (text_x > width-text_width): text_x = width - text_width text_y = y - text_height/2 if (text_y < 0): text_y = 0 if (text_y > height-text_height): text_y = height - text_height # Draw the text on the image draw.text((text_x, text_y), text, fill=font_color, font=font) # Convert the PIL Image to a tensor image_np = np.array(image).astype(np.float32) / 255.0 image_tensor = torch.from_numpy(image_np).unsqueeze(0) return image_tensor, {"ui": {"images": image_tensor}} # region text/NSP nspterminology = None # Cache the NSP terminology def laodNSP(): global nspterminology if (nspterminology != None): return nspterminology # Fetch the NSP Pantry local_pantry = os.path.join(SUIT_DIR, "nsp_pantry.json") if not os.path.exists(local_pantry): print(f'{PACKAGE_NAME}:downloading NSP') response = urlopen( 'https://raw.githubusercontent.com/WASasquatch/noodle-soup-prompts/main/nsp_pantry.json') tmp_pantry = json.loads(response.read()) # Dump JSON locally pantry_serialized = json.dumps(tmp_pantry, indent=4) with open(local_pantry, "w") as f: f.write(pantry_serialized) del response, tmp_pantry # Load local pantry with open(local_pantry, 'r') as f: nspterminology = json.load(f) print(f'{PACKAGE_NAME}:NSP ready') return nspterminology class RandomNSP_O: @classmethod def laodCategories(s): nspterminology = laodNSP() terminologies = [] for term in nspterminology: terminologies.append(term) return (terminologies) @classmethod def INPUT_TYPES(s): return {"required": { "terminology": (s.laodCategories(),), "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), }} RETURN_TYPES = ("STRING",) FUNCTION = "fun" CATEGORY = "O/text/NSP" def fun(self, terminology, seed): nspterminology = laodNSP() # Set the seed random.seed(seed) result = random.choice(nspterminology[terminology]) return (result, {"ui": {"STRING": result}}) class ConcatRandomNSP_O: @classmethod def laodCategories(s): nspterminology = laodNSP() terminologies = [] for term in nspterminology: terminologies.append(term) return (terminologies) @classmethod def INPUT_TYPES(s): return {"required": { "text": ("STRING", {"multiline": False, "defaultBehavior": "input"}), "terminology": (s.laodCategories(),), "separator": ("STRING", {"multiline": False, "default": ","}), "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), }} RETURN_TYPES = ("STRING",) FUNCTION = "fun" CATEGORY = "O/text/NSP" def fun(self, text, terminology, separator, seed): nspterminology = laodNSP() # Set the seed random.seed(seed) result = random.choice(nspterminology[terminology]) return (text+separator+result+separator, {"ui": {"STRING": result}}) # endregion text/NSP # region debug text class DebugText_O: """ This node will write a text to the console """ @classmethod def INPUT_TYPES(cls): return {"required": { "text": ("STRING", {"multiline": False, "defaultBehavior": "input"}), "prefix": ("STRING", {"default": "debug", "multiline": False}), }} RETURN_TYPES = () FUNCTION = "debug_string" OUTPUT_NODE = True CATEGORY = "O/debug/text" @staticmethod def debug_string(text, prefix): print(f'{PACKAGE_NAME}:{prefix}:{text}') return () class DebugTextRoute_O: """ This node will write a text to the console """ @classmethod def INPUT_TYPES(cls): return {"required": { "text": ("STRING", {"multiline": False, "defaultBehavior": "input"}), "prefix": ("STRING", {"default": "debug", "multiline": False}), }} RETURN_TYPES = ("STRING",) FUNCTION = "debug_string" CATEGORY = "O/debug/text" @staticmethod def debug_string(text, prefix): print(f'{PACKAGE_NAME}:{prefix}:{text}') return (text,) # endregion # region text/operations class concat_text_O: """ This node will concatenate two strings together """ @ classmethod def INPUT_TYPES(cls): return {"required": { "text1": ("STRING", {"multiline": True, "defaultBehavior": "input"}), "text2": ("STRING", {"multiline": True, "defaultBehavior": "input"}), "separator": ("STRING", {"multiline": False, "default": ","}), }} RETURN_TYPES = ("STRING",) FUNCTION = "fun" CATEGORY = "O/text/operations" @ staticmethod def fun(text1, separator, text2): return (text1 + separator + text2,) class trim_text_O: """ This node will trim a string from the left and right """ @ classmethod def INPUT_TYPES(cls): return {"required": { "text": ("STRING", {"multiline": False, "defaultBehavior": "input"}), }} RETURN_TYPES = ("STRING",) FUNCTION = "fun" CATEGORY = "O/text/operations" def fun(self, text): return (text.strip(),) class replace_text_O: """ This node will replace a string with another string """ @ classmethod def INPUT_TYPES(cls): return {"required": { "text": ("STRING", {"multiline": True, "defaultBehavior": "input"}), "old": ("STRING", {"multiline": False}), "new": ("STRING", {"multiline": False}) }} RETURN_TYPES = ("STRING",) FUNCTION = "fun" CATEGORY = "O/text/operations" @ staticmethod def fun(text, old, new): return (text.replace(old, new),) # replace a text with another text # endregion # endregion TextTools # region Image def upscaleImage(image, upscale_method, WidthFactor, HeightFactor, crop, MulOf46): samples = image.movedim(-1, 1) height = HeightFactor * samples.shape[2] width = WidthFactor * samples.shape[3] if (width > MAX_RESOLUTION): width = MAX_RESOLUTION if (height > MAX_RESOLUTION): height = MAX_RESOLUTION if (MulOf46 == "enabled"): width = enforce_mul_of_64(width) height = enforce_mul_of_64(height) width = int(width) height = int(height) print( f'{PACKAGE_NAME}:upscale from ({samples.shape[2]},{samples.shape[3]}) to ({width},{height})') s = comfy.utils.common_upscale( samples, width, height, upscale_method, crop) s = s.movedim(1, -1) return (s,) class ImageScaleFactorSimple_O: upscale_methods = ["nearest-exact", "bilinear", "area"] crop_methods = ["disabled", "center"] toggle = ["enabled", "disabled"] @classmethod def INPUT_TYPES(s): return {"required": {"image": ("IMAGE",), "upscale_method": (s.upscale_methods,), "Factor": ("FLOAT", {"default": 1.25, "min": 0.0, "max": 10.0, "step": 0.28125}), "MulOf46": (s.toggle, {"default": "enabled"}), "crop": (s.crop_methods,) }} RETURN_TYPES = ("IMAGE",) FUNCTION = "upscale" CATEGORY = "O/image" def upscale(self, image, upscale_method, Factor, crop, MulOf46): return upscaleImage(image, upscale_method, Factor, Factor, crop, MulOf46) class ImageScaleFactor_O: upscale_methods = ["nearest-exact", "bilinear", "area"] crop_methods = ["disabled", "center"] toggle = ["enabled", "disabled"] @classmethod def INPUT_TYPES(s): return {"required": {"image": ("IMAGE",), "upscale_method": (s.upscale_methods,), "WidthFactor": ("FLOAT", {"default": 1.25, "min": 0.0, "max": 10.0, "step": 0.28125}), "HeightFactor": ("FLOAT", {"default": 1.25, "min": 0.0, "max": 10.0, "step": 0.28125}), "MulOf46": (s.toggle, {"default": "enabled"}), "crop": (s.crop_methods,) }} RETURN_TYPES = ("IMAGE",) FUNCTION = "upscale" CATEGORY = "O/image" def upscale(self, image, upscale_method, WidthFactor, HeightFactor, crop, MulOf46): return upscaleImage(image, upscale_method, WidthFactor, HeightFactor, crop, MulOf46) # endregion # region numbers def solveEquation(equation): answer = 0.0 # Check if v is a valid equation or a number using regular expressions try: # Solve the equation using Python's built-in eval function answer = eval(equation) except Exception as e: print(f'{PACKAGE_NAME}: equation is not valid: {equation} error: {e}') answer = "NAN" return answer class applyEquation1param_O: """ This node generate seeds for the model """ @classmethod def INPUT_TYPES(cls): return {"required": { "x": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 0xffffffffffffffff, "defaultBehavior": "input"}), "equation": ("STRING", {"multiline": True, "default": "x*1"}), } } RETURN_TYPES = ("FLOAT", "int",) FUNCTION = "fun" CATEGORY = "O/numbers" def fun(self, x, equation): equation = equation.replace("x", "("+str(x)+")") answer = solveEquation(equation) return (answer, int(answer), ) class applyEquation2params_O: """ This node generate seeds for the model """ @classmethod def INPUT_TYPES(cls): return {"required": { "x": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 0xffffffffffffffff, "defaultBehavior": "input"}), "y": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 0xffffffffffffffff, "defaultBehavior": "input"}), "equation": ("STRING", {"multiline": True, "default": "x+y"}), }, "optional": { "equation_2": ("STRING", {"multiline": True, "default": "x+y"}), } } RETURN_TYPES = ("FLOAT", "INT", "FLOAT", "INT") FUNCTION = "fun" CATEGORY = "O/numbers" def fun(self, x, y, equation, equation_2): answer = 0.0 answer_2 = 0.0 if (equation != ""): equation = equation.replace("x", "("+str(x)+")") equation = equation.replace("y", "("+str(y)+")") answer = solveEquation(equation) if (equation_2 != ""): equation_2 = equation_2.replace("x", "("+str(x)+")") equation_2 = equation_2.replace("y", "("+str(y)+")") answer_2 = solveEquation(equation_2) return (answer, int(answer), answer_2, int(answer_2),) class floatToInt_O: """ This node convert float to int """ @classmethod def INPUT_TYPES(cls): return {"required": { "float": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 0xffffffffffffffff, "defaultBehavior": "input"}), } } RETURN_TYPES = ("INT",) FUNCTION = "fun" CATEGORY = "O/numbers" def fun(self, float): return (int(float),) class intToFloat_O: """ This node convert int to float """ @classmethod def INPUT_TYPES(cls): return {"required": { "int": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "defaultBehavior": "input"}), } } RETURN_TYPES = ("FLOAT",) FUNCTION = "fun" CATEGORY = "O/numbers" def fun(self, int): return (float(int),) class floatToText_O: """ This node convert float to text """ @classmethod def INPUT_TYPES(cls): return {"required": { "float": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 0xffffffffffffffff, "defaultBehavior": "input"}), } } RETURN_TYPES = ("STRING",) FUNCTION = "fun" CATEGORY = "O/numbers" def fun(self, float): return (str(float),) class GetImageWidthAndHeight_O: upscale_methods = ["nearest-exact", "bilinear", "area"] crop_methods = ["disabled", "center"] toggle = ["enabled", "disabled"] @classmethod def INPUT_TYPES(s): return {"required": {"image": ("IMAGE",), } } RETURN_TYPES = ("INT", "INT") FUNCTION = "fun" CATEGORY = "O/numbers" def fun(self, image): samples = image.movedim(-1, 1) height = samples.shape[2] width = samples.shape[3] return (int(width), int(height),) class GetLatentWidthAndHeight_O: """ Upscale the latent code by multiplying the width and height by a factor """ upscale_methods = ["nearest-exact", "bilinear", "area"] crop_methods = ["disabled", "center"] @classmethod def INPUT_TYPES(cls): return { "required": { "samples": ("LATENT",), } } RETURN_TYPES = ("INT", "INT",) FUNCTION = "fun" CATEGORY = "O/numbers" def fun(self, samples): w = samples["samples"].shape[3] h = samples["samples"].shape[2] return (int(w), int(h),) # endregion # region Utils class Text_O: """ to provide text to the model """ @classmethod def INPUT_TYPES(cls): return { "required": { "text": ("STRING", {"multiline": True}), } } RETURN_TYPES = ("STRING",) FUNCTION = "fun" CATEGORY = "O/utils" def fun(self, text): return (text+" ",) class seed_O: """ This node generate seeds for the model """ @classmethod def INPUT_TYPES(cls): return {"required": {"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), }} RETURN_TYPES = ("INT",) FUNCTION = "fun" CATEGORY = "O/utils" def fun(self, seed): return (seed,) class int_O: """ This node generate seeds for the model """ @classmethod def INPUT_TYPES(cls): return {"required": {"int": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), }} RETURN_TYPES = ("INT",) FUNCTION = "fun" CATEGORY = "O/utils" def fun(self, int): return (int,) class float_O: """ This node generate seeds for the model """ @classmethod def INPUT_TYPES(cls): return {"required": {"float": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 0xffffffffffffffff}), }} RETURN_TYPES = ("FLOAT",) FUNCTION = "fun" CATEGORY = "O/utils" def fun(self, float): return (float,) class Note_O: @classmethod def INPUT_TYPES(s): return {"required": {"text": ("STRING", {"multiline": True})}} RETURN_TYPES = () FUNCTION = "fun" OUTPUT_NODE = True CATEGORY = "O/utils" def fun(self, text): return () # endregion # Define the node class mappings NODE_CLASS_MAPPINGS = { # openAITools------------------------------------------ "ChatGPT Simple _O": O_ChatGPT_O, "ChatGPT compact _O": O_ChatGPT_medium_O, # openAiTools > Advanced "load_openAI _O": load_openAI_O, # openAiTools > Advanced > ChatGPT "Chat_Message _O": openAi_chat_message_O, "combine_chat_messages _O": openAi_chat_messages_Combine_O, "Chat completion _O": openAi_chat_completion_O, # openAiTools > Advanced > image "create image _O": openAi_Image_create_O, # "Edit_image _O": openAi_Image_Edit, # coming soon "variation_image _O": openAi_Image_variation_O, # latentTools------------------------------------------ "LatentUpscaleFactor _O": LatentUpscaleFactor_O, "LatentUpscaleFactorSimple _O": LatentUpscaleFactorSimple_O, "selectLatentFromBatch _O": SelectLatentImage_O, # "VAEDecodeParallel _O": VAEDecodeParallel_O, # coming soon # StringTools------------------------------------------ "RandomNSP _O": RandomNSP_O, "ConcatRandomNSP_O": ConcatRandomNSP_O, "Concat Text _O": concat_text_O, "Trim Text _O": trim_text_O, "Replace Text _O": replace_text_O, "saveTextToFile _O": saveTextToFile_O, "Text2Image _O": Text2Image_O, # ImageTools------------------------------------------ "ImageScaleFactor _O": ImageScaleFactor_O, "ImageScaleFactorSimple _O": ImageScaleFactorSimple_O, # NumberTools------------------------------------------ "Equation1param _O": applyEquation1param_O, "Equation2params _O": applyEquation2params_O, "floatToInt _O": floatToInt_O, "intToFloat _O": intToFloat_O, "floatToText _O": floatToText_O, "GetImage_(Width&Height) _O": GetImageWidthAndHeight_O, "GetLatent_(Width&Height) _O": GetLatentWidthAndHeight_O, # debug------------------------------------------ "debug messages_O": DebugOpenAIChatMEssages_O, "debug Completeion _O": DebugOpenAIChatCompletion_O, "Debug Text _O": DebugText_O, "Debug Text route _O": DebugTextRoute_O, # Utils------------------------------------------ "Note _O": Note_O, "Text _O": Text_O, "seed _O": seed_O, "int _O": int_O, "float _O": float_O, }