0.0.1
This commit is contained in:
ae(seth)tics
2025-08-27 12:25:41 -07:00
parent 98fbfb2c48
commit c2f5b13db0
12 changed files with 783 additions and 1 deletions
+71 -1
View File
@@ -1,2 +1,72 @@
# ComfyUI-Utilitools
ComfyUI Custom Nodes: Utilities and Tools for improving user experience.
A collection of utility nodes for ComfyUI that I find handy. Nothing fancy, just stuff that makes workflows a bit easier when you need simple operations.
## What's in here?
These aren't the most sophisticated nodes out there, but they get the job done for basic operations:
### 🧮 Math Nodes
- **Add, Subtract, Multiply, Divide** - Basic math with both int and float outputs
- **Calculator** - Safely evaluate simple math expressions like "2 + 2 * 3"
### 📝 Text Utilities
- **Text Concatenation** - Join two strings with optional separator
- **String Replace** - Find and replace text with optional count limit
### 🔄 Type Conversions
- **Float to Int / Int to Float** - Convert between number types
- **Whatever To String** - Convert any value (int, float, string) to string
### 🤔 Logic & Control
- **If Then Else** - Branch workflows based on boolean conditions
- **Switch** - Select from up to 8 inputs by index
- **Boolean Gates** - AND, OR, NOT operations
### 📋 Data Manipulation
- **Create List** - Make lists from up to 8 items
- **List Index** - Get item from list by index
- **Batch Controller** - Adjust tensor batch sizes
### ⚙️ Workflow Helpers
- **Passthrough** - For workflow organization (does nothing, just passes data through)
- **Counter** - Increment/decrement with optional reset
- **Constants** - Int, Float, and String constant value nodes
### 🖼️ Image Utilities
- **Image Dimensions** - Get width, height, and batch size from images
- **Aspect Ratio** - Calculate ratio as decimal and simplified fraction (like "16:9")
## Installation
Just drop this folder into your ComfyUI `custom_nodes` directory and restart ComfyUI. That's it.
```
ComfyUI/custom_nodes/ComfyUI-Utilitools/
```
## Notes
- All nodes are organized under "Utilitools" categories in the node browser
- Most math operations work with both int and float inputs
- The calculator node only accepts basic math expressions for safety
- These are utility nodes I made for myself - they're useful but not groundbreaking
## Structure
The code is organized by category for easier maintenance:
```
nodes/
├── math.py # Arithmetic operations
├── text.py # Text processing
├── conversion.py # Type conversions
├── logic.py # Conditional logic
├── data.py # Lists and batching
├── workflow.py # Workflow helpers
├── constants.py # Constant values
├── image.py # Image utilities
└── main.py # Central registry
```
Feel free to use, modify, or ignore as needed!
+3
View File
@@ -0,0 +1,3 @@
from .nodes.main import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']
+12
View File
@@ -0,0 +1,12 @@
"""
ComfyUI Utilitools Nodes
A collection of utility nodes for ComfyUI to improve workflow efficiency.
"""
from .main import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
__version__ = "1.0.0"
__author__ = "ComfyUI-Utilitools"
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
+65
View File
@@ -0,0 +1,65 @@
class UtilConstantInt:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"value": ("INT", {"default": 0, "min": -999999, "max": 999999, "step": 1}),
}
}
RETURN_TYPES = ("INT",)
RETURN_NAMES = ("value",)
FUNCTION = "get_value"
CATEGORY = "Utilitools/Constants"
def get_value(self, value):
return (value,)
class UtilConstantFloat:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"value": ("FLOAT", {"default": 0.0, "min": -999999.0, "max": 999999.0, "step": 0.001}),
}
}
RETURN_TYPES = ("FLOAT",)
RETURN_NAMES = ("value",)
FUNCTION = "get_value"
CATEGORY = "Utilitools/Constants"
def get_value(self, value):
return (value,)
class UtilConstantString:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"value": ("STRING", {"default": "", "multiline": True}),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("value",)
FUNCTION = "get_value"
CATEGORY = "Utilitools/Constants"
def get_value(self, value):
return (value,)
NODE_CLASS_MAPPINGS = {
"UtilConstantInt": UtilConstantInt,
"UtilConstantFloat": UtilConstantFloat,
"UtilConstantString": UtilConstantString,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"UtilConstantInt": "Constant Int",
"UtilConstantFloat": "Constant Float",
"UtilConstantString": "Constant String",
}
+75
View File
@@ -0,0 +1,75 @@
class UtilFloatToInt:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"value": ("FLOAT", {"default": 0.0}),
}
}
RETURN_TYPES = ("INT",)
RETURN_NAMES = ("int_value",)
FUNCTION = "convert"
CATEGORY = "Utilitools/Conversion"
def convert(self, value):
return (int(value),)
class UtilIntToFloat:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"value": ("INT", {"default": 0}),
}
}
RETURN_TYPES = ("FLOAT",)
RETURN_NAMES = ("float_value",)
FUNCTION = "convert"
CATEGORY = "Utilitools/Conversion"
def convert(self, value):
return (float(value),)
class UtilWhateverToString:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {},
"optional": {
"int_value": ("INT", {"default": 0}),
"float_value": ("FLOAT", {"default": 0.0}),
"string_value": ("STRING", {"default": ""}),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("string_result",)
FUNCTION = "convert"
CATEGORY = "Utilitools/Conversion"
def convert(self, int_value=None, float_value=None, string_value=None):
if int_value is not None:
return (str(int_value),)
elif float_value is not None:
return (str(float_value),)
elif string_value is not None:
return (str(string_value),)
else:
return ("",)
NODE_CLASS_MAPPINGS = {
"UtilFloatToInt": UtilFloatToInt,
"UtilIntToFloat": UtilIntToFloat,
"UtilWhateverToString": UtilWhateverToString,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"UtilFloatToInt": "Float to Int",
"UtilIntToFloat": "Int to Float",
"UtilWhateverToString": "Whatever To String",
}
+91
View File
@@ -0,0 +1,91 @@
class UtilListCreate:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {},
"optional": {
"item_0": ("*",),
"item_1": ("*",),
"item_2": ("*",),
"item_3": ("*",),
"item_4": ("*",),
"item_5": ("*",),
"item_6": ("*",),
"item_7": ("*",),
}
}
RETURN_TYPES = ("LIST",)
RETURN_NAMES = ("list",)
FUNCTION = "create_list"
CATEGORY = "Utilitools/Data"
def create_list(self, **kwargs):
items = []
for i in range(8):
key = f"item_{i}"
if key in kwargs and kwargs[key] is not None:
items.append(kwargs[key])
return (items,)
class UtilListIndex:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"list": ("LIST",),
"index": ("INT", {"default": 0, "min": 0, "max": 999, "step": 1}),
}
}
RETURN_TYPES = ("*",)
RETURN_NAMES = ("item",)
FUNCTION = "get_item"
CATEGORY = "Utilitools/Data"
def get_item(self, list, index):
if index < len(list):
return (list[index],)
return (None,)
class UtilBatchController:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"input": ("*",),
"batch_size": ("INT", {"default": 1, "min": 1, "max": 64, "step": 1}),
}
}
RETURN_TYPES = ("*",)
RETURN_NAMES = ("output",)
FUNCTION = "control_batch"
CATEGORY = "Utilitools/Data"
def control_batch(self, input, batch_size):
if hasattr(input, 'shape') and len(input.shape) > 0:
if input.shape[0] != batch_size:
if input.shape[0] > batch_size:
return (input[:batch_size],)
else:
import torch
repeats = batch_size // input.shape[0] + (1 if batch_size % input.shape[0] > 0 else 0)
repeated = input.repeat(repeats, *([1] * (len(input.shape) - 1)))
return (repeated[:batch_size],)
return (input,)
NODE_CLASS_MAPPINGS = {
"UtilListCreate": UtilListCreate,
"UtilListIndex": UtilListIndex,
"UtilBatchController": UtilBatchController,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"UtilListCreate": "Create List",
"UtilListIndex": "List Index",
"UtilBatchController": "Batch Controller",
}
+67
View File
@@ -0,0 +1,67 @@
class UtilImageDimensions:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
}
}
RETURN_TYPES = ("INT", "INT", "INT")
RETURN_NAMES = ("width", "height", "batch_size")
FUNCTION = "get_dimensions"
CATEGORY = "Utilitools/Image"
def get_dimensions(self, image):
if len(image.shape) == 4: # [batch, height, width, channels]
batch_size, height, width, channels = image.shape
return (width, height, batch_size)
elif len(image.shape) == 3: # [height, width, channels]
height, width, channels = image.shape
return (width, height, 1)
else:
return (0, 0, 0)
class UtilAspectRatio:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"width": ("INT", {"default": 512, "min": 1, "max": 8192, "step": 1}),
"height": ("INT", {"default": 512, "min": 1, "max": 8192, "step": 1}),
}
}
RETURN_TYPES = ("FLOAT", "STRING")
RETURN_NAMES = ("ratio", "ratio_string")
FUNCTION = "calculate_ratio"
CATEGORY = "Utilitools/Image"
def calculate_ratio(self, width, height):
import math
def gcd(a, b):
while b:
a, b = b, a % b
return a
ratio = width / height
divisor = gcd(width, height)
simplified_width = width // divisor
simplified_height = height // divisor
ratio_string = f"{simplified_width}:{simplified_height}"
return (ratio, ratio_string)
NODE_CLASS_MAPPINGS = {
"UtilImageDimensions": UtilImageDimensions,
"UtilAspectRatio": UtilAspectRatio,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"UtilImageDimensions": "Image Dimensions",
"UtilAspectRatio": "Aspect Ratio",
}
+122
View File
@@ -0,0 +1,122 @@
class UtilIfThenElse:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"condition": ("BOOLEAN", {"default": True}),
"if_true": ("*",),
"if_false": ("*",),
}
}
RETURN_TYPES = ("*",)
RETURN_NAMES = ("result",)
FUNCTION = "execute"
CATEGORY = "Utilitools/Logic"
def execute(self, condition, if_true, if_false):
return (if_true if condition else if_false,)
class UtilSwitch:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"index": ("INT", {"default": 0, "min": 0, "max": 7, "step": 1}),
},
"optional": {
"input_0": ("*",),
"input_1": ("*",),
"input_2": ("*",),
"input_3": ("*",),
"input_4": ("*",),
"input_5": ("*",),
"input_6": ("*",),
"input_7": ("*",),
}
}
RETURN_TYPES = ("*",)
RETURN_NAMES = ("selected",)
FUNCTION = "switch"
CATEGORY = "Utilitools/Logic"
def switch(self, index, **kwargs):
input_key = f"input_{index}"
if input_key in kwargs and kwargs[input_key] is not None:
return (kwargs[input_key],)
return (None,)
class UtilBooleanAND:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"a": ("BOOLEAN", {"default": True}),
"b": ("BOOLEAN", {"default": True}),
}
}
RETURN_TYPES = ("BOOLEAN",)
RETURN_NAMES = ("result",)
FUNCTION = "execute"
CATEGORY = "Utilitools/Logic"
def execute(self, a, b):
return (a and b,)
class UtilBooleanOR:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"a": ("BOOLEAN", {"default": False}),
"b": ("BOOLEAN", {"default": False}),
}
}
RETURN_TYPES = ("BOOLEAN",)
RETURN_NAMES = ("result",)
FUNCTION = "execute"
CATEGORY = "Utilitools/Logic"
def execute(self, a, b):
return (a or b,)
class UtilBooleanNOT:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"value": ("BOOLEAN", {"default": True}),
}
}
RETURN_TYPES = ("BOOLEAN",)
RETURN_NAMES = ("result",)
FUNCTION = "execute"
CATEGORY = "Utilitools/Logic"
def execute(self, value):
return (not value,)
NODE_CLASS_MAPPINGS = {
"UtilIfThenElse": UtilIfThenElse,
"UtilSwitch": UtilSwitch,
"UtilBooleanAND": UtilBooleanAND,
"UtilBooleanOR": UtilBooleanOR,
"UtilBooleanNOT": UtilBooleanNOT,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"UtilIfThenElse": "If Then Else",
"UtilSwitch": "Switch",
"UtilBooleanAND": "Boolean AND",
"UtilBooleanOR": "Boolean OR",
"UtilBooleanNOT": "Boolean NOT",
}
+36
View File
@@ -0,0 +1,36 @@
"""
ComfyUI Utilitools - Main Node Registry
Centralized registration of all utility nodes organized by category.
"""
from .math import NODE_CLASS_MAPPINGS as MATH_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as MATH_DISPLAY_MAPPINGS
from .text import NODE_CLASS_MAPPINGS as TEXT_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as TEXT_DISPLAY_MAPPINGS
from .conversion import NODE_CLASS_MAPPINGS as CONVERSION_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as CONVERSION_DISPLAY_MAPPINGS
from .logic import NODE_CLASS_MAPPINGS as LOGIC_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as LOGIC_DISPLAY_MAPPINGS
from .data import NODE_CLASS_MAPPINGS as DATA_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as DATA_DISPLAY_MAPPINGS
from .workflow import NODE_CLASS_MAPPINGS as WORKFLOW_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as WORKFLOW_DISPLAY_MAPPINGS
from .constants import NODE_CLASS_MAPPINGS as CONSTANTS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as CONSTANTS_DISPLAY_MAPPINGS
from .image import NODE_CLASS_MAPPINGS as IMAGE_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as IMAGE_DISPLAY_MAPPINGS
NODE_CLASS_MAPPINGS = {
**MATH_MAPPINGS,
**TEXT_MAPPINGS,
**CONVERSION_MAPPINGS,
**LOGIC_MAPPINGS,
**DATA_MAPPINGS,
**WORKFLOW_MAPPINGS,
**CONSTANTS_MAPPINGS,
**IMAGE_MAPPINGS,
}
NODE_DISPLAY_NAME_MAPPINGS = {
**MATH_DISPLAY_MAPPINGS,
**TEXT_DISPLAY_MAPPINGS,
**CONVERSION_DISPLAY_MAPPINGS,
**LOGIC_DISPLAY_MAPPINGS,
**DATA_DISPLAY_MAPPINGS,
**WORKFLOW_DISPLAY_MAPPINGS,
**CONSTANTS_DISPLAY_MAPPINGS,
**IMAGE_DISPLAY_MAPPINGS,
}
+130
View File
@@ -0,0 +1,130 @@
"""
Mathematical utility nodes for ComfyUI.
Provides basic arithmetic operations with both integer and float outputs.
"""
class UtilAdd:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"a": ("FLOAT", {"default": 0.0, "min": -999999.0, "max": 999999.0, "step": 0.001}),
"b": ("FLOAT", {"default": 0.0, "min": -999999.0, "max": 999999.0, "step": 0.001}),
}
}
RETURN_TYPES = ("INT", "FLOAT")
RETURN_NAMES = ("int_result", "float_result")
FUNCTION = "add"
CATEGORY = "Utilitools/Math"
def add(self, a, b):
result = a + b
return (int(result), float(result))
class UtilSubtract:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"a": ("FLOAT", {"default": 0.0, "min": -999999.0, "max": 999999.0, "step": 0.001}),
"b": ("FLOAT", {"default": 0.0, "min": -999999.0, "max": 999999.0, "step": 0.001}),
}
}
RETURN_TYPES = ("INT", "FLOAT")
RETURN_NAMES = ("int_result", "float_result")
FUNCTION = "subtract"
CATEGORY = "Utilitools/Math"
def subtract(self, a, b):
result = a - b
return (int(result), float(result))
class UtilMultiply:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"a": ("FLOAT", {"default": 1.0, "min": -999999.0, "max": 999999.0, "step": 0.001}),
"b": ("FLOAT", {"default": 1.0, "min": -999999.0, "max": 999999.0, "step": 0.001}),
}
}
RETURN_TYPES = ("INT", "FLOAT")
RETURN_NAMES = ("int_result", "float_result")
FUNCTION = "multiply"
CATEGORY = "Utilitools/Math"
def multiply(self, a, b):
result = a * b
return (int(result), float(result))
class UtilDivide:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"a": ("FLOAT", {"default": 1.0, "min": -999999.0, "max": 999999.0, "step": 0.001}),
"b": ("FLOAT", {"default": 1.0, "min": -999999.0, "max": 999999.0, "step": 0.001}),
}
}
RETURN_TYPES = ("INT", "FLOAT")
RETURN_NAMES = ("int_result", "float_result")
FUNCTION = "divide"
CATEGORY = "Utilitools/Math"
def divide(self, a, b):
if b == 0:
return (0, 0.0)
result = a / b
return (int(result), float(result))
class UtilCalculator:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"expression": ("STRING", {"default": "2 + 2"}),
}
}
RETURN_TYPES = ("FLOAT", "INT")
RETURN_NAMES = ("float_result", "int_result")
FUNCTION = "calculate"
CATEGORY = "Utilitools/Math"
def calculate(self, expression):
try:
import re
allowed_chars = re.compile(r'^[0-9+\-*/().\s]+$')
if not allowed_chars.match(expression):
return (0.0, 0)
result = eval(expression)
return (float(result), int(result))
except:
return (0.0, 0)
NODE_CLASS_MAPPINGS = {
"UtilAdd": UtilAdd,
"UtilSubtract": UtilSubtract,
"UtilMultiply": UtilMultiply,
"UtilDivide": UtilDivide,
"UtilCalculator": UtilCalculator,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"UtilAdd": "Add",
"UtilSubtract": "Subtract",
"UtilMultiply": "Multiply",
"UtilDivide": "Divide",
"UtilCalculator": "Calculator",
}
+57
View File
@@ -0,0 +1,57 @@
class UtilTextConcat:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"text_a": ("STRING", {"default": ""}),
"text_b": ("STRING", {"default": ""}),
},
"optional": {
"separator": ("STRING", {"default": ""}),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("result",)
FUNCTION = "concat"
CATEGORY = "Utilitools/Text"
def concat(self, text_a, text_b, separator=""):
return (text_a + separator + text_b,)
class UtilStringReplace:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"text": ("STRING", {"default": ""}),
"find": ("STRING", {"default": ""}),
"replace": ("STRING", {"default": ""}),
},
"optional": {
"count": ("INT", {"default": -1, "min": -1, "max": 1000, "step": 1}),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("result",)
FUNCTION = "replace"
CATEGORY = "Utilitools/Text"
def replace(self, text, find, replace, count=-1):
if count == -1:
return (text.replace(find, replace),)
else:
return (text.replace(find, replace, count),)
NODE_CLASS_MAPPINGS = {
"UtilTextConcat": UtilTextConcat,
"UtilStringReplace": UtilStringReplace,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"UtilTextConcat": "Text Concatenation",
"UtilStringReplace": "String Replace",
}
+54
View File
@@ -0,0 +1,54 @@
class UtilPassthrough:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"input": ("*",),
}
}
RETURN_TYPES = ("*",)
RETURN_NAMES = ("output",)
FUNCTION = "passthrough"
CATEGORY = "Utilitools/Workflow"
def passthrough(self, input):
return (input,)
class UtilCounter:
def __init__(self):
self.counter = 0
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"increment": ("INT", {"default": 1, "min": -999, "max": 999, "step": 1}),
},
"optional": {
"reset": ("BOOLEAN", {"default": False}),
}
}
RETURN_TYPES = ("INT",)
RETURN_NAMES = ("count",)
FUNCTION = "count"
CATEGORY = "Utilitools/Workflow"
def count(self, increment, reset=False):
if reset:
self.counter = 0
self.counter += increment
return (self.counter,)
NODE_CLASS_MAPPINGS = {
"UtilPassthrough": UtilPassthrough,
"UtilCounter": UtilCounter,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"UtilPassthrough": "Passthrough",
"UtilCounter": "Counter",
}