2006 lines
80 KiB
Python
2006 lines
80 KiB
Python
import os
|
|
|
|
from comfy import model_management
|
|
import torch
|
|
import comfy.sd
|
|
import comfy.utils
|
|
import folder_paths
|
|
import comfy.samplers
|
|
from nodes import common_ksampler
|
|
from comfy_extras.chainner_models import model_loading
|
|
|
|
from PIL import Image, ImageOps, ImageFilter, ImageDraw
|
|
from PIL.PngImagePlugin import PngInfo
|
|
import numpy as np
|
|
from torchvision.transforms import ToPILImage
|
|
#import cv2
|
|
#from deepface import DeepFace
|
|
|
|
import re
|
|
import random
|
|
import latent_preview
|
|
from datetime import datetime
|
|
import json
|
|
import piexif
|
|
import piexif.helper
|
|
|
|
|
|
MAX_RESOLUTION=8192
|
|
|
|
# Tensor to PIL
|
|
def tensor2pil(image):
|
|
return Image.fromarray(np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8))
|
|
|
|
# Convert PIL to Tensor
|
|
def pil2tensor(image):
|
|
return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0)
|
|
|
|
|
|
# model io
|
|
class WLSH_Checkpoint_Loader_Model_Name:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {"required": { "ckpt_name": (folder_paths.get_filename_list("checkpoints"), ),
|
|
}}
|
|
RETURN_TYPES = ("MODEL", "CLIP", "VAE","STRING",)
|
|
RETURN_NAMES = ("MODEL", "CLIP", "VAE", "modelname")
|
|
FUNCTION = "load_checkpoint"
|
|
|
|
CATEGORY = "WLSH Nodes/loaders"
|
|
|
|
def load_checkpoint(self, ckpt_name, output_vae=True, output_clip=True):
|
|
ckpt_path = folder_paths.get_full_path("checkpoints", ckpt_name)
|
|
name = self.parse_name(ckpt_name)
|
|
|
|
out = comfy.sd.load_checkpoint_guess_config(ckpt_path, output_vae=True, output_clip=True, embedding_directory=folder_paths.get_folder_paths("embeddings"))
|
|
|
|
new_out = list(out)
|
|
new_out.pop()
|
|
new_out.append(name)
|
|
out = tuple(new_out)
|
|
|
|
return (out)
|
|
|
|
def parse_name(self, ckpt_name):
|
|
path = ckpt_name
|
|
filename = path.split("/")[-1]
|
|
filename = filename.split(".")[:-1]
|
|
filename = ".".join(filename)
|
|
return filename
|
|
|
|
# sampling
|
|
class WLSH_KSamplerAdvanced:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {"required":
|
|
{"model": ("MODEL",),
|
|
"add_noise": (["enable", "disable"], ),
|
|
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
|
|
"steps": ("INT", {"default": 20, "min": 1, "max": 10000}),
|
|
"cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0}),
|
|
"sampler_name": (comfy.samplers.KSampler.SAMPLERS, ),
|
|
"scheduler": (comfy.samplers.KSampler.SCHEDULERS, ),
|
|
"positive": ("CONDITIONING", ),
|
|
"negative": ("CONDITIONING", ),
|
|
"latent_image": ("LATENT", ),
|
|
"start_at_step": ("INT", {"default": 0, "min": 0, "max": 10000}),
|
|
"end_at_step": ("INT", {"default": 10000, "min": 0, "max": 10000}),
|
|
"return_with_leftover_noise": (["disable", "enable"], ),
|
|
"denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("LATENT","INFO",)
|
|
FUNCTION = "sample"
|
|
|
|
CATEGORY = "WLSH Nodes/sampling"
|
|
|
|
def sample(self, model, add_noise, seed, steps, cfg, sampler_name, scheduler, positive, negative, latent_image, start_at_step, end_at_step, return_with_leftover_noise, denoise):
|
|
force_full_denoise = False
|
|
if return_with_leftover_noise == "enable":
|
|
force_full_denoise = False
|
|
disable_noise = False
|
|
if add_noise == "disable":
|
|
disable_noise = True
|
|
|
|
info = {"Seed: ": seed, "Steps: ": steps, "CFG scale: ": cfg, "Sampler: ": sampler_name, "Scheduler: ": scheduler, "Start at step: ": start_at_step, "End at step: ": end_at_step, "Denoising strength: ": denoise}
|
|
samples = common_ksampler(model, seed, steps, cfg, sampler_name, scheduler, positive, negative, latent_image,
|
|
denoise=denoise, disable_noise=disable_noise, start_step=start_at_step, last_step=end_at_step, force_full_denoise=force_full_denoise)
|
|
|
|
return (samples[0], info)
|
|
|
|
|
|
class WLSH_Alternating_KSamplerAdvanced:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {"required":
|
|
{"model": ("MODEL",),
|
|
"add_noise": (["enable", "disable"], ),
|
|
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
|
|
"steps": ("INT", {"default": 20, "min": 1, "max": 10000}),
|
|
"cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0}),
|
|
"sampler_name": (comfy.samplers.KSampler.SAMPLERS, ),
|
|
"scheduler": (comfy.samplers.KSampler.SCHEDULERS, ),
|
|
"clip": ("CLIP", ),
|
|
"positive_prompt": ("STRING", {"forceInput": True }),
|
|
"negative_prompt": ("STRING", {"forceInput": True }),
|
|
"latent_image": ("LATENT", ),
|
|
"start_at_step": ("INT", {"default": 0, "min": 0, "max": 10000}),
|
|
"end_at_step": ("INT", {"default": 10000, "min": 0, "max": 10000}),
|
|
"return_with_leftover_noise": (["disable", "enable"], ),
|
|
"denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("LATENT",)
|
|
FUNCTION = "sample"
|
|
|
|
CATEGORY = "WLSH Nodes/sampling"
|
|
|
|
def sample(self, model, add_noise, seed, steps, cfg, sampler_name, scheduler, clip, positive_prompt, negative_prompt, latent_image, start_at_step, end_at_step, return_with_leftover_noise, denoise):
|
|
noise_seed = seed
|
|
force_full_denoise = False
|
|
if return_with_leftover_noise == "enable":
|
|
force_full_denoise = False
|
|
disable_noise = False
|
|
if add_noise == "disable":
|
|
disable_noise = True
|
|
|
|
# alternating prompt parser
|
|
# syntax: {A|B} will sequentially alternate between A and B
|
|
def parse_prompt(input_string, stepnum):
|
|
def replace_match(match):
|
|
options = match.group(1).split('|')
|
|
return options[(stepnum - 1) % len(options)]
|
|
|
|
pattern = r'<(.*?)>'
|
|
parsed_string = re.sub(pattern, replace_match, input_string)
|
|
return parsed_string
|
|
|
|
latent_input = latent_image
|
|
for step in range(0,steps):
|
|
positive_txt = parse_prompt(positive_prompt,step+1)
|
|
positive = [[clip.encode(positive_txt), {}]]
|
|
negative_txt = parse_prompt(negative_prompt,step+1)
|
|
negative = [[clip.encode(negative_txt), {}]]
|
|
|
|
if(step < steps):
|
|
force_full_denoise = True
|
|
if(step > 0):
|
|
# disable_noise=True
|
|
denoise=(steps-step)/(steps)
|
|
|
|
latent_image = common_ksampler(model, noise_seed, 1, cfg, sampler_name, scheduler, positive, negative, latent_input,
|
|
denoise=denoise, disable_noise=disable_noise, start_step=start_at_step, last_step=end_at_step,
|
|
force_full_denoise=force_full_denoise)
|
|
|
|
latent_input = latent_image[0]
|
|
|
|
return latent_image
|
|
# return alternating_ksampler(clip, model, noise_seed, steps, cfg, sampler_name, scheduler, positive_prompt, negative_prompt, latent_image,
|
|
# denoise=denoise, disable_noise=disable_noise, start_step=start_at_step, last_step=end_at_step, force_full_denoise=force_full_denoise)
|
|
|
|
# utilities
|
|
class WLSH_Seed_to_Number:
|
|
def __init__(self):
|
|
pass
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"seed": ("SEED",),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("INT",)
|
|
FUNCTION = "number_to_seed"
|
|
|
|
CATEGORY = "WLSH Nodes/number"
|
|
|
|
def number_to_seed(self, seed):
|
|
return (int(seed["seed"]), )
|
|
|
|
class WLSH_Seed_and_Int:
|
|
def __init__(self):
|
|
pass
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"seed": ("INT", {"default": 0, "min": 0,
|
|
"max": 0xffffffffffffffff})
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("INT","SEED",)
|
|
FUNCTION = "seed_and_int"
|
|
|
|
CATEGORY = "WLSH Nodes/number"
|
|
|
|
def seed_and_int(self, seed):
|
|
return (seed,{"seed": seed} )
|
|
|
|
class WLSH_SDXL_Steps:
|
|
def __init__(self):
|
|
pass
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"precondition": ("INT", {"default": 3, "min": 1, "max": 10000}),
|
|
"base": ("INT", {"default": 12, "min": 1, "max": 10000}),
|
|
"total": ("INT", {"default": 20, "min": 1, "max": 10000}),
|
|
}
|
|
}
|
|
RETURN_TYPES = ("INT","INT","INT",)
|
|
RETURN_NAMES = ("pre", "base", "total")
|
|
FUNCTION = "set_steps"
|
|
|
|
CATEGORY="WLSH Nodes/number"
|
|
|
|
|
|
def set_steps(self,precondition,base,total):
|
|
return(precondition,base,total)
|
|
|
|
class WLSH_Int_Multiply:
|
|
def __init__(self):
|
|
pass
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"number": ("INT",{"default": 2, "min": 1, "max": 10000, "forceInput": True}),
|
|
"multiplier": ("INT", {"default": 2, "min": 1, "max": 10000}),
|
|
}
|
|
}
|
|
RETURN_TYPES = ("INT",)
|
|
FUNCTION = "multiply"
|
|
|
|
CATEGORY="WLSH Nodes/number"
|
|
|
|
|
|
def multiply(self,number,multiplier):
|
|
result = number*multiplier
|
|
return (int(result),)
|
|
|
|
class WLSH_Res_Multiply:
|
|
def __init__(self):
|
|
pass
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"width": ("INT", {"default": 512, "min": 16, "max": MAX_RESOLUTION, "forceInput": True}),
|
|
"height": ("INT", {"default": 512, "min": 16, "max": MAX_RESOLUTION, "forceInput": True}),
|
|
"multiplier": ("INT", {"default": 2, "min": 1, "max": 10000}),
|
|
}
|
|
}
|
|
RETURN_TYPES = ("INT","INT",)
|
|
RETURN_NAMES = ("width","height",)
|
|
FUNCTION = "multiply"
|
|
|
|
CATEGORY="WLSH Nodes/number"
|
|
|
|
|
|
def multiply(self,width, height,multiplier):
|
|
adj_width = width*multiplier
|
|
adj_height = height*multiplier
|
|
return (int(adj_width),int(adj_height),)
|
|
|
|
class WLSH_Time_String:
|
|
time_format = ["%Y%m%d%H%M%S","%Y%m%d%H%M","%Y%m%d","%Y-%m-%d-%H%M%S", "%Y-%m-%d-%H%M", "%Y-%m-%d"]
|
|
def __init__(self):
|
|
pass
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"style": (s.time_format,),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("STRING",)
|
|
RETURN_NAMES = ("time_format",)
|
|
FUNCTION = "get_time"
|
|
|
|
CATEGORY = "WLSH Nodes/text"
|
|
|
|
def get_time(self, style):
|
|
now = datetime.now()
|
|
timestamp = now.strftime(style)
|
|
|
|
return (timestamp,)
|
|
|
|
|
|
# Takes an input string and a list string, uses pattern and
|
|
# delimiter from inputs to parse the list_string and replace pattern
|
|
# in the input_string
|
|
class WLSH_Simple_Pattern_Replace:
|
|
def __init__(self):
|
|
pass
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"input_string": ("STRING", {"multiline": True, "forceInput": True}),
|
|
"list_string": ("STRING", {"default": f''}),
|
|
"pattern": ("STRING", {"default": f'$var'}),
|
|
"delimiter": ("STRING", {"default": f','}),
|
|
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = ("STRING",)
|
|
RETURN_NAMES = ("string",)
|
|
|
|
FUNCTION = "replace_string"
|
|
|
|
CATEGORY = "WLSH Nodes/text"
|
|
|
|
def replace_string(self, input_string, list_string, pattern, delimiter, seed):
|
|
# escape special characters and strip whitespace from pattern
|
|
pattern = re.escape(pattern).strip()
|
|
|
|
# find all pattern entries from input and create list
|
|
regex = re.compile(pattern)
|
|
matches = regex.findall(input_string)
|
|
|
|
# return input if nothing found
|
|
if not matches:
|
|
return (input_string,)
|
|
|
|
if seed is not None:
|
|
random.seed(seed)
|
|
|
|
# if provided delimiter not present in input, will try to use whole list
|
|
# we do not want that to happen...
|
|
if delimiter not in list_string:
|
|
raise ValueError("Delimiter not found in list_string")
|
|
|
|
# if pattern appears more than once each entry will have a different random choice
|
|
def replace(match):
|
|
return random.choice(list_string.split(delimiter))
|
|
|
|
new_string = regex.sub(replace, input_string)
|
|
|
|
return (new_string,)
|
|
|
|
class WLSH_String_Append:
|
|
location = ["after","before"]
|
|
separator = ["comma", "space", "newline", "none"]
|
|
|
|
def __init__(self):
|
|
pass
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"addition": ("STRING", {"multiline": True}),
|
|
"placement": (s.location,),
|
|
"separator": (s.separator,),
|
|
},
|
|
"optional": {
|
|
"input_string": ("STRING", {"multiline": True, "forceInput": True}),
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = ("STRING",)
|
|
RETURN_NAMES = ("combined",)
|
|
|
|
FUNCTION = "concat_string"
|
|
|
|
CATEGORY = "WLSH Nodes/text"
|
|
|
|
def concat_string(self,placement, separator, addition="", input_string=""):
|
|
sep = {"comma": ', ', "space": ' ', "newline": '\n', "none":''}
|
|
|
|
if (input_string is None):
|
|
return(addition,)
|
|
|
|
if (placement == "after"):
|
|
new_string = input_string + sep[separator] + addition
|
|
else:
|
|
new_string = addition + sep[separator] + input_string
|
|
|
|
return(new_string,)
|
|
|
|
class WLSH_Prompt_Weight:
|
|
def __init__(self):
|
|
pass
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"prompt": ("STRING", {"multiline": True, "forceInput": True}),
|
|
"weight": ("FLOAT", {"default": 1.0, "min": 0.1, "max": 5.0, "step": 0.1}),
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = ("STRING",)
|
|
RETURN_NAMES = ("prompt",)
|
|
|
|
FUNCTION = "add_weight"
|
|
|
|
CATEGORY = "WLSH Nodes/text"
|
|
|
|
def add_weight(self, prompt, weight):
|
|
if(weight == 1.0):
|
|
new_string = prompt
|
|
else:
|
|
new_string = "(" + prompt + ":" + str(weight) + ")"
|
|
|
|
return(new_string,)
|
|
|
|
class WLSH_SDXL_Resolutions:
|
|
resolution = ["1024x1024|1:1","1152x896|9:7","1216x832|19:13","1344x768|7:4","1536x640|12:5"]
|
|
direction = ["landscape","portrait"]
|
|
|
|
def __init__(self):
|
|
pass
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"resolution": (s.resolution,),
|
|
"direction": (s.direction,),
|
|
}
|
|
}
|
|
RETURN_TYPES = ("INT","INT",)
|
|
RETURN_NAMES = ("width", "height",)
|
|
FUNCTION = "get_resolutions"
|
|
|
|
CATEGORY="WLSH Nodes/number"
|
|
|
|
|
|
def get_resolutions(self,resolution, direction):
|
|
pixels = resolution.split('|')[0]
|
|
width,height = pixels.split('x')
|
|
width = int(width)
|
|
height = int(height)
|
|
if(direction == "portrait"):
|
|
width,height = height,width
|
|
return(width,height)
|
|
|
|
class WLSH_Resolutions_by_Ratio:
|
|
aspects = ["1:1","6:5","5:4","4:3","3:2","16:10","16:9","21:9","43:18","2:1","3:1","4:1"]
|
|
direction = ["landscape","portrait"]
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {"required": { "aspect": (s.aspects,),
|
|
"direction": (s.direction,),
|
|
"shortside": ("INT", {"default": 512, "min": 64, "max": MAX_RESOLUTION, "step": 64})}}
|
|
RETURN_TYPES = ("INT","INT",)
|
|
RETURN_NAMES = ("width", "height",)
|
|
FUNCTION = "get_resolutions"
|
|
CATEGORY="WLSH Nodes/number"
|
|
|
|
def get_resolutions(self, aspect, direction, shortside):
|
|
x,y = aspect.split(':')
|
|
x = int(x)
|
|
y = int(y)
|
|
ratio = x/y
|
|
width = int(shortside * ratio)
|
|
width = (width + 63) & (-64)
|
|
height = shortside
|
|
if(direction == "portrait"):
|
|
width,height = height,width
|
|
return(width,height)
|
|
|
|
class WLSH_Empty_Latent_Image_By_Resolution:
|
|
def __init__(self, device="cpu"):
|
|
self.device = device
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {"required": { "width": ("INT", {"default": 512, "min": 16, "max": MAX_RESOLUTION, "step": 8}),
|
|
"height": ("INT", {"default": 512, "min": 16, "max": MAX_RESOLUTION, "step": 8}),
|
|
"batch_size": ("INT", {"default": 1, "min": 1, "max": 4096})}}
|
|
RETURN_TYPES = ("LATENT","INT","INT",)
|
|
RETURN_NAMES = ("latent", "width", "height",)
|
|
FUNCTION = "generate"
|
|
|
|
CATEGORY = "WLSH Nodes/latent"
|
|
|
|
def generate(self, width, height, batch_size=1):
|
|
adj_width = width // 8
|
|
adj_height = height // 8
|
|
latent = torch.zeros([batch_size, 4, adj_height, adj_width])
|
|
return ({"samples":latent}, adj_width * 8, adj_height * 8, )
|
|
|
|
# latent
|
|
class WLSH_Empty_Latent_Image_By_Ratio:
|
|
aspects = ["1:1","6:5","5:4","4:3","3:2","16:10","16:9","19:9","21:9","43:18","2:1","3:1","4:1"]
|
|
direction = ["landscape","portrait"]
|
|
|
|
def __init__(self, device="cpu"):
|
|
self.device = device
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {"required": { "aspect": (s.aspects,),
|
|
"direction": (s.direction,),
|
|
"shortside": ("INT", {"default": 512, "min": 64, "max": MAX_RESOLUTION, "step": 64}),
|
|
"batch_size": ("INT", {"default": 1, "min": 1, "max": 64})}}
|
|
RETURN_TYPES = ("LATENT","INT","INT",)
|
|
RETURN_NAMES = ("latent", "width", "height",)
|
|
FUNCTION = "generate"
|
|
|
|
CATEGORY = "WLSH Nodes/latent"
|
|
|
|
def generate(self, aspect, direction, shortside, batch_size=1):
|
|
x,y = aspect.split(':')
|
|
x = int(x)
|
|
y = int(y)
|
|
ratio = x/y
|
|
width = int(shortside * ratio)
|
|
width = (width + 63) & (-64)
|
|
height = shortside
|
|
if(direction == "portrait"):
|
|
width,height = height,width
|
|
adj_width = width // 8
|
|
adj_height = height // 8
|
|
latent = torch.zeros([batch_size, 4, adj_height, adj_width])
|
|
return ({"samples":latent}, adj_width * 8, adj_height * 8, )
|
|
|
|
class WLSH_Empty_Latent_Image_By_Pixels:
|
|
aspects = ["1:1","5:4","4:3","3:2","16:10","16:9","19:9","21:9","43:18","2:1","3:1","4:1"]
|
|
direction = ["landscape","portrait"]
|
|
|
|
def __init__(self, device="cpu"):
|
|
self.device = device
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {"required": { "aspect": (s.aspects,),
|
|
"direction": (s.direction,),
|
|
"megapixels": ("FLOAT", {"default": 1.0, "min": 0.01, "max": 16.0, "step": 0.01}),
|
|
"batch_size": ("INT", {"default": 1, "min": 1, "max": 64})}}
|
|
RETURN_TYPES = ("LATENT","INT","INT",)
|
|
RETURN_NAMES = ("latent", "width", "height",)
|
|
FUNCTION = "generate"
|
|
|
|
CATEGORY = "WLSH Nodes/latent"
|
|
|
|
def generate(self, aspect, direction, megapixels, batch_size=1):
|
|
x,y = aspect.split(':')
|
|
x = int(x)
|
|
y = int(y)
|
|
ratio = x/y
|
|
|
|
total = int(megapixels * 1024 * 1024)
|
|
|
|
width = int(np.sqrt(ratio * total))
|
|
width = (width + 63) & (-64)
|
|
height = int(np.sqrt(1/ratio * total))
|
|
height = (height + 63) & (-64)
|
|
if(direction == "portrait"):
|
|
width,height = height,width
|
|
adj_width = width // 8
|
|
adj_height = height // 8
|
|
latent = torch.zeros([batch_size, 4, adj_height, adj_width])
|
|
return ({"samples":latent}, adj_width * 8, adj_height * 8, )
|
|
|
|
|
|
class WLSH_SDXL_Quick_Empty_Latent:
|
|
resolution = ["1024x1024|1:1","1152x896|9:7","1216x832|19:13","1344x768|7:4","1536x640|12:5"]
|
|
direction = ["landscape","portrait"]
|
|
|
|
def __init__(self, device="cpu"):
|
|
self.device = device
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {"required": { "resolution": (s.resolution,),
|
|
"direction": (s.direction,),
|
|
"batch_size": ("INT", {"default": 1, "min": 1, "max": 64})}}
|
|
RETURN_TYPES = ("LATENT","INT","INT", )
|
|
RETURN_NAMES = ("latent", "width", "height",)
|
|
FUNCTION = "generate"
|
|
|
|
CATEGORY = "WLSH Nodes/latent"
|
|
|
|
def generate(self, resolution, direction, batch_size=1):
|
|
pixels = resolution.split('|')[0]
|
|
width,height = pixels.split('x')
|
|
width = int(width)
|
|
height = int(height)
|
|
if(direction == "portrait"):
|
|
width,height = height,width
|
|
adj_width = width // 8
|
|
adj_height = height // 8
|
|
latent = torch.zeros([batch_size, 4, adj_height, adj_width])
|
|
return ({"samples":latent}, adj_width * 8, adj_height * 8,)
|
|
|
|
class WLSH_SDXL_Resolution_Multiplier:
|
|
def __init__(self, device="cpu"):
|
|
self.device = device
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {"required": { "target_width": ("INT", {"default": 512, "min": 16, "max": MAX_RESOLUTION, "step": 8, "forceInput": True}),
|
|
"target_height": ("INT", {"default": 512, "min": 16, "max": MAX_RESOLUTION, "step": 8, "forceInput": True}),
|
|
"multiplier": ("INT", {"default": 2, "min": 1, "max": 12})}}
|
|
RETURN_TYPES = ("INT","INT", )
|
|
RETURN_NAMES = ("width", "height",)
|
|
FUNCTION = "multiply_res"
|
|
|
|
CATEGORY = "WLSH Nodes/number"
|
|
|
|
def multiply_res(self, target_width=1024, target_height=1024, multiplier=2):
|
|
return (target_width*2, target_height*2,)
|
|
|
|
# conditioning
|
|
class WLSH_CLIP_Text_Positive_Negative:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {"required": {"positive": ("STRING", {"multiline": True}),
|
|
"negative": ("STRING", {"multiline": True}),
|
|
"clip": ("CLIP", )}}
|
|
RETURN_TYPES = ("CONDITIONING","CONDITIONING","STRING","STRING")
|
|
RETURN_NAMES = ("positive", "negative","positive_text","negative_text")
|
|
FUNCTION = "encode"
|
|
|
|
CATEGORY = "WLSH Nodes/conditioning"
|
|
|
|
def encode(self, clip, positive, negative):
|
|
return ([[clip.encode(positive), {}]],[[clip.encode(negative), {}]],positive,negative)
|
|
|
|
class WLSH_CLIP_Positive_Negative:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {"required": {
|
|
"clip": ("CLIP", ),
|
|
"positive_text": ("STRING",{"default": f'', "multiline": True}),
|
|
"negative_text": ("STRING",{"default": f'', "multiline": True})
|
|
}}
|
|
RETURN_TYPES = ("CONDITIONING","CONDITIONING",)
|
|
RETURN_NAMES = ("positive", "negative",)
|
|
FUNCTION = "encode"
|
|
|
|
CATEGORY = "WLSH Nodes/conditioning"
|
|
|
|
def encode(self, clip, positive_text, negative_text):
|
|
return ([[clip.encode(positive_text), {}]],[[clip.encode(negative_text), {}]] )
|
|
|
|
class WLSH_CLIP_Text_Positive_Negative_XL:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {"required": {
|
|
"width": ("INT", {"default": 1024.0, "min": 0, "max": MAX_RESOLUTION}),
|
|
"height": ("INT", {"default": 1024.0, "min": 0, "max": MAX_RESOLUTION}),
|
|
"crop_w": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION}),
|
|
"crop_h": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION}),
|
|
"target_width": ("INT", {"default": 1024.0, "min": 0, "max": MAX_RESOLUTION}),
|
|
"target_height": ("INT", {"default": 1024.0, "min": 0, "max": MAX_RESOLUTION}),
|
|
"positive_g": ("STRING", {"multiline": True, "default": "POS_G"}),
|
|
"positive_l": ("STRING", {"multiline": True, "default": "POS_L"}),
|
|
"negative_g": ("STRING", {"multiline": True, "default": "NEG_G"}),
|
|
"negative_l": ("STRING", {"multiline": True, "default": "NEG_L"}),
|
|
"clip": ("CLIP", ),
|
|
}}
|
|
|
|
RETURN_TYPES = ("CONDITIONING","CONDITIONING","STRING","STRING")
|
|
RETURN_NAMES = ("positive", "negative", "positive_text", "negative_text")
|
|
FUNCTION = "encode"
|
|
|
|
CATEGORY = "WLSH Nodes/conditioning"
|
|
|
|
def encode(self, clip, width, height, crop_w, crop_h, target_width, target_height, positive_g, positive_l,negative_g, negative_l):
|
|
tokens = clip.tokenize(positive_g)
|
|
tokens["l"] = clip.tokenize(positive_l)["l"]
|
|
if len(tokens["l"]) != len(tokens["g"]):
|
|
empty = clip.tokenize("")
|
|
while len(tokens["l"]) < len(tokens["g"]):
|
|
tokens["l"] += empty["l"]
|
|
while len(tokens["l"]) > len(tokens["g"]):
|
|
tokens["g"] += empty["g"]
|
|
condP, pooledP = clip.encode_from_tokens(tokens, return_pooled=True)
|
|
tokensN = clip.tokenize(negative_g)
|
|
tokensN["l"] = clip.tokenize(negative_l)["l"]
|
|
if len(tokensN["l"]) != len(tokensN["g"]):
|
|
empty = clip.tokenize("")
|
|
while len(tokensN["l"]) < len(tokensN["g"]):
|
|
tokensN["l"] += empty["l"]
|
|
while len(tokensN["l"]) > len(tokensN["g"]):
|
|
tokensN["g"] += empty["g"]
|
|
condN, pooledN = clip.encode_from_tokens(tokensN, return_pooled=True)
|
|
|
|
#combine pos_l and pos_g prompts
|
|
positive_text = positive_g + ", " + positive_l
|
|
negative_text = negative_g + ", " + negative_l
|
|
return ([[condP, {"pooled_output": pooledP, "width": width, "height": height, "crop_w": crop_w, "crop_h": crop_h, "target_width": target_width, "target_height": target_height}]],[[condN, {"pooled_output": pooledP, "width": width, "height": height, "crop_w": crop_w, "crop_h": crop_h, "target_width": target_width, "target_height": target_height}]], positive_text, negative_text, )
|
|
|
|
|
|
class WLSH_CLIP_Positive_Negative_XL:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {"required": {
|
|
"width": ("INT", {"default": 1024.0, "min": 0, "max": MAX_RESOLUTION}),
|
|
"height": ("INT", {"default": 1024.0, "min": 0, "max": MAX_RESOLUTION}),
|
|
"crop_w": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION}),
|
|
"crop_h": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION}),
|
|
"target_width": ("INT", {"default": 1024.0, "min": 0, "max": MAX_RESOLUTION}),
|
|
"target_height": ("INT", {"default": 1024.0, "min": 0, "max": MAX_RESOLUTION}),
|
|
"positive_g": ("STRING", {"multiline": True, "default": "POS_G"}),
|
|
"positive_l": ("STRING", {"multiline": True, "default": "POS_L"}),
|
|
"negative_g": ("STRING", {"multiline": True, "default": "NEG_G"}),
|
|
"negative_l": ("STRING", {"multiline": True, "default": "NEG_L"}),
|
|
"clip": ("CLIP", ),
|
|
}}
|
|
|
|
RETURN_TYPES = ("CONDITIONING","CONDITIONING",)
|
|
RETURN_NAMES = ("positive", "negative",)
|
|
FUNCTION = "encode"
|
|
|
|
CATEGORY = "WLSH Nodes/conditioning"
|
|
|
|
def encode(self, clip, width, height, crop_w, crop_h, target_width, target_height, positive_g, positive_l,negative_g, negative_l):
|
|
tokens = clip.tokenize(positive_g)
|
|
tokens["l"] = clip.tokenize(positive_l)["l"]
|
|
if len(tokens["l"]) != len(tokens["g"]):
|
|
empty = clip.tokenize("")
|
|
while len(tokens["l"]) < len(tokens["g"]):
|
|
tokens["l"] += empty["l"]
|
|
while len(tokens["l"]) > len(tokens["g"]):
|
|
tokens["g"] += empty["g"]
|
|
condP, pooledP = clip.encode_from_tokens(tokens, return_pooled=True)
|
|
tokensN = clip.tokenize(negative_g)
|
|
tokensN["l"] = clip.tokenize(negative_l)["l"]
|
|
if len(tokensN["l"]) != len(tokensN["g"]):
|
|
empty = clip.tokenize("")
|
|
while len(tokensN["l"]) < len(tokensN["g"]):
|
|
tokensN["l"] += empty["l"]
|
|
while len(tokensN["l"]) > len(tokensN["g"]):
|
|
tokensN["g"] += empty["g"]
|
|
condN, pooledN = clip.encode_from_tokens(tokensN, return_pooled=True)
|
|
return ([[condP, {"pooled_output": pooledP, "width": width, "height": height, "crop_w": crop_w, "crop_h": crop_h, "target_width": target_width, "target_height": target_height}]],[[condN, {"pooled_output": pooledP, "width": width, "height": height, "crop_w": crop_w, "crop_h": crop_h, "target_width": target_width, "target_height": target_height}]], )
|
|
|
|
class WLSH_CLIP_Text_Unified:
|
|
conditioners = ["SD1.5", "SDXL"]
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {"required": {
|
|
"positive": ("STRING", {"multiline": True, "default": ""}),
|
|
"negative": ("STRING", {"multiline": True, "default": ""}),
|
|
"clip": ("CLIP", ),
|
|
"conditioner": (s.conditioners,),
|
|
},
|
|
"optional": {
|
|
"width": ("INT", {"default": 1024.0, "min": 0, "max": MAX_RESOLUTION}),
|
|
"height": ("INT", {"default": 1024.0, "min": 0, "max": MAX_RESOLUTION}),
|
|
}}
|
|
|
|
RETURN_TYPES = ("CONDITIONING","CONDITIONING","STRING","STRING")
|
|
RETURN_NAMES = ("positive", "negative", "positive_text", "negative_text")
|
|
FUNCTION = "encode"
|
|
|
|
CATEGORY = "WLSH Nodes/conditioning"
|
|
|
|
def encode(self, clip, positive, negative, conditioner, width=1024, height=1024, ):
|
|
if(conditioner == "SDXL"):
|
|
# double height for target
|
|
target_width = 2*width
|
|
target_height = 2*height
|
|
|
|
# no crop
|
|
crop_w = 0
|
|
crop_h = 0
|
|
|
|
# duplicate pos_g as pos_l
|
|
tokens = clip.tokenize(positive)
|
|
tokens["l"] = clip.tokenize(positive)["l"]
|
|
if len(tokens["l"]) != len(tokens["g"]):
|
|
empty = clip.tokenize("")
|
|
while len(tokens["l"]) < len(tokens["g"]):
|
|
tokens["l"] += empty["l"]
|
|
while len(tokens["l"]) > len(tokens["g"]):
|
|
tokens["g"] += empty["g"]
|
|
condP, pooledP = clip.encode_from_tokens(tokens, return_pooled=True)
|
|
|
|
# duplicate neg_g as neg_l
|
|
tokensN = clip.tokenize(negative)
|
|
tokensN["l"] = clip.tokenize(negative)["l"]
|
|
if len(tokensN["l"]) != len(tokensN["g"]):
|
|
empty = clip.tokenize("")
|
|
while len(tokensN["l"]) < len(tokensN["g"]):
|
|
tokensN["l"] += empty["l"]
|
|
while len(tokensN["l"]) > len(tokensN["g"]):
|
|
tokensN["g"] += empty["g"]
|
|
condN, pooledN = clip.encode_from_tokens(tokensN, return_pooled=True)
|
|
|
|
positive_text = positive
|
|
negative_text = negative
|
|
return ([[condP, {"pooled_output": pooledP, "width": width, "height": height, "crop_w": crop_w, "crop_h": crop_h, "target_width": target_width, "target_height": target_height}]],[[condN, {"pooled_output": pooledP, "width": width, "height": height, "crop_w": crop_w, "crop_h": crop_h, "target_width": target_width, "target_height": target_height}]], positive_text, negative_text, )
|
|
elif(conditioner == "SD1.5"):
|
|
return ([[clip.encode(positive), {}]],[[clip.encode(negative), {}]],positive,negative)
|
|
|
|
# upscaling
|
|
class WLSH_Image_Scale_By_Factor:
|
|
upscale_methods = ["nearest-exact", "bilinear", "area"]
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {"required": { "original": ("IMAGE",),
|
|
"upscale_method": (s.upscale_methods,),
|
|
"factor": ("FLOAT", {"default": 2.0, "min": 0.1, "max": 8.0, "step": 0.1})
|
|
}}
|
|
RETURN_TYPES = ("IMAGE",)
|
|
FUNCTION = "upscale"
|
|
|
|
CATEGORY = "WLSH Nodes/upscaling"
|
|
|
|
def upscale(self, original, upscale_method, factor):
|
|
old_width = original.shape[2]
|
|
old_height = original.shape[1]
|
|
new_width= int(old_width * factor)
|
|
new_height = int(old_height * factor)
|
|
print("Processing image with shape: ",old_width,"x",old_height,"to ",new_width,"x",new_height)
|
|
samples = original.movedim(-1,1)
|
|
s = comfy.utils.common_upscale(samples, new_width, new_height, upscale_method, crop="disabled")
|
|
s = s.movedim(1,-1)
|
|
return (s,)
|
|
|
|
class WLSH_Image_Scale_By_Shortside:
|
|
upscale_methods = ["nearest-exact", "bilinear", "area"]
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {"required": { "original": ("IMAGE",),
|
|
"upscale_method": (s.upscale_methods,),
|
|
"shortside": ("INT", {"default": 512, "min": 32, "max": 4096, "step": 32})
|
|
}}
|
|
RETURN_TYPES = ("IMAGE","INT","INT",)
|
|
RETURN_NAMES = ("IMAGE", "width", "height",)
|
|
FUNCTION = "upscale"
|
|
|
|
CATEGORY = "WLSH Nodes/upscaling"
|
|
|
|
def upscale(self, original, upscale_method, shortside):
|
|
old_width = original.shape[2]
|
|
old_height = original.shape[1]
|
|
old_shortside = min(old_width, old_height)
|
|
factor = shortside/max(1,old_shortside)
|
|
new_width= int(old_width * factor)
|
|
new_height = int(old_height * factor)
|
|
print("Processing image with shape: ",old_width,"x",old_height,"to ",new_width,"x",new_height)
|
|
samples = original.movedim(-1,1)
|
|
s = comfy.utils.common_upscale(samples, new_width, new_height, upscale_method, crop="disabled")
|
|
s = s.movedim(1,-1)
|
|
return (s,new_width,new_height,)
|
|
|
|
class WLSH_SDXL_Quick_Image_Scale:
|
|
upscale_methods = ["nearest-exact", "bilinear", "area"]
|
|
resolution = ["1024x1024|1:1","1152x896|9:7","1216x832|19:13","1344x768|7:4","1536x640|12:5"]
|
|
direction = ["landscape","portrait"]
|
|
crop_methods = ["disabled", "center"]
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {"required": { "original": ("IMAGE",),
|
|
"upscale_method": (s.upscale_methods,),
|
|
"resolution": (s.resolution,),
|
|
"direction": (s.direction,),
|
|
"crop": (s.crop_methods,),
|
|
}}
|
|
RETURN_TYPES = ("IMAGE",)
|
|
FUNCTION = "upscale"
|
|
|
|
CATEGORY = "WLSH Nodes/upscaling"
|
|
|
|
def upscale(self, original, upscale_method, resolution, direction, crop):
|
|
pixels = resolution.split('|')[0]
|
|
width,height = pixels.split('x')
|
|
new_width = int(width)
|
|
new_height = int(height)
|
|
if(direction == "portrait"):
|
|
new_width,new_height = new_height,new_width
|
|
old_width = original.shape[2]
|
|
old_height = original.shape[1]
|
|
#print("Processing image with shape: ",old_width,"x",old_height,"to ",new_width,"x",new_height)
|
|
samples = original.movedim(-1,1)
|
|
s = comfy.utils.common_upscale(samples, new_width, new_height, upscale_method, crop)
|
|
s = s.movedim(1,-1)
|
|
return (s,)
|
|
|
|
|
|
class WLSH_Upscale_By_Factor_With_Model:
|
|
upscale_methods = ["nearest-exact", "bilinear", "area"]
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {"required": { "upscale_model": ("UPSCALE_MODEL",), "image": ("IMAGE",),
|
|
"upscale_method": (s.upscale_methods,),
|
|
"factor": ("FLOAT", {"default": 2.0, "min": 0.1, "max": 8.0, "step": 0.1})
|
|
}}
|
|
RETURN_TYPES = ("IMAGE",)
|
|
FUNCTION = "upscale"
|
|
|
|
CATEGORY = "WLSH Nodes/upscaling"
|
|
|
|
def upscale(self, image, upscale_model, upscale_method, factor):
|
|
# upscale image using upscaling model
|
|
device = model_management.get_torch_device()
|
|
upscale_model.to(device)
|
|
in_img = image.movedim(-1,-3).to(device)
|
|
s = comfy.utils.tiled_scale(in_img, lambda a: upscale_model(a), tile_x=128 + 64, tile_y=128 + 64, overlap = 8, upscale_amount=upscale_model.scale)
|
|
upscale_model.cpu()
|
|
upscaled = torch.clamp(s.movedim(-3,-1), min=0, max=1.0)
|
|
|
|
# get dimensions of orginal image
|
|
old_width = image.shape[2]
|
|
old_height = image.shape[1]
|
|
|
|
# scale dimensions by provided factor
|
|
new_width= int(old_width * factor)
|
|
new_height = int(old_height * factor)
|
|
print("Processing image with shape: ",old_width,"x",old_height,"to ",new_width,"x",new_height)
|
|
|
|
# apply simple scaling to image
|
|
samples = upscaled.movedim(-1,1)
|
|
s = comfy.utils.common_upscale(samples, new_width, new_height, upscale_method, crop="disabled")
|
|
s = s.movedim(1,-1)
|
|
|
|
return (s,)
|
|
|
|
|
|
# outpainting
|
|
class WLSH_Outpaint_To_Image:
|
|
directions = ["left","right","up","down"]
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {"required": { "image": ("IMAGE",),
|
|
"direction": (s.directions,),
|
|
"pixels": ("INT", {"default": 128, "min": 32, "max": 512, "step": 32}),
|
|
"mask_padding": ("INT",{"default": 12, "min": 0, "max": 64, "step": 4})
|
|
}}
|
|
RETURN_TYPES = ("IMAGE","MASK")
|
|
FUNCTION = "outpaint"
|
|
|
|
CATEGORY = "WLSH Nodes/inpainting"
|
|
|
|
def convert_image(self, im, direction, mask_padding):
|
|
width, height = im.size
|
|
im = im.convert("RGBA")
|
|
alpha = Image.new('L',(width,height),255)
|
|
im.putalpha(alpha)
|
|
return im
|
|
|
|
|
|
def outpaint(self, image, direction, mask_padding, pixels):
|
|
image = tensor2pil(image)
|
|
# i = 255. * image.cpu().numpy()
|
|
# image = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
|
|
|
|
image = self.convert_image(image, direction, mask_padding)
|
|
if direction == "right":
|
|
border = (0,0,pixels,0)
|
|
new_image = ImageOps.expand(image,border=border,fill=(0,0,0,0))
|
|
elif direction == "left":
|
|
border = (pixels,0,0,0)
|
|
new_image = ImageOps.expand(image,border=border,fill=(0,0,0,0))
|
|
elif direction == "up":
|
|
border = (0,pixels,0,0)
|
|
new_image = ImageOps.expand(image,border=border,fill=(0,0,0,0))
|
|
elif direction == "down":
|
|
border = (0,0,0,pixels)
|
|
new_image = ImageOps.expand(image,border=border,fill=(0,0,0,0))
|
|
|
|
image = new_image.convert("RGB")
|
|
image = np.array(image).astype(np.float32) / 255.0
|
|
image = torch.from_numpy(image)[None,]
|
|
if 'A' in new_image.getbands():
|
|
mask = np.array(new_image.getchannel('A')).astype(np.float32) / 255.0
|
|
mask = 1. - torch.from_numpy(mask)
|
|
else:
|
|
mask = torch.zeros((64,64), dtype=torch.float32, device="cpu")
|
|
|
|
#print("bands: ", new_image.getbands())
|
|
# if 'A' in new_image.getbands():
|
|
# mask = np.array(new_image.getchannel('A')).astype(np.float32) / 255.0
|
|
# mask = 1. - torch.from_numpy(mask)
|
|
# #print("getting mask from alpha")
|
|
# else:
|
|
# mask = torch.zeros((64,64), dtype=torch.float32, device="cpu")
|
|
# # print("generating mask")
|
|
# new_image = new_image.convert("RGB")
|
|
# new_image = pil2tensor(new_image)
|
|
|
|
return (image,mask)
|
|
|
|
class WLSH_VAE_Encode_For_Inpaint_Padding:
|
|
def __init__(self, device="cpu"):
|
|
self.device = device
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {"required": { "pixels": ("IMAGE", ), "vae": ("VAE", ), "mask": ("MASK", ),
|
|
"mask_padding": ("INT",{"default": 24, "min": 6, "max": 128, "step": 2})}}
|
|
RETURN_TYPES = ("LATENT",)
|
|
FUNCTION = "encode"
|
|
|
|
CATEGORY = "WLSH Nodes/inpainting"
|
|
|
|
def encode(self, vae, pixels, mask, mask_padding=3):
|
|
x = (pixels.shape[1] // 64) * 64
|
|
y = (pixels.shape[2] // 64) * 64
|
|
mask = torch.nn.functional.interpolate(mask[None,None,], size=(pixels.shape[1], pixels.shape[2]), mode="bilinear")[0][0]
|
|
|
|
pixels = pixels.clone()
|
|
if pixels.shape[1] != x or pixels.shape[2] != y:
|
|
pixels = pixels[:,:x,:y,:]
|
|
mask = mask[:x,:y]
|
|
|
|
#grow mask by a few pixels to keep things seamless in latent space
|
|
kernel_tensor = torch.ones((1, 1, mask_padding, mask_padding))
|
|
mask_erosion = torch.clamp(torch.nn.functional.conv2d((mask.round())[None], kernel_tensor, padding=3), 0, 1)
|
|
m = (1.0 - mask.round())
|
|
for i in range(3):
|
|
pixels[:,:,:,i] -= 0.5
|
|
pixels[:,:,:,i] *= m
|
|
pixels[:,:,:,i] += 0.5
|
|
t = vae.encode(pixels)
|
|
|
|
return ({"samples":t, "noise_mask": (mask_erosion[0][:x,:y].round())}, )
|
|
|
|
class WLSH_Generate_Edge_Mask:
|
|
directions = ["left","right","up","down"]
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {"required": { "image": ("IMAGE",),
|
|
"direction": (s.directions,),
|
|
"pixels": ("INT", {"default": 128, "min": 32, "max": 512, "step": 32}),
|
|
"overlap": ("INT", {"default": 64, "min": 16, "max": 256, "step": 16})
|
|
}}
|
|
RETURN_TYPES = ("IMAGE",)
|
|
FUNCTION = "gen_second_mask"
|
|
|
|
CATEGORY = "WLSH Nodes/inpainting"
|
|
|
|
def gen_second_mask(self, direction, image, pixels, overlap):
|
|
image = tensor2pil(image)
|
|
new_width,new_height = image.size
|
|
|
|
# generate new image fully un-masked
|
|
mask2 = Image.new('RGBA',(new_width,new_height),(0,0,0,255))
|
|
mask_thickness = overlap
|
|
if (direction == "up"):
|
|
# horizontal mask width of new image and height of 1/4 padding
|
|
new_mask = Image.new('RGBA',(new_width, mask_thickness),(0,122,0,255))
|
|
mask2.paste(new_mask,(0,(pixels-int(mask_thickness/2))))
|
|
elif (direction == "down"):
|
|
# horizontal mask width of new image and height of 1/4 padding
|
|
new_mask = Image.new('RGBA',(new_width, mask_thickness),(0,122,0,255))
|
|
mask2.paste(new_mask,(0,new_height-pixels - int(mask_thickness/2)))
|
|
elif (direction == "left"):
|
|
# vertical mask height of new image and width of 1/4 padding
|
|
new_mask = Image.new('RGBA',(mask_thickness,new_height),(0,122,0,255))
|
|
mask2.paste(new_mask,(pixels - int(mask_thickness/2),0))
|
|
elif (direction == "right"):
|
|
# vertical mask height of new image and width of 1/4 padding
|
|
new_mask = Image.new('RGBA',(mask_thickness,new_height),(0,122,0,255))
|
|
mask2.paste(new_mask,(new_width - pixels - int(mask_thickness/2),0))
|
|
mask2 = mask2.filter(ImageFilter.GaussianBlur(radius=5))
|
|
mask2 = np.array(mask2).astype(np.float32) / 255.0
|
|
mask2 = torch.from_numpy(mask2)[None,]
|
|
return (mask2,)
|
|
|
|
# image I/O
|
|
def get_timestamp(time_format="%Y-%m-%d-%H%M%S"):
|
|
now = datetime.now()
|
|
try:
|
|
timestamp = now.strftime(time_format)
|
|
except:
|
|
timestamp = now.strftime("%Y-%m-%d-%H%M%S")
|
|
|
|
return(timestamp)
|
|
|
|
def make_filename(filename="ComfyUI", seed={"seed":0}, modelname="sd", counter=0, time_format="%Y-%m-%d-%H%M%S"):
|
|
'''
|
|
Builds a filename by reading in a filename format and returning a formatted string using input tokens
|
|
Tokens:
|
|
%time - timestamp using the time_format value
|
|
%model - modelname using the modelname input
|
|
%seed - seed from the seed input
|
|
%counter - counter integer from the counter input
|
|
'''
|
|
timestamp = get_timestamp(time_format)
|
|
|
|
# parse input string
|
|
filename = filename.replace("%time",timestamp)
|
|
filename = filename.replace("%model",modelname)
|
|
filename = filename.replace("%seed",str(seed))
|
|
filename = filename.replace("%counter",str(counter))
|
|
|
|
if filename == "":
|
|
filename = timestamp
|
|
return(filename)
|
|
|
|
def make_comment(positive="no positive prompt info", negative="no negative prompt info", modelname="unknown", seed=-1, info=None):
|
|
comment = ""
|
|
if(info is None):
|
|
comment = "Positive prompt:\n" + positive + "\nNegative prompt:\n" + negative + "\nModel: " + modelname + "\nSeed: " + str(seed)
|
|
return comment
|
|
else:
|
|
# reformat to stop long precision
|
|
try:
|
|
info['CFG scale: '] = "{:.2f}".format(info['CFG scale: '])
|
|
except:
|
|
pass
|
|
try:
|
|
info['Denoising strength: '] = "{:.2f}".format(info['Denoising strength: '])
|
|
except:
|
|
pass
|
|
|
|
comment = "Positive prompt:\n" + positive + "\nNegative prompt:\n" + negative + "\nModel: " + modelname
|
|
for key in info:
|
|
newline = "\n" + key + str(info[key])
|
|
comment += newline
|
|
# print(comment)
|
|
return comment
|
|
|
|
# version without INFO input for TTN compatability
|
|
class WLSH_Image_Save_With_Prompt:
|
|
def __init__(self):
|
|
# get default output directory
|
|
self.type = "output"
|
|
self.output_dir = folder_paths.output_directory
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"images": ("IMAGE", ),
|
|
"filename": ("STRING", {"default": f'%time_%seed', "multiline": False}),
|
|
"path": ("STRING", {"default": '', "multiline": False}),
|
|
"extension": (['png', 'jpeg', 'tiff', 'gif'], ),
|
|
"quality": ("INT", {"default": 100, "min": 1, "max": 100, "step": 1}),
|
|
},
|
|
"optional": {
|
|
"positive": ("STRING",{ "multiline": True, "forceInput": True}, ),
|
|
"negative": ("STRING",{"multiline": True, "forceInput": True}, ),
|
|
"seed": ("INT",{"default": 0, "min": 0, "max": 0xffffffffffffffff, "forceInput": True}),
|
|
"modelname": ("STRING",{"default": '', "multiline": False, "forceInput": True}),
|
|
"counter": ("INT",{"default": 0, "min": 0, "max": 0xffffffffffffffff }),
|
|
"time_format": ("STRING", {"default": "%Y-%m-%d-%H%M%S", "multiline": False}),
|
|
},
|
|
"hidden": {
|
|
"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = ()
|
|
FUNCTION = "save_files"
|
|
|
|
OUTPUT_NODE = True
|
|
|
|
CATEGORY = "WLSH Nodes/IO"
|
|
|
|
def save_files(self, images, positive="unknown", negative="unknown", seed=-1, modelname="unknown", counter=0, filename='', path="",
|
|
time_format="%Y-%m-%d-%H%M%S", extension='png', quality=100, prompt=None, extra_pnginfo=None):
|
|
filename = make_filename(filename, seed, modelname, counter, time_format)
|
|
comment = make_comment(positive, negative, modelname, seed, info=None)
|
|
# comment = "Positive Prompt:\n" + positive + "\nNegative Prompt:\n" + negative + "\nModel: " + modelname + "\nSeed: " + str(seed)
|
|
|
|
output_path = os.path.join(self.output_dir,path)
|
|
|
|
# create missing paths - from WAS Node Suite
|
|
if output_path.strip() != '':
|
|
if not os.path.exists(output_path.strip()):
|
|
print(f'The path `{output_path.strip()}` specified doesn\'t exist! Creating directory.')
|
|
os.makedirs(output_path, exist_ok=True)
|
|
|
|
paths = self.save_images(images, output_path, path, filename,comment, extension, quality, prompt, extra_pnginfo)
|
|
|
|
return { "ui": { "images": paths } }
|
|
|
|
def save_images(self, images, output_path, path, filename_prefix="ComfyUI", comment="", extension='png', quality=100, prompt=None, extra_pnginfo=None):
|
|
def map_filename(filename):
|
|
prefix_len = len(filename_prefix)
|
|
prefix = filename[:prefix_len + 1]
|
|
try:
|
|
digits = int(filename[prefix_len + 1:].split('_')[0])
|
|
except:
|
|
digits = 0
|
|
return (digits, prefix)
|
|
|
|
imgCount = 1
|
|
paths = list()
|
|
for image in images:
|
|
i = 255. * image.cpu().numpy()
|
|
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
|
|
metadata = PngInfo()
|
|
|
|
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]))
|
|
metadata.add_text("parameters", comment)
|
|
metadata.add_text("comment", comment)
|
|
if(images.size()[0] > 1):
|
|
filename_prefix += "_{:02d}".format(imgCount)
|
|
|
|
file = f"{filename_prefix}.{extension}"
|
|
if extension == 'png':
|
|
# print(comment)
|
|
img.save(os.path.join(output_path, file), comment=comment, pnginfo=metadata, optimize=True)
|
|
elif extension == 'webp':
|
|
img.save(os.path.join(output_path, file), quality=quality)
|
|
elif extension == 'jpeg':
|
|
img.save(os.path.join(output_path, file), quality=quality, comment=comment, optimize=True)
|
|
elif extension == 'tiff':
|
|
img.save(os.path.join(output_path, file), quality=quality, optimize=True)
|
|
else:
|
|
img.save(os.path.join(output_path, file))
|
|
paths.append({
|
|
"filename": file,
|
|
"subfolder": path,
|
|
"type": self.type
|
|
})
|
|
imgCount += 1
|
|
return(paths)
|
|
|
|
class WLSH_Image_Save_With_Prompt_Info:
|
|
def __init__(self):
|
|
# get default output directory
|
|
self.type = "output"
|
|
self.output_dir = folder_paths.output_directory
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"images": ("IMAGE", ),
|
|
"filename": ("STRING", {"default": f'%time_%seed', "multiline": False}),
|
|
"path": ("STRING", {"default": '', "multiline": False}),
|
|
"extension": (['png', 'jpeg', 'tiff', 'gif'], ),
|
|
"quality": ("INT", {"default": 100, "min": 1, "max": 100, "step": 1}),
|
|
},
|
|
"optional": {
|
|
"positive": ("STRING",{ "multiline": True, "forceInput": True}, ),
|
|
"negative": ("STRING",{"multiline": True, "forceInput": True}, ),
|
|
"seed": ("INT",{"default": 0, "min": 0, "max": 0xffffffffffffffff, "forceInput": True}),
|
|
"modelname": ("STRING",{"default": '', "multiline": False, "forceInput": True}),
|
|
"counter": ("INT",{"default": 0, "min": 0, "max": 0xffffffffffffffff }),
|
|
"time_format": ("STRING", {"default": "%Y-%m-%d-%H%M%S", "multiline": False}),
|
|
"info": ("INFO",)
|
|
},
|
|
"hidden": {
|
|
"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = ()
|
|
FUNCTION = "save_files"
|
|
|
|
OUTPUT_NODE = True
|
|
|
|
CATEGORY = "WLSH Nodes/IO"
|
|
|
|
def save_files(self, images, positive="unknown", negative="unknown", seed=-1, modelname="unknown", info=None, counter=0, filename='', path="",
|
|
time_format="%Y-%m-%d-%H%M%S", extension='png', quality=100, prompt=None, extra_pnginfo=None):
|
|
filename = make_filename(filename, seed, modelname, counter, time_format)
|
|
comment = make_comment(positive, negative, modelname, seed, info)
|
|
# comment = "Positive Prompt:\n" + positive + "\nNegative Prompt:\n" + negative + "\nModel: " + modelname + "\nSeed: " + str(seed)
|
|
|
|
output_path = os.path.join(self.output_dir,path)
|
|
|
|
# create missing paths - from WAS Node Suite
|
|
if output_path.strip() != '':
|
|
if not os.path.exists(output_path.strip()):
|
|
print(f'The path `{output_path.strip()}` specified doesn\'t exist! Creating directory.')
|
|
os.makedirs(output_path, exist_ok=True)
|
|
|
|
paths = self.save_images(images, output_path, path, filename,comment, extension, quality, prompt, extra_pnginfo)
|
|
|
|
return { "ui": { "images": paths } }
|
|
|
|
def save_images(self, images, output_path, path, filename_prefix="ComfyUI", comment="", extension='png', quality=100, prompt=None, extra_pnginfo=None):
|
|
def map_filename(filename):
|
|
prefix_len = len(filename_prefix)
|
|
prefix = filename[:prefix_len + 1]
|
|
try:
|
|
digits = int(filename[prefix_len + 1:].split('_')[0])
|
|
except:
|
|
digits = 0
|
|
return (digits, prefix)
|
|
|
|
imgCount = 1
|
|
paths = list()
|
|
for image in images:
|
|
i = 255. * image.cpu().numpy()
|
|
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
|
|
metadata = PngInfo()
|
|
|
|
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]))
|
|
metadata.add_text("parameters", comment)
|
|
metadata.add_text("comment", comment)
|
|
if(images.size()[0] > 1):
|
|
filename_prefix += "_{:02d}".format(imgCount)
|
|
|
|
file = f"{filename_prefix}.{extension}"
|
|
if extension == 'png':
|
|
# print(comment)
|
|
img.save(os.path.join(output_path, file), comment=comment, pnginfo=metadata, optimize=True)
|
|
elif extension == 'webp':
|
|
img.save(os.path.join(output_path, file), quality=quality)
|
|
elif extension == 'jpeg':
|
|
img.save(os.path.join(output_path, file), quality=quality, comment=comment, optimize=True)
|
|
elif extension == 'tiff':
|
|
img.save(os.path.join(output_path, file), quality=quality, optimize=True)
|
|
else:
|
|
img.save(os.path.join(output_path, file))
|
|
paths.append({
|
|
"filename": file,
|
|
"subfolder": path,
|
|
"type": self.type
|
|
})
|
|
imgCount += 1
|
|
return(paths)
|
|
|
|
class WLSH_Image_Save_With_File_Info:
|
|
def __init__(self):
|
|
# get default output directory
|
|
self.output_dir = folder_paths.output_directory
|
|
self.type = "output"
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"images": ("IMAGE", ),
|
|
"filename": ("STRING", {"default": f'%time_%seed', "multiline": False}),
|
|
"path": ("STRING", {"default": '', "multiline": False}),
|
|
"extension": (['png', 'jpeg', 'tiff', 'gif'], ),
|
|
"quality": ("INT", {"default": 100, "min": 1, "max": 100, "step": 1}),
|
|
},
|
|
"optional": {
|
|
"positive": ("STRING",{"default": '', "multiline": True, "forceInput": True}),
|
|
"negative": ("STRING",{"default": '', "multiline": True, "forceInput": True}),
|
|
"seed": ("INT",{"default": 0, "min": 0, "max": 0xffffffffffffffff, "forceInput": True}),
|
|
"modelname": ("STRING",{"default": '', "multiline": False, "forceInput": True}),
|
|
"counter": ("INT",{"default": 0, "min": 0, "max": 0xffffffffffffffff }),
|
|
"time_format": ("STRING", {"default": "%Y-%m-%d-%H%M%S", "multiline": False}),
|
|
"info": ("INFO",)
|
|
},
|
|
"hidden": {
|
|
"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = ()
|
|
FUNCTION = "save_files"
|
|
|
|
OUTPUT_NODE = True
|
|
|
|
CATEGORY = "WLSH Nodes/IO"
|
|
|
|
def save_files(self, images, positive="unknown", negative="unknown", seed=-1, modelname="unknown", info=None, counter=0, filename='', path="",
|
|
time_format="%Y-%m-%d-%H%M%S", extension='png', quality=100, prompt=None, extra_pnginfo=None):
|
|
filename = make_filename(filename, seed, modelname, counter, time_format)
|
|
comment = make_comment(positive, negative, modelname, seed, info)
|
|
output_path = os.path.join(self.output_dir,path)
|
|
|
|
# create missing paths - from WAS Node Suite
|
|
if output_path.strip() != '':
|
|
if not os.path.exists(output_path.strip()):
|
|
print(f'The path `{output_path.strip()}` specified doesn\'t exist! Creating directory.')
|
|
os.makedirs(output_path, exist_ok=True)
|
|
|
|
paths = self.save_images(images, output_path, path, filename, comment, extension, quality, prompt, extra_pnginfo)
|
|
self.save_text_file(filename, output_path, comment, seed, modelname)
|
|
#return
|
|
return { "ui": { "images": paths } }
|
|
|
|
def save_images(self, images, output_path, path, filename_prefix="ComfyUI", comment="", extension='png', quality=100, prompt=None, extra_pnginfo=None):
|
|
def map_filename(filename):
|
|
prefix_len = len(filename_prefix)
|
|
prefix = filename[:prefix_len + 1]
|
|
try:
|
|
digits = int(filename[prefix_len + 1:].split('_')[0])
|
|
except:
|
|
digits = 0
|
|
return (digits, prefix)
|
|
|
|
imgCount = 1
|
|
paths = list()
|
|
for image in images:
|
|
i = 255. * image.cpu().numpy()
|
|
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
|
|
metadata = PngInfo()
|
|
|
|
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]))
|
|
metadata.add_text("parameters", comment)
|
|
if(images.size()[0] > 1):
|
|
filename_prefix += "_{:02d}".format(imgCount)
|
|
|
|
file = f"{filename_prefix}.{extension}"
|
|
if extension == 'png':
|
|
# print(comment)
|
|
img.save(os.path.join(output_path, file), comment=comment, pnginfo=metadata, optimize=True)
|
|
elif extension == 'webp':
|
|
img.save(os.path.join(output_path, file), quality=quality)
|
|
elif extension == 'jpeg':
|
|
img.save(os.path.join(output_path, file), quality=quality, comment=comment, optimize=True)
|
|
elif extension == 'tiff':
|
|
img.save(os.path.join(output_path, file), quality=quality, optimize=True)
|
|
else:
|
|
img.save(os.path.join(output_path, file))
|
|
paths.append({
|
|
"filename": file,
|
|
"subfolder": path,
|
|
"type": self.type
|
|
})
|
|
imgCount += 1
|
|
return(paths)
|
|
|
|
def save_text_file(self, filename, output_path, comment="", seed=0, modelname=""):
|
|
# Write text file
|
|
self.writeTextFile(os.path.join(output_path, filename + '.txt'), comment)
|
|
|
|
return
|
|
|
|
# Save Text FileNotFoundError
|
|
def writeTextFile(self, file, content):
|
|
try:
|
|
with open(file, 'w') as f:
|
|
f.write(content)
|
|
except OSError:
|
|
print('Unable to save file `{file}`')
|
|
|
|
class WLSH_Image_Save_With_File:
|
|
def __init__(self):
|
|
# get default output directory
|
|
self.output_dir = folder_paths.output_directory
|
|
self.type = "output"
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"images": ("IMAGE", ),
|
|
"filename": ("STRING", {"default": f'%time_%seed', "multiline": False}),
|
|
"path": ("STRING", {"default": '', "multiline": False}),
|
|
"extension": (['png', 'jpeg', 'tiff', 'gif'], ),
|
|
"quality": ("INT", {"default": 100, "min": 1, "max": 100, "step": 1}),
|
|
},
|
|
"optional": {
|
|
"positive": ("STRING",{"default": '', "multiline": True, "forceInput": True}),
|
|
"negative": ("STRING",{"default": '', "multiline": True, "forceInput": True}),
|
|
"seed": ("INT",{"default": 0, "min": 0, "max": 0xffffffffffffffff, "forceInput": True}),
|
|
"modelname": ("STRING",{"default": '', "multiline": False, "forceInput": True}),
|
|
"counter": ("INT",{"default": 0, "min": 0, "max": 0xffffffffffffffff }),
|
|
"time_format": ("STRING", {"default": "%Y-%m-%d-%H%M%S", "multiline": False}),
|
|
},
|
|
"hidden": {
|
|
"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = ()
|
|
FUNCTION = "save_files"
|
|
|
|
OUTPUT_NODE = True
|
|
|
|
CATEGORY = "WLSH Nodes/IO"
|
|
|
|
def save_files(self, images, positive="unknown", negative="unknown", seed=-1, modelname="unknown", counter=0, filename='', path="",
|
|
time_format="%Y-%m-%d-%H%M%S", extension='png', quality=100, prompt=None, extra_pnginfo=None):
|
|
filename = make_filename(filename, seed, modelname, counter, time_format)
|
|
comment = make_comment(positive, negative, modelname, seed, info=None)
|
|
output_path = os.path.join(self.output_dir,path)
|
|
|
|
# create missing paths - from WAS Node Suite
|
|
if output_path.strip() != '':
|
|
if not os.path.exists(output_path.strip()):
|
|
print(f'The path `{output_path.strip()}` specified doesn\'t exist! Creating directory.')
|
|
os.makedirs(output_path, exist_ok=True)
|
|
|
|
paths = self.save_images(images, output_path, path, filename, comment, extension, quality, prompt, extra_pnginfo)
|
|
self.save_text_file(filename, output_path, comment, seed, modelname)
|
|
#return
|
|
return { "ui": { "images": paths } }
|
|
|
|
def save_images(self, images, output_path, path, filename_prefix="ComfyUI", comment="", extension='png', quality=100, prompt=None, extra_pnginfo=None):
|
|
def map_filename(filename):
|
|
prefix_len = len(filename_prefix)
|
|
prefix = filename[:prefix_len + 1]
|
|
try:
|
|
digits = int(filename[prefix_len + 1:].split('_')[0])
|
|
except:
|
|
digits = 0
|
|
return (digits, prefix)
|
|
|
|
imgCount = 1
|
|
paths = list()
|
|
for image in images:
|
|
i = 255. * image.cpu().numpy()
|
|
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
|
|
metadata = PngInfo()
|
|
|
|
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]))
|
|
metadata.add_text("parameters", comment)
|
|
if(images.size()[0] > 1):
|
|
filename_prefix += "_{:02d}".format(imgCount)
|
|
|
|
file = f"{filename_prefix}.{extension}"
|
|
if extension == 'png':
|
|
# print(comment)
|
|
img.save(os.path.join(output_path, file), comment=comment, pnginfo=metadata, optimize=True)
|
|
elif extension == 'webp':
|
|
img.save(os.path.join(output_path, file), quality=quality)
|
|
elif extension == 'jpeg':
|
|
img.save(os.path.join(output_path, file), quality=quality, comment=comment, optimize=True)
|
|
elif extension == 'tiff':
|
|
img.save(os.path.join(output_path, file), quality=quality, optimize=True)
|
|
else:
|
|
img.save(os.path.join(output_path, file))
|
|
paths.append({
|
|
"filename": file,
|
|
"subfolder": path,
|
|
"type": self.type
|
|
})
|
|
imgCount += 1
|
|
return(paths)
|
|
|
|
def save_text_file(self, filename, output_path, comment="", seed=0, modelname=""):
|
|
# Write text file
|
|
self.writeTextFile(os.path.join(output_path, filename + '.txt'), comment)
|
|
|
|
return
|
|
|
|
# Save Text FileNotFoundError
|
|
def writeTextFile(self, file, content):
|
|
try:
|
|
with open(file, 'w') as f:
|
|
f.write(content)
|
|
except OSError:
|
|
print('Unable to save file `{file}`')
|
|
|
|
class WLSH_Save_Prompt_File:
|
|
def __init__(self):
|
|
# get default output directory
|
|
self.output_dir = folder_paths.output_directory
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"filename": ("STRING",{"default": 'info', "multiline": False}),
|
|
"path": ("STRING", {"default": '', "multiline": False}),
|
|
"positive": ("STRING",{"default": '', "multiline": True, "forceInput": True}),
|
|
},
|
|
"optional": {
|
|
"negative": ("STRING",{"default": '', "multiline": True, "forceInput": True}),
|
|
"modelname": ("STRING",{"default": '', "multiline": False, "forceInput": True}),
|
|
"seed": ("INT",{"default": 0, "min": 0, "max": 0xffffffffffffffff, "forceInput": True}),
|
|
"counter": ("INT",{"default": 0, "min": 0, "max": 0xffffffffffffffff }),
|
|
"time_format": ("STRING", {"default": "%Y-%m-%d-%H%M%S", "multiline": False}),
|
|
}
|
|
}
|
|
|
|
OUTPUT_NODE = True
|
|
RETURN_TYPES = ()
|
|
FUNCTION = "save_text_file"
|
|
|
|
CATEGORY = "WLSH Nodes/IO"
|
|
|
|
def save_text_file(self, positive="", negative="", seed=-1, modelname="unknown", path="", counter=0, time_format="%Y-%m-%d-%H%M%S", filename=""):
|
|
|
|
output_path = os.path.join(self.output_dir,path)
|
|
|
|
# create missing paths - from WAS Node Suite
|
|
if output_path.strip() != '':
|
|
if not os.path.exists(output_path.strip()):
|
|
print(f'The path `{output_path.strip()}` specified doesn\'t exist! Creating directory.')
|
|
os.makedirs(output_path, exist_ok=True)
|
|
|
|
text_data = make_comment(positive, negative, modelname, seed, info=None)
|
|
|
|
|
|
filename = make_filename(filename, seed, modelname, counter, time_format)
|
|
# Write text file
|
|
self.writeTextFile(os.path.join(output_path, filename + '.txt'), text_data)
|
|
|
|
return( text_data, )
|
|
|
|
# Save Text FileNotFoundError
|
|
def writeTextFile(self, file, content):
|
|
try:
|
|
with open(file, 'w') as f:
|
|
f.write(content)
|
|
except OSError:
|
|
print(f'Error: Unable to save file `{file}`')
|
|
|
|
class WLSH_Save_Prompt_File_Info:
|
|
def __init__(self):
|
|
# get default output directory
|
|
self.output_dir = folder_paths.output_directory
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"filename": ("STRING",{"default": 'info', "multiline": False}),
|
|
"path": ("STRING", {"default": '', "multiline": False}),
|
|
"positive": ("STRING",{"default": '', "multiline": True, "forceInput": True}),
|
|
},
|
|
"optional": {
|
|
"negative": ("STRING",{"default": '', "multiline": True, "forceInput": True}),
|
|
"modelname": ("STRING",{"default": '', "multiline": False, "forceInput": True}),
|
|
"seed": ("INT",{"default": 0, "min": 0, "max": 0xffffffffffffffff, "forceInput": True}),
|
|
"counter": ("INT",{"default": 0, "min": 0, "max": 0xffffffffffffffff }),
|
|
"time_format": ("STRING", {"default": "%Y-%m-%d-%H%M%S", "multiline": False}),
|
|
"info": ("INFO",)
|
|
}
|
|
}
|
|
|
|
OUTPUT_NODE = True
|
|
RETURN_TYPES = ()
|
|
FUNCTION = "save_text_file"
|
|
|
|
CATEGORY = "WLSH Nodes/IO"
|
|
|
|
def save_text_file(self, positive="", negative="", seed=-1, modelname="unknown", info=None, path="", counter=0, time_format="%Y-%m-%d-%H%M%S", filename=""):
|
|
|
|
output_path = os.path.join(self.output_dir,path)
|
|
|
|
# create missing paths - from WAS Node Suite
|
|
if output_path.strip() != '':
|
|
if not os.path.exists(output_path.strip()):
|
|
print(f'The path `{output_path.strip()}` specified doesn\'t exist! Creating directory.')
|
|
os.makedirs(output_path, exist_ok=True)
|
|
|
|
text_data = make_comment(positive, negative, modelname, seed, info)
|
|
|
|
|
|
filename = make_filename(filename, seed, modelname, counter, time_format)
|
|
# Write text file
|
|
self.writeTextFile(os.path.join(output_path, filename + '.txt'), text_data)
|
|
|
|
return( text_data, )
|
|
|
|
# Save Text FileNotFoundError
|
|
def writeTextFile(self, file, content):
|
|
try:
|
|
with open(file, 'w') as f:
|
|
f.write(content)
|
|
except OSError:
|
|
print(f'Error: Unable to save file `{file}`')
|
|
|
|
class WLSH_Save_Positive_Prompt_File:
|
|
def __init__(self):
|
|
# get default output directory
|
|
self.output_dir = folder_paths.output_directory
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"filename": ("STRING",{"default": 'info', "multiline": False}),
|
|
"path": ("STRING", {"default": '', "multiline": False}),
|
|
"positive": ("STRING",{"default": '', "multiline": True, "forceInput": True}),
|
|
}
|
|
}
|
|
|
|
OUTPUT_NODE = True
|
|
RETURN_TYPES = ()
|
|
FUNCTION = "save_text_file"
|
|
|
|
CATEGORY = "WLSH Nodes/IO"
|
|
|
|
def save_text_file(self, positive="", path="", filename=""):
|
|
|
|
output_path = os.path.join(self.output_dir,path)
|
|
|
|
# create missing paths - from WAS Node Suite
|
|
if output_path.strip() != '':
|
|
if not os.path.exists(output_path.strip()):
|
|
print(f'The path `{output_path.strip()}` specified doesn\'t exist! Creating directory.')
|
|
os.makedirs(output_path, exist_ok=True)
|
|
|
|
# Ensure content to save, use timestamp if no name given
|
|
if filename.strip == '':
|
|
print(f'Warning: There is no text specified to save! Text is empty. Saving file with timestamp')
|
|
filename = get_timestamp('%Y%m%d%H%M%S')
|
|
|
|
# Write text file after checking for empty prompt
|
|
if positive == "":
|
|
positive ="No prompt data"
|
|
|
|
self.writeTextFile(os.path.join(output_path, filename + '.txt'), positive)
|
|
|
|
return( positive, )
|
|
|
|
# Save Text FileNotFoundError
|
|
def writeTextFile(self, file, content):
|
|
try:
|
|
with open(file, 'w') as f:
|
|
f.write(content)
|
|
except OSError:
|
|
print(f'Error: Unable to save file `{file}`')
|
|
# images
|
|
|
|
class WLSH_Image_Grayscale:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {"required": { "original": ("IMAGE",), }}
|
|
RETURN_TYPES = ("IMAGE",)
|
|
RETURN_NAMES = ("grayscale",)
|
|
FUNCTION = "make_grayscale"
|
|
|
|
CATEGORY = "WLSH Nodes/image"
|
|
|
|
def make_grayscale(self, original):
|
|
image = tensor2pil(original)
|
|
image = ImageOps.grayscale(image)
|
|
image = image.convert("RGB")
|
|
image = np.array(image).astype(np.float32) / 255.0
|
|
image = torch.from_numpy(image)[None,]
|
|
return (image,)
|
|
|
|
|
|
class WLSH_Read_Prompt:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
input_dir = folder_paths.get_input_directory()
|
|
files = [f for f in os.listdir(input_dir) if os.path.isfile(os.path.join(input_dir, f))]
|
|
return {"required":
|
|
{
|
|
"verbose": (["true", "false"],),
|
|
"image": (sorted(files), {"image_upload": True}),
|
|
},
|
|
}
|
|
CATEGORY = "WLSH Nodes/image"
|
|
''' Return order:
|
|
positive prompt(string), negative prompt(string), seed(int), steps(int), cfg(float),
|
|
width(int), height(int)
|
|
'''
|
|
RETURN_TYPES = ("IMAGE", "STRING","STRING","INT", "INT", "FLOAT", "INT","INT")
|
|
RETURN_NAMES = ("image", "positive", "negative", "seed", "steps", "cfg", "width", "height")
|
|
FUNCTION = "get_image_data"
|
|
|
|
def get_image_data(self, image, verbose):
|
|
image_path = folder_paths.get_annotated_filepath(image)
|
|
with open(image_path,'rb') as file:
|
|
img = Image.open(file)
|
|
extension = image_path.split('.')[-1]
|
|
image = img.convert("RGB")
|
|
image = np.array(image).astype(np.float32) / 255.0
|
|
image = torch.from_numpy(image)[None,]
|
|
|
|
parameters = ""
|
|
comfy = False
|
|
if extension.lower() == 'png':
|
|
try:
|
|
parameters = img.info['parameters']
|
|
if not parameters.startswith("Positive prompt"):
|
|
parameters = "Positive prompt: " + parameters
|
|
except:
|
|
parameters = ""
|
|
print("Error loading prompt info from png")
|
|
# return "Error loading prompt info."
|
|
elif extension.lower() in ("jpg", "jpeg", "webp"):
|
|
try:
|
|
exif = piexif.load(img.info["exif"])
|
|
parameters = (exif or {}).get("Exif", {}).get(piexif.ExifIFD.UserComment, b'')
|
|
parameters = piexif.helper.UserComment.load(parameters)
|
|
if not parameters.startswith("Positive prompt"):
|
|
parameters = "Positive prompt: " + parameters
|
|
except:
|
|
try:
|
|
parameters = str(img.info['comment'])
|
|
comfy = True
|
|
# legacy fixes
|
|
parameters = parameters.replace("Positive Prompt", "Positive prompt")
|
|
parameters = parameters.replace("Negative Prompt", "Negative prompt")
|
|
parameters = parameters.replace("Start at Step", "Start at step")
|
|
parameters = parameters.replace("End at Step", "End at step")
|
|
parameters = parameters.replace("Denoising Strength", "Denoising strength")
|
|
except:
|
|
parameters = ""
|
|
print("Error loading prompt info from jpeg")
|
|
# return "Error loading prompt info."
|
|
|
|
if(comfy and extension.lower() == 'jpeg'):
|
|
parameters = parameters.replace('\\n',' ')
|
|
else:
|
|
parameters = parameters.replace('\n',' ')
|
|
|
|
|
|
patterns = [
|
|
"Positive prompt: ",
|
|
"Negative prompt: ",
|
|
"Steps: ",
|
|
"Start at step: ",
|
|
"End at step: ",
|
|
"Sampler: ",
|
|
"Scheduler: ",
|
|
"CFG scale: ",
|
|
"Seed: ",
|
|
"Size: ",
|
|
"Model: ",
|
|
"Model hash: ",
|
|
"Denoising strength: ",
|
|
"Version: ",
|
|
"ControlNet 0",
|
|
"Controlnet 1",
|
|
"Batch size: ",
|
|
"Batch pos: ",
|
|
"Hires upscale: ",
|
|
"Hires steps: ",
|
|
"Hires upscaler: ",
|
|
"Template: ",
|
|
"Negative Template: ",
|
|
]
|
|
if(comfy and extension.lower() == 'jpeg'):
|
|
parameters = parameters[2:]
|
|
parameters = parameters[:-1]
|
|
|
|
keys = re.findall("|".join(patterns), parameters)
|
|
values = re.split("|".join(patterns), parameters)
|
|
values = [x for x in values if x]
|
|
results = {}
|
|
result_string = ""
|
|
for item in range(len(keys)):
|
|
result_string += keys[item] + values[item].rstrip(', ')
|
|
result_string += "\n"
|
|
results[keys[item].replace(": ","")] = values[item].rstrip(', ')
|
|
|
|
if(verbose == "true"):
|
|
print(result_string)
|
|
|
|
try:
|
|
positive = results['Positive prompt']
|
|
except:
|
|
positive = ""
|
|
try:
|
|
negative = results['Negative prompt']
|
|
except:
|
|
negative = ""
|
|
try:
|
|
seed = int(results['Seed'])
|
|
except:
|
|
seed = -1
|
|
try:
|
|
steps = int(results['Steps'])
|
|
except:
|
|
steps = 20
|
|
try:
|
|
cfg = float(results['CFG scale'])
|
|
except:
|
|
cfg = 8.0
|
|
try:
|
|
width,height = img.size
|
|
except:
|
|
width,height = 512,512
|
|
|
|
''' Return order:
|
|
positive prompt(string), negative prompt(string), seed(int), steps(int), cfg(float),
|
|
width(int), height(int)
|
|
'''
|
|
|
|
return(image, positive, negative, seed, steps, cfg, width, height)
|
|
|
|
@classmethod
|
|
def IS_CHANGED(s, image, verbose):
|
|
image_path = folder_paths.get_annotated_filepath(image)
|
|
m = hashlib.sha256()
|
|
with open(image_path, 'rb') as f:
|
|
m.update(f.read())
|
|
return m.digest().hex()
|
|
|
|
@classmethod
|
|
def VALIDATE_INPUTS(s, image, verbose):
|
|
if not folder_paths.exists_annotated_filepath(image):
|
|
return "Invalid image file: {}".format(image)
|
|
|
|
return True
|
|
|
|
|
|
class WLSH_Build_Filename_String:
|
|
def __init__(s):
|
|
pass
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"filename": ("STRING",{"%time_%seed": 'info', "multiline": False}),
|
|
},
|
|
"optional": {
|
|
"modelname": ("STRING",{"default": '', "multiline": False}),
|
|
"seed": ("INT",{"default": 0, "min": 0, "max": 0xffffffffffffffff }),
|
|
"counter": ("SEED",{"default": 0}),
|
|
"time_format": ("STRING", {"default": "%Y-%m-%d-%H%M%S", "multiline": False}),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("STRING",)
|
|
RETURN_NAMES = ("filename",)
|
|
FUNCTION = "build_filename"
|
|
|
|
CATEGORY = "WLSH Nodes/text"
|
|
|
|
def build_filename(self, filename="ComfyUI", modelname="model", time_format="%Y-%m-%d-%H%M%S", seed=0, counter=0):
|
|
|
|
filename = make_filename(filename,seed,modelname,counter,time_format)
|
|
return(filename)
|
|
|
|
NODE_CLASS_MAPPINGS = {
|
|
#loaders
|
|
"Checkpoint Loader w/Name (WLSH)": WLSH_Checkpoint_Loader_Model_Name,
|
|
#samplers
|
|
"KSamplerAdvanced (WLSH)": WLSH_KSamplerAdvanced,
|
|
# "Alternating KSampler (WLSH)": WLSH_Alternating_KSamplerAdvanced,
|
|
#conditioning
|
|
"CLIP Positive-Negative (WLSH)": WLSH_CLIP_Positive_Negative,
|
|
"CLIP Positive-Negative w/Text (WLSH)": WLSH_CLIP_Text_Positive_Negative,
|
|
"CLIP Positive-Negative XL (WLSH)": WLSH_CLIP_Positive_Negative_XL,
|
|
"CLIP Positive-Negative XL w/Text (WLSH)": WLSH_CLIP_Text_Positive_Negative_XL,
|
|
"CLIP +/- w/Text Unified (WLSH)": WLSH_CLIP_Text_Unified,
|
|
#latent
|
|
"Empty Latent by Pixels (WLSH)": WLSH_Empty_Latent_Image_By_Pixels,
|
|
"Empty Latent by Ratio (WLSH)" : WLSH_Empty_Latent_Image_By_Ratio,
|
|
"Empty Latent by Size (WLSH)": WLSH_Empty_Latent_Image_By_Resolution,
|
|
"SDXL Quick Empty Latent (WLSH)" : WLSH_SDXL_Quick_Empty_Latent,
|
|
#image
|
|
"Image Load with Metadata (WLSH)": WLSH_Read_Prompt,
|
|
"Grayscale Image (WLSH)": WLSH_Image_Grayscale,
|
|
#inpainting
|
|
"Generate Border Mask (WLSH)": WLSH_Generate_Edge_Mask,
|
|
"Outpaint to Image (WLSH)": WLSH_Outpaint_To_Image,
|
|
"VAE Encode for Inpaint w/Padding (WLSH)": WLSH_VAE_Encode_For_Inpaint_Padding,
|
|
#upscaling
|
|
"Image Scale By Factor (WLSH)": WLSH_Image_Scale_By_Factor,
|
|
"Image Scale by Shortside (WLSH)": WLSH_Image_Scale_By_Shortside,
|
|
"SDXL Quick Image Scale (WLSH)": WLSH_SDXL_Quick_Image_Scale,
|
|
"Upscale by Factor with Model (WLSH)": WLSH_Upscale_By_Factor_With_Model,
|
|
#numbers
|
|
"Multiply Integer (WLSH)": WLSH_Int_Multiply,
|
|
"Quick Resolution Multiply (WLSH)": WLSH_Res_Multiply,
|
|
"Resolutions by Ratio (WLSH)": WLSH_Resolutions_by_Ratio,
|
|
"Seed to Number (WLSH)": WLSH_Seed_to_Number,
|
|
"Seed and Int (WLSH)": WLSH_Seed_and_Int,
|
|
"SDXL Steps (WLSH)": WLSH_SDXL_Steps,
|
|
"SDXL Resolutions (WLSH)": WLSH_SDXL_Resolutions,
|
|
#text
|
|
"Build Filename String (WLSH)": WLSH_Build_Filename_String,
|
|
"Time String (WLSH)": WLSH_Time_String,
|
|
"Simple Pattern Replace (WLSH)": WLSH_Simple_Pattern_Replace,
|
|
"Simple String Combine (WLSH)": WLSH_String_Append,
|
|
"Prompt Weight (WLSH)": WLSH_Prompt_Weight,
|
|
#IO
|
|
"Image Save with Prompt (WLSH)": WLSH_Image_Save_With_Prompt,
|
|
"Image Save with Prompt/Info (WLSH)": WLSH_Image_Save_With_Prompt_Info,
|
|
"Image Save with Prompt File (WLSH)": WLSH_Image_Save_With_File,
|
|
"Image Save with Prompt/Info File (WLSH)": WLSH_Image_Save_With_File_Info,
|
|
"Save Prompt (WLSH)": WLSH_Save_Prompt_File,
|
|
"Save Prompt/Info (WLSH)": WLSH_Save_Prompt_File_Info,
|
|
"Save Positive Prompt(WLSH)": WLSH_Save_Positive_Prompt_File
|
|
}
|