diff --git a/README.md b/README.md index c442ee6..d362489 100644 --- a/README.md +++ b/README.md @@ -1 +1,36 @@ -# ComfyUI_DD_Nodes \ No newline at end of file +# ComfyUI DD Nodes + +Custom nodes for [ComfyUI](https://github.com/comfyanonymous/ComfyUI) by Digital Divas. + +## Nodes + +### Text Nodes +- **DD Text Concatenate** - Concatenates two text strings with an optional separator +- **DD Text Replace** - Replaces occurrences of a search string within text +- **DD Show Text** - Displays text output in the UI + +### Image Nodes +- **DD Image Resize** - Resizes an image to specified dimensions (nearest, bilinear, bicubic, area) +- **DD Image Batch** - Combines two images into a single batch +- **DD Get Image Size** - Returns the width and height of an image + +### Math Nodes +- **DD Integer Math** - Performs math operations on two integers (add, subtract, multiply, divide, modulo, power, min, max) +- **DD Float Math** - Performs math operations on two floats +- **DD Int to Float** - Converts an integer to a float +- **DD Float to Int** - Converts a float to an integer with rounding mode selection + +## Installation + +Clone this repository into your ComfyUI `custom_nodes` directory: + +```bash +cd ComfyUI/custom_nodes +git clone https://github.com/digital-divas-admin/ComfyUI_DD_Nodes.git +``` + +Restart ComfyUI to load the nodes. + +## License + +MIT License - see [LICENSE](LICENSE) for details. diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..339f200 --- /dev/null +++ b/__init__.py @@ -0,0 +1,21 @@ +"""ComfyUI DD Nodes - Custom nodes for ComfyUI by Digital Divas.""" + +from .nodes.text_nodes import ( + NODE_CLASS_MAPPINGS as TEXT_NODES, + NODE_DISPLAY_NAME_MAPPINGS as TEXT_NAMES, +) +from .nodes.image_nodes import ( + NODE_CLASS_MAPPINGS as IMAGE_NODES, + NODE_DISPLAY_NAME_MAPPINGS as IMAGE_NAMES, +) +from .nodes.math_nodes import ( + NODE_CLASS_MAPPINGS as MATH_NODES, + NODE_DISPLAY_NAME_MAPPINGS as MATH_NAMES, +) + +NODE_CLASS_MAPPINGS = {**TEXT_NODES, **IMAGE_NODES, **MATH_NODES} +NODE_DISPLAY_NAME_MAPPINGS = {**TEXT_NAMES, **IMAGE_NAMES, **MATH_NAMES} + +WEB_DIRECTORY = "./js" + +__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"] diff --git a/nodes/__init__.py b/nodes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/nodes/image_nodes.py b/nodes/image_nodes.py new file mode 100644 index 0000000..0541f7b --- /dev/null +++ b/nodes/image_nodes.py @@ -0,0 +1,99 @@ +import torch + + +class DD_ImageResize: + """Resizes an image to the specified dimensions.""" + + RESIZE_METHODS = ["nearest", "bilinear", "bicubic", "area"] + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "image": ("IMAGE",), + "width": ("INT", {"default": 512, "min": 1, "max": 8192, "step": 1}), + "height": ("INT", {"default": 512, "min": 1, "max": 8192, "step": 1}), + "method": (cls.RESIZE_METHODS, {"default": "bilinear"}), + }, + } + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("image",) + FUNCTION = "resize" + CATEGORY = "DD Nodes/Image" + + def resize(self, image, width, height, method): + # image shape: (batch, height, width, channels) + batch = image.shape[0] + # Convert to (batch, channels, height, width) for interpolation + img = image.permute(0, 3, 1, 2) + resized = torch.nn.functional.interpolate( + img, size=(height, width), mode=method + ) + # Convert back to (batch, height, width, channels) + resized = resized.permute(0, 2, 3, 1) + return (resized,) + + +class DD_ImageBatch: + """Combines two images into a single batch.""" + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "image_a": ("IMAGE",), + "image_b": ("IMAGE",), + }, + } + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("image",) + FUNCTION = "batch" + CATEGORY = "DD Nodes/Image" + + def batch(self, image_a, image_b): + if image_a.shape[1:] != image_b.shape[1:]: + # Resize image_b to match image_a dimensions + img_b = image_b.permute(0, 3, 1, 2) + img_b = torch.nn.functional.interpolate( + img_b, + size=(image_a.shape[1], image_a.shape[2]), + mode="bilinear", + ) + image_b = img_b.permute(0, 2, 3, 1) + return (torch.cat([image_a, image_b], dim=0),) + + +class DD_GetImageSize: + """Returns the width and height of an image.""" + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "image": ("IMAGE",), + }, + } + + RETURN_TYPES = ("INT", "INT") + RETURN_NAMES = ("width", "height") + FUNCTION = "get_size" + CATEGORY = "DD Nodes/Image" + + def get_size(self, image): + # image shape: (batch, height, width, channels) + return (image.shape[2], image.shape[1]) + + +NODE_CLASS_MAPPINGS = { + "DD_ImageResize": DD_ImageResize, + "DD_ImageBatch": DD_ImageBatch, + "DD_GetImageSize": DD_GetImageSize, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "DD_ImageResize": "DD Image Resize", + "DD_ImageBatch": "DD Image Batch", + "DD_GetImageSize": "DD Get Image Size", +} diff --git a/nodes/math_nodes.py b/nodes/math_nodes.py new file mode 100644 index 0000000..36e00c0 --- /dev/null +++ b/nodes/math_nodes.py @@ -0,0 +1,133 @@ +class DD_IntMath: + """Performs basic math operations on two integers.""" + + OPERATIONS = ["add", "subtract", "multiply", "divide", "modulo", "power", "min", "max"] + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "a": ("INT", {"default": 0, "min": -2**31, "max": 2**31 - 1}), + "b": ("INT", {"default": 0, "min": -2**31, "max": 2**31 - 1}), + "operation": (cls.OPERATIONS, {"default": "add"}), + }, + } + + RETURN_TYPES = ("INT", "FLOAT") + RETURN_NAMES = ("int_result", "float_result") + FUNCTION = "compute" + CATEGORY = "DD Nodes/Math" + + def compute(self, a, b, operation): + ops = { + "add": lambda: a + b, + "subtract": lambda: a - b, + "multiply": lambda: a * b, + "divide": lambda: a / b if b != 0 else 0, + "modulo": lambda: a % b if b != 0 else 0, + "power": lambda: a**b, + "min": lambda: min(a, b), + "max": lambda: max(a, b), + } + result = ops[operation]() + return (int(result), float(result)) + + +class DD_FloatMath: + """Performs basic math operations on two floats.""" + + OPERATIONS = ["add", "subtract", "multiply", "divide", "power", "min", "max"] + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "a": ("FLOAT", {"default": 0.0, "min": -1e10, "max": 1e10, "step": 0.01}), + "b": ("FLOAT", {"default": 0.0, "min": -1e10, "max": 1e10, "step": 0.01}), + "operation": (cls.OPERATIONS, {"default": "add"}), + }, + } + + RETURN_TYPES = ("FLOAT", "INT") + RETURN_NAMES = ("float_result", "int_result") + FUNCTION = "compute" + CATEGORY = "DD Nodes/Math" + + def compute(self, a, b, operation): + ops = { + "add": lambda: a + b, + "subtract": lambda: a - b, + "multiply": lambda: a * b, + "divide": lambda: a / b if b != 0 else 0.0, + "power": lambda: a**b, + "min": lambda: min(a, b), + "max": lambda: max(a, b), + } + result = ops[operation]() + return (float(result), int(result)) + + +class DD_IntToFloat: + """Converts an integer to a float.""" + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "value": ("INT", {"default": 0, "min": -2**31, "max": 2**31 - 1}), + }, + } + + RETURN_TYPES = ("FLOAT",) + RETURN_NAMES = ("float_value",) + FUNCTION = "convert" + CATEGORY = "DD Nodes/Math" + + def convert(self, value): + return (float(value),) + + +class DD_FloatToInt: + """Converts a float to an integer.""" + + ROUND_MODES = ["round", "floor", "ceil", "truncate"] + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "value": ("FLOAT", {"default": 0.0, "min": -1e10, "max": 1e10, "step": 0.01}), + "mode": (cls.ROUND_MODES, {"default": "round"}), + }, + } + + RETURN_TYPES = ("INT",) + RETURN_NAMES = ("int_value",) + FUNCTION = "convert" + CATEGORY = "DD Nodes/Math" + + def convert(self, value, mode): + import math + + modes = { + "round": lambda: round(value), + "floor": lambda: math.floor(value), + "ceil": lambda: math.ceil(value), + "truncate": lambda: int(value), + } + return (modes[mode](),) + + +NODE_CLASS_MAPPINGS = { + "DD_IntMath": DD_IntMath, + "DD_FloatMath": DD_FloatMath, + "DD_IntToFloat": DD_IntToFloat, + "DD_FloatToInt": DD_FloatToInt, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "DD_IntMath": "DD Integer Math", + "DD_FloatMath": "DD Float Math", + "DD_IntToFloat": "DD Int to Float", + "DD_FloatToInt": "DD Float to Int", +} diff --git a/nodes/text_nodes.py b/nodes/text_nodes.py new file mode 100644 index 0000000..f1ccf4b --- /dev/null +++ b/nodes/text_nodes.py @@ -0,0 +1,84 @@ +class DD_TextConcatenate: + """Concatenates two text strings with an optional separator.""" + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "text_a": ("STRING", {"default": "", "multiline": True}), + "text_b": ("STRING", {"default": "", "multiline": True}), + }, + "optional": { + "separator": ("STRING", {"default": ", "}), + }, + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("text",) + FUNCTION = "concatenate" + CATEGORY = "DD Nodes/Text" + + def concatenate(self, text_a, text_b, separator=", "): + if not text_a and not text_b: + return ("",) + if not text_a: + return (text_b,) + if not text_b: + return (text_a,) + return (f"{text_a}{separator}{text_b}",) + + +class DD_TextReplace: + """Replaces occurrences of a search string within text.""" + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "text": ("STRING", {"default": "", "multiline": True}), + "find": ("STRING", {"default": ""}), + "replace": ("STRING", {"default": ""}), + }, + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("text",) + FUNCTION = "replace" + CATEGORY = "DD Nodes/Text" + + def replace(self, text, find, replace): + return (text.replace(find, replace),) + + +class DD_ShowText: + """Displays text output in the UI.""" + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "text": ("STRING", {"forceInput": True}), + }, + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("text",) + FUNCTION = "show" + CATEGORY = "DD Nodes/Text" + OUTPUT_NODE = True + + def show(self, text): + return {"ui": {"text": [text]}, "result": (text,)} + + +NODE_CLASS_MAPPINGS = { + "DD_TextConcatenate": DD_TextConcatenate, + "DD_TextReplace": DD_TextReplace, + "DD_ShowText": DD_ShowText, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "DD_TextConcatenate": "DD Text Concatenate", + "DD_TextReplace": "DD Text Replace", + "DD_ShowText": "DD Show Text", +}