Files
PressWagon-ComfyUI-StringsA…/strings_and_things.py
T
PressWagon 06af7af263 Completed to do list item: lora collector chaining
The PWLoraNameCollector node can now be chained with itself to collect more than 5 lora names together.
2024-12-21 23:02:52 +11:00

393 lines
14 KiB
Python

import folder_paths
import torch
from torchvision.transforms import ToPILImage, ToTensor
from scipy.fftpack import fft2, fftshift
from PIL import Image
import numpy as np
class AnyType(str):
"""A special type that can be connected to any other types. Credit to pythongosssss"""
def __ne__(self, __value: object) -> bool:
return False
any_type = AnyType("*")
# Picks a Lora as an output to both send to a lora loader and the metadata as a string
class PWLoraSelector:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"lora_name": (folder_paths.get_filename_list("loras"),), # List of LoRA filenames
}
}
CATEGORY = 'Strings&Things'
RETURN_TYPES = (folder_paths.get_filename_list("loras"), "STRING",)
RETURN_NAMES = ("LORA_NAME", "metadata",)
FUNCTION = "get_lora_name"
def get_lora_name(self, lora_name):
output = str(lora_name)
return (output, output)
# Takes up to 5 Lora names. Should now support chaining together multiple PWLoraNameCollector nodes.
class PWLoraNameCollector:
@classmethod
def INPUT_TYPES(cls):
return {
"optional": {
"Lora_A": ("STRING",),
"Lora_B": ("STRING",),
"Lora_C": ("STRING",),
"Lora_D": ("STRING",),
"Lora_E": ("STRING",),
}
}
CATEGORY = 'Strings&Things'
RETURN_TYPES = (any_type,)
RETURN_NAMES = ("LORA_NAMES",)
FUNCTION = "LoraNameConcat"
def LoraNameConcat(self, Lora_A=None, Lora_B=None, Lora_C=None, Lora_D=None, Lora_E=None):
# Collect all inputs into a list
inputs = [Lora_A, Lora_B, Lora_C, Lora_D, Lora_E]
# Flatten inputs to handle cases where any of them might already be a list
lora_names = []
for item in inputs:
if item: # Ignore None values
if isinstance(item, list):
lora_names.extend(item) # Unpack list items
else:
lora_names.append(item) # Append single string
return (lora_names,)
# A node for printing string data to the console
class DebugString:
CATEGORY = "Strings&Things"
FUNCTION = "PrintString"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"DebugString": (any_type,),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("DebugOutput",) # Optional output, maybe it'll come in handy some day.
OUTPUT_NODE = True
def PrintString(self, DebugString):
DebugString=str(DebugString)
print(f"\033[1;33mDebugString: {DebugString}\033[0m") # Output the string to the console for debugging
return (DebugString,) # Return the input string for potential chaining
class FormatConcatStrings:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"separator": ("STRING", {
"multiline": False,
"default": " ",
}),
},
"optional": {
"A_Pre_string": ("STRING", {
"multiline": False,
}),
"A_Body_string": ("STRING", {
"multiline": False,
}),
"A_Post_string": ("STRING", {
"multiline": False,
}),
"B_Pre_string": ("STRING", {
"multiline": False,
}),
"B_Body_string": ("STRING", {
"multiline": False,
}),
"B_Post_string": ("STRING", {
"multiline": False,
}),
"C_Pre_string": ("STRING", {
"multiline": False,
}),
"C_Body_string": ("STRING", {
"multiline": False,
}),
"C_Post_string": ("STRING", {
"multiline": False,
}),
},
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("FormattedString",)
FUNCTION = "triple_format_concat_strings"
OUTPUT_NODE = True
CATEGORY = "Strings&Things"
def triple_format_concat_strings(self, separator=" ", A_Pre_string="", A_Body_string="", A_Post_string="", B_Pre_string="", B_Body_string="", B_Post_string="", C_Pre_string="", C_Body_string="", C_Post_string=""):
A_part = (A_Pre_string + A_Body_string + A_Post_string) if A_Body_string else ""
B_part = (B_Pre_string + B_Body_string + B_Post_string) if B_Body_string else ""
C_part = (C_Pre_string + C_Body_string + C_Post_string) if C_Body_string else ""
formatted_string = separator.join(filter(None, [A_part, B_part, C_part]))
return (formatted_string,)
# A node for adding pre and post text. Will only output if Body is not empty.
class FormattingSingle:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {},
"optional": {
"PRE_string": ("STRING", {
"multiline": False,
}),
"BODY_string": ("STRING", {
"multiline": False,
}),
"POST_string": ("STRING", {
"multiline": False,
}),
},
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("FormattedString",)
FUNCTION = "format_single"
OUTPUT_NODE = True
CATEGORY = "Strings&Things"
def format_single (self, PRE_string="", BODY_string="", POST_string=""):
if not BODY_string: # BODY_string is empty or None to avoid formatting empty strings
formatted_string = ""
else:
formatted_string = PRE_string + BODY_string + POST_string
return (formatted_string,)
""" Part 2
Weird nodes that may or may not be useful
-Image mosaic censor node
-Fourier analysis node
-[SDXL & 1.5 Only for now] Cosine Similarity & Euclidean Distance
"""
# Applies a mosaic effect to the entire image. Can be composited back onto the original
class MosaicEffectNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"tile_size": ("INT", {
"default": 10,
"min": 1,
"max": 100,
"step": 1
}),
},
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("MosaicImage",)
FUNCTION = "apply_mosaic"
CATEGORY = "Strings&Things/Extras"
OUTPUT_NODE = True
def apply_mosaic(self, image, tile_size=10):
# Rearrange the tensor from Comfy's [batch_size, height, width, channels] format to PIL's [batch_size, channels, height, width] format
image = image.permute(0, 3, 1, 2)
# Convert the tensor to a PIL image (processing only the first image in the batch)
image = ToPILImage()(image[0])
# Get image dimensions
width, height = image.size
# Create a new image for the output (same size)
mosaic_image = Image.new("RGB", (width, height))
# Process the image in tiles
for y in range(0, height, tile_size):
for x in range(0, width, tile_size):
# Define the tile box
box = (x, y, x + tile_size, y + tile_size)
# Crop the tile from the image
tile = image.crop(box)
# Get the median color of the tile
median_color = self.get_median_color(tile)
# Fill the tile area in the output image with the median color
for yy in range(y, min(y + tile_size, height)):
for xx in range(x, min(x + tile_size, width)):
mosaic_image.putpixel((xx, yy), median_color)
# Convert the PIL image back to tensor
mosaic_image_tensor = ToTensor()(mosaic_image).unsqueeze(0) # Add batch dimension back
mosaic_image_tensor = mosaic_image_tensor.permute(0, 2, 3, 1)
return (mosaic_image_tensor,)
@staticmethod
def get_median_color(tile):
# Convert the tile to a numpy array
pixels = np.array(tile)
# Calculate the median color for each channel
r = np.median(pixels[:, :, 0])
g = np.median(pixels[:, :, 1])
b = np.median(pixels[:, :, 2])
return int(r), int(g), int(b)
class FourierAnalysisNode:
@staticmethod
def INPUT_TYPES():
return {
"required": {
"image": ("IMAGE",),
},
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("Image",)
FUNCTION = "FAnalysis"
OUTPUT_NODE = True
CATEGORY = "Strings&Things/Extras"
def FAnalysis(self,image):
# Rearrange the tensor from [batch_size, height, width, channels] to [batch_size, channels, height, width]
image = image.permute(0, 3, 1, 2)
# Dropping batch dimension
image = image.squeeze(0)
# Perform Fourier Transform on each channel separately
fft_r = fft2(image[0].numpy()) # Red channel
fft_g = fft2(image[1].numpy()) # Green channel
fft_b = fft2(image[2].numpy()) # Blue channel
# Shift the zero frequency component to the center
fft_r_shifted = fftshift(fft_r)
fft_g_shifted = fftshift(fft_g)
fft_b_shifted = fftshift(fft_b)
# Take log of the magnitude for visualization and normalize to 0-255 range
magnitude_r = np.log(1 + np.abs(fft_r_shifted))
magnitude_g = np.log(1 + np.abs(fft_g_shifted))
magnitude_b = np.log(1 + np.abs(fft_b_shifted))
# Normalize each channel to range [0, 255]
magnitude_r = (magnitude_r - np.min(magnitude_r)) / (np.max(magnitude_r) - np.min(magnitude_r)) * 255
magnitude_g = (magnitude_g - np.min(magnitude_g)) / (np.max(magnitude_g) - np.min(magnitude_g)) * 255
magnitude_b = (magnitude_b - np.min(magnitude_b)) / (np.max(magnitude_b) - np.min(magnitude_b)) * 255
# Convert the magnitude to uint8 type
magnitude_r = magnitude_r.astype(np.uint8)
magnitude_g = magnitude_g.astype(np.uint8)
magnitude_b = magnitude_b.astype(np.uint8)
# Stack the channels back into a 3D array (height, width, 3)
magnitude_image = np.stack([magnitude_r, magnitude_g, magnitude_b], axis=-1)
# Convert the magnitude image back to a PIL image
fourier_image_pil = Image.fromarray(magnitude_image)
# Convert back to tensor
image = ToTensor()(fourier_image_pil).unsqueeze(0) # Add batch dimension back
image = image.permute(0, 2, 3, 1) # Converting from [B, C, H, W] to [B, H, W, C]
return (image,)
# Takes two text inputs and two CLIP inputs and calculates the Cosine Similarity and Euclidean distance between them
class TextEmbeddingsInterrogator:
FUNCTION = "interrogation"
CATEGORY = "Strings&Things/Extras"
DESCRIPTION = "Calculates the Cosine Similarity and Euclidean Distance between two text embeddings"
OUTPUT_NODE = True
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("Output string",)
@staticmethod
def INPUT_TYPES():
return {
"required": {
"CLIP_1": ("CLIP",),
"Text_1": ("STRING",{"default":""}),
"CLIP_2": ("CLIP",),
"Text_2": ("STRING",{"default":""}),
}
}
def get_text_embeddings(self, clip, text):
# Tokenize text
tokens = clip.tokenize(text)
embeddings = clip.encode_from_tokens_scheduled(tokens)
# embeddings is a list of lists of tensors, we want the only entry in the the top level list (hence [0]) and the second entry of that subordinate list (hence [0][1])
# but we need to reference the dictionary key to extract the element from that tensor to get the Pooled Attention
pooled_embeddings = embeddings[0][1]['pooled_output']
return pooled_embeddings
def interrogation(self, Text_1, Text_2, CLIP_1, CLIP_2):
#get text embeddings
embedding1 = self.get_text_embeddings(CLIP_1, Text_1)
embedding2 = self.get_text_embeddings(CLIP_2, Text_2)
# Normalize the embeddings
embedding1_norm = embedding1 / embedding1.norm(p=2)
embedding2_norm = embedding2 / embedding2.norm(p=2)
# Compute the cosine similarity as a dot product between the normalized vectors
cos_sim = torch.mm(embedding1_norm, embedding2_norm.T)
cos_sim_value = cos_sim.item()
# Calculate distance between embeddings
distance = torch.norm(embedding1 - embedding2, p=2)
distance_value = distance.item()
print(f'\033[1;33mText: "{Text_1}", "{Text_2}"\033[0m')
print(f"\033[1;33mCosSim: {cos_sim_value}\033[0m")
print(f"\033[1;33mEuclidean Distance: {distance_value}\033[0m")
output_string = f"Cosine Similarity: {cos_sim_value}, Euclidean Distance: {distance_value}"
return output_string
NODE_CLASS_MAPPINGS = {
"PWLoraSelector": PWLoraSelector,
"PWLoraNameCollector": PWLoraNameCollector,
"DebugString": DebugString,
"FormatConcatStrings": FormatConcatStrings,
"FormattingSingle": FormattingSingle,
"MosaicEffectNode": MosaicEffectNode,
"FourierAnalysisNode": FourierAnalysisNode,
"TextEmbeddingsInterrogator": TextEmbeddingsInterrogator,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"PWLoraSelector": "Lora Selector",
"PWLoraNameCollector": "Lora Name Collector",
"DebugString": "🔧 Debug String",
"FormatConcatStrings": "Formatting and Concatenating Strings",
"FormattingSingle": "Formatting Single String",
"MosaicEffectNode": "Apply Mosaic Effect",
"FourierAnalysisNode": "Fourier Analysis",
"TextEmbeddingsInterrogator": "Text Embeddings Interrogator",
}