Files
YMC-GitHub-ymc-node-suite-c…/YMC_Node_Suite.py
T

2129 lines
71 KiB
Python

# [save text file of was node suite](D:\code\python\comfyui\custom_nodes\was-node-suite-comfyui\WAS_Node_Suite.py)
from PIL import Image, ImageFilter, ImageEnhance, ImageOps, ImageDraw, ImageChops, ImageFont
from PIL.PngImagePlugin import PngInfo
from io import BytesIO
# from typing import Optional
from typing import Callable, List, Dict, Any, Union, Tuple, cast,Optional
from urllib.request import urlopen
import comfy.diffusers_convert
import comfy.samplers
import comfy.sd
import comfy.utils
import comfy.clip_vision
import model_management
import folder_paths as comfy_paths
import model_management
from comfy_extras.chainner_models import model_loading
import glob
import hashlib
import json
import nodes
import math
import numpy as np
from numba import jit
import os
import random
import re
import requests
import socket
import subprocess
import sys
import time
import torch
from tqdm import tqdm
MANIFEST = {
"name": "ymc node suite",
"version": (1,0,0),
"author": "ymc-github",
"project": "https://github.com/ymc-github/ymc-node-suite-comfyui",
"description": "an extensive node suite for comfyui",
}
#! SET SYS PATH
sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)), "ymc_node_suite_comfyui"))
sys.path.append(comfy_paths.base_path)
#! SYSTEM HOOKS
"""color str"""
class cstr(str):
class color:
END = '\33[0m'
BOLD = '\33[1m'
ITALIC = '\33[3m'
UNDERLINE = '\33[4m'
BLINK = '\33[5m'
BLINK2 = '\33[6m'
SELECTED = '\33[7m'
BLACK = '\33[30m'
RED = '\33[31m'
GREEN = '\33[32m'
YELLOW = '\33[33m'
BLUE = '\33[34m'
VIOLET = '\33[35m'
BEIGE = '\33[36m'
WHITE = '\33[37m'
BLACKBG = '\33[40m'
REDBG = '\33[41m'
GREENBG = '\33[42m'
YELLOWBG = '\33[43m'
BLUEBG = '\33[44m'
VIOLETBG = '\33[45m'
BEIGEBG = '\33[46m'
WHITEBG = '\33[47m'
GREY = '\33[90m'
LIGHTRED = '\33[91m'
LIGHTGREEN = '\33[92m'
LIGHTYELLOW = '\33[93m'
LIGHTBLUE = '\33[94m'
LIGHTVIOLET = '\33[95m'
LIGHTBEIGE = '\33[96m'
LIGHTWHITE = '\33[97m'
GREYBG = '\33[100m'
LIGHTREDBG = '\33[101m'
LIGHTGREENBG = '\33[102m'
LIGHTYELLOWBG = '\33[103m'
LIGHTBLUEBG = '\33[104m'
LIGHTVIOLETBG = '\33[105m'
LIGHTBEIGEBG = '\33[106m'
LIGHTWHITEBG = '\33[107m'
@staticmethod
def add_code(name, code):
if not hasattr(cstr.color, name.upper()):
setattr(cstr.color, name.upper(), code)
else:
raise ValueError(f"'cstr' object already contains a code with the name '{name}'.")
def __new__(cls, text):
return super().__new__(cls, text)
def __getattr__(self, attr):
if attr.lower().startswith("_cstr"):
code = getattr(self.color, attr.upper().lstrip("_cstr"))
modified_text = self.replace(f"__{attr[1:]}__", f"{code}")
return cstr(modified_text)
elif attr.upper() in dir(self.color):
code = getattr(self.color, attr.upper())
modified_text = f"{code}{self}{self.color.END}"
return cstr(modified_text)
elif attr.lower() in dir(cstr):
return getattr(cstr, attr.lower())
else:
raise AttributeError(f"'cstr' object has no attribute '{attr}'")
def print(self, **kwargs):
print(self, **kwargs)
#! MESSAGE TEMPLATES
cstr.color.add_code("msg", f"{cstr.color.BLUE}Ymc Node Suite: {cstr.color.END}")
cstr.color.add_code("warning", f"{cstr.color.BLUE}Ymc Node Suite {cstr.color.LIGHTYELLOW}Warning: {cstr.color.END}")
cstr.color.add_code("error", f"{cstr.color.RED}Ymc Node Suite {cstr.color.END}Error: {cstr.color.END}")
#! GLOBALS
NODE_FILE = os.path.abspath(__file__)
MIDAS_INSTALLED = False
CUSTOM_NODES_DIR = comfy_paths.folder_names_and_paths["custom_nodes"][0][0]
MODELS_DIR = comfy_paths.models_dir
WAS_SUITE_ROOT = os.path.dirname(NODE_FILE)
WAS_DATABASE = os.path.join(WAS_SUITE_ROOT, 'was_suite_settings.json')
WAS_HISTORY_DATABASE = os.path.join(WAS_SUITE_ROOT, 'was_history.json')
WAS_CONFIG_FILE = os.path.join(WAS_SUITE_ROOT, 'was_suite_config.json')
STYLES_PATH = os.path.join(WAS_SUITE_ROOT, 'styles.json')
ALLOWED_EXT = ('.jpeg', '.jpg', '.png',
'.tiff', '.gif', '.bmp', '.webp')
#! INSTALLATION CLEANUP
# Delete legacy nodes
legacy_was_nodes = ['fDOF_WAS.py', 'Image_Blank_WAS.py', 'Image_Blend_WAS.py', 'Image_Canny_Filter_WAS.py', 'Canny_Filter_WAS.py', 'Image_Combine_WAS.py', 'Image_Edge_Detection_WAS.py', 'Image_Film_Grain_WAS.py', 'Image_Filters_WAS.py',
'Image_Flip_WAS.py', 'Image_Nova_Filter_WAS.py', 'Image_Rotate_WAS.py', 'Image_Style_Filter_WAS.py', 'Latent_Noise_Injection_WAS.py', 'Latent_Upscale_WAS.py', 'MiDaS_Depth_Approx_WAS.py', 'NSP_CLIPTextEncoder.py', 'Samplers_WAS.py']
legacy_was_nodes_found = []
WAS_NODES_DIR_NAME="ymc-node-suite-comfyui"
WAS_NODES_INDEX="Ymc_Node_Suite.py"
if os.path.basename(CUSTOM_NODES_DIR) == WAS_NODES_DIR_NAME:
legacy_was_nodes.append(WAS_NODES_INDEX)
f_disp = False
node_path_dir = os.getcwd()+os.sep+'ComfyUI'+os.sep+'custom_nodes'+os.sep
for f in legacy_was_nodes:
file = f'{node_path_dir}{f}'
if os.path.exists(file):
if not f_disp:
cstr("Found legacy nodes. Archiving legacy nodes...").msg.print()
f_disp = True
legacy_was_nodes_found.append(file)
if legacy_was_nodes_found:
import zipfile
from os.path import basename
archive = zipfile.ZipFile(
f'{node_path_dir}WAS_Legacy_Nodes_Backup_{round(time.time())}.zip', "w")
for f in legacy_was_nodes_found:
archive.write(f, basename(f))
try:
os.remove(f)
except OSError:
pass
archive.close()
if f_disp:
cstr("Legacy cleanup complete.").msg.print()
#! WAS SUITE CONFIG
was_conf_template = {
"run_requirements": True,
"suppress_uncomfy_warnings": True,
"show_startup_junk": True,
"show_inspiration_quote": True,
"text_nodes_type": "STRING",
"webui_styles": None,
"webui_styles_persistent_update": True,
"blip_model_url": "https://storage.googleapis.com/sfr-vision-language-research/BLIP/models/model_base_capfilt_large.pth",
"blip_model_vqa_url": "https://storage.googleapis.com/sfr-vision-language-research/BLIP/models/model_base_vqa_capfilt_large.pth",
"sam_model_vith_url": "https://dl.fbaipublicfiles.com/segment_anything/sam_vit_h_4b8939.pth",
"sam_model_vitl_url": "https://dl.fbaipublicfiles.com/segment_anything/sam_vit_l_0b3195.pth",
"sam_model_vitb_url": "https://dl.fbaipublicfiles.com/segment_anything/sam_vit_b_01ec64.pth",
"history_display_limit": 36,
"use_legacy_ascii_text": False,
"ffmpeg_bin_path": "/path/to/ffmpeg",
"ffmpeg_extra_codecs": {
"avc1": ".mp4",
"h264": ".mkv",
},
"wildcards_path": os.path.join(WAS_SUITE_ROOT, "wildcards"),
"wildcard_api": True,
}
# Create, Load, or Update Config
def getSuiteConfig():
global was_conf_template
try:
with open(WAS_CONFIG_FILE, "r") as f:
was_config = json.load(f)
except OSError as e:
cstr(f"Unable to load conf file at `{WAS_CONFIG_FILE}`. Using internal config template.").error.print()
return was_conf_template
except Exception as e:
cstr(f"Unable to load conf file at `{WAS_CONFIG_FILE}`. Using internal config template.").error.print()
return was_conf_template
return was_config
def updateSuiteConfig(conf):
try:
with open(WAS_CONFIG_FILE, "w", encoding='utf-8') as f:
json.dump(conf, f, indent=4)
except OSError as e:
print(e)
return False
except Exception as e:
print(e)
return False
return True
if not os.path.exists(WAS_CONFIG_FILE):
if updateSuiteConfig(was_conf_template):
cstr(f'Created default conf file at `{WAS_CONFIG_FILE}`.').msg.print()
was_config = getSuiteConfig()
else:
cstr(f"Unable to create default conf file at `{WAS_CONFIG_FILE}`. Using internal config template.").error.print()
was_config = was_conf_template
else:
was_config = getSuiteConfig()
update_config = False
for sett_ in was_conf_template.keys():
if not was_config.__contains__(sett_):
was_config.update({sett_: was_conf_template[sett_]})
update_config = True
if update_config:
updateSuiteConfig(was_config)
# WAS SETTINGS MANAGER
class WASDatabase:
"""
The WAS Suite Database Class provides a simple key-value database that stores
data in a flatfile using the JSON format. Each key-value pair is associated with
a category.
Attributes:
filepath (str): The path to the JSON file where the data is stored.
data (dict): The dictionary that holds the data read from the JSON file.
Methods:
insert(category, key, value): Inserts a key-value pair into the database
under the specified category.
get(category, key): Retrieves the value associated with the specified
key and category from the database.
update(category, key): Update a value associated with the specified
key and category from the database.
delete(category, key): Deletes the key-value pair associated with the
specified key and category from the database.
_save(): Saves the current state of the database to the JSON file.
"""
def __init__(self, filepath):
self.filepath = filepath
try:
with open(filepath, 'r') as f:
self.data = json.load(f)
except FileNotFoundError:
self.data = {}
def catExists(self, category):
return self.data.__contains__(category)
def keyExists(self, category, key):
return self.data[category].__contains__(key)
def insert(self, category, key, value):
if category not in self.data:
self.data[category] = {}
self.data[category][key] = value
self._save()
def update(self, category, key, value):
if category in self.data and key in self.data[category]:
self.data[category][key] = value
self._save()
def updateCat(self, category, dictionary):
self.data[category].update(dictionary)
self._save()
def get(self, category, key):
return self.data.get(category, {}).get(key, None)
def getDB(self):
return self.data
def insertCat(self, category):
if self.data.__contains__(category):
cstr(f"The database category `{category}` already exists!").error.print()
return
self.data[category] = {}
self._save()
def getDict(self, category):
if not self.data.__contains__(category):
cstr(f"\033[34mYmc Node Suite\033[0m Error: The database category `{category}` does not exist!").error.print()
return self.data[category]
def delete(self, category, key):
if category in self.data and key in self.data[category]:
del self.data[category][key]
self._save()
def _save(self):
try:
with open(self.filepath, 'w') as f:
json.dump(self.data, f, indent=4)
except FileNotFoundError:
cstr(f"Cannot save database to file '{self.filepath}'."
" Storing the data in the object instead. Does the folder and node file have write permissions?").warning.print()
# Initialize the settings database
WDB = WASDatabase(WAS_DATABASE)
# WAS Token Class
class TextTokens:
def __init__(self):
self.WDB = WDB
if not self.WDB.getDB().__contains__('custom_tokens'):
self.WDB.insertCat('custom_tokens')
self.custom_tokens = self.WDB.getDict('custom_tokens')
self.tokens = {
'[time]': str(time.time()).replace('.','_'),
'[hostname]': socket.gethostname(),
}
if '.' in self.tokens['[time]']:
self.tokens['[time]'] = self.tokens['[time]'].split('.')[0]
try:
self.tokens['[user]'] = ( os.getlogin() if os.getlogin() else 'null' )
except Exception:
self.tokens['[user]'] = 'null'
def addToken(self, name, value):
self.custom_tokens.update({name: value})
self._update()
def removeToken (self, name):
self.custom_tokens.pop(name)
self._update()
def format_time(self, format_code):
return time.strftime(format_code, time.localtime(time.time()))
def parseTokens(self, text):
tokens = self.tokens.copy()
if self.custom_tokens:
tokens.update(self.custom_tokens)
# Update time
tokens['[time]'] = str(time.time())
if '.' in tokens['[time]']:
tokens['[time]'] = tokens['[time]'].split('.')[0]
for token, value in tokens.items():
if token.startswith('[time('):
continue
text = text.replace(token, value)
def replace_custom_time(match):
format_code = match.group(1)
return self.format_time(format_code)
text = re.sub(r'\[time\((.*?)\)\]', replace_custom_time, text)
return text
def _update(self):
self.WDB.updateCat('custom_tokens', self.custom_tokens)
# Update text file history
def update_history_text_files(new_paths):
HDB = WASDatabase(WAS_HISTORY_DATABASE)
if HDB.catExists("History") and HDB.keyExists("History", "TextFiles"):
saved_paths = HDB.get("History", "TextFiles")
for path_ in saved_paths:
if not os.path.exists(path_):
saved_paths.remove(path_)
if isinstance(new_paths, str):
if new_paths in saved_paths:
saved_paths.remove(new_paths)
saved_paths.append(new_paths)
elif isinstance(new_paths, list):
for path_ in new_paths:
if path_ in saved_paths:
saved_paths.remove(path_)
saved_paths.append(path_)
HDB.update("History", "TextFiles", saved_paths)
else:
if not HDB.catExists("History"):
HDB.insertCat("History")
if isinstance(new_paths, str):
HDB.insert("History", "TextFiles", [new_paths])
elif isinstance(new_paths, list):
HDB.insert("History", "TextFiles", new_paths)
# Update output image history
# def update_history_output_images(new_paths):
# HDB = WASDatabase(WAS_HISTORY_DATABASE)
# category = "Output_Images"
# if HDB.catExists("History") and HDB.keyExists("History", category):
# saved_paths = HDB.get("History", category)
# for path_ in saved_paths:
# if not os.path.exists(path_):
# saved_paths.remove(path_)
# if isinstance(new_paths, str):
# if new_paths in saved_paths:
# saved_paths.remove(new_paths)
# saved_paths.append(new_paths)
# elif isinstance(new_paths, list):
# for path_ in new_paths:
# if path_ in saved_paths:
# saved_paths.remove(path_)
# saved_paths.append(path_)
# HDB.update("History", category, saved_paths)
# else:
# if not HDB.catExists("History"):
# HDB.insertCat("History")
# if isinstance(new_paths, str):
# HDB.insert("History", category, [new_paths])
# elif isinstance(new_paths, list):
# HDB.insert("History", category, new_paths)
# set INPUT_TYPES
class WAS_Text_Save:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"text": ("STRING", {"forceInput": True}),
"path": ("STRING", {"default": './ComfyUI/output/[time(%Y-%m-%d)]', "multiline": False}),
"filename_prefix": ("STRING", {"default": "ComfyUI"}),
"filename_delimiter": ("STRING", {"default":"_"}),
"filename_number_padding": ("INT", {"default":4, "min":2, "max":9, "step":1}),
"overwrite_mode": (["false", "prefix_as_filename"],),
"ext": ("STRING", {"default":".txt"}),
}
}
OUTPUT_NODE = True
# feat(core): add feat that noted in issue 1
# https://github.com/YMC-GitHub/ymc-node-suite-comfyui/issues/1
RETURN_TYPES = ("STRING", "STRING","STRING")
RETURN_NAMES = ('content','filenameWithoutExt','ext')
FUNCTION = "save_text_file"
CATEGORY = "Ymc Suite/IO"
def save_text_file(self, text, path, filename_prefix='ComfyUI', filename_delimiter='_', filename_number_padding=4,overwrite_mode='false',ext='.txt'):
tokens = TextTokens()
path = tokens.parseTokens(path)
filename_prefix = tokens.parseTokens(filename_prefix)
# feat(core): add paths when it does not exits
if not os.path.exists(path):
cstr(f"The path `{path}` doesn't exist! Creating it...").warning.print()
try:
os.makedirs(path, exist_ok=True)
except OSError as e:
cstr(f"The path `{path}` could not be created! Is there write access?\n{e}").error.print()
# feat(core): out info when text is empty
if text.strip() == '':
cstr(f"There is no text specified to save! Text is empty.").error.print()
delimiter = filename_delimiter
number_padding = int(filename_number_padding)
# file_extension = '.txt'
file_extension = ext
filename ,counter= self.generate_filename_data(path, filename_prefix, delimiter, number_padding, file_extension)
filenameWithoutExt = f"{filename_prefix}{delimiter}{counter:0{number_padding}}"
# feat(core): use prefix as file name when overwrite_mode is 'prefix_as_filename'
if overwrite_mode == 'prefix_as_filename':
filename = f"{filename_prefix}{file_extension}"
filenameWithoutExt = f"{filename_prefix}"
fileLocation = os.path.join(path, filename)
self.writeTextFile(fileLocation, text)
update_history_text_files(fileLocation)
# filenameWithoutExt,ext = os.path.splitext(filename)
# return (text, { "ui": { "string": text } } )
# feat(core): use the 2th result is filename without ext
# feat(core): use the 3th result is ext
return text, filenameWithoutExt,file_extension
def generate_filename_data(self, path, prefix, delimiter, number_padding, extension):
pattern = f"{re.escape(prefix)}{re.escape(delimiter)}(\\d{{{number_padding}}})"
existing_counters = [
int(re.search(pattern, filename).group(1))
for filename in os.listdir(path)
if re.match(pattern, filename)
]
existing_counters.sort(reverse=True)
if existing_counters:
counter = existing_counters[0] + 1
else:
counter = 1
filename = f"{prefix}{delimiter}{counter:0{number_padding}}{extension}"
while os.path.exists(os.path.join(path, filename)):
counter += 1
filename = f"{prefix}{delimiter}{counter:0{number_padding}}{extension}"
return filename,counter
def writeTextFile(self, file, content):
try:
with open(file, 'w', encoding='utf-8', newline='\n') as f:
f.write(content)
except OSError:
cstr(f"Unable to save file `{file}`").error.print()
# Image Save (NSP Compatible)
# Originally From ComfyUI/nodes.py
# class WAS_Image_Save:
# def __init__(self):
# self.output_dir = comfy_paths.output_directory
# self.type = 'output'
# @classmethod
# def INPUT_TYPES(cls):
# return {
# "required": {
# "images": ("IMAGE", ),
# "output_path": ("STRING", {"default": '[time(%Y-%m-%d)]', "multiline": False}),
# "filename_prefix": ("STRING", {"default": "ComfyUI"}),
# "filename_delimiter": ("STRING", {"default":"_"}),
# "filename_number_padding": ("INT", {"default":4, "min":1, "max":9, "step":1}),
# "filename_number_start": (["false", "true"],),
# "extension": (['png', 'jpeg', 'gif', 'tiff', 'webp'], ),
# "quality": ("INT", {"default": 100, "min": 1, "max": 100, "step": 1}),
# "lossless_webp": (["false", "true"],),
# "overwrite_mode": (["false", "prefix_as_filename"],),
# "show_history": (["false", "true"],),
# "show_history_by_prefix": (["true", "false"],),
# "embed_workflow": (["true", "false"],),
# "show_previews": (["true", "false"],),
# },
# "hidden": {
# "prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"
# },
# }
# RETURN_TYPES = ()
# FUNCTION = "was_save_images"
# OUTPUT_NODE = True
# CATEGORY = "WAS Suite/IO"
# def was_save_images(self, images, output_path='', filename_prefix="ComfyUI", filename_delimiter='_',
# extension='png', quality=100, lossless_webp="false", prompt=None, extra_pnginfo=None,
# overwrite_mode='false', filename_number_padding=4, filename_number_start='false',
# show_history='false', show_history_by_prefix="true", embed_workflow="true",
# show_previews="true"):
# delimiter = filename_delimiter
# number_padding = filename_number_padding
# lossless_webp = (lossless_webp == "true")
# # Define token system
# tokens = TextTokens()
# original_output = self.output_dir
# # Parse prefix tokens
# filename_prefix = tokens.parseTokens(filename_prefix)
# # Setup output path
# if output_path in [None, '', "none", "."]:
# output_path = self.output_dir
# else:
# output_path = tokens.parseTokens(output_path)
# if not os.path.isabs(output_path):
# output_path = os.path.join(self.output_dir, output_path)
# base_output = os.path.basename(output_path)
# if output_path.endswith("ComfyUI/output") or output_path.endswith("ComfyUI\output"):
# base_output = ""
# # Check output destination
# if output_path.strip() != '':
# if not os.path.isabs(output_path):
# output_path = os.path.join(comfy_paths.output_directory, output_path)
# if not os.path.exists(output_path.strip()):
# cstr(f'The path `{output_path.strip()}` specified doesn\'t exist! Creating directory.').warning.print()
# os.makedirs(output_path, exist_ok=True)
# # Find existing counter values
# if filename_number_start == 'true':
# pattern = f"(\\d{{{filename_number_padding}}}){re.escape(delimiter)}{re.escape(filename_prefix)}"
# else:
# pattern = f"{re.escape(filename_prefix)}{re.escape(delimiter)}(\\d{{{filename_number_padding}}})"
# existing_counters = [
# int(re.search(pattern, filename).group(1))
# for filename in os.listdir(output_path)
# if re.match(pattern, os.path.basename(filename))
# ]
# existing_counters.sort(reverse=True)
# # Set initial counter value
# if existing_counters:
# counter = existing_counters[0] + 1
# else:
# counter = 1
# # Set initial counter value
# if existing_counters:
# counter = existing_counters[0] + 1
# else:
# counter = 1
# # Set Extension
# file_extension = '.' + extension
# if file_extension not in ALLOWED_EXT:
# cstr(f"The extension `{extension}` is not valid. The valid formats are: {', '.join(sorted(ALLOWED_EXT))}").error.print()
# file_extension = "png"
# results = list()
# for image in images:
# i = 255. * image.cpu().numpy()
# img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
# metadata = PngInfo()
# if embed_workflow == 'true':
# if prompt is not None:
# metadata.add_text("prompt", json.dumps(prompt))
# if extra_pnginfo is not None:
# for x in extra_pnginfo:
# metadata.add_text(x, json.dumps(extra_pnginfo[x]))
# if overwrite_mode == 'prefix_as_filename':
# file = f"{filename_prefix}{file_extension}"
# else:
# if filename_number_start == 'true':
# file = f"{counter:0{number_padding}}{delimiter}{filename_prefix}{file_extension}"
# else:
# file = f"{filename_prefix}{delimiter}{counter:0{number_padding}}{file_extension}"
# if os.path.exists(os.path.join(output_path, file)):
# counter += 1
# try:
# output_file = os.path.abspath(os.path.join(output_path, file))
# if extension == 'png':
# img.save(output_file,
# pnginfo=metadata, optimize=True)
# elif extension == 'webp':
# img.save(output_file, quality=quality)
# elif extension == 'jpeg':
# img.save(output_file,
# quality=quality, optimize=True)
# elif extension == 'tiff':
# img.save(output_file,
# quality=quality, optimize=True)
# elif extension == 'webp':
# img.save(output_file, quality=quality,
# lossless=lossless_webp, exif=metadata)
# cstr(f"Image file saved to: {output_file}").msg.print()
# if show_history != 'true' and show_previews == 'true':
# subfolder = self.get_subfolder_path(output_file, original_output)
# results.append({
# "filename": file,
# "subfolder": subfolder,
# "type": self.type
# })
# # Update the output image history
# update_history_output_images(output_file)
# except OSError as e:
# cstr(f'Unable to save file to: {output_file}').error.print()
# print(e)
# except Exception as e:
# cstr('Unable to save file due to the to the following error:').error.print()
# print(e)
# if overwrite_mode == 'false':
# counter += 1
# filtered_paths = []
# if show_history == 'true' and show_previews == 'true':
# HDB = WASDatabase(WAS_HISTORY_DATABASE)
# conf = getSuiteConfig()
# if HDB.catExists("History") and HDB.keyExists("History", "Output_Images"):
# history_paths = HDB.get("History", "Output_Images")
# else:
# history_paths = None
# if history_paths:
# for image_path in history_paths:
# image_subdir = self.get_subfolder_path(image_path, self.output_dir)
# current_subdir = self.get_subfolder_path(output_file, self.output_dir)
# if not os.path.exists(image_path):
# continue
# if show_history_by_prefix == 'true' and image_subdir != current_subdir:
# continue
# if show_history_by_prefix == 'true' and not os.path.basename(image_path).startswith(filename_prefix):
# continue
# filtered_paths.append(image_path)
# if conf.__contains__('history_display_limit'):
# filtered_paths = filtered_paths[-conf['history_display_limit']:]
# filtered_paths.reverse()
# if filtered_paths:
# for image_path in filtered_paths:
# subfolder = self.get_subfolder_path(image_path, self.output_dir)
# image_data = {
# "filename": os.path.basename(image_path),
# "subfolder": subfolder,
# "type": self.type
# }
# results.append(image_data)
# if show_previews == 'true':
# return {"ui": {"images": results}}
# else:
# return {"ui": {"images": []}}
# def get_subfolder_path(self, image_path, output_path):
# output_parts = output_path.strip(os.sep).split(os.sep)
# image_parts = image_path.strip(os.sep).split(os.sep)
# common_parts = os.path.commonprefix([output_parts, image_parts])
# subfolder_parts = image_parts[len(common_parts):]
# subfolder_path = os.sep.join(subfolder_parts[:-1])
# return subfolder_path
"""python-list-util"""
def list_del_exculde(start,oldlist,exclist,newlist):
if start==len(oldlist):return newlist #base condition
if not(exclist[start] in oldlist): #checking if
newlist.append(oldlist[start])
return list_del_exculde(start+1,oldlist,exclist,newlist)
def list_fill_one(t,n=20):
return [t] * n
def list_2_tupe(l):
return tuple(l)
def list_shuffle(l:list,keep_n=0):
"""
list shuffle with keep n token
return a new list
"""
el=l.copy()
index=keep_n
if index >=1:
el=l[index:]
random.shuffle(el)
if index >=1:
el=l[0:index]+el
return el
"""cutoff-region-util"""
# cutoff region util
class CutoffRegionUtil:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"main_text": ("STRING", {"default": "dress", "multiline": True}),
"target_text":("STRING", {"default": "white", "multiline": False}),
"weight": ("STRING", {"default": "1.0", "multiline": False}),
}
}
RETURN_TYPES = ("STRING","STRING","STRING",)
RETURN_NAMES = ('region_text','target_text','prompt_weight')
FUNCTION = "exec"
CATEGORY = "Ymc Suite/cutoff-region-util"
OUTPUT_NODE = True
def exec(self, main_text,target_text,weight):
# Converted inputs are sent as the string of 'undefined' if not connected
if target_text == "undefined":
target_text = ""
if main_text == "undefined":
main_text = ""
region_text = ""
if weight is None or weight=='' or weight=='1.0':
region_text = (" ").join(filter(None, [target_text, main_text]))
else:
# region_text = (" ").join(filter(None, ["(",target_text, main_text,":",weight,")"]))
region_text = (" ").join(filter(None, [target_text, main_text]))
region_text = ("").join(filter(None, ["(",region_text,":",weight,")"]))
# return {"ui": {"text": (region_text,)}, "result": (region_text,)}
return region_text,target_text,weight
# cutoff region util
TEXT_BASE_COLORS=['red','orange','yellow','green','cyan','blue','purple','black','white','pink','custom']
TEXT_BASE_COLORS_COUNT=len(TEXT_BASE_COLORS)
class CutoffRegionUtilColorTextList:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"custom": ("STRING", {"default": "red", "multiline": False})
}
}
# RETURN_TYPES = ("STRING","STRING","STRING","STRING","STRING","STRING","STRING","STRING","STRING","STRING","STRING",)
RETURN_TYPES = list_2_tupe(list_fill_one('STRING',TEXT_BASE_COLORS_COUNT))
RETURN_NAMES = list_2_tupe(TEXT_BASE_COLORS)
FUNCTION = "exec"
CATEGORY = "Ymc Suite/cutoff-region-util"
OUTPUT_NODE = True
def exec(self, custom):
# Converted inputs are sent as the string of 'undefined' if not connected
if custom == "undefined":
custom = "sliver"
# return red,orange,yellow,green,cyan,blue,purple,black,white,pink,custom
TEXT_BASE_COLORS_VALUE = TEXT_BASE_COLORS.copy()
TEXT_BASE_COLORS_VALUE[TEXT_BASE_COLORS_COUNT-1]=custom
return list_2_tupe(TEXT_BASE_COLORS_VALUE)
TEXT_LIST_RETURN_NAMES=['a','b','c','d','e','f','g']
TEXT_LIST_COUNT=len(TEXT_LIST_RETURN_NAMES)
class TextUtilPathList:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"a": ("STRING",{"forceInput":False}),
"b": ("STRING",{"forceInput":False}),
"c": ("STRING",{"forceInput":False}),
"d": ("STRING",{"forceInput":False}),
"e": ("STRING",{"forceInput":False}),
"f": ("STRING",{"forceInput":False}),
"g": ("STRING",{"forceInput":False}),
}
}
RETURN_TYPES = list_2_tupe(list_fill_one('STRING',TEXT_LIST_COUNT))
RETURN_NAMES = list_2_tupe(TEXT_LIST_RETURN_NAMES)
FUNCTION = "exec"
CATEGORY = "Ymc Suite/text-util"
OUTPUT_NODE = True
def exec(self, a,b,c,d,e,f,g):
return list_2_tupe([a,b,c,d,e,f,g])
# "text": (TEXT_TYPE, {"forceInput": (True if TEXT_TYPE == 'STRING' else False)}),
class TextUtilJoinText:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"splitchar": ("STRING", {"default": "_", "multiline": False}),
"a": ("STRING",{"forceInput":False}),
"b": ("STRING",{"forceInput":False}),
"c": ("STRING",{"forceInput":False}),
"d": ("STRING",{"forceInput":False}),
"e": ("STRING",{"forceInput":False}),
"f": ("STRING",{"forceInput":False}),
"g": ("STRING",{"forceInput":False}),
}
}
RETURN_TYPES = list_2_tupe(list_fill_one('STRING',1))
RETURN_NAMES = list_2_tupe(['text'])
FUNCTION = "exec"
CATEGORY = "Ymc Suite/text-util"
OUTPUT_NODE = True
def exec(self, splitchar,a,b,c,d,e,f,g):
ls=[a,b,c,d,e,f,g]
ls = list(filter(None, ls))
joinedtxt=splitchar.join(ls)
return list_2_tupe([joinedtxt])
class TextUtilRandomText:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"text": ("STRING", {"default": '', "multiline": False}),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
"char": ("STRING", {"default": '|', "multiline": False}),
}
}
RETURN_TYPES = ("STRING",'INT',)
RETURN_NAMES = list_2_tupe(['text','seed'])
FUNCTION = "exec"
CATEGORY = "Ymc Suite/text-util"
def exec(self, text, seed,char='|'):
iteml = text.split(char)
random.seed(seed)
choice = random.choice(iteml)
return list_2_tupe([choice,seed])
@classmethod
def IS_CHANGED(cls, **kwargs):
return float("NaN")
class TextUtilLoopText:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"text": ("STRING", {"default": '', "multiline": False}),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
"char": ("STRING", {"default": '|', "multiline": False}),
}
}
RETURN_TYPES = ("STRING",'INT','STRING')
RETURN_NAMES = list_2_tupe(['text','seed','index'])
FUNCTION = "exec"
CATEGORY = "Ymc Suite/text-util"
def exec(self, text, seed,char='|'):
iteml = text.split(char)
random.seed(seed)
# get random index in range
# index = random.randint(0, len(iteml)-1)
# get loop index with de/cr seed
length=len(iteml)
maxindex=(length-1)
minindex=0
index=minindex
if seed == 0:
index=0
else:
if maxindex > seed:
index=seed
else:
index = seed % maxindex
if index <=0:
index=0
if index >length:
index=maxindex
choice = iteml[index]
# print(maxindex,seed,index,choice)
return list_2_tupe([choice,seed,str(index)])
@classmethod
def IS_CHANGED(cls, **kwargs):
return float("NaN")
class TextUtilSearchText:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"text": ("STRING", {"default": '', "multiline": False}),
"find": ("STRING", {"default": '', "multiline": False}),
"replace": ("STRING", {"default": '', "multiline": False}),
"action": (["yes",'no'],),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = list_2_tupe(['text'])
FUNCTION = "exec"
CATEGORY = "Ymc Suite/text-util"
def exec(self, text, find, replace,action='yes'):
if 'no' in action:
return list_2_tupe([text])
return (self.replace_substring(text, find, replace), )
def replace_substring(self, text, find, replace):
import re
text = re.sub(find, replace, text)
return text
class TextUtilSwitchText:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"a": ("STRING", {"default": '', "multiline": False}),
"b": ("STRING", {"default": '', "multiline": False}),
"boolean_number": ("NUMBER",),
}
}
RETURN_TYPES = ('STRING',)
FUNCTION = "exec"
CATEGORY = "Ymc Suite/text-util"
def exec(self, a='', b='', boolean_number=1):
if int(round(boolean_number)) == 1:
return (a, )
else:
return (b, )
# D:\code\python\comfyui\nodes.py
# class ImageInvert:
# @classmethod
# def INPUT_TYPES(s):
# return {"required": { "image": ("IMAGE",)}}
# RETURN_TYPES = ("IMAGE",)
# FUNCTION = "invert"
# CATEGORY = "image"
# def invert(self, image):
# s = 1.0 - image
# return (s,)
"""region-util"""
def region_strify(l:list):
tl=list(map(str,l))
tl=list(filter(None, tl))
return ','.join(tl)
def region_listfy(s:str):
tl=s.split(",")
tl=list(filter(None, tl))
return tl
def region_intify(l:list):
tl=list(map(int,l))
return tl
def region_get_location_from_center_size(cx=128, cy=128, w=256, h=256):
left=cx-w/2
top=cy-h/2
right=cx+w/2
bottom=cy+h/2
floatl=[left,top,right,bottom,cx,cy,w,h]
intl=list(map(int,floatl))
return intl
REGION_GET_CROP_LOCATION_BY_CENTER_RETURN_NAMES=['left','top','right','bottom','cx','cy','w','h']
REGION_GET_CROP_LOCATION_BY_CENTER_RETURN_NAMES_COUNT=len(REGION_GET_CROP_LOCATION_BY_CENTER_RETURN_NAMES)
class RegionUtilGetCropLocationByCenter:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"cx": ("INT", {"default":128, "max": 10000000, "min":0, "step":1}),
"cy": ("INT", {"default":128, "max": 10000000, "min":0, "step":1}),
"w": ("INT", {"default":256, "max": 10000000, "min":0, "step":1}),
"h": ("INT", {"default":256, "max": 10000000, "min":0, "step":1}),
}
}
RETURN_TYPES = list_2_tupe(list_fill_one('INT',REGION_GET_CROP_LOCATION_BY_CENTER_RETURN_NAMES_COUNT))
RETURN_NAMES = list_2_tupe(REGION_GET_CROP_LOCATION_BY_CENTER_RETURN_NAMES)
FUNCTION = "exec"
CATEGORY = "Ymc Suite/region-util"
OUTPUT_NODE = True
def exec(self, cx=128, cy=128, w=256, h=256):
intl=region_get_location_from_center_size(cx,cy,w,h)
return list_2_tupe(intl)
class RegionUtilGetCropLocationFromCenterSizeText:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"text": ("STRING", {"default": '128,128,256,256', "multiline": False}),
}
}
RETURN_TYPES = list_2_tupe(list_fill_one('INT',REGION_GET_CROP_LOCATION_BY_CENTER_RETURN_NAMES_COUNT))
RETURN_NAMES = list_2_tupe(REGION_GET_CROP_LOCATION_BY_CENTER_RETURN_NAMES)
FUNCTION = "exec"
CATEGORY = "Ymc Suite/region-util"
OUTPUT_NODE = True
def exec(self, text='128,128,256,256'):
tl=region_intify(region_listfy(text))
cx,cy,w,h=tl
intl=region_get_location_from_center_size(cx,cy,w,h)
return list_2_tupe(intl)
REGION_GET_CROP_LOCATION_BY_LT_RETURN_NAMES=['left','top','right','bottom','cx','cy','w','h']
REGION_GET_CROP_LOCATION_BY_LT_RETURN_NAMES_COUNT=len(REGION_GET_CROP_LOCATION_BY_LT_RETURN_NAMES)
class RegionUtilGetCropLocationByLT:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"left": ("INT", {"default":0, "max": 10000000, "min":0, "step":1}),
"top": ("INT", {"default":0, "max": 10000000, "min":0, "step":1}),
"w": ("INT", {"default":256, "max": 10000000, "min":0, "step":1}),
"h": ("INT", {"default":256, "max": 10000000, "min":0, "step":1}),
}
}
RETURN_TYPES = list_2_tupe(list_fill_one('INT',REGION_GET_CROP_LOCATION_BY_LT_RETURN_NAMES_COUNT))
RETURN_NAMES = list_2_tupe(REGION_GET_CROP_LOCATION_BY_LT_RETURN_NAMES)
FUNCTION = "exec"
CATEGORY = "Ymc Suite/region-util"
OUTPUT_NODE = True
def exec(self, left=0, top=0, w=256, h=256):
cx=left+w/2
cy=top+h/2
right=cx+w/2
bottom=cy+h/2
floatl=[left,top,right,bottom,cx,cy,w,h]
intl=list(map(int,floatl))
return list_2_tupe(intl)
REGION_LOCATION_RETURN_NAMES=['left','top','right','bottom']
class RegionUtilGetPadOutLocationBySize:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"w": ("INT", {"default":256, "max": 10000000, "min":0, "step":1}),
"h": ("INT", {"default":256, "max": 10000000, "min":0, "step":1}),
}
}
RETURN_TYPES = list_2_tupe(list_fill_one('INT',len(REGION_LOCATION_RETURN_NAMES)))
RETURN_NAMES = list_2_tupe(REGION_LOCATION_RETURN_NAMES)
FUNCTION = "exec"
CATEGORY = "Ymc Suite/region-util"
OUTPUT_NODE = True
def exec(self, w=256, h=256):
c=0
if w<h:
c=1
left=-1*c*(w-h)/2
right=left
c=0
if w>=h:
c=1
top=c*(w-h)/2
bottom=top
floatl=[left,top,right,bottom]
intl=list(map(int,floatl))
return list_2_tupe(intl)
"""canvas-util"""
class CanvasUtilCalSize:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"w": ("INT", {"default":512, "max": 10000000, "min":0, "step":1}),
"h": ("INT", {"default":512, "max": 10000000, "min":0, "step":1}),
"lockoff": (["false", "w","h"],),
# "rate": (["1:1", "2:3","9:16"],),
"rate": ("STRING", {"default": "1:1", "multiline": False}),
"reverse": (["false", "true"],),
}
}
RETURN_TYPES = list_2_tupe(list_fill_one('INT',2))
RETURN_NAMES = list_2_tupe(['w','h'])
FUNCTION = "exec"
CATEGORY = "Ymc Suite/canvas-util"
OUTPUT_NODE = True
def exec(self,w=512, h=512,lockoff='false',rate='1:1',reverse='false'):
rw=w
rh=h
# get h or w with lockoff
if lockoff != "false":
ratel=list(map(int,rate.split(":")))
raten=(ratel[0]/ratel[1])
if lockoff == "w":
rh=int(w/raten)
if lockoff == "h":
rw=int(h*raten)
floatl=[rw,rh]
# reverse result of w h
if reverse == "true":
floatl=[rh,rw]
intl=list(map(int,floatl))
return list_2_tupe(intl)
"""hks-util"""
class HksUtilCalDenoiseStep:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"steps": ("INT", {"default":20, "max": 50, "min":0, "step":1}),
"denoise": ("FLOAT", {"min": 0, "max": 1, "step": 0.05, "default": 0.3}),
}
}
RETURN_TYPES = list_2_tupe(list_fill_one('INT',2))
RETURN_NAMES = list_2_tupe(['steps','step_at_start'])
FUNCTION = "exec"
CATEGORY = "Ymc Suite/hks-util"
OUTPUT_NODE = True
def exec(self,steps=20, denoise=0.3):
floatl=[steps,steps*(1-denoise)]
intl=list(map(int,floatl))
return list_2_tupe(intl)
# text-util
def tags_listify(s:str):
ls = s.split(",")
ls = list(filter(None, ls))
return ls
def tags_strify(l:list[str]):
return ','.join(l)
def tag_trim(s:str):
trimedtag=s
trimedtag=re.sub(r'^ *', '', trimedtag,0)
trimedtag=re.sub(r' *$', '', trimedtag,0)
return trimedtag
def tag_ms2os(s:str):
return re.sub(re.compile(r' {1,}'), ' ', s,0)
def tag_unweight(s:str):
# unweight,trim,
unwdtag=s.replace("(","").replace(")","")
unwdtag=re.sub(r':\d.*', '', unwdtag,0)
unwdtag=tag_trim(unwdtag)
unwdtag=tag_ms2os(unwdtag)
return unwdtag
def tags_unweight(s:str):
ls=tags_listify(s)
nls=[]
for tag in ls:
stdedtag=tag_unweight(tag)
if stdedtag !="":
nls.append(stdedtag)
return ",".join(nls)
def tags_onelineify(s:str):
return re.sub(r'\n', ',', s,0)
def tag_std(s:str):
# trim,ms2os
stdedtag=s
stdedtag=tag_trim(stdedtag)
stdedtag=tag_ms2os(stdedtag)
return stdedtag
def tags_dup(s:str):
nls=tags_listify(s)
nls=list(dict.fromkeys(nls))
return ",".join(nls)
def tags_std(s:str):
ls=tags_listify(s)
nls=[]
for tag in ls:
stdedtag=tag_std(tag)
if stdedtag !="":
nls.append(stdedtag)
return ",".join(nls)
def tags_ignore(s:str,i:str):
ls=tags_listify(s)
li=tags_listify(i)
nls=[]
for tag in ls:
stdedtag=tag_unweight(tag)
if stdedtag !="" and not(stdedtag in li):
nls.append(tag)
return ",".join(nls)
def tags_replace(s:str,i:str,r:str):
ls=tags_listify(s)
li=tags_listify(i)
# lr=tags_listify(r)
nls=[]
for tag in ls:
stdedtag=tag_unweight(tag)
if stdedtag !="":
if stdedtag in li:
# replace found with r
nls.append(r)
else:
nls.append(tag)
return ",".join(nls)
def tags_search(s:str,i:str):
ls=tags_listify(s)
li=tags_listify(i)
nls=[]
for tag in ls:
stdedtag=tag_unweight(tag)
if stdedtag !="" :
for pattern in li:
if pattern in stdedtag:
nls.append(tag)
return ",".join(nls)
def tags_head(s:str,i:str):
"""
move some tags to head
"""
head=tags_listify(i)
tails=tags_listify(tags_ignore(s,i))
return ",".join(head+tails)
def tags_tail(s:str,i:str):
"""
move some tags to head
"""
tails=tags_listify(i)
head=tags_listify(tags_ignore(s,i))
return ",".join(head+tails)
class TextUtilPromptAdd:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"text": ("STRING",{"forceInput":False}),
"toadd": ("STRING",{"forceInput":False}),
"action": (["add2head",'add2tail','del|add2head','del|add2tail','none'],),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ('STRING',)
FUNCTION = "exec"
CATEGORY = "Ymc Suite/text-util"
OUTPUT_NODE = True
def exec(self, text='',toadd='',action='add2head'):
# if action =="add2head":
# return list_2_tupe([tags_head(text,prev)])
# if action =="add2tail":
# return list_2_tupe([tags_tail(text,prev)])
if 'none' in action:
return list_2_tupe([text])
prev=toadd
if 'del' in action:
prev=tags_ignore(text,toadd)
if 'add2head' in action:
prev=tags_head(text,prev)
if 'add2tail' in action:
prev=tags_tail(text,prev)
return list_2_tupe([prev])
class TextUtilPromptAdvSearch:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"text": ("STRING",{"forceInput":False}),
"search": ("STRING",{"forceInput":False}),
"action": (['none',"del", "move2head",'move2tail',"replace","search"],),
"replace": ("STRING",{"forceInput":False}),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ('STRING',)
FUNCTION = "exec"
CATEGORY = "Ymc Suite/text-util"
OUTPUT_NODE = True
def exec(self, text='',search='',action='none',replace=''):
if action == "none":
return list_2_tupe([text])
if action == "search":
return list_2_tupe([tags_search(text,search)])
searchedtags=tags_search(text,search)
if action =="replace":
return list_2_tupe([tags_replace(text,searchedtags,replace)])
if action =="del":
return list_2_tupe([tags_ignore(text,searchedtags)])
if action =="move2head":
return list_2_tupe([tags_head(text,searchedtags)])
if action =="move2tail":
return list_2_tupe([tags_tail(text,searchedtags)])
class TextUtilPromptSearch:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"text": ("STRING",{"forceInput":False}),
"search": ("STRING",{"forceInput":False}),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ('STRING',)
FUNCTION = "exec"
CATEGORY = "Ymc Suite/text-util"
OUTPUT_NODE = True
def exec(self, text='',search=''):
return list_2_tupe([tags_search(text,search)])
class TextUtilPromptDup:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"text": ("STRING",{"forceInput":False}),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ('STRING',)
FUNCTION = "exec"
CATEGORY = "Ymc Suite/text-util"
OUTPUT_NODE = True
def exec(self, text):
return list_2_tupe([tags_dup(text)])
# tags_onelineify
class TextUtilPromptAdvDup:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"text": ("STRING",{"forceInput":False}),
# "mode": (["alline", "in-each-line"],),
}
}
RETURN_TYPES = ("STRING","STRING",)
RETURN_NAMES = ('STRING',"STRING",)
FUNCTION = "exec"
CATEGORY = "Ymc Suite/text-util"
OUTPUT_NODE = True
def exec(self, text):
a=''
res=[]
textl=text.split("\n")
for line in textl:
res.append(tags_dup(line))
a='\n'.join(res)
tags_dup(tags_onelineify(text))
return list_2_tupe([tags_dup(tags_onelineify(a)),a])
class TextUtilPromptShuffle:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"text": ("STRING",{"forceInput":False}),
"keep_n_token": ("STRING", {"default": "0", "multiline": False}),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ('STRING',)
FUNCTION = "exec"
CATEGORY = "Ymc Suite/text-util"
OUTPUT_NODE = True
def exec(self, text='',keep_n_token='0',seed=0):
random.seed(seed)
textl=tags_listify(tags_onelineify(text))
el=list_shuffle(textl,int(keep_n_token))
return list_2_tupe([','.join(el)])
class TextUtilPromptDel:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"search": ("STRING",{"forceInput":False}),
"todel": ("STRING",{"forceInput":False}),
"action": (["yes",'no'],),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ('STRING',)
FUNCTION = "exec"
CATEGORY = "Ymc Suite/text-util"
OUTPUT_NODE = True
def exec(self, search='',todel='',action='yes'):
# https://www.geeksforgeeks.org/python-remove-empty-strings-from-list-of-strings/
if 'no' in action:
return list_2_tupe([search])
ca=tags_ignore(search,tags_unweight(todel))
return list_2_tupe([ca])
class TextUtilPromptStd:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"text": ("STRING",{"forceInput":False}),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ('STRING',)
FUNCTION = "exec"
CATEGORY = "Ymc Suite/text-util"
OUTPUT_NODE = True
def exec(self, text):
return list_2_tupe([tags_std(text)])
class TextUtilPromptUnweight:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"text": ("STRING",{"forceInput":False}),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ('STRING',)
FUNCTION = "exec"
CATEGORY = "Ymc Suite/text-util"
OUTPUT_NODE = True
def exec(self, text):
return list_2_tupe([tags_unweight(text)])
class TextUtilPromptJoin:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"a": ("STRING",{"forceInput":False}),
"b": ("STRING",{"forceInput":False}),
"c": ("STRING",{"forceInput":False}),
"d": ("STRING",{"forceInput":False}),
"e": ("STRING",{"forceInput":False}),
"f": ("STRING",{"forceInput":False}),
"g": ("STRING",{"forceInput":False}),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ('STRING',)
FUNCTION = "exec"
CATEGORY = "Ymc Suite/text-util"
OUTPUT_NODE = True
def exec(self, a,b,c,d,e,f,g):
vl=[a,b,c,d,e,f,g]
vs = (",").join(vl)
vs=tags_std(vs)
# vs = vs.replace(" ", " ").replace(" ,", ",").replace(", ", ",").replace(",,", ",").replace(",,", ",")
return {"ui": {"text": (vs,)}, "result": (vs,)}
# class TextUtilTextShow:
# @classmethod
# def INPUT_TYPES(s):
# return {"required": {
# "text": ("STRING", {"forceInput": True}),
# }}
# # INPUT_IS_LIST = True
# RETURN_TYPES = ("STRING",)
# FUNCTION = "notify"
# OUTPUT_NODE = True
# # OUTPUT_IS_LIST = (True,)
# CATEGORY = "Ymc Suite/text-util"
# def notify(self, text):
# return {"ui": {"text": text}, "result": (text,)}
class NumberUtilRandomNum:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"number_type": (["integer", "float", "bool"],),
"minimum": ("FLOAT", {"default": 0, "min": -18446744073709551615, "max": 18446744073709551615}),
"maximum": ("FLOAT", {"default": 0, "min": -18446744073709551615, "max": 18446744073709551615}),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
}
}
RETURN_TYPES = ("NUMBER", "FLOAT", "INT")
FUNCTION = "exec"
CATEGORY = "WAS Suite/Number"
def exec(self, minimum, maximum, seed, number_type='integer'):
# Set Generator Seed
random.seed(seed)
# Return random number
if number_type:
if number_type == 'integer':
number = random.randint(minimum, maximum)
elif number_type == 'float':
number = random.uniform(minimum, maximum)
elif number_type == 'bool':
number = random.random()
else:
return
# Return number
return (number, float(number), int(number))
@classmethod
def IS_CHANGED(cls, seed, **kwargs):
m = hashlib.sha256()
m.update(seed)
return m.digest().hex()
def io_file_list(dir='',pattern='*.txt'):
res=[]
for filename in glob.glob(os.path.join(dir,pattern)):
res.append(filename)
return res
def io_file_get_txt(loc):
res=''
with open(loc) as f:
res = f.read()
f.close()
return res
class IoUtilFileListGet:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"path": ("STRING", {"default": "", "multiline": False}),
"pattern": ("STRING", {"default": "*.txt", "multiline": False}),
# "mode": ("STRING", {"default": "filename", "multiline": False}),
"mode": (["filename", "abspath"],),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ('STRING',)
FUNCTION = "exec"
CATEGORY = "Ymc Suite/IO"
OUTPUT_NODE = True
def exec(self, path='',pattern='*.txt',mode='filename'):
text=io_file_list(path,pattern)
if mode=='filename':
text=list(map(os.path.basename,text))
text='\n'.join(text)
return list_2_tupe([text])
class IoUtilFileListGetText:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"path": ("STRING", {"default": "", "multiline": False}),
"pattern": ("STRING", {"default": "*.txt", "multiline": False}),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ('STRING',)
FUNCTION = "exec"
CATEGORY = "Ymc Suite/IO"
OUTPUT_NODE = True
def exec(self, path='',pattern='*.txt'):
plist=io_file_list(path,pattern)
text=[]
for loc in plist:
text.append(io_file_get_txt(loc))
# to multi line
text='\n'.join(text)
return list_2_tupe([text])
"""pipe-util"""
class PipeUtilToBasicPipe:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"model": ("MODEL",),
"clip": ("CLIP",),
"vae": ("VAE",),
"positive": ("CONDITIONING",),
"negative": ("CONDITIONING",),
},
}
RETURN_TYPES = ("BASIC_PIPE", "MODEL", "CLIP", "VAE", "CONDITIONING", "CONDITIONING")
RETURN_NAMES = ("basic_pipe", "model", "clip", "vae", "positive", "negative")
FUNCTION = "doit"
CATEGORY = "Ymc Suite/pipe-util"
def doit(self, model, clip, vae, positive, negative):
basic_pipe = (model, clip, vae, positive, negative)
return basic_pipe,model, clip, vae, positive, negative
"""img-util"""
class ImgUtilGetImageSize:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
}
}
RETURN_TYPES = ("INT", "INT")
RETURN_NAMES = ("width", "height")
FUNCTION = "get_size"
CATEGORY = "Ymc Suite/img-util"
def get_size(self, image):
_, height, width, _ = image.shape
return (width, height)
class ImgUtilSwitchInputImage:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image_a": ("IMAGE",),
"image_b": ("IMAGE",),
"boolean_number": ("NUMBER",),
}
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "image_input_switch"
CATEGORY = "Ymc Suite/img-util"
def image_input_switch(self, image_a, image_b, boolean_number=1):
if int(round(boolean_number)) == 1:
return (image_a, )
else:
return (image_b, )
"""conditioning-util"""
class ConditioningUtilInputSwitch:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"conditioning_a": ("CONDITIONING",),
"conditioning_b": ("CONDITIONING",),
"boolean_number": ("NUMBER",),
}
}
RETURN_TYPES = ("CONDITIONING",)
FUNCTION = "conditioning_input_switch"
CATEGORY = "Ymc Suite/conditioning-util"
def conditioning_input_switch(self, conditioning_a, conditioning_b, boolean_number=1):
if int(round(boolean_number)) == 1:
return (conditioning_a, )
else:
return (conditioning_b, )
"""xyz-util"""
re_int = re.compile(r"\s*([+-]?\s*\d+)\s*")
re_range = re.compile(r"\s*([+-]?\s*\d+)\s*-\s*([+-]?\s*\d+)(?:\s*\(([+-]\d+)\s*\))?\s*")
re_range2 = re.compile(r"\s*([+-]?\s*\d+)\s*-\s*([+-]?\s*\d+)(?:\s*\[([+-]\d+)\s*\])?\s*")
re_float = re.compile(r"\s*([+-]?\s*\d+(?:.\d*)?)\s*")
re_range_float = re.compile(r"\s*([+-]?\s*\d+(?:.\d*)?)\s*-\s*([+-]?\s*\d+(?:.\d*)?)(?:\s*\(([+-]\d+(?:.\d*)?)\s*\))?\s*")
re_range_float2 = re.compile(r"\s*([+-]?\s*\d+(?:.\d*)?)\s*-\s*([+-]?\s*\d+(?:.\d*)?)(?:\s*\[([+-]\d+)\s*\])?\s*")
def xyz_generate_floats(batch_count, first_float, last_float):
if batch_count > 1:
interval = (last_float - first_float) / (batch_count - 1)
return [round(first_float + i * interval, 3) for i in range(batch_count)]
else:
return [first_float] if batch_count == 1 else []
def xyz_generate_ints(batch_count, first_int, last_int):
if batch_count > 1:
interval = (last_int - first_int) / (batch_count - 1)
values = [int(first_int + i * interval) for i in range(batch_count)]
else:
values = [first_int] if batch_count == 1 else []
values = list(set(values)) # Remove duplicates
values.sort() # Sort in ascending order
return values
def xyz_gen_frange(start, end, step):
x = float(start)
end = float(end+0.1)
step = float(step)
while x < end:
yield x
x += step
def xyz_parse_int(input: str):
m = re_int.fullmatch(input)
if m is not None:
return int(m.group(1))
m = re_range.fullmatch(input)
if m is not None:
start, end, step = m.group(1), m.group(2), m.group(3)
if step is None:
step = 1
return list(range(int(start), int(end) + 1, int(step)))
m = re_range2.fullmatch(input)
if m is not None:
start, end, count = m.group(1), m.group(2), m.group(3)
if count is None:
count = 1
return xyz_generate_ints(int(count),int(start),int(end))
if m is None:
raise ValueError(f'failed to process: {input}')
def xyz_parse_float(input: str):
m = re_float.fullmatch(input)
if m is not None:
return float(m.group(1))
m = re_range_float.fullmatch(input)
if m is not None:
start, end, step = m.group(1), m.group(2), m.group(3)
if step is None:
step = 1.0
return list(xyz_gen_frange(float(start), float(end), float(step)))
m = re_range_float2.fullmatch(input)
if m is not None:
start, end, count = m.group(1), m.group(2), m.group(3)
if count is None:
count = 1
return xyz_generate_floats(int(count),float(start),float(end))
if m is None:
raise ValueError(f'failed to process: {input}')
def xyz_parse(input: str, cont: Union[Callable[[str],Any],None]):
vs = [ x.strip() for x in input.split(',') ]
if cont is not None:
new_vs = []
for v in vs:
new_v = cont(v)
if isinstance(new_v, list):
new_vs += new_v
else:
new_vs.append(new_v)
vs = new_vs
return vs
def xyz_float2str(v,):
return '{:.2f}'.format(v)
def xyz_get_n_float(s,n=0):
fstr=str(s)
a,b,c=fstr.partition('.')
if n==0:
return ''.join([a])
c=(c+"0"*n)[:n]
return ".".join([a,c])
class XyzUtilKVTxtToOther:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"v": ("STRING", {"default": "", "multiline": False}),
# "name": ("STRING", {"default": "steps", "multiline": False}),
"k": ("STRING", {"default": "Steps", "multiline": False}),
"namepreset": (["none","no-shortname","empty-shortname","Steps", "CFG Scale","Denoise"],),
"shortname": ("STRING", {"default": "", "multiline": False}),
# "keepndecimal": ("STRING", {"default": "0", "multiline": False}),
"keepndecimal": ("INT", {"default": 1, "min": 0, "max": 8}),
}
}
# RETURN_TYPES = ("INT","INT","INT","STRING","XY")
# RETURN_NAMES = ('s','e','count','gridAnnotion','X or Y')
RETURN_TYPES = ("INT","STRING","XY")
RETURN_NAMES = ('count','row of col text','X or Y')
FUNCTION = "exec"
CATEGORY = "Ymc Suite/xyz-util"
OUTPUT_NODE = True
def exec(self, v='',k='Steps',namepreset='none',shortname='',keepndecimal=1):
intkl=['Steps','Clip Skip','custom']
floatkl=['CFG Scale','Denoise',"custom-float"]
# use name preset
name=namepreset if not (namepreset in ['none','no-shortname',"empty-shortname"]) else k
# name="Steps" if name=='' else name
txtl=[]
il=[]
tl=[]
if name in intkl:
tl=xyz_parse(v,xyz_parse_int)
txtl=list(map(str,tl))
il=list(map(int,tl))
if name in floatkl:
tl=xyz_parse(v,xyz_parse_float)
txtl=list(map(str,tl))
#
# if keepndecimal!='':
# dc=int(keepndecimal)
if keepndecimal>0:
for index,value in enumerate(txtl):
txtl[index]=xyz_get_n_float(value,keepndecimal)
il=list(map(float,txtl))
print(tl)
print(il)
print(txtl)
# gen x or y key for image grid
kname=name
if shortname !="" and not(namepreset in ['none','no-shortname','empty-shortname']):
kname=shortname
if (namepreset in ["empty-shortname"]):
kname=""
if kname !="":
kname=kname+':'
rowOfColTxt=''+kname+';'.join(txtl)
# s=il[0]
# e=il[-1]
count=len(il)
dset=(name, il)
# return list_2_tupe([s,e,count,gridAnnotion,dset])
return list_2_tupe([count,rowOfColTxt,dset])
# class XyzUtilTxtToFloat:
# @classmethod
# def INPUT_TYPES(s):
# return {
# "required": {
# "text": ("STRING", {"default": "", "multiline": False}),
# "name": ("STRING", {"default": "steps", "multiline": False}),
# }
# }
# RETURN_TYPES = ("FLOAT","FLOAT","FLOAT","STRING","DICT")
# RETURN_NAMES = ('s','e','count','gridAnnotion','DICT')
# FUNCTION = "exec"
# CATEGORY = "Ymc Suite/xyz-util"
# OUTPUT_NODE = True
# def exec(self, text='',name='steps'):
# tl=xyz_parse(text,xyz_parse_float)
# il=list(map(float,tl.copy()))
# # tl=list(map(str,tl))
# gridAnnotion=name+':'+';'.join(tl)
# s=il[0]
# e=il[-1]
# count=len(il)
# dset=(name, il)
# return list_2_tupe([s,e,count,gridAnnotion,dset])
# feat(core): rename 'Save Text File' to io-text-save for issue 3
# https://github.com/YMC-GitHub/ymc-node-suite-comfyui/issues/3
# NODE MAPPING
NODE_CLASS_MAPPINGS = {
"io-text-save": WAS_Text_Save,
# "io-image-save": WAS_Image_Save,
"cutoff-region-util": CutoffRegionUtil,
"pipe-util-to-basic-pipe": PipeUtilToBasicPipe,
"canvas-util-cal-size": CanvasUtilCalSize,
# "img-util-get-image-size": ImgUtilGetImageSize,
# "img-util-switch-input-image": ImgUtilSwitchInputImage,
# "conditioning-util-input-switch": ConditioningUtilInputSwitch,
"hks-util-cal-denoise-step": HksUtilCalDenoiseStep,
"io-util-file-list-get": IoUtilFileListGet,
"io-util-file-list-get-text": IoUtilFileListGetText,
"text-preset-colors": CutoffRegionUtilColorTextList,
"text-util-prompt-join": TextUtilPromptJoin,
"text-util-prompt-del":TextUtilPromptDel,
"text-util-prompt-std":TextUtilPromptStd,
"text-util-prompt-unweight":TextUtilPromptUnweight,
"text-util-prompt-dup":TextUtilPromptDup,
"text-util-prompt-adv-dup":TextUtilPromptAdvDup,
"text-util-prompt-shuffle":TextUtilPromptShuffle,
"text-util-prompt-search":TextUtilPromptSearch,
"text-util-prompt-adv-search":TextUtilPromptAdvSearch,
"text-util-prompt-add-prompt":TextUtilPromptAdd,
"text-util-path-list": TextUtilPathList,
"text-util-join-text": TextUtilJoinText,
"text-util-search-text": TextUtilSearchText,
"text-util-random-text": TextUtilRandomText,
"text-util-loop-text": TextUtilLoopText,
"text-util-switch-text":TextUtilSwitchText,
# "text-util-show-text": TextUtilTextShow,
"xyz-util-txt-to-int": XyzUtilKVTxtToOther,
"number-util-random-num": NumberUtilRandomNum,
"region-util-get-pad-out-location-by-size": RegionUtilGetPadOutLocationBySize,
"region-util-get-crop-location-from-center-size-text": RegionUtilGetCropLocationFromCenterSizeText,
"region-util-get-by-center-and-size": RegionUtilGetCropLocationByCenter,
"region-util-get-by-lt": RegionUtilGetCropLocationByLT
}
NODE_DISPLAY_NAME_MAPPINGS = {
"io-text-save": 'save text file',
# "io-image-save": WAS_Image_Save,
"pipe-util-to-basic-pipe": 'to basic pipe',
"canvas-util-cal-size": 'cal size',
# "img-util-get-image-size": 'get image size',
# "img-util-switch-input-image": 'switch input img',
# "conditioning-util-input-switch": 'switch input conditioning',
"hks-util-cal-denoise-step": 'cal denoise step',
"cutoff-region-util": 'gen region text',
"io-util-file-list-get-text": 'get multi line text of file list',
"io-util-file-list-get": 'get file list',
"text-preset-colors": 'base colors',
"text-util-prompt-join": 'join prompt',
"text-util-prompt-del": 'del prompt',
"text-util-prompt-std": 'std prompt',
"text-util-prompt-unweight": 'unweight prompt',
"text-util-prompt-dup": 'dup prompt',
"text-util-prompt-adv-dup":'dup prompt (adv)',
"text-util-prompt-shuffle":'shuffle prompt',
"text-util-prompt-search":'search propmt',
"text-util-prompt-adv-search":'search prompt (adv)',
"text-util-prompt-add-prompt":'add prompt',
"text-util-path-list": 'path preset list',
"text-util-join-text": 'join text',
"text-util-search-text": 'search text and replace',
"text-util-random-text": 'get random text',
"text-util-loop-text": 'get random or loop text',
"text-util-switch-text":'switch text',
# "text-util-show-text": 'show text',
"xyz-util-txt-to-int": 'txt to int',
"number-util-random-num": 'random number',
"region-util-get-pad-out-location-by-size": 'get pad out location by size',
"region-util-get-crop-location-from-center-size-text": 'get region by center and size (from text)',
"region-util-get-by-center-and-size": 'get region by center and size',
"region-util-get-by-lt": 'get region by left top'
}