diff --git a/chibi_nodes.py b/chibi_nodes.py index 5c6098d..eb5be6b 100644 --- a/chibi_nodes.py +++ b/chibi_nodes.py @@ -5,26 +5,32 @@ import comfy.utils import comfy.sample import comfy.samplers from comfy.cli_args import args -from PIL import Image, ImageOps, ImageFont, ImageDraw, ExifTags +from PIL import Image, ImageOps, ImageFont, ImageDraw from PIL.PngImagePlugin import PngInfo import numpy as np import re import random import os import time -import json +import json import math import hashlib import latent_preview -### GLOBALS ### -MAX_RESOLUTION=32768 + +# GLOBALS +MAX_RESOLUTION = 32768 base_path = os.path.dirname(os.path.realpath(__file__)) extras_dir = os.path.join(base_path, "extras") -folder_paths.folder_names_and_paths["chibi-wildcards"] = ([os.path.join(extras_dir, "chibi-wildcards")], {".txt"}) -folder_paths.folder_names_and_paths["chibi-fonts"] = ([os.path.join(extras_dir, "fonts")], {".ttf"}) - +folder_paths.folder_names_and_paths["chibi-wildcards"] = ( + [os.path.join(extras_dir, "chibi-wildcards")], + {".txt"}, +) +folder_paths.folder_names_and_paths["chibi-fonts"] = ( + [os.path.join(extras_dir, "fonts")], + {".ttf"}, +) class Loader: @@ -33,27 +39,48 @@ class Loader: @classmethod def INPUT_TYPES(s): - return {"required":{ - "Checkpoint": (folder_paths.get_filename_list("checkpoints"), ), - "Vae": (["Included"] + folder_paths.get_filename_list("vae"), ), - "stop_at_clip_layer": ("INT", {"default": -1, "min": -24, "max": -1, "step": 1}), - "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 { + "required": { + "Checkpoint": (folder_paths.get_filename_list("checkpoints"),), + "Vae": (["Included"] + folder_paths.get_filename_list("vae"),), + "stop_at_clip_layer": ( + "INT", + {"default": -1, "min": -24, "max": -1, "step": 1}, + ), + "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 = ("MODEL","VAE","CLIP","LATENT",) + RETURN_TYPES = ( + "MODEL", + "VAE", + "CLIP", + "LATENT", + ) FUNCTION = "loader" CATEGORY = "Chibi-Nodes" - def loader(self, Checkpoint,Vae,stop_at_clip_layer,width,height,batch_size): + def loader(self, Checkpoint, Vae, stop_at_clip_layer, width, height, batch_size): ckpt_path = folder_paths.get_full_path("checkpoints", Checkpoint) output_vae = False - + if Vae == "Included": output_vae = True - - ckpt = comfy.sd.load_checkpoint_guess_config(ckpt_path, output_vae=output_vae, output_clip=True, embedding_directory=folder_paths.get_folder_paths("embeddings")) + + ckpt = comfy.sd.load_checkpoint_guess_config( + ckpt_path, + output_vae=output_vae, + output_clip=True, + embedding_directory=folder_paths.get_folder_paths("embeddings"), + ) if Vae == "Included": vae = ckpt[:3][2] @@ -66,8 +93,7 @@ class Loader: latent = torch.zeros([batch_size, 4, height // 8, width // 8]) - return(ckpt[:3][0],vae,clip,{"samples":latent}) - + return (ckpt[:3][0], vae, clip, {"samples": latent}) class Prompts: @@ -78,32 +104,57 @@ class Prompts: def INPUT_TYPES(s): return { "required": { - - "Positive": ("STRING", {"default": "Positive Prompt","multiline": True}), - "Negative": ("STRING", {"default": "Negative Prompt","multiline": True}), - - }, - "optional":{ - "clip": ("CLIP",), - }, - + "Positive": ( + "STRING", + {"default": "Positive Prompt", "multiline": True}, + ), + "Negative": ( + "STRING", + {"default": "Negative Prompt", "multiline": True}, + ), + }, + "optional": { + "clip": ("CLIP",), + }, } - RETURN_TYPES = ("CONDITIONING","CONDITIONING","CLIP","STRING","STRING") - RETURN_NAMES = ("Positive CONDITIONING", "Negative CONDITIONING", "CLIP", "Positive text", "Negative text") + RETURN_TYPES = ("CONDITIONING", "CONDITIONING", "CLIP", "STRING", "STRING") + RETURN_NAMES = ( + "Positive CONDITIONING", + "Negative CONDITIONING", + "CLIP", + "Positive text", + "Negative text", + ) FUNCTION = "prompts" CATEGORY = "Chibi-Nodes" - def prompts(self, Positive, Negative, clip=None,): + def prompts( + self, + Positive, + Negative, + clip=None, + ): if clip: pos_cond_raw = clip.tokenize(Positive) neg_cond_raw = clip.tokenize(Negative) - pos_cond, pos_pooled = clip.encode_from_tokens(pos_cond_raw, return_pooled=True) - neg_cond, neg_pooled = clip.encode_from_tokens(neg_cond_raw, return_pooled=True) - - return ([[pos_cond, {"pooled_output": pos_pooled}]],[[neg_cond, {"pooled_output": neg_pooled}]],clip,Positive,Negative) + pos_cond, pos_pooled = clip.encode_from_tokens( + pos_cond_raw, return_pooled=True + ) + neg_cond, neg_pooled = clip.encode_from_tokens( + neg_cond_raw, return_pooled=True + ) + + return ( + [[pos_cond, {"pooled_output": pos_pooled}]], + [[neg_cond, {"pooled_output": neg_pooled}]], + clip, + Positive, + Negative, + ) else: - return (None, None, None, Positive,Negative) + return (None, None, None, Positive, Negative) + class ImageTool: def __init__(self): @@ -113,15 +164,21 @@ class ImageTool: def INPUT_TYPES(s): return { "required": { - "image": ("IMAGE",), - "width": ("INT", {"default": 1920, "min": 16, "max": MAX_RESOLUTION, "step": 1}), - "height": ("INT", {"default": 1080, "min": 16, "max": MAX_RESOLUTION, "step": 1}), - "crop": ([False,True],), - "rotate": ("INT", {"default": 0, "min": 0, "max": 360, "step": 1}), - "mirror": ([False,True],), - "flip":([False,True],), - "bgcolor": (["black","white"],), - }, + "image": ("IMAGE",), + "width": ( + "INT", + {"default": 1920, "min": 16, "max": MAX_RESOLUTION, "step": 1}, + ), + "height": ( + "INT", + {"default": 1080, "min": 16, "max": MAX_RESOLUTION, "step": 1}, + ), + "crop": ([False, True],), + "rotate": ("INT", {"default": 0, "min": 0, "max": 360, "step": 1}), + "mirror": ([False, True],), + "flip": ([False, True],), + "bgcolor": (["black", "white"],), + }, } RETURN_TYPES = ("IMAGE",) @@ -129,37 +186,40 @@ class ImageTool: CATEGORY = "Chibi-Nodes/Image" def imagetools(self, image, height, width, crop, rotate, mirror, flip, bgcolor): - image = Image.fromarray(np.clip(255. * image[0].cpu().numpy(),0,255).astype(np.uint8)) - image = image.rotate(rotate,fillcolor=bgcolor) + image = Image.fromarray( + np.clip(255.0 * image[0].cpu().numpy(), 0, 255).astype(np.uint8) + ) + image = image.rotate(rotate, fillcolor=bgcolor) - #black and white - #corrections? - #generate mask from background color (crop, rotate) + # black and white + # corrections? + # generate mask from background color (crop, rotate) if mirror: image = ImageOps.mirror(image) - + if flip: image = ImageOps.flip(image) if crop: - im_width, im_height = image.size - left = (im_width - width)/2 - top = (im_height - height)/2 - right = (im_width + width)/2 - bottom = (im_height + height)/2 - image = image.crop((left, top, right, bottom)) - + im_width, im_height = image.size + left = (im_width - width) / 2 + top = (im_height - height) / 2 + right = (im_width + width) / 2 + bottom = (im_height + height) / 2 + image = image.crop((left, top, right, bottom)) + else: - image = image.resize((width,height), Image.LANCZOS) + image = image.resize((width, height), Image.LANCZOS) image = ImageOps.exif_transpose(image) image = image.convert("RGB") image = np.array(image).astype(np.float32) / 255.0 image = torch.from_numpy(image)[None,] - - return(image,) - + + return (image,) + + class Wildcards: def __init__(self): pass @@ -167,110 +227,184 @@ class Wildcards: @classmethod def INPUT_TYPES(s): return { - "required":{ - "textfile" : [sorted(folder_paths.get_filename_list("chibi-wildcards"))], - "keyword":("STRING", {"default": "__wildcard__","multiline": False}), - "entries_returned": ("INT", {"default": 1, "min": 1, "max": 10, "step": 1}), + "required": { + "textfile": [sorted(folder_paths.get_filename_list("chibi-wildcards"))], + "keyword": ("STRING", {"default": "__wildcard__", "multiline": False}), + "entries_returned": ( + "INT", + {"default": 1, "min": 1, "max": 10, "step": 1}, + ), }, - "optional":{ + "optional": { "clip": ("CLIP",), - "text" : ("STRING",{"default": '', "multiline": False, "forceInput": True}), + "seed": ("INT", {"forceInput": True}), + "text": ( + "STRING", + {"default": "", "multiline": False, "forceInput": True}, + ), + }, } - RETURN_TYPES = ("CONDITIONING","STRING",) - RETURN_NAMES = ("CONDITIONING","text",) + + RETURN_TYPES = ( + "CONDITIONING", + "STRING", + ) + RETURN_NAMES = ( + "CONDITIONING", + "text", + ) FUNCTION = "wildcards" CATEGORY = "Chibi-Nodes" - seed = random.seed() - + # if seed is not 8008135: + # random.seed(seed) + # else: + # random.seed() - def IS_CHANGED(s,seed): - seed = random.seed() + def IS_CHANGED(s, seed): + if seed is not None: + random.seed(seed) + else: + random.seed() + + def wildcards( + self, + textfile, + keyword, + entries_returned, + clip=None, + seed=None, + text="", + ): + if seed is not None: + random.seed(seed) + else: + random.seed() - def wildcards(self, textfile,keyword,entries_returned,clip=None,text='',): entries = "" with open(folder_paths.get_full_path("chibi-wildcards", textfile)) as f: lines = f.readlines() - for i in range(0,entries_returned): + for i in range(0, entries_returned): aline = random.choice(lines).rstrip() if entries == "": entries = aline else: - entries = entries + " " + aline - + entries = entries + " " + aline + aline = entries - if text != '': - raw = text.replace(keyword,aline) + if text != "": + raw = text.replace(keyword, aline) if clip: cond_raw = clip.tokenize(raw) - cond, pooled = clip.encode_from_tokens(cond_raw, return_pooled=True) - return([[cond, {"pooled_output": pooled}]],raw,) + cond, pooled = clip.encode_from_tokens( + cond_raw, return_pooled=True) + return ( + [[cond, {"pooled_output": pooled}]], + raw, + ) else: - return(None,raw,) - + return ( + None, + raw, + ) + else: if clip: cond_raw = clip.tokenize(aline) - cond, pooled = clip.encode_from_tokens(cond_raw, return_pooled=True) - return([[cond, {"pooled_output": pooled}]],aline,) + cond, pooled = clip.encode_from_tokens( + cond_raw, return_pooled=True) + return ( + [[cond, {"pooled_output": pooled}]], + aline, + ) else: - return(None,aline,) + return ( + None, + aline, + ) + - class LoadEmbedding: def __init__(self): pass @classmethod def INPUT_TYPES(s): - return{"required":{ - "text" : ("STRING",{"default": '', "multiline": False, "forceInput": True}), - "embedding":[sorted(folder_paths.get_filename_list("embeddings"),)], - "weight": ("FLOAT", {"default": 1.0, "min": -2, "max": 2, "step": 0.1, "round": 0.01}), - - }, - "hidden":{ - "preview_image": ("IMAGE",) - }} - - RETURN_TYPES = ("STRING","IMAGE",) - RETURN_NAMES = ("text","Preview Image") + return { + "required": { + "text": ( + "STRING", + {"default": "", "multiline": False, "forceInput": True}, + ), + "embedding": [ + sorted( + folder_paths.get_filename_list("embeddings"), + ) + ], + "weight": ( + "FLOAT", + {"default": 1.0, "min": -2, "max": 2, + "step": 0.1, "round": 0.01}, + ), + }, + "hidden": {"preview_image": ("IMAGE",)}, + } + + RETURN_TYPES = ( + "STRING", + "IMAGE", + ) + RETURN_NAMES = ("text", "Preview Image") FUNCTION = "loadembedding" CATEGORY = "Chibi-Nodes" - - def loadembedding(self, text, embedding,weight, preview_image=None): + def loadembedding(self, text, embedding, weight, preview_image=None): output = text + ", (embedding:" + embedding + ":" + str(weight) + ")" file_path = folder_paths.get_full_path("embeddings", embedding) file_ext = file_path.split(".", 1)[1] - if os.path.exists(folder_paths.get_full_path("embeddings", embedding).replace(f".{file_ext}", ".preview.png")): - img_path = folder_paths.get_full_path("embeddings", embedding).replace(f".{file_ext}",".preview.png") + if os.path.exists( + folder_paths.get_full_path("embeddings", embedding).replace( + f".{file_ext}", ".preview.png" + ) + ): + img_path = folder_paths.get_full_path("embeddings", embedding).replace( + f".{file_ext}", ".preview.png" + ) + # if os.path.exists( + # folder_paths.get_full_path("embeddings", embedding).replace( + # ".pt", ".preview.png" + # ) + # ): + + # print( + # folder_paths.get_full_path("embeddings", embedding).replace( + # f".{file_ext}", ".preview.png" + # ) + # ) image = Image.open(img_path) image = ImageOps.exif_transpose(image) image = image.convert("RGB") image = np.array(image).astype(np.float32) / 255.0 preview_image = torch.from_numpy(image)[None,] - - return (output,preview_image) - + + return (output, preview_image) + else: - W, H = (256,256) - image = Image.new('RGB', (W, H), (255, 255, 255)) + W, H = (256, 256) + image = Image.new("RGB", (W, H), (255, 255, 255)) imaget = ImageDraw.Draw(image) msg = "No Preview" - imaget.text(((W-60)/2,H/2),msg,(0,0,0)) - + imaget.text(((W - 60) / 2, H / 2), msg, (0, 0, 0)) image = np.array(image).astype(np.float32) / 255.0 preview_image = torch.from_numpy(image)[None,] - - return (output,preview_image) - + + return (output, preview_image) class ConditionTextMulti: @@ -282,64 +416,125 @@ class ConditionTextMulti: return { "required": { "clip": ("CLIP",), - }, - "optional":{ - "first" : ("STRING", {"default": '', "multiline": False, "forceInput": True}), - "second" : ("STRING", {"default": '', "multiline": False, "forceInput": True}), - "third" : ("STRING", {"default": '', "multiline": False, "forceInput": True}), - "fourth" : ("STRING", {"default": '', "multiline": False, "forceInput": True}), - } + }, + "optional": { + "first": ( + "STRING", + {"default": "", "multiline": False, "forceInput": True}, + ), + "second": ( + "STRING", + {"default": "", "multiline": False, "forceInput": True}, + ), + "third": ( + "STRING", + {"default": "", "multiline": False, "forceInput": True}, + ), + "fourth": ( + "STRING", + {"default": "", "multiline": False, "forceInput": True}, + ), + }, } - RETURN_TYPES = ("CLIP","CONDITIONING","CONDITIONING","CONDITIONING","CONDITIONING",) - RETURN_NAMES = ("CLIP","first","second","third","fourth",) + RETURN_TYPES = ( + "CLIP", + "CONDITIONING", + "CONDITIONING", + "CONDITIONING", + "CONDITIONING", + ) + RETURN_NAMES = ( + "CLIP", + "first", + "second", + "third", + "fourth", + ) FUNCTION = "conditiontext" CATEGORY = "Chibi-Nodes/Text" - def conditiontext(self, clip, first='', second='', third='', fourth='', ): + def conditiontext( + self, + clip, + first="", + second="", + third="", + fourth="", + ): emptystring = "" returnedcond = [] - #!I probably want to fix this mess at some point. - if first != '': + # I probably want to fix this mess at some point. + if first != "": firstraw = clip.tokenize(first) - first_cond, first_pooled = clip.encode_from_tokens(firstraw, return_pooled=True) - returnedcond.append([[first_cond, {"pooled_output": first_pooled}]]) + first_cond, first_pooled = clip.encode_from_tokens( + firstraw, return_pooled=True + ) + returnedcond.append( + [[first_cond, {"pooled_output": first_pooled}]]) else: emptyraw = clip.tokenize(emptystring) - empty_cond, empty_pooled = clip.encode_from_tokens(emptyraw, return_pooled=True) - returnedcond.append([[empty_cond, {"pooled_output": empty_pooled}]]) + empty_cond, empty_pooled = clip.encode_from_tokens( + emptyraw, return_pooled=True + ) + returnedcond.append( + [[empty_cond, {"pooled_output": empty_pooled}]]) - if second != '': + if second != "": secondraw = clip.tokenize(second) - second_cond, second_pooled = clip.encode_from_tokens(secondraw, return_pooled=True) - returnedcond.append([[second_cond, {"pooled_output": second_pooled}]]) + second_cond, second_pooled = clip.encode_from_tokens( + secondraw, return_pooled=True + ) + returnedcond.append( + [[second_cond, {"pooled_output": second_pooled}]]) else: emptyraw = clip.tokenize(emptystring) - empty_cond, empty_pooled = clip.encode_from_tokens(emptyraw, return_pooled=True) - returnedcond.append([[empty_cond, {"pooled_output": empty_pooled}]]) + empty_cond, empty_pooled = clip.encode_from_tokens( + emptyraw, return_pooled=True + ) + returnedcond.append( + [[empty_cond, {"pooled_output": empty_pooled}]]) - if third != '': + if third != "": thirdraw = clip.tokenize(third) - third_cond, third_pooled = clip.encode_from_tokens(thirdraw, return_pooled=True) - returnedcond.append([[third_cond, {"pooled_output": third_pooled}]]) + third_cond, third_pooled = clip.encode_from_tokens( + thirdraw, return_pooled=True + ) + returnedcond.append( + [[third_cond, {"pooled_output": third_pooled}]]) else: emptyraw = clip.tokenize(emptystring) - empty_cond, empty_pooled = clip.encode_from_tokens(emptyraw, return_pooled=True) - returnedcond.append([[empty_cond, {"pooled_output": empty_pooled}]]) + empty_cond, empty_pooled = clip.encode_from_tokens( + emptyraw, return_pooled=True + ) + returnedcond.append( + [[empty_cond, {"pooled_output": empty_pooled}]]) - if fourth != '': + if fourth != "": fourthraw = clip.tokenize(fourth) - fourth_cond, fourth_pooled = clip.encode_from_tokens(fourthraw, return_pooled=True) - returnedcond.append([[fourth_cond, {"pooled_output": fourth_pooled}]]) + fourth_cond, fourth_pooled = clip.encode_from_tokens( + fourthraw, return_pooled=True + ) + returnedcond.append( + [[fourth_cond, {"pooled_output": fourth_pooled}]]) else: emptyraw = clip.tokenize(emptystring) - empty_cond, empty_pooled = clip.encode_from_tokens(emptyraw, return_pooled=True) - returnedcond.append([[empty_cond, {"pooled_output": empty_pooled}]]) + empty_cond, empty_pooled = clip.encode_from_tokens( + emptyraw, return_pooled=True + ) + returnedcond.append( + [[empty_cond, {"pooled_output": empty_pooled}]]) + + return ( + clip, + returnedcond[0], + returnedcond[1], + returnedcond[2], + returnedcond[3], + ) + - - return (clip,returnedcond[0],returnedcond[1],returnedcond[2],returnedcond[3],) - class ConditionText: def __init__(self): pass @@ -349,23 +544,33 @@ class ConditionText: return { "required": { "clip": ("CLIP",), - "text" : ("STRING", {"forceInput": True},), - }} + "text": ( + "STRING", + {"forceInput": True}, + ), + } + } - RETURN_TYPES = ("CLIP","CONDITIONING",) + RETURN_TYPES = ( + "CLIP", + "CONDITIONING", + ) FUNCTION = "conditiontext" CATEGORY = "Chibi-Nodes/Text" - def conditiontext(self, clip, text=None ): + def conditiontext(self, clip, text=None): - if text != None: + if text is not None: tokens = clip.tokenize(text) else: tokens = clip.tokenize("") cond, pooled = clip.encode_from_tokens(tokens, return_pooled=True) + return ( + clip, + [[cond, {"pooled_output": pooled}]], + ) - return (clip,[[cond, {"pooled_output": pooled}]],) class SaveImages: def __init__(self): @@ -375,47 +580,72 @@ class SaveImages: def INPUT_TYPES(s): return { "required": { - "filename_type":(["Timestamp","Fixed","Fixed Single"],), - "fixed_filename":("STRING",{"default":"output",}) - - }, - "optional":{ - "images" : ("IMAGE",), - "latents" : ("LATENT",), - "vae" : ("VAE",), - "fixed_filename_override":("STRING",{"forceInput": True},) - }, - "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"}, + "filename_type": (["Timestamp", "Fixed", "Fixed Single"],), + "fixed_filename": ( + "STRING", + { + "default": "output", + }, + ), + }, + "optional": { + "images": ("IMAGE",), + "latents": ("LATENT",), + "vae": ("VAE",), + "fixed_filename_override": ( + "STRING", + {"forceInput": True}, + ), + }, + "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"}, } - - RETURN_TYPES = ("IMAGE","STRING",) - RETURN_NAMES = ("images","filename_list",) + + RETURN_TYPES = ( + "IMAGE", + "STRING", + ) + RETURN_NAMES = ( + "images", + "filename_list", + ) FUNCTION = "saveimage" OUTPUT_NODE = True CATEGORY = "Chibi-Nodes" - - seed = random.seed() - - def IS_CHANGED(s,seed): - seed = random.seed() + def IS_CHANGED(s,): + random.seed() - def saveimage(self,filename_type,fixed_filename,fixed_filename_override=None,vae=None, latents=None, images=None, prompt=None, extra_pnginfo=None): - if fixed_filename_override != None: - fixed_filename_override = fixed_filename_override.rsplit(".",1)[0] + def saveimage( + self, + filename_type, + fixed_filename, + fixed_filename_override=None, + vae=None, + latents=None, + images=None, + prompt=None, + extra_pnginfo=None, + ): + if fixed_filename_override is not None: + fixed_filename_override = fixed_filename_override.rsplit(".", 1)[0] fixed_filename = fixed_filename_override now = str(round(time.time())) - - results = list() filename_list = [] counter = 0 - if images != None: - full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(now, folder_paths.get_output_directory(), images[0].shape[1], images[0].shape[0]) + if images is not None: + full_output_folder, filename, counter, subfolder, filename_prefix = ( + folder_paths.get_save_image_path( + now, + folder_paths.get_output_directory(), + images[0].shape[1], + images[0].shape[0], + ) + ) for image in images: - i = 255. * image.cpu().numpy() + i = 255.0 * image.cpu().numpy() img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8)) metadata = None if not args.disable_metadata: @@ -425,7 +655,7 @@ class SaveImages: if extra_pnginfo is not None: for x in extra_pnginfo: metadata.add_text(x, json.dumps(extra_pnginfo[x])) - + if filename_type == "Timestamp": file = f"{now}_{counter:03}.png" if filename_type == "Fixed": @@ -434,21 +664,30 @@ class SaveImages: file = f"{fixed_filename}.png" filename_list.append(file) - img.save(os.path.join(full_output_folder, file), pnginfo=metadata, compress_level=4) - results.append({ - "filename": file, - "subfolder": subfolder, - "type": "output" - }) + img.save( + os.path.join(full_output_folder, file), + pnginfo=metadata, + compress_level=4, + ) + results.append( + {"filename": file, "subfolder": subfolder, "type": "output"} + ) counter += 1 return_results = images - if vae != None: - if latents != None: - + if vae is not None: + if latents is not None: + decoded_latents = vae.decode(latents["samples"]) - full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(now, folder_paths.get_output_directory(), decoded_latents[0].shape[1], decoded_latents[0].shape[0]) + full_output_folder, filename, counter, subfolder, filename_prefix = ( + folder_paths.get_save_image_path( + now, + folder_paths.get_output_directory(), + decoded_latents[0].shape[1], + decoded_latents[0].shape[0], + ) + ) for latent in decoded_latents: - i = 255. * latent.cpu().numpy() + i = 255.0 * latent.cpu().numpy() img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8)) metadata = None if not args.disable_metadata: @@ -457,7 +696,8 @@ class SaveImages: 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( + x, json.dumps(extra_pnginfo[x])) if filename_type == "Timestamp": file = f"{now}_{counter:03}.png" @@ -467,18 +707,25 @@ class SaveImages: file = f"{fixed_filename}.png" filename_list.append(file) - img.save(os.path.join(full_output_folder, file), pnginfo=metadata, compress_level=4) - results.append({ - "filename": file, - "subfolder": subfolder, - "type": "output" - }) + img.save( + os.path.join(full_output_folder, file), + pnginfo=metadata, + compress_level=4, + ) + results.append( + {"filename": file, "subfolder": subfolder, "type": "output"} + ) counter += 1 return_results = decoded_latents # return { "ui": { "images": results }} - return {"ui": { "images": results },"result": (return_results,str(filename_list),)} - + return { + "ui": {"images": results}, + "result": ( + return_results, + str(filename_list), + ), + } class Textbox: @@ -489,27 +736,38 @@ class Textbox: def INPUT_TYPES(s): return { "required": { - "text":("STRING", {"default": '',"multiline": True,"forceInput": False,"print_to_screen": True}), - + "text": ( + "STRING", + { + "default": "", + "multiline": True, + "forceInput": False, + "print_to_screen": True, + }, + ), + }, + "optional": { + "passthrough": ( + "STRING", + {"default": "", "multiline": True, "forceInput": True}, + ) }, - "optional": { - "passthrough":("STRING", {"default": "","multiline": True,"forceInput": True}) - }, } - + RETURN_TYPES = ("STRING",) RETURN_NAMES = ("text",) OUTPUT_NODE = True FUNCTION = "textbox" CATEGORY = "Chibi-Nodes/Text" - def textbox(self,text="",passthrough=""): + def textbox(self, text="", passthrough=""): if passthrough != "": text = passthrough - return {"ui": {"text": text},"result": (text,)} + return {"ui": {"text": text}, "result": (text,)} else: return (text,) + class ImageSizeInfo: def __init__(self): pass @@ -517,15 +775,23 @@ class ImageSizeInfo: @classmethod def INPUT_TYPES(s): return { - "required":{ - "image": ("IMAGE",) - }, - "hidden":{ + "required": {"image": ("IMAGE",)}, + "hidden": { "width": ("INT",), "height": ("INT",), - }} - RETURN_TYPES = ("IMAGE","INT","INT",) - RETURN_NAMES = ("IMAGE","width","height",) + }, + } + + RETURN_TYPES = ( + "IMAGE", + "INT", + "INT", + ) + RETURN_NAMES = ( + "IMAGE", + "width", + "height", + ) OUTPUT_NODE = True FUNCTION = "imagesizeinfo" CATEGORY = "Chibi-Nodes/Image" @@ -534,7 +800,14 @@ class ImageSizeInfo: shape = image.shape width = shape[2] height = shape[1] - return {"ui": {"width": [width], "height": [height]},"result": (image,width,height,)} + return { + "ui": {"width": [width], "height": [height]}, + "result": ( + image, + width, + height, + ), + } class ImageSimpleResize: @@ -544,16 +817,21 @@ class ImageSimpleResize: @classmethod def INPUT_TYPES(s): return { - "required":{ + "required": { "image": ("IMAGE",), - "size": ("INT", {"default": 512, "min": 16, "max": MAX_RESOLUTION, "step": 1}), - "edge":(["largest","smallest","all","width","height"],), + "size": ( + "INT", + {"default": 512, "min": 16, "max": MAX_RESOLUTION, "step": 1}, + ), + "edge": (["largest", "smallest", "all", "width", "height"],), }, - "optional":{ - "size_override":("INT",{"forceInput": True}), - "vae":("VAE",) - }} - RETURN_TYPES = ("IMAGE","LATENT") + "optional": { + "size_override": ("INT", {"forceInput": True}), + "vae": ("VAE",), + }, + } + + RETURN_TYPES = ("IMAGE", "LATENT") OUTPUT_NODE = False FUNCTION = "imagesimpleresize" CATEGORY = "Chibi-Nodes/Image" @@ -565,73 +843,98 @@ class ImageSimpleResize: width = image.shape[2] height = image.shape[1] ratio = height / width - image = Image.fromarray(np.clip(255. * image[0].cpu().numpy(),0,255).astype(np.uint8)) + image = Image.fromarray( + np.clip(255.0 * image[0].cpu().numpy(), 0, 255).astype(np.uint8) + ) if edge == "largest": if width > height: if size < width: - image = ImageOps.contain(image, (size,MAX_RESOLUTION), Image.LANCZOS) + image = ImageOps.contain( + image, (size, MAX_RESOLUTION), Image.LANCZOS + ) else: - image = image.resize((round(size),round(size*ratio)), Image.LANCZOS) + image = image.resize( + (round(size), round(size * ratio)), Image.LANCZOS + ) if width < height: if size < height: - image = ImageOps.contain(image, (MAX_RESOLUTION,size), Image.LANCZOS) + image = ImageOps.contain( + image, (MAX_RESOLUTION, size), Image.LANCZOS + ) else: - image = image.resize((round(size/ratio),round(size)), Image.LANCZOS) + image = image.resize( + (round(size / ratio), round(size)), Image.LANCZOS + ) if width == height: if size < width: - image = ImageOps.contain(image, (size,size), Image.LANCZOS) + image = ImageOps.contain( + image, (size, size), Image.LANCZOS) else: - image = image.resize((round(size),round(size)), Image.LANCZOS) - + image = image.resize( + (round(size), round(size)), Image.LANCZOS) if edge == "smallest": if width > height: if size < height: - image = ImageOps.contain(image, (MAX_RESOLUTION,size), Image.LANCZOS) + image = ImageOps.contain( + image, (MAX_RESOLUTION, size), Image.LANCZOS + ) else: - image = image.resize((round(size/ratio),round(size)), Image.LANCZOS) + image = image.resize( + (round(size / ratio), round(size)), Image.LANCZOS + ) if width < height: if size < width: - image = ImageOps.contain(image, (size,MAX_RESOLUTION), Image.LANCZOS) + image = ImageOps.contain( + image, (size, MAX_RESOLUTION), Image.LANCZOS + ) else: - image = image.resize((round(size),round(size*ratio)), Image.LANCZOS) + image = image.resize( + (round(size), round(size * ratio)), Image.LANCZOS + ) if width == height: if size < width: - image = ImageOps.contain(image, (size,size), Image.LANCZOS) + image = ImageOps.contain( + image, (size, size), Image.LANCZOS) else: - image = image.resize((round(size),round(size)), Image.LANCZOS) + image = image.resize( + (round(size), round(size)), Image.LANCZOS) if edge == "all": - image = image.resize((round(size),round(size)), Image.LANCZOS) + image = image.resize((round(size), round(size)), Image.LANCZOS) if edge == "width": - image = image.resize((round(size),round(height)), Image.LANCZOS) + image = image.resize((round(size), round(height)), Image.LANCZOS) if edge == "height": - image = image.resize((round(width),round(size)), Image.LANCZOS) - + image = image.resize((round(width), round(size)), Image.LANCZOS) image = ImageOps.exif_transpose(image) image = image.convert("RGB") image = np.array(image).astype(np.float32) / 255.0 image = torch.from_numpy(image)[None,] - - if vae != None: + + if vae is not None: latent = image x = (latent.shape[1] // 8) * 8 y = (latent.shape[2] // 8) * 8 - if latent.shape[1] != x or latent.shape[2] != y: + if latent.shape[1] is not x or latent.shape[2] is not y: x_offset = (latent.shape[1] % 8) // 2 y_offset = (latent.shape[2] % 8) // 2 - latent = latent[:, x_offset:x + x_offset, y_offset:y + y_offset, :] - latent = vae.encode(latent[:,:,:,:3]) + latent = latent[:, x_offset: x + + x_offset, y_offset: y + y_offset, :] + latent = vae.encode(latent[:, :, :, :3]) - return (image,{"samples":latent}) + return (image, {"samples": latent}) else: - return(image,None,) - + return ( + image, + None, + ) + + class Int2String: def __init__(self): pass @@ -639,16 +942,16 @@ class Int2String: @classmethod def INPUT_TYPES(s): return { - "required":{ - "Int":("INT",{"forceInput": True}) - },} + "required": {"Int": ("INT", {"forceInput": True})}, + } + RETURN_TYPES = ("STRING",) OUTPUT_NODE = False FUNCTION = "int2string" CATEGORY = "Chibi-Nodes/Text" def int2string(self, Int): - return(str(Int),) + return (str(Int),) class LoadImageExtended: @@ -659,58 +962,78 @@ class LoadImageExtended: @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": - {"image": (sorted(files), {"image_upload": True}), - }, - "optional":{"vae": ("VAE", )} - } + files = [ + f + for f in os.listdir(input_dir) + if os.path.isfile(os.path.join(input_dir, f)) + ] + return { + "required": { + "image": (sorted(files), {"image_upload": True}), + }, + "optional": {"vae": ("VAE",)}, + } CATEGORY = "Chibi-Nodes/Image" - #changes here - RETURN_TYPES = ("IMAGE", "MASK", "LATENT", "STRING", "STRING","INT","INT",) - RETURN_NAMES = ("IMAGE", "MASK", "LATENT","filename","image Info","width","height",) + # changes here + RETURN_TYPES = ( + "IMAGE", + "MASK", + "LATENT", + "STRING", + "STRING", + "INT", + "INT", + ) + RETURN_NAMES = ( + "IMAGE", + "MASK", + "LATENT", + "filename", + "image Info", + "width", + "height", + ) # FUNCTION = "load_image" def load_image(self, image, vae=None): - image_path = folder_paths.get_annotated_filepath(image) - filename = image_path.rsplit('/',1)[-1] + filename = image_path.rsplit("/", 1)[-1] im = Image.open(image_path) - ### Start ai-info.py section, with no exif + # Start ai-info.py section, with no exif def type_changer(value): if value.isnumeric(): return int(value) else: return value - - im.load() prompt = {} if "prompt" in im.info.keys(): - #comfyui, workflow is also available but we aren't getting that today + # comfyui, workflow is also available but we aren't getting that today # prompt = {} - prompt.update({"prompt":json.loads(im.info["prompt"])}) + prompt.update({"prompt": json.loads(im.info["prompt"])}) else: # automatic111, gosh this is a mess. if "parameters" in im.info.keys(): parameters = im.info["parameters"] prompt = {"parameters": {}} - parameters = re.split('(Negative prompt): |(Negative Template): |(Template): |(ControlNet): |\n',parameters) + parameters = re.split( + "(Negative prompt): |(Negative Template): |(Template): |(ControlNet): |\n", + parameters, + ) - - #removes None and new lines + # removes None and new lines parameters_clean_none = [] - for i in range(0,len(parameters)): - if parameters[i] == None: + for i in range(0, len(parameters)): + if parameters[i] is None: pass elif parameters[i] == "": pass @@ -718,62 +1041,64 @@ class LoadImageExtended: parameters_clean_none.append(parameters[i]) parameters = parameters_clean_none - - #settings field + # settings field parameters_settings = {} - for i in range(0,len(parameters)): - if parameters[i].split(":",1)[0] == "Steps": - parameters[i] = re.split(", ",parameters[i]) + for i in range(0, len(parameters)): + if parameters[i].split(":", 1)[0] == "Steps": + parameters[i] = re.split(", ", parameters[i]) for k in parameters[i]: - k = k.split(": ",1) + k = k.split(": ", 1) if len(k) == 2: k[1] = type_changer(k[1]) - #makes "Size" : "(widthxheight)" into two keys + # makes "Size" : "(widthxheight)" into two keys if k[0] == "Size": k[1] = k[1].split("x") - for s in range(0,len(k[1])): + for s in range(0, len(k[1])): k[1][s] = type_changer(k[1][s]) - parameters_settings.update({"width" : k[1][0]}) - parameters_settings.update({"height" : k[1][1]}) - - else: - parameters_settings.update({k[0]:k[1]}) + parameters_settings.update( + {"width": k[1][0]}) + parameters_settings.update( + {"height": k[1][1]}) + else: + parameters_settings.update({k[0]: k[1]}) parameters[i] = parameters_settings - - #builder + + # builder parameters_built = {} - for i in range(0,len(parameters)): + for i in range(0, len(parameters)): match parameters[i]: case "Negative prompt": - parameters_built.update({parameters[i]:parameters[i+1]}) + parameters_built.update( + {parameters[i]: parameters[i + 1]}) case "Negative Template": - parameters_built.update({parameters[i]:parameters[i+1]}) + parameters_built.update( + {parameters[i]: parameters[i + 1]}) case "Template": - parameters_built.update({parameters[i]:parameters[i+1]}) + parameters_built.update( + {parameters[i]: parameters[i + 1]}) case "ControlNet": - parameters_built.update({parameters[i]:parameters[i+1]}) + parameters_built.update( + {parameters[i]: parameters[i + 1]}) case dict(): parameters_built.update(parameters[i]) case _: if i == 0: - parameters_built.update({"Positive prompt": parameters[i]}) + parameters_built.update( + {"Positive prompt": parameters[i]} + ) pass - - prompt["parameters"] = parameters_built - if type(prompt) == dict: - prompt = json.dumps(prompt,indent=2) + if type(prompt) is dict: + prompt = json.dumps(prompt, indent=2) - elif type(prompt) == str: - prompt = json.dumps(json.loads(prompt),indent=2) - - - ###end section + elif type(prompt) is str: + prompt = json.dumps(json.loads(prompt), indent=2) + # end section im = ImageOps.exif_transpose(im) image = im.convert("RGB") @@ -782,31 +1107,48 @@ class LoadImageExtended: shape = image.shape width = shape[2] height = shape[1] - if 'A' in im.getbands(): - mask = np.array(im.getchannel('A')).astype(np.float32) / 255.0 - mask = 1. - torch.from_numpy(mask) + if "A" in im.getbands(): + mask = np.array(im.getchannel("A")).astype(np.float32) / 255.0 + mask = 1.0 - torch.from_numpy(mask) else: - mask = torch.zeros((64,64), dtype=torch.float32, device="cpu") - if vae != None: + mask = torch.zeros((64, 64), dtype=torch.float32, device="cpu") + if vae is not None: latent = image x = (latent.shape[1] // 8) * 8 y = (latent.shape[2] // 8) * 8 - if latent.shape[1] != x or latent.shape[2] != y: + if latent.shape[1] is not x or latent.shape[2] is not y: x_offset = (latent.shape[1] % 8) // 2 y_offset = (latent.shape[2] % 8) // 2 - latent = latent[:, x_offset:x + x_offset, y_offset:y + y_offset, :] - latent = vae.encode(latent[:,:,:,:3]) + latent = latent[:, x_offset: x + + x_offset, y_offset: y + y_offset, :] + latent = vae.encode(latent[:, :, :, :3]) - return (image, mask.unsqueeze(0),{"samples":latent},filename,str(prompt),width,height,) + return ( + image, + mask.unsqueeze(0), + {"samples": latent}, + filename, + str(prompt), + width, + height, + ) else: - return (image, mask.unsqueeze(0),None,filename,str(prompt),width,height,) + return ( + image, + mask.unsqueeze(0), + None, + filename, + str(prompt), + width, + height, + ) @classmethod def IS_CHANGED(s, image, vae=None): image_path = folder_paths.get_annotated_filepath(image) m = hashlib.sha256() - with open(image_path, 'rb') as f: + with open(image_path, "rb") as f: m.update(f.read()) return m.digest().hex() @@ -816,8 +1158,6 @@ class LoadImageExtended: return "Invalid image file: {}".format(image) return True - - class SimpleSampler: @@ -826,32 +1166,55 @@ class SimpleSampler: @classmethod def INPUT_TYPES(s): - return {"required": + return { + "required": { + "model": ("MODEL",), + "sampler": ( + [ + "Normal - euler", + "Normal - uni_pc", + "LCM Lora - lcm", + "SDXL Turbo - dpmpp_sde karras", + ], + ), + "positive": ("CONDITIONING",), + "negative": ("CONDITIONING",), + "latents": ("LATENT",), + "mode": (["txt2img", "img2img"],), + }, + "optional": { + "seed": ( + "INT", { - "model": ("MODEL",), - "sampler":(["Normal - euler","Normal - uni_pc","LCM Lora - lcm","SDXL Turbo - dpmpp_sde karras"],), - - "positive": ("CONDITIONING", ), - "negative": ("CONDITIONING", ), - "latents": ("LATENT", ), - "mode": (["txt2img","img2img"],) - - }, - "optional": { - "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff,"forceInput": True}), - } - } + "forceInput": True, + }, + ), + }, + } RETURN_TYPES = ("LATENT",) FUNCTION = "sample" CATEGORY = "Chibi-Nodes" + def IS_CHANGED(s, seed): + if seed is not None: + random.seed(seed) + else: + random.seed() - def IS_CHANGED(s,seed): - seed = random.seed() - - def sample(self, model,sampler, positive, negative, latents, mode,seed=None, scheduler="normal",sampler_name="euler"): + def sample( + self, + model, + sampler, + positive, + negative, + latents, + mode, + seed=None, + scheduler="normal", + sampler_name="euler", + ): # ['euler', 'euler_ancestral', 'heun', 'heunpp2', 'dpm_2', 'dpm_2_ancestral', 'lms', 'dpm_fast', 'dpm_adaptive','dpmpp_2s_ancestral', 'dpmpp_sde', 'dpmpp_sde_gpu', 'dpmpp_2m', 'dpmpp_2m_sde', 'dpmpp_2m_sde_gpu', 'dpmpp_3m_sde', 'dpmpp_3m_sde_gpu', 'ddpm', 'lcm', 'ddim', 'uni_pc', 'uni_pc_bh2'] # ['normal', 'karras', 'exponential', 'sgm_uniform', 'simple', 'ddim_uniform'] @@ -886,10 +1249,10 @@ class SimpleSampler: case _: denoise = 1.0 - - - if seed == None: - seed = random.seed() + if seed is not None: + random.seed(seed) + else: + random.seed() seed = math.floor(random.random() * 10000000000000000) latent_image = latents["samples"] @@ -901,16 +1264,33 @@ class SimpleSampler: if "noise_mask" in latents: noise_mask = latents["noise_mask"] - callback = latent_preview.prepare_callback(model, steps) - samples = comfy.sample.sample(model=model, noise=noise, steps=steps, cfg=cfg, sampler_name=sampler_name, scheduler=scheduler, positive=positive, negative=negative, latent_image=latent_image, - denoise=denoise, disable_noise=False, start_step=0, last_step=steps, - force_full_denoise=True, noise_mask=noise_mask, callback=callback, disable_pbar=False, seed=seed) + samples = comfy.sample.sample( + model=model, + noise=noise, + steps=steps, + cfg=cfg, + sampler_name=sampler_name, + scheduler=scheduler, + positive=positive, + negative=negative, + latent_image=latent_image, + denoise=denoise, + disable_noise=False, + start_step=0, + last_step=steps, + force_full_denoise=True, + noise_mask=noise_mask, + callback=callback, + disable_pbar=False, + seed=seed, + ) out = latents.copy() out["samples"] = samples return (out,) + class SeedGenerator: def __init__(self): pass @@ -918,93 +1298,156 @@ class SeedGenerator: @classmethod def INPUT_TYPES(s): return { - "required":{ - "mode":(["Random","Fixed"],), - "fixed_seed":("INT",{"default": 8008135, "min": 0, "max": 0xffffffffffffffff, "step": 1}) + "required": { + "mode": (["Random", "Fixed"],), + "fixed_seed": ( + "INT", + { + "default": 8008135, + "min": 0, + "max": 0xFFFFFFFFFFFFFFFF, + "step": 1, + }, + ), + }, + } - },} - RETURN_TYPES = ("INT","STRING",) - RETURN_NAMES = ("seed","text") + RETURN_TYPES = ( + "INT", + "STRING", + ) + RETURN_NAMES = ("seed", "text") OUTPUT_NODE = False FUNCTION = "generator" CATEGORY = "Chibi-Nodes/Numbers" + def IS_CHANGED(s): + random.seed() - def IS_CHANGED(s,fixed_seed): - seed = random.seed() - - def generator(self, mode,fixed_seed): + def generator(self, mode, fixed_seed): if mode == "Random": - fixed_seed = math.floor(random.random() * 10000000000000000) + random_seed = math.floor(random.random() * 10000000000000000) + return ( + random_seed, + str(random_seed), + ) if mode == "Fixed": - fixed_seed = fixed_seed - - return(fixed_seed,str(fixed_seed),) - + return ( + fixed_seed, + str(fixed_seed), + ) + + class ImageAddText: def __init__(self): pass @classmethod def INPUT_TYPES(s): - return{ - "required":{ - - "text": ("STRING",{"default":"Chibi-Nodes","multiline":True},), - "font" : [sorted(folder_paths.get_filename_list("chibi-fonts"))], - "font_size":("INT",{"default": 24, "min": 0, "max": 200, "step": 1}), - "font_colour":(["black","white","red","green","blue"],), - "invert_mask":([False,True],), - "position_x":("INT",{"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 1}), - "position_y":("INT",{"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 1}), - "width":("INT",{"default": 512, "min": 0, "max": MAX_RESOLUTION, "step": 1}), - "height":("INT",{"default": 512, "min": 0, "max": MAX_RESOLUTION, "step": 1}) + return { + "required": { + "text": ( + "STRING", + {"default": "Chibi-Nodes", "multiline": True}, + ), + "font": [sorted(folder_paths.get_filename_list("chibi-fonts"))], + "font_size": ("INT", {"default": 24, "min": 0, "max": 200, "step": 1}), + "font_colour": (["black", "white", "red", "green", "blue"],), + "invert_mask": ([False, True],), + "position_x": ( + "INT", + {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 1}, + ), + "position_y": ( + "INT", + {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 1}, + ), + "width": ( + "INT", + {"default": 512, "min": 0, "max": MAX_RESOLUTION, "step": 1}, + ), + "height": ( + "INT", + {"default": 512, "min": 0, "max": MAX_RESOLUTION, "step": 1}, + ), + }, + "optional": { + "image": ("IMAGE",), }, - "optional":{ - "image": ("IMAGE",), - } } - - RETURN_TYPES = ("IMAGE","MASK","STRING",) - RETURN_NAMES = ("IMAGE","MASK","text",) + + RETURN_TYPES = ( + "IMAGE", + "MASK", + "STRING", + ) + RETURN_NAMES = ( + "IMAGE", + "MASK", + "text", + ) FUNCTION = "addtext" CATEGORY = "Chibi-Nodes/Image" - def addtext(self, text, width, height,font,font_size,position_x,position_y,font_colour,invert_mask,image=None): - if image != None: - width = image.shape[2] - height = image.shape[1] - image = Image.fromarray(np.clip(255. * image[0].cpu().numpy(),0,255).astype(np.uint8)) - image = image.convert("RGBA") - else: - image = Image.new('RGBA', (width,height), (255, 255, 255, 0)) + def addtext( + self, + text, + width, + height, + font, + font_size, + position_x, + position_y, + font_colour, + invert_mask, + image=None, + ): + if image is not None: + width = image.shape[2] + height = image.shape[1] + image = Image.fromarray( + np.clip(255.0 * image[0].cpu().numpy(), + 0, 255).astype(np.uint8) + ) + image = image.convert("RGBA") + else: + image = Image.new("RGBA", (width, height), (255, 255, 255, 0)) - text_image = Image.new('RGBA', (width,height), (0, 255, 255, 0)) - imaget = ImageDraw.Draw(text_image,) + text_image = Image.new("RGBA", (width, height), (0, 255, 255, 0)) + imaget = ImageDraw.Draw( + text_image, + ) - msg = text - imaget.fontmode = 'L' - fnt = ImageFont.truetype(folder_paths.get_full_path("chibi-fonts", font), font_size) + msg = text + imaget.fontmode = "L" + fnt = ImageFont.truetype( + folder_paths.get_full_path("chibi-fonts", font), font_size + ) - imaget.text((position_x,position_y),msg,font=fnt,fill=font_colour) + imaget.text((position_x, position_y), msg, font=fnt, fill=font_colour) + + if "A" in text_image.getbands(): + mask = np.array(text_image.getchannel( + "A")).astype(np.float32) / 255.0 + mask = 1.0 - torch.from_numpy(mask) + else: + mask = torch.zeros((64, 64), dtype=torch.float32, device="cpu") + image.paste(text_image, (0, 0), text_image) + image = ImageOps.exif_transpose(image) + image = image.convert("RGB") + image = np.array(image).astype(np.float32) / 255.0 + image = torch.from_numpy(image)[None,] + + if invert_mask: + mask = 1.0 - mask + + return ( + image, + mask.unsqueeze(0), + text, + ) - if 'A' in text_image.getbands(): - mask = np.array(text_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") - image.paste(text_image,(0,0),text_image) - image = ImageOps.exif_transpose(image) - image = image.convert("RGB") - image = np.array(image).astype(np.float32) / 255.0 - image = torch.from_numpy(image)[None,] - if invert_mask: - mask = 1.0 - mask - - - return (image,mask.unsqueeze(0),text,) - class TextSplit: def __init__(self): pass @@ -1012,24 +1455,32 @@ class TextSplit: @classmethod def INPUT_TYPES(s): return { - "required":{ - "text":("STRING",{"default":"","forceInput": True},), - "separator":("STRING",{"default":"."},), - "reverse": ([False,True],), - "return_half":(["First Half","Second Half"],), - },} + "required": { + "text": ( + "STRING", + {"default": "", "forceInput": True}, + ), + "separator": ( + "STRING", + {"default": "."}, + ), + "reverse": ([False, True],), + "return_half": (["First Half", "Second Half"],), + }, + } + RETURN_TYPES = ("STRING",) RETURN_NAMES = ("text",) OUTPUT_NODE = True FUNCTION = "dosplit" CATEGORY = "Chibi-Nodes/Text" - def dosplit(self, text,separator,reverse,return_half): - if reverse == True: - text = text.rsplit(separator,1) - if reverse == False: - text = text.split(separator,1) - + def dosplit(self, text, separator, reverse, return_half): + if reverse: + text = text.rsplit(separator, 1) + else: + text = text.split(separator, 1) + if len(text) == 2: if return_half == "First Half": text = text[0] @@ -1046,11 +1497,21 @@ class RandomResolutionLatent: @classmethod def INPUT_TYPES(s): return { - "required":{ + "required": { "batch_size": ("INT", {"default": 1, "min": 1, "max": 4096}), - },} - RETURN_TYPES = ("LATENT","INT","INT",) - RETURN_NAMES = ("LATENT","width","height",) + }, + } + + RETURN_TYPES = ( + "LATENT", + "INT", + "INT", + ) + RETURN_NAMES = ( + "LATENT", + "width", + "height", + ) OUTPUT_NODE = True FUNCTION = "randres" CATEGORY = "Chibi-Nodes/Numbers" @@ -1058,44 +1519,48 @@ class RandomResolutionLatent: def IS_CHANGED(s): random.seed() - def randres(self,batch_size): - resolutions = [512,768,1024] + def randres(self, batch_size): + resolutions = [512, 768, 1024] res_list = [] for x in resolutions: for y in resolutions: - a = (x,y) - b = (y,x) - if not a in res_list: + a = (x, y) + b = (y, x) + if a not in res_list: res_list.append(a) - if not b in res_list: + if b not in res_list: res_list.append(b) + rand_res = random.choice(res_list) + latent = torch.zeros( + [batch_size, 4, rand_res[0] // 8, rand_res[1] // 8]) - rand_res = random.choice(res_list) - latent = torch.zeros([batch_size, 4, rand_res[0]//8, rand_res[1]//8]) + return ( + {"samples": latent}, + rand_res[0], + rand_res[1], + ) - return({"samples":latent},rand_res[0],rand_res[1],) - NODE_CLASS_MAPPINGS = { - "Loader":Loader, - "SimpleSampler" : SimpleSampler, + "Loader": Loader, + "SimpleSampler": SimpleSampler, "Prompts": Prompts, "ImageTool": ImageTool, "Wildcards": Wildcards, "LoadEmbedding": LoadEmbedding, - "ConditionText": ConditionText, - "ConditionTextMulti": ConditionTextMulti, - "Textbox":Textbox, - "ImageSizeInfo" : ImageSizeInfo, - "ImageSimpleResize" : ImageSimpleResize, - "ImageAddText" : ImageAddText, + "ConditionText": ConditionText, + "ConditionTextMulti": ConditionTextMulti, + "Textbox": Textbox, + "ImageSizeInfo": ImageSizeInfo, + "ImageSimpleResize": ImageSimpleResize, + "ImageAddText": ImageAddText, "Int2String": Int2String, "LoadImageExtended": LoadImageExtended, - "SeedGenerator" : SeedGenerator, - "SaveImages":SaveImages, + "SeedGenerator": SeedGenerator, + "SaveImages": SaveImages, "TextSplit": TextSplit, "RandomResolutionLatent": RandomResolutionLatent, }