diff --git a/.github/images/workflowpt1.png b/.github/images/workflowpt1.png new file mode 100644 index 0000000..25e6327 Binary files /dev/null and b/.github/images/workflowpt1.png differ diff --git a/.github/images/workflowpt2.png b/.github/images/workflowpt2.png new file mode 100644 index 0000000..f94d8e2 Binary files /dev/null and b/.github/images/workflowpt2.png differ diff --git a/README.md b/README.md index ddec606..e3446f8 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,52 @@ -# ComfyUI-RenderRiftNodes +# RenderRift Node Pack -## Description -Hey everyone! I've whipped up a cool little node pack for ComfyUI. It's just a few nodes I made for myself, but I think they're pretty neat. We've got three main players here. First, there’s the Custom Output Folder String Generator for keeping your files tidy. Next, the LoadImageWithMetadata node – it’s a real game changer because it lets you pull up image metadata, which is perfect for the third node, the Image Metadata Overlay Node. This one lets you overlay that metadata right onto your images. LoadImageWithMetadata was actually made specifically to get the Metadata Overlay Node working. +Welcome to the RenderRift Custom Node Pack for the ComfyUI. This was created as I need a couple of extra nodes to help in my Animatediff workflow. Below you'll find detailed explanations of each node and their respective functionalities. -## Examples -TBC \ No newline at end of file +## Nodes Description + +### RR_VideoPathMetaExtraction +- **Purpose**: Works in conjunction with VHS_LoadVideoPath to set the file_path for a video and extracts the metadata from the file to send to RR_Image_metadata_overlay. +- **Functionality**: + - Provides metadata to RR_Image_Metadata_Overlay + - Provides file path to VHS_LoadVideoPath + +### RR_Image_Metadata_Overlay +- **Purpose**: Main node for displaying video and metadata in a grid format. +- **Features**: + - Supports 2-6 videos & their corresponding metadatas. + - Optional support for an "original video" for comparison in vid2vid workflows. + - Grid format display with data overlay options. +- **Predefined Options for Overlay**: + - checkpoint, ksampler, controlnets, animatediff, ipadapter, loras. + - `search_query`: Enter a class_type from metadata to output all related values. +- **Usage**: Central to creating comparative video overlays with metadata insights. + + +### RR_Date_Folder_Format Node +- **Purpose**: Outputs a string for saving images/videos in a specific format. To be used as an input to the filename_prefix +- **Output Format**: + - `{todays_date}/1lq_` + - `{todays_date}/1_lq/lqimg_` + - `{todays_date}/1hq_` + - `{todays_date}/1_hq/hqimg_` + - `{todays_date}/1facedetailer_` + - `{todays_date}/1_facedetailer/facedetailerimg_` + + +## Workflows + +### Workflow 1: Node Type Listing +![Workflow part 1](.github/images/workflowpt1.png) +- **Description**: Retrieves a list of node types from your video. +- **Steps**: + - Run the first group in the workflow to obtain the node types. + +### Workflow 2: Video Comparison Setup +![Workflow part 2](.github/images/workflowpt2.png) +- **Description**: Sets up video paths for comparison and overlay selection in RR_Image_Metadata_Overlay. +- **Requirements**: Ensure videos have been saved using VHS_VideoCombine with metadata enabled. +- **Steps**: + - Bypass the first group if node types are known. + - Select video paths for comparison. + - Choose overlays for the RR_Image_Metadata_Overlay. + - Save the output as a comparison video. diff --git a/__init__.py b/__init__.py index 1cdcb2b..2996814 100644 --- a/__init__.py +++ b/__init__.py @@ -1,16 +1,20 @@ -from .date_integer_node import DateIntegerNode, MetadataOverlayNode, AnalyseMetadata, LoadImageWithMeta +from .date_integer_node import DateIntegerNode, MetadataOverlayNode, AnalyseMetadata, LoadImageWithMeta, VideoPathMetaExtraction, DisplayMetaOptions NODE_CLASS_MAPPINGS = { "DateIntegerNode": DateIntegerNode, "AnalyseMetadata": AnalyseMetadata, "MetadataOverlayNode": MetadataOverlayNode, "LoadImageWithMeta": LoadImageWithMeta, + "VideoPathMetaExtraction": VideoPathMetaExtraction, + "DisplayMetaOptions": DisplayMetaOptions, } NODE_DISPLAY_NAME_MAPPINGS = { - "DateIntegerNode": "Customer Date Folder Format", - "MetadataOverlayNode": "Image Metadata Overlay Node", - "LoadImageWithMeta": "LoadImageWithMeta", + "DateIntegerNode": "RR_Date_Folder_Format", + "MetadataOverlayNode": "RR_Image_Metadata_Overlay", + "LoadImageWithMeta": "RR_LoadImageWithMeta (Depreciated)", + "VideoPathMetaExtraction": "RR_VideoPathMetaExtraction", + "DisplayMetaOptions": "RR_DisplayMetaOptions", } __all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS'] diff --git a/date_integer_node.py b/date_integer_node.py index 1bbc28d..56f56ed 100644 --- a/date_integer_node.py +++ b/date_integer_node.py @@ -1,4 +1,5 @@ -from datetime import datetime +from cmd import PROMPT +from datetime import datetime from pickle import APPEND import os import torch @@ -7,6 +8,7 @@ import json import textwrap from PIL import Image, ImageOps, ImageDraw, ImageFont import folder_paths +from tinytag import TinyTag def process_text(anything): return {"text": [str(anything)]} @@ -40,6 +42,7 @@ class MetadataOverlayNode: "metadata5": ("METADATA",), "image6": ("IMAGE",), "metadata6": ("METADATA",), + "search_query": ("STRING", {"default": ''}), }, } @@ -47,11 +50,10 @@ class MetadataOverlayNode: pass - def extract_metadata(self, metadata, checkpoint, ksampler, controlnets, animdiff, ipadpter, loras): + def extract_metadata(self, metadata, checkpoint, ksampler, controlnets, animdiff, ipadpter, loras, search_query=None): if metadata == "Original Video": return "Original Video" - metadata = metadata.get('prompt') if metadata: try: # Parse the JSON metadata @@ -65,8 +67,15 @@ class MetadataOverlayNode: for node_id, node in prompt_data.items(): node_type = node.get('class_type') inputs = node.get('inputs', {}) + + if search_query and node_type == search_query: + # Format and return the inputs of the matching class type + details = search_query + "\n" + '\n'.join([f"{key}: {value}" for key, value in inputs.items()]) + class_type_details.setdefault(node_type, []).append(details) + + # Collect the details for each class_type of interest - if animdiff and node_type == "ADE_AnimateDiffLoaderWithContext": + elif animdiff and node_type == "ADE_AnimateDiffLoaderWithContext": # Extract the context_options ID from the inputs context_options_id = inputs.get('context_options', [])[0] context_details = "" @@ -96,7 +105,7 @@ class MetadataOverlayNode: 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: @@ -127,6 +136,7 @@ class MetadataOverlayNode: metadata5=None, image6=None, metadata6=None, + search_query=None ): images_and_metadata = [] @@ -157,12 +167,12 @@ class MetadataOverlayNode: 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] + all_texts = [self.extract_metadata(meta, checkpoint, ksampler, controlnets, animdiff, ipadpter, loras, search_query) 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) + text = self.extract_metadata(meta, checkpoint, ksampler, controlnets, animdiff, ipadpter, loras, search_query) 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) @@ -322,13 +332,8 @@ class DateIntegerNode: 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" @@ -339,7 +344,7 @@ class DateIntegerNode: CATEGORY = "RenderRiftNodes" - def generate_date_string(self, text='', enable_date_folder=False, text_c='', text_d='', int_1=0): + def generate_date_string(self, int_1=0): tokens = TextTokens() @@ -364,8 +369,8 @@ class DateIntegerNode: 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_") + fd_video_path = append_text(fd_video_path, str(int_1) + "facedetailer_") + fd_img_path = append_text(fd_img_path, str(int_1) + "_facedetailer/facedetailerimg_") hq_video_path = tokens.parseTokens(hq_video_path) hq_img_path = tokens.parseTokens(hq_img_path) @@ -472,4 +477,88 @@ class LoadImageWithMeta: if not folder_paths.exists_annotated_filepath(image): return "Invalid image file: {}".format(image) - return True \ No newline at end of file + return True + + +class DisplayMetaOptions: + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "metadata": ("METADATA",), + }, + } + + RETURN_TYPES = ("STRING",) + FUNCTION = "process_metadata" + + def process_metadata(self, metadata): + + try: + json_data = json.loads(metadata) + prompt_data = json_data.get("prompt", json_data) + + class_types = [(key, value['class_type']) for key, value in prompt_data.items() if 'class_type' in value] + + sorted_class_types_string = '\n'.join([f"{class_type}" for id, class_type in class_types]) + + + + + # Convert the list of strings to a JSON string + print(sorted_class_types_string) + return [sorted_class_types_string] + except json.JSONDecodeError: + return "Error parsing metadata. Ensure it is in valid JSON format." + + + + + +class VideoPathMetaExtraction: + CATEGORY = "RenderRiftNodes" + RETURN_TYPES = ("STRING", "METADATA") + FUNCTION = "process_video_file" + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "video_file_path": ("STRING", {"default": ''}) + } + } + + def __init__(self): + pass + + def process_video_file(self, video_file_path): + # Check if the file exists + if not os.path.exists(video_file_path): + return ("File not found", {}) + + # Extract metadata from the video + metadata = self.extract_metadata(video_file_path) + + return (video_file_path, metadata) + + def extract_metadata(self, file_path): + # Implement metadata extraction logic here + # This is an example, replace with actual logic + try: + + if file_path.lower().endswith('.mp4'): + try: + tag = TinyTag.get(file_path) + metadata = tag.comment + return metadata + except Exception as e: + print("Error extracting metadata using TinyTag:", e) + return + else: + print("Unsupported file type.") + return + except Exception as e: + return f"Error extracting metadata: {str(e)}" diff --git a/requirements.txt b/requirements.txt index 5873a22..835d05e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ -Pillow \ No newline at end of file +Pillow +tinytag \ No newline at end of file