Add custom nodes, Civitai loras (LFS), and vast.ai setup script
Some checks failed
Python Linting / Run Ruff (push) Has been cancelled
Python Linting / Run Pylint (push) Has been cancelled
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.10, [self-hosted Linux], stable) (push) Has been cancelled
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.11, [self-hosted Linux], stable) (push) Has been cancelled
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.12, [self-hosted Linux], stable) (push) Has been cancelled
Full Comfy CI Workflow Runs / test-unix-nightly (12.1, , linux, 3.11, [self-hosted Linux], nightly) (push) Has been cancelled
Execution Tests / test (macos-latest) (push) Has been cancelled
Execution Tests / test (ubuntu-latest) (push) Has been cancelled
Execution Tests / test (windows-latest) (push) Has been cancelled
Test server launches without errors / test (push) Has been cancelled
Unit Tests / test (macos-latest) (push) Has been cancelled
Unit Tests / test (ubuntu-latest) (push) Has been cancelled
Unit Tests / test (windows-2022) (push) Has been cancelled
Some checks failed
Python Linting / Run Ruff (push) Has been cancelled
Python Linting / Run Pylint (push) Has been cancelled
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.10, [self-hosted Linux], stable) (push) Has been cancelled
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.11, [self-hosted Linux], stable) (push) Has been cancelled
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.12, [self-hosted Linux], stable) (push) Has been cancelled
Full Comfy CI Workflow Runs / test-unix-nightly (12.1, , linux, 3.11, [self-hosted Linux], nightly) (push) Has been cancelled
Execution Tests / test (macos-latest) (push) Has been cancelled
Execution Tests / test (ubuntu-latest) (push) Has been cancelled
Execution Tests / test (windows-latest) (push) Has been cancelled
Test server launches without errors / test (push) Has been cancelled
Unit Tests / test (macos-latest) (push) Has been cancelled
Unit Tests / test (ubuntu-latest) (push) Has been cancelled
Unit Tests / test (windows-2022) (push) Has been cancelled
Includes 30 custom nodes committed directly, 7 Civitai-exclusive loras stored via Git LFS, and a setup script that installs all dependencies and downloads HuggingFace-hosted models on vast.ai. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1
custom_nodes/ComfyUI-Crystools/nodes/__init__.py
Normal file
1
custom_nodes/ComfyUI-Crystools/nodes/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# intentionally left blank
|
||||
80
custom_nodes/ComfyUI-Crystools/nodes/_names.py
Normal file
80
custom_nodes/ComfyUI-Crystools/nodes/_names.py
Normal file
@@ -0,0 +1,80 @@
|
||||
from enum import Enum
|
||||
|
||||
prefix = '🪛 '
|
||||
|
||||
# IMPORTANT DON'T CHANGE THE 'NAME' AND 'TYPE' OF THE ENUMS, IT WILL BREAK THE COMPATIBILITY!
|
||||
# remember: NAME is for search, DESC is for contextual
|
||||
class CLASSES(Enum):
|
||||
CBOOLEAN_NAME = 'Primitive boolean [Crystools]'
|
||||
CBOOLEAN_DESC = prefix + 'Primitive boolean'
|
||||
CTEXT_NAME = 'Primitive string [Crystools]'
|
||||
CTEXT_DESC = prefix + 'Primitive string'
|
||||
CTEXTML_NAME = 'Primitive string multiline [Crystools]'
|
||||
CTEXTML_DESC = prefix + 'Primitive string multiline'
|
||||
CINTEGER_NAME = 'Primitive integer [Crystools]'
|
||||
CINTEGER_DESC = prefix + 'Primitive integer'
|
||||
CFLOAT_NAME = 'Primitive float [Crystools]'
|
||||
CFLOAT_DESC = prefix + 'Primitive float'
|
||||
|
||||
CDEBUGGER_CONSOLE_ANY_NAME = 'Show any [Crystools]'
|
||||
CDEBUGGER_ANY_DESC = prefix + 'Show any value to console/display'
|
||||
CDEBUGGER_CONSOLE_ANY_TO_JSON_NAME = 'Show any to JSON [Crystools]'
|
||||
CDEBUGGER_CONSOLE_ANY_TO_JSON_DESC = prefix + 'Show any to JSON'
|
||||
|
||||
CLIST_ANY_TYPE = 'ListAny'
|
||||
CLIST_ANY_NAME = 'List of any [Crystools]'
|
||||
CLIST_ANY_DESC = prefix + 'List of any'
|
||||
CLIST_STRING_TYPE = 'ListString'
|
||||
CLIST_STRING_NAME = 'List of strings [Crystools]'
|
||||
CLIST_STRING_DESC = prefix + 'List of strings'
|
||||
|
||||
CSWITCH_FROM_ANY_NAME = 'Switch from any [Crystools]'
|
||||
CSWITCH_FROM_ANY_DESC = prefix + 'Switch from any'
|
||||
CSWITCH_ANY_NAME = 'Switch any [Crystools]'
|
||||
CSWITCH_ANY_DESC = prefix + 'Switch any'
|
||||
CSWITCH_STRING_NAME = 'Switch string [Crystools]'
|
||||
CSWITCH_STRING_DESC = prefix + 'Switch string'
|
||||
CSWITCH_CONDITIONING_NAME = 'Switch conditioning [Crystools]'
|
||||
CSWITCH_CONDITIONING_DESC = prefix + 'Switch conditioning'
|
||||
CSWITCH_IMAGE_NAME = 'Switch image [Crystools]'
|
||||
CSWITCH_IMAGE_DESC = prefix + 'Switch image'
|
||||
CSWITCH_MASK_NAME = 'Switch mask [Crystools]'
|
||||
CSWITCH_MASK_DESC = prefix + 'Switch mask'
|
||||
CSWITCH_LATENT_NAME = 'Switch latent [Crystools]'
|
||||
CSWITCH_LATENT_DESC = prefix + 'Switch latent'
|
||||
|
||||
CPIPE_ANY_TYPE = 'CPipeAny'
|
||||
CPIPE_TO_ANY_NAME = 'Pipe to/edit any [Crystools]'
|
||||
CPIPE_TO_ANY_DESC = prefix + 'Pipe to/edit any'
|
||||
CPIPE_FROM_ANY_NAME = 'Pipe from any [Crystools]'
|
||||
CPIPE_FROM_ANY_DESC = prefix + 'Pipe from any'
|
||||
|
||||
CIMAGE_LOAD_METADATA_NAME = 'Load image with metadata [Crystools]'
|
||||
CIMAGE_LOAD_METADATA_DESC = prefix + 'Load image with metadata'
|
||||
CIMAGE_GET_RESOLUTION_NAME = 'Get resolution [Crystools]'
|
||||
CIMAGE_GET_RESOLUTION_DESC = prefix + 'Get resolution'
|
||||
CIMAGE_PREVIEW_IMAGE_NAME = 'Preview from image [Crystools]'
|
||||
CIMAGE_PREVIEW_IMAGE_DESC = prefix + 'Preview from image'
|
||||
CIMAGE_PREVIEW_METADATA_NAME = 'Preview from metadata [Crystools]'
|
||||
CIMAGE_PREVIEW_METADATA_DESC = prefix + 'Preview from metadata'
|
||||
CIMAGE_SAVE_METADATA_NAME = 'Save image with extra metadata [Crystools]'
|
||||
CIMAGE_SAVE_METADATA_DESC = prefix + 'Save image with extra metadata'
|
||||
|
||||
CMETADATA_EXTRACTOR_NAME = 'Metadata extractor [Crystools]'
|
||||
CMETADATA_EXTRACTOR_DESC = prefix + 'Metadata extractor'
|
||||
CMETADATA_COMPARATOR_NAME = 'Metadata comparator [Crystools]'
|
||||
CMETADATA_COMPARATOR_DESC = prefix + 'Metadata comparator'
|
||||
|
||||
CUTILS_JSON_COMPARATOR_NAME = 'JSON comparator [Crystools]'
|
||||
CUTILS_JSON_COMPARATOR_DESC = prefix + 'JSON comparator'
|
||||
CUTILS_STAT_SYSTEM_NAME = 'Stats system [Crystools]'
|
||||
CUTILS_STAT_SYSTEM_DESC = prefix + 'Stats system (powered by WAS)'
|
||||
|
||||
# CPARAMETERS_NAME = 'External parameter from JSON file [Crystools]'
|
||||
# CPARAMETERS_DESC = prefix + 'External parameters from JSON file'
|
||||
|
||||
CJSONFILE_NAME = 'Read JSON file [Crystools]'
|
||||
CJSONFILE_DESC = prefix + 'Read JSON file (BETA)'
|
||||
|
||||
CJSONEXTRACTOR_NAME = 'JSON extractor [Crystools]'
|
||||
CJSONEXTRACTOR_DESC = prefix + 'JSON extractor (BETA)'
|
||||
123
custom_nodes/ComfyUI-Crystools/nodes/debugger.py
Normal file
123
custom_nodes/ComfyUI-Crystools/nodes/debugger.py
Normal file
@@ -0,0 +1,123 @@
|
||||
import json
|
||||
from ..core import CONFIG, CATEGORY, BOOLEAN, BOOLEAN_FALSE, KEYS, TEXTS, STRING, logger, any
|
||||
|
||||
class CConsoleAny:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
},
|
||||
"optional": {
|
||||
"any_value": (any,),
|
||||
"console": BOOLEAN_FALSE,
|
||||
"display": BOOLEAN,
|
||||
KEYS.PREFIX.value: STRING,
|
||||
},
|
||||
"hidden": {
|
||||
# "unique_id": "UNIQUE_ID",
|
||||
# "extra_pnginfo": "EXTRA_PNGINFO",
|
||||
},
|
||||
}
|
||||
|
||||
CATEGORY = CATEGORY.MAIN.value + CATEGORY.DEBUGGER.value
|
||||
INPUT_IS_LIST = True
|
||||
|
||||
RETURN_TYPES = ()
|
||||
OUTPUT_NODE = True
|
||||
|
||||
FUNCTION = "execute"
|
||||
|
||||
def execute(self, any_value=None, console=False, display=True, prefix=None):
|
||||
console = console[0]
|
||||
display = display[0]
|
||||
prefix = prefix[0]
|
||||
text = ""
|
||||
textToDisplay = TEXTS.INACTIVE_MSG.value
|
||||
|
||||
if any_value is not None:
|
||||
try:
|
||||
if type(any_value) == list:
|
||||
for item in any_value:
|
||||
try:
|
||||
text += str(item)
|
||||
except Exception as e:
|
||||
text += "source exists, but could not be serialized.\n"
|
||||
logger.warn(e)
|
||||
else:
|
||||
logger.warn("any_value is not a list")
|
||||
|
||||
except Exception:
|
||||
try:
|
||||
text = json.dumps(any_value)[1:-1]
|
||||
except Exception:
|
||||
text = 'source exists, but could not be serialized.'
|
||||
|
||||
logger.debug(f"Show any to console is running...")
|
||||
|
||||
if console:
|
||||
if prefix is not None and prefix != "":
|
||||
print(f"{prefix}: {text}")
|
||||
else:
|
||||
print(text)
|
||||
|
||||
if display:
|
||||
textToDisplay = text
|
||||
|
||||
value = [console, display, prefix, textToDisplay]
|
||||
# setWidgetValues(value, unique_id, extra_pnginfo)
|
||||
|
||||
return {"ui": {"text": value}}
|
||||
|
||||
|
||||
class CConsoleAnyToJson:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
},
|
||||
"optional": {
|
||||
"any_value": (any,),
|
||||
},
|
||||
}
|
||||
|
||||
CATEGORY = CATEGORY.MAIN.value + CATEGORY.DEBUGGER.value
|
||||
INPUT_IS_LIST = True
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("string",)
|
||||
OUTPUT_NODE = True
|
||||
|
||||
FUNCTION = "execute"
|
||||
|
||||
def execute(self, any_value=None):
|
||||
text = TEXTS.INACTIVE_MSG.value
|
||||
|
||||
if any_value is not None and isinstance(any_value, list):
|
||||
item = any_value[0]
|
||||
|
||||
if isinstance(item, dict):
|
||||
try:
|
||||
text = json.dumps(item, indent=CONFIG["indent"])
|
||||
except Exception as e:
|
||||
text = "The input is a dict, but could not be serialized.\n"
|
||||
logger.warn(e)
|
||||
|
||||
elif isinstance(item, list):
|
||||
try:
|
||||
text = json.dumps(item, indent=CONFIG["indent"])
|
||||
except Exception as e:
|
||||
text = "The input is a list, but could not be serialized.\n"
|
||||
logger.warn(e)
|
||||
|
||||
else:
|
||||
text = str(item)
|
||||
|
||||
logger.debug(f"Show any-json to console is running...")
|
||||
|
||||
return {"ui": {"text": [text]}, "result": (text,)}
|
||||
517
custom_nodes/ComfyUI-Crystools/nodes/image.py
Normal file
517
custom_nodes/ComfyUI-Crystools/nodes/image.py
Normal file
@@ -0,0 +1,517 @@
|
||||
import fnmatch
|
||||
import os
|
||||
import random
|
||||
import sys
|
||||
import json
|
||||
import piexif
|
||||
import hashlib
|
||||
from datetime import datetime
|
||||
import torch
|
||||
import numpy as np
|
||||
from pathlib import Path
|
||||
from PIL import Image, ImageOps
|
||||
from PIL.ExifTags import TAGS, GPSTAGS, IFD
|
||||
from PIL.PngImagePlugin import PngImageFile
|
||||
from PIL.JpegImagePlugin import JpegImageFile
|
||||
from nodes import PreviewImage, SaveImage
|
||||
import folder_paths
|
||||
|
||||
from ..core import CATEGORY, CONFIG, BOOLEAN, METADATA_RAW,TEXTS, setWidgetValues, logger, getResolutionByTensor, get_size
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)), "comfy"))
|
||||
|
||||
|
||||
class CImagePreviewFromImage(PreviewImage):
|
||||
def __init__(self):
|
||||
self.output_dir = folder_paths.get_temp_directory()
|
||||
self.type = "temp"
|
||||
self.prefix_append = "_" + ''.join(random.choice("abcdefghijklmnopqrstupvxyz") for x in range(5))
|
||||
self.compress_level = 1
|
||||
self.data_cached = None
|
||||
self.data_cached_text = None
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
# if it is required, in next node does not receive any value even the cache!
|
||||
},
|
||||
"optional": {
|
||||
"image": ("IMAGE",),
|
||||
},
|
||||
"hidden": {
|
||||
"prompt": "PROMPT",
|
||||
"extra_pnginfo": "EXTRA_PNGINFO",
|
||||
},
|
||||
}
|
||||
|
||||
CATEGORY = CATEGORY.MAIN.value + CATEGORY.IMAGE.value
|
||||
RETURN_TYPES = ("METADATA_RAW",)
|
||||
RETURN_NAMES = ("Metadata RAW",)
|
||||
OUTPUT_NODE = True
|
||||
|
||||
FUNCTION = "execute"
|
||||
|
||||
def execute(self, image=None, prompt=None, extra_pnginfo=None):
|
||||
text = ""
|
||||
title = ""
|
||||
data = {
|
||||
"result": [''],
|
||||
"ui": {
|
||||
"text": [''],
|
||||
"images": [],
|
||||
}
|
||||
}
|
||||
|
||||
if image is not None:
|
||||
saved = self.save_images(image, "crystools/i", prompt, extra_pnginfo)
|
||||
image = saved["ui"]["images"][0]
|
||||
image_path = Path(self.output_dir).joinpath(image["subfolder"], image["filename"])
|
||||
|
||||
img, promptFromImage, metadata = buildMetadata(image_path)
|
||||
|
||||
images = [image]
|
||||
result = metadata
|
||||
|
||||
data["result"] = [result]
|
||||
data["ui"]["images"] = images
|
||||
|
||||
title = "Source: Image link \n"
|
||||
text += buildPreviewText(metadata)
|
||||
text += f"Current prompt (NO FROM IMAGE!):\n"
|
||||
text += json.dumps(promptFromImage, indent=CONFIG["indent"])
|
||||
|
||||
self.data_cached_text = text
|
||||
self.data_cached = data
|
||||
|
||||
elif image is None and self.data_cached is not None:
|
||||
title = "Source: Image link - CACHED\n"
|
||||
data = self.data_cached
|
||||
text = self.data_cached_text
|
||||
|
||||
else:
|
||||
logger.debug("Source: Empty on CImagePreviewFromImage")
|
||||
text = "Source: Empty"
|
||||
|
||||
data['ui']['text'] = [title + text]
|
||||
return data
|
||||
|
||||
|
||||
class CImagePreviewFromMetadata(PreviewImage):
|
||||
def __init__(self):
|
||||
self.data_cached = None
|
||||
self.data_cached_text = None
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
# if it is required, in next node does not receive any value even the cache!
|
||||
},
|
||||
"optional": {
|
||||
"metadata_raw": METADATA_RAW,
|
||||
},
|
||||
}
|
||||
|
||||
CATEGORY = CATEGORY.MAIN.value + CATEGORY.IMAGE.value
|
||||
RETURN_TYPES = ("METADATA_RAW",)
|
||||
RETURN_NAMES = ("Metadata RAW",)
|
||||
OUTPUT_NODE = True
|
||||
|
||||
FUNCTION = "execute"
|
||||
|
||||
def execute(self, metadata_raw=None):
|
||||
text = ""
|
||||
title = ""
|
||||
data = {
|
||||
"result": [''],
|
||||
"ui": {
|
||||
"text": [''],
|
||||
"images": [],
|
||||
}
|
||||
}
|
||||
|
||||
if metadata_raw is not None and metadata_raw != '':
|
||||
promptFromImage = {}
|
||||
if "prompt" in metadata_raw:
|
||||
promptFromImage = metadata_raw["prompt"]
|
||||
|
||||
title = "Source: Metadata RAW\n"
|
||||
text += buildPreviewText(metadata_raw)
|
||||
text += f"Prompt from image:\n"
|
||||
text += json.dumps(promptFromImage, indent=CONFIG["indent"])
|
||||
|
||||
images = self.resolveImage(metadata_raw["fileinfo"]["filename"])
|
||||
result = metadata_raw
|
||||
|
||||
data["result"] = [result]
|
||||
data["ui"]["images"] = images
|
||||
|
||||
self.data_cached_text = text
|
||||
self.data_cached = data
|
||||
|
||||
elif metadata_raw is None and self.data_cached is not None:
|
||||
title = "Source: Metadata RAW - CACHED\n"
|
||||
data = self.data_cached
|
||||
text = self.data_cached_text
|
||||
|
||||
else:
|
||||
logger.debug("Source: Empty on CImagePreviewFromMetadata")
|
||||
text = "Source: Empty"
|
||||
|
||||
data["ui"]["text"] = [title + text]
|
||||
return data
|
||||
|
||||
def resolveImage(self, filename=None):
|
||||
images = []
|
||||
|
||||
if filename is not None:
|
||||
image_input_folder = os.path.normpath(folder_paths.get_input_directory())
|
||||
image_input_folder_abs = Path(image_input_folder).resolve()
|
||||
|
||||
image_path = os.path.normpath(filename)
|
||||
image_path_abs = Path(image_path).resolve()
|
||||
|
||||
if Path(image_path_abs).is_file() is False:
|
||||
raise Exception(TEXTS.FILE_NOT_FOUND.value)
|
||||
|
||||
try:
|
||||
# get common path, should be input/output/temp folder
|
||||
common = os.path.commonpath([image_input_folder_abs, image_path_abs])
|
||||
|
||||
if common != image_input_folder:
|
||||
raise Exception("Path invalid (should be in the input folder)")
|
||||
|
||||
relative = os.path.normpath(os.path.relpath(image_path_abs, image_input_folder_abs))
|
||||
|
||||
images.append({
|
||||
"filename": Path(relative).name,
|
||||
"subfolder": os.path.dirname(relative),
|
||||
"type": "input"
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.warn(e)
|
||||
|
||||
return images
|
||||
|
||||
|
||||
class CImageGetResolution:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"image": ("IMAGE",),
|
||||
},
|
||||
"hidden": {
|
||||
"unique_id": "UNIQUE_ID",
|
||||
"extra_pnginfo": "EXTRA_PNGINFO",
|
||||
},
|
||||
}
|
||||
|
||||
CATEGORY = CATEGORY.MAIN.value + CATEGORY.IMAGE.value
|
||||
RETURN_TYPES = ("INT", "INT",)
|
||||
RETURN_NAMES = ("width", "height",)
|
||||
OUTPUT_NODE = True
|
||||
|
||||
FUNCTION = "execute"
|
||||
|
||||
def execute(self, image, extra_pnginfo=None, unique_id=None):
|
||||
res = getResolutionByTensor(image)
|
||||
text = [f"{res['x']}x{res['y']}"]
|
||||
setWidgetValues(text, unique_id, extra_pnginfo)
|
||||
logger.debug(f"Resolution: {text}")
|
||||
return {"ui": {"text": text}, "result": (res["x"], res["y"])}
|
||||
|
||||
|
||||
# subfolders based on: https://github.com/catscandrive/comfyui-imagesubfolders
|
||||
class CImageLoadWithMetadata:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
input_dir = folder_paths.get_input_directory()
|
||||
|
||||
exclude_files = {"Thumbs.db", "*.DS_Store", "desktop.ini", "*.lock" }
|
||||
exclude_folders = {"clipspace", ".*"}
|
||||
|
||||
file_list = []
|
||||
|
||||
for root, dirs, files in os.walk(input_dir, followlinks=True):
|
||||
# Exclude specific folders
|
||||
dirs[:] = [d for d in dirs if not any(fnmatch.fnmatch(d, exclude) for exclude in exclude_folders)]
|
||||
files = [f for f in files if not any(fnmatch.fnmatch(f, exclude) for exclude in exclude_files)]
|
||||
|
||||
|
||||
for file in files:
|
||||
relpath = os.path.relpath(os.path.join(root, file), start=input_dir)
|
||||
# fix for windows
|
||||
relpath = relpath.replace("\\", "/")
|
||||
file_list.append(relpath)
|
||||
|
||||
return {
|
||||
"required": {
|
||||
"image": (sorted(file_list), {"image_upload": True})
|
||||
},
|
||||
}
|
||||
|
||||
CATEGORY = CATEGORY.MAIN.value + CATEGORY.IMAGE.value
|
||||
RETURN_TYPES = ("IMAGE", "MASK", "JSON", "METADATA_RAW")
|
||||
RETURN_NAMES = ("image", "mask", "prompt", "Metadata RAW")
|
||||
OUTPUT_NODE = True
|
||||
|
||||
FUNCTION = "execute"
|
||||
|
||||
def execute(self, image):
|
||||
image_path = folder_paths.get_annotated_filepath(image)
|
||||
|
||||
imgF = Image.open(image_path)
|
||||
img, prompt, metadata = buildMetadata(image_path)
|
||||
if imgF.format == 'WEBP':
|
||||
# Use piexif to extract EXIF data from WebP image
|
||||
try:
|
||||
exif_data = piexif.load(image_path)
|
||||
prompt, metadata = self.process_exif_data(exif_data)
|
||||
except ValueError:
|
||||
prompt = {}
|
||||
|
||||
img = ImageOps.exif_transpose(img)
|
||||
image = img.convert("RGB")
|
||||
image = np.array(image).astype(np.float32) / 255.0
|
||||
image = torch.from_numpy(image)[None,]
|
||||
if 'A' in img.getbands():
|
||||
mask = np.array(img.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), prompt, metadata
|
||||
|
||||
def process_exif_data(self, exif_data):
|
||||
metadata = {}
|
||||
# 检查 '0th' 键下的 271 值,提取 Prompt 信息
|
||||
if '0th' in exif_data and 271 in exif_data['0th']:
|
||||
prompt_data = exif_data['0th'][271].decode('utf-8')
|
||||
# 移除可能的前缀 'Prompt:'
|
||||
prompt_data = prompt_data.replace('Prompt:', '', 1)
|
||||
# 假设 prompt_data 是一个字符串,尝试将其转换为 JSON 对象
|
||||
try:
|
||||
metadata['prompt'] = json.loads(prompt_data)
|
||||
except json.JSONDecodeError:
|
||||
metadata['prompt'] = prompt_data
|
||||
|
||||
# 检查 '0th' 键下的 270 值,提取 Workflow 信息
|
||||
if '0th' in exif_data and 270 in exif_data['0th']:
|
||||
workflow_data = exif_data['0th'][270].decode('utf-8')
|
||||
# 移除可能的前缀 'Workflow:'
|
||||
workflow_data = workflow_data.replace('Workflow:', '', 1)
|
||||
try:
|
||||
# 尝试将字节字符串转换为 JSON 对象
|
||||
metadata['workflow'] = json.loads(workflow_data)
|
||||
except json.JSONDecodeError:
|
||||
# 如果转换失败,则将原始字符串存储在 metadata 中
|
||||
metadata['workflow'] = workflow_data
|
||||
|
||||
metadata.update(exif_data)
|
||||
return metadata
|
||||
|
||||
@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 CImageSaveWithExtraMetadata(SaveImage):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.data_cached = None
|
||||
self.data_cached_text = None
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
# if it is required, in next node does not receive any value even the cache!
|
||||
"image": ("IMAGE",),
|
||||
"filename_prefix": ("STRING", {"default": "ComfyUI"}),
|
||||
"with_workflow": BOOLEAN,
|
||||
},
|
||||
"optional": {
|
||||
"metadata_extra": ("STRING", {"multiline": True, "default": json.dumps({
|
||||
"Title": "Image generated by Crystian",
|
||||
"Description": "More info: https:\/\/www.instagram.com\/crystian.ia",
|
||||
"Author": "crystian.ia",
|
||||
"Software": "ComfyUI",
|
||||
"Category": "StableDiffusion",
|
||||
"Rating": 5,
|
||||
"UserComment": "",
|
||||
"Keywords": [
|
||||
""
|
||||
],
|
||||
"Copyrights": "",
|
||||
}, indent=CONFIG["indent"]).replace("\\/", "/"),
|
||||
}),
|
||||
},
|
||||
"hidden": {
|
||||
"prompt": "PROMPT",
|
||||
"extra_pnginfo": "EXTRA_PNGINFO",
|
||||
},
|
||||
}
|
||||
|
||||
CATEGORY = CATEGORY.MAIN.value + CATEGORY.IMAGE.value
|
||||
RETURN_TYPES = ("METADATA_RAW",)
|
||||
RETURN_NAMES = ("Metadata RAW",)
|
||||
OUTPUT_NODE = True
|
||||
|
||||
FUNCTION = "execute"
|
||||
|
||||
def execute(self, image=None, filename_prefix="ComfyUI", with_workflow=True, metadata_extra=None, prompt=None, extra_pnginfo=None):
|
||||
data = {
|
||||
"result": [''],
|
||||
"ui": {
|
||||
"text": [''],
|
||||
"images": [],
|
||||
}
|
||||
}
|
||||
if image is not None:
|
||||
if with_workflow is True:
|
||||
extra_pnginfo_new = extra_pnginfo.copy()
|
||||
prompt = prompt.copy()
|
||||
else:
|
||||
extra_pnginfo_new = None
|
||||
prompt = None
|
||||
|
||||
if metadata_extra is not None and metadata_extra != 'undefined':
|
||||
try:
|
||||
# metadata_extra = json.loads(f"{{{metadata_extra}}}") // a fix?
|
||||
metadata_extra = json.loads(metadata_extra)
|
||||
except Exception as e:
|
||||
logger.error(f"Error parsing metadata_extra (it will send as string), error: {e}")
|
||||
metadata_extra = {"extra": str(metadata_extra)}
|
||||
|
||||
if isinstance(metadata_extra, dict):
|
||||
for k, v in metadata_extra.items():
|
||||
if extra_pnginfo_new is None:
|
||||
extra_pnginfo_new = {}
|
||||
|
||||
extra_pnginfo_new[k] = v
|
||||
|
||||
saved = super().save_images(image, filename_prefix, prompt, extra_pnginfo_new)
|
||||
|
||||
image = saved["ui"]["images"][0]
|
||||
image_path = Path(self.output_dir).joinpath(image["subfolder"], image["filename"])
|
||||
img, promptFromImage, metadata = buildMetadata(image_path)
|
||||
|
||||
images = [image]
|
||||
result = metadata
|
||||
|
||||
data["result"] = [result]
|
||||
data["ui"]["images"] = images
|
||||
|
||||
else:
|
||||
logger.debug("Source: Empty on CImageSaveWithExtraMetadata")
|
||||
|
||||
return data
|
||||
|
||||
|
||||
|
||||
def buildMetadata(image_path):
|
||||
if Path(image_path).is_file() is False:
|
||||
raise Exception(TEXTS.FILE_NOT_FOUND.value)
|
||||
|
||||
img = Image.open(image_path)
|
||||
|
||||
metadata = {}
|
||||
prompt = {}
|
||||
|
||||
metadata["fileinfo"] = {
|
||||
"filename": Path(image_path).as_posix(),
|
||||
"resolution": f"{img.width}x{img.height}",
|
||||
"date": str(datetime.fromtimestamp(os.path.getmtime(image_path))),
|
||||
"size": str(get_size(image_path)),
|
||||
}
|
||||
|
||||
# only for png files
|
||||
if isinstance(img, PngImageFile):
|
||||
metadataFromImg = img.info
|
||||
|
||||
# for all metadataFromImg convert to string (but not for workflow and prompt!)
|
||||
for k, v in metadataFromImg.items():
|
||||
# from ComfyUI
|
||||
if k == "workflow":
|
||||
try:
|
||||
metadata["workflow"] = json.loads(metadataFromImg["workflow"])
|
||||
except Exception as e:
|
||||
logger.warn(f"Error parsing metadataFromImg 'workflow': {e}")
|
||||
|
||||
# from ComfyUI
|
||||
elif k == "prompt":
|
||||
try:
|
||||
metadata["prompt"] = json.loads(metadataFromImg["prompt"])
|
||||
|
||||
# extract prompt to use on metadataFromImg
|
||||
prompt = metadata["prompt"]
|
||||
except Exception as e:
|
||||
logger.warn(f"Error parsing metadataFromImg 'prompt': {e}")
|
||||
|
||||
else:
|
||||
try:
|
||||
# for all possible metadataFromImg by user
|
||||
metadata[str(k)] = json.loads(v)
|
||||
except Exception as e:
|
||||
logger.debug(f"Error parsing {k} as json, trying as string: {e}")
|
||||
try:
|
||||
metadata[str(k)] = str(v)
|
||||
except Exception as e:
|
||||
logger.debug(f"Error parsing {k} it will be skipped: {e}")
|
||||
|
||||
if isinstance(img, JpegImageFile):
|
||||
exif = img.getexif()
|
||||
|
||||
for k, v in exif.items():
|
||||
tag = TAGS.get(k, k)
|
||||
if v is not None:
|
||||
metadata[str(tag)] = str(v)
|
||||
|
||||
for ifd_id in IFD:
|
||||
try:
|
||||
if ifd_id == IFD.GPSInfo:
|
||||
resolve = GPSTAGS
|
||||
else:
|
||||
resolve = TAGS
|
||||
|
||||
ifd = exif.get_ifd(ifd_id)
|
||||
ifd_name = str(ifd_id.name)
|
||||
metadata[ifd_name] = {}
|
||||
|
||||
for k, v in ifd.items():
|
||||
tag = resolve.get(k, k)
|
||||
metadata[ifd_name][str(tag)] = str(v)
|
||||
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
|
||||
return img, prompt, metadata
|
||||
|
||||
|
||||
def buildPreviewText(metadata):
|
||||
text = f"File: {metadata['fileinfo']['filename']}\n"
|
||||
text += f"Resolution: {metadata['fileinfo']['resolution']}\n"
|
||||
text += f"Date: {metadata['fileinfo']['date']}\n"
|
||||
text += f"Size: {metadata['fileinfo']['size']}\n"
|
||||
return text
|
||||
149
custom_nodes/ComfyUI-Crystools/nodes/list.py
Normal file
149
custom_nodes/ComfyUI-Crystools/nodes/list.py
Normal file
@@ -0,0 +1,149 @@
|
||||
from ..core import STRING, TEXTS, KEYS, CATEGORY, any, logger
|
||||
from ._names import CLASSES
|
||||
|
||||
|
||||
class CListAny:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
},
|
||||
"optional": {
|
||||
"any_1": (any,),
|
||||
"any_2": (any,),
|
||||
"any_3": (any,),
|
||||
"any_4": (any,),
|
||||
"any_5": (any,),
|
||||
"any_6": (any,),
|
||||
"any_7": (any,),
|
||||
"any_8": (any,),
|
||||
}
|
||||
}
|
||||
|
||||
CATEGORY = CATEGORY.MAIN.value + CATEGORY.LIST.value
|
||||
RETURN_TYPES = (any,),
|
||||
RETURN_NAMES = ("any_list",)
|
||||
OUTPUT_IS_LIST = (True,)
|
||||
|
||||
FUNCTION = "execute"
|
||||
|
||||
def execute(self,
|
||||
any_1=None,
|
||||
any_2=None,
|
||||
any_3=None,
|
||||
any_4=None,
|
||||
any_5=None,
|
||||
any_6=None,
|
||||
any_7=None,
|
||||
any_8=None):
|
||||
|
||||
list_any = []
|
||||
|
||||
if any_1 is not None:
|
||||
try:
|
||||
list_any.append(any_1)
|
||||
except Exception as e:
|
||||
logger.warn(e)
|
||||
if any_2 is not None:
|
||||
try:
|
||||
list_any.append(any_2)
|
||||
except Exception as e:
|
||||
logger.warn(e)
|
||||
if any_3 is not None:
|
||||
try:
|
||||
list_any.append(any_3)
|
||||
except Exception as e:
|
||||
logger.warn(e)
|
||||
if any_4 is not None:
|
||||
try:
|
||||
list_any.append(any_4)
|
||||
except Exception as e:
|
||||
logger.warn(e)
|
||||
if any_5 is not None:
|
||||
try:
|
||||
list_any.append(any_5)
|
||||
except Exception as e:
|
||||
logger.warn(e)
|
||||
if any_6 is not None:
|
||||
try:
|
||||
list_any.append(any_6)
|
||||
except Exception as e:
|
||||
logger.warn(e)
|
||||
if any_7 is not None:
|
||||
try:
|
||||
list_any.append(any_7)
|
||||
except Exception as e:
|
||||
logger.warn(e)
|
||||
if any_8 is not None:
|
||||
try:
|
||||
list_any.append(any_8)
|
||||
except Exception as e:
|
||||
logger.warn(e)
|
||||
|
||||
# yes, double brackets are needed because of the OUTPUT_IS_LIST... ¯\_(ツ)_/¯
|
||||
return [[list_any]]
|
||||
|
||||
|
||||
class CListString:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
},
|
||||
"optional": {
|
||||
"string_1": STRING,
|
||||
"string_2": STRING,
|
||||
"string_3": STRING,
|
||||
"string_4": STRING,
|
||||
"string_5": STRING,
|
||||
"string_6": STRING,
|
||||
"string_7": STRING,
|
||||
"string_8": STRING,
|
||||
"delimiter": ("STRING", {"default": " "}),
|
||||
}
|
||||
}
|
||||
|
||||
CATEGORY = CATEGORY.MAIN.value + CATEGORY.LIST.value
|
||||
RETURN_TYPES = ("STRING", CLASSES.CLIST_STRING_TYPE.value,)
|
||||
RETURN_NAMES = (TEXTS.CONCAT.value, KEYS.LIST.value)
|
||||
OUTPUT_IS_LIST = (False, True, )
|
||||
|
||||
FUNCTION = "execute"
|
||||
|
||||
def execute(self,
|
||||
string_1=None,
|
||||
string_2=None,
|
||||
string_3=None,
|
||||
string_4=None,
|
||||
string_5=None,
|
||||
string_6=None,
|
||||
string_7=None,
|
||||
string_8=None,
|
||||
delimiter=""):
|
||||
|
||||
list_str = []
|
||||
|
||||
if string_1 is not None and string_1 != "":
|
||||
list_str.append(string_1)
|
||||
if string_2 is not None and string_2 != "":
|
||||
list_str.append(string_2)
|
||||
if string_3 is not None and string_3 != "":
|
||||
list_str.append(string_3)
|
||||
if string_4 is not None and string_4 != "":
|
||||
list_str.append(string_4)
|
||||
if string_5 is not None and string_5 != "":
|
||||
list_str.append(string_5)
|
||||
if string_6 is not None and string_6 != "":
|
||||
list_str.append(string_6)
|
||||
if string_7 is not None and string_7 != "":
|
||||
list_str.append(string_7)
|
||||
if string_8 is not None and string_8 != "":
|
||||
list_str.append(string_8)
|
||||
|
||||
return delimiter.join(list_str), [list_str]
|
||||
164
custom_nodes/ComfyUI-Crystools/nodes/metadata.py
Normal file
164
custom_nodes/ComfyUI-Crystools/nodes/metadata.py
Normal file
@@ -0,0 +1,164 @@
|
||||
import json
|
||||
import re
|
||||
from ..core import CATEGORY, CONFIG, METADATA_RAW, TEXTS, findJsonsDiff, logger
|
||||
|
||||
|
||||
class CMetadataExtractor:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"metadata_raw": METADATA_RAW,
|
||||
},
|
||||
"optional": {
|
||||
}
|
||||
}
|
||||
|
||||
CATEGORY = CATEGORY.MAIN.value + CATEGORY.METADATA.value
|
||||
RETURN_TYPES = ("JSON", "JSON", "JSON", "JSON", "STRING", "STRING")
|
||||
RETURN_NAMES = ("prompt", "workflow", "file info", "raw to JSON", "raw to property", "raw to csv")
|
||||
# OUTPUT_NODE = True
|
||||
|
||||
FUNCTION = "execute"
|
||||
|
||||
def execute(self, metadata_raw=None):
|
||||
prompt = {}
|
||||
workflow = {}
|
||||
fileinfo = {}
|
||||
text = ""
|
||||
csv = ""
|
||||
|
||||
if metadata_raw is not None and isinstance(metadata_raw, dict):
|
||||
try:
|
||||
for key, value in metadata_raw.items():
|
||||
|
||||
if isinstance(value, dict):
|
||||
# yes, double json.dumps is needed for jsons
|
||||
value = json.dumps(json.dumps(value))
|
||||
else:
|
||||
value = json.dumps(value)
|
||||
|
||||
text += f"\"{key}\"={value}\n"
|
||||
# remove spaces
|
||||
# value = re.sub(' +', ' ', value)
|
||||
value = re.sub('\n', ' ', value)
|
||||
csv += f'"{key}"\t{value}\n'
|
||||
|
||||
if csv != "":
|
||||
csv = '"key"\t"value"\n' + csv
|
||||
|
||||
except Exception as e:
|
||||
logger.warn(e)
|
||||
|
||||
try:
|
||||
if "prompt" in metadata_raw:
|
||||
prompt = metadata_raw["prompt"]
|
||||
else:
|
||||
raise Exception("Prompt not found in metadata_raw")
|
||||
except Exception as e:
|
||||
logger.warn(e)
|
||||
|
||||
try:
|
||||
if "workflow" in metadata_raw:
|
||||
workflow = metadata_raw["workflow"]
|
||||
else:
|
||||
raise Exception("Workflow not found in metadata_raw")
|
||||
except Exception as e:
|
||||
logger.warn(e)
|
||||
|
||||
try:
|
||||
if "fileinfo" in metadata_raw:
|
||||
fileinfo = metadata_raw["fileinfo"]
|
||||
else:
|
||||
raise Exception("Fileinfo not found in metadata_raw")
|
||||
except Exception as e:
|
||||
logger.warn(e)
|
||||
|
||||
elif metadata_raw is None:
|
||||
logger.debug("metadata_raw is None")
|
||||
else:
|
||||
logger.warn(TEXTS.INVALID_METADATA_MSG.value)
|
||||
|
||||
return (json.dumps(prompt, indent=CONFIG["indent"]),
|
||||
json.dumps(workflow, indent=CONFIG["indent"]),
|
||||
json.dumps(fileinfo, indent=CONFIG["indent"]),
|
||||
json.dumps(metadata_raw, indent=CONFIG["indent"]),
|
||||
text, csv)
|
||||
|
||||
|
||||
class CMetadataCompare:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"metadata_raw_old": METADATA_RAW,
|
||||
"metadata_raw_new": METADATA_RAW,
|
||||
"what": (["Prompt", "Workflow", "Fileinfo"],),
|
||||
},
|
||||
"optional": {
|
||||
}
|
||||
}
|
||||
|
||||
CATEGORY = CATEGORY.MAIN.value + CATEGORY.METADATA.value
|
||||
RETURN_TYPES = ("JSON",)
|
||||
RETURN_NAMES = ("diff",)
|
||||
OUTPUT_NODE = True
|
||||
|
||||
FUNCTION = "execute"
|
||||
|
||||
def execute(self, what, metadata_raw_old=None, metadata_raw_new=None):
|
||||
prompt_old = {}
|
||||
workflow_old = {}
|
||||
fileinfo_old = {}
|
||||
prompt_new = {}
|
||||
workflow_new = {}
|
||||
fileinfo_new = {}
|
||||
diff = ""
|
||||
|
||||
if type(metadata_raw_old) == dict and type(metadata_raw_new) == dict:
|
||||
|
||||
if "prompt" in metadata_raw_old:
|
||||
prompt_old = metadata_raw_old["prompt"]
|
||||
else:
|
||||
logger.warn("Prompt not found in metadata_raw_old")
|
||||
|
||||
if "workflow" in metadata_raw_old:
|
||||
workflow_old = metadata_raw_old["workflow"]
|
||||
else:
|
||||
logger.warn("Workflow not found in metadata_raw_old")
|
||||
|
||||
if "fileinfo" in metadata_raw_old:
|
||||
fileinfo_old = metadata_raw_old["fileinfo"]
|
||||
else:
|
||||
logger.warn("Fileinfo not found in metadata_raw_old")
|
||||
|
||||
if "prompt" in metadata_raw_new:
|
||||
prompt_new = metadata_raw_new["prompt"]
|
||||
else:
|
||||
logger.warn("Prompt not found in metadata_raw_new")
|
||||
|
||||
if "workflow" in metadata_raw_new:
|
||||
workflow_new = metadata_raw_new["workflow"]
|
||||
else:
|
||||
logger.warn("Workflow not found in metadata_raw_new")
|
||||
|
||||
if "fileinfo" in metadata_raw_new:
|
||||
fileinfo_new = metadata_raw_new["fileinfo"]
|
||||
else:
|
||||
logger.warn("Fileinfo not found in metadata_raw_new")
|
||||
|
||||
if what == "Prompt":
|
||||
diff = findJsonsDiff(prompt_old, prompt_new)
|
||||
elif what == "Workflow":
|
||||
diff = findJsonsDiff(workflow_old, workflow_new)
|
||||
else:
|
||||
diff = findJsonsDiff(fileinfo_old, fileinfo_new)
|
||||
|
||||
diff = json.dumps(diff, indent=CONFIG["indent"])
|
||||
|
||||
else:
|
||||
invalid_msg = TEXTS.INVALID_METADATA_MSG.value
|
||||
logger.warn(invalid_msg)
|
||||
diff = invalid_msg
|
||||
|
||||
return {"ui": {"text": [diff]}, "result": (diff,)}
|
||||
170
custom_nodes/ComfyUI-Crystools/nodes/parameters.py
Normal file
170
custom_nodes/ComfyUI-Crystools/nodes/parameters.py
Normal file
@@ -0,0 +1,170 @@
|
||||
import json
|
||||
from ..core import CONFIG, any, JSON_WIDGET, CATEGORY, STRING, INT, FLOAT, BOOLEAN, logger, get_nested_value
|
||||
|
||||
# class CParameter:
|
||||
# def __init__(self):
|
||||
# pass
|
||||
#
|
||||
# @classmethod
|
||||
# def INPUT_TYPES(cls):
|
||||
# return {
|
||||
# "required": {
|
||||
# },
|
||||
# "optional": {
|
||||
# "path_to_json": STRING,
|
||||
# "key": STRING,
|
||||
# "default": STRING,
|
||||
# },
|
||||
# }
|
||||
#
|
||||
# CATEGORY = CATEGORY.MAIN.value + CATEGORY.UTILS.value
|
||||
# INPUT_IS_LIST = False
|
||||
#
|
||||
# RETURN_TYPES = (any,)
|
||||
# RETURN_NAMES = ("any",)
|
||||
#
|
||||
# FUNCTION = "execute"
|
||||
#
|
||||
# def execute(self, path_to_json=None, key=True, default=None):
|
||||
# text = default
|
||||
# value = text
|
||||
#
|
||||
# if path_to_json is not None and path_to_json != "":
|
||||
# logger.debug(f"External parameter from: '{path_to_json}'")
|
||||
# try:
|
||||
# with open(path_to_json, 'r') as file:
|
||||
# data = json.load(file)
|
||||
# logger.debug(f"File found, data: '{data}'")
|
||||
#
|
||||
# result = get_value(data, key, default)
|
||||
# text = result["text"]
|
||||
# value = result["value"]
|
||||
#
|
||||
# except Exception as e:
|
||||
# logger.error(e)
|
||||
# text = f"Error reading file: {e}\nReturning default value: '{default}'"
|
||||
# value = default
|
||||
#
|
||||
# return {"ui": {"text": [text]}, "result": [value]}
|
||||
|
||||
class CJsonFile:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
},
|
||||
"optional": {
|
||||
"path_to_json": STRING,
|
||||
},
|
||||
}
|
||||
|
||||
CATEGORY = CATEGORY.MAIN.value + CATEGORY.UTILS.value
|
||||
INPUT_IS_LIST = False
|
||||
|
||||
RETURN_TYPES = ("JSON",)
|
||||
RETURN_NAMES = ("json",)
|
||||
|
||||
FUNCTION = "execute"
|
||||
|
||||
def IS_CHANGED(path_to_json=None):
|
||||
return True
|
||||
|
||||
def execute(self, path_to_json=None):
|
||||
text = ""
|
||||
data = {}
|
||||
|
||||
if path_to_json is not None and path_to_json != "":
|
||||
logger.debug(f"Open json file: '{path_to_json}'")
|
||||
try:
|
||||
with open(path_to_json, 'r') as file:
|
||||
data = json.load(file)
|
||||
text = json.dumps(data, indent=CONFIG["indent"])
|
||||
logger.debug(f"File found, data: '{str(data)}'")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
text = f"Error reading file: {e}"
|
||||
|
||||
return {"ui": {"text": [text]}, "result": [data]}
|
||||
|
||||
class CJsonExtractor:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"json": JSON_WIDGET,
|
||||
},
|
||||
"optional": {
|
||||
"key": STRING,
|
||||
"default": STRING,
|
||||
},
|
||||
}
|
||||
|
||||
CATEGORY = CATEGORY.MAIN.value + CATEGORY.UTILS.value
|
||||
INPUT_IS_LIST = False
|
||||
|
||||
RETURN_TYPES = (any, "STRING", "INT", "FLOAT", "BOOLEAN")
|
||||
RETURN_NAMES = ("any", "string", "int", "float", "boolean")
|
||||
|
||||
# OUTPUT_IS_LIST = (False,)
|
||||
|
||||
FUNCTION = "execute"
|
||||
|
||||
def execute(cls, json=None, key=True, default=None):
|
||||
result = get_value(json, key, default)
|
||||
|
||||
result["any"] = result["value"]
|
||||
try:
|
||||
result["string"] = str(result["value"])
|
||||
except Exception as e:
|
||||
result["string"] = result["value"]
|
||||
|
||||
try:
|
||||
result["int"] = int(result["value"])
|
||||
except Exception as e:
|
||||
result["int"] = result["value"]
|
||||
|
||||
try:
|
||||
result["float"] = float(result["value"])
|
||||
except Exception as e:
|
||||
result["float"] = result["value"]
|
||||
|
||||
try:
|
||||
result["boolean"] = result["value"].lower() == "true"
|
||||
except Exception as e:
|
||||
result["boolean"] = result["value"]
|
||||
|
||||
return {
|
||||
"ui": {"text": [result["text"]]},
|
||||
"result": [
|
||||
result["any"],
|
||||
result["string"],
|
||||
result["int"],
|
||||
result["float"],
|
||||
result["boolean"]
|
||||
]
|
||||
}
|
||||
|
||||
def get_value(data, key, default=None):
|
||||
text = ""
|
||||
val = ""
|
||||
|
||||
if key is not None and key != "":
|
||||
val = get_nested_value(data, key, default)
|
||||
if default != val:
|
||||
text = f"Key found, return value: '{val}'"
|
||||
else:
|
||||
text = f"Key no found, return default value: '{val}'"
|
||||
else:
|
||||
text = f"Key is empty, return default value: '{val}'"
|
||||
|
||||
return {
|
||||
"text": text,
|
||||
"value": val
|
||||
}
|
||||
74
custom_nodes/ComfyUI-Crystools/nodes/pipe.py
Normal file
74
custom_nodes/ComfyUI-Crystools/nodes/pipe.py
Normal file
@@ -0,0 +1,74 @@
|
||||
from ..core import CATEGORY, any
|
||||
from ._names import CLASSES
|
||||
|
||||
|
||||
class CPipeToAny:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {},
|
||||
"optional": {
|
||||
CLASSES.CPIPE_ANY_TYPE.value: (CLASSES.CPIPE_ANY_TYPE.value,),
|
||||
"any_1": (any,),
|
||||
"any_2": (any,),
|
||||
"any_3": (any,),
|
||||
"any_4": (any,),
|
||||
"any_5": (any,),
|
||||
"any_6": (any,),
|
||||
}
|
||||
}
|
||||
|
||||
CATEGORY = CATEGORY.MAIN.value + CATEGORY.PIPE.value
|
||||
RETURN_TYPES = (CLASSES.CPIPE_ANY_TYPE.value,)
|
||||
|
||||
FUNCTION = "execute"
|
||||
|
||||
def execute(self, CPipeAny=None, any_1=None, any_2=None, any_3=None, any_4=None, any_5=None, any_6=None):
|
||||
any_1_original = None
|
||||
any_2_original = None
|
||||
any_3_original = None
|
||||
any_4_original = None
|
||||
any_5_original = None
|
||||
any_6_original = None
|
||||
|
||||
if CPipeAny != None:
|
||||
any_1_original, any_2_original, any_3_original, any_4_original, any_5_original, any_6_original = CPipeAny
|
||||
|
||||
CAnyPipeMod = []
|
||||
|
||||
CAnyPipeMod.append(any_1 if any_1 is not None else any_1_original)
|
||||
CAnyPipeMod.append(any_2 if any_2 is not None else any_2_original)
|
||||
CAnyPipeMod.append(any_3 if any_3 is not None else any_3_original)
|
||||
CAnyPipeMod.append(any_4 if any_4 is not None else any_4_original)
|
||||
CAnyPipeMod.append(any_5 if any_5 is not None else any_5_original)
|
||||
CAnyPipeMod.append(any_6 if any_6 is not None else any_6_original)
|
||||
|
||||
return (CAnyPipeMod,)
|
||||
|
||||
|
||||
class CPipeFromAny:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
CLASSES.CPIPE_ANY_TYPE.value: (CLASSES.CPIPE_ANY_TYPE.value,),
|
||||
},
|
||||
"optional": {
|
||||
}
|
||||
}
|
||||
|
||||
CATEGORY = CATEGORY.MAIN.value + CATEGORY.PIPE.value
|
||||
RETURN_TYPES = (CLASSES.CPIPE_ANY_TYPE.value, any, any, any, any, any, any,)
|
||||
RETURN_NAMES = (CLASSES.CPIPE_ANY_TYPE.value, "any_1", "any_2", "any_3", "any_4", "any_5", "any_6",)
|
||||
|
||||
FUNCTION = "execute"
|
||||
|
||||
def execute(self, CPipeAny=None, ):
|
||||
any_1, any_2, any_3, any_4, any_5, any_6 = CPipeAny
|
||||
return CPipeAny, any_1, any_2, any_3, any_4, any_5, any_6
|
||||
111
custom_nodes/ComfyUI-Crystools/nodes/primitive.py
Normal file
111
custom_nodes/ComfyUI-Crystools/nodes/primitive.py
Normal file
@@ -0,0 +1,111 @@
|
||||
from ..core import BOOLEAN, CATEGORY, STRING, INT, FLOAT, STRING_ML
|
||||
|
||||
|
||||
class CBoolean:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"boolean": BOOLEAN,
|
||||
}
|
||||
}
|
||||
|
||||
CATEGORY = CATEGORY.MAIN.value + CATEGORY.PRIMITIVE.value
|
||||
RETURN_TYPES = ("BOOLEAN",)
|
||||
RETURN_NAMES = ("boolean",)
|
||||
|
||||
FUNCTION = "execute"
|
||||
|
||||
def execute(self, boolean=True):
|
||||
return (boolean,)
|
||||
|
||||
|
||||
class CText:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"string": STRING,
|
||||
}
|
||||
}
|
||||
|
||||
CATEGORY = CATEGORY.MAIN.value + CATEGORY.PRIMITIVE.value
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("string",)
|
||||
|
||||
FUNCTION = "execute"
|
||||
|
||||
def execute(self, string=""):
|
||||
return (string,)
|
||||
|
||||
|
||||
class CTextML:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"string": STRING_ML,
|
||||
}
|
||||
}
|
||||
|
||||
CATEGORY = CATEGORY.MAIN.value + CATEGORY.PRIMITIVE.value
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("string",)
|
||||
|
||||
FUNCTION = "execute"
|
||||
|
||||
def execute(self, string=""):
|
||||
return (string,)
|
||||
|
||||
|
||||
class CInteger:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"int": INT,
|
||||
}
|
||||
}
|
||||
|
||||
CATEGORY = CATEGORY.MAIN.value + CATEGORY.PRIMITIVE.value
|
||||
RETURN_TYPES = ("INT",)
|
||||
RETURN_NAMES = ("int",)
|
||||
|
||||
FUNCTION = "execute"
|
||||
|
||||
def execute(self, int=True):
|
||||
return (int,)
|
||||
|
||||
|
||||
class CFloat:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"float": FLOAT,
|
||||
}
|
||||
}
|
||||
|
||||
CATEGORY = CATEGORY.MAIN.value + CATEGORY.PRIMITIVE.value
|
||||
RETURN_TYPES = ("FLOAT",)
|
||||
RETURN_NAMES = ("float",)
|
||||
|
||||
FUNCTION = "execute"
|
||||
|
||||
def execute(self, float=True):
|
||||
return (float,)
|
||||
225
custom_nodes/ComfyUI-Crystools/nodes/switch.py
Normal file
225
custom_nodes/ComfyUI-Crystools/nodes/switch.py
Normal file
@@ -0,0 +1,225 @@
|
||||
from ..core import BOOLEAN, STRING, CATEGORY, any, logger
|
||||
|
||||
|
||||
class CSwitchFromAny:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"any": (any, ),
|
||||
"boolean": BOOLEAN,
|
||||
}
|
||||
}
|
||||
|
||||
CATEGORY = CATEGORY.MAIN.value + CATEGORY.SWITCH.value
|
||||
RETURN_TYPES = (any, any,)
|
||||
RETURN_NAMES = ("on_true", "on_false",)
|
||||
|
||||
FUNCTION = "execute"
|
||||
|
||||
def execute(self, any,boolean=True):
|
||||
logger.debug("Any switch: " + str(boolean))
|
||||
|
||||
if boolean:
|
||||
return any, None
|
||||
else:
|
||||
return None, any
|
||||
|
||||
class CSwitchBooleanAny:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"on_true": (any, {"lazy": True}),
|
||||
"on_false": (any, {"lazy": True}),
|
||||
"boolean": BOOLEAN,
|
||||
}
|
||||
}
|
||||
|
||||
CATEGORY = CATEGORY.MAIN.value + CATEGORY.SWITCH.value
|
||||
RETURN_TYPES = (any,)
|
||||
|
||||
FUNCTION = "execute"
|
||||
|
||||
def check_lazy_status(self, on_true=None, on_false=None, boolean=True):
|
||||
needed = "on_true" if boolean else "on_false"
|
||||
return [needed]
|
||||
|
||||
def execute(self, on_true, on_false, boolean=True):
|
||||
logger.debug("Any switch: " + str(boolean))
|
||||
|
||||
if boolean:
|
||||
return (on_true,)
|
||||
else:
|
||||
return (on_false,)
|
||||
|
||||
|
||||
class CSwitchBooleanString:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"on_true": ("STRING", {"default": "", "lazy": True}),
|
||||
"on_false": ("STRING", {"default": "", "lazy": True}),
|
||||
"boolean": BOOLEAN,
|
||||
}
|
||||
}
|
||||
|
||||
CATEGORY = CATEGORY.MAIN.value + CATEGORY.SWITCH.value
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("string",)
|
||||
|
||||
FUNCTION = "execute"
|
||||
|
||||
def check_lazy_status(self, on_true=None, on_false=None, boolean=True):
|
||||
needed = "on_true" if boolean else "on_false"
|
||||
return [needed]
|
||||
|
||||
def execute(self, on_true, on_false, boolean=True):
|
||||
logger.debug("String switch: " + str(boolean))
|
||||
|
||||
if boolean:
|
||||
return (on_true,)
|
||||
else:
|
||||
return (on_false,)
|
||||
|
||||
|
||||
class CSwitchBooleanConditioning:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"on_true": ("CONDITIONING", {"lazy": True}),
|
||||
"on_false": ("CONDITIONING", {"lazy": True}),
|
||||
"boolean": BOOLEAN,
|
||||
}
|
||||
}
|
||||
|
||||
CATEGORY = CATEGORY.MAIN.value + CATEGORY.SWITCH.value
|
||||
RETURN_TYPES = ("CONDITIONING",)
|
||||
RETURN_NAMES = ("conditioning",)
|
||||
|
||||
FUNCTION = "execute"
|
||||
|
||||
def check_lazy_status(self, on_true=None, on_false=None, boolean=True):
|
||||
needed = "on_true" if boolean else "on_false"
|
||||
return [needed]
|
||||
|
||||
def execute(self, on_true, on_false, boolean=True):
|
||||
logger.debug("Conditioning switch: " + str(boolean))
|
||||
|
||||
if boolean:
|
||||
return (on_true,)
|
||||
else:
|
||||
return (on_false,)
|
||||
|
||||
|
||||
class CSwitchBooleanImage:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"on_true": ("IMAGE", {"lazy": True}),
|
||||
"on_false": ("IMAGE", {"lazy": True}),
|
||||
"boolean": BOOLEAN,
|
||||
}
|
||||
}
|
||||
|
||||
CATEGORY = CATEGORY.MAIN.value + CATEGORY.SWITCH.value
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
RETURN_NAMES = ("image",)
|
||||
|
||||
FUNCTION = "execute"
|
||||
|
||||
def check_lazy_status(self, on_true=None, on_false=None, boolean=True):
|
||||
needed = "on_true" if boolean else "on_false"
|
||||
return [needed]
|
||||
|
||||
def execute(self, on_true, on_false, boolean=True):
|
||||
logger.debug("Image switch: " + str(boolean))
|
||||
|
||||
if boolean:
|
||||
return (on_true,)
|
||||
else:
|
||||
return (on_false,)
|
||||
|
||||
|
||||
class CSwitchBooleanLatent:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"on_true": ("LATENT", {"lazy": True}),
|
||||
"on_false": ("LATENT", {"lazy": True}),
|
||||
"boolean": BOOLEAN,
|
||||
}
|
||||
}
|
||||
|
||||
CATEGORY = CATEGORY.MAIN.value + CATEGORY.SWITCH.value
|
||||
RETURN_TYPES = ("LATENT",)
|
||||
RETURN_NAMES = ("latent",)
|
||||
|
||||
FUNCTION = "execute"
|
||||
|
||||
def check_lazy_status(self, on_true=None, on_false=None, boolean=True):
|
||||
needed = "on_true" if boolean else "on_false"
|
||||
return [needed]
|
||||
|
||||
def execute(self, on_true, on_false, boolean=True):
|
||||
logger.debug("Latent switch: " + str(boolean))
|
||||
|
||||
if boolean:
|
||||
return (on_true,)
|
||||
else:
|
||||
return (on_false,)
|
||||
|
||||
|
||||
class CSwitchBooleanMask:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"on_true": ("MASK", {"lazy": True}),
|
||||
"on_false": ("MASK", {"lazy": True}),
|
||||
"boolean": BOOLEAN,
|
||||
}
|
||||
}
|
||||
|
||||
CATEGORY = CATEGORY.MAIN.value + CATEGORY.SWITCH.value
|
||||
RETURN_TYPES = ("MASK",)
|
||||
RETURN_NAMES = ("mask",)
|
||||
|
||||
FUNCTION = "execute"
|
||||
|
||||
def check_lazy_status(self, on_true=None, on_false=None, boolean=True):
|
||||
needed = "on_true" if boolean else "on_false"
|
||||
return [needed]
|
||||
|
||||
def execute(self, on_true, on_false, boolean=True):
|
||||
logger.debug("Mask switch: " + str(boolean))
|
||||
|
||||
if boolean:
|
||||
return (on_true,)
|
||||
else:
|
||||
return (on_false,)
|
||||
55
custom_nodes/ComfyUI-Crystools/nodes/utils.py
Normal file
55
custom_nodes/ComfyUI-Crystools/nodes/utils.py
Normal file
@@ -0,0 +1,55 @@
|
||||
from ..core import CATEGORY, JSON_WIDGET, findJsonStrDiff, get_system_stats, logger
|
||||
|
||||
|
||||
class CUtilsCompareJsons:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"json_old": JSON_WIDGET,
|
||||
"json_new": JSON_WIDGET,
|
||||
},
|
||||
"optional": {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
CATEGORY = CATEGORY.MAIN.value + CATEGORY.UTILS.value
|
||||
RETURN_TYPES = ("JSON",)
|
||||
RETURN_NAMES = ("json_compared",)
|
||||
OUTPUT_NODE = True
|
||||
|
||||
FUNCTION = "execute"
|
||||
|
||||
def execute(self, json_old, json_new):
|
||||
json = findJsonStrDiff(json_old, json_new)
|
||||
return (str(json),)
|
||||
|
||||
|
||||
# Credits to: https://github.com/WASasquatch/was-node-suite-comfyui for the following node!
|
||||
class CUtilsStatSystem:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"latent": ("LATENT",),
|
||||
}
|
||||
}
|
||||
|
||||
CATEGORY = CATEGORY.MAIN.value + CATEGORY.UTILS.value
|
||||
RETURN_TYPES = ("LATENT",)
|
||||
RETURN_NAMES = ("latent",)
|
||||
|
||||
FUNCTION = "execute"
|
||||
|
||||
def execute(self, latent):
|
||||
log = "Samples Passthrough:\n"
|
||||
for stat in get_system_stats():
|
||||
log += stat + "\n"
|
||||
|
||||
logger.debug(log)
|
||||
|
||||
return {"ui": {"text": [log]}, "result": (latent,)}
|
||||
Reference in New Issue
Block a user