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

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:
2026-02-09 00:55:26 +00:00
parent 2b70ab9ad0
commit f09734b0ee
2274 changed files with 748556 additions and 3 deletions

View File

@@ -0,0 +1,6 @@
from .logger import *
from .keys import *
from .types import *
from .config import *
from .common import *
from .version import *

View File

@@ -0,0 +1,119 @@
import os
import json
import torch
from deepdiff import DeepDiff
from ..core import CONFIG, logger
# just a helper function to set the widget values (or clear them)
def setWidgetValues(value=None, unique_id=None, extra_pnginfo=None) -> None:
if unique_id and extra_pnginfo:
workflow = extra_pnginfo["workflow"]
node = next((x for x in workflow["nodes"] if str(x["id"]) == unique_id), None)
if node:
node["widgets_values"] = value
return None
# find difference between two jsons
def findJsonStrDiff(json1, json2):
msgError = "Could not compare jsons"
returnJson = {"error": msgError}
try:
# TODO review this
# dict1 = json.loads(json1)
# dict2 = json.loads(json2)
returnJson = findJsonsDiff(json1, json2)
returnJson = json.dumps(returnJson, indent=CONFIG["indent"])
except Exception as e:
logger.warn(f"{msgError}: {e}")
return returnJson
def findJsonsDiff(json1, json2):
msgError = "Could not compare jsons"
returnJson = {"error": msgError}
try:
diff = DeepDiff(json1, json2, ignore_order=True, verbose_level=2)
returnJson = {k: v for k, v in diff.items() if
k in ('dictionary_item_added', 'dictionary_item_removed', 'values_changed')}
# just for print "values_changed" at first
returnJson = dict(reversed(returnJson.items()))
except Exception as e:
logger.warn(f"{msgError}: {e}")
return returnJson
# powered by:
# https://github.com/WASasquatch/was-node-suite-comfyui/blob/main/WAS_Node_Suite.py
# class: WAS_Samples_Passthrough_Stat_System
def get_system_stats():
import psutil
# RAM
ram = psutil.virtual_memory()
ram_used = ram.used / (1024 ** 3)
ram_total = ram.total / (1024 ** 3)
ram_stats = f"Used RAM: {ram_used:.2f} GB / Total RAM: {ram_total:.2f} GB"
# VRAM (with PyTorch)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
vram_used = torch.cuda.memory_allocated(device) / (1024 ** 3)
vram_total = torch.cuda.get_device_properties(device).total_memory / (1024 ** 3)
vram_stats = f"Used VRAM: {vram_used:.2f} GB / Total VRAM: {vram_total:.2f} GB"
# Hard Drive Space
hard_drive = psutil.disk_usage("/")
used_space = hard_drive.used / (1024 ** 3)
total_space = hard_drive.total / (1024 ** 3)
hard_drive_stats = f"Used Space: {used_space:.2f} GB / Total Space: {total_space:.2f} GB"
return [ram_stats, vram_stats, hard_drive_stats]
# return x and y resolution of an image (torch tensor)
def getResolutionByTensor(image=None) -> dict:
res = {"x": 0, "y": 0}
if image is not None:
img = image.movedim(-1, 1)
res["x"] = img.shape[3]
res["y"] = img.shape[2]
return res
# by https://stackoverflow.com/questions/6080477/how-to-get-the-size-of-tar-gz-in-mb-file-in-python
def get_size(path):
size = os.path.getsize(path)
if size < 1024:
return f"{size} bytes"
elif size < pow(1024, 2):
return f"{round(size / 1024, 2)} KB"
elif size < pow(1024, 3):
return f"{round(size / (pow(1024, 2)), 2)} MB"
elif size < pow(1024, 4):
return f"{round(size / (pow(1024, 3)), 2)} GB"
def get_nested_value(data, dotted_key, default=None):
keys = dotted_key.split('.')
for key in keys:
if isinstance(data, str):
data = json.loads(data)
if isinstance(data, dict) and key in data:
data = data[key]
else:
return default
return data

View File

@@ -0,0 +1,7 @@
import os
import logging
CONFIG = {
"loglevel": int(os.environ.get("CRYSTOOLS_LOGLEVEL", logging.INFO)),
"indent": int(os.environ.get("CRYSTOOLS_INDENT", 2))
}

View File

@@ -0,0 +1,29 @@
from enum import Enum
class TEXTS(Enum):
CUSTOM_NODE_NAME = "Crystools"
LOGGER_PREFIX = "Crystools"
CONCAT = "concatenated"
INACTIVE_MSG = "inactive"
INVALID_METADATA_MSG = "Invalid metadata raw"
FILE_NOT_FOUND = "File not found!"
class CATEGORY(Enum):
TESTING = "_for_testing"
MAIN = "crystools 🪛"
PRIMITIVE = "/Primitive"
DEBUGGER = "/Debugger"
LIST = "/List"
SWITCH = "/Switch"
PIPE = "/Pipe"
IMAGE = "/Image"
UTILS = "/Utils"
METADATA = "/Metadata"
# remember, all keys should be in lowercase!
class KEYS(Enum):
LIST = "list_string"
PREFIX = "prefix"

View File

@@ -0,0 +1,39 @@
# by https://github.com/Kosinkadink/ComfyUI-Advanced-ControlNet/blob/main/control/logger.py
import sys
import copy
import logging
from .keys import TEXTS
from .config import CONFIG
class ColoredFormatter(logging.Formatter):
COLORS = {
"DEBUG": "\033[0;36m", # CYAN
"INFO": "\033[0;32m", # GREEN
"WARNING": "\033[0;33m", # YELLOW
"ERROR": "\033[0;31m", # RED
"CRITICAL": "\033[0;37;41m", # WHITE ON RED
"RESET": "\033[0m", # RESET COLOR
}
def format(self, record):
colored_record = copy.copy(record)
levelname = colored_record.levelname
seq = self.COLORS.get(levelname, self.COLORS["RESET"])
colored_record.levelname = f"{seq}{levelname}{self.COLORS['RESET']}"
return super().format(colored_record)
# Create a new logger
logger = logging.getLogger(TEXTS.LOGGER_PREFIX.value)
logger.propagate = False
# Add handler if we don't have one.
if not logger.handlers:
handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(ColoredFormatter("[%(name)s %(levelname)s] %(message)s"))
logger.addHandler(handler)
# Configure logger
loglevel = CONFIG["loglevel"]
logger.setLevel(loglevel)

View File

@@ -0,0 +1,36 @@
import sys
FLOAT = ("FLOAT", {"default": 1,
"min": -sys.float_info.max,
"max": sys.float_info.max,
"step": 0.01})
BOOLEAN = ("BOOLEAN", {"default": True})
BOOLEAN_FALSE = ("BOOLEAN", {"default": False})
INT = ("INT", {"default": 1,
"min": -sys.maxsize,
"max": sys.maxsize,
"step": 1})
STRING = ("STRING", {"default": ""})
STRING_ML = ("STRING", {"multiline": True, "default": ""})
STRING_WIDGET = ("STRING", {"forceInput": True})
JSON_WIDGET = ("JSON", {"forceInput": True})
METADATA_RAW = ("METADATA_RAW", {"forceInput": True})
class AnyType(str):
"""A special class that is always equal in not equal comparisons. Credit to pythongosssss"""
def __eq__(self, _) -> bool:
return True
def __ne__(self, __value: object) -> bool:
return False
any = AnyType("*")

View File

@@ -0,0 +1 @@
version = "1.27.4"