1160 lines
38 KiB
Python
1160 lines
38 KiB
Python
import hashlib
|
|
import os
|
|
import time
|
|
from pathlib import Path
|
|
import random
|
|
from PIL.PngImagePlugin import PngInfo # type: ignore
|
|
import comfy.samplers # type: ignore
|
|
import folder_paths # type: ignore
|
|
import node_helpers # type: ignore
|
|
import numpy as np # type: ignore
|
|
import torch # type: ignore
|
|
from PIL import Image, ImageSequence, ImageOps # type: ignore
|
|
from .backend.checker_board import ChessTensor, ChessPattern
|
|
from nodes import common_ksampler, SaveImage, PreviewImage # type: ignore
|
|
import json
|
|
from .backend.file_handeler import FileHandler
|
|
from .backend.grid_filler import (
|
|
fill_grid_with_images_new,
|
|
tensor_to_images,
|
|
image_to_tensor,
|
|
)
|
|
from .backend.metadata_extractor import get_prompt
|
|
from .backend.overlay import add_text_bar
|
|
from .backend.prompter import read_replace_and_combine, templates
|
|
from .backend.prompter_multi import (
|
|
combine_multi,
|
|
templates_basic,
|
|
templates_extra1,
|
|
templates_extra2,
|
|
templates_extra3,
|
|
)
|
|
from .backend.shared import styles, tensor2pil, pil2tensor, project_dir, FlexibleOptionalInputType, any_type
|
|
from comfy.cli_args import args # type: ignore
|
|
import re
|
|
|
|
|
|
class IToolsLoadImagePlus:
|
|
@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})},
|
|
}
|
|
|
|
CATEGORY = "iTools"
|
|
|
|
RETURN_TYPES = ("IMAGE", "MASK", "STRING", "STRING")
|
|
RETURN_NAMES = ("IMAGE", "MASK", "possible prompt", "image name")
|
|
FUNCTION = "load_image"
|
|
DESCRIPTION = (
|
|
"An enhancement of the original ComfyUI ImageLoader node. It attempts to return the possible prompt "
|
|
"used to create an image."
|
|
)
|
|
|
|
def load_image(self, image):
|
|
image_path = folder_paths.get_annotated_filepath(image)
|
|
filename = image.rsplit(".", 1)[0] # get image name
|
|
img = node_helpers.pillow(Image.open, image_path)
|
|
|
|
output_images = []
|
|
output_masks = []
|
|
w, h = None, None
|
|
|
|
excluded_formats = ["MPO"]
|
|
|
|
for i in ImageSequence.Iterator(img):
|
|
i = node_helpers.pillow(ImageOps.exif_transpose, i)
|
|
|
|
if i.mode == "I":
|
|
i = i.point(lambda i: i * (1 / 255))
|
|
image = i.convert("RGB")
|
|
|
|
if len(output_images) == 0:
|
|
w = image.size[0]
|
|
h = image.size[1]
|
|
|
|
if image.size[0] != w or image.size[1] != h:
|
|
continue
|
|
|
|
image = np.array(image).astype(np.float32) / 255.0
|
|
image = torch.from_numpy(image)[None,]
|
|
if "A" in i.getbands():
|
|
mask = np.array(i.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")
|
|
output_images.append(image)
|
|
output_masks.append(mask.unsqueeze(0))
|
|
|
|
if len(output_images) > 1 and img.format not in excluded_formats:
|
|
output_image = torch.cat(output_images, dim=0)
|
|
output_mask = torch.cat(output_masks, dim=0)
|
|
else:
|
|
output_image = output_images[0]
|
|
output_mask = output_masks[0]
|
|
|
|
output_prompt = get_prompt(image_path)
|
|
return (output_image, output_mask, output_prompt, filename)
|
|
|
|
@classmethod
|
|
def IS_CHANGED(cls, image):
|
|
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(cls, image):
|
|
if not folder_paths.exists_annotated_filepath(image):
|
|
return "Invalid image file: {}".format(image)
|
|
|
|
return True
|
|
|
|
|
|
class IToolsPromptLoader:
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"file_path": ("STRING", {"default": "prompts.txt", "multiline": False}),
|
|
"seed": ("INT", {"default": 0, "min": 0, "max": 0xFFFF}),
|
|
}
|
|
}
|
|
|
|
CATEGORY = "iTools"
|
|
|
|
RETURN_TYPES = ("STRING", "INT")
|
|
RETURN_NAMES = ("prompt", "count")
|
|
FUNCTION = "load_file"
|
|
DESCRIPTION = (
|
|
"Will return a prompt (line number) from txt file at given "
|
|
"index, note that count start from zero."
|
|
)
|
|
|
|
def load_file(self, file_path, seed, fallback="Yes"):
|
|
prompt = ""
|
|
count = 0
|
|
if file_path == "prompts.txt":
|
|
file = os.path.join(project_dir, "examples", "prompts.txt")
|
|
else:
|
|
file = file_path.replace('"', "")
|
|
if os.path.exists(file):
|
|
fh = FileHandler(file)
|
|
try:
|
|
count = fh.len_lines()
|
|
line = fh.read_line(seed)
|
|
prompt = fh.unescape_quotes(line)
|
|
except IndexError:
|
|
if fallback == "Yes":
|
|
seed = seed % fh.len_lines()
|
|
line = fh.read_line(seed)
|
|
prompt = fh.unescape_quotes(line)
|
|
else:
|
|
prompt = f"File not exist, {file}"
|
|
return prompt, count
|
|
|
|
|
|
class IToolsPromptSaver:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"prompt": ("STRING", {"forceInput": True}),
|
|
"file_path": ("STRING", {"default": "prompts.txt", "multiline": False}),
|
|
}
|
|
}
|
|
|
|
CATEGORY = "iTools"
|
|
RETURN_TYPES = ()
|
|
OUTPUT_NODE = True
|
|
FUNCTION = "save_to_file"
|
|
DESCRIPTION = "Will append the given prompt as a new line to the given txt file"
|
|
|
|
def save_to_file(self, file_path, prompt):
|
|
if file_path == "prompts.txt":
|
|
file = os.path.join(project_dir, "examples", "prompts.txt")
|
|
else:
|
|
file = file_path.replace('"', "")
|
|
if os.path.exists(file) and prompt is not None and prompt != "":
|
|
fh = FileHandler(file)
|
|
try:
|
|
fh.append_line(prompt)
|
|
print(f"Prompt: {prompt} saved to {file}")
|
|
except Exception as e:
|
|
print(f"Error while writing the prompt: {e}")
|
|
else:
|
|
print(f"Error while writing the prompt")
|
|
return (True,)
|
|
|
|
|
|
class IToolsPromptStyler:
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"text_positive": ("STRING", {"default": "", "multiline": True}),
|
|
"text_negative": ("STRING", {"default": "", "multiline": False}),
|
|
"style_file": (styles, {"default": "basic.yaml"}),
|
|
"template_name": (templates,),
|
|
},
|
|
}
|
|
|
|
@classmethod
|
|
def VALIDATE_INPUTS(cls, template_name):
|
|
# YOLO, anything goes!
|
|
return True
|
|
|
|
def IS_CHANGED(text_positive, text_negative, template_name, style_file):
|
|
if template_name == "random":
|
|
return float("nan") # Force re-execution if template is "random"
|
|
|
|
RETURN_TYPES = ("STRING", "STRING", "STRING")
|
|
RETURN_NAMES = ("positive_prompt", "negative_prompt", "used_template")
|
|
FUNCTION = "prompt_styler"
|
|
CATEGORY = "iTools"
|
|
DESCRIPTION = (
|
|
"Helps you quickly populate your prompt using a template stored in YAML file."
|
|
)
|
|
|
|
def prompt_styler(self, text_positive, text_negative, template_name, style_file):
|
|
positive_prompt, negative_prompt, used_template = read_replace_and_combine(
|
|
template_name, text_positive, text_negative, style_file
|
|
)
|
|
return positive_prompt, negative_prompt, used_template
|
|
|
|
|
|
class IToolsAddOverlay:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"image": ("IMAGE", {}),
|
|
"text": ("STRING", {"default": "img info:", "multiline": False}),
|
|
"background_color": (
|
|
"STRING",
|
|
{"default": "#000000AA", "multiline": False},
|
|
),
|
|
"font_size": ("INT", {"default": 40, "min": 10, "max": 1000}),
|
|
"overlay_mode": ("BOOLEAN", {"default": True}),
|
|
}
|
|
}
|
|
|
|
CATEGORY = "iTools"
|
|
RETURN_TYPES = ("IMAGE",)
|
|
RETURN_NAMES = ("image",)
|
|
FUNCTION = "add_text_overlay"
|
|
DESCRIPTION = (
|
|
"Will add an overlay bottom bar to show a given text, you may change the background color of the "
|
|
"overlay bar and the font size."
|
|
)
|
|
|
|
def add_text_overlay(self, image, text, font_size, background_color, overlay_mode):
|
|
|
|
# Convert image to tensor
|
|
pil_image = tensor2pil(image)
|
|
|
|
# Add overlay or underlay
|
|
if overlay_mode:
|
|
composite = add_text_bar(
|
|
pil_image,
|
|
text,
|
|
font_size=font_size,
|
|
background_color=background_color,
|
|
position="overlay",
|
|
)
|
|
else:
|
|
composite = add_text_bar(
|
|
pil_image,
|
|
text,
|
|
font_size=font_size,
|
|
background_color=background_color,
|
|
position="underlay",
|
|
)
|
|
|
|
# Convert back to tensor
|
|
out = pil2tensor(composite)
|
|
|
|
return (out,)
|
|
|
|
|
|
class IToolsLoadImages:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
default_dir = folder_paths.output_directory
|
|
return {
|
|
"required": {
|
|
"images_directory": (
|
|
"STRING",
|
|
{"default": default_dir, "multiline": False},
|
|
),
|
|
"start_index": ("INT", {"default": 0, "min": 0, "max": 200}),
|
|
"load_limit": ("INT", {"default": 4, "min": 2, "max": 200}),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("IMAGE", "STRING", "INT")
|
|
RETURN_NAMES = ("images", "images names", "count")
|
|
FUNCTION = "load_images"
|
|
CATEGORY = "iTools"
|
|
OUTPUT_IS_LIST = (True, True, False)
|
|
DESCRIPTION = (
|
|
"Will return list of images from a given directory with a given limit, for example if the limit is "
|
|
"4 it will return first 4 images in that directory. it will also return the list of these images "
|
|
"names."
|
|
)
|
|
|
|
def load_images(self, images_directory, load_limit, start_index):
|
|
image_extensions = {".png", ".jpg", ".jpeg", ".webp", ".bmp", ".gif"}
|
|
images_path = Path(images_directory.replace('"', ""))
|
|
|
|
if not images_path.exists():
|
|
raise FileNotFoundError(
|
|
f"Image directory {images_directory} does not exist"
|
|
)
|
|
|
|
images = []
|
|
images_names = []
|
|
for idx, image_path in enumerate(images_path.iterdir()):
|
|
if idx < start_index:
|
|
continue # Skip images until reaching the start_index
|
|
if image_path.suffix.lower() in image_extensions:
|
|
images.append(pil2tensor(Image.open(image_path)))
|
|
images_names.append(
|
|
image_path.stem
|
|
) # Add the image name without extension
|
|
if len(images) >= load_limit:
|
|
break
|
|
|
|
return images, images_names, len(images)
|
|
|
|
|
|
class IToolsPromptStylerExtra:
|
|
|
|
def __init__(self):
|
|
pass
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"text_positive": ("STRING", {"default": "", "multiline": True}),
|
|
"text_negative": ("STRING", {"default": "", "multiline": False}),
|
|
"base_file": ((styles), {"default": "basic.yaml"}),
|
|
"base_style": ((templates_basic),),
|
|
"second_file": ((styles), {"default": "camera.yaml"}),
|
|
"second_style": ((templates_extra1),),
|
|
"third_file": ((styles), {"default": "artist.yaml"}),
|
|
"third_style": ((templates_extra2),),
|
|
"fourth_file": ((styles), {"default": "mood.yaml"}),
|
|
"fourth_style": ((templates_extra3),),
|
|
},
|
|
}
|
|
|
|
@classmethod
|
|
def VALIDATE_INPUTS(cls, base_style, second_style, third_style, fourth_style):
|
|
# YOLO, anything goes!
|
|
return True
|
|
|
|
def IS_CHANGED(
|
|
text_positive,
|
|
text_negative,
|
|
base_file,
|
|
base_style,
|
|
second_file,
|
|
second_style,
|
|
third_file,
|
|
third_style,
|
|
fourth_file,
|
|
fourth_style,
|
|
):
|
|
if (
|
|
base_style == "random"
|
|
or second_style == "random"
|
|
or third_style == "random"
|
|
or fourth_style == "random"
|
|
):
|
|
return float("nan") # Force re-execution if template is "random"
|
|
|
|
RETURN_TYPES = (
|
|
"STRING",
|
|
"STRING",
|
|
"STRING",
|
|
)
|
|
RETURN_NAMES = ("positive_prompt", "negative_prompt", "used_templates")
|
|
FUNCTION = "prompt_styler_extra"
|
|
CATEGORY = "iTools"
|
|
DESCRIPTION = "Helps you quickly populate your prompt using templates from up to 4 YAML files."
|
|
|
|
def prompt_styler_extra(
|
|
self,
|
|
text_positive,
|
|
text_negative,
|
|
base_file,
|
|
base_style,
|
|
second_file,
|
|
second_style,
|
|
third_file,
|
|
third_style,
|
|
fourth_file,
|
|
fourth_style,
|
|
):
|
|
positive_prompt, negative_prompt, _templates = combine_multi(
|
|
text_positive,
|
|
text_negative,
|
|
base_file,
|
|
base_style,
|
|
second_file,
|
|
second_style,
|
|
third_file,
|
|
third_style,
|
|
fourth_file,
|
|
fourth_style,
|
|
)
|
|
return positive_prompt, negative_prompt, _templates
|
|
|
|
|
|
class IToolsGridFiller:
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
directions = ["rows", "cols"]
|
|
return {
|
|
"required": {
|
|
"images": ("IMAGE", {}),
|
|
"width": ("INT", {"default": 1024, "min": 256, "max": 8192}),
|
|
"height": ("INT", {"default": 1024, "min": 256, "max": 8192}),
|
|
"rows": ("INT", {"default": 3, "min": 1, "max": 10}),
|
|
"cols": ("INT", {"default": 3, "min": 1, "max": 10}),
|
|
"gaps": ("FLOAT", {"default": 2, "min": 0.0, "max": 50, "steps": 1}),
|
|
"background_color": (
|
|
"STRING",
|
|
{"default": "#000000AA", "multiline": False},
|
|
),
|
|
"fill_direction": (directions, {"default": directions[0]}),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("IMAGE",)
|
|
RETURN_NAMES = ("images",)
|
|
FUNCTION = "fill_grid"
|
|
CATEGORY = "iTools"
|
|
INPUT_IS_LIST = (True, False, False, False, False, False, False)
|
|
OUTPUT_IS_LIST = (False, False, False, False, False, False, False)
|
|
DESCRIPTION = (
|
|
"Arranging a set of images into specified rows and columns, applying "
|
|
"optional spacing and background color"
|
|
)
|
|
|
|
def fill_grid(
|
|
self, images, width, height, rows, cols, gaps, background_color, fill_direction
|
|
):
|
|
# Convert tensor to Pillow images
|
|
pillow_images = tensor_to_images(images)
|
|
|
|
# Process images using the provided function
|
|
processed_image = fill_grid_with_images_new(
|
|
pillow_images,
|
|
rows=rows,
|
|
cols=cols,
|
|
grid_size=(width, height),
|
|
gap=gaps,
|
|
bg_color=background_color,
|
|
direction=fill_direction[0],
|
|
)
|
|
print("fill_direction", fill_direction)
|
|
# Convert the processed Pillow image back to a tensor
|
|
output_tensor = image_to_tensor(processed_image)
|
|
|
|
return (output_tensor,)
|
|
|
|
|
|
class IToolsLineLoader:
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"lines": ("STRING", {"default": "cat\ndog\nbunny", "multiline": True}),
|
|
"seed": (
|
|
"INT",
|
|
{
|
|
"default": 0,
|
|
"control_after_generate": "increment",
|
|
"min": 0,
|
|
"max": 0xFFF,
|
|
},
|
|
),
|
|
}
|
|
}
|
|
|
|
CATEGORY = "iTools"
|
|
|
|
RETURN_TYPES = ("STRING", "INT")
|
|
RETURN_NAMES = ("line loaded", "count")
|
|
FUNCTION = "load_line"
|
|
DESCRIPTION = (
|
|
"Will return a line from a multi line text at given "
|
|
"index, note that count start from zero."
|
|
)
|
|
|
|
def load_line(self, lines, seed, fallback="Yes"):
|
|
# Split the multiline string into individual lines
|
|
line_list = lines.splitlines()
|
|
|
|
# Count the total number of lines
|
|
count = len(line_list)
|
|
|
|
# Check if the seed index is valid
|
|
if 0 <= seed < count:
|
|
line = line_list[seed]
|
|
elif fallback == "Yes" and count > 0:
|
|
# If fallback is "Yes", mod the seed by the line count to wrap around
|
|
seed_mod = seed % count
|
|
line = line_list[seed_mod]
|
|
else:
|
|
# If the index is out of range and no fallback, return an empty string
|
|
line = ""
|
|
|
|
return line, count
|
|
|
|
|
|
class IToolsTextReplacer:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"text_in": ("STRING", {"forceInput": True, "multiline": False}),
|
|
"match": ("STRING", {"forceInput": False, "multiline": False}),
|
|
"replace": ("STRING", {"forceInput": False, "multiline": False}),
|
|
}
|
|
}
|
|
|
|
CATEGORY = "iTools"
|
|
|
|
RETURN_TYPES = ("STRING",)
|
|
RETURN_NAMES = ("text_out",)
|
|
FUNCTION = "replace_text"
|
|
DESCRIPTION = "Help you replace a match in a given text."
|
|
|
|
def replace_text(self, text_in, match, replace):
|
|
print(text_in)
|
|
return (text_in.replace(match, replace),)
|
|
|
|
|
|
class IToolsRegexNode:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
patterns = [
|
|
"custom",
|
|
"contains_hello",
|
|
"cat_or_dog",
|
|
"starts_with_abc",
|
|
"ends_with_xyz",
|
|
"any_character",
|
|
"digit",
|
|
"non_digit",
|
|
"whitespace",
|
|
"non_whitespace",
|
|
"word_character",
|
|
"non_word_character",
|
|
"all_caps",
|
|
"all_lower",
|
|
"integer",
|
|
"floating_point",
|
|
"no_numbers",
|
|
"email",
|
|
"phone_number",
|
|
"double_quoted",
|
|
"double_quoted_plus",
|
|
"single_quoted",
|
|
"single_quoted_plus",
|
|
"in_parentheses",
|
|
"in_parentheses_plus",
|
|
"angle_brackets",
|
|
"angle_brackets_plus",
|
|
]
|
|
|
|
return {
|
|
"required": {
|
|
"text_in": ("STRING", {"forceInput": True, "multiline": False}),
|
|
"regex_pattern": (
|
|
"STRING",
|
|
{"default": "", "forceInput": False, "multiline": False},
|
|
),
|
|
"pattern_picker": (patterns, {"default": "custom"}),
|
|
"replace_match": ("STRING", {"forceInput": False, "multiline": False}),
|
|
"replace_non_match": (
|
|
"STRING",
|
|
{"forceInput": False, "multiline": False},
|
|
),
|
|
}
|
|
}
|
|
|
|
CATEGORY = "iTools"
|
|
RETURN_TYPES = ("STRING",)
|
|
RETURN_NAMES = ("match",)
|
|
FUNCTION = "match_text"
|
|
DESCRIPTION = "Uses Regex to find, match, or modify text. Returns matches if no replacement is set, otherwise, replaces matches or non-matches as specified."
|
|
|
|
def match_text(
|
|
self, text_in, regex_pattern, pattern_picker, replace_match, replace_non_match
|
|
):
|
|
matches = re.findall(regex_pattern, text_in) # Find all matches
|
|
|
|
if replace_match == "" and replace_non_match == "":
|
|
result = "".join(matches)
|
|
return (result.strip(),) # ok do not change
|
|
elif replace_match != "" and replace_non_match == "":
|
|
# Replace all matches globally
|
|
result = re.sub(regex_pattern, replace_match, text_in)
|
|
return (result.strip(),) # ok do not change
|
|
elif replace_non_match != "" and replace_match == "":
|
|
# Replace all non-matching parts with replace_non_match
|
|
parts = []
|
|
last_end = 0
|
|
for match in re.finditer(regex_pattern, text_in):
|
|
start, end = match.span()
|
|
parts.append(replace_non_match) # Non-matching part before the match
|
|
parts.append(text_in[start:end]) # Matching part
|
|
last_end = end
|
|
parts.append(replace_non_match) # Non-matching part after the last match
|
|
result = "".join(parts)
|
|
return (result.strip(),)
|
|
else:
|
|
# Replace matches with replace_match and non-matches with replace_non_match
|
|
parts = []
|
|
last_end = 0
|
|
for match in re.finditer(regex_pattern, text_in):
|
|
start, end = match.span()
|
|
if last_end < start:
|
|
parts.append(
|
|
replace_non_match
|
|
) # Non-matching part before the match
|
|
parts.append(replace_match) # Replace the match
|
|
last_end = end
|
|
if last_end < len(text_in):
|
|
parts.append(
|
|
replace_non_match
|
|
) # Non-matching part after the last match
|
|
result = "".join(parts)
|
|
return (result.strip(),)
|
|
|
|
|
|
class IToolsKSampler:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"model": (
|
|
"MODEL",
|
|
{"tooltip": "The model used for denoising the input latent."},
|
|
),
|
|
"seed": (
|
|
"INT",
|
|
{
|
|
"default": 0,
|
|
"min": 0,
|
|
"max": 0xFFFFFFFFFFFFFFFF,
|
|
"tooltip": "The random seed used for creating the noise.",
|
|
},
|
|
),
|
|
"steps": (
|
|
"INT",
|
|
{
|
|
"default": 20,
|
|
"min": 1,
|
|
"max": 10000,
|
|
"tooltip": "The number of steps used in the denoising process.",
|
|
},
|
|
),
|
|
"cfg": (
|
|
"FLOAT",
|
|
{
|
|
"default": 8.0,
|
|
"min": 0.0,
|
|
"max": 100.0,
|
|
"step": 0.1,
|
|
"round": 0.01,
|
|
"tooltip": "The Classifier-Free Guidance scale balances creativity and adherence to the prompt. Higher values result in images more closely matching the prompt however too high values will negatively impact quality.",
|
|
},
|
|
),
|
|
"sampler_name": (
|
|
comfy.samplers.KSampler.SAMPLERS,
|
|
{
|
|
"tooltip": "The algorithm used when sampling, this can affect the quality, speed, and style of the generated output."
|
|
},
|
|
),
|
|
"scheduler": (
|
|
comfy.samplers.KSampler.SCHEDULERS,
|
|
{
|
|
"tooltip": "The scheduler controls how noise is gradually removed to form the image."
|
|
},
|
|
),
|
|
"positive": (
|
|
"CONDITIONING",
|
|
{
|
|
"tooltip": "The conditioning describing the attributes you want to include in the image."
|
|
},
|
|
),
|
|
"negative": (
|
|
"CONDITIONING",
|
|
{
|
|
"tooltip": "The conditioning describing the attributes you want to exclude from the image."
|
|
},
|
|
),
|
|
"latent_image": ("LATENT", {"tooltip": "The latent image to denoise."}),
|
|
"denoise": (
|
|
"FLOAT",
|
|
{
|
|
"default": 1.0,
|
|
"min": 0.0,
|
|
"max": 1.0,
|
|
"step": 0.01,
|
|
"tooltip": "The amount of denoising applied, lower values will maintain the structure of the initial image allowing for image to image sampling.",
|
|
},
|
|
),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("LATENT", "STRING")
|
|
RETURN_NAMES = ("LATENT", "INFO")
|
|
OUTPUT_TOOLTIPS = ("The denoised latent.",)
|
|
FUNCTION = "sample"
|
|
|
|
CATEGORY = "iTools"
|
|
DESCRIPTION = (
|
|
"Identical to the original KSampler, but additionally provides the settings used to generate the "
|
|
"image and the execution time."
|
|
)
|
|
|
|
def sample(
|
|
self,
|
|
model,
|
|
seed,
|
|
steps,
|
|
cfg,
|
|
sampler_name,
|
|
scheduler,
|
|
positive,
|
|
negative,
|
|
latent_image,
|
|
denoise=1.0,
|
|
):
|
|
start_time = time.time()
|
|
result = common_ksampler(
|
|
model,
|
|
seed,
|
|
steps,
|
|
cfg,
|
|
sampler_name,
|
|
scheduler,
|
|
positive,
|
|
negative,
|
|
latent_image,
|
|
denoise=denoise,
|
|
)
|
|
end_time = time.time()
|
|
execution_time = f"{end_time - start_time:.3f}s"
|
|
|
|
info = f"time:{execution_time} "
|
|
info += f"seed:{seed} "
|
|
info += f"steps:{steps} "
|
|
info += f"cfg:{cfg} "
|
|
info += f"sampler:{sampler_name} "
|
|
info += f"scheduler:{scheduler} "
|
|
|
|
return result[0], info
|
|
|
|
|
|
class IToolsVaePreview:
|
|
def __init__(self):
|
|
self.output_dir = folder_paths.get_temp_directory()
|
|
self.type = "temp"
|
|
self.prefix_append = "_temp_" + "".join(
|
|
random.choice("abcdefghijklmnopqrstupvxyz") for x in range(5)
|
|
)
|
|
self.compress_level = 1
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
# "images": ("IMAGE", ),
|
|
"samples": ("LATENT", {"tooltip": "The latent to be decoded."}),
|
|
"vae": (
|
|
"VAE",
|
|
{"tooltip": "The VAE model used for decoding the latent."},
|
|
),
|
|
},
|
|
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
|
|
}
|
|
|
|
RETURN_TYPES = ("IMAGE",)
|
|
RETURN_NAMES = ("images",)
|
|
FUNCTION = "vae_preview"
|
|
|
|
OUTPUT_NODE = True
|
|
|
|
CATEGORY = "iTools"
|
|
DESCRIPTION = "Merges VAE decoding and image preview into one node."
|
|
|
|
def vae_preview(
|
|
self,
|
|
samples,
|
|
vae,
|
|
filename_prefix="ComfyUI",
|
|
prompt=None,
|
|
extra_pnginfo=None,
|
|
):
|
|
|
|
def decode(vae, samples):
|
|
images = vae.decode(samples["samples"])
|
|
if len(images.shape) == 5: #Combine batches
|
|
images = images.reshape(-1, images.shape[-3], images.shape[-2], images.shape[-1])
|
|
return (images, )
|
|
|
|
return_options = decode(vae, samples)
|
|
images = return_options[0]
|
|
filename_prefix += self.prefix_append
|
|
full_output_folder, filename, counter, subfolder, filename_prefix = (
|
|
folder_paths.get_save_image_path(
|
|
filename_prefix, self.output_dir, images[0].shape[1], images[0].shape[0]
|
|
)
|
|
)
|
|
results = list()
|
|
for batch_number, image in enumerate(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]))
|
|
|
|
filename_with_batch_num = filename.replace("%batch_num%", str(batch_number))
|
|
file = f"{filename_with_batch_num}_{counter:05}_.png"
|
|
img.save(
|
|
os.path.join(full_output_folder, file),
|
|
pnginfo=metadata,
|
|
compress_level=self.compress_level,
|
|
)
|
|
results.append(
|
|
{"filename": file, "subfolder": subfolder, "type": self.type}
|
|
)
|
|
counter += 1
|
|
|
|
return {"ui": {"images": results}, "result": return_options}
|
|
|
|
|
|
class IToolsCheckerBoard:
|
|
def __init__(self): ...
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"width": ("INT", {"default": 1024, "min": 256, "max": 8192}),
|
|
"height": ("INT", {"default": 1024, "min": 256, "max": 8192}),
|
|
"rows": ("INT", {"default": 9, "min": 1, "max": 128}),
|
|
"cols": ("INT", {"default": 9, "min": 1, "max": 128}),
|
|
"pattern": (
|
|
ChessPattern.to_list(),
|
|
{"default": ChessPattern.to_list()[1]},
|
|
),
|
|
"is_colored": ("BOOLEAN", {"default": False}),
|
|
"seed": ("INT", {"default": 0, "min": 0, "max": 0xFFF}),
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = ("IMAGE", "MASK")
|
|
FUNCTION = "generate_checkerboard"
|
|
# OUTPUT_NODE = True
|
|
CATEGORY = "iTools"
|
|
DESCRIPTION = "Generates chessboard-like patterns, either in black and white or with random colors"
|
|
|
|
def generate_checkerboard(
|
|
self, width, height, rows, cols, pattern, is_colored, seed
|
|
):
|
|
|
|
_tensor = ChessTensor(
|
|
width=width,
|
|
height=height,
|
|
rows=rows,
|
|
cols=cols,
|
|
pattern=ChessPattern.from_string(pattern),
|
|
colored=is_colored,
|
|
)
|
|
_img = pil2tensor(_tensor.pil_img)
|
|
|
|
_mask = _img[:, :, :, 0]
|
|
return _img, _mask
|
|
|
|
|
|
class IToolsLoadRandomImage:
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
default_dir = folder_paths.output_directory
|
|
return {
|
|
"required": {
|
|
"images_directory": (
|
|
"STRING",
|
|
{"default": default_dir, "multiline": False},
|
|
),
|
|
# "load_limit": ("INT", {"default": 100, "min": 2, "max": 200}),
|
|
"seed": ("INT", {"default": 0, "min": 0, "max": 0xFFF}),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = (
|
|
"IMAGE",
|
|
"STRING",
|
|
)
|
|
RETURN_NAMES = (
|
|
"image",
|
|
"image name",
|
|
)
|
|
FUNCTION = "load_random_image"
|
|
CATEGORY = "iTools"
|
|
DESCRIPTION = "Will return image from a given directory. it will also return the name of these image."
|
|
|
|
def load_random_image(self, images_directory, seed):
|
|
image_extensions = {".png", ".jpg", ".jpeg", ".webp", ".bmp", ".gif"}
|
|
images_path = Path(images_directory.replace('"', ""))
|
|
|
|
if not images_path.exists():
|
|
raise FileNotFoundError(
|
|
f"Image directory {images_directory} does not exist"
|
|
)
|
|
|
|
all_images = [] # Store all valid image paths
|
|
|
|
for image_path in images_path.iterdir():
|
|
if image_path.suffix.lower() in image_extensions:
|
|
all_images.append(image_path)
|
|
|
|
if not all_images:
|
|
raise ValueError("No valid images found in the directory")
|
|
|
|
# Calculate the random index based on the seed
|
|
random_index = seed % len(all_images)
|
|
selected_image_path = all_images[random_index]
|
|
|
|
# Load the selected image
|
|
image = pil2tensor(Image.open(selected_image_path))
|
|
image_name = selected_image_path.stem
|
|
|
|
return image, image_name
|
|
|
|
|
|
class IToolsPreviewText:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"text": ("STRING", {"forceInput": True}),
|
|
},
|
|
"hidden": {
|
|
"unique_id": "UNIQUE_ID",
|
|
"extra_pnginfo": "EXTRA_PNGINFO",
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = ("STRING",)
|
|
RETURN_NAMES = ("text",)
|
|
FUNCTION = "preview_text"
|
|
CATEGORY = "iTools"
|
|
DESCRIPTION = "Will show text from string input."
|
|
INPUT_IS_LIST = True
|
|
OUTPUT_NODE = True
|
|
OUTPUT_IS_LIST = (True,)
|
|
|
|
def preview_text(s, text, extra_pnginfo, unique_id):
|
|
if unique_id is not None and extra_pnginfo is not None:
|
|
if not isinstance(extra_pnginfo, list):
|
|
print("Error: extra_pnginfo is not a list")
|
|
elif (
|
|
not isinstance(extra_pnginfo[0], dict)
|
|
or "workflow" not in extra_pnginfo[0]
|
|
):
|
|
print("Error: extra_pnginfo[0] is not a dict or missing 'workflow' key")
|
|
else:
|
|
workflow = extra_pnginfo[0]["workflow"]
|
|
node = next(
|
|
(x for x in workflow["nodes"] if str(x["id"]) == str(unique_id[0])),
|
|
None,
|
|
)
|
|
if node:
|
|
node["widgets_values"] = [text]
|
|
|
|
return {"ui": {"text": text}, "result": (text,)}
|
|
|
|
|
|
class IToolsPreviewImage(SaveImage):
|
|
def __init__(self):
|
|
self.output_dir = folder_paths.get_temp_directory()
|
|
self.type = "temp"
|
|
self.prefix_append = "_temp_" + "".join(
|
|
random.choice("abcdefghijklmnopqrstupvxyz") for x in range(5)
|
|
)
|
|
self.compress_level = 1
|
|
|
|
CATEGORY = "iTools"
|
|
DESCRIPTION = "The easiest way to preview, compare current and previous images, and track your prompt history."
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"images": ("IMAGE",),
|
|
},
|
|
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
|
|
}
|
|
|
|
|
|
class IToolsCompareImage(PreviewImage):
|
|
|
|
CATEGORY = "iTools"
|
|
DESCRIPTION = "Compare A and B images"
|
|
FUNCTION = "compare_images"
|
|
OUTPUT_NODE = True
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"A": ("IMAGE",),
|
|
"B": ("IMAGE",),
|
|
},
|
|
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
|
|
}
|
|
|
|
def compare_images(
|
|
self, A, B, filename_prefix=None, prompt=None, extra_pnginfo=None
|
|
):
|
|
_a = self.save_images(
|
|
A,
|
|
)[
|
|
"ui"
|
|
]["images"]
|
|
_b = self.save_images(
|
|
B,
|
|
)[
|
|
"ui"
|
|
]["images"]
|
|
|
|
return {
|
|
"ui": {"images": _a + _b}
|
|
} # {'ui': {'images': [{'filename': 'ComfyUI_temp_00001_.png', 'subfolder': '', 'type': 'temp'}]}}
|
|
|
|
|
|
class IToolsPromptRecord:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"text": (
|
|
"STRING",
|
|
{"default": "", "multiline": True, "placeholder": "text"},
|
|
),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("STRING",)
|
|
RETURN_NAMES = ("text",)
|
|
FUNCTION = "text_entry"
|
|
OUTPUT_NODE = True
|
|
CATEGORY = "iTools"
|
|
DESCRIPTION = (
|
|
"Tracks your prompts during node execution or when using ▶ button.\n"
|
|
"Provides quick access to previously used prompts. "
|
|
"Includes a history system that saves your favorite prompts."
|
|
)
|
|
|
|
def text_entry(self, text):
|
|
return {"ui": {"text": text}, "result": (text,)}
|
|
|
|
|
|
class IToolsInstructorNode:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {},
|
|
"optional": FlexibleOptionalInputType(any_type),
|
|
}
|
|
|
|
RETURN_TYPES = ("STRING",)
|
|
RETURN_NAMES = ("instruction",)
|
|
FUNCTION = "process_instructions"
|
|
CATEGORY = "iTools"
|
|
OUTPUT_NODE = True
|
|
|
|
def process_instructions(self, **kwargs):
|
|
final_text = ""
|
|
if "InstructorWidget" in kwargs:
|
|
data = kwargs["InstructorWidget"]
|
|
# Use the pre-assembled text from JS
|
|
final_text = data.get("finalText", "")
|
|
|
|
return (final_text,)
|
|
|
|
# A dictionary that contains all nodes you want to export with their names
|
|
# NOTE: names should be globally unique
|
|
NODE_CLASS_MAPPINGS = {
|
|
"iToolsLoadImagePlus": IToolsLoadImagePlus,
|
|
"iToolsPromptLoader": IToolsPromptLoader,
|
|
"iToolsPromptSaver": IToolsPromptSaver,
|
|
"iToolsAddOverlay": IToolsAddOverlay,
|
|
"iToolsLoadImages": IToolsLoadImages,
|
|
"iToolsPromptStyler": IToolsPromptStyler,
|
|
"iToolsPromptStylerExtra": IToolsPromptStylerExtra,
|
|
"iToolsGridFiller": IToolsGridFiller,
|
|
"iToolsLineLoader": IToolsLineLoader,
|
|
"iToolsTextReplacer": IToolsTextReplacer,
|
|
"iToolsKSampler": IToolsKSampler,
|
|
"iToolsVaePreview": IToolsVaePreview,
|
|
"iToolsCheckerBoard": IToolsCheckerBoard,
|
|
"iToolsLoadRandomImage": IToolsLoadRandomImage,
|
|
"iToolsPreviewText": IToolsPreviewText,
|
|
"iToolsRegexNode": IToolsRegexNode,
|
|
"iToolsPreviewImage": IToolsPreviewImage,
|
|
"iToolsCompareImage": IToolsCompareImage,
|
|
"iToolsPromptRecord": IToolsPromptRecord,
|
|
"iToolsInstructorNode": IToolsInstructorNode,
|
|
}
|
|
|
|
# A dictionary that contains the friendly/humanly readable titles for the nodes
|
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
|
"iToolsLoadImagePlus": "iTools Load Image 🏕️",
|
|
"iToolsPromptLoader": "iTools Prompt Loader",
|
|
"iToolsPromptSaver": "iTools Prompt Saver",
|
|
"iToolsAddOverlay": "iTools Add Text Overlay",
|
|
"iToolsLoadImages": "iTools Load Images 📦",
|
|
"iToolsPromptStyler": "iTools Prompt Styler 🖌️",
|
|
"iToolsPromptStylerExtra": "iTools Prompt Styler Extra 🖌️",
|
|
"iToolsGridFiller": "iTools Grid Filler 📲",
|
|
"iToolsLineLoader": "iTools Line Loader",
|
|
"iToolsTextReplacer": "iTools Text Replacer",
|
|
"iToolsKSampler": "iTools KSampler",
|
|
"iToolsVaePreview": "iTools Preview Bridge ⛳",
|
|
"iToolsCheckerBoard": "iTools Checkerboard 🏁",
|
|
"iToolsLoadRandomImage": "iTools Load Random Image 🎲",
|
|
"iToolsPreviewText": "iTools Text Preview",
|
|
"iToolsRegexNode": "iTools Regex Editor",
|
|
"iToolsPreviewImage": "iTools Image Preview 🍿",
|
|
"iToolsCompareImage": "iTools Image Compare 🔍",
|
|
"iToolsPromptRecord": "iTools Prompt Record 🪶",
|
|
"iToolsInstructorNode": "iTools Instructor 👨🏻🏫",
|
|
}
|