Files
RenderRift-ComfyUI-RenderRi…/date_integer_node.py
T
2023-12-20 17:47:46 +00:00

476 lines
19 KiB
Python

from datetime import datetime
from pickle import APPEND
import os
import torch
import numpy as np
import json
import textwrap
from PIL import Image, ImageOps, ImageDraw, ImageFont
from tinytag import TinyTag
import folder_paths
def process_text(anything):
return {"text": [str(anything)]}
class MetadataOverlayNode:
CATEGORY = "RenderRiftNodes"
RETURN_TYPES = ("IMAGE",)
FUNCTION = "overlay_text_on_image"
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"image1": ("IMAGE",),
"metadata1": ("METADATA",),
"image2": ("IMAGE",),
"metadata2": ("METADATA",),
"checkpoint": ("BOOLEAN", {"default": False}),
"ksampler": ("BOOLEAN", {"default": False}),
"controlnets": ("BOOLEAN", {"default": False}),
"animdiff": ("BOOLEAN", {"default": False}),
"ipadpter": ("BOOLEAN", {"default": False}),
"loras": ("BOOLEAN", {"default": False}),
},
"optional": {
"origonal_vid_optional": ("IMAGE",),
"image3": ("IMAGE",),
"metadata3": ("METADATA",),
"image4": ("IMAGE",),
"metadata4": ("METADATA",),
"image5": ("IMAGE",),
"metadata5": ("METADATA",),
"image6": ("IMAGE",),
"metadata6": ("METADATA",),
},
}
def __init__(self):
pass
def extract_metadata(self, metadata, checkpoint, ksampler, controlnets, animdiff, ipadpter, loras):
if metadata == "Original Video":
return "Original Video"
metadata = metadata.get('prompt')
if metadata:
try:
# Parse the JSON metadata
json_data = json.loads(metadata)
prompt_data = json_data.get("prompt", json_data)
# Dictionary to hold class_type and their details
class_type_details = {}
# Process prompt_data to extract and organize specific attributes
for node_id, node in prompt_data.items():
node_type = node.get('class_type')
inputs = node.get('inputs', {})
# Collect the details for each class_type of interest
if animdiff and node_type == "ADE_AnimateDiffLoaderWithContext":
# Extract the context_options ID from the inputs
context_options_id = inputs.get('context_options', [])[0]
context_details = ""
# If context_options ID is present, find the related node and extract details
if context_options_id:
context_node = prompt_data.get(context_options_id, {})
context_inputs = context_node.get('inputs', {})
context_details = f" context_length: {context_inputs.get('context_length')}, context_stride: {context_inputs.get('context_stride')}, context_overlap: {context_inputs.get('context_overlap')}, context_schedule: {context_inputs.get('context_schedule')}, closed_loop: {context_inputs.get('closed_loop')}"
details = f"AnimateDiff Details \n ade_model: {inputs.get('model_name')} \n motion_scale: {inputs.get('motion_scale')} {context_details}"
class_type_details.setdefault(node_type, []).append(details)
# Latent or upscale, IPAdapter, IPAdapter Batch Load, Animif diff, Loras, ControlNets, KSampler
elif controlnets and node_type == "ControlNetLoaderAdvanced":
apply_node_id = next((nid for nid, n in prompt_data.items() if n.get('class_type') == "ControlNetApplyAdvanced" and n['inputs'].get('control_net', [])[0] == node_id), None)
if apply_node_id:
apply_inputs = prompt_data[apply_node_id]['inputs']
details = f"CN: {inputs.get('control_net_name')} \n strength: {apply_inputs.get('strength')} \n start_per: {apply_inputs.get('start_percent')} \n end_per: {apply_inputs.get('end_percent')}"
class_type_details.setdefault(node_type, []).append(details)
elif checkpoint and node_type == "CheckpointLoaderSimple":
details = f"ckpt: {inputs.get('ckpt_name')}"
class_type_details.setdefault(node_type, []).append(details)
elif ksampler and node_type == "KSamplerAdvanced":
details = f"Ksamples - steps: {inputs.get('steps')}, cfg: {inputs.get('cfg')} \n sampler: {inputs.get('sampler_name') + ' ' + inputs.get('scheduler')}"
class_type_details.setdefault(node_type, []).append(details)
elif loras and node_type == "LoraLoader":
details = f"lora_name: {inputs.get('model')}, strength_model: {inputs.get('strength_model')}"
class_type_details.setdefault(node_type, []).append(details)
# Prepare the output string
output = '\n'.join(['\n'.join(details) for details in class_type_details.values()])
return output
except json.JSONDecodeError:
return "Error parsing metadata. Ensure it is in valid JSON format."
else:
return "Metadata not found in file."
def overlay_text_on_image(
self,
image1,
metadata1,
image2,
metadata2,
checkpoint=False,
ksampler=False,
controlnets=False,
animdiff=False,
ipadpter=False,
loras=False,
origonal_vid_optional=None,
image3=None,
metadata3=None,
image4=None,
metadata4=None,
image5=None,
metadata5=None,
image6=None,
metadata6=None,
):
images_and_metadata = []
if origonal_vid_optional is not None:
images_and_metadata.append((origonal_vid_optional, "Original Video"))
# Extend the list with other image and metadata pairs
additional_images_and_metadata = [
(image1, metadata1),
(image2, metadata2),
(image3, metadata3),
(image4, metadata4),
(image5, metadata5),
(image6, metadata6),
]
# Filter out None values and add to the main list
images_and_metadata.extend([(img, meta) for img, meta in additional_images_and_metadata if img is not None and meta is not None])
# List to store overlay images
overlay_images = []
# Extract all metadata texts and calculate the maximum border height
# Find the width of the largest image
if image1 is not None:
max_image_width = image1.shape[2] # Assuming image1 is in CxHxW format
max_text_width = max_image_width - 20 # Consistent text width
all_texts = [self.extract_metadata(meta, checkpoint, ksampler, controlnets, animdiff, ipadpter, loras) for _, meta in images_and_metadata]
max_border_height = self.calculate_max_border_height(all_texts, max_text_width)
# Now process each image with the standardized border height
for img, meta in images_and_metadata:
text = self.extract_metadata(meta, checkpoint, ksampler, controlnets, animdiff, ipadpter, loras)
overlay_img = self.overlayText(img, text, max_border_height, max_image_width )
overlay_images.append(overlay_img[0] if isinstance(overlay_img, tuple) else overlay_img)
num_images = len(overlay_images)
if num_images % 3 != 0 and num_images > 2:
# Calculate how many blank images are needed to complete the row
num_blank_images = 3 - (num_images % 3)
# Create and append blank images
blank_image_size = overlay_images[0].shape
for _ in range(num_blank_images):
overlay_images.append(self.create_blank_image(blank_image_size))
# Concatenate images into rows with a maximum of 3 images per row
rows = []
for i in range(0, len(overlay_images), 3):
# Ensure consistent dimensions
images_to_concat = [img.squeeze(0) if img.dim() == 4 else img for img in overlay_images[i:i+3]]
rows.append(torch.cat(images_to_concat, dim=2))
# Concatenate rows vertically
final_image = torch.cat(rows, dim=1) if rows else torch.empty(0)
return (final_image,)
def create_blank_image(self, size):
# Create a blank image tensor of the specified size
# Assuming the images are 3-channel (RGB)
return torch.zeros(size)
def calculate_max_border_height(self, texts, max_text_width, font_size=40 ):
font = ImageFont.truetype("arial.ttf", 20)
max_border_height = 100 # Starting with a minimum height
for text in texts:
# Calculate required height for this text
wrapped_text = self.wrap_text(text, font, max_text_width) # Assuming max_width is set
line_height = font.getsize('A')[1] + 10
total_text_height = len(wrapped_text) * line_height
border_height = max(100, total_text_height + 20)
max_border_height = max(max_border_height, border_height)
return max_border_height
def wrap_text(self, text, font, max_width):
lines = []
# Split the text by newlines to preserve intentional line breaks
paragraphs = text.split('\n')
for paragraph in paragraphs:
words = paragraph.split()
if not words: # This paragraph was an intentional line break
lines.append('')
continue
line = ''
for word in words:
# Check if the word can be added to the line
if font.getsize(line + word)[0] <= max_width:
line += word + ' '
else:
# If the line is not empty, append it and start a new line
if line:
lines.append(line)
line = ''
# If the word itself exceeds the max width, it's placed on a new line
line = word + ' '
# Append the last line of the paragraph
lines.append(line)
return [line.rstrip() for line in lines]
def overlayText(self, image, text, max_border_height, max_text_width):
processed_images = []
for image_tensor in image:
image_np = image_tensor.cpu().numpy()
pil_image = Image.fromarray((image_np * 255).astype(np.uint8))
# Settings for font and text
font = ImageFont.truetype("arial.ttf", 20)
max_width = max_text_width
# Use a function to wrap text based on pixel width
# Get the wrapped text
wrapped_text = self.wrap_text(text, font, max_width)
# Determine the height of each line (including padding)
line_height = font.getsize('A')[1] + 10 # Example height of a single line of text with padding
# Calculate total height required for wrapped text
# total_text_height = len(wrapped_text) * line_height
# Adjust border_height to fit all text
border_height = max_border_height
# Create a new image with extra space for the border
new_image = Image.new("RGB", (pil_image.width, pil_image.height + border_height), "black")
new_image.paste(pil_image, (0, border_height))
# Draw text on the border
draw = ImageDraw.Draw(new_image)
y_text = 10 # Starting position for text
for line in wrapped_text:
draw.text((10, y_text), line, fill=(255, 255, 255), font=font)
y_text += line_height # Move down the y position for the next line
# Convert back to Tensor
image_tensor_out = torch.tensor(np.array(new_image).astype(np.float32) / 255.0)
image_tensor_out = torch.unsqueeze(image_tensor_out, 0)
processed_images.append(image_tensor_out)
# Stack processed images along the batch dimension
final_tensor = torch.cat(processed_images, dim=0)
return final_tensor
def tensor_to_pil(self, image_tensor):
image_np = image_tensor.cpu().numpy()
return Image.fromarray((image_np.squeeze(0) * 255).astype(np.uint8))
def resize_image(self, image, height):
width = int(image.width * height / image.height)
return image.resize((width, height), Image.ANTIALIAS)
def add_text_to_image(self, image, text):
# Create a new image with extra space for the border
border_height = 100
new_image = Image.new("RGB", (image.width, image.height + border_height), "black")
new_image.paste(image, (0, border_height))
# Draw text on the border
draw = ImageDraw.Draw(new_image)
font = ImageFont.truetype("arial.ttf", 40)
draw.text((10, (border_height - 40) // 2), text, fill=(255, 255, 255), font=font)
return new_image
class TextTokens:
def __init__(self):
# Initialize any necessary variables or settings
pass
def parseTokens(self, text):
# For simplicity, this method currently does nothing and just returns the text.
# You can add token parsing logic here if needed.
return text
class DateIntegerNode:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"text": ("STRING", {"default": '', "multiline": False}),
"int_1": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff})
},
"optional": {
"text_c": ("STRING", {"default": '', "multiline": False}),
"text_d": ("STRING", {"default": '', "multiline": False}),
}
}
TEXT_TYPE = "STRING"
BOOLEAN_TYPE = "BOOLEAN"
RETURN_TYPES = (TEXT_TYPE,TEXT_TYPE,TEXT_TYPE,TEXT_TYPE,TEXT_TYPE,TEXT_TYPE,)
FUNCTION = "generate_date_string"
CATEGORY = "RenderRiftNodes"
def generate_date_string(self, text='', enable_date_folder=False, text_c='', text_d='', int_1=0):
tokens = TextTokens()
lq_video_path = datetime.now().strftime("%Y-%m-%d") + "/"
lq_img_path = datetime.now().strftime("%Y-%m-%d") + "/"
hq_video_path = datetime.now().strftime("%Y-%m-%d") + "/"
hq_img_path = datetime.now().strftime("%Y-%m-%d") + "/"
fd_video_path = datetime.now().strftime("%Y-%m-%d") + "/"
fd_img_path = datetime.now().strftime("%Y-%m-%d") + "/"
def append_text(base_text, new_text):
return base_text + new_text
lq_video_path = append_text(lq_video_path, str(int_1) + "lq_")
lq_img_path = append_text(lq_img_path, str(int_1) + "_lq/lqimg_")
lq_video_path = tokens.parseTokens(lq_video_path)
lq_img_path = tokens.parseTokens(lq_img_path)
hq_video_path = append_text(hq_video_path, str(int_1) + "hq_")
hq_img_path = append_text(hq_img_path, str(int_1) + "_hq/hqimg_")
fd_video_path = append_text(fd_video_path, str(int_1) + "hq_")
fd_img_path = append_text(fd_img_path, str(int_1) + "_hq/hqimg_")
hq_video_path = tokens.parseTokens(hq_video_path)
hq_img_path = tokens.parseTokens(hq_img_path)
fd_video_path = tokens.parseTokens(fd_video_path)
fd_img_path = tokens.parseTokens(fd_img_path)
return (lq_video_path,lq_img_path, hq_video_path, hq_img_path, fd_video_path, fd_img_path,)
class AnalyseMetadata:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
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 = "RenderRiftNodes"
RETURN_TYPES = ("STRING",)
FUNCTION = "analyse_metadata"
def analyse_metadata(self, image):
_, metadata = self.load_image_and_extract_metadata(image)
node_types = self.extract_node_types(metadata)
overlayed_image_path = self.overlay_metadata_on_image(image)
return overlayed_image_path if overlayed_image_path else "Failed to overlay metadata"
def extract_node_types(self, metadata):
try:
metadata_dict = json.loads(metadata)
node_types = [node['class_type'] for node in metadata_dict.values()]
return node_types
except json.JSONDecodeError:
return ["Error parsing metadata"]
def load_image_and_extract_metadata(self, image):
image_path = folder_paths.get_annotated_filepath(image)
with Image.open(image_path) as img:
img = ImageOps.exif_transpose(img)
metadata = img.info # Extract metadata
img = img.convert("RGB")
img = np.array(img).astype(np.float32) / 255.0
img_tensor = torch.from_numpy(img)[None,] # Convert to tensor
return img_tensor, metadata.get('Description', '{}') # Return Description or empty dict
@classmethod
def IS_CHANGED(cls, image):
# Implement as needed
pass
@classmethod
def VALIDATE_INPUTS(cls, image):
if not folder_paths.exists_annotated_filepath(image):
return "Invalid image file: {}".format(image)
return True
class LoadImageWithMeta:
@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 = "RenderRiftNodes"
RETURN_TYPES = ("IMAGE", "MASK","METADATA")
FUNCTION = "load_image_with_meta"
def load_image_with_meta(self, image):
image_path = folder_paths.get_annotated_filepath(image)
i = Image.open(image_path)
i = ImageOps.exif_transpose(i)
# Extract metadata (example: EXIF data)
metadata = i.info
# print(metadata)
image = i.convert("RGB")
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. - torch.from_numpy(mask)
else:
mask = torch.zeros((64,64), dtype=torch.float32, device="cpu")
return (image, mask.unsqueeze(0), metadata)
@classmethod
def IS_CHANGED(s, 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(s, image):
if not folder_paths.exists_annotated_filepath(image):
return "Invalid image file: {}".format(image)
return True