Files
chibiace-ComfyUI-Chibi-Nodes/chibi_nodes.py
T
chibiace 290ccef2a8 changes
added seed to wildcards, probably broke a bunch of other stuff.
2024-07-29 21:18:36 +12:00

1567 lines
47 KiB
Python

import torch
import folder_paths
import comfy.sd
import comfy.utils
import comfy.sample
import comfy.samplers
from comfy.cli_args import args
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 math
import hashlib
import latent_preview
# 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"},
)
class Loader:
def __init__(self):
pass
@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_TYPES = (
"MODEL",
"VAE",
"CLIP",
"LATENT",
)
FUNCTION = "loader"
CATEGORY = "Chibi-Nodes"
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"),
)
if Vae == "Included":
vae = ckpt[:3][2]
else:
vae_path = folder_paths.get_full_path("vae", Vae)
vae = comfy.sd.VAE(sd=comfy.utils.load_torch_file(vae_path))
clip = ckpt[:3][1].clone()
clip.clip_layer(stop_at_clip_layer)
latent = torch.zeros([batch_size, 4, height // 8, width // 8])
return (ckpt[:3][0], vae, clip, {"samples": latent})
class Prompts:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"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",
)
FUNCTION = "prompts"
CATEGORY = "Chibi-Nodes"
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,
)
else:
return (None, None, None, Positive, Negative)
class ImageTool:
def __init__(self):
pass
@classmethod
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"],),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "imagetools"
CATEGORY = "Chibi-Nodes/Image"
def imagetools(self, image, height, width, crop, rotate, mirror, flip, 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)
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))
else:
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,)
class Wildcards:
def __init__(self):
pass
@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},
),
},
"optional": {
"clip": ("CLIP",),
"seed": ("INT", {"forceInput": True}),
"text": (
"STRING",
{"default": "", "multiline": False, "forceInput": True},
),
},
}
RETURN_TYPES = (
"CONDITIONING",
"STRING",
)
RETURN_NAMES = (
"CONDITIONING",
"text",
)
FUNCTION = "wildcards"
CATEGORY = "Chibi-Nodes"
# if seed is not 8008135:
# random.seed(seed)
# else:
# 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()
entries = ""
with open(folder_paths.get_full_path("chibi-wildcards", textfile)) as f:
lines = f.readlines()
for i in range(0, entries_returned):
aline = random.choice(lines).rstrip()
if entries == "":
entries = aline
else:
entries = entries + " " + aline
aline = entries
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,
)
else:
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,
)
else:
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")
FUNCTION = "loadembedding"
CATEGORY = "Chibi-Nodes"
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(
# ".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)
else:
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))
image = np.array(image).astype(np.float32) / 255.0
preview_image = torch.from_numpy(image)[None,]
return (output, preview_image)
class ConditionTextMulti:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(s):
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},
),
},
}
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="",
):
emptystring = ""
returnedcond = []
# 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}]])
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}]])
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}]])
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}]])
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}]])
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}]])
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}]])
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}]])
return (
clip,
returnedcond[0],
returnedcond[1],
returnedcond[2],
returnedcond[3],
)
class ConditionText:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"clip": ("CLIP",),
"text": (
"STRING",
{"forceInput": True},
),
}
}
RETURN_TYPES = (
"CLIP",
"CONDITIONING",
)
FUNCTION = "conditiontext"
CATEGORY = "Chibi-Nodes/Text"
def conditiontext(self, clip, 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}]],
)
class SaveImages:
def __init__(self):
pass
@classmethod
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"},
}
RETURN_TYPES = (
"IMAGE",
"STRING",
)
RETURN_NAMES = (
"images",
"filename_list",
)
FUNCTION = "saveimage"
OUTPUT_NODE = True
CATEGORY = "Chibi-Nodes"
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 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 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.0 * image.cpu().numpy()
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
metadata = None
if not args.disable_metadata:
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]))
if filename_type == "Timestamp":
file = f"{now}_{counter:03}.png"
if filename_type == "Fixed":
file = f"{fixed_filename}_{counter:03}.png"
if filename_type == "Fixed Single":
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"}
)
counter += 1
return_results = images
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],
)
)
for latent in decoded_latents:
i = 255.0 * latent.cpu().numpy()
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
metadata = None
if not args.disable_metadata:
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]))
if filename_type == "Timestamp":
file = f"{now}_{counter:03}.png"
if filename_type == "Fixed":
file = f"{fixed_filename}_{counter:03}.png"
if filename_type == "Fixed Single":
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"}
)
counter += 1
return_results = decoded_latents
# return { "ui": { "images": results }}
return {
"ui": {"images": results},
"result": (
return_results,
str(filename_list),
),
}
class Textbox:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"text": (
"STRING",
{
"default": "",
"multiline": True,
"forceInput": False,
"print_to_screen": 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=""):
if passthrough != "":
text = passthrough
return {"ui": {"text": text}, "result": (text,)}
else:
return (text,)
class ImageSizeInfo:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(s):
return {
"required": {"image": ("IMAGE",)},
"hidden": {
"width": ("INT",),
"height": ("INT",),
},
}
RETURN_TYPES = (
"IMAGE",
"INT",
"INT",
)
RETURN_NAMES = (
"IMAGE",
"width",
"height",
)
OUTPUT_NODE = True
FUNCTION = "imagesizeinfo"
CATEGORY = "Chibi-Nodes/Image"
def imagesizeinfo(self, image, width=0, height=0):
shape = image.shape
width = shape[2]
height = shape[1]
return {
"ui": {"width": [width], "height": [height]},
"result": (
image,
width,
height,
),
}
class ImageSimpleResize:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"image": ("IMAGE",),
"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")
OUTPUT_NODE = False
FUNCTION = "imagesimpleresize"
CATEGORY = "Chibi-Nodes/Image"
def imagesimpleresize(self, image, size, edge, size_override=None, vae=None):
if size_override:
size = size_override
width = image.shape[2]
height = image.shape[1]
ratio = height / width
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
)
else:
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
)
else:
image = image.resize(
(round(size / ratio), round(size)), Image.LANCZOS
)
if width == height:
if size < width:
image = ImageOps.contain(
image, (size, size), Image.LANCZOS)
else:
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
)
else:
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
)
else:
image = image.resize(
(round(size), round(size * ratio)), Image.LANCZOS
)
if width == height:
if size < width:
image = ImageOps.contain(
image, (size, size), Image.LANCZOS)
else:
image = image.resize(
(round(size), round(size)), Image.LANCZOS)
if edge == "all":
image = image.resize((round(size), round(size)), Image.LANCZOS)
if edge == "width":
image = image.resize((round(size), round(height)), Image.LANCZOS)
if edge == "height":
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 is not None:
latent = image
x = (latent.shape[1] // 8) * 8
y = (latent.shape[2] // 8) * 8
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])
return (image, {"samples": latent})
else:
return (
image,
None,
)
class Int2String:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(s):
return {
"required": {"Int": ("INT", {"forceInput": True})},
}
RETURN_TYPES = ("STRING",)
OUTPUT_NODE = False
FUNCTION = "int2string"
CATEGORY = "Chibi-Nodes/Text"
def int2string(self, Int):
return (str(Int),)
class LoadImageExtended:
def __init__(self):
pass
@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",)},
}
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",
)
#
FUNCTION = "load_image"
def load_image(self, image, vae=None):
image_path = folder_paths.get_annotated_filepath(image)
filename = image_path.rsplit("/", 1)[-1]
im = Image.open(image_path)
# 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
# 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,
)
# removes None and new lines
parameters_clean_none = []
for i in range(0, len(parameters)):
if parameters[i] is None:
pass
elif parameters[i] == "":
pass
else:
parameters_clean_none.append(parameters[i])
parameters = parameters_clean_none
# 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 k in parameters[i]:
k = k.split(": ", 1)
if len(k) == 2:
k[1] = type_changer(k[1])
# makes "Size" : "(widthxheight)" into two keys
if k[0] == "Size":
k[1] = k[1].split("x")
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[i] = parameters_settings
# builder
parameters_built = {}
for i in range(0, len(parameters)):
match parameters[i]:
case "Negative prompt":
parameters_built.update(
{parameters[i]: parameters[i + 1]})
case "Negative Template":
parameters_built.update(
{parameters[i]: parameters[i + 1]})
case "Template":
parameters_built.update(
{parameters[i]: parameters[i + 1]})
case "ControlNet":
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]}
)
pass
prompt["parameters"] = parameters_built
if type(prompt) is dict:
prompt = json.dumps(prompt, indent=2)
elif type(prompt) is str:
prompt = json.dumps(json.loads(prompt), indent=2)
# end section
im = ImageOps.exif_transpose(im)
image = im.convert("RGB")
image = np.array(image).astype(np.float32) / 255.0
image = torch.from_numpy(image)[None,]
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.0 - torch.from_numpy(mask)
else:
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] 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])
return (
image,
mask.unsqueeze(0),
{"samples": latent},
filename,
str(prompt),
width,
height,
)
else:
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:
m.update(f.read())
return m.digest().hex()
@classmethod
def VALIDATE_INPUTS(s, image, vae=None):
if not folder_paths.exists_annotated_filepath(image):
return "Invalid image file: {}".format(image)
return True
class SimpleSampler:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(s):
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",
{
"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 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']
match sampler:
case "Normal - euler":
sampler_name = "uni_pc"
steps = 20
cfg = 7
case "Normal - uni_pc":
sampler_name = "uni_pc"
steps = 20
cfg = 7
case "LCM Lora - lcm":
sampler_name = "lcm"
steps = 8
cfg = 1.8
case "SDXL Turbo - dpmpp_sde karras":
sampler_name = "ddmpp_sde"
steps = 8
cfg = 1.8
scheduler = "karras"
case _:
steps = 20
cfg = 7
match mode:
case "txt2img":
denoise = 1.0
case "img2img":
denoise = 0.6
case _:
denoise = 1.0
if seed is not None:
random.seed(seed)
else:
random.seed()
seed = math.floor(random.random() * 10000000000000000)
latent_image = latents["samples"]
batch_inds = latents["batch_index"] if "batch_index" in latents else None
noise = comfy.sample.prepare_noise(latent_image, seed, batch_inds)
noise_mask = None
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,
)
out = latents.copy()
out["samples"] = samples
return (out,)
class SeedGenerator:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"mode": (["Random", "Fixed"],),
"fixed_seed": (
"INT",
{
"default": 8008135,
"min": 0,
"max": 0xFFFFFFFFFFFFFFFF,
"step": 1,
},
),
},
}
RETURN_TYPES = (
"INT",
"STRING",
)
RETURN_NAMES = ("seed", "text")
OUTPUT_NODE = False
FUNCTION = "generator"
CATEGORY = "Chibi-Nodes/Numbers"
def IS_CHANGED(s):
random.seed()
def generator(self, mode, fixed_seed):
if mode == "Random":
random_seed = math.floor(random.random() * 10000000000000000)
return (
random_seed,
str(random_seed),
)
if mode == "Fixed":
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},
),
},
"optional": {
"image": ("IMAGE",),
},
}
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 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,
)
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)
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,
)
class TextSplit:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(s):
return {
"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:
text = text.rsplit(separator, 1)
else:
text = text.split(separator, 1)
if len(text) == 2:
if return_half == "First Half":
text = text[0]
if return_half == "Second Half":
text = text[1]
return (text,)
class RandomResolutionLatent:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"batch_size": ("INT", {"default": 1, "min": 1, "max": 4096}),
},
}
RETURN_TYPES = (
"LATENT",
"INT",
"INT",
)
RETURN_NAMES = (
"LATENT",
"width",
"height",
)
OUTPUT_NODE = True
FUNCTION = "randres"
CATEGORY = "Chibi-Nodes/Numbers"
def IS_CHANGED(s):
random.seed()
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 a not in res_list:
res_list.append(a)
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])
return (
{"samples": latent},
rand_res[0],
rand_res[1],
)
NODE_CLASS_MAPPINGS = {
"Loader": Loader,
"SimpleSampler": SimpleSampler,
"Prompts": Prompts,
"ImageTool": ImageTool,
"Wildcards": Wildcards,
"LoadEmbedding": LoadEmbedding,
"ConditionText": ConditionText,
"ConditionTextMulti": ConditionTextMulti,
"Textbox": Textbox,
"ImageSizeInfo": ImageSizeInfo,
"ImageSimpleResize": ImageSimpleResize,
"ImageAddText": ImageAddText,
"Int2String": Int2String,
"LoadImageExtended": LoadImageExtended,
"SeedGenerator": SeedGenerator,
"SaveImages": SaveImages,
"TextSplit": TextSplit,
"RandomResolutionLatent": RandomResolutionLatent,
}