diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..8b32e7b --- /dev/null +++ b/.gitattributes @@ -0,0 +1,15 @@ +# Declare files that always have LF line endings on checkout +* text eol=lf + +# Denote all files that are truly binary and should not be modified +*.bin binary +*.heif binary +*.heic binary +*.hif binary +*.avif binary +*.png binary +*.gif binary +*.webp binary +*.tiff binary +*.jpeg binary +*.jpg binary diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..e84f372 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,32 @@ +exclude: ^(screenshots)/ +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: check-yaml + - id: check-toml + - id: end-of-file-fixer + - id: trailing-whitespace + - id: mixed-line-ending + +- repo: https://github.com/PyCQA/isort + rev: 5.13.2 + hooks: + - id: isort + files: . + +- repo: https://github.com/psf/black + rev: 24.10.0 + hooks: + - id: black + files: . + +- repo: https://github.com/tox-dev/pyproject-fmt + rev: 2.3.1 + hooks: + - id: pyproject-fmt + +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.6.9 + hooks: + - id: ruff diff --git a/README.md b/README.md index a52d549..4fe2e68 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,13 @@ Current `Visionatrix/Text` nodes list: - **VixMultilineText** - node to just hold text(code compatible). - **VixTextConcatenate** - node to concatenate up to 4 different strings with optional delimiter. +- **VixTextReplace** - find and replace substring in text. +- **VixDictionaryConvert** - node to create dictionary from text. +- **VixDictionaryGet** - node to get value by key from dictionary. + +Current `Visionatrix/Image` nodes list: + +- **VixImageFilters** - applies brightness, saturation, sharpness and other simple Pillow filters to an image. ### Incorporated nodes diff --git a/__init__.py b/__init__.py index b777fc9..39a8c6b 100644 --- a/__init__.py +++ b/__init__.py @@ -1,505 +1,3 @@ -import json +from .nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS -from .style_aligned import StyleAlignedBatchAlign - - -class AnyType(str): - def __ne__(self, __value: object) -> bool: - return False - - -any_typ = AnyType("*") - - -class VixUiAspectRatioSelector: - @classmethod - def INPUT_TYPES(s): - return { - "required": { - "aspect_ratio": ( - [ - "1:1 (1024x1024)", - "2:3 (832x1216)", - "3:4 (896x1152)", - "5:8 (768x1216)", - "9:16 (768x1344)", - "9:19 (704x1472)", - "9:21 (640x1536)", - "3:2 (1216x832)", - "4:3 (1152x896)", - "8:5 (1216x768)", - "16:9 (1344x768)", - "19:9 (1472x704)", - "21:9 (1536x640)", - ], - ), - "display_name": ("STRING", {"default": "Aspect Ratio"}), - "optional": ("BOOLEAN", {"default": True}), - "advanced": ("BOOLEAN", {"default": True}), - "order": ("INT", {"default": 20}), - "custom_id": ("STRING", {"default": "aspect_ratio"}), - }, - "optional": { - "hidden": ("BOOLEAN", {"default": False}), - }, - } - - RETURN_TYPES = ("STRING", "INT", "INT") - RETURN_NAMES = ("ratio", "width", "height") - FUNCTION = "do_it" - CATEGORY = "Visionatrix/UI" - - def do_it(self, aspect_ratio, **kwargs): - ratio, dims = aspect_ratio.split(" (") - dims = dims[:-1] # Remove the closing parenthesis - width, height = map(int, dims.split("x")) - return ratio, width, height - - -class VixUiCheckbox: - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "state": ("BOOLEAN", {"default": False}), - "display_name": ("STRING", {"default": "Display Name"}), - "optional": ("BOOLEAN", {"default": True}), - "advanced": ("BOOLEAN", {"default": True}), - "order": ("INT", {"default": 99}), - "custom_id": ("STRING", {"default": ""}), - }, - "optional": { - "hidden": ("BOOLEAN", {"default": False}), - }, - } - - RETURN_TYPES = ("BOOLEAN", "INT") - RETURN_NAMES = ("bool", "int") - CATEGORY = "Visionatrix/UI" - FUNCTION = "do_it" - - @classmethod - def do_it(cls, state, **kwargs) -> tuple: - return state, int(state) - - -class VixUiRangeFloat: - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "value": ("FLOAT", {"default": 4.0, "step": 0.01, "round": False}), - "display_name": ("STRING", {"default": "Display Range"}), - "optional": ("BOOLEAN", {"default": True}), - "advanced": ("BOOLEAN", {"default": True}), - "min": ("FLOAT", {"default": 1.0, "step": 0.01, "round": False}), - "max": ("FLOAT", {"default": 9.0, "step": 0.01, "round": False}), - "step": ("FLOAT", {"default": 0.1, "step": 0.01, "round": False}), - "order": ("INT", {"default": 99}), - "custom_id": ("STRING", {"default": ""}), - }, - "optional": { - "hidden": ("BOOLEAN", {"default": False}), - }, - } - - FUNCTION = "do_it" - CATEGORY = "Visionatrix/UI" - RETURN_TYPES = ("FLOAT",) - - @classmethod - def do_it(cls, value, **kwargs) -> tuple: - return (value,) - - -class VixUiRangeScaleFloat: - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "value": ("FLOAT", {"default": 4.0, "step": 0.01, "round": False}), - "display_name": ("STRING", {"default": "Image Size Factor"}), - "optional": ("BOOLEAN", {"default": True}), - "advanced": ("BOOLEAN", {"default": True}), - "source_input_name": ("STRING", {"default": ""}), - "min": ("FLOAT", {"default": 1.0, "step": 0.01, "round": False}), - "max": ("FLOAT", {"default": 9.0, "step": 0.01, "round": False}), - "step": ("FLOAT", {"default": 0.1, "step": 0.01, "round": False}), - "order": ("INT", {"default": 99}), - "custom_id": ("STRING", {"default": ""}), - }, - "optional": { - "hidden": ("BOOLEAN", {"default": False}), - }, - } - - FUNCTION = "do_it" - CATEGORY = "Visionatrix/UI" - RETURN_TYPES = ("FLOAT",) - - @classmethod - def do_it(cls, value, **kwargs) -> tuple: - return (value,) - - -class VixUiRangeInt: - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "value": ("INT", {"default": 10}), - "display_name": ("STRING", {"default": "Display Range"}), - "optional": ("BOOLEAN", {"default": True}), - "advanced": ("BOOLEAN", {"default": True}), - "min": ("INT", {"default": 1}), - "max": ("INT", {"default": 20}), - "step": ("INT", {"default": 1}), - "order": ("INT", {"default": 99}), - "custom_id": ("STRING", {"default": ""}), - }, - "optional": { - "hidden": ("BOOLEAN", {"default": False}), - }, - } - - FUNCTION = "do_it" - CATEGORY = "Visionatrix/UI" - RETURN_TYPES = ("INT",) - - @classmethod - def do_it(cls, value, **kwargs) -> tuple: - return (value,) - - -class VixUiList: - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "default_value": ("STRING", {}), - "possible_values": ("STRING", {"default": "[]", "multiline": True}), - "display_name": ("STRING", {"default": "Dropdown list"}), - "optional": ("BOOLEAN", {"default": True}), - "advanced": ("BOOLEAN", {"default": True}), - "order": ("INT", {"default": 99}), - "custom_id": ("STRING", {"default": ""}), - }, - "optional": { - "hidden": ("BOOLEAN", {"default": False}), - }, - } - RETURN_TYPES = (any_typ,) - FUNCTION = "do_it" - CATEGORY = "Visionatrix/UI" - - @classmethod - def do_it(cls, default_value, **kwargs) -> tuple: - possible_values = json.loads(kwargs["possible_values"]) - if isinstance(possible_values, dict) and default_value in possible_values: - return (possible_values[default_value],) - return (default_value,) - - -class VixUiPrompt: - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "text": ("STRING", {"default": "", "multiline": True, "dynamicPrompts": True}), - "display_name": ("STRING", {"default": "Prompt"}), - "optional": ("BOOLEAN", {"default": False}), - "advanced": ("BOOLEAN", {"default": False}), - "order": ("INT", {"default": 10}), - "custom_id": ("STRING", {"default": ""}), - }, - "optional": { - "hidden": ("BOOLEAN", {"default": False}), - "translatable": ("BOOLEAN", {"default": False}), - }, - } - RETURN_TYPES = ("STRING",) - FUNCTION = "do_it" - CATEGORY = "Visionatrix/UI" - - @classmethod - def do_it(cls, text, **kwargs) -> tuple: - return (text,) - - -class VixUiCheckboxLogic: - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "state": ("BOOLEAN", {"default": False}), - "display_name": ("STRING", {"default": "Display Name"}), - "optional": ("BOOLEAN", {"default": True}), - "advanced": ("BOOLEAN", {"default": True}), - "order": ("INT", {"default": 99}), - "custom_id": ("STRING", {"default": ""}), - }, - "optional": { - "input_off_state": (any_typ, {"lazy": True}), - "input_on_state": (any_typ, {"lazy": True}), - "hidden": ("BOOLEAN", {"default": False}), - }, - } - - RETURN_TYPES = (any_typ,) - RETURN_NAMES = ("output_to",) - CATEGORY = "Visionatrix/UI" - FUNCTION = "do_it" - - @classmethod - def do_it(cls, state, **kwargs) -> tuple: - if state is False: - return (kwargs.get("input_off_state", None),) - return (kwargs.get("input_on_state", None),) - - @staticmethod - def check_lazy_status(state, **kwargs): - if state is False: - return ["input_off_state"] - return ["input_on_state"] - - -class VixUiListLogic: - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "default_value": ("STRING", {}), - "possible_values": ("STRING", {"default": "[]", "multiline": True}), - "display_name": ("STRING", {"default": "Display Name"}), - "optional": ("BOOLEAN", {"default": True}), - "advanced": ("BOOLEAN", {"default": True}), - "order": ("INT", {"default": 99}), - "custom_id": ("STRING", {"default": ""}), - }, - "optional": { - "input_first": (any_typ, {"lazy": True}), - "input_second": (any_typ, {"lazy": True}), - "input_third": (any_typ, {"lazy": True}), - "input_fourth": (any_typ, {"lazy": True}), - "input_fifth": (any_typ, {"lazy": True}), - "input_sixth": (any_typ, {"lazy": True}), - "hidden": ("BOOLEAN", {"default": False}), - }, - } - - RETURN_TYPES = (any_typ,) - RETURN_NAMES = ("output_to",) - CATEGORY = "Visionatrix/UI" - FUNCTION = "do_it" - - @classmethod - def do_it(cls, default_value, **kwargs) -> tuple: - list_with_values: list = json.loads(kwargs["possible_values"]) - index_to_return = list_with_values.index(default_value) - if index_to_return == 0: - return (kwargs["input_first"],) - if index_to_return == 1: - return (kwargs["input_second"],) - if index_to_return == 2: - return (kwargs["input_third"],) - if index_to_return == 3: - return (kwargs["input_fourth"],) - if index_to_return == 4: - return (kwargs["input_fifth"],) - if index_to_return == 5: - return (kwargs["input_sixth"],) - raise RuntimeError("Workflow logic error") - - @staticmethod - def check_lazy_status(default_value, **kwargs): - list_with_values: list = json.loads(kwargs["possible_values"]) - index_to_return = list_with_values.index(default_value) - if index_to_return == 0: - return ["input_first"] - if index_to_return == 1: - return ["input_second"] - if index_to_return == 2: - return ["input_third"] - if index_to_return == 3: - return ["input_fourth"] - if index_to_return == 4: - return ["input_fifth"] - if index_to_return == 5: - return ["input_sixth"] - raise RuntimeError("Workflow logic error") - - -class VixUiWorkflowMetadata: - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "name": ("STRING", {}), - "display_name": ("STRING", {}), - "description": ("STRING", {"default": ""}), - "author": ("STRING", {}), - "homepage": ("STRING", {"default": ""}), - "documentation": ("STRING", {"default": ""}), - "license": ("STRING", {"default": ""}), - "tags": ("STRING", {"default": "[\"general\"]", "multiline": True}), - "version": ("STRING", {"default": "1.0.0"}) - }, - "optional": { - "requires": ("STRING", {"default": "[]", "multiline": True}), - "is_seed_supported": ("BOOLEAN", {"default": True}), - "is_count_supported": ("BOOLEAN", {"default": True}), - "is_translations_supported": ("BOOLEAN", {"default": False}), - "is_macos_supported": ("BOOLEAN", {"default": True}), - "required_memory_gb": ("FLOAT", {"default": 0.0, "step": 0.1, "round": False}), - "hidden": ("BOOLEAN", {"default": False}), - "remote_vae": ("BOOLEAN", {"default": False}), - } - } - RETURN_TYPES = ("STRING",) - FUNCTION = "do_it" - CATEGORY = "Visionatrix/UI" - - @classmethod - def do_it(cls, text, **kwargs) -> tuple: - return (text,) - - -class VixDynamicLoraDefinition: - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "model": ("MODEL", {"tooltip": "The diffusion model the LoRA will be applied to."}), - "clip": ("CLIP", {"tooltip": "The CLIP model the LoRA will be applied to."}), - "base_model_type": ("STRING", {"tooltip": "The base type of model in CivitAI format."}), - "description": ("STRING", {"tooltip": "Brief explanation of LoRA functionality at the added place."}), - }, - } - - RETURN_TYPES = ("MODEL", "CLIP") - OUTPUT_TOOLTIPS = ("The modified diffusion model.", "The modified CLIP model.") - CATEGORY = "Visionatrix/UI" - FUNCTION = "do_it" - DESCRIPTION = "Node that allows dynamic selection of any supported LoRAs from CivitAI in the Visionatrix UI." - - @classmethod - def do_it(cls, model, clip, **kwargs) -> tuple: - return model, clip - - -class VixCheckboxLogic: - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "state": ("BOOLEAN", {"default": False}), - "input_off_state": (any_typ, {"lazy": True}), - "input_on_state": (any_typ, {"lazy": True}), - }, - "optional": {}, - } - - RETURN_TYPES = (any_typ,) - RETURN_NAMES = ("output_to",) - CATEGORY = "Visionatrix/Logic" - FUNCTION = "do_it" - - @classmethod - def do_it(cls, state, **kwargs) -> tuple: - if state is False: - return (kwargs.get("input_off_state", None),) - return (kwargs.get("input_on_state", None),) - - @staticmethod - def check_lazy_status(state, **kwargs): - if state is False: - return ["input_off_state"] - return ["input_on_state"] - - -class VixMultilineText: - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "text": ("STRING", {"default": "", "multiline": True, "dynamicPrompts": False}), - }, - "optional": {}, - } - RETURN_TYPES = ("STRING",) - FUNCTION = "do_it" - CATEGORY = "Visionatrix/Text" - - @classmethod - def do_it(cls, text, **kwargs) -> tuple: - return (text,) - - -class VixTextConcatenate: - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "delimiter": ("STRING", {"default": ", "}), - "clean_whitespace": (["true", "false"],), - }, - "optional": { - "text_a": ("STRING", {"forceInput": True}), - "text_b": ("STRING", {"forceInput": True}), - "text_c": ("STRING", {"forceInput": True}), - "text_d": ("STRING", {"forceInput": True}), - } - } - - RETURN_TYPES = ("STRING",) - FUNCTION = "do_it" - CATEGORY = "Visionatrix/Text" - - @classmethod - def do_it(cls, delimiter: str, clean_whitespace: str, **kwargs): - delim = "\n" if delimiter == "\\n" else delimiter - strip = clean_whitespace.lower() == "true" - parts = ( - (val.strip() if strip else val) - for _, val in sorted(kwargs.items()) - if isinstance(val, str) - ) - return (delim.join(filter(None, parts)),) - - - -NODE_CLASS_MAPPINGS = { - "VixUiAspectRatioSelector": VixUiAspectRatioSelector, - "VixUiCheckbox": VixUiCheckbox, - "VixUiRangeFloat": VixUiRangeFloat, - "VixUiRangeScaleFloat": VixUiRangeScaleFloat, - "VixUiRangeInt": VixUiRangeInt, - "VixUiList": VixUiList, - "VixUiPrompt": VixUiPrompt, - "VixUiCheckboxLogic": VixUiCheckboxLogic, - "VixUiListLogic": VixUiListLogic, - "VixUiWorkflowMetadata": VixUiWorkflowMetadata, - "VixDynamicLoraDefinition": VixDynamicLoraDefinition, - "VixCheckboxLogic": VixCheckboxLogic, - "StyleAlignedBatchAlign": StyleAlignedBatchAlign, - "VixMultilineText": VixMultilineText, - "VixTextConcatenate": VixTextConcatenate, -} - -NODE_DISPLAY_NAME_MAPPINGS = { - "VixUiAspectRatioSelector": "VixUI-Aspect Ratio", - "VixUiCheckbox": "VixUI-Checkbox", - "VixUiRangeFloat": "VixUI-RangeFloat", - "VixUiRangeScaleFloat": "VixUI-RangeScaleFloat", - "VixUiRangeInt": "VixUI-RangeInt", - "VixUiList": "VixUI-List", - "VixUiPrompt": "VixUI-Prompt", - "VixUiCheckboxLogic": "VixUI-CheckboxLogic", - "VixUiListLogic": "VixUI-ListLogic", - "VixUiWorkflowMetadata": "VixUI-WorkflowMetadata", - "VixDynamicLoraDefinition": "Vix-DynamicLoraDefinition", - "VixCheckboxLogic": "Vix-CheckboxLogic", - "StyleAlignedBatchAlign": "StyleAligned Batch Align", - "VixMultilineText": "Text Multiline", - "VixTextConcatenate": "Text Concatenate", -} +__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"] diff --git a/image.py b/image.py new file mode 100644 index 0000000..101a71c --- /dev/null +++ b/image.py @@ -0,0 +1,125 @@ +import numpy as np +import torch +from PIL import Image, ImageEnhance, ImageFilter + +from .utils import image_to_pillow, pillow_to_image + + +class VixImageFilters: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "image": ("IMAGE",), + "brightness": ( + "FLOAT", + {"default": 0.0, "min": -1.0, "max": 1.0, "step": 0.01}, + ), + "contrast": ( + "FLOAT", + {"default": 1.0, "min": -1.0, "max": 2.0, "step": 0.01}, + ), + "saturation": ( + "FLOAT", + {"default": 1.0, "min": 0.0, "max": 5.0, "step": 0.01}, + ), + "sharpness": ( + "FLOAT", + {"default": 1.0, "min": -5.0, "max": 5.0, "step": 0.01}, + ), + "blur": ("INT", {"default": 0, "min": 0, "max": 16, "step": 1}), + "gaussian_blur": ( + "FLOAT", + {"default": 0.0, "min": 0.0, "max": 1024.0, "step": 0.1}, + ), + "edge_enhance": ( + "FLOAT", + {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}, + ), + }, + } + + RETURN_TYPES = ("IMAGE",) + FUNCTION = "do_it" + CATEGORY = "Visionatrix/Image" + + @classmethod + def process_image( + cls, + img: torch.Tensor, + brightness: float, + contrast: float, + saturation: float, + sharpness: float, + blur: int, + gaussian_blur: float, + edge_enhance: float, + processing_list: bool = False, + ) -> torch.Tensor: + img = np.clip(img + brightness, 0.0, 1.0) if brightness != 0.0 else img + img = np.clip(img * contrast, 0.0, 1.0) if contrast != 1.0 else img + + pil_image = None + + if saturation != 1.0: + pil_image = ImageEnhance.Color(image_to_pillow(img)).enhance(saturation) + + if sharpness != 1.0: + pil_image = ImageEnhance.Sharpness(pil_image or image_to_pillow(img)).enhance(sharpness) + + if blur > 0: + pil_image = pil_image or image_to_pillow(img) + for _ in range(blur): + pil_image = pil_image.filter(ImageFilter.BLUR) + + if gaussian_blur > 0.0: + pil_image = pil_image or image_to_pillow(img) + pil_image = pil_image.filter(ImageFilter.GaussianBlur(radius=gaussian_blur)) + + if edge_enhance > 0.0: + pil_image = pil_image or image_to_pillow(img) + edge_enhanced = pil_image.filter(ImageFilter.EDGE_ENHANCE_MORE) + mask = Image.new("L", pil_image.size, color=round(edge_enhance * 255)) + pil_image = Image.composite(edge_enhanced, pil_image, mask) + + return pillow_to_image(pil_image) if pil_image else (img.unsqueeze(0) if processing_list else img) + + def do_it( + self, + image: torch.Tensor | list[torch.Tensor], + brightness: float, + contrast: float, + saturation: float, + sharpness: float, + blur: int, + gaussian_blur: float, + edge_enhance: float, + ): + if len(image) > 1: + result = [ + self.process_image( + img, + brightness, + contrast, + saturation, + sharpness, + blur, + gaussian_blur, + edge_enhance, + processing_list=True, + ) + for img in image + ] + return (torch.cat(result, dim=0),) + return ( + self.process_image( + image, + brightness, + contrast, + saturation, + sharpness, + blur, + gaussian_blur, + edge_enhance, + ), + ) diff --git a/nodes.py b/nodes.py new file mode 100644 index 0000000..ccf0243 --- /dev/null +++ b/nodes.py @@ -0,0 +1,484 @@ +import json + +from . import image, text +from .style_aligned import StyleAlignedBatchAlign + + +class AnyType(str): + def __ne__(self, __value: object) -> bool: + return False + + +any_typ = AnyType("*") + + +class VixUiAspectRatioSelector: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "aspect_ratio": ( + [ + "1:1 (1024x1024)", + "2:3 (832x1216)", + "3:4 (896x1152)", + "5:8 (768x1216)", + "9:16 (768x1344)", + "9:19 (704x1472)", + "9:21 (640x1536)", + "3:2 (1216x832)", + "4:3 (1152x896)", + "8:5 (1216x768)", + "16:9 (1344x768)", + "19:9 (1472x704)", + "21:9 (1536x640)", + ], + ), + "display_name": ("STRING", {"default": "Aspect Ratio"}), + "optional": ("BOOLEAN", {"default": True}), + "advanced": ("BOOLEAN", {"default": True}), + "order": ("INT", {"default": 20}), + "custom_id": ("STRING", {"default": "aspect_ratio"}), + }, + "optional": { + "hidden": ("BOOLEAN", {"default": False}), + }, + } + + RETURN_TYPES = ("STRING", "INT", "INT") + RETURN_NAMES = ("ratio", "width", "height") + FUNCTION = "do_it" + CATEGORY = "Visionatrix/UI" + + def do_it(self, aspect_ratio, **kwargs): + ratio, dims = aspect_ratio.split(" (") + dims = dims[:-1] # Remove the closing parenthesis + width, height = map(int, dims.split("x")) + return ratio, width, height + + +class VixUiCheckbox: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "state": ("BOOLEAN", {"default": False}), + "display_name": ("STRING", {"default": "Display Name"}), + "optional": ("BOOLEAN", {"default": True}), + "advanced": ("BOOLEAN", {"default": True}), + "order": ("INT", {"default": 99}), + "custom_id": ("STRING", {"default": ""}), + }, + "optional": { + "hidden": ("BOOLEAN", {"default": False}), + }, + } + + RETURN_TYPES = ("BOOLEAN", "INT") + RETURN_NAMES = ("bool", "int") + CATEGORY = "Visionatrix/UI" + FUNCTION = "do_it" + + @classmethod + def do_it(cls, state, **kwargs) -> tuple: + return state, int(state) + + +class VixUiRangeFloat: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "value": ("FLOAT", {"default": 4.0, "step": 0.01, "round": False}), + "display_name": ("STRING", {"default": "Display Range"}), + "optional": ("BOOLEAN", {"default": True}), + "advanced": ("BOOLEAN", {"default": True}), + "min": ("FLOAT", {"default": 1.0, "step": 0.01, "round": False}), + "max": ("FLOAT", {"default": 9.0, "step": 0.01, "round": False}), + "step": ("FLOAT", {"default": 0.1, "step": 0.01, "round": False}), + "order": ("INT", {"default": 99}), + "custom_id": ("STRING", {"default": ""}), + }, + "optional": { + "hidden": ("BOOLEAN", {"default": False}), + }, + } + + FUNCTION = "do_it" + CATEGORY = "Visionatrix/UI" + RETURN_TYPES = ("FLOAT",) + + @classmethod + def do_it(cls, value, **kwargs) -> tuple: + return (value,) + + +class VixUiRangeScaleFloat: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "value": ("FLOAT", {"default": 4.0, "step": 0.01, "round": False}), + "display_name": ("STRING", {"default": "Image Size Factor"}), + "optional": ("BOOLEAN", {"default": True}), + "advanced": ("BOOLEAN", {"default": True}), + "source_input_name": ("STRING", {"default": ""}), + "min": ("FLOAT", {"default": 1.0, "step": 0.01, "round": False}), + "max": ("FLOAT", {"default": 9.0, "step": 0.01, "round": False}), + "step": ("FLOAT", {"default": 0.1, "step": 0.01, "round": False}), + "order": ("INT", {"default": 99}), + "custom_id": ("STRING", {"default": ""}), + }, + "optional": { + "hidden": ("BOOLEAN", {"default": False}), + }, + } + + FUNCTION = "do_it" + CATEGORY = "Visionatrix/UI" + RETURN_TYPES = ("FLOAT",) + + @classmethod + def do_it(cls, value, **kwargs) -> tuple: + return (value,) + + +class VixUiRangeInt: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "value": ("INT", {"default": 10}), + "display_name": ("STRING", {"default": "Display Range"}), + "optional": ("BOOLEAN", {"default": True}), + "advanced": ("BOOLEAN", {"default": True}), + "min": ("INT", {"default": 1}), + "max": ("INT", {"default": 20}), + "step": ("INT", {"default": 1}), + "order": ("INT", {"default": 99}), + "custom_id": ("STRING", {"default": ""}), + }, + "optional": { + "hidden": ("BOOLEAN", {"default": False}), + }, + } + + FUNCTION = "do_it" + CATEGORY = "Visionatrix/UI" + RETURN_TYPES = ("INT",) + + @classmethod + def do_it(cls, value, **kwargs) -> tuple: + return (value,) + + +class VixUiList: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "default_value": ("STRING", {}), + "possible_values": ("STRING", {"default": "[]", "multiline": True}), + "display_name": ("STRING", {"default": "Dropdown list"}), + "optional": ("BOOLEAN", {"default": True}), + "advanced": ("BOOLEAN", {"default": True}), + "order": ("INT", {"default": 99}), + "custom_id": ("STRING", {"default": ""}), + }, + "optional": { + "hidden": ("BOOLEAN", {"default": False}), + }, + } + + RETURN_TYPES = (any_typ,) + FUNCTION = "do_it" + CATEGORY = "Visionatrix/UI" + + @classmethod + def do_it(cls, default_value, **kwargs) -> tuple: + possible_values = json.loads(kwargs["possible_values"]) + if isinstance(possible_values, dict) and default_value in possible_values: + return (possible_values[default_value],) + return (default_value,) + + +class VixUiPrompt: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "text": ( + "STRING", + {"default": "", "multiline": True, "dynamicPrompts": True}, + ), + "display_name": ("STRING", {"default": "Prompt"}), + "optional": ("BOOLEAN", {"default": False}), + "advanced": ("BOOLEAN", {"default": False}), + "order": ("INT", {"default": 10}), + "custom_id": ("STRING", {"default": ""}), + }, + "optional": { + "hidden": ("BOOLEAN", {"default": False}), + "translatable": ("BOOLEAN", {"default": False}), + }, + } + + RETURN_TYPES = ("STRING",) + FUNCTION = "do_it" + CATEGORY = "Visionatrix/UI" + + @classmethod + def do_it(cls, text, **kwargs) -> tuple: + return (text,) + + +class VixUiCheckboxLogic: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "state": ("BOOLEAN", {"default": False}), + "display_name": ("STRING", {"default": "Display Name"}), + "optional": ("BOOLEAN", {"default": True}), + "advanced": ("BOOLEAN", {"default": True}), + "order": ("INT", {"default": 99}), + "custom_id": ("STRING", {"default": ""}), + }, + "optional": { + "input_off_state": (any_typ, {"lazy": True}), + "input_on_state": (any_typ, {"lazy": True}), + "hidden": ("BOOLEAN", {"default": False}), + }, + } + + RETURN_TYPES = (any_typ,) + RETURN_NAMES = ("output_to",) + CATEGORY = "Visionatrix/UI" + FUNCTION = "do_it" + + @classmethod + def do_it(cls, state, **kwargs) -> tuple: + if state is False: + return (kwargs.get("input_off_state"),) + return (kwargs.get("input_on_state"),) + + @staticmethod + def check_lazy_status(state, **kwargs): + if state is False: + return ["input_off_state"] + return ["input_on_state"] + + +class VixUiListLogic: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "default_value": ("STRING", {}), + "possible_values": ("STRING", {"default": "[]", "multiline": True}), + "display_name": ("STRING", {"default": "Display Name"}), + "optional": ("BOOLEAN", {"default": True}), + "advanced": ("BOOLEAN", {"default": True}), + "order": ("INT", {"default": 99}), + "custom_id": ("STRING", {"default": ""}), + }, + "optional": { + "input_first": (any_typ, {"lazy": True}), + "input_second": (any_typ, {"lazy": True}), + "input_third": (any_typ, {"lazy": True}), + "input_fourth": (any_typ, {"lazy": True}), + "input_fifth": (any_typ, {"lazy": True}), + "input_sixth": (any_typ, {"lazy": True}), + "hidden": ("BOOLEAN", {"default": False}), + }, + } + + RETURN_TYPES = (any_typ,) + RETURN_NAMES = ("output_to",) + CATEGORY = "Visionatrix/UI" + FUNCTION = "do_it" + + @classmethod + def do_it(cls, default_value, **kwargs) -> tuple: + list_with_values: list = json.loads(kwargs["possible_values"]) + index_to_return = list_with_values.index(default_value) + if index_to_return == 0: + return (kwargs["input_first"],) + if index_to_return == 1: + return (kwargs["input_second"],) + if index_to_return == 2: + return (kwargs["input_third"],) + if index_to_return == 3: + return (kwargs["input_fourth"],) + if index_to_return == 4: + return (kwargs["input_fifth"],) + if index_to_return == 5: + return (kwargs["input_sixth"],) + raise RuntimeError("Workflow logic error") + + @staticmethod + def check_lazy_status(default_value, **kwargs): + list_with_values: list = json.loads(kwargs["possible_values"]) + index_to_return = list_with_values.index(default_value) + if index_to_return == 0: + return ["input_first"] + if index_to_return == 1: + return ["input_second"] + if index_to_return == 2: + return ["input_third"] + if index_to_return == 3: + return ["input_fourth"] + if index_to_return == 4: + return ["input_fifth"] + if index_to_return == 5: + return ["input_sixth"] + raise RuntimeError("Workflow logic error") + + +class VixUiWorkflowMetadata: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "name": ("STRING", {}), + "display_name": ("STRING", {}), + "description": ("STRING", {"default": ""}), + "author": ("STRING", {}), + "homepage": ("STRING", {"default": ""}), + "documentation": ("STRING", {"default": ""}), + "license": ("STRING", {"default": ""}), + "tags": ("STRING", {"default": '["general"]', "multiline": True}), + "version": ("STRING", {"default": "1.0.0"}), + }, + "optional": { + "requires": ("STRING", {"default": "[]", "multiline": True}), + "is_seed_supported": ("BOOLEAN", {"default": True}), + "is_count_supported": ("BOOLEAN", {"default": True}), + "is_translations_supported": ("BOOLEAN", {"default": False}), + "is_macos_supported": ("BOOLEAN", {"default": True}), + "required_memory_gb": ( + "FLOAT", + {"default": 0.0, "step": 0.1, "round": False}, + ), + "hidden": ("BOOLEAN", {"default": False}), + "remote_vae": ("BOOLEAN", {"default": False}), + }, + } + + RETURN_TYPES = ("STRING",) + FUNCTION = "do_it" + CATEGORY = "Visionatrix/UI" + + @classmethod + def do_it(cls, text, **kwargs) -> tuple: + return (text,) + + +class VixDynamicLoraDefinition: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "model": ( + "MODEL", + {"tooltip": "The diffusion model the LoRA will be applied to."}, + ), + "clip": ( + "CLIP", + {"tooltip": "The CLIP model the LoRA will be applied to."}, + ), + "base_model_type": ( + "STRING", + {"tooltip": "The base type of model in CivitAI format."}, + ), + "description": ( + "STRING", + {"tooltip": "Brief explanation of LoRA functionality at the added place."}, + ), + }, + } + + RETURN_TYPES = ("MODEL", "CLIP") + OUTPUT_TOOLTIPS = ("The modified diffusion model.", "The modified CLIP model.") + CATEGORY = "Visionatrix/UI" + FUNCTION = "do_it" + DESCRIPTION = "Node that allows dynamic selection of any supported LoRAs from CivitAI in the Visionatrix UI." + + @classmethod + def do_it(cls, model, clip, **kwargs) -> tuple: + return model, clip + + +class VixCheckboxLogic: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "state": ("BOOLEAN", {"default": False}), + "input_off_state": (any_typ, {"lazy": True}), + "input_on_state": (any_typ, {"lazy": True}), + }, + "optional": {}, + } + + RETURN_TYPES = (any_typ,) + RETURN_NAMES = ("output_to",) + CATEGORY = "Visionatrix/Logic" + FUNCTION = "do_it" + + @classmethod + def do_it(cls, state, **kwargs) -> tuple: + if state is False: + return (kwargs.get("input_off_state"),) + return (kwargs.get("input_on_state"),) + + @staticmethod + def check_lazy_status(state, **kwargs): + if state is False: + return ["input_off_state"] + return ["input_on_state"] + + +NODE_CLASS_MAPPINGS = { + "VixUiAspectRatioSelector": VixUiAspectRatioSelector, + "VixUiCheckbox": VixUiCheckbox, + "VixUiRangeFloat": VixUiRangeFloat, + "VixUiRangeScaleFloat": VixUiRangeScaleFloat, + "VixUiRangeInt": VixUiRangeInt, + "VixUiList": VixUiList, + "VixUiPrompt": VixUiPrompt, + "VixUiCheckboxLogic": VixUiCheckboxLogic, + "VixUiListLogic": VixUiListLogic, + "VixUiWorkflowMetadata": VixUiWorkflowMetadata, + "VixDynamicLoraDefinition": VixDynamicLoraDefinition, + "VixCheckboxLogic": VixCheckboxLogic, + "StyleAlignedBatchAlign": StyleAlignedBatchAlign, + "VixMultilineText": text.VixMultilineText, + "VixTextConcatenate": text.VixTextConcatenate, + "VixTextReplace": text.VixTextReplace, + "VixDictionaryConvert": text.VixDictionaryConvert, + "VixDictionaryGet": text.VixDictionaryGet, + "VixImageFilters": image.VixImageFilters, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "VixUiAspectRatioSelector": "Aspect Ratio (VixUI)", + "VixUiCheckbox": "Checkbox (VixUI)", + "VixUiRangeFloat": "Range Float (VixUI)", + "VixUiRangeScaleFloat": "Range Scale Float (VixUI)", + "VixUiRangeInt": "Range Int (VixUI)", + "VixUiList": "List (VixUI)", + "VixUiPrompt": "Prompt (VixUI)", + "VixUiCheckboxLogic": "Checkbox Logic (VixUI)", + "VixUiListLogic": "List Logic (VixUI)", + "VixUiWorkflowMetadata": "Workflow Metadata (VixUI)", + "VixDynamicLoraDefinition": "Vix Dynamic Lora Definition", + "VixCheckboxLogic": "Vix Checkbox Logic", + "StyleAlignedBatchAlign": "StyleAligned Batch Align", + "VixMultilineText": "Text Multiline", + "VixTextConcatenate": "Text Concatenate", + "VixTextReplace": "Text Replace", + "VixDictionaryConvert": "Convert to Dictionary", + "VixDictionaryGet": "Dictionary Get", + "VixImageFilters": "Image Filters", +} diff --git a/pyproject.toml b/pyproject.toml index 7ff05f7..e47cc52 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,10 +1,53 @@ [project] name = "comfyui-visionatrix" +version = "1.2.0" description = "The ComfyUI-Visionatrix nodes are designed for convenient ComfyUI to [a/Visionatrix](https://github.com/Visionatrix/Visionatrix) workflow support migration, in particular to extract prompt input params (input, textarea, checkbox, select, range, file) to be used in simplified Visionatrix UI." -version = "1.1.0" -license = {file = "LICENSE"} +license = { file = "LICENSE" } +classifiers = [ + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", +] urls.Repository = "https://github.com/Visionatrix/ComfyUI-Visionatrix" +[tool.black] +line-length = 120 +preview = true + +[tool.ruff] +target-version = "py310" +line-length = 120 +lint.select = [ + "A", + "B", + "C", + "E", + "F", + "G", + "I", + "PIE", + "Q", + "RET", + "RUF", + "S", + "SIM", + "UP", + "W", +] +lint.extend-ignore = [ + "I001", + "RUF100", + "S311", + "S603", +] +lint.mccabe.max-complexity = 20 + +[tool.isort] +profile = "black" + [tool.comfy] PublisherId = "visionatrix" DisplayName = "ComfyUI-Visionatrix" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..516918c --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +torch +pillow +numpy diff --git a/style_aligned.py b/style_aligned.py index e1fa608..71571cc 100644 --- a/style_aligned.py +++ b/style_aligned.py @@ -3,12 +3,10 @@ # Copyright (c) 2023 Brian Fitzgerald # original repository: https://github.com/brianfitzgerald/style_aligned_comfy -import torch.nn as nn import torch - +import torch.nn as nn from comfy.model_patcher import ModelPatcher - T = torch.Tensor @@ -54,8 +52,7 @@ def adain(feat: T) -> T: feat_style_mean = expand_first(feat_mean) feat_style_std = expand_first(feat_std) feat = (feat - feat_mean) / feat_std - feat = feat * feat_style_std + feat_style_mean - return feat + return feat * feat_style_std + feat_style_mean class SharedAttentionProcessor: @@ -89,16 +86,14 @@ def get_norm_layers( norm_layers_["group"].append(layer) else: for child_layer in layer.children(): - get_norm_layers( - child_layer, norm_layers_, share_layer_norm, share_group_norm - ) + get_norm_layers(child_layer, norm_layers_, share_layer_norm, share_group_norm) def register_norm_forward( norm_layer: nn.GroupNorm | nn.LayerNorm, ) -> nn.GroupNorm | nn.LayerNorm: if not hasattr(norm_layer, "orig_forward"): - setattr(norm_layer, "orig_forward", norm_layer.forward) + setattr(norm_layer, "orig_forward", norm_layer.forward) # noqa orig_forward = norm_layer.orig_forward def forward_(hidden_states: T) -> T: @@ -118,9 +113,7 @@ def register_shared_norm( ): norm_layers = {"group": [], "layer": []} get_norm_layers(model.model, norm_layers, share_layer_norm, share_group_norm) - print( - f"Patching {len(norm_layers['group'])} group norms, {len(norm_layers['layer'])} layer norms." - ) + print(f"Patching {len(norm_layers['group'])} group norms, {len(norm_layers['layer'])} layer norms.") return [register_norm_forward(layer) for layer in norm_layers["group"]] + [ register_norm_forward(layer) for layer in norm_layers["layer"] ] diff --git a/text.py b/text.py new file mode 100644 index 0000000..072a2ad --- /dev/null +++ b/text.py @@ -0,0 +1,119 @@ +import ast +import re + + +class VixMultilineText: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "text": ( + "STRING", + {"default": "", "multiline": True, "dynamicPrompts": False}, + ), + }, + "optional": {}, + } + + RETURN_TYPES = ("STRING",) + FUNCTION = "do_it" + CATEGORY = "Visionatrix/Text" + + @classmethod + def do_it(cls, text, **kwargs) -> tuple: + return (text,) + + +class VixTextConcatenate: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "delimiter": ("STRING", {"default": ", "}), + "clean_whitespace": (["true", "false"],), + }, + "optional": { + "text_a": ("STRING", {"forceInput": True}), + "text_b": ("STRING", {"forceInput": True}), + "text_c": ("STRING", {"forceInput": True}), + "text_d": ("STRING", {"forceInput": True}), + }, + } + + RETURN_TYPES = ("STRING",) + FUNCTION = "do_it" + CATEGORY = "Visionatrix/Text" + + @classmethod + def do_it(cls, delimiter: str, clean_whitespace: str, **kwargs): + delim = "\n" if delimiter == "\\n" else delimiter + strip = clean_whitespace.lower() == "true" + parts = ((val.strip() if strip else val) for _, val in sorted(kwargs.items()) if isinstance(val, str)) + return (delim.join(filter(None, parts)),) + + +class VixTextReplace: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "text": ("STRING", {"forceInput": True}), + "find": ("STRING", {"default": "", "multiline": False}), + "replace": ("STRING", {"default": "", "multiline": False}), + }, + "optional": {}, + } + + RETURN_TYPES = ("STRING", "NUMBER", "FLOAT", "INT") + RETURN_NAMES = ( + "result_text", + "replacement_count_number", + "replacement_count_float", + "replacement_count_int", + ) + FUNCTION = "do_it" + CATEGORY = "Visionatrix/Text" + + @classmethod + def do_it(cls, text: str, find: str, replace: str): + modified_text, count = re.subn(find, replace, text) + return modified_text, count, float(count), count + + +class VixDictionaryConvert: + @classmethod + def INPUT_TYPES(cls): + return { + "required": {"dictionary_text": ("STRING", {"forceInput": True})}, + "optional": {}, + } + + RETURN_TYPES = ("DICT",) + FUNCTION = "do_it" + CATEGORY = "Visionatrix/Text" + + @classmethod + def do_it(cls, dictionary_text: str): + return (ast.literal_eval(dictionary_text),) + + +class VixDictionaryGet: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "dictionary": ("DICT",), + "key": ("STRING", {"default": "", "multiline": False}), + }, + "optional": { + "default_value": ("STRING", {"default": "", "multiline": False}), + }, + } + + RETURN_TYPES = ("STRING",) + FUNCTION = "do_it" + CATEGORY = "Visionatrix/Text" + + @classmethod + def do_it(cls, dictionary: dict, key: str, default_value=""): + return (str(dictionary.get(key, default_value)),) diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..0d88ebc --- /dev/null +++ b/utils.py @@ -0,0 +1,18 @@ +import numpy as np +from PIL import Image +from torch import Tensor, from_numpy + + +def image_to_pillow(image: Tensor) -> Image.Image: + return Image.fromarray(np.clip(255.0 * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8)) + + +def images_to_pillow(images: Tensor | list[Tensor]) -> list[Image.Image]: + pillow_images = [] + for _bn, image in enumerate(images): + pillow_images.append(image_to_pillow(image)) + return pillow_images + + +def pillow_to_image(image: Image.Image) -> Tensor: + return from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0)