From b7961b8be7f44ce5f15c28a206f61a1dfd8c3952 Mon Sep 17 00:00:00 2001 From: tusharbhutt Date: Wed, 18 Oct 2023 12:23:12 -0600 Subject: [PATCH] Add files via upload --- README.md | 59 +++++++- __init__.py | 12 +- changlelog.md | 4 + endless_nodes.py | 358 +++++++++++++++++++++++++++++------------------ requirements.txt | 5 +- 5 files changed, 295 insertions(+), 143 deletions(-) diff --git a/README.md b/README.md index e8fba09..3eadeef 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Some basic custom nodes for the ComfyUI user interface for Stable Diffusion. Features: + An image saver for images and JSON files to base folder, custom folders for one, or custom folders for both. Also allows for Python timestamping -+ (REMOVED FOR NOW) Two aesthetic scoring models, one based on the same as AUTO1111, the other based on Image Reward ++ Two aesthetic scoring models, one based on the same as AUTO1111, the other based on Image Reward + Converters for various numerical functions to the other (int, float, number) and to strings. Still in beta + Switches for text and numbers + Parameter collection nodes @@ -12,6 +12,12 @@ When using the [ComfyUI](https://github.com/comfyanonymous/ComfyUI) interface fo Rightly or wrongly, I am pretending to teach myself a bit of Python to get some nodes up and running to do what I'd like. Yes, I am using ChatGPT, and yes I am a glutton for punishment. There are no promises that these nodes will work for you or that I will maintain them. Feel free to do with them as you wish, according to the license model. +**UPDATE: Oct 14, 2023** + ++ ***REINSTATED the Aesthetic Scorers and provided instructions on how to add Clip to your system if you are having issues*** ++ Added the Global Envoy, which allows you to add height, width, and steps to many modules. The steps include start, stop and when to cut over to refinement, if you want (either as discrete steps, or a percentage) + + **UPDATE: Oct 8, 2023** + ***Added a standalone saver node. Fixed bug in this node and main node where image files were overwritten*** @@ -66,6 +72,41 @@ You can also get the nodes via the [ComfyUI Manager](https://github.com/ltdrdata **NOTE: Requires CLIP and Pytorch-Lightning for the Aesthetic Scorer and ImageReward for my take on the Image Reward node scorer. Also require colorama for error messages to console. I've added them in the requirement file but if it doesn't work, you will need to download manually** +## IF YOU HAVE ISSUES WITH CLIP.... + +Some people are reporting having issues with installing Clip from OpenAI, especially if using ComfyUI Manager. I can tell you in looking at the issues on OpenAI's GitHub, it's not an uncommon complaint. So, I started from scratch on a new PC that had no software on it other than Windows and tested to see if I could get CLIP installed. After some minor issues, I was able to get the Python module loaded and working. If I can do this, I'm sure you can as well, but it will take a bit of effort and you need to have some basic skill in installing software in Windows. For users of other platforms or if you installed the other Windows version, I'm assuming you have Python and GIT installed already, so the first few instructions will not apply, but the remainder could. + +This is what I did: + +**TL;DR: Install GIT and Python, ensure the location of Python and Pip are in your PATH, and then use GIT to install Emndles-Nodes, whih + +- Installed the Windows standalone version of ComfyUI +- This will install a portable version of Python for you and add the Python folder to your Win +- Downloaded GIT from here: https://git-scm.com/download/win +- **IMPORTANT** This is where having the standalone version may trip you up: both the C:\ComfyUI\python_embeded\Scripts _and_ C:\ComfyUI\python_embeded\ folders need to be in the PATH sectiom of your system variables (_not_ the user variables). If I recall only the former is added, so you will have to add the second one manually + +![environ](./img/environ.png) + + +- If you do not know how to add environmental variables, [here's a link for you to read](https://www.howtogeek.com/787217/how-to-edit-environment-variables-on-windows-10-or-11/) +- If you do not have both folders added, the program will fail to find Pip and will not correctly install Clip +- Once you have added that PATH, go to the custom folder for Endless Nodes, open up a command prompt (right-click and choose "Command Prompt here") and type the following: + +```python -m pip install -r requirements.txt>``` + +- Note the '-m' and '-r' in the command! You should see a tonne of programs loading and then a success notice. Now Endless-Nodes should load properly +- If you did a GIT install of ComfyUI and the ComfyUI Manager fails you, using the command noted above should work for you too, or you may be able to drop the -m part + +### What to do if installation fails + +You can try to install Clip directly via the following commands(still within the Endless-Nodes folder): + +```pip install ftfy regex tqdm +pip install git+https://github.com/openai/CLIP.git``` + +If **_that_** does not work, then it likely some configuration on your machine is preventing the install. Ask me **nicely** for a specific module if you're looking for one and I'll see if I can separate it out for you. Your tone matters, I'm too old to pay attention to people who think I blew up their machines and I will be as short and presumptuous with you as you are with me. If that bothers you, some self-relfection may be in order. + + ## Node List ## Endless Image Saver @@ -77,7 +118,7 @@ This is why I tried my hand at Python in the first place! There are many, many, + You can cobble some savers to save an image together with a text file, but the timestamp on the text file tends to be 2-3 seconds off from the image + No saver I know of lets you save the JSON file to a **completely different folder** -So… this node will allow you to save your image file wherever you want, with full support for standard [Python date and time conventions]( https://docs.python.org/2/library/time.html#time.sleep), and you can save the JSON file somewhere else. +So… this node will allow you to save your image file wherever you want, with full support for standard [Python date and time conventions](https://strftime.org/) and you can save the JSON file somewhere else. I have more plans for this, but it’s ready for release now. @@ -89,7 +130,19 @@ Does it work... ? ![imagesaverfile](./img/imagesaverfile.png) -JSONs to the left of me, images to the right of me, and here I am stuck in the midle with you! It works! +JSONs to the left of me, images to the right of me, and here I am stuck in the middle with you! It works! + +** NOTE: this module is [available separately here](https://github.com/tusharbhutt/Endless-Nodes/blob/main/endless_nodes.py), just toss it in the ComfyUI custom_nodes folder if you don't want the remaining nodes + +### A note on timestamp formats + +This module uses the standard Python date and time stamp formats, it **_does not_** use the date and time format popular in the WAS Suite. See below for equivalency examples: + + - WAS Suite would use: ```[time(%Y-%m-%d__%I-%M%p)]``` + - Python stadndard is: ```%Y_%m_%d__%I-%M%p``` + +Note that there is no need to use "\[time(your string here)\]" + ## Aesthetic Scorer diff --git a/__init__.py b/__init__.py index f9eb669..7e15a36 100644 --- a/__init__.py +++ b/__init__.py @@ -14,13 +14,14 @@ NODE_CLASS_MAPPINGS = { "ESS Six Integer IO Switch": EndlessNode_SixIntIOSwitch, "ESS Six Integer IO Widget": EndlessNode_SixIntIOWidget, "ESS Parameterizer": EndlessNode_XLParameterizer, +# "ESS Global Envoy": EndlessNode_XLGlobalEnvoy, "ESS Parameterizer & Prompts": EndlessNode_XLParameterizerPrompt, "ESS Combo Parameterizer": EndlessNode_ComboXLParameterizer, "ESS Combo Parameterizer & Prompts": EndlessNode_ComboXLParameterizerPrompt, "ESS Image Saver with JSON": EndlessNode_ImageSaver, -# "ESS Aesthetic Scoring": EndlessNode_Scoring, + "ESS Aesthetic Scoring": EndlessNode_Scoring, # "ESS Aesthetic Scoring Auto": EndlessNode_ScoringAutoScore, -# "ESS Image Reward": EndlessNode_ImageReward, + "ESS Image Reward": EndlessNode_ImageReward, # "ESS Image Reward Auto": EndlessNode_ImageRewardAutoScore, "ESS Float to Integer": EndlessNode_FloattoInt, "ESS Float to Number": EndlessNode_FloattoNum, @@ -47,13 +48,14 @@ NODE_DISPLAY_NAME_MAPPINGS = { "ESS Six Integer IO Switch": "♾️🌊✨ Six Integer IO Switch", "ESS Six Integer IO Widget": "♾️🌊✨ Six Integer IO Widget", "ESS Parameterizer": "♾️🌊✨ Parameterizer", +# "ESS Global Envoy": "♾️🌊✨ Global Envoy", "ESS Parameterizer & Prompts": "♾️🌊✨ Parameterizer & Prompts", "ESS Combo Parameterizer": "♾️🌊✨ Combo Parameterizer", "ESS Combo Parameterizer & Prompts": "♾️🌊✨ Combo Parameterizer & Prompts", "ESS Image Saver with JSON": "♾️🌊✨ Image Saver with JSON", -# "ESS Aesthetic Scoring": "♾️🌊✨ Aesthetic Scoring", + "ESS Aesthetic Scoring": "♾️🌊✨ Aesthetic Scoring", # "ESS Aesthetic Scoring Auto": "♾️🌊✨ Aesthetic Scoring Auto", -# "ESS Image Reward": "♾️🌊✨ Image Reward", + "ESS Image Reward": "♾️🌊✨ Image Reward", # "ESS Image Reward Auto": "♾️🌊✨ Image Reward Auto", "ESS Float to Integer": "♾️🌊✨ Float to Integer", "ESS Float to Number": "♾️🌊✨ Float to Number", @@ -75,4 +77,4 @@ NODE_DISPLAY_NAME_MAPPINGS = { __all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS'] -print("\033[36m An Endless Sea of Stars Custom Nodes V0.37 \033[34m: \033[92mLoaded\033[0m") +print("\033[36m An Endless Sea of Stars Custom Nodes V0.38b \033[34m: \033[92mLoaded\033[0m") \ No newline at end of file diff --git a/changlelog.md b/changlelog.md index 3959f5e..9658808 100644 --- a/changlelog.md +++ b/changlelog.md @@ -1,3 +1,7 @@ +Oct 18/23: V0.38: (UNRELEASED)Putting in hooks for future fixes and improvements +Oct 18/23: V0.37: (UNRELEASED) Attempt to pass scoring metadata to image saver +Oct 18/23: V0.36: (UNRELEASED) Attempt to add new node to simplify resolution input and steps +Oct 14/23: V0.35: Fixed issues with scoring nodes and updated image saver for some bugs Oct 07/23, V0.33: Removed Aesthetic Scorer and ImageReward until I can figure out why the CLIP module isn't working for a few people Oct 05/23, V0.32: (UNRELEASED)Set rules for image saver so paths + filename length do not exceed 248 (leaves room for extension) Oct 04/23, V0.31: Release of V0.28 functionality (int, float, num to X), added String to X, code cleanup, vanity node renaming and recategorization diff --git a/endless_nodes.py b/endless_nodes.py index ff72384..453c83c 100644 --- a/endless_nodes.py +++ b/endless_nodes.py @@ -46,8 +46,8 @@ from PIL.PngImagePlugin import PngInfo from colorama import init, Fore, Back, Style from os.path import join from warnings import filterwarnings -# import ImageReward as RM -# import clip +import ImageReward as RM +import clip import colorama import datetime import folder_paths @@ -56,7 +56,7 @@ import json import math import numpy as np import os -#import pytorch_lightning as pl +import pytorch_lightning as pl import re import socket import statistics @@ -64,7 +64,7 @@ import sys import time import torch import torch.nn as nn -# import random +import random sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)), "comfy")) @@ -324,6 +324,42 @@ class EndlessNode_XLParameterizer: def Parameterizer(self,base_width,base_height,base_crop_w,base_crop_h,base_target_w,base_target_h,refiner_width,refiner_height,refiner_ascore): return(base_width,base_height,base_crop_w,base_crop_h,base_target_w,base_target_h,refiner_width,refiner_height,refiner_ascore) + + + + +#---------------------------------------------- +# CLIP text encode box without prompt (short) + +class EndlessNode_XLGlobalEnvoy: + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "width": ("INT", {"default": 1024, "min": 64, "max": 8192, "step": 16}), + "height": ("INT", {"default": 1024, "min": 64, "max": 8192, "step": 16}), + "start": ("INT", {"default": 0, "min": 0, "max": 2048, "step": 1}), + "switchover": ("INT", {"default": 0, "min": 0, "max": 2048, "step": 1}), + "stop": ("INT", {"default": 1, "min": 1, "max": 2048, "step": 1}), + "percstep": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}), + }, + "display_names": {"percstep": "Switchover Percentage", + } + } + RETURN_TYPES = ("INT","INT","INT","INT","INT",) + RETURN_NAMES = ("Width","Height","Start Step", "Switchover at Step", "End Step") + FUNCTION = "global_envoy" + + CATEGORY = "Endless 🌊✨/Parameters" + + + def global_envoy(self,width, height,start,switchover,stop,percstep): + if percstep != 0.0: + switchover = round(stop*percstep) + return(width,height,start, stop, switchover) #---------------------------------------------- # Text Encode Combo Box with prompt @@ -404,91 +440,117 @@ class EndlessNode_ComboXLParameterizer: #---------------------------------------------- # Aesthetic Scoring Node -# folder_paths.folder_names_and_paths["aesthetic"] = ([os.path.join(folder_paths.models_dir,"aesthetic")], folder_paths.supported_pt_extensions) +folder_paths.folder_names_and_paths["aesthetic"] = ([os.path.join(folder_paths.models_dir,"aesthetic")], folder_paths.supported_pt_extensions) -# class MLP(pl.LightningModule): - # def __init__(self, input_size, xcol='emb', ycol='avg_rating'): - # super().__init__() - # self.input_size = input_size - # self.xcol = xcol - # self.ycol = ycol - # self.layers = nn.Sequential( - # nn.Linear(self.input_size, 1024), - # #nn.ReLU(), - # nn.Dropout(0.2), - # nn.Linear(1024, 128), - # #nn.ReLU(), - # nn.Dropout(0.2), - # nn.Linear(128, 64), - # #nn.ReLU(), - # nn.Dropout(0.1), - # nn.Linear(64, 16), - # #nn.ReLU(), - # nn.Linear(16, 1) - # ) - # def forward(self, x): - # return self.layers(x) - # def training_step(self, batch, batch_idx): - # x = batch[self.xcol] - # y = batch[self.ycol].reshape(-1, 1) - # x_hat = self.layers(x) - # loss = F.mse_loss(x_hat, y) - # return loss - # def validation_step(self, batch, batch_idx): - # x = batch[self.xcol] - # y = batch[self.ycol].reshape(-1, 1) - # x_hat = self.layers(x) - # loss = F.mse_loss(x_hat, y) - # return loss - # def configure_optimizers(self): - # optimizer = torch.optim.Adam(self.parameters(), lr=1e-3) - # return optimizer -# def normalized(a, axis=-1, order=2): - # import numpy as np # pylint: disable=import-outside-toplevel - # l2 = np.atleast_1d(np.linalg.norm(a, order, axis)) - # l2[l2 == 0] = 1 - # return a / np.expand_dims(l2, axis) +class MLP(pl.LightningModule): + def __init__(self, input_size, xcol='emb', ycol='avg_rating'): + super().__init__() + self.input_size = input_size + self.xcol = xcol + self.ycol = ycol + self.layers = nn.Sequential( + nn.Linear(self.input_size, 1024), + #nn.ReLU(), + nn.Dropout(0.2), + nn.Linear(1024, 128), + #nn.ReLU(), + nn.Dropout(0.2), + nn.Linear(128, 64), + #nn.ReLU(), + nn.Dropout(0.1), + nn.Linear(64, 16), + #nn.ReLU(), + nn.Linear(16, 1) + ) + def forward(self, x): + return self.layers(x) + def training_step(self, batch, batch_idx): + x = batch[self.xcol] + y = batch[self.ycol].reshape(-1, 1) + x_hat = self.layers(x) + loss = F.mse_loss(x_hat, y) + return loss + def validation_step(self, batch, batch_idx): + x = batch[self.xcol] + y = batch[self.ycol].reshape(-1, 1) + x_hat = self.layers(x) + loss = F.mse_loss(x_hat, y) + return loss + def configure_optimizers(self): + optimizer = torch.optim.Adam(self.parameters(), lr=1e-3) + return optimizer +def normalized(a, axis=-1, order=2): + import numpy as np # pylint: disable=import-outside-toplevel + l2 = np.atleast_1d(np.linalg.norm(a, order, axis)) + l2[l2 == 0] = 1 + return a / np.expand_dims(l2, axis) -# class EndlessNode_Scoring: - # def __init__(self): - # pass +class EndlessNode_Scoring: + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "model_name": (folder_paths.get_filename_list("aesthetic"), {"multiline": False, "default": "chadscorer.pth"}), + "image": ("IMAGE",), + } + } + + RETURN_TYPES = ("NUMBER","FLOAT","STRING") + FUNCTION = "calc_score" + CATEGORY = "Endless 🌊✨/Scoring" + + def calc_score(self, model_name, image): + m_path = folder_paths.folder_names_and_paths["aesthetic"][0] + m_path2 = os.path.join(m_path[0], model_name) + model = MLP(768) # CLIP embedding dim is 768 for CLIP ViT L 14 + s = torch.load(m_path2) + model.load_state_dict(s) + model.to("cuda") + model.eval() + device = "cuda" + model2, preprocess = clip.load("ViT-L/14", device=device) # RN50x64 + tensor_image = image[0] + img = (tensor_image * 255).to(torch.uint8).numpy() + pil_image = Image.fromarray(img, mode='RGB') + image2 = preprocess(pil_image).unsqueeze(0).to(device) + with torch.no_grad(): + image_features = model2.encode_image(image2) + im_emb_arr = normalized(image_features.cpu().detach().numpy()) + prediction = model(torch.from_numpy(im_emb_arr).to(device).type(torch.cuda.FloatTensor)) + final_prediction = round(float(prediction[0]), 2) + del model + return (final_prediction,final_prediction,str(final_prediction),) + + + +## This may help in some way to return the score results to a dialog box. +# class OutputString: + # @classmethod + # def INPUT_TYPES(cls): + # return { + # "required": { + # "string": ("STRING", {}), + # } + # } + + # RETURN_TYPES = () + # FUNCTION = "output_string" + + # OUTPUT_NODE = True + + # CATEGORY = "utils" + + # def output_string(self, string): + # return { "ui": { "string": string } } + - # @classmethod - # def INPUT_TYPES(cls): - # return { - # "required": { - # "model_name": (folder_paths.get_filename_list("aesthetic"), {"multiline": False, "default": "chadscorer.pth"}), - # "image": ("IMAGE",), - # } - # } - # RETURN_TYPES = ("NUMBER","IMAGE") - # FUNCTION = "calc_score" - # CATEGORY = "Endless 🌊✨/Scoring" - # def calc_score(self, model_name, image): - # m_path = folder_paths.folder_names_and_paths["aesthetic"][0] - # m_path2 = os.path.join(m_path[0], model_name) - # model = MLP(768) # CLIP embedding dim is 768 for CLIP ViT L 14 - # s = torch.load(m_path2) - # model.load_state_dict(s) - # model.to("cuda") - # model.eval() - # device = "cuda" - # model2, preprocess = clip.load("ViT-L/14", device=device) # RN50x64 - # tensor_image = image[0] - # img = (tensor_image * 255).to(torch.uint8).numpy() - # pil_image = Image.fromarray(img, mode='RGB') - # image2 = preprocess(pil_image).unsqueeze(0).to(device) - # with torch.no_grad(): - # image_features = model2.encode_image(image2) - # im_emb_arr = normalized(image_features.cpu().detach().numpy()) - # prediction = model(torch.from_numpy(im_emb_arr).to(device).type(torch.cuda.FloatTensor)) - # final_prediction = round(float(prediction[0]), 2) - # del model - # return (final_prediction,) # #---------------------------------------------- NOT WORKING, NEED TO LOOK AT IT # # Aesthetic Scoring Node with Scoring passed to image @@ -538,41 +600,41 @@ class EndlessNode_ComboXLParameterizer: #---------------------------------------------- # Image Reward Scoring -# class EndlessNode_ImageReward: - # def __init__(self): - # self.model = None +class EndlessNode_ImageReward: + def __init__(self): + self.model = None - # @classmethod - # def INPUT_TYPES(cls): - # return { - # "required": { - # "model": ("STRING", {"multiline": False, "default": "ImageReward-v1.0"}), - # "prompt": ("STRING", {"multiline": True, "forceInput": True}), - # "images": ("IMAGE",), - # }, - # } + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "model": ("STRING", {"multiline": False, "default": "ImageReward-v1.0"}), + "prompt": ("STRING", {"multiline": True, "forceInput": True}), + "images": ("IMAGE",), + }, + } - # RETURN_TYPES = ("FLOAT", "STRING", "FLOAT", "STRING") - # RETURN_NAMES = ("SCORE_FLOAT", "SCORE_STRING", "VALUE_FLOAT", "VALUE_STRING") + RETURN_TYPES = ("FLOAT", "STRING", "FLOAT", "STRING") + RETURN_NAMES = ("SCORE_FLOAT", "SCORE_STRING", "VALUE_FLOAT", "VALUE_STRING") - # CATEGORY = "Endless 🌊✨/Scoring" + CATEGORY = "Endless 🌊✨/Scoring" - # FUNCTION = "process_images" + FUNCTION = "process_images" - # def process_images(self, model, prompt, images,): #rounded): - # if self.model is None: - # self.model = RM.load(model) + def process_images(self, model, prompt, images,): #rounded): + if self.model is None: + self.model = RM.load(model) - # score = 0.0 - # for image in images: - # # convert to PIL image - # i = 255.0 * image.cpu().numpy() - # img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8)) - # score += self.model.score(prompt, [img]) - # score /= len(images) - # # assume std dev follows normal distribution curve - # valuescale = 0.5 * (1 + math.erf(score / math.sqrt(2))) * 10 # *10 to get a value between -10 - # return (score, str(score), valuescale, str(valuescale)) + score = 0.0 + for image in images: + # convert to PIL image + i = 255.0 * image.cpu().numpy() + img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8)) + score += self.model.score(prompt, [img]) + score /= len(images) + # assume std dev follows normal distribution curve + valuescale = 0.5 * (1 + math.erf(score / math.sqrt(2))) * 10 # *10 to get a value between -10 + return (score, str(score), valuescale, str(valuescale),) # #---------------------------------------------- NOT WORKING, NEED TO LOOK AT @@ -657,13 +719,13 @@ class EndlessNode_ImageSaver: image_folder=None, json_folder=None, prompt=None, extra_pnginfo=None): # Replace illegal characters in the filename prefix with dashes - filename_prefix = re.sub(r'[<>:"\/\\|?*]', '-', filename_prefix) + filename_prefix = re.sub(r'[<>:"/\\|?*]', '-', filename_prefix) # Set IMG Extension img_extension = '.png' counter = 1 - + results = list() for image in images: @@ -671,6 +733,13 @@ class EndlessNode_ImageSaver: img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8)) metadata = PngInfo() + + def encode_emoji(obj): + if isinstance(obj, str): + return obj.encode('utf-8', 'surrogatepass').decode('utf-8') + return obj + + if prompt is not None: metadata.add_text("prompt", json.dumps(prompt)) if extra_pnginfo is not None: @@ -680,16 +749,16 @@ class EndlessNode_ImageSaver: img_file, json_file = self.generate_filenames(filename_prefix, delimiter, counter, filename_number_padding, filename_number_start, img_extension, image_folder, json_folder) - + try: if img_extension == '.png': img.save(img_file, pnginfo=metadata, compress_level=4) elif img_extension == '.jpeg': img.save(img_file, quality=100, optimize=True) - with open(json_file, 'w', encoding='utf-8', newline='\n') as f: if prompt is not None: - f.write(json.dumps(prompt, indent=4)) + f.write("Prompt:\n" + json.dumps(prompt, indent="\t", default=encode_emoji, ensure_ascii=False)) + f.write("\nExtra PNG Info:\n" + json.dumps(extra_pnginfo, indent="\t", default=encode_emoji, ensure_ascii=False)) print(Fore.GREEN + f"+ File(s) saved to: {img_file}") @@ -723,18 +792,25 @@ class EndlessNode_ImageSaver: img_file = f"{filename_prefix}{delimiter}{counter:0{filename_number_padding}}{img_extension}" json_file = f"{filename_prefix}{delimiter}{counter:0{filename_number_padding}}.json" + # Apply placeholders for date and time in filenames img_file = self.replace_date_time_placeholders(img_file) json_file = self.replace_date_time_placeholders(json_file) # Construct full paths for image and text files based on folders provided if image_folder: - img_file = os.path.join(image_folder, img_file) + image_folder = self.replace_date_time_placeholders(image_folder) + img_folder_path = os.path.join(self.output_dir, image_folder) if not image_folder.startswith(self.output_dir) else image_folder + os.makedirs(img_folder_path, exist_ok=True) # Create image folder if it doesn't exist + img_file = os.path.join(img_folder_path, img_file) else: img_file = os.path.join(self.output_dir, img_file) if json_folder: - json_file = os.path.join(json_folder, json_file) + json_folder = self.replace_date_time_placeholders(json_folder) + json_folder_path = os.path.join(self.output_dir, json_folder) if not json_folder.startswith(self.output_dir) else json_folder + os.makedirs(json_folder_path, exist_ok=True) # Create json folder if it doesn't exist + json_file = os.path.join(json_folder_path, json_file) else: json_file = os.path.join(os.path.dirname(img_file), json_file) @@ -753,34 +829,48 @@ class EndlessNode_ImageSaver: json_file = self.replace_date_time_placeholders(json_file) if image_folder: - img_file = os.path.join(image_folder, img_file) + image_folder = self.replace_date_time_placeholders(image_folder) + img_folder_path = os.path.join(self.output_dir, image_folder) if not image_folder.startswith(self.output_dir) else image_folder + os.makedirs(img_folder_path, exist_ok=True) # Create image folder if it doesn't exist + img_file = os.path.join(img_folder_path, img_file) else: img_file = os.path.join(self.output_dir, img_file) if json_folder: - json_file = os.path.join(json_folder, json_file) + json_folder = self.replace_date_time_placeholders(json_folder) + json_folder_path = os.path.join(self.output_dir, json_folder) if not json_folder.startswith(self.output_dir) else json_folder + os.makedirs(json_folder_path, exist_ok=True) # Create json folder if it doesn't exist + json_file = os.path.join(json_folder_path, json_file) else: json_file = os.path.join(os.path.dirname(img_file), json_file) return img_file, json_file def replace_date_time_placeholders(self, filename): - # Replace date and time placeholders with actual date and time strings - now = datetime.datetime.now() - placeholders = { - '%Y': now.strftime('%Y'), # Year with century as a decimal number - '%y': now.strftime('%y'), # Year without century as a zero-padded decimal number - '%m': now.strftime('%m'), # Month as a zero-padded decimal number - '%d': now.strftime('%d'), # Day of the month as a zero-padded decimal number - '%H': now.strftime('%H'), # Hour (24-hour clock) as a zero-padded decimal number - '%M': now.strftime('%M'), # Minute as a zero-padded decimal number - '%S': now.strftime('%S'), # Second as a zero-padded decimal number - } + def replace_match(match): + placeholder = match.group(0) + try: + formatted_value = now.strftime(placeholder) + return formatted_value + except ValueError: + return placeholder - for placeholder, replacement in placeholders.items(): - filename = filename.replace(placeholder, replacement) + # Define the pattern to match date and time placeholders + pattern = r'%[a-zA-Z]' + + # Get the current datetime + now = datetime.datetime.now() + + # Use re.sub to find and replace all placeholders + filename = re.sub(pattern, replace_match, filename) return filename + + # def truncate_string(s, length): + # if len(s) > length: + # return s[:length] + # return s + # ______________________________________________________________________________________________________________________________________________________________ # CONVERTER NODES BLOCK # # @@ -1007,7 +1097,7 @@ class EndlessNode_NumtoString: "required": {"NumberValue": ("NUMBER",)}, } - RETURN_TYPES = ("STRING") + RETURN_TYPES = ("STRING",) FUNCTION = "inputnum" def inputnum(self, NumberValue): diff --git a/requirements.txt b/requirements.txt index 3d90aaa..4f72e75 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,4 @@ -colorama \ No newline at end of file +lightning +clip @ git+https://github.com/openai/CLIP.git +colorama +image-reward==1.4 \ No newline at end of file