diff --git a/.gitattributes b/.gitattributes index 4903fccb8598829a7b70cd486229d466dda8ed04..bcff092210d92aac864389878b582c90fafeef77 100644 --- a/.gitattributes +++ b/.gitattributes @@ -46,3 +46,28 @@ ComfyUI/comfy_api_nodes/__pycache__/nodes_bytedance.cpython-312.pyc filter=lfs d ComfyUI/comfy_api_nodes/__pycache__/nodes_kling.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text ComfyUI/comfy_api_nodes/apis/__pycache__/__init__.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text ComfyUI/comfy_extras/__pycache__/nodes_gaussian_splat.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text +ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/brushnet/__pycache__/unet_2d_blocks.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text +ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/kolors/chatglm/tokenizer/vocab.txt filter=lfs diff=lfs merge=lfs -text +ComfyUI/custom_nodes/ComfyUI-Easy-Use/resources/OpenSans-Medium.ttf filter=lfs diff=lfs merge=lfs -text +ComfyUI/custom_nodes/ComfyUI-Easy-Use/resources/wenquan.ttf filter=lfs diff=lfs merge=lfs -text +ComfyUI/custom_nodes/ComfyUI-KJNodes/docs/images/2024-04-03_20_49_29-ComfyUI.png filter=lfs diff=lfs merge=lfs -text +ComfyUI/custom_nodes/ComfyUI-KJNodes/fonts/FreeMono.ttf filter=lfs diff=lfs merge=lfs -text +ComfyUI/custom_nodes/ComfyUI-KJNodes/fonts/FreeMonoBoldOblique.otf filter=lfs diff=lfs merge=lfs -text +ComfyUI/custom_nodes/ComfyUI-KJNodes/fonts/TTNorms-Black.otf filter=lfs diff=lfs merge=lfs -text +ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/__pycache__/image_nodes.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text +ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/__pycache__/ltxv_nodes.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text +ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/__pycache__/model_optimization_nodes.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text +ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/__pycache__/nodes.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text +ComfyUI/custom_nodes/ComfyUI-LTXVideo/example_workflows/assets/base[[:space:]]model[[:space:]]image.png filter=lfs diff=lfs merge=lfs -text +ComfyUI/custom_nodes/ComfyUI-LTXVideo/example_workflows/assets/buildings[[:space:]]ff.png filter=lfs diff=lfs merge=lfs -text +ComfyUI/custom_nodes/ComfyUI-LTXVideo/example_workflows/assets/buildings.mp4 filter=lfs diff=lfs merge=lfs -text +ComfyUI/custom_nodes/ComfyUI-LTXVideo/example_workflows/assets/distilled[[:space:]]image.png filter=lfs diff=lfs merge=lfs -text +ComfyUI/custom_nodes/ComfyUI-LTXVideo/example_workflows/assets/hdr_input_video.mp4 filter=lfs diff=lfs merge=lfs -text +ComfyUI/custom_nodes/ComfyUI-LTXVideo/example_workflows/assets/ingredients_input.jpg filter=lfs diff=lfs merge=lfs -text +ComfyUI/custom_nodes/ComfyUI-LTXVideo/example_workflows/assets/inpainting_input.mp4 filter=lfs diff=lfs merge=lfs -text +ComfyUI/custom_nodes/ComfyUI-LTXVideo/example_workflows/assets/instant_shave_input.mp4 filter=lfs diff=lfs merge=lfs -text +ComfyUI/custom_nodes/ComfyUI-LTXVideo/example_workflows/assets/lipdub_input.mp4 filter=lfs diff=lfs merge=lfs -text +ComfyUI/custom_nodes/ComfyUI-LTXVideo/example_workflows/assets/motion_track_input.jpg filter=lfs diff=lfs merge=lfs -text +ComfyUI/custom_nodes/ComfyUI-LTXVideo/example_workflows/assets/outpainting_input.mp4 filter=lfs diff=lfs merge=lfs -text +ComfyUI/custom_nodes/ComfyUI-LTXVideo/example_workflows/assets/spatial_upscale_4x_input.mp4 filter=lfs diff=lfs merge=lfs -text +ComfyUI/custom_nodes/ComfyUI-LTXVideo/gemma_configs/tokenizer.json filter=lfs diff=lfs merge=lfs -text diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/api/bizyair.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/api/bizyair.py new file mode 100644 index 0000000000000000000000000000000000000000..b7b458155032c303faac131f2b29825fec35f4ce --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/api/bizyair.py @@ -0,0 +1,372 @@ +import yaml +import pathlib +import base64 +import io +import json +import os +import pickle +import zlib +import urllib.parse +import urllib.request +import urllib.error +from enum import Enum +from functools import singledispatch +from typing import Any, List, Union + +import numpy as np +import torch +from PIL import Image + +root_path = pathlib.Path(__file__).parent.parent.parent.parent +config_path = os.path.join(root_path, 'config.yaml') + +class BizyAIRAPI: + def __init__(self): + self.base_url = 'https://bizyair-api.siliconflow.cn/x/v1' + self.api_key = None + + + def getAPIKey(self): + if self.api_key is None: + if os.path.isfile(config_path): + with open(config_path, 'r') as f: + data = yaml.load(f, Loader=yaml.FullLoader) + if 'BIZYAIR_API_KEY' not in data: + raise Exception("Please add BIZYAIR_API_KEY to config.yaml") + self.api_key = data['BIZYAIR_API_KEY'] + else: + raise Exception("Please add config.yaml to root path") + return self.api_key + + def send_post_request(self, url, payload, headers): + try: + data = json.dumps(payload).encode("utf-8") + req = urllib.request.Request(url, data=data, headers=headers, method="POST") + with urllib.request.urlopen(req) as response: + response_data = response.read().decode("utf-8") + return response_data + except urllib.error.URLError as e: + if "Unauthorized" in str(e): + raise Exception( + "Key is invalid, please refer to https://cloud.siliconflow.cn to get the API key.\n" + "If you have the key, please click the 'BizyAir Key' button at the bottom right to set the key." + ) + else: + raise Exception( + f"Failed to connect to the server: {e}, if you have no key, " + ) + + # joycaption + def joyCaption(self, payload, image, apikey_override=None, API_URL='/supernode/joycaption2'): + if apikey_override is not None: + api_key = apikey_override + else: + api_key = self.getAPIKey() + url = f"{self.base_url}{API_URL}" + print('Sending request to:', url) + auth = f"Bearer {api_key}" + headers = { + "accept": "application/json", + "content-type": "application/json", + "authorization": auth, + } + input_image = encode_data(image, disable_image_marker=True) + payload["image"] = input_image + + ret: str = self.send_post_request(url=url, payload=payload, headers=headers) + ret = json.loads(ret) + + try: + if "result" in ret: + ret = json.loads(ret["result"]) + except Exception as e: + raise Exception(f"Unexpected response: {ret} {e=}") + + if ret["type"] == "error": + raise Exception(ret["message"]) + + msg = ret["data"] + if msg["type"] not in ("comfyair", "bizyair",): + raise Exception(f"Unexpected response type: {msg}") + + caption = msg["data"] + + return caption + +bizyairAPI = BizyAIRAPI() + + + +BIZYAIR_DEBUG = True +# Marker to identify base64-encoded tensors +TENSOR_MARKER = "TENSOR:" +IMAGE_MARKER = "IMAGE:" + + +class TaskStatus(Enum): + PENDING = "pending" + PROCESSING = "processing" + COMPLETED = "completed" + + +def convert_image_to_rgb(image: Image.Image) -> Image.Image: + if image.mode != "RGB": + return image.convert("RGB") + return image + + +def encode_image_to_base64( + image: Image.Image, format: str = "png", quality: int = 100, lossless=False +) -> str: + image = convert_image_to_rgb(image) + with io.BytesIO() as output: + image.save(output, format=format, quality=quality, lossless=lossless) + output.seek(0) + img_bytes = output.getvalue() + if BIZYAIR_DEBUG: + print(f"encode_image_to_base64: {format_bytes(len(img_bytes))}") + return base64.b64encode(img_bytes).decode("utf-8") + + +def decode_base64_to_np(img_data: str, format: str = "png") -> np.ndarray: + img_bytes = base64.b64decode(img_data) + if BIZYAIR_DEBUG: + print(f"decode_base64_to_np: {format_bytes(len(img_bytes))}") + with io.BytesIO(img_bytes) as input_buffer: + img = Image.open(input_buffer) + # https://github.com/comfyanonymous/ComfyUI/blob/a178e25912b01abf436eba1cfaab316ba02d272d/nodes.py#L1511 + img = img.convert("RGB") + return np.array(img) + + +def decode_base64_to_image(img_data: str) -> Image.Image: + img_bytes = base64.b64decode(img_data) + with io.BytesIO(img_bytes) as input_buffer: + img = Image.open(input_buffer) + if BIZYAIR_DEBUG: + format_info = img.format.upper() if img.format else "Unknown" + print(f"decode image format: {format_info}") + return img + + +def format_bytes(num_bytes: int) -> str: + """ + Converts a number of bytes to a human-readable string with units (B, KB, or MB). + + :param num_bytes: The number of bytes to convert. + :return: A string representing the number of bytes in a human-readable format. + """ + if num_bytes < 1024: + return f"{num_bytes} B" + elif num_bytes < 1024 * 1024: + return f"{num_bytes / 1024:.2f} KB" + else: + return f"{num_bytes / (1024 * 1024):.2f} MB" + + +def _legacy_encode_comfy_image(image: torch.Tensor, image_format="png") -> str: + input_image = image.cpu().detach().numpy() + i = 255.0 * input_image[0] + input_image = np.clip(i, 0, 255).astype(np.uint8) + base64ed_image = encode_image_to_base64( + Image.fromarray(input_image), format=image_format + ) + return base64ed_image + + +def _legacy_decode_comfy_image( + img_data: Union[List, str], image_format="png" +) -> torch.tensor: + if isinstance(img_data, List): + decoded_imgs = [decode_comfy_image(x, old_version=True) for x in img_data] + + combined_imgs = torch.cat(decoded_imgs, dim=0) + return combined_imgs + + out = decode_base64_to_np(img_data, format=image_format) + out = np.array(out).astype(np.float32) / 255.0 + output = torch.from_numpy(out)[None,] + return output + + +def _new_encode_comfy_image(images: torch.Tensor, image_format="WEBP", **kwargs) -> str: + """https://docs.comfy.org/essentials/custom_node_snippets#save-an-image-batch + Encode a batch of images to base64 strings. + + Args: + images (torch.Tensor): A batch of images. + image_format (str, optional): The format of the images. Defaults to "WEBP". + + Returns: + str: A JSON string containing the base64-encoded images. + """ + results = {} + for batch_number, image in enumerate(images): + i = 255.0 * image.cpu().numpy() + img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8)) + base64ed_image = encode_image_to_base64(img, format=image_format, **kwargs) + results[batch_number] = base64ed_image + + return json.dumps(results) + + +def _new_decode_comfy_image(img_datas: str, image_format="WEBP") -> torch.tensor: + """ + Decode a batch of base64-encoded images. + + Args: + img_datas (str): A JSON string containing the base64-encoded images. + image_format (str, optional): The format of the images. Defaults to "WEBP". + + Returns: + torch.Tensor: A tensor containing the decoded images. + """ + img_datas = json.loads(img_datas) + + decoded_imgs = [] + for img_data in img_datas.values(): + decoded_image = decode_base64_to_np(img_data, format=image_format) + decoded_image = np.array(decoded_image).astype(np.float32) / 255.0 + decoded_imgs.append(torch.from_numpy(decoded_image)[None,]) + + return torch.cat(decoded_imgs, dim=0) + + +def encode_comfy_image( + image: torch.Tensor, image_format="WEBP", old_version=False, lossless=False +) -> str: + if old_version: + return _legacy_encode_comfy_image(image, image_format) + return _new_encode_comfy_image(image, image_format, lossless=lossless) + + +def decode_comfy_image( + img_data: Union[List, str], image_format="WEBP", old_version=False +) -> torch.tensor: + if old_version: + return _legacy_decode_comfy_image(img_data, image_format) + return _new_decode_comfy_image(img_data, image_format) + + +def tensor_to_base64(tensor: torch.Tensor, compress=True) -> str: + tensor_np = tensor.cpu().detach().numpy() + + tensor_bytes = pickle.dumps(tensor_np) + if compress: + tensor_bytes = zlib.compress(tensor_bytes) + + tensor_b64 = base64.b64encode(tensor_bytes).decode("utf-8") + return tensor_b64 + + +def base64_to_tensor(tensor_b64: str, compress=True) -> torch.Tensor: + tensor_bytes = base64.b64decode(tensor_b64) + + if compress: + tensor_bytes = zlib.decompress(tensor_bytes) + + tensor_np = pickle.loads(tensor_bytes) + + tensor = torch.from_numpy(tensor_np) + return tensor + + +@singledispatch +def decode_data(input, old_version=False): + raise NotImplementedError(f"Unsupported type: {type(input)}") + + +@decode_data.register(int) +@decode_data.register(float) +@decode_data.register(bool) +@decode_data.register(type(None)) +def _(input, **kwargs): + return input + + +@decode_data.register(dict) +def _(input, **kwargs): + return {k: decode_data(v, **kwargs) for k, v in input.items()} + + +@decode_data.register(list) +def _(input, **kwargs): + return [decode_data(x, **kwargs) for x in input] + + +@decode_data.register(str) +def _(input: str, **kwargs): + if input.startswith(TENSOR_MARKER): + tensor_b64 = input[len(TENSOR_MARKER) :] + return base64_to_tensor(tensor_b64) + elif input.startswith(IMAGE_MARKER): + tensor_b64 = input[len(IMAGE_MARKER) :] + old_version = kwargs.get("old_version", False) + return decode_comfy_image(tensor_b64, old_version=old_version) + return input + + +@singledispatch +def encode_data(output, disable_image_marker=False, old_version=False): + raise NotImplementedError(f"Unsupported type: {type(output)}") + + +@encode_data.register(dict) +def _(output, **kwargs): + return {k: encode_data(v, **kwargs) for k, v in output.items()} + + +@encode_data.register(list) +def _(output, **kwargs): + return [encode_data(x, **kwargs) for x in output] + + +def is_image_tensor(tensor) -> bool: + """https://docs.comfy.org/essentials/custom_node_datatypes#image + + Check if the given tensor is in the format of an IMAGE (shape [B, H, W, C] where C=3). + + `Args`: + tensor (torch.Tensor): The tensor to check. + + `Returns`: + bool: True if the tensor is in the IMAGE format, False otherwise. + """ + try: + if not isinstance(tensor, torch.Tensor): + return False + + if len(tensor.shape) != 4: + return False + + B, H, W, C = tensor.shape + if C != 3: + return False + + return True + except: + return False + + +@encode_data.register(torch.Tensor) +def _(output, **kwargs): + if is_image_tensor(output) and not kwargs.get("disable_image_marker", False): + old_version = kwargs.get("old_version", False) + lossless = kwargs.get("lossless", True) + return IMAGE_MARKER + encode_comfy_image( + output, image_format="WEBP", old_version=old_version, lossless=lossless + ) + return TENSOR_MARKER + tensor_to_base64(output) + + +@encode_data.register(int) +@encode_data.register(float) +@encode_data.register(bool) +@encode_data.register(type(None)) +def _(output, **kwargs): + return output + + +@encode_data.register(str) +def _(output, **kwargs): + return output diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/api/fluxai.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/api/fluxai.py new file mode 100644 index 0000000000000000000000000000000000000000..eaaa4d486eb3f82a41ddfdf722a459e3283dd3cc --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/api/fluxai.py @@ -0,0 +1,51 @@ +import json +import os +import yaml +import requests +import pathlib +from aiohttp import web + +root_path = pathlib.Path(__file__).parent.parent.parent.parent +config_path = os.path.join(root_path,'config.yaml') +class FluxAIAPI: + def __init__(self): + self.api_url = "https://fluxaiimagegenerator.com/api" + self.origin = "https://fluxaiimagegenerator.com" + self.user_agent = None + self.cookie = None + + def promptGenerate(self, text, cookies=None): + cookie = self.cookie if cookies is None else cookies + if cookie is None: + if os.path.isfile(config_path): + with open(config_path, 'r') as f: + data = yaml.load(f, Loader=yaml.FullLoader) + if 'FLUXAI_COOKIE' not in data: + raise Exception("Please add FLUXAI_COOKIE to config.yaml") + if "FLUXAI_USER_AGENT" in data: + self.user_agent = data["FLUXAI_USER_AGENT"] + self.cookie = cookie = data['FLUXAI_COOKIE'] + + headers = { + "Cookie": cookie, + "Referer": "https://fluxaiimagegenerator.com/flux-prompt-generator", + "Origin": self.origin, + "Content-Type": "application/json", + } + if self.user_agent is not None: + headers['User-Agent'] = self.user_agent + + url = self.api_url + '/prompt' + json = { + "prompt": text + } + + response = requests.post(url, json=json, headers=headers) + res = response.json() + if "error" in res: + return res['error'] + elif "data" in res and "prompt" in res['data']: + return res['data']['prompt'] + +fluxaiAPI = FluxAIAPI() + diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/api/stability.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/api/stability.py new file mode 100644 index 0000000000000000000000000000000000000000..3b40db2e64db8394fab9d80847b1ed2ce004ae17 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/api/stability.py @@ -0,0 +1,200 @@ +import json +import os +import yaml +import requests +import pathlib +from aiohttp import web +from server import PromptServer +from ..image import tensor2pil, pil2tensor, image2base64, pil2byte +from ..log import log_node_error + + +root_path = pathlib.Path(__file__).parent.parent.parent.parent +config_path = os.path.join(root_path,'config.yaml') +default_key = [{'name':'Default', 'key':''}] + + +class StabilityAPI: + def __init__(self): + self.api_url = "https://api.stability.ai" + self.api_keys = None + self.api_current = 0 + self.user_info = {} + + def getErrors(self, code): + errors = { + 400: "Bad Request", + 403: "ApiKey Forbidden", + 413: "Your request was larger than 10MiB.", + 429: "You have made more than 150 requests in 10 seconds.", + 500: "Internal Server Error", + } + return errors.get(code, "Unknown Error") + + def getAPIKeys(self): + if os.path.isfile(config_path): + with open(config_path, 'r') as f: + data = yaml.load(f, Loader=yaml.FullLoader) + if not data: + data = {'STABILITY_API_KEY': default_key, 'STABILITY_API_DEFAULT':0} + with open(config_path, 'w') as f: + yaml.dump(data, f) + if 'STABILITY_API_KEY' not in data: + data['STABILITY_API_KEY'] = default_key + data['STABILITY_API_DEFAULT'] = 0 + with open(config_path, 'w') as f: + yaml.dump(data, f) + api_keys = data['STABILITY_API_KEY'] + self.api_current = data['STABILITY_API_DEFAULT'] + self.api_keys = api_keys + return api_keys + else: + # create a yaml file + with open(config_path, 'w') as f: + data = {'STABILITY_API_KEY': default_key, 'STABILITY_API_DEFAULT':0} + yaml.dump(data, f) + return data['STABILITY_API_KEY'] + pass + + def setAPIKeys(self, api_keys): + if len(api_keys) > 0: + self.api_keys = api_keys + # load and save the yaml file + with open(config_path, 'r') as f: + data = yaml.load(f, Loader=yaml.FullLoader) + data['STABILITY_API_KEY'] = api_keys + with open(config_path, 'w') as f: + yaml.dump(data, f) + return True + + def setAPIDefault(self, current): + if current is not None: + self.api_current = current + # load and save the yaml file + with open(config_path, 'r') as f: + data = yaml.load(f, Loader=yaml.FullLoader) + data['STABILITY_API_DEFAULT'] = current + with open(config_path, 'w') as f: + yaml.dump(data, f) + return True + + def generate_sd3_image(self, prompt, negative_prompt, aspect_ratio, model, seed, mode='text-to-image', image=None, strength=1, output_format='png', node_name='easy stableDiffusion3API'): + url = f"{self.api_url}/v2beta/stable-image/generate/sd3" + api_key = self.api_keys[self.api_current]['key'] + files = None + data = { + "prompt": prompt, + "mode": mode, + "model": model, + "seed": seed, + "output_format": output_format, + } + if model == 'sd3': + data['negative_prompt'] = negative_prompt + + if mode == 'text-to-image': + files = {"none": ''} + data['aspect_ratio'] = aspect_ratio + elif mode == 'image-to-image': + pil_image = tensor2pil(image) + image_byte = pil2byte(pil_image) + files = {"image": ("output.png", image_byte, 'image/png')} + data['strength'] = strength + + response = requests.post(url, + headers={"authorization": f"{api_key}", "accept": "application/json"}, + files=files, + data=data, + ) + if response.status_code == 200: + PromptServer.instance.send_sync('stable-diffusion-api-generate-succeed',{"model":model}) + json_data = response.json() + image_base64 = json_data['image'] + image_data = image2base64(image_base64) + output_t = pil2tensor(image_data) + return output_t + else: + if 'application/json' in response.headers['Content-Type']: + error_info = response.json() + log_node_error(node_name, error_info.get('name', 'No name provided')) + log_node_error(node_name, error_info.get('errors', ['No details provided'])) + error_status_text = self.getErrors(response.status_code) + PromptServer.instance.send_sync('easyuse-toast',{"type": "error", "content": error_status_text}) + raise Exception(f"Failed to generate image: {error_status_text}") + + # get user account + async def getUserAccount(self, cache=True): + url = f"{self.api_url}/v1/user/account" + api_key = self.api_keys[self.api_current]['key'] + name = self.api_keys[self.api_current]['name'] + if cache and name in self.user_info: + return self.user_info[name] + else: + response = requests.get(url, headers={"Authorization": f"Bearer {api_key}"}) + if response.status_code == 200: + user_info = response.json() + self.user_info[name] = user_info + return user_info + else: + PromptServer.instance.send_sync('easyuse-toast',{'type': 'error', 'content': self.getErrors(response.status_code)}) + return None + + # get user balance + async def getUserBalance(self): + url = f"{self.api_url}/v1/user/balance" + api_key = self.api_keys[self.api_current]['key'] + response = requests.get(url, headers={ + "Authorization": f"Bearer {api_key}" + }) + if response.status_code == 200: + return response.json() + else: + PromptServer.instance.send_sync('easyuse-toast', {'type': 'error', 'content': self.getErrors(response.status_code)}) + return None + +stableAPI = StabilityAPI() + +@PromptServer.instance.routes.get("/easyuse/stability/api_keys") +async def get_stability_api_keys(request): + stableAPI.getAPIKeys() + return web.json_response({"keys": stableAPI.api_keys, "current": stableAPI.api_current}) + +@PromptServer.instance.routes.post("/easyuse/stability/set_api_keys") +async def set_stability_api_keys(request): + post = await request.post() + api_keys = post.get("api_keys") + current = post.get('current') + if api_keys is not None: + api_keys = json.loads(api_keys) + stableAPI.setAPIKeys(api_keys) + if current is not None: + print(current) + stableAPI.setAPIDefault(int(current)) + account = await stableAPI.getUserAccount() + balance = await stableAPI.getUserBalance() + return web.json_response({'account': account, 'balance': balance}) + else: + return web.json_response({'status': 'ok'}) + else: + return web.Response(status=400) + +@PromptServer.instance.routes.post("/easyuse/stability/set_apikey_default") +async def set_stability_api_default(request): + post = await request.post() + current = post.get("current") + if current is not None and current < len(stableAPI.api_keys): + stableAPI.api_current = current + return web.json_response({'status': 'ok'}) + else: + return web.Response(status=400) + +@PromptServer.instance.routes.get("/easyuse/stability/user_info") +async def get_account_info(request): + account = await stableAPI.getUserAccount() + balance = await stableAPI.getUserBalance() + return web.json_response({'account': account, 'balance': balance}) + +@PromptServer.instance.routes.get("/easyuse/stability/balance") +async def get_balance_info(request): + balance = await stableAPI.getUserBalance() + return web.json_response({'balance': balance}) diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/cache.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/cache.py new file mode 100644 index 0000000000000000000000000000000000000000..b3249f95b8c539ee0140418f5c7fe9af7dbd86f5 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/cache.py @@ -0,0 +1,87 @@ +import itertools +from typing import Optional + +class TaggedCache: + def __init__(self, tag_settings: Optional[dict]=None): + self._tag_settings = tag_settings or {} # tag cache size + self._data = {} + + def __getitem__(self, key): + for tag_data in self._data.values(): + if key in tag_data: + return tag_data[key] + raise KeyError(f'Key `{key}` does not exist') + + def __setitem__(self, key, value: tuple): + # value: (tag: str, (islist: bool, data: *)) + + # if key already exists, pop old value + for tag_data in self._data.values(): + if key in tag_data: + tag_data.pop(key, None) + break + + tag = value[0] + if tag not in self._data: + + try: + from cachetools import LRUCache + + default_size = 20 + if 'ckpt' in tag: + default_size = 5 + elif tag in ['latent', 'image']: + default_size = 100 + + self._data[tag] = LRUCache(maxsize=self._tag_settings.get(tag, default_size)) + + except (ImportError, ModuleNotFoundError): + # TODO: implement a simple lru dict + self._data[tag] = {} + self._data[tag][key] = value + + def __delitem__(self, key): + for tag_data in self._data.values(): + if key in tag_data: + del tag_data[key] + return + raise KeyError(f'Key `{key}` does not exist') + + def __contains__(self, key): + return any(key in tag_data for tag_data in self._data.values()) + + def items(self): + yield from itertools.chain(*map(lambda x :x.items(), self._data.values())) + + def get(self, key, default=None): + """D.get(k[,d]) -> D[k] if k in D, else d. d defaults to None.""" + for tag_data in self._data.values(): + if key in tag_data: + return tag_data[key] + return default + + def clear(self): + # clear all cache + self._data = {} + +cache_settings = {} +cache = TaggedCache(cache_settings) +cache_count = {} + +def update_cache(k, tag, v): + cache[k] = (tag, v) + cnt = cache_count.get(k) + if cnt is None: + cnt = 0 + cache_count[k] = cnt + else: + cache_count[k] += 1 +def remove_cache(key): + if key == '*': + cache.clear() + cache_count.clear() + elif key in cache: + del cache[key] + cache_count.pop(key, None) + else: + print(f"invalid {key}") diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/chooser.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/chooser.py new file mode 100644 index 0000000000000000000000000000000000000000..139a0645cc171e2b91a22582a496da8dd7372372 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/chooser.py @@ -0,0 +1,153 @@ +from threading import Event + +import torch + +from server import PromptServer +from aiohttp import web +from comfy import model_management as mm +from comfy_execution.graph import ExecutionBlocker +import time + +class ChooserCancelled(Exception): + pass + +def get_chooser_cache(): + """获取选择器缓存""" + if not hasattr(PromptServer.instance, '_easyuse_chooser_node'): + PromptServer.instance._easyuse_chooser_node = {} + return PromptServer.instance._easyuse_chooser_node + +def cleanup_session_data(node_id): + """清理会话数据""" + node_data = get_chooser_cache() + if node_id in node_data: + session_keys = ["event", "selected", "images", "total_count", "cancelled"] + for key in session_keys: + if key in node_data[node_id]: + del node_data[node_id][key] + +def wait_for_chooser(id, images, mode, period=0.1): + try: + node_data = get_chooser_cache() + images = [images[i:i + 1, ...] for i in range(images.shape[0])] + if mode == "Keep Last Selection": + if id in node_data and "last_selection" in node_data[id]: + last_selection = node_data[id]["last_selection"] + if last_selection and len(last_selection) > 0: + valid_indices = [idx for idx in last_selection if 0 <= idx < len(images)] + if valid_indices: + try: + PromptServer.instance.send_sync("easyuse-image-keep-selection", { + "id": id, + "selected": valid_indices + }) + except Exception as e: + pass + cleanup_session_data(id) + indices_str = ','.join(str(i) for i in valid_indices) + images = [images[idx] for idx in valid_indices] + images = torch.cat(images, dim=0) + return {"result": (images,)} + + if id in node_data: + del node_data[id] + + event = Event() + node_data[id] = { + "event": event, + "images": images, + "selected": None, + "total_count": len(images), + "cancelled": False, + } + + while id in node_data: + node_info = node_data[id] + if node_info.get("cancelled", False): + cleanup_session_data(id) + raise ChooserCancelled("Manual selection cancelled") + + if "selected" in node_info and node_info["selected"] is not None: + break + + time.sleep(period) + + if id in node_data: + node_info = node_data[id] + selected_indices = node_info.get("selected") + + if selected_indices is not None and len(selected_indices) > 0: + valid_indices = [idx for idx in selected_indices if 0 <= idx < len(images)] + if valid_indices: + selected_images = [images[idx] for idx in valid_indices] + + if id not in node_data: + node_data[id] = {} + node_data[id]["last_selection"] = valid_indices + cleanup_session_data(id) + selected_images = torch.cat(selected_images, dim=0) + return {"result": (selected_images,)} + else: + cleanup_session_data(id) + return {"result": (images[0] if len(images) > 0 else ExecutionBlocker(None),)} + else: + cleanup_session_data(id) + return { + "result": (images[0] if len(images) > 0 else ExecutionBlocker(None),)} + else: + return {"result": (images[0] if len(images) > 0 else ExecutionBlocker(None),)} + + except ChooserCancelled: + raise mm.InterruptProcessingException() + except Exception as e: + node_data = get_chooser_cache() + if id in node_data: + cleanup_session_data(id) + if 'image_list' in locals() and len(images) > 0: + return {"result": (images[0])} + else: + return {"result": (ExecutionBlocker(None),)} + + +@PromptServer.instance.routes.post('/easyuse/image_chooser_message') +async def handle_image_selection(request): + try: + data = await request.json() + node_id = data.get("node_id") + selected = data.get("selected", []) + action = data.get("action") + + node_data = get_chooser_cache() + + if node_id not in node_data: + return web.json_response({"code": -1, "error": "Node data does not exist"}) + + try: + node_info = node_data[node_id] + + if "total_count" not in node_info: + return web.json_response({"code": -1, "error": "The node has been processed"}) + + if action == "cancel": + node_info["cancelled"] = True + node_info["selected"] = [] + elif action == "select" and isinstance(selected, list): + valid_indices = [idx for idx in selected if isinstance(idx, int) and 0 <= idx < node_info["total_count"]] + if valid_indices: + node_info["selected"] = valid_indices + node_info["cancelled"] = False + else: + return web.json_response({"code": -1, "error": "Invalid Selection Index"}) + else: + return web.json_response({"code": -1, "error": "Invalid operation"}) + + node_info["event"].set() + return web.json_response({"code": 1}) + + except Exception as e: + if node_id in node_data and "event" in node_data[node_id]: + node_data[node_id]["event"].set() + return web.json_response({"code": -1, "message": "Processing Failed"}) + + except Exception as e: + return web.json_response({"code": -1, "message": "Request Failed"}) diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/colorfix.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/colorfix.py new file mode 100644 index 0000000000000000000000000000000000000000..7792e7c1410083a4ff42175ddbd01638ca47675b --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/colorfix.py @@ -0,0 +1,115 @@ +import torch +from PIL import Image +from torch import Tensor +from torch.nn import functional as F + +from torchvision.transforms import ToTensor, ToPILImage + +def adain_color_fix(target: Image, source: Image): + # Convert images to tensors + to_tensor = ToTensor() + target_tensor = to_tensor(target).unsqueeze(0) + source_tensor = to_tensor(source).unsqueeze(0) + + # Apply adaptive instance normalization + result_tensor = adaptive_instance_normalization(target_tensor, source_tensor) + + # Convert tensor back to image + to_image = ToPILImage() + result_image = to_image(result_tensor.squeeze(0).clamp_(0.0, 1.0)) + + return result_image + +def wavelet_color_fix(target: Image, source: Image): + source = source.resize(target.size, resample=Image.Resampling.LANCZOS) + + # Convert images to tensors + to_tensor = ToTensor() + target_tensor = to_tensor(target).unsqueeze(0) + source_tensor = to_tensor(source).unsqueeze(0) + + # Apply wavelet reconstruction + result_tensor = wavelet_reconstruction(target_tensor, source_tensor) + + # Convert tensor back to image + to_image = ToPILImage() + result_image = to_image(result_tensor.squeeze(0).clamp_(0.0, 1.0)) + + return result_image + +def calc_mean_std(feat: Tensor, eps=1e-5): + """Calculate mean and std for adaptive_instance_normalization. + Args: + feat (Tensor): 4D tensor. + eps (float): A small value added to the variance to avoid + divide-by-zero. Default: 1e-5. + """ + size = feat.size() + assert len(size) == 4, 'The input feature should be 4D tensor.' + b, c = size[:2] + feat_var = feat.view(b, c, -1).var(dim=2) + eps + feat_std = feat_var.sqrt().view(b, c, 1, 1) + feat_mean = feat.view(b, c, -1).mean(dim=2).view(b, c, 1, 1) + return feat_mean, feat_std + +def adaptive_instance_normalization(content_feat:Tensor, style_feat:Tensor): + """Adaptive instance normalization. + Adjust the reference features to have the similar color and illuminations + as those in the degradate features. + Args: + content_feat (Tensor): The reference feature. + style_feat (Tensor): The degradate features. + """ + size = content_feat.size() + style_mean, style_std = calc_mean_std(style_feat) + content_mean, content_std = calc_mean_std(content_feat) + normalized_feat = (content_feat - content_mean.expand(size)) / content_std.expand(size) + return normalized_feat * style_std.expand(size) + style_mean.expand(size) + +def wavelet_blur(image: Tensor, radius: int): + """ + Apply wavelet blur to the input tensor. + """ + # input shape: (1, 3, H, W) + # convolution kernel + kernel_vals = [ + [0.0625, 0.125, 0.0625], + [0.125, 0.25, 0.125], + [0.0625, 0.125, 0.0625], + ] + kernel = torch.tensor(kernel_vals, dtype=image.dtype, device=image.device) + # add channel dimensions to the kernel to make it a 4D tensor + kernel = kernel[None, None] + # repeat the kernel across all input channels + kernel = kernel.repeat(3, 1, 1, 1) + image = F.pad(image, (radius, radius, radius, radius), mode='replicate') + # apply convolution + output = F.conv2d(image, kernel, groups=3, dilation=radius) + return output + +def wavelet_decomposition(image: Tensor, levels=5): + """ + Apply wavelet decomposition to the input tensor. + This function only returns the low frequency & the high frequency. + """ + high_freq = torch.zeros_like(image) + for i in range(levels): + radius = 2 ** i + low_freq = wavelet_blur(image, radius) + high_freq += (image - low_freq) + image = low_freq + + return high_freq, low_freq + +def wavelet_reconstruction(content_feat:Tensor, style_feat:Tensor): + """ + Apply wavelet decomposition, so that the content will have the same color as the style. + """ + # calculate the wavelet decomposition of the content feature + content_high_freq, content_low_freq = wavelet_decomposition(content_feat) + del content_low_freq + # calculate the wavelet decomposition of the style feature + style_high_freq, style_low_freq = wavelet_decomposition(style_feat) + del style_high_freq + # reconstruct the content feature with the style's high frequency + return content_high_freq + style_low_freq \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/conditioning.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/conditioning.py new file mode 100644 index 0000000000000000000000000000000000000000..b9d9733aa37d5038121f9ca5c91cc52fa7f8beef --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/conditioning.py @@ -0,0 +1,57 @@ +from .utils import find_wildcards_seed, find_nearest_steps, is_linked_styles_selector +from .log import log_node_warn +from .translate import zh_to_en, has_chinese +from .wildcards import process_with_loras +from .adv_encode import advanced_encode + +from nodes import ConditioningConcat, ConditioningCombine, ConditioningAverage, ConditioningSetTimestepRange, CLIPTextEncode + +def prompt_to_cond(type, model, clip, clip_skip, lora_stack, text, prompt_token_normalization, prompt_weight_interpretation, a1111_prompt_style ,my_unique_id, prompt, easyCache, can_load_lora=True, steps=None, model_type=None): + styles_selector = is_linked_styles_selector(prompt, my_unique_id, type) + title = "Positive encoding" if type == 'positive' else "Negative encoding" + + # Translate cn to en + if model_type not in ['hydit'] and text is not None and has_chinese(text): + text = zh_to_en([text])[0] + + if model_type in ['hydit', 'flux', 'mochi']: + log_node_warn(title + "...") + embeddings_final, = CLIPTextEncode().encode(clip, text) if text is not None else (None,) + + return (embeddings_final, "", model, clip) + + log_node_warn(title + "...") + + positive_seed = find_wildcards_seed(my_unique_id, text, prompt) + model, clip, text, cond_decode, show_prompt, pipe_lora_stack = process_with_loras( + text, model, clip, type, positive_seed, can_load_lora, lora_stack, easyCache) + wildcard_prompt = cond_decode if show_prompt or styles_selector else "" + + clipped = clip.clone() + # 当clip模型不存在t5xxl时,可执行跳过层 + if not hasattr(clip.cond_stage_model, 't5xxl'): + if clip_skip != 0: + clipped.clip_layer(clip_skip) + + steps = steps if steps is not None else find_nearest_steps(my_unique_id, prompt) + return (advanced_encode(clipped, text, prompt_token_normalization, + prompt_weight_interpretation, w_max=1.0, + apply_to_pooled='enable', + a1111_prompt_style=a1111_prompt_style, steps=steps) if text is not None else None, wildcard_prompt, model, clipped) + +def set_cond(old_cond, new_cond, mode, average_strength, old_cond_start, old_cond_end, new_cond_start, new_cond_end): + if not old_cond: + return new_cond + else: + if mode == "replace": + return new_cond + elif mode == "concat": + return ConditioningConcat().concat(new_cond, old_cond)[0] + elif mode == "combine": + return ConditioningCombine().combine(old_cond, new_cond)[0] + elif mode == 'average': + return ConditioningAverage().addWeighted(new_cond, old_cond, average_strength)[0] + elif mode == 'timestep': + cond_1 = ConditioningSetTimestepRange().set_range(old_cond, old_cond_start, old_cond_end)[0] + cond_2 = ConditioningSetTimestepRange().set_range(new_cond, new_cond_start, new_cond_end)[0] + return ConditioningCombine().combine(cond_1, cond_2)[0] \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/controlnet.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/controlnet.py new file mode 100644 index 0000000000000000000000000000000000000000..ca7a1b8adc7f019d491a3379124fdccc7cc82664 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/controlnet.py @@ -0,0 +1,93 @@ +import folder_paths +import comfy.controlnet +import comfy.model_management +from nodes import NODE_CLASS_MAPPINGS + +union_controlnet_types = {"auto": -1, "openpose": 0, "depth": 1, "hed/pidi/scribble/ted": 2, "canny/lineart/anime_lineart/mlsd": 3, "normal": 4, "segment": 5, "tile": 6, "repaint": 7} + +class easyControlnet: + def __init__(self): + pass + + def apply(self, control_net_name, image, positive, negative, strength, start_percent=0, end_percent=1, control_net=None, scale_soft_weights=1, mask=None, union_type=None, easyCache=None, use_cache=True, model=None, vae=None): + if strength == 0: + return (positive, negative) + + # kolors controlnet patch + from ..modules.kolors.loader import is_kolors_model, applyKolorsUnet + if is_kolors_model(model): + from ..modules.kolors.model_patch import patch_controlnet + if control_net is None: + with applyKolorsUnet(): + control_net = easyCache.load_controlnet(control_net_name, scale_soft_weights, use_cache) + control_net = patch_controlnet(model, control_net) + else: + if control_net is None: + if easyCache is not None: + control_net = easyCache.load_controlnet(control_net_name, scale_soft_weights, use_cache) + else: + controlnet_path = folder_paths.get_full_path("controlnet", control_net_name) + control_net = comfy.controlnet.load_controlnet(controlnet_path) + + # union controlnet + if union_type is not None: + control_net = control_net.copy() + type_number = union_controlnet_types[union_type] + if type_number >= 0: + control_net.set_extra_arg("control_type", [type_number]) + else: + control_net.set_extra_arg("control_type", []) + + if mask is not None: + mask = mask.to(self.device) + + if mask is not None and len(mask.shape) < 3: + mask = mask.unsqueeze(0) + + control_hint = image.movedim(-1, 1) + + is_cond = True + if negative is None: + p = [] + for t in positive: + n = [t[0], t[1].copy()] + c_net = control_net.copy().set_cond_hint(control_hint, strength, (start_percent, end_percent)) + if 'control' in t[1]: + c_net.set_previous_controlnet(t[1]['control']) + n[1]['control'] = c_net + n[1]['control_apply_to_uncond'] = True + if mask is not None: + n[1]['mask'] = mask + n[1]['set_area_to_bounds'] = False + p.append(n) + positive = p + else: + cnets = {} + out = [] + for conditioning in [positive, negative]: + c = [] + for t in conditioning: + d = t[1].copy() + + prev_cnet = d.get('control', None) + if prev_cnet in cnets: + c_net = cnets[prev_cnet] + else: + c_net = control_net.copy().set_cond_hint(control_hint, strength, (start_percent, end_percent), vae) + c_net.set_previous_controlnet(prev_cnet) + cnets[prev_cnet] = c_net + + d['control'] = c_net + d['control_apply_to_uncond'] = False + + if mask is not None: + d['mask'] = mask + d['set_area_to_bounds'] = False + + n = [t[0], d] + c.append(n) + out.append(c) + positive = out[0] + negative = out[1] + + return (positive, negative) \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/dynthres_core.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/dynthres_core.py new file mode 100644 index 0000000000000000000000000000000000000000..0c6945031f29bbb3a1486b48db34506cfaedfd54 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/dynthres_core.py @@ -0,0 +1,167 @@ +import torch, math + +######################### DynThresh Core ######################### + +class DynThresh: + + Modes = ["Constant", "Linear Down", "Cosine Down", "Half Cosine Down", "Linear Up", "Cosine Up", "Half Cosine Up", "Power Up", "Power Down", "Linear Repeating", "Cosine Repeating", "Sawtooth"] + Startpoints = ["MEAN", "ZERO"] + Variabilities = ["AD", "STD"] + + def __init__(self, mimic_scale, threshold_percentile, mimic_mode, mimic_scale_min, cfg_mode, cfg_scale_min, sched_val, experiment_mode, max_steps, separate_feature_channels, scaling_startpoint, variability_measure, interpolate_phi): + self.mimic_scale = mimic_scale + self.threshold_percentile = threshold_percentile + self.mimic_mode = mimic_mode + self.cfg_mode = cfg_mode + self.max_steps = max_steps + self.cfg_scale_min = cfg_scale_min + self.mimic_scale_min = mimic_scale_min + self.experiment_mode = experiment_mode + self.sched_val = sched_val + self.sep_feat_channels = separate_feature_channels + self.scaling_startpoint = scaling_startpoint + self.variability_measure = variability_measure + self.interpolate_phi = interpolate_phi + + def interpret_scale(self, scale, mode, min): + scale -= min + max = self.max_steps - 1 + frac = self.step / max + if mode == "Constant": + pass + elif mode == "Linear Down": + scale *= 1.0 - frac + elif mode == "Half Cosine Down": + scale *= math.cos(frac) + elif mode == "Cosine Down": + scale *= math.cos(frac * 1.5707) + elif mode == "Linear Up": + scale *= frac + elif mode == "Half Cosine Up": + scale *= 1.0 - math.cos(frac) + elif mode == "Cosine Up": + scale *= 1.0 - math.cos(frac * 1.5707) + elif mode == "Power Up": + scale *= math.pow(frac, self.sched_val) + elif mode == "Power Down": + scale *= 1.0 - math.pow(frac, self.sched_val) + elif mode == "Linear Repeating": + portion = (frac * self.sched_val) % 1.0 + scale *= (0.5 - portion) * 2 if portion < 0.5 else (portion - 0.5) * 2 + elif mode == "Cosine Repeating": + scale *= math.cos(frac * 6.28318 * self.sched_val) * 0.5 + 0.5 + elif mode == "Sawtooth": + scale *= (frac * self.sched_val) % 1.0 + scale += min + return scale + + def dynthresh(self, cond, uncond, cfg_scale, weights): + mimic_scale = self.interpret_scale(self.mimic_scale, self.mimic_mode, self.mimic_scale_min) + cfg_scale = self.interpret_scale(cfg_scale, self.cfg_mode, self.cfg_scale_min) + # uncond shape is (batch, 4, height, width) + conds_per_batch = cond.shape[0] / uncond.shape[0] + assert conds_per_batch == int(conds_per_batch), "Expected # of conds per batch to be constant across batches" + cond_stacked = cond.reshape((-1, int(conds_per_batch)) + uncond.shape[1:]) + + ### Normal first part of the CFG Scale logic, basically + diff = cond_stacked - uncond.unsqueeze(1) + if weights is not None: + diff = diff * weights + relative = diff.sum(1) + + ### Get the normal result for both mimic and normal scale + mim_target = uncond + relative * mimic_scale + cfg_target = uncond + relative * cfg_scale + ### If we weren't doing mimic scale, we'd just return cfg_target here + + ### Now recenter the values relative to their average rather than absolute, to allow scaling from average + mim_flattened = mim_target.flatten(2) + cfg_flattened = cfg_target.flatten(2) + mim_means = mim_flattened.mean(dim=2).unsqueeze(2) + cfg_means = cfg_flattened.mean(dim=2).unsqueeze(2) + mim_centered = mim_flattened - mim_means + cfg_centered = cfg_flattened - cfg_means + + if self.sep_feat_channels: + if self.variability_measure == 'STD': + mim_scaleref = mim_centered.std(dim=2).unsqueeze(2) + cfg_scaleref = cfg_centered.std(dim=2).unsqueeze(2) + else: # 'AD' + mim_scaleref = mim_centered.abs().max(dim=2).values.unsqueeze(2) + cfg_scaleref = torch.quantile(cfg_centered.abs(), self.threshold_percentile, dim=2).unsqueeze(2) + + else: + if self.variability_measure == 'STD': + mim_scaleref = mim_centered.std() + cfg_scaleref = cfg_centered.std() + else: # 'AD' + mim_scaleref = mim_centered.abs().max() + cfg_scaleref = torch.quantile(cfg_centered.abs(), self.threshold_percentile) + + if self.scaling_startpoint == 'ZERO': + scaling_factor = mim_scaleref / cfg_scaleref + result = cfg_flattened * scaling_factor + + else: # 'MEAN' + if self.variability_measure == 'STD': + cfg_renormalized = (cfg_centered / cfg_scaleref) * mim_scaleref + else: # 'AD' + ### Get the maximum value of all datapoints (with an optional threshold percentile on the uncond) + max_scaleref = torch.maximum(mim_scaleref, cfg_scaleref) + ### Clamp to the max + cfg_clamped = cfg_centered.clamp(-max_scaleref, max_scaleref) + ### Now shrink from the max to normalize and grow to the mimic scale (instead of the CFG scale) + cfg_renormalized = (cfg_clamped / max_scaleref) * mim_scaleref + + ### Now add it back onto the averages to get into real scale again and return + result = cfg_renormalized + cfg_means + + actual_res = result.unflatten(2, mim_target.shape[2:]) + + if self.interpolate_phi != 1.0: + actual_res = actual_res * self.interpolate_phi + cfg_target * (1.0 - self.interpolate_phi) + + if self.experiment_mode == 1: + num = actual_res.cpu().numpy() + for y in range(0, 64): + for x in range (0, 64): + if num[0][0][y][x] > 1.0: + num[0][1][y][x] *= 0.5 + if num[0][1][y][x] > 1.0: + num[0][1][y][x] *= 0.5 + if num[0][2][y][x] > 1.5: + num[0][2][y][x] *= 0.5 + actual_res = torch.from_numpy(num).to(device=uncond.device) + elif self.experiment_mode == 2: + num = actual_res.cpu().numpy() + for y in range(0, 64): + for x in range (0, 64): + over_scale = False + for z in range(0, 4): + if abs(num[0][z][y][x]) > 1.5: + over_scale = True + if over_scale: + for z in range(0, 4): + num[0][z][y][x] *= 0.7 + actual_res = torch.from_numpy(num).to(device=uncond.device) + elif self.experiment_mode == 3: + coefs = torch.tensor([ + # R G B W + [0.298, 0.207, 0.208, 0.0], # L1 + [0.187, 0.286, 0.173, 0.0], # L2 + [-0.158, 0.189, 0.264, 0.0], # L3 + [-0.184, -0.271, -0.473, 1.0], # L4 + ], device=uncond.device) + res_rgb = torch.einsum("laxy,ab -> lbxy", actual_res, coefs) + max_r, max_g, max_b, max_w = res_rgb[0][0].max(), res_rgb[0][1].max(), res_rgb[0][2].max(), res_rgb[0][3].max() + max_rgb = max(max_r, max_g, max_b) + print(f"test max = r={max_r}, g={max_g}, b={max_b}, w={max_w}, rgb={max_rgb}") + if self.step / (self.max_steps - 1) > 0.2: + if max_rgb < 2.0 and max_w < 3.0: + res_rgb /= max_rgb / 2.4 + else: + if max_rgb > 2.4 and max_w > 3.0: + res_rgb /= max_rgb / 2.4 + actual_res = torch.einsum("laxy,ab -> lbxy", res_rgb, coefs.inverse()) + + return actual_res \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/easing.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/easing.py new file mode 100644 index 0000000000000000000000000000000000000000..db300c93e3764f2ce30b590c9f559d6936628265 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/easing.py @@ -0,0 +1,27 @@ +@staticmethod +def easyIn(t: float)-> float: + return t*t +@staticmethod +def easyOut(t: float)-> float: + return -(t * (t - 2)) +@staticmethod +def easyInOut(t: float)-> float: + if t < 0.5: + return 2*t*t + else: + return (-2*t*t) + (4*t) - 1 + +class EasingBase: + + def easing(self, t: float, function='linear') -> float: + if function == 'easyIn': + return easyIn(t) + elif function == 'easyOut': + return easyOut(t) + elif function == 'easyInOut': + return easyInOut(t) + else: + return t + + def ease(self, start, end, t) -> float: + return end * t + start * (1 - t) \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/gradual_latent_hires_fix.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/gradual_latent_hires_fix.py new file mode 100644 index 0000000000000000000000000000000000000000..82d94704f24d5ed36f733cf78ebdc60bd8eecd51 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/gradual_latent_hires_fix.py @@ -0,0 +1,273 @@ +import torch +from torchvision.transforms.functional import gaussian_blur +from comfy.k_diffusion.sampling import default_noise_sampler, get_ancestral_step, to_d, BrownianTreeNoiseSampler +from tqdm.auto import trange + +@torch.no_grad() +def sample_euler_ancestral( + model, + x, + sigmas, + extra_args=None, + callback=None, + disable=None, + eta=1.0, + s_noise=1.0, + noise_sampler=None, + upscale_ratio=2.0, + start_step=5, + end_step=15, + upscale_n_step=3, + unsharp_kernel_size=3, + unsharp_sigma=0.5, + unsharp_strength=0.0, +): + """Ancestral sampling with Euler method steps.""" + extra_args = {} if extra_args is None else extra_args + noise_sampler = default_noise_sampler(x) if noise_sampler is None else noise_sampler + s_in = x.new_ones([x.shape[0]]) + + # make upscale info + upscale_steps = [] + step = start_step - 1 + while step < end_step - 1: + upscale_steps.append(step) + step += upscale_n_step + height, width = x.shape[2:] + upscale_shapes = [ + (int(height * (((upscale_ratio - 1) / i) + 1)), int(width * (((upscale_ratio - 1) / i) + 1))) + for i in reversed(range(1, len(upscale_steps) + 1)) + ] + upscale_info = {k: v for k, v in zip(upscale_steps, upscale_shapes)} + + for i in trange(len(sigmas) - 1, disable=disable): + denoised = model(x, sigmas[i] * s_in, **extra_args) + sigma_down, sigma_up = get_ancestral_step(sigmas[i], sigmas[i + 1], eta=eta) + if callback is not None: + callback({"x": x, "i": i, "sigma": sigmas[i], "sigma_hat": sigmas[i], "denoised": denoised}) + d = to_d(x, sigmas[i], denoised) + # Euler method + dt = sigma_down - sigmas[i] + x = x + d * dt + if sigmas[i + 1] > 0: + # Resize + if i in upscale_info: + x = torch.nn.functional.interpolate(x, size=upscale_info[i], mode="bicubic", align_corners=False) + if unsharp_strength > 0: + blurred = gaussian_blur(x, kernel_size=unsharp_kernel_size, sigma=unsharp_sigma) + x = x + unsharp_strength * (x - blurred) + + noise_sampler = default_noise_sampler(x) + noise = noise_sampler(sigmas[i], sigmas[i + 1]) + x = x + noise * sigma_up * s_noise + return x + + +@torch.no_grad() +def sample_dpmpp_2s_ancestral( + model, + x, + sigmas, + extra_args=None, + callback=None, + disable=None, + eta=1.0, + s_noise=1.0, + noise_sampler=None, + upscale_ratio=2.0, + start_step=5, + end_step=15, + upscale_n_step=3, + unsharp_kernel_size=3, + unsharp_sigma=0.5, + unsharp_strength=0.0, +): + """Ancestral sampling with DPM-Solver++(2S) second-order steps.""" + extra_args = {} if extra_args is None else extra_args + s_in = x.new_ones([x.shape[0]]) + sigma_fn = lambda t: t.neg().exp() + t_fn = lambda sigma: sigma.log().neg() + + # make upscale info + upscale_steps = [] + step = start_step - 1 + while step < end_step - 1: + upscale_steps.append(step) + step += upscale_n_step + height, width = x.shape[2:] + upscale_shapes = [ + (int(height * (((upscale_ratio - 1) / i) + 1)), int(width * (((upscale_ratio - 1) / i) + 1))) + for i in reversed(range(1, len(upscale_steps) + 1)) + ] + upscale_info = {k: v for k, v in zip(upscale_steps, upscale_shapes)} + + for i in trange(len(sigmas) - 1, disable=disable): + denoised = model(x, sigmas[i] * s_in, **extra_args) + sigma_down, sigma_up = get_ancestral_step(sigmas[i], sigmas[i + 1], eta=eta) + if callback is not None: + callback({"x": x, "i": i, "sigma": sigmas[i], "sigma_hat": sigmas[i], "denoised": denoised}) + if sigma_down == 0: + # Euler method + d = to_d(x, sigmas[i], denoised) + dt = sigma_down - sigmas[i] + x = x + d * dt + else: + # DPM-Solver++(2S) + t, t_next = t_fn(sigmas[i]), t_fn(sigma_down) + r = 1 / 2 + h = t_next - t + s = t + r * h + x_2 = (sigma_fn(s) / sigma_fn(t)) * x - (-h * r).expm1() * denoised + denoised_2 = model(x_2, sigma_fn(s) * s_in, **extra_args) + x = (sigma_fn(t_next) / sigma_fn(t)) * x - (-h).expm1() * denoised_2 + # Noise addition + if sigmas[i + 1] > 0: + # Resize + if i in upscale_info: + x = torch.nn.functional.interpolate(x, size=upscale_info[i], mode="bicubic", align_corners=False) + if unsharp_strength > 0: + blurred = gaussian_blur(x, kernel_size=unsharp_kernel_size, sigma=unsharp_sigma) + x = x + unsharp_strength * (x - blurred) + noise_sampler = default_noise_sampler(x) + noise = noise_sampler(sigmas[i], sigmas[i + 1]) + x = x + noise * sigma_up * s_noise + return x + + +@torch.no_grad() +def sample_dpmpp_2m_sde( + model, + x, + sigmas, + extra_args=None, + callback=None, + disable=None, + eta=1.0, + s_noise=1.0, + noise_sampler=None, + solver_type="midpoint", + upscale_ratio=2.0, + start_step=5, + end_step=15, + upscale_n_step=3, + unsharp_kernel_size=3, + unsharp_sigma=0.5, + unsharp_strength=0.0, +): + """DPM-Solver++(2M) SDE.""" + + if solver_type not in {"heun", "midpoint"}: + raise ValueError("solver_type must be 'heun' or 'midpoint'") + + seed = extra_args.get("seed", None) + sigma_min, sigma_max = sigmas[sigmas > 0].min(), sigmas.max() + extra_args = {} if extra_args is None else extra_args + s_in = x.new_ones([x.shape[0]]) + + old_denoised = None + h_last = None + h = None + + # make upscale info + upscale_steps = [] + step = start_step - 1 + while step < end_step - 1: + upscale_steps.append(step) + step += upscale_n_step + height, width = x.shape[2:] + upscale_shapes = [ + (int(height * (((upscale_ratio - 1) / i) + 1)), int(width * (((upscale_ratio - 1) / i) + 1))) + for i in reversed(range(1, len(upscale_steps) + 1)) + ] + upscale_info = {k: v for k, v in zip(upscale_steps, upscale_shapes)} + + for i in trange(len(sigmas) - 1, disable=disable): + denoised = model(x, sigmas[i] * s_in, **extra_args) + if callback is not None: + callback({"x": x, "i": i, "sigma": sigmas[i], "sigma_hat": sigmas[i], "denoised": denoised}) + if sigmas[i + 1] == 0: + # Denoising step + x = denoised + else: + # DPM-Solver++(2M) SDE + t, s = -sigmas[i].log(), -sigmas[i + 1].log() + h = s - t + eta_h = eta * h + + x = sigmas[i + 1] / sigmas[i] * (-eta_h).exp() * x + (-h - eta_h).expm1().neg() * denoised + + if old_denoised is not None: + r = h_last / h + if solver_type == "heun": + x = x + ((-h - eta_h).expm1().neg() / (-h - eta_h) + 1) * (1 / r) * (denoised - old_denoised) + elif solver_type == "midpoint": + x = x + 0.5 * (-h - eta_h).expm1().neg() * (1 / r) * (denoised - old_denoised) + + if eta: + # Resize + if i in upscale_info: + x = torch.nn.functional.interpolate(x, size=upscale_info[i], mode="bicubic", align_corners=False) + if unsharp_strength > 0: + blurred = gaussian_blur(x, kernel_size=unsharp_kernel_size, sigma=unsharp_sigma) + x = x + unsharp_strength * (x - blurred) + denoised = None # 次ステップとサイズがあわないのでとりあえずNoneにしておく。 + noise_sampler = BrownianTreeNoiseSampler(x, sigma_min, sigma_max, seed=seed, cpu=True) + x = x + noise_sampler(sigmas[i], sigmas[i + 1]) * sigmas[i + 1] * (-2 * eta_h).expm1().neg().sqrt() * s_noise + + old_denoised = denoised + h_last = h + return x + + +@torch.no_grad() +def sample_lcm( + model, + x, + sigmas, + extra_args=None, + callback=None, + disable=None, + noise_sampler=None, + eta=None, + s_noise=None, + upscale_ratio=2.0, + start_step=5, + end_step=15, + upscale_n_step=3, + unsharp_kernel_size=3, + unsharp_sigma=0.5, + unsharp_strength=0.0, +): + extra_args = {} if extra_args is None else extra_args + s_in = x.new_ones([x.shape[0]]) + + # make upscale info + upscale_steps = [] + step = start_step - 1 + while step < end_step - 1: + upscale_steps.append(step) + step += upscale_n_step + height, width = x.shape[2:] + upscale_shapes = [ + (int(height * (((upscale_ratio - 1) / i) + 1)), int(width * (((upscale_ratio - 1) / i) + 1))) + for i in reversed(range(1, len(upscale_steps) + 1)) + ] + upscale_info = {k: v for k, v in zip(upscale_steps, upscale_shapes)} + + for i in trange(len(sigmas) - 1, disable=disable): + denoised = model(x, sigmas[i] * s_in, **extra_args) + if callback is not None: + callback({"x": x, "i": i, "sigma": sigmas[i], "sigma_hat": sigmas[i], "denoised": denoised}) + + x = denoised + if sigmas[i + 1] > 0: + # Resize + if i in upscale_info: + x = torch.nn.functional.interpolate(x, size=upscale_info[i], mode="bicubic", align_corners=False) + if unsharp_strength > 0: + blurred = gaussian_blur(x, kernel_size=unsharp_kernel_size, sigma=unsharp_sigma) + x = x + unsharp_strength * (x - blurred) + noise_sampler = default_noise_sampler(x) + x += sigmas[i + 1] * noise_sampler(sigmas[i], sigmas[i + 1]) + + return x diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/image.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/image.py new file mode 100644 index 0000000000000000000000000000000000000000..1d9dc8181ae3708950838e62206400d440f585ad --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/image.py @@ -0,0 +1,227 @@ +import os +import base64 +import torch +import numpy as np +from enum import Enum +from PIL import Image +from io import BytesIO +from typing import List, Union + +import folder_paths +from .utils import install_package + +# PIL to Tensor +def pil2tensor(image): + return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0) +# Tensor to PIL +def tensor2pil(image): + return Image.fromarray(np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8)) +# np to Tensor +def np2tensor(img_np: Union[np.ndarray, List[np.ndarray]]) -> torch.Tensor: + if isinstance(img_np, list): + return torch.cat([np2tensor(img) for img in img_np], dim=0) + return torch.from_numpy(img_np.astype(np.float32) / 255.0).unsqueeze(0) +# Tensor to np +def tensor2np(tensor: torch.Tensor) -> List[np.ndarray]: + if len(tensor.shape) == 3: # Single image + return np.clip(255.0 * tensor.cpu().numpy(), 0, 255).astype(np.uint8) + else: # Batch of images + return [np.clip(255.0 * t.cpu().numpy(), 0, 255).astype(np.uint8) for t in tensor] + +def pil2byte(pil_image, format='PNG'): + byte_arr = BytesIO() + pil_image.save(byte_arr, format=format) + byte_arr.seek(0) + return byte_arr + +def image2base64(image_base64): + image_bytes = base64.b64decode(image_base64) + image_data = Image.open(BytesIO(image_bytes)) + return image_data + +# Get new bounds +def get_new_bounds(width, height, left, right, top, bottom): + """Returns the new bounds for an image with inset crop data.""" + left = 0 + left + right = width - right + top = 0 + top + bottom = height - bottom + return (left, right, top, bottom) + +def RGB2RGBA(image: Image, mask: Image) -> Image: + (R, G, B) = image.convert('RGB').split() + return Image.merge('RGBA', (R, G, B, mask.convert('L'))) + +def image2mask(image: Image) -> torch.Tensor: + _image = image.convert('RGBA') + alpha = _image.split()[0] + bg = Image.new("L", _image.size) + _image = Image.merge('RGBA', (bg, bg, bg, alpha)) + ret_mask = torch.tensor([pil2tensor(_image)[0, :, :, 3].tolist()]) + return ret_mask + +def mask2image(mask: torch.Tensor) -> Image: + masks = tensor2np(mask) + for m in masks: + _mask = Image.fromarray(m).convert("L") + _image = Image.new("RGBA", _mask.size, color='white') + _image = Image.composite( + _image, Image.new("RGBA", _mask.size, color='black'), _mask) + return _image + +# 图像融合 +class blendImage: + def g(self, x): + return torch.where(x <= 0.25, ((16 * x - 12) * x + 4) * x, torch.sqrt(x)) + + def blend_mode(self, img1, img2, mode): + if mode == "normal": + return img2 + elif mode == "multiply": + return img1 * img2 + elif mode == "screen": + return 1 - (1 - img1) * (1 - img2) + elif mode == "overlay": + return torch.where(img1 <= 0.5, 2 * img1 * img2, 1 - 2 * (1 - img1) * (1 - img2)) + elif mode == "soft_light": + return torch.where(img2 <= 0.5, img1 - (1 - 2 * img2) * img1 * (1 - img1), + img1 + (2 * img2 - 1) * (self.g(img1) - img1)) + elif mode == "difference": + return img1 - img2 + else: + raise ValueError(f"Unsupported blend mode: {mode}") + + def blend_images(self, image1: torch.Tensor, image2: torch.Tensor, blend_factor: float, blend_mode: str = 'normal'): + image2 = image2.to(image1.device) + if image1.shape != image2.shape: + image2 = image2.permute(0, 3, 1, 2) + image2 = comfy.utils.common_upscale(image2, image1.shape[2], image1.shape[1], upscale_method='bicubic', + crop='center') + image2 = image2.permute(0, 2, 3, 1) + + blended_image = self.blend_mode(image1, image2, blend_mode) + blended_image = image1 * (1 - blend_factor) + blended_image * blend_factor + blended_image = torch.clamp(blended_image, 0, 1) + return blended_image + + +def empty_image(width, height, batch_size=1, color=0): + r = torch.full([batch_size, height, width, 1], ((color >> 16) & 0xFF) / 0xFF) + g = torch.full([batch_size, height, width, 1], ((color >> 8) & 0xFF) / 0xFF) + b = torch.full([batch_size, height, width, 1], ((color) & 0xFF) / 0xFF) + return torch.cat((r, g, b), dim=-1) + + +class ResizeMode(Enum): + RESIZE = "Just Resize" + INNER_FIT = "Crop and Resize" + OUTER_FIT = "Resize and Fill" + def int_value(self): + if self == ResizeMode.RESIZE: + return 0 + elif self == ResizeMode.INNER_FIT: + return 1 + elif self == ResizeMode.OUTER_FIT: + return 2 + assert False, "NOTREACHED" + +# credit by https://github.com/chflame163/ComfyUI_LayerStyle/blob/main/py/imagefunc.py#L591C1-L617C22 +def fit_resize_image(image: Image, target_width: int, target_height: int, fit: str, resize_sampler: str, + background_color: str = '#000000') -> Image: + image = image.convert('RGB') + orig_width, orig_height = image.size + if image is not None: + if fit == 'letterbox': + if orig_width / orig_height > target_width / target_height: # 更宽,上下留黑 + fit_width = target_width + fit_height = int(target_width / orig_width * orig_height) + else: # 更瘦,左右留黑 + fit_height = target_height + fit_width = int(target_height / orig_height * orig_width) + fit_image = image.resize((fit_width, fit_height), resize_sampler) + ret_image = Image.new('RGB', size=(target_width, target_height), color=background_color) + ret_image.paste(fit_image, box=((target_width - fit_width) // 2, (target_height - fit_height) // 2)) + elif fit == 'crop': + if orig_width / orig_height > target_width / target_height: # 更宽,裁左右 + fit_width = int(orig_height * target_width / target_height) + fit_image = image.crop( + ((orig_width - fit_width) // 2, 0, (orig_width - fit_width) // 2 + fit_width, orig_height)) + else: # 更瘦,裁上下 + fit_height = int(orig_width * target_height / target_width) + fit_image = image.crop( + (0, (orig_height - fit_height) // 2, orig_width, (orig_height - fit_height) // 2 + fit_height)) + ret_image = fit_image.resize((target_width, target_height), resize_sampler) + else: + ret_image = image.resize((target_width, target_height), resize_sampler) + return ret_image + +# CLIP反推 +import comfy.utils +from torchvision import transforms +Config, Interrogator = None, None +class CI_Inference: + ci_model = None + cache_path: str + + def __init__(self): + self.ci_model = None + self.low_vram = False + self.cache_path = os.path.join(folder_paths.models_dir, "clip_interrogator") + + def _load_model(self, model_name, low_vram=False): + if not (self.ci_model and model_name == self.ci_model.config.clip_model_name and self.low_vram == low_vram): + self.low_vram = low_vram + print(f"Load model: {model_name}") + + config = Config( + device="cuda" if torch.cuda.is_available() else "cpu", + download_cache=True, + clip_model_name=model_name, + clip_model_path=self.cache_path, + cache_path=self.cache_path, + caption_model_name='blip-large' + ) + + if low_vram: + config.apply_low_vram_defaults() + + self.ci_model = Interrogator(config) + + def _interrogate(self, image, mode, caption=None): + if mode == 'best': + prompt = self.ci_model.interrogate(image, caption=caption) + elif mode == 'classic': + prompt = self.ci_model.interrogate_classic(image, caption=caption) + elif mode == 'fast': + prompt = self.ci_model.interrogate_fast(image, caption=caption) + elif mode == 'negative': + prompt = self.ci_model.interrogate_negative(image) + else: + raise Exception(f"Unknown mode {mode}") + return prompt + + def image_to_prompt(self, image, mode, model_name='ViT-L-14/openai', low_vram=False): + try: + from clip_interrogator import Config, Interrogator + global Config, Interrogator + except: + install_package("clip_interrogator", "0.6.0") + from clip_interrogator import Config, Interrogator + + pbar = comfy.utils.ProgressBar(len(image)) + + self._load_model(model_name, low_vram) + prompt = [] + for i in range(len(image)): + im = image[i] + + im = tensor2pil(im) + im = im.convert('RGB') + + _prompt = self._interrogate(im, mode) + pbar.update(1) + prompt.append(_prompt) + + return prompt + +ci = CI_Inference() diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/lllite.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/lllite.py new file mode 100644 index 0000000000000000000000000000000000000000..5fd686ebb3356da497bb78fdd7287e46cabc176a --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/lllite.py @@ -0,0 +1,237 @@ +import math +import torch +import comfy + + +def extra_options_to_module_prefix(extra_options): + # extra_options = {'transformer_index': 2, 'block_index': 8, 'original_shape': [2, 4, 128, 128], 'block': ('input', 7), 'n_heads': 20, 'dim_head': 64} + + # block is: [('input', 4), ('input', 5), ('input', 7), ('input', 8), ('middle', 0), + # ('output', 0), ('output', 1), ('output', 2), ('output', 3), ('output', 4), ('output', 5)] + # transformer_index is: [0, 1, 2, 3, 4, 5, 6, 7, 8], for each block + # block_index is: 0-1 or 0-9, depends on the block + # input 7 and 8, middle has 10 blocks + + # make module name from extra_options + block = extra_options["block"] + block_index = extra_options["block_index"] + if block[0] == "input": + module_pfx = f"lllite_unet_input_blocks_{block[1]}_1_transformer_blocks_{block_index}" + elif block[0] == "middle": + module_pfx = f"lllite_unet_middle_block_1_transformer_blocks_{block_index}" + elif block[0] == "output": + module_pfx = f"lllite_unet_output_blocks_{block[1]}_1_transformer_blocks_{block_index}" + else: + raise Exception("invalid block name") + return module_pfx + + +def load_control_net_lllite_patch(path, cond_image, multiplier, num_steps, start_percent, end_percent): + # calculate start and end step + start_step = math.floor(num_steps * start_percent * 0.01) if start_percent > 0 else 0 + end_step = math.floor(num_steps * end_percent * 0.01) if end_percent > 0 else num_steps + + # load weights + ctrl_sd = comfy.utils.load_torch_file(path, safe_load=True) + + # split each weights for each module + module_weights = {} + for key, value in ctrl_sd.items(): + fragments = key.split(".") + module_name = fragments[0] + weight_name = ".".join(fragments[1:]) + + if module_name not in module_weights: + module_weights[module_name] = {} + module_weights[module_name][weight_name] = value + + # load each module + modules = {} + for module_name, weights in module_weights.items(): + # ここの自動判定を何とかしたい + if "conditioning1.4.weight" in weights: + depth = 3 + elif weights["conditioning1.2.weight"].shape[-1] == 4: + depth = 2 + else: + depth = 1 + + module = LLLiteModule( + name=module_name, + is_conv2d=weights["down.0.weight"].ndim == 4, + in_dim=weights["down.0.weight"].shape[1], + depth=depth, + cond_emb_dim=weights["conditioning1.0.weight"].shape[0] * 2, + mlp_dim=weights["down.0.weight"].shape[0], + multiplier=multiplier, + num_steps=num_steps, + start_step=start_step, + end_step=end_step, + ) + info = module.load_state_dict(weights) + modules[module_name] = module + if len(modules) == 1: + module.is_first = True + + print(f"loaded {path} successfully, {len(modules)} modules") + + # cond imageをセットする + cond_image = cond_image.permute(0, 3, 1, 2) # b,h,w,3 -> b,3,h,w + cond_image = cond_image * 2.0 - 1.0 # 0-1 -> -1-+1 + + for module in modules.values(): + module.set_cond_image(cond_image) + + class control_net_lllite_patch: + def __init__(self, modules): + self.modules = modules + + def __call__(self, q, k, v, extra_options): + module_pfx = extra_options_to_module_prefix(extra_options) + + is_attn1 = q.shape[-1] == k.shape[-1] # self attention + if is_attn1: + module_pfx = module_pfx + "_attn1" + else: + module_pfx = module_pfx + "_attn2" + + module_pfx_to_q = module_pfx + "_to_q" + module_pfx_to_k = module_pfx + "_to_k" + module_pfx_to_v = module_pfx + "_to_v" + + if module_pfx_to_q in self.modules: + q = q + self.modules[module_pfx_to_q](q) + if module_pfx_to_k in self.modules: + k = k + self.modules[module_pfx_to_k](k) + if module_pfx_to_v in self.modules: + v = v + self.modules[module_pfx_to_v](v) + + return q, k, v + + def to(self, device): + for d in self.modules.keys(): + self.modules[d] = self.modules[d].to(device) + return self + + return control_net_lllite_patch(modules) + +class LLLiteModule(torch.nn.Module): + def __init__( + self, + name: str, + is_conv2d: bool, + in_dim: int, + depth: int, + cond_emb_dim: int, + mlp_dim: int, + multiplier: int, + num_steps: int, + start_step: int, + end_step: int, + ): + super().__init__() + self.name = name + self.is_conv2d = is_conv2d + self.multiplier = multiplier + self.num_steps = num_steps + self.start_step = start_step + self.end_step = end_step + self.is_first = False + + modules = [] + modules.append(torch.nn.Conv2d(3, cond_emb_dim // 2, kernel_size=4, stride=4, padding=0)) # to latent (from VAE) size*2 + if depth == 1: + modules.append(torch.nn.ReLU(inplace=True)) + modules.append(torch.nn.Conv2d(cond_emb_dim // 2, cond_emb_dim, kernel_size=2, stride=2, padding=0)) + elif depth == 2: + modules.append(torch.nn.ReLU(inplace=True)) + modules.append(torch.nn.Conv2d(cond_emb_dim // 2, cond_emb_dim, kernel_size=4, stride=4, padding=0)) + elif depth == 3: + # kernel size 8は大きすぎるので、4にする / kernel size 8 is too large, so set it to 4 + modules.append(torch.nn.ReLU(inplace=True)) + modules.append(torch.nn.Conv2d(cond_emb_dim // 2, cond_emb_dim // 2, kernel_size=4, stride=4, padding=0)) + modules.append(torch.nn.ReLU(inplace=True)) + modules.append(torch.nn.Conv2d(cond_emb_dim // 2, cond_emb_dim, kernel_size=2, stride=2, padding=0)) + + self.conditioning1 = torch.nn.Sequential(*modules) + + if self.is_conv2d: + self.down = torch.nn.Sequential( + torch.nn.Conv2d(in_dim, mlp_dim, kernel_size=1, stride=1, padding=0), + torch.nn.ReLU(inplace=True), + ) + self.mid = torch.nn.Sequential( + torch.nn.Conv2d(mlp_dim + cond_emb_dim, mlp_dim, kernel_size=1, stride=1, padding=0), + torch.nn.ReLU(inplace=True), + ) + self.up = torch.nn.Sequential( + torch.nn.Conv2d(mlp_dim, in_dim, kernel_size=1, stride=1, padding=0), + ) + else: + self.down = torch.nn.Sequential( + torch.nn.Linear(in_dim, mlp_dim), + torch.nn.ReLU(inplace=True), + ) + self.mid = torch.nn.Sequential( + torch.nn.Linear(mlp_dim + cond_emb_dim, mlp_dim), + torch.nn.ReLU(inplace=True), + ) + self.up = torch.nn.Sequential( + torch.nn.Linear(mlp_dim, in_dim), + ) + + self.depth = depth + self.cond_image = None + self.cond_emb = None + self.current_step = 0 + + # @torch.inference_mode() + def set_cond_image(self, cond_image): + # print("set_cond_image", self.name) + self.cond_image = cond_image + self.cond_emb = None + self.current_step = 0 + + def forward(self, x): + if self.num_steps > 0: + if self.current_step < self.start_step: + self.current_step += 1 + return torch.zeros_like(x) + elif self.current_step >= self.end_step: + if self.is_first and self.current_step == self.end_step: + print(f"end LLLite: step {self.current_step}") + self.current_step += 1 + if self.current_step >= self.num_steps: + self.current_step = 0 # reset + return torch.zeros_like(x) + else: + if self.is_first and self.current_step == self.start_step: + print(f"start LLLite: step {self.current_step}") + self.current_step += 1 + if self.current_step >= self.num_steps: + self.current_step = 0 # reset + + if self.cond_emb is None: + # print(f"cond_emb is None, {self.name}") + cx = self.conditioning1(self.cond_image.to(x.device, dtype=x.dtype)) + if not self.is_conv2d: + # reshape / b,c,h,w -> b,h*w,c + n, c, h, w = cx.shape + cx = cx.view(n, c, h * w).permute(0, 2, 1) + self.cond_emb = cx + + cx = self.cond_emb + # print(f"forward {self.name}, {cx.shape}, {x.shape}") + + # uncond/condでxはバッチサイズが2倍 + if x.shape[0] != cx.shape[0]: + if self.is_conv2d: + cx = cx.repeat(x.shape[0] // cx.shape[0], 1, 1, 1) + else: + # print("x.shape[0] != cx.shape[0]", x.shape[0], cx.shape[0]) + cx = cx.repeat(x.shape[0] // cx.shape[0], 1, 1) + + cx = torch.cat([cx, self.down(x)], dim=1 if self.is_conv2d else 2) + cx = self.mid(cx) + cx = self.up(cx) + return cx * self.multiplier \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/loader.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/loader.py new file mode 100644 index 0000000000000000000000000000000000000000..6702ed5aa4534c6215b3d92b84d1e363bff06461 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/loader.py @@ -0,0 +1,570 @@ +import re, time, os, psutil +import folder_paths +import comfy.utils +import comfy.sd +import comfy.controlnet + +from comfy.model_patcher import ModelPatcher +from nodes import NODE_CLASS_MAPPINGS +from collections import defaultdict +from .log import log_node_info, log_node_error +from ..modules.dit.pixArt.loader import load_pixart + +diffusion_loaders = ["easy fullLoader", "easy a1111Loader", "easy fluxLoader", "easy comfyLoader", "easy hunyuanDiTLoader", "easy zero123Loader", "easy svdLoader"] +stable_cascade_loaders = ["easy cascadeLoader"] +dit_loaders = ['easy pixArtLoader'] +controlnet_loaders = ["easy controlnetLoader", "easy controlnetLoaderADV", "easy controlnetLoader++"] +instant_loaders = ["easy instantIDApply", "easy instantIDApplyADV"] +cascade_vae_node = ["easy preSamplingCascade", "easy fullCascadeKSampler"] +model_merge_node = ["easy XYInputs: ModelMergeBlocks"] +lora_widget = ["easy fullLoader", "easy a1111Loader", "easy comfyLoader", "easy fluxLoader"] + +class easyLoader: + def __init__(self): + self.loaded_objects = { + "ckpt": defaultdict(tuple), # {ckpt_name: (model, ...)} + "unet": defaultdict(tuple), + "clip": defaultdict(tuple), + "clip_vision": defaultdict(tuple), + "bvae": defaultdict(tuple), + "vae": defaultdict(object), + "lora": defaultdict(dict), # {lora_name: {UID: (model_lora, clip_lora)}} + "controlnet": defaultdict(dict), + "t5": defaultdict(tuple), + "chatglm3": defaultdict(tuple), + } + self.memory_threshold = self.determine_memory_threshold(1) + self.lora_name_cache = [] + + def clean_values(self, values: str): + original_values = values.split("; ") + cleaned_values = [] + + for value in original_values: + cleaned_value = value.strip(';').strip() + if cleaned_value == "": + continue + try: + cleaned_value = int(cleaned_value) + except ValueError: + try: + cleaned_value = float(cleaned_value) + except ValueError: + pass + cleaned_values.append(cleaned_value) + + return cleaned_values + + def clear_unused_objects(self, desired_names: set, object_type: str): + keys = set(self.loaded_objects[object_type].keys()) + for key in keys - desired_names: + del self.loaded_objects[object_type][key] + + def get_input_value(self, entry, key, prompt=None): + val = entry["inputs"][key] + if isinstance(val, str): + return val + elif isinstance(val, list): + if prompt is not None and val[0]: + return prompt[val[0]]['inputs'][key] + else: + return val[0] + else: + return str(val) + + def process_pipe_loader(self, entry, desired_ckpt_names, desired_vae_names, desired_lora_names, desired_lora_settings, num_loras=3, suffix=""): + for idx in range(1, num_loras + 1): + lora_name_key = f"{suffix}lora{idx}_name" + desired_lora_names.add(self.get_input_value(entry, lora_name_key)) + setting = f'{self.get_input_value(entry, lora_name_key)};{entry["inputs"][f"{suffix}lora{idx}_model_strength"]};{entry["inputs"][f"{suffix}lora{idx}_clip_strength"]}' + desired_lora_settings.add(setting) + + desired_ckpt_names.add(self.get_input_value(entry, f"{suffix}ckpt_name")) + desired_vae_names.add(self.get_input_value(entry, f"{suffix}vae_name")) + + def update_loaded_objects(self, prompt): + desired_ckpt_names = set() + desired_unet_names = set() + desired_clip_names = set() + desired_vae_names = set() + desired_lora_names = set() + desired_lora_settings = set() + desired_controlnet_names = set() + desired_t5_names = set() + desired_glm3_names = set() + + for entry in prompt.values(): + class_type = entry["class_type"] + if class_type in lora_widget: + lora_name = self.get_input_value(entry, "lora_name") + desired_lora_names.add(lora_name) + setting = f'{lora_name};{entry["inputs"]["lora_model_strength"]};{entry["inputs"]["lora_clip_strength"]}' + desired_lora_settings.add(setting) + + if class_type in diffusion_loaders: + desired_ckpt_names.add(self.get_input_value(entry, "ckpt_name", prompt)) + desired_vae_names.add(self.get_input_value(entry, "vae_name")) + + elif class_type in ['easy kolorsLoader']: + desired_unet_names.add(self.get_input_value(entry, "unet_name")) + desired_vae_names.add(self.get_input_value(entry, "vae_name")) + desired_glm3_names.add(self.get_input_value(entry, "chatglm3_name")) + + elif class_type in dit_loaders: + t5_name = self.get_input_value(entry, "mt5_name") if "mt5_name" in entry["inputs"] else None + clip_name = self.get_input_value(entry, "clip_name") if "clip_name" in entry["inputs"] else None + model_name = self.get_input_value(entry, "model_name") + ckpt_name = self.get_input_value(entry, "ckpt_name", prompt) + if t5_name: + desired_t5_names.add(t5_name) + if clip_name: + desired_clip_names.add(clip_name) + desired_ckpt_names.add(ckpt_name+'_'+model_name) + + elif class_type in stable_cascade_loaders: + desired_unet_names.add(self.get_input_value(entry, "stage_c")) + desired_unet_names.add(self.get_input_value(entry, "stage_b")) + desired_clip_names.add(self.get_input_value(entry, "clip_name")) + desired_vae_names.add(self.get_input_value(entry, "stage_a")) + + elif class_type in cascade_vae_node: + encode_vae_name = self.get_input_value(entry, "encode_vae_name") + decode_vae_name = self.get_input_value(entry, "decode_vae_name") + if encode_vae_name and encode_vae_name != 'None': + desired_vae_names.add(encode_vae_name) + if decode_vae_name and decode_vae_name != 'None': + desired_vae_names.add(decode_vae_name) + + elif class_type in controlnet_loaders: + control_net_name = self.get_input_value(entry, "control_net_name", prompt) + scale_soft_weights = self.get_input_value(entry, "scale_soft_weights") + desired_controlnet_names.add(f'{control_net_name};{scale_soft_weights}') + + elif class_type in instant_loaders: + control_net_name = self.get_input_value(entry, "control_net_name", prompt) + scale_soft_weights = self.get_input_value(entry, "cn_soft_weights") + desired_controlnet_names.add(f'{control_net_name};{scale_soft_weights}') + + elif class_type in model_merge_node: + desired_ckpt_names.add(self.get_input_value(entry, "ckpt_name_1")) + desired_ckpt_names.add(self.get_input_value(entry, "ckpt_name_2")) + vae_use = self.get_input_value(entry, "vae_use") + if vae_use != 'Use Model 1' and vae_use != 'Use Model 2': + desired_vae_names.add(vae_use) + + object_types = ["ckpt", "unet", "clip", "bvae", "vae", "lora", "controlnet", "t5"] + for object_type in object_types: + if object_type == 'unet': + desired_names = desired_unet_names + elif object_type in ["ckpt", "clip", "bvae"]: + if object_type == 'clip': + desired_names = desired_ckpt_names.union(desired_clip_names) + else: + desired_names = desired_ckpt_names + elif object_type == "vae": + desired_names = desired_vae_names + elif object_type == "controlnet": + desired_names = desired_controlnet_names + elif object_type == "t5": + desired_names = desired_t5_names + elif object_type == "chatglm3": + desired_names = desired_glm3_names + else: + desired_names = desired_lora_names + self.clear_unused_objects(desired_names, object_type) + + def add_to_cache(self, obj_type, key, value): + """ + Add an item to the cache with the current timestamp. + """ + timestamped_value = (value, time.time()) + self.loaded_objects[obj_type][key] = timestamped_value + + def determine_memory_threshold(self, percentage=0.8): + """ + Determines the memory threshold as a percentage of the total available memory. + Args: + - percentage (float): The fraction of total memory to use as the threshold. + Should be a value between 0 and 1. Default is 0.8 (80%). + Returns: + - memory_threshold (int): Memory threshold in bytes. + """ + total_memory = psutil.virtual_memory().total + memory_threshold = total_memory * percentage + return memory_threshold + + def get_memory_usage(self): + """ + Returns the memory usage of the current process in bytes. + """ + process = psutil.Process(os.getpid()) + return process.memory_info().rss + + def eviction_based_on_memory(self): + """ + Evicts objects from cache based on memory usage and priority. + """ + current_memory = self.get_memory_usage() + if current_memory < self.memory_threshold: + return + eviction_order = ["vae", "lora", "bvae", "clip", "ckpt", "controlnet", "unet", "t5", "chatglm3"] + for obj_type in eviction_order: + if current_memory < self.memory_threshold: + break + # Sort items based on age (using the timestamp) + items = list(self.loaded_objects[obj_type].items()) + items.sort(key=lambda x: x[1][1]) # Sorting by timestamp + + for item in items: + if current_memory < self.memory_threshold: + break + del self.loaded_objects[obj_type][item[0]] + current_memory = self.get_memory_usage() + + def load_checkpoint(self, ckpt_name, config_name=None, load_vision=False): + cache_name = ckpt_name + if config_name not in [None, "Default"]: + cache_name = ckpt_name + "_" + config_name + if cache_name in self.loaded_objects["ckpt"]: + clip_vision = self.loaded_objects["clip_vision"][cache_name][0] if load_vision else None + clip = self.loaded_objects["clip"][cache_name][0] if not load_vision else None + return self.loaded_objects["ckpt"][cache_name][0], clip, self.loaded_objects["bvae"][cache_name][0], clip_vision + + ckpt_path = folder_paths.get_full_path("checkpoints", ckpt_name) + + output_clip = False if load_vision else True + output_clipvision = True if load_vision else False + if config_name not in [None, "Default"]: + config_path = folder_paths.get_full_path("configs", config_name) + loaded_ckpt = comfy.sd.load_checkpoint(config_path, ckpt_path, output_vae=True, output_clip=output_clip, embedding_directory=folder_paths.get_folder_paths("embeddings")) + else: + model_options = {} + if re.search("nf4", ckpt_name): + from ..modules.bitsandbytes_NF4 import OPS + model_options = {"custom_operations": OPS} + loaded_ckpt = comfy.sd.load_checkpoint_guess_config(ckpt_path, output_vae=True, output_clip=output_clip, output_clipvision=output_clipvision, embedding_directory=folder_paths.get_folder_paths("embeddings"), model_options=model_options) + + self.add_to_cache("ckpt", cache_name, loaded_ckpt[0]) + self.add_to_cache("bvae", cache_name, loaded_ckpt[2]) + + clip = loaded_ckpt[1] + clip_vision = loaded_ckpt[3] + if clip: + self.add_to_cache("clip", cache_name, clip) + if clip_vision: + self.add_to_cache("clip_vision", cache_name, clip_vision) + + self.eviction_based_on_memory() + + return loaded_ckpt[0], clip, loaded_ckpt[2], clip_vision + + def load_vae(self, vae_name): + if vae_name in self.loaded_objects["vae"]: + return self.loaded_objects["vae"][vae_name][0] + + vae_path = folder_paths.get_full_path("vae", vae_name) + sd = comfy.utils.load_torch_file(vae_path) + loaded_vae = comfy.sd.VAE(sd=sd) + self.add_to_cache("vae", vae_name, loaded_vae) + self.eviction_based_on_memory() + + return loaded_vae + + def load_unet(self, unet_name): + if unet_name in self.loaded_objects["unet"]: + log_node_info("Load UNet", f"{unet_name} cached") + return self.loaded_objects["unet"][unet_name][0] + + unet_path = folder_paths.get_full_path("unet", unet_name) + model = comfy.sd.load_unet(unet_path) + self.add_to_cache("unet", unet_name, model) + self.eviction_based_on_memory() + + return model + + def load_controlnet(self, control_net_name, scale_soft_weights=1, use_cache=True): + unique_id = f'{control_net_name};{str(scale_soft_weights)}' + if use_cache and unique_id in self.loaded_objects["controlnet"]: + return self.loaded_objects["controlnet"][unique_id][0] + if scale_soft_weights < 1: + if "ScaledSoftControlNetWeights" in NODE_CLASS_MAPPINGS: + soft_weight_cls = NODE_CLASS_MAPPINGS['ScaledSoftControlNetWeights'] + (weights, timestep_keyframe) = soft_weight_cls().load_weights(scale_soft_weights, False) + cn_adv_cls = NODE_CLASS_MAPPINGS['ControlNetLoaderAdvanced'] + control_net, = cn_adv_cls().load_controlnet(control_net_name, timestep_keyframe) + else: + raise Exception(f"[Advanced-ControlNet Not Found] you need to install 'COMFYUI-Advanced-ControlNet'") + else: + controlnet_path = folder_paths.get_full_path("controlnet", control_net_name) + control_net = comfy.controlnet.load_controlnet(controlnet_path) + if use_cache: + self.add_to_cache("controlnet", unique_id, control_net) + self.eviction_based_on_memory() + + return control_net + def load_clip(self, clip_name, type='stable_diffusion', load_clip=None): + if clip_name in self.loaded_objects["clip"]: + return self.loaded_objects["clip"][clip_name][0] + + if type == 'stable_diffusion': + clip_type = comfy.sd.CLIPType.STABLE_DIFFUSION + elif type == 'stable_cascade': + clip_type = comfy.sd.CLIPType.STABLE_CASCADE + elif type == 'sd3': + clip_type = comfy.sd.CLIPType.SD3 + elif type == 'flux': + clip_type = comfy.sd.CLIPType.FLUX + elif type == 'stable_audio': + clip_type = comfy.sd.CLIPType.STABLE_AUDIO + clip_path = folder_paths.get_full_path("clip", clip_name) + load_clip = comfy.sd.load_clip(ckpt_paths=[clip_path], embedding_directory=folder_paths.get_folder_paths("embeddings"), clip_type=clip_type) + self.add_to_cache("clip", clip_name, load_clip) + self.eviction_based_on_memory() + + return load_clip + + def load_lora(self, lora, model=None, clip=None, type=None , use_cache=True): + lora_name = lora["lora_name"] + model = model if model is not None else lora["model"] + clip = clip if clip is not None else lora["clip"] + model_strength = lora["model_strength"] + clip_strength = lora["clip_strength"] + lbw = lora["lbw"] if "lbw" in lora else None + lbw_a = lora["lbw_a"] if "lbw_a" in lora else None + lbw_b = lora["lbw_b"] if "lbw_b" in lora else None + + model_hash = str(model)[44:-1] + clip_hash = str(clip)[25:-1] if clip else '' + + unique_id = f'{model_hash};{clip_hash};{lora_name};{model_strength};{clip_strength}' + + if use_cache and unique_id in self.loaded_objects["lora"]: + log_node_info("Load LORA",f"{lora_name} cached") + return self.loaded_objects["lora"][unique_id][0] + + orig_lora_name = lora_name + lora_name = self.resolve_lora_name(lora_name) + + if lora_name is not None: + lora_path = folder_paths.get_full_path("loras", lora_name) + else: + lora_path = None + + if lora_path is not None: + log_node_info("Load LORA",f"{lora_name}: model={model_strength:.3f}, clip={clip_strength:.3f}, LBW={lbw}, A={lbw_a}, B={lbw_b}") + if lbw: + lbw = lora["lbw"] + lbw_a = lora["lbw_a"] + lbw_b = lora["lbw_b"] + if 'LoraLoaderBlockWeight //Inspire' not in NODE_CLASS_MAPPINGS: + raise Exception('[InspirePack Not Found] you need to install ComfyUI-Inspire-Pack') + cls = NODE_CLASS_MAPPINGS['LoraLoaderBlockWeight //Inspire'] + model, clip, _ = cls().doit(model, clip, lora_name, model_strength, clip_strength, False, 0, + lbw_a, lbw_b, "", lbw) + else: + _lora = comfy.utils.load_torch_file(lora_path, safe_load=True) + keys = _lora.keys() + if "down_blocks.0.resnets.0.norm1.bias" in keys: + print('Using LORA for Resadapter') + key_map = {} + key_map = comfy.lora.model_lora_keys_unet(model.model, key_map) + mapping_norm = {} + + for key in keys: + if ".weight" in key: + key_name_in_ori_sd = key_map[key.replace(".weight", "")] + mapping_norm[key_name_in_ori_sd] = _lora[key] + elif ".bias" in key: + key_name_in_ori_sd = key_map[key.replace(".bias", "")] + mapping_norm[key_name_in_ori_sd.replace(".weight", ".bias")] = _lora[ + key + ] + else: + print("===>Unexpected key", key) + mapping_norm[key] = _lora[key] + + for k in mapping_norm.keys(): + if k not in model.model.state_dict(): + print("===>Missing key:", k) + model.model.load_state_dict(mapping_norm, strict=False) + return (model, clip) + + # PixArt + if type is not None and type == 'PixArt': + from ..modules.dit.pixArt.loader import load_pixart_lora + model = load_pixart_lora(model, _lora, lora_path, model_strength) + else: + model, clip = comfy.sd.load_lora_for_models(model, clip, _lora, model_strength, clip_strength) + + if use_cache: + self.add_to_cache("lora", unique_id, (model, clip)) + self.eviction_based_on_memory() + else: + log_node_error(f"LORA NOT FOUND", orig_lora_name) + + return model, clip + + def resolve_lora_name(self, name): + if os.path.exists(name): + return name + else: + if len(self.lora_name_cache) == 0: + loras = folder_paths.get_filename_list("loras") + self.lora_name_cache.extend(loras) + for x in self.lora_name_cache: + if x.endswith(name): + return x + + # 如果刷新网页后新添加的lora走这个逻辑 + log_node_info("LORA NOT IN CACHE", f"{name}") + loras = folder_paths.get_filename_list("loras") + for x in loras: + if x.endswith(name): + self.lora_name_cache.append(x) + return x + + return None + + def load_main(self, ckpt_name, config_name, vae_name, lora_name, lora_model_strength, lora_clip_strength, optional_lora_stack, model_override, clip_override, vae_override, prompt, nf4=False): + model: ModelPatcher | None = None + clip: comfy.sd.CLIP | None = None + vae: comfy.sd.VAE | None = None + clip_vision = None + lora_stack = [] + + # Check for model override + can_load_lora = True + # 判断是否存在 模型或Lora叠加xyplot, 若存在优先缓存第一个模型 + # Determine whether there is a model or Lora overlapping xyplot, and if there is, prioritize caching the first model. + xy_model_id = next((x for x in prompt if str(prompt[x]["class_type"]) in ["easy XYInputs: ModelMergeBlocks", + "easy XYInputs: Checkpoint"]), None) + # This will find nodes that aren't actively connected to anything, and skip loading lora's for them. + xy_lora_id = next((x for x in prompt if str(prompt[x]["class_type"]) == "easy XYInputs: Lora"), None) + if xy_lora_id is not None: + can_load_lora = False + if xy_model_id is not None: + node = prompt[xy_model_id] + if "ckpt_name_1" in node["inputs"]: + ckpt_name_1 = node["inputs"]["ckpt_name_1"] + model, clip, vae, clip_vision = self.load_checkpoint(ckpt_name_1) + can_load_lora = False + elif model_override is not None and clip_override is not None and vae_override is not None: + model = model_override + clip = clip_override + vae = vae_override + else: + model, clip, vae, clip_vision = self.load_checkpoint(ckpt_name, config_name) + if model_override is not None: + model = model_override + if vae_override is not None: + vae = vae_override + elif clip_override is not None: + clip = clip_override + + + if optional_lora_stack is not None and can_load_lora: + for lora in optional_lora_stack: + # This is a subtle bit of code because it uses the model created by the last call, and passes it to the next call. + lora = {"lora_name": lora[0], "model": model, "clip": clip, "model_strength": lora[1], + "clip_strength": lora[2]} + model, clip = self.load_lora(lora) + lora['model'] = model + lora['clip'] = clip + lora_stack.append(lora) + + if lora_name != "None" and can_load_lora: + lora = {"lora_name": lora_name, "model": model, "clip": clip, "model_strength": lora_model_strength, + "clip_strength": lora_clip_strength} + model, clip = self.load_lora(lora) + lora_stack.append(lora) + + # Check for custom VAE + if vae_name not in ["Baked VAE", "Baked-VAE"]: + vae = self.load_vae(vae_name) + # CLIP skip + if not clip: + raise Exception("No CLIP found") + + return model, clip, vae, clip_vision, lora_stack + + # Kolors + def load_kolors_unet(self, unet_name): + if unet_name in self.loaded_objects["unet"]: + log_node_info("Load Kolors UNet", f"{unet_name} cached") + return self.loaded_objects["unet"][unet_name][0] + else: + from ..modules.kolors.loader import applyKolorsUnet + with applyKolorsUnet(): + unet_path = folder_paths.get_full_path("unet", unet_name) + sd = comfy.utils.load_torch_file(unet_path) + model = comfy.sd.load_unet_state_dict(sd) + if model is None: + raise RuntimeError("ERROR: Could not detect model type of: {}".format(unet_path)) + + self.add_to_cache("unet", unet_name, model) + self.eviction_based_on_memory() + + return model + + def load_chatglm3(self, chatglm3_name): + from ..modules.kolors.loader import load_chatglm3 + if chatglm3_name in self.loaded_objects["chatglm3"]: + log_node_info("Load ChatGLM3", f"{chatglm3_name} cached") + return self.loaded_objects["chatglm3"][chatglm3_name][0] + + chatglm_model = load_chatglm3(model_path=folder_paths.get_full_path("llm", chatglm3_name)) + self.add_to_cache("chatglm3", chatglm3_name, chatglm_model) + self.eviction_based_on_memory() + + return chatglm_model + + + # DiT + def load_dit_ckpt(self, ckpt_name, model_name, **kwargs): + if (ckpt_name+'_'+model_name) in self.loaded_objects["ckpt"]: + return self.loaded_objects["ckpt"][ckpt_name+'_'+model_name][0] + model = None + ckpt_path = folder_paths.get_full_path("checkpoints", ckpt_name) + model_type = kwargs['model_type'] if "model_type" in kwargs else 'PixArt' + if model_type == 'PixArt': + pixart_conf = kwargs['pixart_conf'] + model_conf = pixart_conf[model_name] + model = load_pixart(ckpt_path, model_conf) + if model: + self.add_to_cache("ckpt", ckpt_name + '_' + model_name, model) + self.eviction_based_on_memory() + return model + + def load_t5_from_sd3_clip(self, sd3_clip, padding): + try: + from comfy.text_encoders.sd3_clip import SD3Tokenizer, SD3ClipModel + except: + from comfy.sd3_clip import SD3Tokenizer, SD3ClipModel + import copy + + clip = sd3_clip.clone() + assert clip.cond_stage_model.t5xxl is not None, "CLIP must have T5 loaded!" + + # remove transformer + transformer = clip.cond_stage_model.t5xxl.transformer + clip.cond_stage_model.t5xxl.transformer = None + + # clone object + tmp = SD3ClipModel(clip_l=False, clip_g=False, t5=False) + tmp.t5xxl = copy.deepcopy(clip.cond_stage_model.t5xxl) + # put transformer back + clip.cond_stage_model.t5xxl.transformer = transformer + tmp.t5xxl.transformer = transformer + + # override special tokens + tmp.t5xxl.special_tokens = copy.deepcopy(clip.cond_stage_model.t5xxl.special_tokens) + tmp.t5xxl.special_tokens.pop("end") # make sure empty tokens match + + # tokenizer + tok = SD3Tokenizer() + tok.t5xxl.min_length = padding + + clip.cond_stage_model = tmp + clip.tokenizer = tok + + return clip diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/log.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/log.py new file mode 100644 index 0000000000000000000000000000000000000000..8eea58831473f20b694711891fa234da31e9138d --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/log.py @@ -0,0 +1,77 @@ +COLORS_FG = { + 'BLACK': '\33[30m', + 'RED': '\33[31m', + 'GREEN': '\33[32m', + 'YELLOW': '\33[33m', + 'BLUE': '\33[34m', + 'MAGENTA': '\33[35m', + 'CYAN': '\33[36m', + 'WHITE': '\33[37m', + 'GREY': '\33[90m', + 'BRIGHT_RED': '\33[91m', + 'BRIGHT_GREEN': '\33[92m', + 'BRIGHT_YELLOW': '\33[93m', + 'BRIGHT_BLUE': '\33[94m', + 'BRIGHT_MAGENTA': '\33[95m', + 'BRIGHT_CYAN': '\33[96m', + 'BRIGHT_WHITE': '\33[97m', +} +COLORS_STYLE = { + 'RESET': '\33[0m', + 'BOLD': '\33[1m', + 'NORMAL': '\33[22m', + 'ITALIC': '\33[3m', + 'UNDERLINE': '\33[4m', + 'BLINK': '\33[5m', + 'BLINK2': '\33[6m', + 'SELECTED': '\33[7m', +} +COLORS_BG = { + 'BLACK': '\33[40m', + 'RED': '\33[41m', + 'GREEN': '\33[42m', + 'YELLOW': '\33[43m', + 'BLUE': '\33[44m', + 'MAGENTA': '\33[45m', + 'CYAN': '\33[46m', + 'WHITE': '\33[47m', + 'GREY': '\33[100m', + 'BRIGHT_RED': '\33[101m', + 'BRIGHT_GREEN': '\33[102m', + 'BRIGHT_YELLOW': '\33[103m', + 'BRIGHT_BLUE': '\33[104m', + 'BRIGHT_MAGENTA': '\33[105m', + 'BRIGHT_CYAN': '\33[106m', + 'BRIGHT_WHITE': '\33[107m', +} + +def log_node_success(node_name, message=None): + """Logs a success message.""" + _log_node(COLORS_FG["GREEN"], node_name, message) + +def log_node_info(node_name, message=None): + """Logs an info message.""" + _log_node(COLORS_FG["CYAN"], node_name, message) + + +def log_node_warn(node_name, message=None): + """Logs an warn message.""" + _log_node(COLORS_FG["YELLOW"], node_name, message) + +def log_node_error(node_name, message=None): + """Logs an warn message.""" + _log_node(COLORS_FG["RED"], node_name, message) + +def log_node(node_name, message=None): + """Logs a message.""" + _log_node(COLORS_FG["CYAN"], node_name, message) + + +def _log_node(color, node_name, message=None, prefix=''): + print(_get_log_msg(color, node_name, message, prefix=prefix)) + +def _get_log_msg(color, node_name, message=None, prefix=''): + msg = f'{COLORS_STYLE["BOLD"]}{color}{prefix}[EasyUse] {node_name.replace(" (EasyUse)", "")}' + msg += f':{COLORS_STYLE["RESET"]} {message}' if message is not None else f'{COLORS_STYLE["RESET"]}' + return msg + diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/math.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/math.py new file mode 100644 index 0000000000000000000000000000000000000000..a651327abbf0437dca78917694cdc9db0760f0b0 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/math.py @@ -0,0 +1,148 @@ +""" +Math utility functions for formula evaluation +""" +import math +import re + +def evaluate_formula(formula: str, a=0, b=0, c=0, d=0): + """ + 计算字符串数学公式 + + 支持的运算符和函数: + - 基本运算:+, -, *, /, //, %, ** + - 比较运算:>, <, >=, <=, ==, != + - 数学函数:abs, pow, round, ceil, floor, sqrt, exp, log, log10 + - 三角函数:sin, cos, tan, asin, acos, atan + - 常量:pi, e + + Args: + formula: 数学公式字符串,可以使用变量a、b、c、d + a: 变量a的值 + b: 变量b的值 + c: 变量c的值 + d: 变量d的值 + + Returns: + 如果任意输入为list则返回list[float],否则返回float + + Examples: + >>> evaluate_formula("a + b", 1, 2) + 3.0 + >>> evaluate_formula("pow(a, 2)", 5) + 25.0 + >>> evaluate_formula("ceil(a / b)", 5, 2) + 3.0 + >>> evaluate_formula("(a>b)*b+(a<=b)*a", 5, 3) + 3.0 + >>> evaluate_formula("(a>b)*b+(a<=b)*a", 2, 3) + 2.0 + """ + # 安全的数学函数白名单 + safe_dict = { + # 基本运算 + 'abs': abs, + 'pow': pow, + 'round': round, + # 数学函数 + 'ceil': math.ceil, + 'floor': math.floor, + 'sqrt': math.sqrt, + 'exp': math.exp, + 'log': math.log, + 'log10': math.log10, + # 三角函数 + 'sin': math.sin, + 'cos': math.cos, + 'tan': math.tan, + 'asin': math.asin, + 'acos': math.acos, + 'atan': math.atan, + # 常量 + 'pi': math.pi, + 'e': math.e, + } + + # 判断是否有 list 输入 + list_inputs = {k: v for k, v in {'a': a, 'b': b, 'c': c, 'd': d}.items() if isinstance(v, (list, tuple))} + scalar_inputs = {k: v for k, v in {'a': a, 'b': b, 'c': c, 'd': d}.items() if not isinstance(v, (list, tuple))} + + def _eval_single(vals: dict) -> float: + env = dict(safe_dict) + env.update({k: float(v) for k, v in vals.items()}) + try: + result = eval(formula, {"__builtins__": {}}, env) + return float(result) + except Exception as e: + raise ValueError(f"公式计算错误: {str(e)}") + + if not list_inputs: + # 全是标量 + return _eval_single({k: v for k, v in {'a': a, 'b': b, 'c': c, 'd': d}.items()}) + + # 有 list 输入,逐元素计算 + max_len = max(len(v) for v in list_inputs.values()) + results = [] + for i in range(max_len): + vals = {k: float(v) for k, v in scalar_inputs.items()} + for k, v in list_inputs.items(): + vals[k] = float(v[i] if i < len(v) else v[-1]) + results.append(_eval_single(vals)) + return results + + +def ceil_value(value: float) -> int: + """向上取整""" + return math.ceil(value) + + +def floor_value(value: float) -> int: + """向下取整""" + return math.floor(value) + + +def round_value(value: float, decimals: int = 0) -> float: + """ + 四舍五入 + + Args: + value: 要取整的值 + decimals: 保留小数位数 + + Returns: + 四舍五入后的值 + """ + return round(value, decimals) + + +def power(base: float, exponent: float) -> float: + """计算幂运算""" + return math.pow(base, exponent) + + +def sqrt_value(value: float) -> float: + """计算平方根""" + if value < 0: + raise ValueError("不能对负数求平方根") + return math.sqrt(value) + + +def add(a: float, b: float) -> float: + """加法""" + return a + b + + +def subtract(a: float, b: float) -> float: + """减法""" + return a - b + + +def multiply(a: float, b: float) -> float: + """乘法""" + return a * b + + +def divide(a: float, b: float) -> float: + """除法""" + if b == 0: + raise ValueError("除数不能为零") + return a / b diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/messages.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/messages.py new file mode 100644 index 0000000000000000000000000000000000000000..a5ea47e51362fc3731fa241c46e1343f3909d330 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/messages.py @@ -0,0 +1,55 @@ +from server import PromptServer +from aiohttp import web +import time +import json + +class MessageCancelled(Exception): + pass + +class Message: + stash = {} + messages = {} + cancelled = False + + @classmethod + def addMessage(cls, id, message): + if message == '__cancel__': + cls.messages = {} + cls.cancelled = True + elif message == '__start__': + cls.messages = {} + cls.stash = {} + cls.cancelled = False + else: + cls.messages[str(id)] = message + + @classmethod + def waitForMessage(cls, id, period=0.1, asList=False): + sid = str(id) + while not (sid in cls.messages) and not ("-1" in cls.messages): + if cls.cancelled: + cls.cancelled = False + raise MessageCancelled() + time.sleep(period) + if cls.cancelled: + cls.cancelled = False + raise MessageCancelled() + message = cls.messages.pop(str(id), None) or cls.messages.pop("-1") + try: + if asList: + return [str(x.strip()) for x in message.split(",")] + else: + try: + return json.loads(message) + except ValueError: + return message + except ValueError: + print( f"ERROR IN MESSAGE - failed to parse '${message}' as ${'comma separated list of strings' if asList else 'string'}") + return [message] if asList else message + + +@PromptServer.instance.routes.post('/easyuse/message_callback') +async def message_callback(request): + post = await request.post() + Message.addMessage(post.get("id"), post.get("message")) + return web.json_response({}) \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/model.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/model.py new file mode 100644 index 0000000000000000000000000000000000000000..41ccf51aa185cb86385ddf544ba632664ba21753 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/model.py @@ -0,0 +1,58 @@ +import json +import os +import folder_paths +import server +from .utils import find_tags + +class easyModelManager: + + def __init__(self): + self.img_suffixes = [".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp", ".tiff", ".svg", ".tif", ".tiff"] + self.default_suffixes = [".ckpt", ".pt", ".bin", ".pth", ".safetensors"] + self.models_config = { + "checkpoints": {"suffix": self.default_suffixes}, + "loras": {"suffix": self.default_suffixes}, + "unet": {"suffix": self.default_suffixes}, + } + self.model_lists = {} + + def find_thumbnail(self, model_type, name): + file_no_ext = os.path.splitext(name)[0] + for ext in self.img_suffixes: + full_path = folder_paths.get_full_path(model_type, file_no_ext + ext) + if os.path.isfile(str(full_path)): + return full_path + return None + + def get_model_lists(self, model_type): + if model_type not in self.models_config: + return [] + filenames = folder_paths.get_filename_list(model_type) + model_lists = [] + for name in filenames: + model_suffix = os.path.splitext(name)[-1] + if model_suffix not in self.models_config[model_type]["suffix"]: + continue + else: + cfg = { + "name": os.path.basename(os.path.splitext(name)[0]), + "full_name": name, + "remark": '', + "file_path": folder_paths.get_full_path(model_type, name), + "type": model_type, + "suffix": model_suffix, + "dir_tags": find_tags(name), + "cover": self.find_thumbnail(model_type, name), + "metadata": None, + "sha256": None + } + model_lists.append(cfg) + + return model_lists + + def get_model_info(self, model_type, model_name): + pass + +# if __name__ == "__main__": +# manager = easyModelManager() +# print(manager.get_model_lists("checkpoints")) \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/sampler.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/sampler.py new file mode 100644 index 0000000000000000000000000000000000000000..70716efbc7665482a7f789a31ee6e1ad2804f1ed --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/sampler.py @@ -0,0 +1,1053 @@ +import comfy +import comfy.model_management +import comfy.samplers +import torch +import numpy as np +import latent_preview +from nodes import MAX_RESOLUTION +from PIL import Image +from typing import Dict, List, Optional, Tuple, Union, Any +from ..modules.brushnet.model_patch import add_model_patch + +class easySampler: + def __init__(self): + self.last_helds: dict[str, list] = { + "results": [], + "pipe_line": [], + } + self.device = comfy.model_management.intermediate_device() + + @staticmethod + def tensor2pil(image: torch.Tensor) -> Image.Image: + """Convert a torch tensor to a PIL image.""" + return Image.fromarray(np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8)) + + @staticmethod + def pil2tensor(image: Image.Image) -> torch.Tensor: + """Convert a PIL image to a torch tensor.""" + return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0) + + @staticmethod + def enforce_mul_of_64(d): + d = int(d) + if d <= 7: + d = 8 + leftover = d % 8 # 8 is the number of pixels per byte + if leftover != 0: # if the number of pixels is not a multiple of 8 + if (leftover < 4): # if the number of pixels is less than 4 + d -= leftover # remove the leftover pixels + else: # if the number of pixels is more than 4 + d += 8 - leftover # add the leftover pixels + + return int(d) + + @staticmethod + def safe_split(to_split: str, delimiter: str) -> List[str]: + """Split the input string and return a list of non-empty parts.""" + parts = to_split.split(delimiter) + parts = [part for part in parts if part not in ('', ' ', ' ')] + + while len(parts) < 2: + parts.append('None') + return parts + + def emptyLatent(self, resolution, empty_latent_width, empty_latent_height, batch_size=1, compression=0, model_type='sd', video_length=25): + if resolution not in ["自定义 x 自定义", 'width x height (custom)']: + try: + width, height = map(int, resolution.split(' x ')) + empty_latent_width = width + empty_latent_height = height + except ValueError: + raise ValueError("Invalid base_resolution format.") + if model_type == 'sd3': + latent = torch.ones([batch_size, 16, empty_latent_height // 8, empty_latent_width // 8], device=self.device) * 0.0609 + samples = {"samples": latent} + elif model_type == 'mochi': + latent = torch.zeros([batch_size, 12, ((video_length - 1) // 6) + 1, empty_latent_height // 8, empty_latent_width // 8], device=self.device) + samples = {"samples": latent} + elif compression == 0: + latent = torch.zeros([batch_size, 4, empty_latent_height // 8, empty_latent_width // 8], device=self.device) + samples = {"samples": latent} + else: + latent_c = torch.zeros( + [batch_size, 16, empty_latent_height // compression, empty_latent_width // compression]) + latent_b = torch.zeros([batch_size, 4, empty_latent_height // 4, empty_latent_width // 4]) + + samples = ({"samples": latent_c}, {"samples": latent_b}) + return samples + + def prepare_noise(self, latent_image, seed, noise_inds=None, noise_device="cpu", incremental_seed_mode="comfy", + variation_seed=None, variation_strength=None): + """ + creates random noise given a latent image and a seed. + optional arg skip can be used to skip and discard x number of noise generations for a given seed + """ + + latent_size = latent_image.size() + latent_size_1batch = [1, latent_size[1], latent_size[2], latent_size[3]] + + if variation_strength is not None and variation_strength > 0 or incremental_seed_mode.startswith( + "variation str inc"): + if noise_device == "cpu": + variation_generator = torch.manual_seed(variation_seed) + else: + torch.cuda.manual_seed(variation_seed) + variation_generator = None + + variation_latent = torch.randn(latent_size_1batch, dtype=latent_image.dtype, layout=latent_image.layout, + generator=variation_generator, device=noise_device) + else: + variation_latent = None + + def apply_variation(input_latent, strength_up=None): + if variation_latent is None: + return input_latent + else: + strength = variation_strength + + if strength_up is not None: + strength += strength_up + + variation_noise = variation_latent.expand(input_latent.size()[0], -1, -1, -1) + result = (1 - strength) * input_latent + strength * variation_noise + return result + + # method: incremental seed batch noise + if noise_inds is None and incremental_seed_mode == "incremental": + batch_cnt = latent_size[0] + + latents = None + for i in range(batch_cnt): + if noise_device == "cpu": + generator = torch.manual_seed(seed + i) + else: + torch.cuda.manual_seed(seed + i) + generator = None + + latent = torch.randn(latent_size_1batch, dtype=latent_image.dtype, layout=latent_image.layout, + generator=generator, device=noise_device) + + latent = apply_variation(latent) + + if latents is None: + latents = latent + else: + latents = torch.cat((latents, latent), dim=0) + + return latents + + # method: incremental variation batch noise + elif noise_inds is None and incremental_seed_mode.startswith("variation str inc"): + batch_cnt = latent_size[0] + + latents = None + for i in range(batch_cnt): + if noise_device == "cpu": + generator = torch.manual_seed(seed) + else: + torch.cuda.manual_seed(seed) + generator = None + + latent = torch.randn(latent_size_1batch, dtype=latent_image.dtype, layout=latent_image.layout, + generator=generator, device=noise_device) + + step = float(incremental_seed_mode[18:]) + latent = apply_variation(latent, step * i) + + if latents is None: + latents = latent + else: + latents = torch.cat((latents, latent), dim=0) + + return latents + + # method: comfy batch noise + if noise_device == "cpu": + generator = torch.manual_seed(seed) + else: + torch.cuda.manual_seed(seed) + generator = None + + if noise_inds is None: + latents = torch.randn(latent_image.size(), dtype=latent_image.dtype, layout=latent_image.layout, + generator=generator, device=noise_device) + latents = apply_variation(latents) + return latents + + unique_inds, inverse = np.unique(noise_inds, return_inverse=True) + noises = [] + for i in range(unique_inds[-1] + 1): + noise = torch.randn([1] + list(latent_image.size())[1:], dtype=latent_image.dtype, + layout=latent_image.layout, + generator=generator, device=noise_device) + if i in unique_inds: + noises.append(noise) + noises = [noises[i] for i in inverse] + noises = torch.cat(noises, axis=0) + return noises + + def common_ksampler(self, model, seed, steps, cfg, sampler_name, scheduler, positive, negative, latent, denoise=1.0, + disable_noise=False, start_step=None, last_step=None, force_full_denoise=False, + preview_latent=True, disable_pbar=False, noise_device='CPU'): + device = comfy.model_management.get_torch_device() + noise_device = 'cpu' if noise_device == 'CPU' else device + latent_image = latent["samples"] + latent_image = comfy.sample.fix_empty_latent_channels(model, latent_image) + + noise_mask = None + if "noise_mask" in latent: + noise_mask = latent["noise_mask"] + + preview_format = "JPEG" + if preview_format not in ["JPEG", "PNG"]: + preview_format = "JPEG" + + previewer = False + + if preview_latent: + previewer = latent_preview.get_previewer(device, model.model.latent_format) + + pbar = comfy.utils.ProgressBar(steps) + + def callback(step, x0, x, total_steps): + preview_bytes = None + if previewer: + preview_bytes = previewer.decode_latent_to_preview_image(preview_format, x0) + pbar.update_absolute(step + 1, total_steps, preview_bytes) + + if disable_noise: + noise = torch.zeros(latent_image.size(), dtype=latent_image.dtype, layout=latent_image.layout, + device=noise_device) + else: + batch_inds = latent["batch_index"] if "batch_index" in latent else None + noise = self.prepare_noise(latent_image, seed, batch_inds, noise_device=noise_device) + + ####################################################################################### + # add model patch + # brushnet + add_model_patch(model) + ####################################################################################### + samples = comfy.sample.sample(model, noise, steps, cfg, sampler_name, scheduler, positive, negative, + latent_image, + denoise=denoise, disable_noise=disable_noise, start_step=start_step, + last_step=last_step, + force_full_denoise=force_full_denoise, noise_mask=noise_mask, + callback=callback, + disable_pbar=disable_pbar, seed=seed) + out = latent.copy() + out["samples"] = samples + return out + + def custom_ksampler(self, model, seed, steps, cfg, _sampler, sigmas, positive, negative, latent, + disable_noise=False, preview_latent=True, disable_pbar=False, noise_device='CPU'): + + device = comfy.model_management.get_torch_device() + noise_device = 'cpu' if noise_device == 'CPU' else device + + latent_image = latent["samples"] + + if disable_noise: + noise = torch.zeros(latent_image.size(), dtype=latent_image.dtype, layout=latent_image.layout, device=noise_device) + else: + batch_inds = latent["batch_index"] if "batch_index" in latent else None + noise = self.prepare_noise(latent_image, seed, batch_inds, noise_device=noise_device) + + noise_mask = None + if "noise_mask" in latent: + noise_mask = latent["noise_mask"] + + preview_format = "JPEG" + if preview_format not in ["JPEG", "PNG"]: + preview_format = "JPEG" + + previewer = False + + if preview_latent: + previewer = latent_preview.get_previewer(device, model.model.latent_format) + + pbar = comfy.utils.ProgressBar(steps) + + def callback(step, x0, x, total_steps): + preview_bytes = None + if previewer: + preview_bytes = previewer.decode_latent_to_preview_image(preview_format, x0) + pbar.update_absolute(step + 1, total_steps, preview_bytes) + + samples = comfy.samplers.sample(model, noise, positive, negative, cfg, device, _sampler, sigmas, latent_image=latent_image, model_options=model.model_options, + denoise_mask=noise_mask, callback=callback, disable_pbar=disable_pbar, seed=seed) + + out = latent.copy() + out["samples"] = samples + return out + + def custom_advanced_ksampler(self, guider, sampler, sigmas, latent_image, add_noise='enable', seed=0, preview_latent=False): + latent = latent_image + latent_image = latent["samples"] + latent = latent.copy() + latent_image = comfy.sample.fix_empty_latent_channels(guider.model_patcher, latent_image) + latent["samples"] = latent_image + + device = comfy.model_management.get_torch_device() + noise_device = device if add_noise == 'enable (GPU=A1111)' else 'cpu' + + if add_noise == 'disable': + noise = torch.zeros(latent_image.shape, dtype=latent_image.dtype, layout=latent_image.layout, device="cpu") + else: + batch_inds = latent["batch_index"] if "batch_index" in latent else None + noise = self.prepare_noise(latent_image, seed, batch_inds, noise_device=noise_device) + + noise_mask = None + if "noise_mask" in latent: + noise_mask = latent["noise_mask"] + + x0_output = {} + previewer = False + + model = guider.model_patcher + steps = sigmas.shape[-1] - 1 + if preview_latent: + previewer = latent_preview.get_previewer(model.load_device, model.model.latent_format) + + pbar = comfy.utils.ProgressBar(steps) + + preview_format = "JPEG" + if preview_format not in ["JPEG", "PNG"]: + preview_format = "JPEG" + def callback(step, x0, x, total_steps): + if x0_output is not None: + x0_output["x0"] = x0 + + preview_bytes = None + if previewer: + preview_bytes = previewer.decode_latent_to_preview_image(preview_format, x0) + pbar.update_absolute(step + 1, total_steps, preview_bytes) + + disable_pbar = not comfy.utils.PROGRESS_BAR_ENABLED + samples = guider.sample(noise, latent_image, sampler, sigmas, denoise_mask=noise_mask, + callback=callback, disable_pbar=disable_pbar, seed=seed) + samples = samples.to(comfy.model_management.intermediate_device()) + + out = latent.copy() + out["samples"] = samples + if "x0" in x0_output: + out_denoised = latent.copy() + out_denoised["samples"] = guider.model_patcher.model.process_latent_out(x0_output["x0"].cpu()) + else: + out_denoised = out + return (out, out_denoised) + + def get_value_by_id(self, key: str, my_unique_id: Any) -> Optional[Any]: + """Retrieve value by its associated ID.""" + try: + for value, id_ in self.last_helds[key]: + if id_ == my_unique_id: + return value + except KeyError: + return None + + def update_value_by_id(self, key: str, my_unique_id: Any, new_value: Any) -> Union[bool, None]: + """Update the value associated with a given ID. Return True if updated, False if appended, None if key doesn't exist.""" + try: + for i, (value, id_) in enumerate(self.last_helds[key]): + if id_ == my_unique_id: + self.last_helds[key][i] = (new_value, id_) + return True + self.last_helds[key].append((new_value, my_unique_id)) + return False + except KeyError: + return False + + def upscale(self, samples, upscale_method, scale_by, crop): + s = samples.copy() + width = self.enforce_mul_of_64(round(samples["samples"].shape[3] * scale_by)) + height = self.enforce_mul_of_64(round(samples["samples"].shape[2] * scale_by)) + + if (width > MAX_RESOLUTION): + width = MAX_RESOLUTION + if (height > MAX_RESOLUTION): + height = MAX_RESOLUTION + + s["samples"] = comfy.utils.common_upscale(samples["samples"], width, height, upscale_method, crop) + return (s,) + + def handle_upscale(self, samples: dict, upscale_method: str, factor: float, crop: bool) -> dict: + """Upscale the samples if the upscale_method is not set to 'None'.""" + if upscale_method != "None": + samples = self.upscale(samples, upscale_method, factor, crop)[0] + return samples + + def init_state(self, my_unique_id: Any, key: str, default: Any) -> Any: + """Initialize the state by either fetching the stored value or setting a default.""" + value = self.get_value_by_id(key, my_unique_id) + if value is not None: + return value + return default + + def get_output(self, pipe: dict,) -> Tuple: + """Return a tuple of various elements fetched from the input pipe dictionary.""" + return ( + pipe, + pipe.get("images"), + pipe.get("model"), + pipe.get("positive"), + pipe.get("negative"), + pipe.get("samples"), + pipe.get("vae"), + pipe.get("clip"), + pipe.get("seed"), + ) + + def get_output_sdxl(self, sdxl_pipe: dict) -> Tuple: + """Return a tuple of various elements fetched from the input sdxl_pipe dictionary.""" + return ( + sdxl_pipe, + sdxl_pipe.get("model"), + sdxl_pipe.get("positive"), + sdxl_pipe.get("negative"), + sdxl_pipe.get("vae"), + sdxl_pipe.get("refiner_model"), + sdxl_pipe.get("refiner_positive"), + sdxl_pipe.get("refiner_negative"), + sdxl_pipe.get("refiner_vae"), + sdxl_pipe.get("samples"), + sdxl_pipe.get("clip"), + sdxl_pipe.get("images"), + sdxl_pipe.get("seed") + ) + +def loglinear_interp(t_steps, num_steps): + """ + Performs log-linear interpolation of a given array of decreasing numbers. + """ + xs = np.linspace(0, 1, len(t_steps)) + ys = np.log(t_steps[::-1]) + + new_xs = np.linspace(0, 1, num_steps) + new_ys = np.interp(new_xs, xs, ys) + + interped_ys = np.exp(new_ys)[::-1].copy() + return interped_ys + +class alignYourStepsScheduler: + + NOISE_LEVELS = { + "SD1": [14.6146412293, 6.4745760956, 3.8636745985, 2.6946151520, 1.8841921177, 1.3943805092, 0.9642583904, + 0.6523686016, 0.3977456272, 0.1515232662, 0.0291671582], + "SDXL": [14.6146412293, 6.3184485287, 3.7681790315, 2.1811480769, 1.3405244945, 0.8620721141, 0.5550693289, + 0.3798540708, 0.2332364134, 0.1114188177, 0.0291671582], + "SVD": [700.00, 54.5, 15.886, 7.977, 4.248, 1.789, 0.981, 0.403, 0.173, 0.034, 0.002]} + + def get_sigmas(self, model_type, steps, denoise): + + total_steps = steps + if denoise < 1.0: + if denoise <= 0.0: + return (torch.FloatTensor([]),) + total_steps = round(steps * denoise) + + sigmas = self.NOISE_LEVELS[model_type][:] + if (steps + 1) != len(sigmas): + sigmas = loglinear_interp(sigmas, steps + 1) + + sigmas = sigmas[-(total_steps + 1):] + sigmas[-1] = 0 + return (torch.FloatTensor(sigmas),) + + +class gitsScheduler: + + NOISE_LEVELS = { + 0.80: [ + [14.61464119, 7.49001646, 0.02916753], + [14.61464119, 11.54541874, 6.77309084, 0.02916753], + [14.61464119, 11.54541874, 7.49001646, 3.07277966, 0.02916753], + [14.61464119, 11.54541874, 7.49001646, 5.85520077, 2.05039096, 0.02916753], + [14.61464119, 12.2308979, 8.75849152, 7.49001646, 5.85520077, 2.05039096, 0.02916753], + [14.61464119, 12.2308979, 8.75849152, 7.49001646, 5.85520077, 3.07277966, 1.56271636, 0.02916753], + [14.61464119, 12.96784878, 11.54541874, 8.75849152, 7.49001646, 5.85520077, 3.07277966, 1.56271636, + 0.02916753], + [14.61464119, 13.76078796, 12.2308979, 10.90732002, 8.75849152, 7.49001646, 5.85520077, 3.07277966, + 1.56271636, 0.02916753], + [14.61464119, 13.76078796, 12.96784878, 12.2308979, 10.90732002, 8.75849152, 7.49001646, 5.85520077, + 3.07277966, 1.56271636, 0.02916753], + [14.61464119, 13.76078796, 12.96784878, 12.2308979, 10.90732002, 9.24142551, 8.30717278, 7.49001646, + 5.85520077, 3.07277966, 1.56271636, 0.02916753], + [14.61464119, 13.76078796, 12.96784878, 12.2308979, 10.90732002, 9.24142551, 8.30717278, 7.49001646, + 6.14220476, 4.86714602, 3.07277966, 1.56271636, 0.02916753], + [14.61464119, 13.76078796, 12.96784878, 12.2308979, 11.54541874, 10.31284904, 9.24142551, 8.30717278, + 7.49001646, 6.14220476, 4.86714602, 3.07277966, 1.56271636, 0.02916753], + [14.61464119, 13.76078796, 12.96784878, 12.2308979, 11.54541874, 10.90732002, 10.31284904, 9.24142551, + 8.30717278, 7.49001646, 6.14220476, 4.86714602, 3.07277966, 1.56271636, 0.02916753], + [14.61464119, 13.76078796, 12.96784878, 12.2308979, 11.54541874, 10.90732002, 10.31284904, 9.24142551, + 8.75849152, 8.30717278, 7.49001646, 6.14220476, 4.86714602, 3.07277966, 1.56271636, 0.02916753], + [14.61464119, 13.76078796, 12.96784878, 12.2308979, 11.54541874, 10.90732002, 10.31284904, 9.24142551, + 8.75849152, 8.30717278, 7.49001646, 6.14220476, 4.86714602, 3.1956799, 1.98035145, 0.86115354, 0.02916753], + [14.61464119, 13.76078796, 12.96784878, 12.2308979, 11.54541874, 10.90732002, 10.31284904, 9.75859547, + 9.24142551, 8.75849152, 8.30717278, 7.49001646, 6.14220476, 4.86714602, 3.1956799, 1.98035145, 0.86115354, + 0.02916753], + [14.61464119, 13.76078796, 12.96784878, 12.2308979, 11.54541874, 10.90732002, 10.31284904, 9.75859547, + 9.24142551, 8.75849152, 8.30717278, 7.49001646, 6.77309084, 5.85520077, 4.65472794, 3.07277966, 1.84880662, + 0.83188516, 0.02916753], + [14.61464119, 13.76078796, 12.96784878, 12.2308979, 11.54541874, 10.90732002, 10.31284904, 9.75859547, + 9.24142551, 8.75849152, 8.30717278, 7.88507891, 7.49001646, 6.77309084, 5.85520077, 4.65472794, 3.07277966, + 1.84880662, 0.83188516, 0.02916753], + [14.61464119, 13.76078796, 12.96784878, 12.2308979, 11.54541874, 10.90732002, 10.31284904, 9.75859547, + 9.24142551, 8.75849152, 8.30717278, 7.88507891, 7.49001646, 6.77309084, 5.85520077, 4.86714602, 3.75677586, + 2.84484982, 1.78698075, 0.803307, 0.02916753], + ], + 0.85: [ + [14.61464119, 7.49001646, 0.02916753], + [14.61464119, 7.49001646, 1.84880662, 0.02916753], + [14.61464119, 11.54541874, 6.77309084, 1.56271636, 0.02916753], + [14.61464119, 11.54541874, 7.11996698, 3.07277966, 1.24153244, 0.02916753], + [14.61464119, 11.54541874, 7.49001646, 5.09240818, 2.84484982, 0.95350921, 0.02916753], + [14.61464119, 12.2308979, 8.75849152, 7.49001646, 5.09240818, 2.84484982, 0.95350921, 0.02916753], + [14.61464119, 12.2308979, 8.75849152, 7.49001646, 5.58536053, 3.1956799, 1.84880662, 0.803307, 0.02916753], + [14.61464119, 12.96784878, 11.54541874, 8.75849152, 7.49001646, 5.58536053, 3.1956799, 1.84880662, 0.803307, + 0.02916753], + [14.61464119, 12.96784878, 11.54541874, 8.75849152, 7.49001646, 6.14220476, 4.65472794, 3.07277966, + 1.84880662, 0.803307, 0.02916753], + [14.61464119, 13.76078796, 12.2308979, 10.90732002, 8.75849152, 7.49001646, 6.14220476, 4.65472794, + 3.07277966, 1.84880662, 0.803307, 0.02916753], + [14.61464119, 13.76078796, 12.2308979, 10.90732002, 9.24142551, 8.30717278, 7.49001646, 6.14220476, + 4.65472794, 3.07277966, 1.84880662, 0.803307, 0.02916753], + [14.61464119, 13.76078796, 12.96784878, 12.2308979, 10.90732002, 9.24142551, 8.30717278, 7.49001646, + 6.14220476, 4.65472794, 3.07277966, 1.84880662, 0.803307, 0.02916753], + [14.61464119, 13.76078796, 12.96784878, 12.2308979, 11.54541874, 10.31284904, 9.24142551, 8.30717278, + 7.49001646, 6.14220476, 4.65472794, 3.07277966, 1.84880662, 0.803307, 0.02916753], + [14.61464119, 13.76078796, 12.96784878, 12.2308979, 11.54541874, 10.31284904, 9.24142551, 8.30717278, + 7.49001646, 6.14220476, 4.86714602, 3.60512662, 2.6383388, 1.56271636, 0.72133851, 0.02916753], + [14.61464119, 13.76078796, 12.96784878, 12.2308979, 11.54541874, 10.31284904, 9.24142551, 8.30717278, + 7.49001646, 6.77309084, 5.85520077, 4.65472794, 3.46139455, 2.45070267, 1.56271636, 0.72133851, + 0.02916753], + [14.61464119, 13.76078796, 12.96784878, 12.2308979, 11.54541874, 10.31284904, 9.24142551, 8.75849152, + 8.30717278, 7.49001646, 6.77309084, 5.85520077, 4.65472794, 3.46139455, 2.45070267, 1.56271636, 0.72133851, + 0.02916753], + [14.61464119, 13.76078796, 12.96784878, 12.2308979, 11.54541874, 10.90732002, 10.31284904, 9.24142551, + 8.75849152, 8.30717278, 7.49001646, 6.77309084, 5.85520077, 4.65472794, 3.46139455, 2.45070267, 1.56271636, + 0.72133851, 0.02916753], + [14.61464119, 13.76078796, 12.96784878, 12.2308979, 11.54541874, 10.90732002, 10.31284904, 9.75859547, + 9.24142551, 8.75849152, 8.30717278, 7.49001646, 6.77309084, 5.85520077, 4.65472794, 3.46139455, 2.45070267, + 1.56271636, 0.72133851, 0.02916753], + [14.61464119, 13.76078796, 12.96784878, 12.2308979, 11.54541874, 10.90732002, 10.31284904, 9.75859547, + 9.24142551, 8.75849152, 8.30717278, 7.88507891, 7.49001646, 6.77309084, 5.85520077, 4.65472794, 3.46139455, + 2.45070267, 1.56271636, 0.72133851, 0.02916753], + ], + 0.90: [ + [14.61464119, 6.77309084, 0.02916753], + [14.61464119, 7.49001646, 1.56271636, 0.02916753], + [14.61464119, 7.49001646, 3.07277966, 0.95350921, 0.02916753], + [14.61464119, 7.49001646, 4.86714602, 2.54230714, 0.89115214, 0.02916753], + [14.61464119, 11.54541874, 7.49001646, 4.86714602, 2.54230714, 0.89115214, 0.02916753], + [14.61464119, 11.54541874, 7.49001646, 5.09240818, 3.07277966, 1.61558151, 0.69515091, 0.02916753], + [14.61464119, 12.2308979, 8.75849152, 7.11996698, 4.86714602, 3.07277966, 1.61558151, 0.69515091, + 0.02916753], + [14.61464119, 12.2308979, 8.75849152, 7.49001646, 5.85520077, 4.45427561, 2.95596409, 1.61558151, + 0.69515091, 0.02916753], + [14.61464119, 12.2308979, 8.75849152, 7.49001646, 5.85520077, 4.45427561, 3.1956799, 2.19988537, 1.24153244, + 0.57119018, 0.02916753], + [14.61464119, 12.96784878, 10.90732002, 8.75849152, 7.49001646, 5.85520077, 4.45427561, 3.1956799, + 2.19988537, 1.24153244, 0.57119018, 0.02916753], + [14.61464119, 12.96784878, 11.54541874, 9.24142551, 8.30717278, 7.49001646, 5.85520077, 4.45427561, + 3.1956799, 2.19988537, 1.24153244, 0.57119018, 0.02916753], + [14.61464119, 12.96784878, 11.54541874, 9.24142551, 8.30717278, 7.49001646, 6.14220476, 4.86714602, + 3.75677586, 2.84484982, 1.84880662, 1.08895338, 0.52423614, 0.02916753], + [14.61464119, 13.76078796, 12.2308979, 10.90732002, 9.24142551, 8.30717278, 7.49001646, 6.14220476, + 4.86714602, 3.75677586, 2.84484982, 1.84880662, 1.08895338, 0.52423614, 0.02916753], + [14.61464119, 13.76078796, 12.2308979, 10.90732002, 9.24142551, 8.30717278, 7.49001646, 6.44769001, + 5.58536053, 4.45427561, 3.32507086, 2.45070267, 1.61558151, 0.95350921, 0.45573691, 0.02916753], + [14.61464119, 13.76078796, 12.96784878, 12.2308979, 10.90732002, 9.24142551, 8.30717278, 7.49001646, + 6.44769001, 5.58536053, 4.45427561, 3.32507086, 2.45070267, 1.61558151, 0.95350921, 0.45573691, + 0.02916753], + [14.61464119, 13.76078796, 12.96784878, 12.2308979, 10.90732002, 9.24142551, 8.30717278, 7.49001646, + 6.77309084, 5.85520077, 4.86714602, 3.91689563, 3.07277966, 2.27973175, 1.56271636, 0.95350921, 0.45573691, + 0.02916753], + [14.61464119, 13.76078796, 12.96784878, 12.2308979, 11.54541874, 10.31284904, 9.24142551, 8.30717278, + 7.49001646, 6.77309084, 5.85520077, 4.86714602, 3.91689563, 3.07277966, 2.27973175, 1.56271636, 0.95350921, + 0.45573691, 0.02916753], + [14.61464119, 13.76078796, 12.96784878, 12.2308979, 11.54541874, 10.31284904, 9.24142551, 8.75849152, + 8.30717278, 7.49001646, 6.77309084, 5.85520077, 4.86714602, 3.91689563, 3.07277966, 2.27973175, 1.56271636, + 0.95350921, 0.45573691, 0.02916753], + [14.61464119, 13.76078796, 12.96784878, 12.2308979, 11.54541874, 10.31284904, 9.24142551, 8.75849152, + 8.30717278, 7.49001646, 6.77309084, 5.85520077, 5.09240818, 4.45427561, 3.60512662, 2.95596409, 2.19988537, + 1.51179266, 0.89115214, 0.43325692, 0.02916753], + ], + 0.95: [ + [14.61464119, 6.77309084, 0.02916753], + [14.61464119, 6.77309084, 1.56271636, 0.02916753], + [14.61464119, 7.49001646, 2.84484982, 0.89115214, 0.02916753], + [14.61464119, 7.49001646, 4.86714602, 2.36326075, 0.803307, 0.02916753], + [14.61464119, 7.49001646, 4.86714602, 2.95596409, 1.56271636, 0.64427125, 0.02916753], + [14.61464119, 11.54541874, 7.49001646, 4.86714602, 2.95596409, 1.56271636, 0.64427125, 0.02916753], + [14.61464119, 11.54541874, 7.49001646, 4.86714602, 3.07277966, 1.91321158, 1.08895338, 0.50118381, + 0.02916753], + [14.61464119, 11.54541874, 7.49001646, 5.85520077, 4.45427561, 3.07277966, 1.91321158, 1.08895338, + 0.50118381, 0.02916753], + [14.61464119, 12.2308979, 8.75849152, 7.49001646, 5.85520077, 4.45427561, 3.07277966, 1.91321158, + 1.08895338, 0.50118381, 0.02916753], + [14.61464119, 12.2308979, 8.75849152, 7.49001646, 5.85520077, 4.45427561, 3.1956799, 2.19988537, 1.41535246, + 0.803307, 0.38853383, 0.02916753], + [14.61464119, 12.2308979, 8.75849152, 7.49001646, 5.85520077, 4.65472794, 3.46139455, 2.6383388, 1.84880662, + 1.24153244, 0.72133851, 0.34370604, 0.02916753], + [14.61464119, 12.96784878, 10.90732002, 8.75849152, 7.49001646, 5.85520077, 4.65472794, 3.46139455, + 2.6383388, 1.84880662, 1.24153244, 0.72133851, 0.34370604, 0.02916753], + [14.61464119, 12.96784878, 10.90732002, 8.75849152, 7.49001646, 6.14220476, 4.86714602, 3.75677586, + 2.95596409, 2.19988537, 1.56271636, 1.05362725, 0.64427125, 0.32104823, 0.02916753], + [14.61464119, 12.96784878, 10.90732002, 8.75849152, 7.49001646, 6.44769001, 5.58536053, 4.65472794, + 3.60512662, 2.95596409, 2.19988537, 1.56271636, 1.05362725, 0.64427125, 0.32104823, 0.02916753], + [14.61464119, 12.96784878, 11.54541874, 9.24142551, 8.30717278, 7.49001646, 6.44769001, 5.58536053, + 4.65472794, 3.60512662, 2.95596409, 2.19988537, 1.56271636, 1.05362725, 0.64427125, 0.32104823, + 0.02916753], + [14.61464119, 12.96784878, 11.54541874, 9.24142551, 8.30717278, 7.49001646, 6.44769001, 5.58536053, + 4.65472794, 3.75677586, 3.07277966, 2.45070267, 1.78698075, 1.24153244, 0.83188516, 0.50118381, 0.22545385, + 0.02916753], + [14.61464119, 12.96784878, 11.54541874, 9.24142551, 8.30717278, 7.49001646, 6.77309084, 5.85520077, + 5.09240818, 4.45427561, 3.60512662, 2.95596409, 2.36326075, 1.72759056, 1.24153244, 0.83188516, 0.50118381, + 0.22545385, 0.02916753], + [14.61464119, 13.76078796, 12.2308979, 10.90732002, 9.24142551, 8.30717278, 7.49001646, 6.77309084, + 5.85520077, 5.09240818, 4.45427561, 3.60512662, 2.95596409, 2.36326075, 1.72759056, 1.24153244, 0.83188516, + 0.50118381, 0.22545385, 0.02916753], + [14.61464119, 13.76078796, 12.2308979, 10.90732002, 9.24142551, 8.30717278, 7.49001646, 6.77309084, + 5.85520077, 5.09240818, 4.45427561, 3.75677586, 3.07277966, 2.45070267, 1.91321158, 1.46270394, 1.05362725, + 0.72133851, 0.43325692, 0.19894916, 0.02916753], + ], + 1.00: [ + [14.61464119, 1.56271636, 0.02916753], + [14.61464119, 6.77309084, 0.95350921, 0.02916753], + [14.61464119, 6.77309084, 2.36326075, 0.803307, 0.02916753], + [14.61464119, 7.11996698, 3.07277966, 1.56271636, 0.59516323, 0.02916753], + [14.61464119, 7.49001646, 4.86714602, 2.84484982, 1.41535246, 0.57119018, 0.02916753], + [14.61464119, 7.49001646, 4.86714602, 2.84484982, 1.61558151, 0.86115354, 0.38853383, 0.02916753], + [14.61464119, 11.54541874, 7.49001646, 4.86714602, 2.84484982, 1.61558151, 0.86115354, 0.38853383, + 0.02916753], + [14.61464119, 11.54541874, 7.49001646, 4.86714602, 3.07277966, 1.98035145, 1.24153244, 0.72133851, + 0.34370604, 0.02916753], + [14.61464119, 11.54541874, 7.49001646, 5.85520077, 4.45427561, 3.07277966, 1.98035145, 1.24153244, + 0.72133851, 0.34370604, 0.02916753], + [14.61464119, 11.54541874, 7.49001646, 5.85520077, 4.45427561, 3.1956799, 2.27973175, 1.51179266, + 0.95350921, 0.54755926, 0.25053367, 0.02916753], + [14.61464119, 11.54541874, 7.49001646, 5.85520077, 4.45427561, 3.1956799, 2.36326075, 1.61558151, + 1.08895338, 0.72133851, 0.41087446, 0.17026083, 0.02916753], + [14.61464119, 11.54541874, 8.75849152, 7.49001646, 5.85520077, 4.45427561, 3.1956799, 2.36326075, + 1.61558151, 1.08895338, 0.72133851, 0.41087446, 0.17026083, 0.02916753], + [14.61464119, 11.54541874, 8.75849152, 7.49001646, 5.85520077, 4.65472794, 3.60512662, 2.84484982, + 2.12350607, 1.56271636, 1.08895338, 0.72133851, 0.41087446, 0.17026083, 0.02916753], + [14.61464119, 11.54541874, 8.75849152, 7.49001646, 5.85520077, 4.65472794, 3.60512662, 2.84484982, + 2.19988537, 1.61558151, 1.162866, 0.803307, 0.50118381, 0.27464288, 0.09824532, 0.02916753], + [14.61464119, 11.54541874, 8.75849152, 7.49001646, 5.85520077, 4.65472794, 3.75677586, 3.07277966, + 2.45070267, 1.84880662, 1.36964464, 1.01931262, 0.72133851, 0.45573691, 0.25053367, 0.09824532, + 0.02916753], + [14.61464119, 11.54541874, 8.75849152, 7.49001646, 6.14220476, 5.09240818, 4.26497746, 3.46139455, + 2.84484982, 2.19988537, 1.67050016, 1.24153244, 0.92192322, 0.64427125, 0.43325692, 0.25053367, 0.09824532, + 0.02916753], + [14.61464119, 11.54541874, 8.75849152, 7.49001646, 6.14220476, 5.09240818, 4.26497746, 3.60512662, + 2.95596409, 2.45070267, 1.91321158, 1.51179266, 1.12534678, 0.83188516, 0.59516323, 0.38853383, 0.22545385, + 0.09824532, 0.02916753], + [14.61464119, 12.2308979, 9.24142551, 8.30717278, 7.49001646, 6.14220476, 5.09240818, 4.26497746, + 3.60512662, 2.95596409, 2.45070267, 1.91321158, 1.51179266, 1.12534678, 0.83188516, 0.59516323, 0.38853383, + 0.22545385, 0.09824532, 0.02916753], + [14.61464119, 12.2308979, 9.24142551, 8.30717278, 7.49001646, 6.77309084, 5.85520077, 5.09240818, + 4.26497746, 3.60512662, 2.95596409, 2.45070267, 1.91321158, 1.51179266, 1.12534678, 0.83188516, 0.59516323, + 0.38853383, 0.22545385, 0.09824532, 0.02916753], + ], + 1.05: [ + [14.61464119, 0.95350921, 0.02916753], + [14.61464119, 6.77309084, 0.89115214, 0.02916753], + [14.61464119, 6.77309084, 2.05039096, 0.72133851, 0.02916753], + [14.61464119, 6.77309084, 2.84484982, 1.28281462, 0.52423614, 0.02916753], + [14.61464119, 6.77309084, 3.07277966, 1.61558151, 0.803307, 0.34370604, 0.02916753], + [14.61464119, 7.49001646, 4.86714602, 2.84484982, 1.56271636, 0.803307, 0.34370604, 0.02916753], + [14.61464119, 7.49001646, 4.86714602, 2.84484982, 1.61558151, 0.95350921, 0.52423614, 0.22545385, + 0.02916753], + [14.61464119, 7.49001646, 4.86714602, 3.07277966, 1.98035145, 1.24153244, 0.74807048, 0.41087446, + 0.17026083, 0.02916753], + [14.61464119, 7.49001646, 4.86714602, 3.1956799, 2.27973175, 1.51179266, 0.95350921, 0.59516323, 0.34370604, + 0.13792117, 0.02916753], + [14.61464119, 7.49001646, 5.09240818, 3.46139455, 2.45070267, 1.61558151, 1.08895338, 0.72133851, + 0.45573691, 0.25053367, 0.09824532, 0.02916753], + [14.61464119, 11.54541874, 7.49001646, 5.09240818, 3.46139455, 2.45070267, 1.61558151, 1.08895338, + 0.72133851, 0.45573691, 0.25053367, 0.09824532, 0.02916753], + [14.61464119, 11.54541874, 7.49001646, 5.85520077, 4.45427561, 3.1956799, 2.36326075, 1.61558151, + 1.08895338, 0.72133851, 0.45573691, 0.25053367, 0.09824532, 0.02916753], + [14.61464119, 11.54541874, 7.49001646, 5.85520077, 4.45427561, 3.1956799, 2.45070267, 1.72759056, + 1.24153244, 0.86115354, 0.59516323, 0.38853383, 0.22545385, 0.09824532, 0.02916753], + [14.61464119, 11.54541874, 7.49001646, 5.85520077, 4.65472794, 3.60512662, 2.84484982, 2.19988537, + 1.61558151, 1.162866, 0.83188516, 0.59516323, 0.38853383, 0.22545385, 0.09824532, 0.02916753], + [14.61464119, 11.54541874, 7.49001646, 5.85520077, 4.65472794, 3.60512662, 2.84484982, 2.19988537, + 1.67050016, 1.28281462, 0.95350921, 0.72133851, 0.52423614, 0.34370604, 0.19894916, 0.09824532, + 0.02916753], + [14.61464119, 11.54541874, 7.49001646, 5.85520077, 4.65472794, 3.60512662, 2.95596409, 2.36326075, + 1.84880662, 1.41535246, 1.08895338, 0.83188516, 0.61951244, 0.45573691, 0.32104823, 0.19894916, 0.09824532, + 0.02916753], + [14.61464119, 11.54541874, 7.49001646, 5.85520077, 4.65472794, 3.60512662, 2.95596409, 2.45070267, + 1.91321158, 1.51179266, 1.20157266, 0.95350921, 0.74807048, 0.57119018, 0.43325692, 0.29807833, 0.19894916, + 0.09824532, 0.02916753], + [14.61464119, 11.54541874, 8.30717278, 7.11996698, 5.85520077, 4.65472794, 3.60512662, 2.95596409, + 2.45070267, 1.91321158, 1.51179266, 1.20157266, 0.95350921, 0.74807048, 0.57119018, 0.43325692, 0.29807833, + 0.19894916, 0.09824532, 0.02916753], + [14.61464119, 11.54541874, 8.30717278, 7.11996698, 5.85520077, 4.65472794, 3.60512662, 2.95596409, + 2.45070267, 1.98035145, 1.61558151, 1.32549286, 1.08895338, 0.86115354, 0.69515091, 0.54755926, 0.41087446, + 0.29807833, 0.19894916, 0.09824532, 0.02916753], + ], + 1.10: [ + [14.61464119, 0.89115214, 0.02916753], + [14.61464119, 2.36326075, 0.72133851, 0.02916753], + [14.61464119, 5.85520077, 1.61558151, 0.57119018, 0.02916753], + [14.61464119, 6.77309084, 2.45070267, 1.08895338, 0.45573691, 0.02916753], + [14.61464119, 6.77309084, 2.95596409, 1.56271636, 0.803307, 0.34370604, 0.02916753], + [14.61464119, 6.77309084, 3.07277966, 1.61558151, 0.89115214, 0.4783645, 0.19894916, 0.02916753], + [14.61464119, 6.77309084, 3.07277966, 1.84880662, 1.08895338, 0.64427125, 0.34370604, 0.13792117, + 0.02916753], + [14.61464119, 7.49001646, 4.86714602, 2.84484982, 1.61558151, 0.95350921, 0.54755926, 0.27464288, + 0.09824532, 0.02916753], + [14.61464119, 7.49001646, 4.86714602, 2.95596409, 1.91321158, 1.24153244, 0.803307, 0.4783645, 0.25053367, + 0.09824532, 0.02916753], + [14.61464119, 7.49001646, 4.86714602, 3.07277966, 2.05039096, 1.41535246, 0.95350921, 0.64427125, + 0.41087446, 0.22545385, 0.09824532, 0.02916753], + [14.61464119, 7.49001646, 4.86714602, 3.1956799, 2.27973175, 1.61558151, 1.12534678, 0.803307, 0.54755926, + 0.36617002, 0.22545385, 0.09824532, 0.02916753], + [14.61464119, 7.49001646, 4.86714602, 3.32507086, 2.45070267, 1.72759056, 1.24153244, 0.89115214, + 0.64427125, 0.45573691, 0.32104823, 0.19894916, 0.09824532, 0.02916753], + [14.61464119, 7.49001646, 5.09240818, 3.60512662, 2.84484982, 2.05039096, 1.51179266, 1.08895338, 0.803307, + 0.59516323, 0.43325692, 0.29807833, 0.19894916, 0.09824532, 0.02916753], + [14.61464119, 7.49001646, 5.09240818, 3.60512662, 2.84484982, 2.12350607, 1.61558151, 1.24153244, + 0.95350921, 0.72133851, 0.54755926, 0.41087446, 0.29807833, 0.19894916, 0.09824532, 0.02916753], + [14.61464119, 7.49001646, 5.85520077, 4.45427561, 3.1956799, 2.45070267, 1.84880662, 1.41535246, 1.08895338, + 0.83188516, 0.64427125, 0.50118381, 0.36617002, 0.25053367, 0.17026083, 0.09824532, 0.02916753], + [14.61464119, 7.49001646, 5.85520077, 4.45427561, 3.1956799, 2.45070267, 1.91321158, 1.51179266, 1.20157266, + 0.95350921, 0.74807048, 0.59516323, 0.45573691, 0.34370604, 0.25053367, 0.17026083, 0.09824532, + 0.02916753], + [14.61464119, 7.49001646, 5.85520077, 4.45427561, 3.46139455, 2.84484982, 2.19988537, 1.72759056, + 1.36964464, 1.08895338, 0.86115354, 0.69515091, 0.54755926, 0.43325692, 0.34370604, 0.25053367, 0.17026083, + 0.09824532, 0.02916753], + [14.61464119, 11.54541874, 7.49001646, 5.85520077, 4.45427561, 3.46139455, 2.84484982, 2.19988537, + 1.72759056, 1.36964464, 1.08895338, 0.86115354, 0.69515091, 0.54755926, 0.43325692, 0.34370604, 0.25053367, + 0.17026083, 0.09824532, 0.02916753], + [14.61464119, 11.54541874, 7.49001646, 5.85520077, 4.45427561, 3.46139455, 2.84484982, 2.19988537, + 1.72759056, 1.36964464, 1.08895338, 0.89115214, 0.72133851, 0.59516323, 0.4783645, 0.38853383, 0.29807833, + 0.22545385, 0.17026083, 0.09824532, 0.02916753], + ], + 1.15: [ + [14.61464119, 0.83188516, 0.02916753], + [14.61464119, 1.84880662, 0.59516323, 0.02916753], + [14.61464119, 5.85520077, 1.56271636, 0.52423614, 0.02916753], + [14.61464119, 5.85520077, 1.91321158, 0.83188516, 0.34370604, 0.02916753], + [14.61464119, 5.85520077, 2.45070267, 1.24153244, 0.59516323, 0.25053367, 0.02916753], + [14.61464119, 5.85520077, 2.84484982, 1.51179266, 0.803307, 0.41087446, 0.17026083, 0.02916753], + [14.61464119, 5.85520077, 2.84484982, 1.56271636, 0.89115214, 0.50118381, 0.25053367, 0.09824532, + 0.02916753], + [14.61464119, 6.77309084, 3.07277966, 1.84880662, 1.12534678, 0.72133851, 0.43325692, 0.22545385, + 0.09824532, 0.02916753], + [14.61464119, 6.77309084, 3.07277966, 1.91321158, 1.24153244, 0.803307, 0.52423614, 0.34370604, 0.19894916, + 0.09824532, 0.02916753], + [14.61464119, 7.49001646, 4.86714602, 2.95596409, 1.91321158, 1.24153244, 0.803307, 0.52423614, 0.34370604, + 0.19894916, 0.09824532, 0.02916753], + [14.61464119, 7.49001646, 4.86714602, 3.07277966, 2.05039096, 1.36964464, 0.95350921, 0.69515091, 0.4783645, + 0.32104823, 0.19894916, 0.09824532, 0.02916753], + [14.61464119, 7.49001646, 4.86714602, 3.07277966, 2.12350607, 1.51179266, 1.08895338, 0.803307, 0.59516323, + 0.43325692, 0.29807833, 0.19894916, 0.09824532, 0.02916753], + [14.61464119, 7.49001646, 4.86714602, 3.07277966, 2.12350607, 1.51179266, 1.08895338, 0.803307, 0.59516323, + 0.45573691, 0.34370604, 0.25053367, 0.17026083, 0.09824532, 0.02916753], + [14.61464119, 7.49001646, 4.86714602, 3.07277966, 2.19988537, 1.61558151, 1.24153244, 0.95350921, + 0.74807048, 0.59516323, 0.45573691, 0.34370604, 0.25053367, 0.17026083, 0.09824532, 0.02916753], + [14.61464119, 7.49001646, 4.86714602, 3.1956799, 2.45070267, 1.78698075, 1.32549286, 1.01931262, 0.803307, + 0.64427125, 0.50118381, 0.38853383, 0.29807833, 0.22545385, 0.17026083, 0.09824532, 0.02916753], + [14.61464119, 7.49001646, 4.86714602, 3.1956799, 2.45070267, 1.78698075, 1.32549286, 1.01931262, 0.803307, + 0.64427125, 0.52423614, 0.41087446, 0.32104823, 0.25053367, 0.19894916, 0.13792117, 0.09824532, + 0.02916753], + [14.61464119, 7.49001646, 4.86714602, 3.1956799, 2.45070267, 1.84880662, 1.41535246, 1.12534678, 0.89115214, + 0.72133851, 0.59516323, 0.4783645, 0.38853383, 0.32104823, 0.25053367, 0.19894916, 0.13792117, 0.09824532, + 0.02916753], + [14.61464119, 7.49001646, 4.86714602, 3.1956799, 2.45070267, 1.84880662, 1.41535246, 1.12534678, 0.89115214, + 0.72133851, 0.59516323, 0.50118381, 0.41087446, 0.34370604, 0.27464288, 0.22545385, 0.17026083, 0.13792117, + 0.09824532, 0.02916753], + [14.61464119, 7.49001646, 4.86714602, 3.1956799, 2.45070267, 1.84880662, 1.41535246, 1.12534678, 0.89115214, + 0.72133851, 0.59516323, 0.50118381, 0.41087446, 0.34370604, 0.29807833, 0.25053367, 0.19894916, 0.17026083, + 0.13792117, 0.09824532, 0.02916753], + ], + 1.20: [ + [14.61464119, 0.803307, 0.02916753], + [14.61464119, 1.56271636, 0.52423614, 0.02916753], + [14.61464119, 2.36326075, 0.92192322, 0.36617002, 0.02916753], + [14.61464119, 2.84484982, 1.24153244, 0.59516323, 0.25053367, 0.02916753], + [14.61464119, 5.85520077, 2.05039096, 0.95350921, 0.45573691, 0.17026083, 0.02916753], + [14.61464119, 5.85520077, 2.45070267, 1.24153244, 0.64427125, 0.29807833, 0.09824532, 0.02916753], + [14.61464119, 5.85520077, 2.45070267, 1.36964464, 0.803307, 0.45573691, 0.25053367, 0.09824532, 0.02916753], + [14.61464119, 5.85520077, 2.84484982, 1.61558151, 0.95350921, 0.59516323, 0.36617002, 0.19894916, + 0.09824532, 0.02916753], + [14.61464119, 5.85520077, 2.84484982, 1.67050016, 1.08895338, 0.74807048, 0.50118381, 0.32104823, + 0.19894916, 0.09824532, 0.02916753], + [14.61464119, 5.85520077, 2.95596409, 1.84880662, 1.24153244, 0.83188516, 0.59516323, 0.41087446, + 0.27464288, 0.17026083, 0.09824532, 0.02916753], + [14.61464119, 5.85520077, 3.07277966, 1.98035145, 1.36964464, 0.95350921, 0.69515091, 0.50118381, + 0.36617002, 0.25053367, 0.17026083, 0.09824532, 0.02916753], + [14.61464119, 6.77309084, 3.46139455, 2.36326075, 1.56271636, 1.08895338, 0.803307, 0.59516323, 0.45573691, + 0.34370604, 0.25053367, 0.17026083, 0.09824532, 0.02916753], + [14.61464119, 6.77309084, 3.46139455, 2.45070267, 1.61558151, 1.162866, 0.86115354, 0.64427125, 0.50118381, + 0.38853383, 0.29807833, 0.22545385, 0.17026083, 0.09824532, 0.02916753], + [14.61464119, 7.49001646, 4.65472794, 3.07277966, 2.12350607, 1.51179266, 1.08895338, 0.83188516, + 0.64427125, 0.50118381, 0.38853383, 0.29807833, 0.22545385, 0.17026083, 0.09824532, 0.02916753], + [14.61464119, 7.49001646, 4.65472794, 3.07277966, 2.12350607, 1.51179266, 1.08895338, 0.83188516, + 0.64427125, 0.50118381, 0.41087446, 0.32104823, 0.25053367, 0.19894916, 0.13792117, 0.09824532, + 0.02916753], + [14.61464119, 7.49001646, 4.65472794, 3.07277966, 2.12350607, 1.51179266, 1.08895338, 0.83188516, + 0.64427125, 0.50118381, 0.41087446, 0.34370604, 0.27464288, 0.22545385, 0.17026083, 0.13792117, 0.09824532, + 0.02916753], + [14.61464119, 7.49001646, 4.65472794, 3.07277966, 2.19988537, 1.61558151, 1.20157266, 0.92192322, + 0.72133851, 0.57119018, 0.45573691, 0.36617002, 0.29807833, 0.25053367, 0.19894916, 0.17026083, 0.13792117, + 0.09824532, 0.02916753], + [14.61464119, 7.49001646, 4.65472794, 3.07277966, 2.19988537, 1.61558151, 1.24153244, 0.95350921, + 0.74807048, 0.59516323, 0.4783645, 0.38853383, 0.32104823, 0.27464288, 0.22545385, 0.19894916, 0.17026083, + 0.13792117, 0.09824532, 0.02916753], + [14.61464119, 7.49001646, 4.65472794, 3.07277966, 2.19988537, 1.61558151, 1.24153244, 0.95350921, + 0.74807048, 0.59516323, 0.50118381, 0.41087446, 0.34370604, 0.29807833, 0.25053367, 0.22545385, 0.19894916, + 0.17026083, 0.13792117, 0.09824532, 0.02916753], + ], + 1.25: [ + [14.61464119, 0.72133851, 0.02916753], + [14.61464119, 1.56271636, 0.50118381, 0.02916753], + [14.61464119, 2.05039096, 0.803307, 0.32104823, 0.02916753], + [14.61464119, 2.36326075, 0.95350921, 0.43325692, 0.17026083, 0.02916753], + [14.61464119, 2.84484982, 1.24153244, 0.59516323, 0.27464288, 0.09824532, 0.02916753], + [14.61464119, 3.07277966, 1.51179266, 0.803307, 0.43325692, 0.22545385, 0.09824532, 0.02916753], + [14.61464119, 5.85520077, 2.36326075, 1.24153244, 0.72133851, 0.41087446, 0.22545385, 0.09824532, + 0.02916753], + [14.61464119, 5.85520077, 2.45070267, 1.36964464, 0.83188516, 0.52423614, 0.34370604, 0.19894916, + 0.09824532, 0.02916753], + [14.61464119, 5.85520077, 2.84484982, 1.61558151, 0.98595673, 0.64427125, 0.43325692, 0.27464288, + 0.17026083, 0.09824532, 0.02916753], + [14.61464119, 5.85520077, 2.84484982, 1.67050016, 1.08895338, 0.74807048, 0.52423614, 0.36617002, + 0.25053367, 0.17026083, 0.09824532, 0.02916753], + [14.61464119, 5.85520077, 2.84484982, 1.72759056, 1.162866, 0.803307, 0.59516323, 0.45573691, 0.34370604, + 0.25053367, 0.17026083, 0.09824532, 0.02916753], + [14.61464119, 5.85520077, 2.95596409, 1.84880662, 1.24153244, 0.86115354, 0.64427125, 0.4783645, 0.36617002, + 0.27464288, 0.19894916, 0.13792117, 0.09824532, 0.02916753], + [14.61464119, 5.85520077, 2.95596409, 1.84880662, 1.28281462, 0.92192322, 0.69515091, 0.52423614, + 0.41087446, 0.32104823, 0.25053367, 0.19894916, 0.13792117, 0.09824532, 0.02916753], + [14.61464119, 5.85520077, 2.95596409, 1.91321158, 1.32549286, 0.95350921, 0.72133851, 0.54755926, + 0.43325692, 0.34370604, 0.27464288, 0.22545385, 0.17026083, 0.13792117, 0.09824532, 0.02916753], + [14.61464119, 5.85520077, 2.95596409, 1.91321158, 1.32549286, 0.95350921, 0.72133851, 0.57119018, + 0.45573691, 0.36617002, 0.29807833, 0.25053367, 0.19894916, 0.17026083, 0.13792117, 0.09824532, + 0.02916753], + [14.61464119, 5.85520077, 2.95596409, 1.91321158, 1.32549286, 0.95350921, 0.74807048, 0.59516323, 0.4783645, + 0.38853383, 0.32104823, 0.27464288, 0.22545385, 0.19894916, 0.17026083, 0.13792117, 0.09824532, + 0.02916753], + [14.61464119, 5.85520077, 3.07277966, 2.05039096, 1.41535246, 1.05362725, 0.803307, 0.61951244, 0.50118381, + 0.41087446, 0.34370604, 0.29807833, 0.25053367, 0.22545385, 0.19894916, 0.17026083, 0.13792117, 0.09824532, + 0.02916753], + [14.61464119, 5.85520077, 3.07277966, 2.05039096, 1.41535246, 1.05362725, 0.803307, 0.64427125, 0.52423614, + 0.43325692, 0.36617002, 0.32104823, 0.27464288, 0.25053367, 0.22545385, 0.19894916, 0.17026083, 0.13792117, + 0.09824532, 0.02916753], + [14.61464119, 5.85520077, 3.07277966, 2.05039096, 1.46270394, 1.08895338, 0.83188516, 0.66947293, + 0.54755926, 0.45573691, 0.38853383, 0.34370604, 0.29807833, 0.27464288, 0.25053367, 0.22545385, 0.19894916, + 0.17026083, 0.13792117, 0.09824532, 0.02916753], + ], + 1.30: [ + [14.61464119, 0.72133851, 0.02916753], + [14.61464119, 1.24153244, 0.43325692, 0.02916753], + [14.61464119, 1.56271636, 0.59516323, 0.22545385, 0.02916753], + [14.61464119, 1.84880662, 0.803307, 0.36617002, 0.13792117, 0.02916753], + [14.61464119, 2.36326075, 1.01931262, 0.52423614, 0.25053367, 0.09824532, 0.02916753], + [14.61464119, 2.84484982, 1.36964464, 0.74807048, 0.41087446, 0.22545385, 0.09824532, 0.02916753], + [14.61464119, 3.07277966, 1.56271636, 0.89115214, 0.54755926, 0.34370604, 0.19894916, 0.09824532, + 0.02916753], + [14.61464119, 3.07277966, 1.61558151, 0.95350921, 0.61951244, 0.41087446, 0.27464288, 0.17026083, + 0.09824532, 0.02916753], + [14.61464119, 5.85520077, 2.45070267, 1.36964464, 0.83188516, 0.54755926, 0.36617002, 0.25053367, + 0.17026083, 0.09824532, 0.02916753], + [14.61464119, 5.85520077, 2.45070267, 1.41535246, 0.92192322, 0.64427125, 0.45573691, 0.34370604, + 0.25053367, 0.17026083, 0.09824532, 0.02916753], + [14.61464119, 5.85520077, 2.6383388, 1.56271636, 1.01931262, 0.72133851, 0.50118381, 0.36617002, 0.27464288, + 0.19894916, 0.13792117, 0.09824532, 0.02916753], + [14.61464119, 5.85520077, 2.84484982, 1.61558151, 1.05362725, 0.74807048, 0.54755926, 0.41087446, + 0.32104823, 0.25053367, 0.19894916, 0.13792117, 0.09824532, 0.02916753], + [14.61464119, 5.85520077, 2.84484982, 1.61558151, 1.08895338, 0.77538133, 0.57119018, 0.43325692, + 0.34370604, 0.27464288, 0.22545385, 0.17026083, 0.13792117, 0.09824532, 0.02916753], + [14.61464119, 5.85520077, 2.84484982, 1.61558151, 1.08895338, 0.803307, 0.59516323, 0.45573691, 0.36617002, + 0.29807833, 0.25053367, 0.19894916, 0.17026083, 0.13792117, 0.09824532, 0.02916753], + [14.61464119, 5.85520077, 2.84484982, 1.61558151, 1.08895338, 0.803307, 0.59516323, 0.4783645, 0.38853383, + 0.32104823, 0.27464288, 0.22545385, 0.19894916, 0.17026083, 0.13792117, 0.09824532, 0.02916753], + [14.61464119, 5.85520077, 2.84484982, 1.72759056, 1.162866, 0.83188516, 0.64427125, 0.50118381, 0.41087446, + 0.34370604, 0.29807833, 0.25053367, 0.22545385, 0.19894916, 0.17026083, 0.13792117, 0.09824532, + 0.02916753], + [14.61464119, 5.85520077, 2.84484982, 1.72759056, 1.162866, 0.83188516, 0.64427125, 0.52423614, 0.43325692, + 0.36617002, 0.32104823, 0.27464288, 0.25053367, 0.22545385, 0.19894916, 0.17026083, 0.13792117, 0.09824532, + 0.02916753], + [14.61464119, 5.85520077, 2.84484982, 1.78698075, 1.24153244, 0.92192322, 0.72133851, 0.57119018, + 0.45573691, 0.38853383, 0.34370604, 0.29807833, 0.27464288, 0.25053367, 0.22545385, 0.19894916, 0.17026083, + 0.13792117, 0.09824532, 0.02916753], + [14.61464119, 5.85520077, 2.84484982, 1.78698075, 1.24153244, 0.92192322, 0.72133851, 0.57119018, 0.4783645, + 0.41087446, 0.36617002, 0.32104823, 0.29807833, 0.27464288, 0.25053367, 0.22545385, 0.19894916, 0.17026083, + 0.13792117, 0.09824532, 0.02916753], + ], + 1.35: [ + [14.61464119, 0.69515091, 0.02916753], + [14.61464119, 0.95350921, 0.34370604, 0.02916753], + [14.61464119, 1.56271636, 0.57119018, 0.19894916, 0.02916753], + [14.61464119, 1.61558151, 0.69515091, 0.29807833, 0.09824532, 0.02916753], + [14.61464119, 1.84880662, 0.83188516, 0.43325692, 0.22545385, 0.09824532, 0.02916753], + [14.61464119, 2.45070267, 1.162866, 0.64427125, 0.36617002, 0.19894916, 0.09824532, 0.02916753], + [14.61464119, 2.84484982, 1.36964464, 0.803307, 0.50118381, 0.32104823, 0.19894916, 0.09824532, 0.02916753], + [14.61464119, 2.84484982, 1.41535246, 0.83188516, 0.54755926, 0.36617002, 0.25053367, 0.17026083, + 0.09824532, 0.02916753], + [14.61464119, 2.84484982, 1.56271636, 0.95350921, 0.64427125, 0.45573691, 0.32104823, 0.22545385, + 0.17026083, 0.09824532, 0.02916753], + [14.61464119, 2.84484982, 1.56271636, 0.95350921, 0.64427125, 0.45573691, 0.34370604, 0.25053367, + 0.19894916, 0.13792117, 0.09824532, 0.02916753], + [14.61464119, 3.07277966, 1.61558151, 1.01931262, 0.72133851, 0.52423614, 0.38853383, 0.29807833, + 0.22545385, 0.17026083, 0.13792117, 0.09824532, 0.02916753], + [14.61464119, 3.07277966, 1.61558151, 1.01931262, 0.72133851, 0.52423614, 0.41087446, 0.32104823, + 0.25053367, 0.19894916, 0.17026083, 0.13792117, 0.09824532, 0.02916753], + [14.61464119, 3.07277966, 1.61558151, 1.05362725, 0.74807048, 0.54755926, 0.43325692, 0.34370604, + 0.27464288, 0.22545385, 0.19894916, 0.17026083, 0.13792117, 0.09824532, 0.02916753], + [14.61464119, 3.07277966, 1.72759056, 1.12534678, 0.803307, 0.59516323, 0.45573691, 0.36617002, 0.29807833, + 0.25053367, 0.22545385, 0.19894916, 0.17026083, 0.13792117, 0.09824532, 0.02916753], + [14.61464119, 3.07277966, 1.72759056, 1.12534678, 0.803307, 0.59516323, 0.4783645, 0.38853383, 0.32104823, + 0.27464288, 0.25053367, 0.22545385, 0.19894916, 0.17026083, 0.13792117, 0.09824532, 0.02916753], + [14.61464119, 5.85520077, 2.45070267, 1.51179266, 1.01931262, 0.74807048, 0.57119018, 0.45573691, + 0.36617002, 0.32104823, 0.27464288, 0.25053367, 0.22545385, 0.19894916, 0.17026083, 0.13792117, 0.09824532, + 0.02916753], + [14.61464119, 5.85520077, 2.6383388, 1.61558151, 1.08895338, 0.803307, 0.61951244, 0.50118381, 0.41087446, + 0.34370604, 0.29807833, 0.27464288, 0.25053367, 0.22545385, 0.19894916, 0.17026083, 0.13792117, 0.09824532, + 0.02916753], + [14.61464119, 5.85520077, 2.6383388, 1.61558151, 1.08895338, 0.803307, 0.64427125, 0.52423614, 0.43325692, + 0.36617002, 0.32104823, 0.29807833, 0.27464288, 0.25053367, 0.22545385, 0.19894916, 0.17026083, 0.13792117, + 0.09824532, 0.02916753], + [14.61464119, 5.85520077, 2.6383388, 1.61558151, 1.08895338, 0.803307, 0.64427125, 0.52423614, 0.45573691, + 0.38853383, 0.34370604, 0.32104823, 0.29807833, 0.27464288, 0.25053367, 0.22545385, 0.19894916, 0.17026083, + 0.13792117, 0.09824532, 0.02916753], + ], + 1.40: [ + [14.61464119, 0.59516323, 0.02916753], + [14.61464119, 0.95350921, 0.34370604, 0.02916753], + [14.61464119, 1.08895338, 0.43325692, 0.13792117, 0.02916753], + [14.61464119, 1.56271636, 0.64427125, 0.27464288, 0.09824532, 0.02916753], + [14.61464119, 1.61558151, 0.803307, 0.43325692, 0.22545385, 0.09824532, 0.02916753], + [14.61464119, 2.05039096, 0.95350921, 0.54755926, 0.34370604, 0.19894916, 0.09824532, 0.02916753], + [14.61464119, 2.45070267, 1.24153244, 0.72133851, 0.43325692, 0.27464288, 0.17026083, 0.09824532, + 0.02916753], + [14.61464119, 2.45070267, 1.24153244, 0.74807048, 0.50118381, 0.34370604, 0.25053367, 0.17026083, + 0.09824532, 0.02916753], + [14.61464119, 2.45070267, 1.28281462, 0.803307, 0.52423614, 0.36617002, 0.27464288, 0.19894916, 0.13792117, + 0.09824532, 0.02916753], + [14.61464119, 2.45070267, 1.28281462, 0.803307, 0.54755926, 0.38853383, 0.29807833, 0.22545385, 0.17026083, + 0.13792117, 0.09824532, 0.02916753], + [14.61464119, 2.84484982, 1.41535246, 0.86115354, 0.59516323, 0.43325692, 0.32104823, 0.25053367, + 0.19894916, 0.17026083, 0.13792117, 0.09824532, 0.02916753], + [14.61464119, 2.84484982, 1.51179266, 0.95350921, 0.64427125, 0.45573691, 0.34370604, 0.27464288, + 0.22545385, 0.19894916, 0.17026083, 0.13792117, 0.09824532, 0.02916753], + [14.61464119, 2.84484982, 1.51179266, 0.95350921, 0.64427125, 0.4783645, 0.36617002, 0.29807833, 0.25053367, + 0.22545385, 0.19894916, 0.17026083, 0.13792117, 0.09824532, 0.02916753], + [14.61464119, 2.84484982, 1.56271636, 0.98595673, 0.69515091, 0.52423614, 0.41087446, 0.34370604, + 0.29807833, 0.25053367, 0.22545385, 0.19894916, 0.17026083, 0.13792117, 0.09824532, 0.02916753], + [14.61464119, 2.84484982, 1.56271636, 1.01931262, 0.72133851, 0.54755926, 0.43325692, 0.36617002, + 0.32104823, 0.27464288, 0.25053367, 0.22545385, 0.19894916, 0.17026083, 0.13792117, 0.09824532, + 0.02916753], + [14.61464119, 2.84484982, 1.61558151, 1.05362725, 0.74807048, 0.57119018, 0.45573691, 0.38853383, + 0.34370604, 0.29807833, 0.27464288, 0.25053367, 0.22545385, 0.19894916, 0.17026083, 0.13792117, 0.09824532, + 0.02916753], + [14.61464119, 2.84484982, 1.61558151, 1.08895338, 0.803307, 0.61951244, 0.50118381, 0.41087446, 0.36617002, + 0.32104823, 0.29807833, 0.27464288, 0.25053367, 0.22545385, 0.19894916, 0.17026083, 0.13792117, 0.09824532, + 0.02916753], + [14.61464119, 2.84484982, 1.61558151, 1.08895338, 0.803307, 0.61951244, 0.50118381, 0.43325692, 0.38853383, + 0.34370604, 0.32104823, 0.29807833, 0.27464288, 0.25053367, 0.22545385, 0.19894916, 0.17026083, 0.13792117, + 0.09824532, 0.02916753], + [14.61464119, 2.84484982, 1.61558151, 1.08895338, 0.803307, 0.64427125, 0.52423614, 0.45573691, 0.41087446, + 0.36617002, 0.34370604, 0.32104823, 0.29807833, 0.27464288, 0.25053367, 0.22545385, 0.19894916, 0.17026083, + 0.13792117, 0.09824532, 0.02916753], + ], + 1.45: [ + [14.61464119, 0.59516323, 0.02916753], + [14.61464119, 0.803307, 0.25053367, 0.02916753], + [14.61464119, 0.95350921, 0.34370604, 0.09824532, 0.02916753], + [14.61464119, 1.24153244, 0.54755926, 0.25053367, 0.09824532, 0.02916753], + [14.61464119, 1.56271636, 0.72133851, 0.36617002, 0.19894916, 0.09824532, 0.02916753], + [14.61464119, 1.61558151, 0.803307, 0.45573691, 0.27464288, 0.17026083, 0.09824532, 0.02916753], + [14.61464119, 1.91321158, 0.95350921, 0.57119018, 0.36617002, 0.25053367, 0.17026083, 0.09824532, + 0.02916753], + [14.61464119, 2.19988537, 1.08895338, 0.64427125, 0.41087446, 0.27464288, 0.19894916, 0.13792117, + 0.09824532, 0.02916753], + [14.61464119, 2.45070267, 1.24153244, 0.74807048, 0.50118381, 0.34370604, 0.25053367, 0.19894916, + 0.13792117, 0.09824532, 0.02916753], + [14.61464119, 2.45070267, 1.24153244, 0.74807048, 0.50118381, 0.36617002, 0.27464288, 0.22545385, + 0.17026083, 0.13792117, 0.09824532, 0.02916753], + [14.61464119, 2.45070267, 1.28281462, 0.803307, 0.54755926, 0.41087446, 0.32104823, 0.25053367, 0.19894916, + 0.17026083, 0.13792117, 0.09824532, 0.02916753], + [14.61464119, 2.45070267, 1.28281462, 0.803307, 0.57119018, 0.43325692, 0.34370604, 0.27464288, 0.22545385, + 0.19894916, 0.17026083, 0.13792117, 0.09824532, 0.02916753], + [14.61464119, 2.45070267, 1.28281462, 0.83188516, 0.59516323, 0.45573691, 0.36617002, 0.29807833, + 0.25053367, 0.22545385, 0.19894916, 0.17026083, 0.13792117, 0.09824532, 0.02916753], + [14.61464119, 2.45070267, 1.28281462, 0.83188516, 0.59516323, 0.45573691, 0.36617002, 0.32104823, + 0.27464288, 0.25053367, 0.22545385, 0.19894916, 0.17026083, 0.13792117, 0.09824532, 0.02916753], + [14.61464119, 2.84484982, 1.51179266, 0.95350921, 0.69515091, 0.52423614, 0.41087446, 0.34370604, + 0.29807833, 0.27464288, 0.25053367, 0.22545385, 0.19894916, 0.17026083, 0.13792117, 0.09824532, + 0.02916753], + [14.61464119, 2.84484982, 1.51179266, 0.95350921, 0.69515091, 0.52423614, 0.43325692, 0.36617002, + 0.32104823, 0.29807833, 0.27464288, 0.25053367, 0.22545385, 0.19894916, 0.17026083, 0.13792117, 0.09824532, + 0.02916753], + [14.61464119, 2.84484982, 1.56271636, 0.98595673, 0.72133851, 0.54755926, 0.45573691, 0.38853383, + 0.34370604, 0.32104823, 0.29807833, 0.27464288, 0.25053367, 0.22545385, 0.19894916, 0.17026083, 0.13792117, + 0.09824532, 0.02916753], + [14.61464119, 2.84484982, 1.56271636, 1.01931262, 0.74807048, 0.57119018, 0.4783645, 0.41087446, 0.36617002, + 0.34370604, 0.32104823, 0.29807833, 0.27464288, 0.25053367, 0.22545385, 0.19894916, 0.17026083, 0.13792117, + 0.09824532, 0.02916753], + [14.61464119, 2.84484982, 1.56271636, 1.01931262, 0.74807048, 0.59516323, 0.50118381, 0.43325692, + 0.38853383, 0.36617002, 0.34370604, 0.32104823, 0.29807833, 0.27464288, 0.25053367, 0.22545385, 0.19894916, + 0.17026083, 0.13792117, 0.09824532, 0.02916753], + ], + 1.50: [ + [14.61464119, 0.54755926, 0.02916753], + [14.61464119, 0.803307, 0.25053367, 0.02916753], + [14.61464119, 0.86115354, 0.32104823, 0.09824532, 0.02916753], + [14.61464119, 1.24153244, 0.54755926, 0.25053367, 0.09824532, 0.02916753], + [14.61464119, 1.56271636, 0.72133851, 0.36617002, 0.19894916, 0.09824532, 0.02916753], + [14.61464119, 1.61558151, 0.803307, 0.45573691, 0.27464288, 0.17026083, 0.09824532, 0.02916753], + [14.61464119, 1.61558151, 0.83188516, 0.52423614, 0.34370604, 0.25053367, 0.17026083, 0.09824532, + 0.02916753], + [14.61464119, 1.84880662, 0.95350921, 0.59516323, 0.38853383, 0.27464288, 0.19894916, 0.13792117, + 0.09824532, 0.02916753], + [14.61464119, 1.84880662, 0.95350921, 0.59516323, 0.41087446, 0.29807833, 0.22545385, 0.17026083, + 0.13792117, 0.09824532, 0.02916753], + [14.61464119, 1.84880662, 0.95350921, 0.61951244, 0.43325692, 0.32104823, 0.25053367, 0.19894916, + 0.17026083, 0.13792117, 0.09824532, 0.02916753], + [14.61464119, 2.19988537, 1.12534678, 0.72133851, 0.50118381, 0.36617002, 0.27464288, 0.22545385, + 0.19894916, 0.17026083, 0.13792117, 0.09824532, 0.02916753], + [14.61464119, 2.19988537, 1.12534678, 0.72133851, 0.50118381, 0.36617002, 0.29807833, 0.25053367, + 0.22545385, 0.19894916, 0.17026083, 0.13792117, 0.09824532, 0.02916753], + [14.61464119, 2.36326075, 1.24153244, 0.803307, 0.57119018, 0.43325692, 0.34370604, 0.29807833, 0.25053367, + 0.22545385, 0.19894916, 0.17026083, 0.13792117, 0.09824532, 0.02916753], + [14.61464119, 2.36326075, 1.24153244, 0.803307, 0.57119018, 0.43325692, 0.34370604, 0.29807833, 0.27464288, + 0.25053367, 0.22545385, 0.19894916, 0.17026083, 0.13792117, 0.09824532, 0.02916753], + [14.61464119, 2.36326075, 1.24153244, 0.803307, 0.59516323, 0.45573691, 0.36617002, 0.32104823, 0.29807833, + 0.27464288, 0.25053367, 0.22545385, 0.19894916, 0.17026083, 0.13792117, 0.09824532, 0.02916753], + [14.61464119, 2.36326075, 1.24153244, 0.803307, 0.59516323, 0.45573691, 0.38853383, 0.34370604, 0.32104823, + 0.29807833, 0.27464288, 0.25053367, 0.22545385, 0.19894916, 0.17026083, 0.13792117, 0.09824532, + 0.02916753], + [14.61464119, 2.45070267, 1.32549286, 0.86115354, 0.64427125, 0.50118381, 0.41087446, 0.36617002, + 0.34370604, 0.32104823, 0.29807833, 0.27464288, 0.25053367, 0.22545385, 0.19894916, 0.17026083, 0.13792117, + 0.09824532, 0.02916753], + [14.61464119, 2.45070267, 1.36964464, 0.92192322, 0.69515091, 0.54755926, 0.45573691, 0.41087446, + 0.36617002, 0.34370604, 0.32104823, 0.29807833, 0.27464288, 0.25053367, 0.22545385, 0.19894916, 0.17026083, + 0.13792117, 0.09824532, 0.02916753], + [14.61464119, 2.45070267, 1.41535246, 0.95350921, 0.72133851, 0.57119018, 0.4783645, 0.43325692, 0.38853383, + 0.36617002, 0.34370604, 0.32104823, 0.29807833, 0.27464288, 0.25053367, 0.22545385, 0.19894916, 0.17026083, + 0.13792117, 0.09824532, 0.02916753], + ], + } + + def get_sigmas(self, coeff, steps, denoise): + total_steps = steps + if denoise < 1.0: + if denoise <= 0.0: + return (torch.FloatTensor([]),) + total_steps = round(steps * denoise) + + if steps <= 20: + sigmas = self.NOISE_LEVELS[round(coeff, 2)][steps-2][:] + else: + sigmas = self.NOISE_LEVELS[round(coeff, 2)][-1][:] + sigmas = loglinear_interp(sigmas, steps + 1) + + sigmas = sigmas[-(total_steps + 1):] + sigmas[-1] = 0 + return (torch.FloatTensor(sigmas), ) \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/styleAlign.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/styleAlign.py new file mode 100644 index 0000000000000000000000000000000000000000..fefae6022fa021531907df8e2d4ceab28012b880 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/styleAlign.py @@ -0,0 +1,148 @@ +import torch +import torch.nn as nn +from comfy.model_patcher import ModelPatcher +from typing import Union + +T = torch.Tensor + + +def exists(val): + return val is not None + + +def default(val, d): + if exists(val): + return val + return d + + +class StyleAlignedArgs: + def __init__(self, share_attn: str) -> None: + self.adain_keys = "k" in share_attn + self.adain_values = "v" in share_attn + self.adain_queries = "q" in share_attn + + share_attention: bool = True + adain_queries: bool = True + adain_keys: bool = True + adain_values: bool = True + + +def expand_first( + feat: T, + scale=1.0, +) -> T: + """ + Expand the first element so it has the same shape as the rest of the batch. + """ + b = feat.shape[0] + feat_style = torch.stack((feat[0], feat[b // 2])).unsqueeze(1) + if scale == 1: + feat_style = feat_style.expand(2, b // 2, *feat.shape[1:]) + else: + feat_style = feat_style.repeat(1, b // 2, 1, 1, 1) + feat_style = torch.cat([feat_style[:, :1], scale * feat_style[:, 1:]], dim=1) + return feat_style.reshape(*feat.shape) + + +def concat_first(feat: T, dim=2, scale=1.0) -> T: + """ + concat the the feature and the style feature expanded above + """ + feat_style = expand_first(feat, scale=scale) + return torch.cat((feat, feat_style), dim=dim) + + +def calc_mean_std(feat, eps: float = 1e-5) -> "tuple[T, T]": + feat_std = (feat.var(dim=-2, keepdims=True) + eps).sqrt() + feat_mean = feat.mean(dim=-2, keepdims=True) + return feat_mean, feat_std + +def adain(feat: T) -> T: + feat_mean, feat_std = calc_mean_std(feat) + 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 + +class SharedAttentionProcessor: + def __init__(self, args: StyleAlignedArgs, scale: float): + self.args = args + self.scale = scale + + def __call__(self, q, k, v, extra_options): + if self.args.adain_queries: + q = adain(q) + if self.args.adain_keys: + k = adain(k) + if self.args.adain_values: + v = adain(v) + if self.args.share_attention: + k = concat_first(k, -2, scale=self.scale) + v = concat_first(v, -2) + + return q, k, v + + +def get_norm_layers( + layer: nn.Module, + norm_layers_: "dict[str, list[Union[nn.GroupNorm, nn.LayerNorm]]]", + share_layer_norm: bool, + share_group_norm: bool, +): + if isinstance(layer, nn.LayerNorm) and share_layer_norm: + norm_layers_["layer"].append(layer) + if isinstance(layer, nn.GroupNorm) and share_group_norm: + 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 + ) + + +def register_norm_forward( + norm_layer: Union[nn.GroupNorm, nn.LayerNorm], +) -> Union[nn.GroupNorm, nn.LayerNorm]: + if not hasattr(norm_layer, "orig_forward"): + setattr(norm_layer, "orig_forward", norm_layer.forward) + orig_forward = norm_layer.orig_forward + + def forward_(hidden_states: T) -> T: + n = hidden_states.shape[-2] + hidden_states = concat_first(hidden_states, dim=-2) + hidden_states = orig_forward(hidden_states) # type: ignore + return hidden_states[..., :n, :] + + norm_layer.forward = forward_ # type: ignore + return norm_layer + + +def register_shared_norm( + model: ModelPatcher, + share_group_norm: bool = True, + share_layer_norm: bool = True, +): + 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." + ) + return [register_norm_forward(layer) for layer in norm_layers["group"]] + [ + register_norm_forward(layer) for layer in norm_layers["layer"] + ] + + +SHARE_NORM_OPTIONS = ["both", "group", "layer", "disabled"] +SHARE_ATTN_OPTIONS = ["q+k", "q+k+v", "disabled"] + + +def styleAlignBatch(model, share_norm, share_attn, scale=1.0): + m = model.clone() + share_group_norm = share_norm in ["group", "both"] + share_layer_norm = share_norm in ["layer", "both"] + register_shared_norm(model, share_group_norm, share_layer_norm) + args = StyleAlignedArgs(share_attn) + m.set_model_attn1_patch(SharedAttentionProcessor(args, scale)) + return m \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/translate.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/translate.py new file mode 100644 index 0000000000000000000000000000000000000000..89e304dc6675d2a3f4c0a0866ab6c19181495516 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/translate.py @@ -0,0 +1,247 @@ +#credit to shadowcz007 for this module +#from https://github.com/shadowcz007/comfyui-mixlab-nodes/blob/main/nodes/TextGenerateNode.py +import re +import os +import folder_paths + +import comfy.utils +import torch +from transformers import AutoModelForSeq2SeqLM, AutoTokenizer + +from .utils import install_package +try: + from lark import Lark, Transformer, v_args +except: + print('install lark...') + install_package('lark') + from lark import Lark, Transformer, v_args + +model_path = os.path.join(folder_paths.models_dir, 'prompt_generator') +zh_en_model_path = os.path.join(model_path, 'opus-mt-zh-en') +zh_en_model, zh_en_tokenizer = None, None + +def correct_prompt_syntax(prompt=""): + # print("input prompt",prompt) + corrected_elements = [] + # 处理成统一的英文标点 + prompt = prompt.replace('(', '(').replace(')', ')').replace(',', ',').replace(';', ',').replace('。', '.').replace(':',':').replace('\\',',') + # 删除多余的空格 + prompt = re.sub(r'\s+', ' ', prompt).strip() + prompt = prompt.replace("< ","<").replace(" >",">").replace("( ","(").replace(" )",")").replace("[ ","[").replace(' ]',']') + + # 分词 + prompt_elements = prompt.split(',') + + def balance_brackets(element, open_bracket, close_bracket): + open_brackets_count = element.count(open_bracket) + close_brackets_count = element.count(close_bracket) + return element + close_bracket * (open_brackets_count - close_brackets_count) + + for element in prompt_elements: + element = element.strip() + + # 处理空元素 + if not element: + continue + + # 检查并处理圆括号、方括号、尖括号 + if element[0] in '([': + corrected_element = balance_brackets(element, '(', ')') if element[0] == '(' else balance_brackets(element, '[', ']') + elif element[0] == '<': + corrected_element = balance_brackets(element, '<', '>') + else: + # 删除开头的右括号或右方括号 + corrected_element = element.lstrip(')]') + + corrected_elements.append(corrected_element) + + # 重组修正后的prompt + return ','.join(corrected_elements) + +def detect_language(input_str): + # 统计中文和英文字符的数量 + count_cn = count_en = 0 + for char in input_str: + if '\u4e00' <= char <= '\u9fff': + count_cn += 1 + elif char.isalpha(): + count_en += 1 + + # 根据统计的字符数量判断主要语言 + if count_cn > count_en: + return "cn" + elif count_en > count_cn: + return "en" + else: + return "unknow" + +def has_chinese(text): + has_cn = False + _text = text + _text = re.sub(r'<.*?>', '', _text) + _text = re.sub(r'__.*?__', '', _text) + _text = re.sub(r'embedding:.*?$', '', _text) + for char in _text: + if '\u4e00' <= char <= '\u9fff': + has_cn = True + break + elif char.isalpha(): + continue + return has_cn + +def translate(text): + global zh_en_model_path, zh_en_model, zh_en_tokenizer + + if not os.path.exists(zh_en_model_path): + zh_en_model_path = 'Helsinki-NLP/opus-mt-zh-en' + + if zh_en_model is None: + + zh_en_model = AutoModelForSeq2SeqLM.from_pretrained(zh_en_model_path).eval() + zh_en_tokenizer = AutoTokenizer.from_pretrained(zh_en_model_path, padding=True, truncation=True) + + zh_en_model.to("cuda" if torch.cuda.is_available() else "cpu") + with torch.no_grad(): + encoded = zh_en_tokenizer([text], return_tensors="pt") + encoded.to(zh_en_model.device) + sequences = zh_en_model.generate(**encoded) + return zh_en_tokenizer.batch_decode(sequences, skip_special_tokens=True)[0] + +@v_args(inline=True) # Decorator to flatten the tree directly into the function arguments +class ChinesePromptTranslate(Transformer): + + def sentence(self, *args): + return ", ".join(args) + + def phrase(self, *args): + return "".join(args) + + def emphasis(self, *args): + # Reconstruct the emphasis with translated content + return "(" + "".join(args) + ")" + + def weak_emphasis(self, *args): + print('weak_emphasis:', args) + return "[" + "".join(args) + "]" + + def embedding(self, *args): + print('prompt embedding', args[0]) + if len(args) == 1: + embedding_name = str(args[0]) + return f"embedding:{embedding_name}" + elif len(args) > 1: + embedding_name, *numbers = args + + if len(numbers) == 2: + return f"embedding:{embedding_name}:{numbers[0]}:{numbers[1]}" + elif len(numbers) == 1: + return f"embedding:{embedding_name}:{numbers[0]}" + else: + return f"embedding:{embedding_name}" + + def lora(self, *args): + if len(args) == 1: + return f"" + elif len(args) > 1: + # print('lora', args) + _, loar_name, *numbers = args + loar_name = str(loar_name).strip() + if len(numbers) == 2: + return f"" + elif len(numbers) == 1: + return f"" + else: + return f"" + + def weight(self, word, number): + translated_word = translate(str(word)).rstrip('.') + return f"({translated_word}:{str(number).strip()})" + + def schedule(self, *args): + print('prompt schedule', args) + data = [str(arg).strip() for arg in args] + + return f"[{':'.join(data)}]" + + def word(self, word): + # Translate each word using the dictionary + word = str(word) + match_cn = re.search(r'@.*?@', word) + if re.search(r'__.*?__', word): + return word.rstrip('.') + elif match_cn: + chinese = match_cn.group() + before = word.split('@', 1) + before = before[0] if len(before) > 0 else '' + before = translate(str(before)).rstrip('.') if before else '' + after = word.rsplit('@', 1) + after = after[len(after)-1] if len(after) > 1 else '' + after = translate(after).rstrip('.') if after else '' + return before + chinese.replace('@', '').rstrip('.') + after + elif detect_language(word) == "cn": + return translate(word).rstrip('.') + else: + return word.rstrip('.') + + +#定义Prompt文法 +grammar = r""" +start: sentence +sentence: phrase ("," phrase)* +phrase: emphasis | weight | word | lora | embedding | schedule +emphasis: "(" sentence ")" -> emphasis + | "[" sentence "]" -> weak_emphasis +weight: "(" word ":" NUMBER ")" +schedule: "[" word ":" word ":" NUMBER "]" +lora: "<" WORD ":" WORD (":" NUMBER)? (":" NUMBER)? ">" +embedding: "embedding" ":" WORD (":" NUMBER)? (":" NUMBER)? +word: WORD + +NUMBER: /\s*-?\d+(\.\d+)?\s*/ +WORD: /[^,:\(\)\[\]<>]+/ +""" +def zh_to_en(text): + global zh_en_model_path, zh_en_model, zh_en_tokenizer + # 进度条 + pbar = comfy.utils.ProgressBar(len(text) + 1) + texts = [correct_prompt_syntax(t) for t in text] + + install_package('sentencepiece', '0.2.0') + + if not os.path.exists(zh_en_model_path): + zh_en_model_path = 'Helsinki-NLP/opus-mt-zh-en' + + if zh_en_model is None: + zh_en_model = AutoModelForSeq2SeqLM.from_pretrained(zh_en_model_path).eval() + zh_en_tokenizer = AutoTokenizer.from_pretrained(zh_en_model_path, padding=True, truncation=True) + + zh_en_model.to("cuda" if torch.cuda.is_available() else "cpu") + + prompt_result = [] + + en_texts = [] + + for t in texts: + if t: + # translated_text = translated_word = translate(zh_en_tokenizer,zh_en_model,str(t)) + parser = Lark(grammar, start="start", parser="lalr", transformer=ChinesePromptTranslate()) + # print('t',t) + result = parser.parse(t).children + # print('en_result',result) + # en_text=translate(zh_en_tokenizer,zh_en_model,text_without_syntax) + en_texts.append(result[0]) + + zh_en_model.to('cpu') + # print("test en_text", en_texts) + # en_text.to("cuda" if torch.cuda.is_available() else "cpu") + + pbar.update(1) + for t in en_texts: + prompt_result.append(t) + pbar.update(1) + + # print('prompt_result', prompt_result, ) + if len(prompt_result) == 0: + prompt_result = [""] + + return prompt_result \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/utils.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..0fc716a279905ba09ad229df7a53b56003c10bfe --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/utils.py @@ -0,0 +1,300 @@ +class AlwaysEqualProxy(str): + def __eq__(self, _): + return True + + def __ne__(self, _): + return False + +class TautologyStr(str): + def __ne__(self, other): + return False + +class ByPassTypeTuple(tuple): + def __getitem__(self, index): + if index>0: + index=0 + item = super().__getitem__(index) + if isinstance(item, str): + return TautologyStr(item) + return item + +comfy_ui_revision = None +def get_comfyui_revision(): + try: + import git + import os + import folder_paths + repo = git.Repo(os.path.dirname(folder_paths.__file__)) + comfy_ui_revision = len(list(repo.iter_commits('HEAD'))) + except: + comfy_ui_revision = "Unknown" + return comfy_ui_revision + + +import sys +import importlib.util +import importlib.metadata +import comfy.model_management as mm +import logging +import gc +from packaging import version +from server import PromptServer + +LOG = logging.getLogger(__name__) + +def is_package_installed(package): + try: + module = importlib.util.find_spec(package) + return module is not None + except ImportError as e: + print(e) + return False + +def install_package(package, v=None, compare=True, compare_version=None): + run_install = True + if is_package_installed(package): + try: + installed_version = importlib.metadata.version(package) + if v is not None: + if compare_version is None: + compare_version = v + if not compare or version.parse(installed_version) >= version.parse(compare_version): + run_install = False + else: + run_install = False + except: + run_install = False + + if run_install: + import subprocess + package_command = package + '==' + v if v is not None else package + PromptServer.instance.send_sync("easyuse-toast", {'content': f"Installing {package_command}...", 'duration': 5000}) + result = subprocess.run([sys.executable, '-s', '-m', 'pip', 'install', package_command], capture_output=True, text=True) + if result.returncode == 0: + PromptServer.instance.send_sync("easyuse-toast", {'content': f"{package} installed successfully", 'type': 'success', 'duration': 5000}) + print(f"Package {package} installed successfully") + return True + else: + PromptServer.instance.send_sync("easyuse-toast", {'content': f"{package} installed failed", 'type': 'error', 'duration': 5000}) + print(f"Package {package} installed failed") + return False + else: + return False + +def compare_revision(num): + global comfy_ui_revision + if not comfy_ui_revision: + comfy_ui_revision = get_comfyui_revision() + return True if comfy_ui_revision == 'Unknown' or int(comfy_ui_revision) >= num else False + +def find_tags(string: str, sep="/") -> list[str]: + """ + find tags from string use the sep for split + Note: string may contain the \\ or / for path separator + """ + if not string: + return [] + string = string.replace("\\", "/") + while "//" in string: + string = string.replace("//", "/") + if string and sep in string: + return string.split(sep)[:-1] + return [] + + +from comfy.model_base import BaseModel +import comfy.supported_models +import comfy.supported_models_base +def get_sd_version(model): + base: BaseModel = model.model + model_config: comfy.supported_models.supported_models_base.BASE = base.model_config + if isinstance(model_config, comfy.supported_models.SDXL): + return 'sdxl' + elif isinstance(model_config, comfy.supported_models.SDXLRefiner): + return 'sdxl_refiner' + elif isinstance( + model_config, (comfy.supported_models.SD15, comfy.supported_models.SD20) + ): + return 'sd1' + elif isinstance( + model_config, (comfy.supported_models.SVD_img2vid) + ): + return 'svd' + elif isinstance(model_config, comfy.supported_models.SD3): + return 'sd3' + elif isinstance(model_config, comfy.supported_models.HunyuanDiT): + return 'hydit' + elif isinstance(model_config, comfy.supported_models.Flux): + return 'flux' + elif isinstance(model_config, comfy.supported_models.GenmoMochi): + return 'mochi' + else: + return 'unknown' + +def find_nearest_steps(clip_id, prompt): + """Find the nearest KSampler or preSampling node that references the given id.""" + def check_link_to_clip(node_id, clip_id, visited=None, node=None): + """Check if a given node links directly or indirectly to a loader node.""" + if visited is None: + visited = set() + + if node_id in visited: + return False + visited.add(node_id) + if "pipe" in node["inputs"]: + link_ids = node["inputs"]["pipe"] + for id in link_ids: + if id != 0 and id == str(clip_id): + return True + return False + + for id in prompt: + node = prompt[id] + if "Sampler" in node["class_type"] or "sampler" in node["class_type"] or "Sampling" in node["class_type"]: + # Check if this KSampler node directly or indirectly references the given CLIPTextEncode node + if check_link_to_clip(id, clip_id, None, node): + steps = node["inputs"]["steps"] if "steps" in node["inputs"] else 1 + return steps + return 1 + +def find_wildcards_seed(clip_id, text, prompt): + """ Find easy wildcards seed value""" + def find_link_clip_id(id, seed, wildcard_id): + node = prompt[id] + if "positive" in node['inputs']: + link_ids = node["inputs"]["positive"] + if type(link_ids) == list: + for id in link_ids: + if id != 0: + if id == wildcard_id: + wildcard_node = prompt[wildcard_id] + seed = wildcard_node["inputs"]["seed"] if "seed" in wildcard_node["inputs"] else None + if seed is None: + seed = wildcard_node["inputs"]["seed_num"] if "seed_num" in wildcard_node["inputs"] else None + return seed + else: + return find_link_clip_id(id, seed, wildcard_id) + else: + return None + else: + return None + if "__" in text: + seed = None + for id in prompt: + node = prompt[id] + if "wildcards" in node["class_type"]: + wildcard_id = id + return find_link_clip_id(str(clip_id), seed, wildcard_id) + return seed + else: + return None + +def is_linked_styles_selector(prompt, unique_id, prompt_type='positive'): + unique_id = unique_id.split('.')[len(unique_id.split('.')) - 1] if "." in unique_id else unique_id + inputs_values = prompt[unique_id]['inputs'][prompt_type] if prompt_type in prompt[unique_id][ + 'inputs'] else None + if type(inputs_values) == list and inputs_values != 'undefined' and inputs_values[0]: + return True if prompt[inputs_values[0]] and prompt[inputs_values[0]]['class_type'] == 'easy stylesSelector' else False + else: + return False + +use_mirror = False +def get_local_filepath(url, dirname, local_file_name=None): + """Get local file path when is already downloaded or download it""" + import os + from server import PromptServer + from urllib.parse import urlparse + from torch.hub import download_url_to_file + global use_mirror + if not os.path.exists(dirname): + os.makedirs(dirname) + if not local_file_name: + parsed_url = urlparse(url) + local_file_name = os.path.basename(parsed_url.path) + destination = os.path.join(dirname, local_file_name) + if not os.path.exists(destination): + try: + if use_mirror: + url = url.replace('huggingface.co', 'hf-mirror.com') + print(f'downloading {url} to {destination}') + PromptServer.instance.send_sync("easyuse-toast", {'content': f'Downloading model to {destination}, please wait...', 'duration': 10000}) + download_url_to_file(url, destination) + except Exception as e: + use_mirror = True + url = url.replace('huggingface.co', 'hf-mirror.com') + print(f'Unable to download from huggingface, trying mirror: {url}') + PromptServer.instance.send_sync("easyuse-toast", {'content': f'Unable to connect to huggingface, trying mirror: {url}', 'duration': 10000}) + try: + download_url_to_file(url, destination) + except Exception as err: + error_msg = str(err.args[0]) if err.args else str(err) + PromptServer.instance.send_sync("easyuse-toast", + {'content': f'Unable to download model from {url}', 'type':'error'}) + raise Exception(f'Download failed. Original URL and mirror both failed.\nError: {error_msg}') + return destination + +def to_lora_patch_dict(state_dict: dict) -> dict: + """ Convert raw lora state_dict to patch_dict that can be applied on + modelpatcher.""" + patch_dict = {} + for k, w in state_dict.items(): + model_key, patch_type, weight_index = k.split('::') + if model_key not in patch_dict: + patch_dict[model_key] = {} + if patch_type not in patch_dict[model_key]: + patch_dict[model_key][patch_type] = [None] * 16 + patch_dict[model_key][patch_type][int(weight_index)] = w + + patch_flat = {} + for model_key, v in patch_dict.items(): + for patch_type, weight_list in v.items(): + patch_flat[model_key] = (patch_type, weight_list) + + return patch_flat + +def easySave(images, filename_prefix, output_type, prompt=None, extra_pnginfo=None): + """Save or Preview Image""" + from nodes import PreviewImage, SaveImage + if output_type in ["Hide", "None"]: + return list() + elif output_type in ["Preview", "Preview&Choose"]: + filename_prefix = 'easyPreview' + results = PreviewImage().save_images(images, filename_prefix, prompt, extra_pnginfo) + return results['ui']['images'] + else: + results = SaveImage().save_images(images, filename_prefix, prompt, extra_pnginfo) + return results['ui']['images'] + +def getMetadata(filepath): + with open(filepath, "rb") as file: + # https://github.com/huggingface/safetensors#format + # 8 bytes: N, an unsigned little-endian 64-bit integer, containing the size of the header + header_size = int.from_bytes(file.read(8), "little", signed=False) + + if header_size <= 0: + raise BufferError("Invalid header size") + + header = file.read(header_size) + if header_size <= 0: + raise BufferError("Invalid header") + + return header + +def cleanGPUUsedForce(): + from .cache import remove_cache + + remove_cache("*") + gc.collect() + try: + import torch + except (ImportError, OSError, RuntimeError) as exc: + LOG.debug("Skipping CUDA synchronize during cleanGPUUsedForce: torch import failed: %s", exc) + else: + try: + if torch.cuda.is_available(): + torch.cuda.synchronize() + except (AttributeError, OSError, RuntimeError) as exc: + LOG.debug("Skipping CUDA synchronize during cleanGPUUsedForce: %s", exc) + + mm.unload_all_models() + mm.soft_empty_cache() diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/wildcards.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/wildcards.py new file mode 100644 index 0000000000000000000000000000000000000000..243a4a259779efb102bd7cd79665493660da06ea --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/wildcards.py @@ -0,0 +1,476 @@ +import json +import os +import random +import re +from math import prod + +import yaml + +import folder_paths + +from .log import log_node_info + +easy_wildcard_dict = {} + +def get_wildcard_list(): + return [f"__{x}__" for x in easy_wildcard_dict.keys()] + +def wildcard_normalize(x): + return x.replace("\\", "/").lower() + +def read_wildcard(k, v): + if isinstance(v, list): + k = wildcard_normalize(k) + easy_wildcard_dict[k] = v + elif isinstance(v, dict): + for k2, v2 in v.items(): + new_key = f"{k}/{k2}" + new_key = wildcard_normalize(new_key) + read_wildcard(new_key, v2) + +def read_wildcard_dict(wildcard_path): + global easy_wildcard_dict + for root, directories, files in os.walk(wildcard_path, followlinks=True): + for file in files: + if file.endswith('.txt'): + file_path = os.path.join(root, file) + rel_path = os.path.relpath(file_path, wildcard_path) + key = os.path.splitext(rel_path)[0].replace('\\', '/').lower() + + try: + with open(file_path, 'r', encoding="UTF-8", errors="ignore") as f: + lines = f.read().splitlines() + easy_wildcard_dict[key] = lines + except UnicodeDecodeError: + with open(file_path, 'r', encoding="ISO-8859-1") as f: + lines = f.read().splitlines() + easy_wildcard_dict[key] = lines + elif file.endswith('.yaml'): + file_path = os.path.join(root, file) + with open(file_path, 'r', encoding="utf-8") as f: + yaml_data = yaml.load(f, Loader=yaml.FullLoader) + + for k, v in yaml_data.items(): + read_wildcard(k, v) + elif file.endswith('.json'): + file_path = os.path.join(root, file) + try: + with open(file_path, 'r', encoding="utf-8") as f: + json_data = json.load(f) + for key, value in json_data.items(): + key = wildcard_normalize(key) + easy_wildcard_dict[key] = value + except ValueError: + print('json files load error') + return easy_wildcard_dict + + +def process(text, seed=None): + + if seed is not None: + random.seed(seed) + + def replace_options(string): + replacements_found = False + + def replace_option(match): + nonlocal replacements_found + options = match.group(1).split('|') + + multi_select_pattern = options[0].split('$$') + select_range = None + select_sep = ' ' + range_pattern = r'(\d+)(-(\d+))?' + range_pattern2 = r'-(\d+)' + + if len(multi_select_pattern) > 1: + r = re.match(range_pattern, options[0]) + + if r is None: + r = re.match(range_pattern2, options[0]) + a = '1' + b = r.group(1).strip() + else: + a = r.group(1).strip() + b = r.group(3).strip() + + if r is not None: + if b is not None and is_numeric_string(a) and is_numeric_string(b): + # PATTERN: num1-num2 + select_range = int(a), int(b) + elif is_numeric_string(a): + # PATTERN: num + x = int(a) + select_range = (x, x) + + if select_range is not None and len(multi_select_pattern) == 2: + # PATTERN: count$$ + options[0] = multi_select_pattern[1] + elif select_range is not None and len(multi_select_pattern) == 3: + # PATTERN: count$$ sep $$ + select_sep = multi_select_pattern[1] + options[0] = multi_select_pattern[2] + + adjusted_probabilities = [] + + total_prob = 0 + + for option in options: + parts = option.split('::', 1) + if len(parts) == 2 and is_numeric_string(parts[0].strip()): + config_value = float(parts[0].strip()) + else: + config_value = 1 # Default value if no configuration is provided + + adjusted_probabilities.append(config_value) + total_prob += config_value + + normalized_probabilities = [prob / total_prob for prob in adjusted_probabilities] + + if select_range is None: + select_count = 1 + else: + select_count = random.randint(select_range[0], select_range[1]) + + if select_count > len(options): + selected_items = options + else: + selected_items = random.choices(options, weights=normalized_probabilities, k=select_count) + selected_items = set(selected_items) + + try_count = 0 + while len(selected_items) < select_count and try_count < 10: + remaining_count = select_count - len(selected_items) + additional_items = random.choices(options, weights=normalized_probabilities, k=remaining_count) + selected_items |= set(additional_items) + try_count += 1 + + selected_items2 = [re.sub(r'^\s*[0-9.]+::', '', x, 1) for x in selected_items] + replacement = select_sep.join(selected_items2) + if '::' in replacement: + pass + + replacements_found = True + return replacement + + pattern = r'{([^{}]*?)}' + replaced_string = re.sub(pattern, replace_option, string) + + return replaced_string, replacements_found + + def replace_wildcard(string): + global easy_wildcard_dict + pattern = r"__([\w\s.\-+/*\\]+?)__" + matches = re.findall(pattern, string) + replacements_found = False + + for match in matches: + keyword = match.lower() + keyword = wildcard_normalize(keyword) + if keyword in easy_wildcard_dict: + replacement = random.choice(easy_wildcard_dict[keyword]) + replacements_found = True + string = string.replace(f"__{match}__", replacement, 1) + elif '*' in keyword: + subpattern = keyword.replace('*', '.*').replace('+', r'\+') + total_patterns = [] + found = False + for k, v in easy_wildcard_dict.items(): + if re.match(subpattern, k) is not None: + total_patterns += v + found = True + + if found: + replacement = random.choice(total_patterns) + replacements_found = True + string = string.replace(f"__{match}__", replacement, 1) + elif '/' not in keyword: + string_fallback = string.replace(f"__{match}__", f"__*/{match}__", 1) + string, replacements_found = replace_wildcard(string_fallback) + + return string, replacements_found + + replace_depth = 100 + stop_unwrap = False + while not stop_unwrap and replace_depth > 1: + replace_depth -= 1 # prevent infinite loop + + # pass1: replace options + pass1, is_replaced1 = replace_options(text) + + while is_replaced1: + pass1, is_replaced1 = replace_options(pass1) + + # pass2: replace wildcards + text, is_replaced2 = replace_wildcard(pass1) + stop_unwrap = not is_replaced1 and not is_replaced2 + + return text + + +def is_numeric_string(input_str): + return re.match(r'^-?\d+(\.\d+)?$', input_str) is not None + + +def safe_float(x): + if is_numeric_string(x): + return float(x) + else: + return 1.0 + + +def extract_lora_values(string): + pattern = r']+)>' + matches = re.findall(pattern, string) + + def touch_lbw(text): + return re.sub(r'LBW=[A-Za-z][A-Za-z0-9_-]*:', r'LBW=', text) + + items = [touch_lbw(match.strip(':')) for match in matches] + + added = set() + result = [] + for item in items: + item = item.split(':') + + lora = None + a = None + b = None + lbw = None + lbw_a = None + lbw_b = None + + if len(item) > 0: + lora = item[0] + + for sub_item in item[1:]: + if is_numeric_string(sub_item): + if a is None: + a = float(sub_item) + elif b is None: + b = float(sub_item) + elif sub_item.startswith("LBW="): + for lbw_item in sub_item[4:].split(';'): + if lbw_item.startswith("A="): + lbw_a = safe_float(lbw_item[2:].strip()) + elif lbw_item.startswith("B="): + lbw_b = safe_float(lbw_item[2:].strip()) + elif lbw_item.strip() != '': + lbw = lbw_item + + if a is None: + a = 1.0 + if b is None: + b = 1.0 + + if lora is not None and lora not in added: + result.append((lora, a, b, lbw, lbw_a, lbw_b)) + added.add(lora) + + return result + + +def remove_lora_tags(string): + pattern = r']+>' + result = re.sub(pattern, '', string) + + return result + +def process_with_loras(wildcard_opt, model, clip, title="Positive", seed=None, can_load_lora=True, pipe_lora_stack=[], easyCache=None): + pass1 = process(wildcard_opt, seed) + loras = extract_lora_values(pass1) + pass2 = remove_lora_tags(pass1) + + has_noodle_key = True if "__" in wildcard_opt else False + has_loras = True if loras != [] else False + show_wildcard_prompt = True if has_noodle_key or has_loras else False + + if can_load_lora and has_loras: + for lora_name, model_weight, clip_weight, lbw, lbw_a, lbw_b in loras: + if (lora_name.split('.')[-1]) not in folder_paths.supported_pt_extensions: + lora_name = lora_name+".safetensors" + lora = { + "lora_name": lora_name, "model": model, "clip": clip, "model_strength": model_weight, + "clip_strength": clip_weight, + "lbw_a": lbw_a, + "lbw_b": lbw_b, + "lbw": lbw + } + model, clip = easyCache.load_lora(lora) + lora["model"] = model + lora["clip"] = clip + pipe_lora_stack.append(lora) + + log_node_info("easy wildcards",f"{title}: {pass2}") + if pass1 != pass2: + log_node_info("easy wildcards",f'{title}_decode: {pass1}') + + return model, clip, pass2, pass1, show_wildcard_prompt, pipe_lora_stack + + +def expand_wildcard(keyword: str) -> tuple[str]: + """传入文件通配符的关键词,从 easy_wildcard_dict 中获取通配符的所有选项。""" + global easy_wildcard_dict + if keyword in easy_wildcard_dict: + return tuple(easy_wildcard_dict[keyword]) + elif '*' in keyword: + subpattern = keyword.replace('*', '.*').replace('+', r"\+") + total_pattern = [] + for k, v in easy_wildcard_dict.items(): + if re.match(subpattern, k) is not None: + total_pattern.extend(v) + if total_pattern: + return tuple(total_pattern) + elif '/' not in keyword: + return expand_wildcard(f"*/{keyword}") + +def expand_options(options: str) -> tuple[str]: + """传入去掉 {} 的选项。 + 展开选项通配符,返回该选项中的每一项,这里的每一项都是一个替换项。 + 不会对选项内容进行任何处理,即便存在空格或特殊符号,也会原样返回。""" + return tuple(options.split("|")) + + +def decimal_to_irregular(n, bases): + """ + 将十进制数转换为不规则进制 + + :param n: 十进制数 + :param bases: 各位置的基数列表,从低位到高位 + :return: 不规则进制表示的列表,从低位到高位 + """ + if n == 0: + return [0] * len(bases) if bases else [0] + + digits = [] + remaining = n + + # 从低位到高位处理 + for base in bases: + digit = remaining % base + digits.append(digit) + remaining = remaining // base + + return digits + + +class WildcardProcessor: + """通配符处理器 + + 通配符格式: + + option : {a|b} + + wildcard: __keyword__ 通配符内容将从 Easy-Use 插件提供的 easy_wildcard_dict 中获取 + """ + + RE_OPTIONS = re.compile(r"{([^{}]*?)}") + RE_WILDCARD = re.compile(r"__([\w\s.\-+/*\\]+?)__") + RE_REPLACER = re.compile(r"{([^{}]*?)}|__([\w\s.\-+/*\\]+?)__") + + # 将输入的提示词转化成符合 python str.format 要求格式的模板,并将 option 和 wildcard 按照顺序在模板中留下 {0}, {1} 等占位符 + template: str + # option、wildcard 的替换项列表,按照在模板中出现的顺序排列,相同的替换项列表只保留第一份 + replacers: dict[int, tuple[str]] + # 占位符的编号和替换项列表的索引的映射,占位符编号按照在模板中出现的顺序排列,方便减少替换项的存储占用 + placeholder_mapping: dict[str, int] # placeholder_id => replacer_id + # 各替换项列表的项数,按照在模板中出现的顺序排列,提前计算,方便后续使用 + placeholder_choices: dict[str, int] # placeholder_id => len(replacer) + + def __init__(self, text: str): + self.__make_template(text) + self.__total = None + + def random(self, seed=None) -> str: + "从所有可能性中随机获取一个" + if seed is not None: + random.seed(seed) + return self.getn(random.randint(0, self.total() - 1)) + + def getn(self, n: int) -> str: + "从所有可能性中获取第 n 个,以 self.total() 为周期循环" + n = n % self.total() + indice = decimal_to_irregular(n, self.placeholder_choices.values()) + replacements = { + placeholder_id: self.replacers[self.placeholder_mapping[placeholder_id]][i] + for placeholder_id, i in zip(self.placeholder_mapping.keys(), indice) + } + return self.template.format(**replacements) + + def getmany(self, limit: int, offset: int = 0) -> list[str]: + """返回一组可能性组成的列表,为了避免结果太长导致内存占用超限,使用 limit 限制列表的长度,使用 offset 调整偏移。 + 若 limit 和 offset 的设置导致预期的结果长度超过剩下的实际长度,则会回到开头。 + """ + return [self.getn(n) for n in range(offset, offset + limit)] + + def total(self) -> int: + "计算可能性的数目" + if self.__total is None: + self.__total = prod(self.placeholder_choices.values()) + return self.__total + + def __make_template(self, text: str): + """将输入的提示词转化成符合 python str.format 要求格式的模板, + 并将 option 和 wildcard 按照顺序在模板中留下 {r0}, {r1} 等占位符, + 即使遇到相同的 option 或 wildcard,留下的占位符编号也不同,从而使每项都独立变化。 + """ + self.placeholder_mapping = {} + placeholder_id = 0 + replacer_id = 0 + replacers_rev = {} # replacers => id + blocks = [] + # 记录所处理过的通配符末尾在文本中的位置,用于拼接完整的模板 + tail = 0 + for match in self.RE_REPLACER.finditer(text): + # 提取并展开通配符内容 + m = match.group(0) + if m.startswith("{"): + choices = expand_options(m[1:-1]) + elif m.startswith("__"): + keyword = m[2:-2].lower() + keyword = wildcard_normalize(keyword) + choices = expand_wildcard(keyword) + else: + raise ValueError(f"{m!r} is not a wildcard or option") + + # 记录通配符的替换项列表和ID,相同的通配符只保留第一个 + if choices not in replacers_rev: + replacers_rev[choices] = replacer_id + replacer_id += 1 + + # 拼接通配符前方文本 + start, end = match.span() + blocks.append(text[tail:start]) + tail = end + # 将通配符替换为占位符,并记录占位符和替换项列表的索引的映射 + blocks.append(f"{{r{placeholder_id}}}") + self.placeholder_mapping[f"r{placeholder_id}"] = replacers_rev[choices] + placeholder_id += 1 + + if tail < len(text): + blocks.append(text[tail:]) + self.template = "".join(blocks) + self.replacers = {v: k for k, v in replacers_rev.items()} + self.placeholder_choices = { + placeholder_id: len(self.replacers[replacer_id]) + for placeholder_id, replacer_id in self.placeholder_mapping.items() + } + + +def test_option(): + text = "{|a|b|c}" + answer = ["", "a", "b", "c"] + p = WildcardProcessor(text) + assert p.total() == len(answer) + assert p.getn(0) == answer[0] + assert p.getmany(4) == answer + assert p.getmany(4, 1) == answer[1:] + + +def test_same(): + text = "{a|b},{a|b}" + answer = ["a,a", "b,a", "a,b", "b,b"] + p = WildcardProcessor(text) + assert p.total() == len(answer) + assert p.getn(0) == answer[0] + assert p.getmany(4) == answer + assert p.getmany(4, 1) == answer[1:] + diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/xyplot.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/xyplot.py new file mode 100644 index 0000000000000000000000000000000000000000..37c8a6fd04dbc91abd4b83a760a05f81e32102d7 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/libs/xyplot.py @@ -0,0 +1,719 @@ +import os, torch +from pathlib import Path +from PIL import Image, ImageDraw, ImageFont +from .utils import easySave, get_sd_version +from .adv_encode import advanced_encode +from .controlnet import easyControlnet +from .log import log_node_warn +from ..modules.layer_diffuse import LayerDiffuse +from ..config import RESOURCES_DIR +from nodes import CLIPTextEncode +import pprint +try: + from comfy_extras.nodes_flux import FluxGuidance +except: + FluxGuidance = None + +class easyXYPlot(): + + def __init__(self, xyPlotData, save_prefix, image_output, prompt, extra_pnginfo, my_unique_id, sampler, easyCache): + self.x_node_type, self.x_type = sampler.safe_split(xyPlotData.get("x_axis"), ': ') + self.y_node_type, self.y_type = sampler.safe_split(xyPlotData.get("y_axis"), ': ') + self.x_values = xyPlotData.get("x_vals") if self.x_type != "None" else [] + self.y_values = xyPlotData.get("y_vals") if self.y_type != "None" else [] + self.custom_font = xyPlotData.get("custom_font") + + self.grid_spacing = xyPlotData.get("grid_spacing") + self.latent_id = 0 + self.output_individuals = xyPlotData.get("output_individuals") + + self.x_label, self.y_label = [], [] + self.max_width, self.max_height = 0, 0 + self.latents_plot = [] + self.image_list = [] + + self.num_cols = len(self.x_values) if len(self.x_values) > 0 else 1 + self.num_rows = len(self.y_values) if len(self.y_values) > 0 else 1 + + self.total = self.num_cols * self.num_rows + self.num = 0 + + self.save_prefix = save_prefix + self.image_output = image_output + self.prompt = prompt + self.extra_pnginfo = extra_pnginfo + self.my_unique_id = my_unique_id + + self.sampler = sampler + self.easyCache = easyCache + + # Helper Functions + @staticmethod + def define_variable(plot_image_vars, value_type, value, index): + + plot_image_vars[value_type] = value + if value_type in ["seed", "Seeds++ Batch"]: + value_label = f"seed: {value}" + else: + value_label = f"{value_type}: {value}" + + if "ControlNet" in value_type: + value_label = f"ControlNet {index + 1}" + + if value_type in ['Lora', 'Checkpoint']: + arr = value.split(',') + model_name = os.path.basename(os.path.splitext(arr[0])[0]) + trigger_words = ' ' + arr[3] if value_type == 'Lora' and len(arr) > 3 and len(arr[3]) > 2 else '' + lora_weight = float(arr[1]) if value_type == 'Lora' and len(arr) > 1 else 0 + lora_weight_desc = f" w:{lora_weight:.2f}" if value_type == 'Lora' and lora_weight != 1.0 else '' + value_label = f"{model_name[:25]}{lora_weight_desc}{trigger_words}" + + if value_type in ["ModelMergeBlocks"]: + if ":" in value: + line = value.split(':') + value_label = f"{line[0]}" + elif len(value) > 16: + value_label = f"ModelMergeBlocks {index + 1}" + else: + value_label = f"MMB: {value}" + + if value_type in ["Pos Condition"]: + value_label = f"pos cond {index + 1}" if index>0 else f"pos cond" + if value_type in ["Neg Condition"]: + value_label = f"neg cond {index + 1}" if index>0 else f"neg cond" + + if value_type in ["Positive Prompt S/R"]: + value_label = f"pos prompt {index + 1}" if index>0 else f"pos prompt" + if value_type in ["Negative Prompt S/R"]: + value_label = f"neg prompt {index + 1}" if index>0 else f"neg prompt" + + if value_type in ["steps", "cfg", "denoise", "clip_skip", + "lora_model_strength", "lora_clip_strength"]: + value_label = f"{value_type}: {value}" + + if value_type == "positive": + value_label = f"pos prompt {index + 1}" + elif value_type == "negative": + value_label = f"neg prompt {index + 1}" + + return plot_image_vars, value_label + + @staticmethod + def get_font(font_size, font_path=None): + if font_path is None: + font_path = str(Path(os.path.join(RESOURCES_DIR, 'OpenSans-Medium.ttf'))) + return ImageFont.truetype(font_path, font_size) + + @staticmethod + def update_label(label, value, num_items): + if len(label) < num_items: + return [*label, value] + return label + + @staticmethod + def rearrange_tensors(latent, num_cols, num_rows): + new_latent = [] + for i in range(num_rows): + for j in range(num_cols): + index = j * num_rows + i + new_latent.append(latent[index]) + return new_latent + + def calculate_background_dimensions(self): + border_size = int((self.max_width // 8) * 1.5) if self.y_type != "None" or self.x_type != "None" else 0 + + bg_width = self.num_cols * (self.max_width + self.grid_spacing) - self.grid_spacing + border_size * ( + self.y_type != "None") + bg_height = self.num_rows * (self.max_height + self.grid_spacing) - self.grid_spacing + border_size * ( + self.x_type != "None") + + # Add space at the bottom of the image for common informaiton about the image + bg_height = bg_height + (border_size*2) +# print(f"Grid Size: width = {bg_width} height = {bg_height} border_size = {border_size}") + + x_offset_initial = border_size if self.y_type != "None" else 0 + y_offset = border_size if self.x_type != "None" else 0 + + return bg_width, bg_height, x_offset_initial, y_offset + + + def adjust_font_size(self, text, initial_font_size, label_width): + font = self.get_font(initial_font_size, self.custom_font) + text_width = font.getbbox(text) +# pprint.pp(f"Initial font size: {initial_font_size}, text: {text}, text_width: {text_width}") + if text_width and text_width[2]: + text_width = text_width[2] + + scaling_factor = 0.9 + if text_width > (label_width * scaling_factor): +# print(f"Adjusting font size from {initial_font_size} to fit text width {text_width} into label width {label_width} scaling_factor {scaling_factor}") + return int(initial_font_size * (label_width / text_width) * scaling_factor) + else: + return initial_font_size + + def textsize(self, d, text, font): + _, _, width, height = d.textbbox((0, 0), text=text, font=font) + return width, height + + def create_label(self, img, text, initial_font_size, is_x_label=True, max_font_size=70, min_font_size=10, label_width=0, label_height=0): + + # if the label_width is specified, leave it along. Otherwise do the old logic. + if label_width == 0: + label_width = img.width if is_x_label else img.height + + text_lines = text.split('\n') + longest_line = max(text_lines, key=len) + + # Adjust font size + font_size = self.adjust_font_size(longest_line, initial_font_size, label_width) + font_size = min(max_font_size, font_size) # Ensure font isn't too large + font_size = max(min_font_size, font_size) # Ensure font isn't too small + + if label_height == 0: + label_height = int(font_size * 1.5) if is_x_label else font_size + + label_bg = Image.new('RGBA', (label_width, label_height), color=(255, 255, 255, 0)) + d = ImageDraw.Draw(label_bg) + + font = self.get_font(font_size, self.custom_font) + + # Check if text will fit, if not insert ellipsis and reduce text + if self.textsize(d, text, font=font)[0] > label_width: + while self.textsize(d, text + '...', font=font)[0] > label_width and len(text) > 0: + text = text[:-1] + text = text + '...' + + # Compute text width and height for multi-line text + + text_widths, text_heights = zip(*[self.textsize(d, line, font=font) for line in text_lines]) + max_text_width = max(text_widths) + total_text_height = sum(text_heights) + + # Compute position for each line of text + lines_positions = [] + current_y = 0 + for line, line_width, line_height in zip(text_lines, text_widths, text_heights): + text_x = (label_width - line_width) // 2 + text_y = current_y + (label_height - total_text_height) // 2 + current_y += line_height + lines_positions.append((line, (text_x, text_y))) + + # Draw each line of text + for line, (text_x, text_y) in lines_positions: + d.text((text_x, text_y), line, fill='black', font=font) + + return label_bg + + def sample_plot_image(self, plot_image_vars, samples, preview_latent, latents_plot, image_list, disable_noise, + start_step, last_step, force_full_denoise, x_value=None, y_value=None): + model, clip, vae, positive, negative, seed, steps, cfg = None, None, None, None, None, None, None, None + sampler_name, scheduler, denoise = None, None, None + + a1111_prompt_style = plot_image_vars['a1111_prompt_style'] if "a1111_prompt_style" in plot_image_vars else False + clip = clip if clip is not None else plot_image_vars["clip"] + steps = plot_image_vars['steps'] if "steps" in plot_image_vars else 1 + + sd_version = get_sd_version(plot_image_vars['model']) + # 高级用法 + if plot_image_vars["x_node_type"] == "advanced" or plot_image_vars["y_node_type"] == "advanced": + if self.x_type == "Seeds++ Batch" or self.y_type == "Seeds++ Batch": + seed = int(x_value) if self.x_type == "Seeds++ Batch" else int(y_value) + if self.x_type == "Steps" or self.y_type == "Steps": + steps = int(x_value) if self.x_type == "Steps" else int(y_value) + if self.x_type == "StartStep" or self.y_type == "StartStep": + start_step = int(x_value) if self.x_type == "StartStep" else int(y_value) + if self.x_type == "EndStep" or self.y_type == "EndStep": + last_step = int(x_value) if self.x_type == "EndStep" else int(y_value) + if self.x_type == "CFG Scale" or self.y_type == "CFG Scale": + cfg = float(x_value) if self.x_type == "CFG Scale" else float(y_value) + if self.x_type == "Sampler" or self.y_type == "Sampler": + sampler_name = x_value if self.x_type == "Sampler" else y_value + if self.x_type == "Scheduler" or self.y_type == "Scheduler": + scheduler = x_value if self.x_type == "Scheduler" else y_value + if self.x_type == "Sampler&Scheduler" or self.y_type == "Sampler&Scheduler": + arr = x_value.split(',') if self.x_type == "Sampler&Scheduler" else y_value.split(',') + if arr[0] and arr[0]!= 'None': + sampler_name = arr[0] + if arr[1] and arr[1]!= 'None': + scheduler = arr[1] + if self.x_type == "Denoise" or self.y_type == "Denoise": + denoise = float(x_value) if self.x_type == "Denoise" else float(y_value) + if self.x_type == "Pos Condition" or self.y_type == "Pos Condition": + positive = plot_image_vars['positive_cond_stack'][int(x_value)] if self.x_type == "Pos Condition" else plot_image_vars['positive_cond_stack'][int(y_value)] + if self.x_type == "Neg Condition" or self.y_type == "Neg Condition": + negative = plot_image_vars['negative_cond_stack'][int(x_value)] if self.x_type == "Neg Condition" else plot_image_vars['negative_cond_stack'][int(y_value)] + # 模型叠加 + if self.x_type == "ModelMergeBlocks" or self.y_type == "ModelMergeBlocks": + ckpt_name_1, ckpt_name_2 = plot_image_vars['models'] + model1, clip1, vae1, clip_vision = self.easyCache.load_checkpoint(ckpt_name_1) + model2, clip2, vae2, clip_vision = self.easyCache.load_checkpoint(ckpt_name_2) + xy_values = x_value if self.x_type == "ModelMergeBlocks" else y_value + if ":" in xy_values: + xy_line = xy_values.split(':') + xy_values = xy_line[1] + + xy_arrs = xy_values.split(',') + # ModelMergeBlocks + if len(xy_arrs) == 3: + input, middle, out = xy_arrs + kwargs = { + "input": input, + "middle": middle, + "out": out + } + elif len(xy_arrs) == 30: + kwargs = {} + kwargs["time_embed."] = xy_arrs[0] + kwargs["label_emb."] = xy_arrs[1] + + for i in range(12): + kwargs["input_blocks.{}.".format(i)] = xy_arrs[2+i] + + for i in range(3): + kwargs["middle_block.{}.".format(i)] = xy_arrs[14+i] + + for i in range(12): + kwargs["output_blocks.{}.".format(i)] = xy_arrs[17+i] + + kwargs["out."] = xy_arrs[29] + else: + raise Exception("ModelMergeBlocks weight length error") + default_ratio = next(iter(kwargs.values())) + + m = model1.clone() + kp = model2.get_key_patches("diffusion_model.") + + for k in kp: + ratio = float(default_ratio) + k_unet = k[len("diffusion_model."):] + + last_arg_size = 0 + for arg in kwargs: + if k_unet.startswith(arg) and last_arg_size < len(arg): + ratio = float(kwargs[arg]) + last_arg_size = len(arg) + + m.add_patches({k: kp[k]}, 1.0 - ratio, ratio) + + vae_use = plot_image_vars['vae_use'] + + clip = clip2 if vae_use == 'Use Model 2' else clip1 + if vae_use == 'Use Model 2': + vae = vae2 + elif vae_use == 'Use Model 1': + vae = vae1 + else: + vae = self.easyCache.load_vae(vae_use) + model = m + + # 如果存在lora_stack叠加lora + optional_lora_stack = plot_image_vars['lora_stack'] + if optional_lora_stack is not None and optional_lora_stack != []: + for lora in optional_lora_stack: + model, clip = self.easyCache.load_lora(lora) + + # 处理clip + clip = clip.clone() + if plot_image_vars['clip_skip'] != 0: + clip.clip_layer(plot_image_vars['clip_skip']) + + # CheckPoint + if self.x_type == "Checkpoint" or self.y_type == "Checkpoint": + xy_values = x_value if self.x_type == "Checkpoint" else y_value + ckpt_name, clip_skip, vae_name = xy_values.split(",") + ckpt_name = ckpt_name.replace('*', ',') + vae_name = vae_name.replace('*', ',') + model, clip, vae, clip_vision = self.easyCache.load_checkpoint(ckpt_name) + if vae_name != 'None': + vae = self.easyCache.load_vae(vae_name) + + # 如果存在lora_stack叠加lora + optional_lora_stack = plot_image_vars['lora_stack'] + if optional_lora_stack is not None and optional_lora_stack != []: + for lora in optional_lora_stack: + lora['model'] = model + lora['clip'] = clip + model, clip = self.easyCache.load_lora(lora) + + # 处理clip + clip = clip.clone() + if clip_skip != 'None': + clip.clip_layer(int(clip_skip)) + positive = plot_image_vars['positive'] + negative = plot_image_vars['negative'] + a1111_prompt_style = plot_image_vars['a1111_prompt_style'] + steps = plot_image_vars['steps'] + clip = clip if clip is not None else plot_image_vars["clip"] + positive = advanced_encode(clip, positive, + plot_image_vars['positive_token_normalization'], + plot_image_vars['positive_weight_interpretation'], + w_max=1.0, + apply_to_pooled="enable", + a1111_prompt_style=a1111_prompt_style, steps=steps) + + negative = advanced_encode(clip, negative, + plot_image_vars['negative_token_normalization'], + plot_image_vars['negative_weight_interpretation'], + w_max=1.0, + apply_to_pooled="enable", + a1111_prompt_style=a1111_prompt_style, steps=steps) + if "positive_cond" in plot_image_vars: + positive = positive + plot_image_vars["positive_cond"] + if "negative_cond" in plot_image_vars: + negative = negative + plot_image_vars["negative_cond"] + + # Lora + if self.x_type == "Lora" or self.y_type == "Lora": +# print(f"Lora: {x_value} {y_value}") + model = model if model is not None else plot_image_vars["model"] + clip = clip if clip is not None else plot_image_vars["clip"] + + # Build lora_stack from both X and Y axes if both are LoRA types + lora_stack = [] + + # Add X axis LoRA if present + if self.x_type == "Lora": + lora_name, lora_model_strength, lora_clip_strength, _ = x_value.split(",") + lora_stack.append({"lora_name": lora_name, "model": model, "clip": clip, "model_strength": float(lora_model_strength), "clip_strength": float(lora_clip_strength)}) + + # Add Y axis LoRA if present + if self.y_type == "Lora": + lora_name, lora_model_strength, lora_clip_strength, _ = y_value.split(",") + lora_stack.append({"lora_name": lora_name, "model": model, "clip": clip, "model_strength": float(lora_model_strength), "clip_strength": float(lora_clip_strength)}) + +# print(f"new_lora_stack: {new_lora_stack}") + + + if 'lora_stack' in plot_image_vars: + lora_stack = lora_stack + plot_image_vars['lora_stack'] + + if lora_stack is not None and lora_stack != []: + for lora in lora_stack: + # Each generation of the model, must use the reference to previously created model / clip objects. + lora['model'] = model + lora['clip'] = clip + model, clip = self.easyCache.load_lora(lora) + + # 提示词 + if "Positive" in self.x_type or "Positive" in self.y_type: + if self.x_type == 'Positive Prompt S/R' or self.y_type == 'Positive Prompt S/R': + positive = x_value if self.x_type == "Positive Prompt S/R" else y_value + + if sd_version == 'flux': + positive, = CLIPTextEncode().encode(clip, positive) + else: + positive = advanced_encode(clip, positive, + plot_image_vars['positive_token_normalization'], + plot_image_vars['positive_weight_interpretation'], + w_max=1.0, + apply_to_pooled="enable", a1111_prompt_style=a1111_prompt_style, steps=steps) + + # if "positive_cond" in plot_image_vars: + # positive = positive + plot_image_vars["positive_cond"] + + if "Negative" in self.x_type or "Negative" in self.y_type: + if self.x_type == 'Negative Prompt S/R' or self.y_type == 'Negative Prompt S/R': + negative = x_value if self.x_type == "Negative Prompt S/R" else y_value + + if sd_version == 'flux': + negative, = CLIPTextEncode().encode(clip, negative) + else: + negative = advanced_encode(clip, negative, + plot_image_vars['negative_token_normalization'], + plot_image_vars['negative_weight_interpretation'], + w_max=1.0, + apply_to_pooled="enable", a1111_prompt_style=a1111_prompt_style, steps=steps) + # if "negative_cond" in plot_image_vars: + # negative = negative + plot_image_vars["negative_cond"] + + # ControlNet + if "ControlNet" in self.x_type or "ControlNet" in self.y_type: + cnet = plot_image_vars["cnet"] if "cnet" in plot_image_vars else None + positive = plot_image_vars["positive_cond"] if "positive" in plot_image_vars else None + negative = plot_image_vars["negative_cond"] if "negative" in plot_image_vars else None + if cnet: + index = x_value if "ControlNet" in self.x_type else y_value + controlnet = cnet[index] + for index, item in enumerate(controlnet): + control_net_name = item[0] + image = item[1] + strength = item[2] + start_percent = item[3] + end_percent = item[4] + provided_control_net = item[5] if len(item) > 5 else None + positive, negative = easyControlnet().apply(control_net_name, image, positive, negative, strength, start_percent, end_percent, provided_control_net, 1) + # Flux guidance + if self.x_type == "Flux Guidance" or self.y_type == "Flux Guidance": + positive = plot_image_vars["positive_cond"] if "positive" in plot_image_vars else None + flux_guidance = float(x_value) if self.x_type == "Flux Guidance" else float(y_value) + positive, = FluxGuidance().append(positive, flux_guidance) + + # 简单用法 + if plot_image_vars["x_node_type"] == "loader" or plot_image_vars["y_node_type"] == "loader": + if self.x_type == 'ckpt_name' or self.y_type == 'ckpt_name': + ckpt_name = x_value if self.x_type == "ckpt_name" else y_value + model, clip, vae, clip_vision = self.easyCache.load_checkpoint(ckpt_name) + + if self.x_type == 'lora_name' or self.y_type == 'lora_name': + model, clip, vae, clip_vision = self.easyCache.load_checkpoint(plot_image_vars['ckpt_name']) + lora_name = x_value if self.x_type == "lora_name" else y_value + lora = {"lora_name": lora_name, "model": model, "clip": clip, "model_strength": 1, "clip_strength": 1} + model, clip = self.easyCache.load_lora(lora) + + if self.x_type == 'lora_model_strength' or self.y_type == 'lora_model_strength': + model, clip, vae, clip_vision = self.easyCache.load_checkpoint(plot_image_vars['ckpt_name']) + lora_model_strength = float(x_value) if self.x_type == "lora_model_strength" else float(y_value) + lora = {"lora_name": plot_image_vars['lora_name'], "model": model, "clip": clip, "model_strength": lora_model_strength, "clip_strength": plot_image_vars['lora_clip_strength']} + model, clip = self.easyCache.load_lora(lora) + + if self.x_type == 'lora_clip_strength' or self.y_type == 'lora_clip_strength': + model, clip, vae, clip_vision = self.easyCache.load_checkpoint(plot_image_vars['ckpt_name']) + lora_clip_strength = float(x_value) if self.x_type == "lora_clip_strength" else float(y_value) + lora = {"lora_name": plot_image_vars['lora_name'], "model": model, "clip": clip, "model_strength": plot_image_vars['lora_model_strength'], "clip_strength": lora_clip_strength} + model, clip = self.easyCache.load_lora(lora) + + # Check for custom VAE + if self.x_type == 'vae_name' or self.y_type == 'vae_name': + vae_name = x_value if self.x_type == "vae_name" else y_value + vae = self.easyCache.load_vae(vae_name) + + # CLIP skip + if not clip: + raise Exception("No CLIP found") + clip = clip.clone() + clip.clip_layer(plot_image_vars['clip_skip']) + + if sd_version == 'flux': + positive, = CLIPTextEncode().encode(clip, positive) + else: + positive = advanced_encode(clip, plot_image_vars['positive'], + plot_image_vars['positive_token_normalization'], + plot_image_vars['positive_weight_interpretation'], w_max=1.0, + apply_to_pooled="enable",a1111_prompt_style=a1111_prompt_style, steps=steps) + + if sd_version == 'flux': + negative, = CLIPTextEncode().encode(clip, negative) + else: + negative = advanced_encode(clip, plot_image_vars['negative'], + plot_image_vars['negative_token_normalization'], + plot_image_vars['negative_weight_interpretation'], w_max=1.0, + apply_to_pooled="enable", a1111_prompt_style=a1111_prompt_style, steps=steps) + + + model = model if model is not None else plot_image_vars["model"] + vae = vae if vae is not None else plot_image_vars["vae"] + positive = positive if positive is not None else plot_image_vars["positive_cond"] + negative = negative if negative is not None else plot_image_vars["negative_cond"] + + seed = seed if seed is not None else plot_image_vars["seed"] + steps = steps if steps is not None else plot_image_vars["steps"] + cfg = cfg if cfg is not None else plot_image_vars["cfg"] + sampler_name = sampler_name if sampler_name is not None else plot_image_vars["sampler_name"] + scheduler = scheduler if scheduler is not None else plot_image_vars["scheduler"] + denoise = denoise if denoise is not None else plot_image_vars["denoise"] + + noise_device = plot_image_vars["noise_device"] if "noise_device" in plot_image_vars else 'cpu' + + # LayerDiffuse + layer_diffusion_method = plot_image_vars["layer_diffusion_method"] if "layer_diffusion_method" in plot_image_vars else None + empty_samples = plot_image_vars["empty_samples"] if "empty_samples" in plot_image_vars else None + + if layer_diffusion_method: + samp_blend_samples = plot_image_vars["blend_samples"] if "blend_samples" in plot_image_vars else None + additional_cond = plot_image_vars["layer_diffusion_cond"] if "layer_diffusion_cond" in plot_image_vars else None + + images = plot_image_vars["images"].movedim(-1, 1) if "images" in plot_image_vars else None + weight = plot_image_vars['layer_diffusion_weight'] if 'layer_diffusion_weight' in plot_image_vars else 1.0 + model, positive, negative = LayerDiffuse().apply_layer_diffusion(model, layer_diffusion_method, weight, samples, + samp_blend_samples, positive, + negative, images, additional_cond) + + samples = empty_samples if layer_diffusion_method is not None and empty_samples is not None else samples + # Sample + samples = self.sampler.common_ksampler(model, seed, steps, cfg, sampler_name, scheduler, positive, negative, samples, + denoise=denoise, disable_noise=disable_noise, preview_latent=preview_latent, + start_step=start_step, last_step=last_step, + force_full_denoise=force_full_denoise, noise_device=noise_device) + + # Decode images and store + latent = samples["samples"] + + # Add the latent tensor to the tensors list + latents_plot.append(latent) + + # Decode the image + image = vae.decode(latent).cpu() + + if self.output_individuals in [True, "True"]: + easySave(image, self.save_prefix, self.image_output) + + # Convert the image from tensor to PIL Image and add it to the list + pil_image = self.sampler.tensor2pil(image) + image_list.append(pil_image) + + # Update max dimensions + self.max_width = max(self.max_width, pil_image.width) + self.max_height = max(self.max_height, pil_image.height) + + # Return the touched variables + return image_list, self.max_width, self.max_height, latents_plot + + # Process Functions + def validate_xy_plot(self): + if self.x_type == 'None' and self.y_type == 'None': + log_node_warn(f'#{self.my_unique_id}','No Valid Plot Types - Reverting to default sampling...') + return False + else: + return True + + def get_latent(self, samples): + # Extract the 'samples' tensor from the dictionary + latent_image_tensor = samples["samples"] + + # Split the tensor into individual image tensors + image_tensors = torch.split(latent_image_tensor, 1, dim=0) + + # Create a list of dictionaries containing the individual image tensors + latent_list = [{'samples': image} for image in image_tensors] + + # Set latent only to the first latent of batch + if self.latent_id >= len(latent_list): + log_node_warn(f'#{self.my_unique_id}',f'The selected latent_id ({self.latent_id}) is out of range.') + log_node_warn(f'#{self.my_unique_id}', f'Automatically setting the latent_id to the last image in the list (index: {len(latent_list) - 1}).') + + self.latent_id = len(latent_list) - 1 + + return latent_list[self.latent_id] + + def get_labels_and_sample(self, plot_image_vars, latent_image, preview_latent, start_step, last_step, + force_full_denoise, disable_noise): + # Handle X-only variation (Y is "None") + if self.y_type == 'None': + for x_index, x_value in enumerate(self.x_values): + plot_image_vars, x_value_label = self.define_variable(plot_image_vars, self.x_type, x_value, x_index) + self.x_label = self.update_label(self.x_label, x_value_label, len(self.x_values)) + + self.image_list, self.max_width, self.max_height, self.latents_plot = self.sample_plot_image( + plot_image_vars, latent_image, preview_latent, self.latents_plot, self.image_list, + disable_noise, start_step, last_step, force_full_denoise, x_value) + self.num += 1 + # Handle Y-only variation (X is "None") + elif self.x_type == 'None': + for y_index, y_value in enumerate(self.y_values): + plot_image_vars, y_value_label = self.define_variable(plot_image_vars, self.y_type, y_value, y_index) + self.y_label = self.update_label(self.y_label, y_value_label, len(self.y_values)) + + self.image_list, self.max_width, self.max_height, self.latents_plot = self.sample_plot_image( + plot_image_vars, latent_image, preview_latent, self.latents_plot, self.image_list, + disable_noise, start_step, last_step, force_full_denoise, y_value=y_value) + self.num += 1 + # Handle both X and Y variation + else: + for x_index, x_value in enumerate(self.x_values): + plot_image_vars, x_value_label = self.define_variable(plot_image_vars, self.x_type, x_value, x_index) + self.x_label = self.update_label(self.x_label, x_value_label, len(self.x_values)) + + for y_index, y_value in enumerate(self.y_values): + plot_image_vars, y_value_label = self.define_variable(plot_image_vars, self.y_type, y_value, y_index) + self.y_label = self.update_label(self.y_label, y_value_label, len(self.y_values)) + + self.image_list, self.max_width, self.max_height, self.latents_plot = self.sample_plot_image( + plot_image_vars, latent_image, preview_latent, self.latents_plot, self.image_list, + disable_noise, start_step, last_step, force_full_denoise, x_value, y_value) + self.num += 1 + + # Rearrange latent array to match preview image grid + self.latents_plot = self.rearrange_tensors(self.latents_plot, self.num_cols, self.num_rows) + + # Concatenate the tensors along the first dimension (dim=0) + self.latents_plot = torch.cat(self.latents_plot, dim=0) + + return self.latents_plot + + def plot_images_and_labels(self, plot_image_vars): + + bg_width, bg_height, x_offset_initial, y_offset = self.calculate_background_dimensions() + + background = Image.new('RGBA', (int(bg_width), int(bg_height)), color=(255, 255, 255, 255)) + + output_image = [] + for row_index in range(self.num_rows): + x_offset = x_offset_initial + + for col_index in range(self.num_cols): + index = col_index * self.num_rows + row_index + img = self.image_list[index] + output_image.append(self.sampler.pil2tensor(img)) + background.paste(img, (x_offset, y_offset)) + + # Handle X label + if row_index == 0 and self.x_type != "None": + label_bg = self.create_label(img, self.x_label[col_index], int(48 * img.width / 512)) + label_y = (y_offset - label_bg.height) // 2 + background.alpha_composite(label_bg, (x_offset, label_y)) + + # Handle Y label + if col_index == 0 and self.y_type != "None": + label_bg = self.create_label(img, self.y_label[row_index], int(48 * img.height / 512), False) + label_bg = label_bg.rotate(90, expand=True) + + label_x = (x_offset - label_bg.width) // 2 + label_y = y_offset + (img.height - label_bg.height) // 2 + background.alpha_composite(label_bg, (label_x, label_y)) + + x_offset += img.width + self.grid_spacing + + y_offset += img.height + self.grid_spacing + + # lookup used models in the image + common_label = "" + # Update to add a function to do the heavy lifting. Parameters are plot_image_vars name, label to use, names of the axis, + + # pprint.pp(plot_image_vars) + + # We don't process LORAs here because there can be multiple of them. + labels = [ + {"id": "ckpt_name", "id_desc": "ckpt", "axis_type" : "Checkpoint"}, + {"id": "vae_name", "id_desc": '', "axis_type" : "vae_name"}, + {"id": "sampler_name", "id_desc": "sampler", "axis_type" : "Sampler"}, + {"id": "scheduler", "id_desc": '', "axis_type" : "Scheduler"}, + {"id": "steps", "id_desc": '', "axis_type" : "Steps"}, + {"id": "Flux Guidance", "id_desc": 'guidance', "axis_type" : "Flux Guidance"}, + {"id": "seed", "id_desc": '', "axis_type" : "Seeds++ Batch"} + ] + + for item in labels: + # Only add the label if it's not one of the axis + # print(f"Checking item: {item['id']} axis_type {item['axis_type']} x_type: {self.x_type} y_type: {self.y_type}") + if self.x_type != item['axis_type'] and self.y_type != item['axis_type']: + common_label += self.add_common_label(item['id'], plot_image_vars, item['id_desc']) + common_label += f"\n" + + if plot_image_vars['lora_stack'] is not None and plot_image_vars['lora_stack'] != []: +# print(f"lora_stack: {plot_image_vars['lora_stack']}") + for lora in plot_image_vars['lora_stack']: + + lora_name = lora['lora_name'] + lora_weight = lora['model_strength'] + if lora_name is not None and len(lora_name) > 0 and lora_weight > 0: + common_label += f"LORA: {lora_name} weight: {lora_weight:.2f} \n" + + common_label = common_label.strip() + + if len(common_label) > 0: + label_height = background.height - y_offset + label_bg = self.create_label(background, common_label, int(48 * background.width / 512), label_width=background.width, label_height=label_height) + label_x = (background.width - label_bg.width) // 2 + label_y = y_offset +# print(f"Adding common label: {common_label} x = {label_x} y = {label_y}") + background.alpha_composite(label_bg, (label_x, label_y)) + + return (self.sampler.pil2tensor(background), output_image) + + def add_common_label(self, tag, plot_image_vars, description = ''): + label = '' + if description == '': description = tag + if tag in plot_image_vars and plot_image_vars[tag] is not None and plot_image_vars[tag] != 'None': + label += f"{description}: {plot_image_vars[tag]} " +# print(f"add_common_label: {tag} description: {description} label: {label}" ) + return label diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/ben/model.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/ben/model.py new file mode 100644 index 0000000000000000000000000000000000000000..df9685bde45110772f5591385a4989ce8fc24675 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/ben/model.py @@ -0,0 +1,1310 @@ +import math +import torch +import torch.nn as nn +import torch.nn.functional as F +from einops import rearrange +import torch.utils.checkpoint as checkpoint +import numpy as np +from timm.models.layers import DropPath, to_2tuple, trunc_normal_ +from PIL import Image, ImageOps +from torchvision import transforms +import numpy as np +import random +import cv2 +import os +import subprocess +import time +import tempfile + + +def set_random_seed(seed): + random.seed(seed) + np.random.seed(seed) + torch.manual_seed(seed) + torch.cuda.manual_seed(seed) + torch.cuda.manual_seed_all(seed) + torch.backends.cudnn.deterministic = True + torch.backends.cudnn.benchmark = False + + +set_random_seed(9) + +torch.set_float32_matmul_precision('highest') + + +class Mlp(nn.Module): + """ Multilayer perceptron.""" + + def __init__(self, in_features, hidden_features=None, out_features=None, act_layer=nn.GELU, drop=0.): + super().__init__() + out_features = out_features or in_features + hidden_features = hidden_features or in_features + self.fc1 = nn.Linear(in_features, hidden_features) + self.act = act_layer() + self.fc2 = nn.Linear(hidden_features, out_features) + self.drop = nn.Dropout(drop) + + def forward(self, x): + x = self.fc1(x) + x = self.act(x) + x = self.drop(x) + x = self.fc2(x) + x = self.drop(x) + return x + + +def window_partition(x, window_size): + """ + Args: + x: (B, H, W, C) + window_size (int): window size + Returns: + windows: (num_windows*B, window_size, window_size, C) + """ + B, H, W, C = x.shape + x = x.view(B, H // window_size, window_size, W // window_size, window_size, C) + windows = x.permute(0, 1, 3, 2, 4, 5).contiguous().view(-1, window_size, window_size, C) + return windows + + +def window_reverse(windows, window_size, H, W): + """ + Args: + windows: (num_windows*B, window_size, window_size, C) + window_size (int): Window size + H (int): Height of image + W (int): Width of image + Returns: + x: (B, H, W, C) + """ + B = int(windows.shape[0] / (H * W / window_size / window_size)) + x = windows.view(B, H // window_size, W // window_size, window_size, window_size, -1) + x = x.permute(0, 1, 3, 2, 4, 5).contiguous().view(B, H, W, -1) + return x + + +class WindowAttention(nn.Module): + """ Window based multi-head self attention (W-MSA) module with relative position bias. + It supports both of shifted and non-shifted window. + Args: + dim (int): Number of input channels. + window_size (tuple[int]): The height and width of the window. + num_heads (int): Number of attention heads. + qkv_bias (bool, optional): If True, add a learnable bias to query, key, value. Default: True + qk_scale (float | None, optional): Override default qk scale of head_dim ** -0.5 if set + attn_drop (float, optional): Dropout ratio of attention weight. Default: 0.0 + proj_drop (float, optional): Dropout ratio of output. Default: 0.0 + """ + + def __init__(self, dim, window_size, num_heads, qkv_bias=True, qk_scale=None, attn_drop=0., proj_drop=0.): + + super().__init__() + self.dim = dim + self.window_size = window_size # Wh, Ww + self.num_heads = num_heads + head_dim = dim // num_heads + self.scale = qk_scale or head_dim ** -0.5 + + # define a parameter table of relative position bias + self.relative_position_bias_table = nn.Parameter( + torch.zeros((2 * window_size[0] - 1) * (2 * window_size[1] - 1), num_heads)) # 2*Wh-1 * 2*Ww-1, nH + + # get pair-wise relative position index for each token inside the window + coords_h = torch.arange(self.window_size[0]) + coords_w = torch.arange(self.window_size[1]) + coords = torch.stack(torch.meshgrid([coords_h, coords_w])) # 2, Wh, Ww + coords_flatten = torch.flatten(coords, 1) # 2, Wh*Ww + relative_coords = coords_flatten[:, :, None] - coords_flatten[:, None, :] # 2, Wh*Ww, Wh*Ww + relative_coords = relative_coords.permute(1, 2, 0).contiguous() # Wh*Ww, Wh*Ww, 2 + relative_coords[:, :, 0] += self.window_size[0] - 1 # shift to start from 0 + relative_coords[:, :, 1] += self.window_size[1] - 1 + relative_coords[:, :, 0] *= 2 * self.window_size[1] - 1 + relative_position_index = relative_coords.sum(-1) # Wh*Ww, Wh*Ww + self.register_buffer("relative_position_index", relative_position_index) + + self.qkv = nn.Linear(dim, dim * 3, bias=qkv_bias) + self.attn_drop = nn.Dropout(attn_drop) + self.proj = nn.Linear(dim, dim) + self.proj_drop = nn.Dropout(proj_drop) + + trunc_normal_(self.relative_position_bias_table, std=.02) + self.softmax = nn.Softmax(dim=-1) + + def forward(self, x, mask=None): + """ Forward function. + Args: + x: input features with shape of (num_windows*B, N, C) + mask: (0/-inf) mask with shape of (num_windows, Wh*Ww, Wh*Ww) or None + """ + B_, N, C = x.shape + qkv = self.qkv(x).reshape(B_, N, 3, self.num_heads, C // self.num_heads).permute(2, 0, 3, 1, 4) + q, k, v = qkv[0], qkv[1], qkv[2] # make torchscript happy (cannot use tensor as tuple) + + q = q * self.scale + attn = (q @ k.transpose(-2, -1)) + + relative_position_bias = self.relative_position_bias_table[self.relative_position_index.view(-1)].view( + self.window_size[0] * self.window_size[1], self.window_size[0] * self.window_size[1], -1) # Wh*Ww,Wh*Ww,nH + relative_position_bias = relative_position_bias.permute(2, 0, 1).contiguous() # nH, Wh*Ww, Wh*Ww + attn = attn + relative_position_bias.unsqueeze(0) + + if mask is not None: + nW = mask.shape[0] + attn = attn.view(B_ // nW, nW, self.num_heads, N, N) + mask.unsqueeze(1).unsqueeze(0) + attn = attn.view(-1, self.num_heads, N, N) + attn = self.softmax(attn) + else: + attn = self.softmax(attn) + + attn = self.attn_drop(attn) + + x = (attn @ v).transpose(1, 2).reshape(B_, N, C) + x = self.proj(x) + x = self.proj_drop(x) + return x + + +class SwinTransformerBlock(nn.Module): + """ Swin Transformer Block. + Args: + dim (int): Number of input channels. + num_heads (int): Number of attention heads. + window_size (int): Window size. + shift_size (int): Shift size for SW-MSA. + mlp_ratio (float): Ratio of mlp hidden dim to embedding dim. + qkv_bias (bool, optional): If True, add a learnable bias to query, key, value. Default: True + qk_scale (float | None, optional): Override default qk scale of head_dim ** -0.5 if set. + drop (float, optional): Dropout rate. Default: 0.0 + attn_drop (float, optional): Attention dropout rate. Default: 0.0 + drop_path (float, optional): Stochastic depth rate. Default: 0.0 + act_layer (nn.Module, optional): Activation layer. Default: nn.GELU + norm_layer (nn.Module, optional): Normalization layer. Default: nn.LayerNorm + """ + + def __init__(self, dim, num_heads, window_size=7, shift_size=0, + mlp_ratio=4., qkv_bias=True, qk_scale=None, drop=0., attn_drop=0., drop_path=0., + act_layer=nn.GELU, norm_layer=nn.LayerNorm): + super().__init__() + self.dim = dim + self.num_heads = num_heads + self.window_size = window_size + self.shift_size = shift_size + self.mlp_ratio = mlp_ratio + assert 0 <= self.shift_size < self.window_size, "shift_size must in 0-window_size" + + self.norm1 = norm_layer(dim) + self.attn = WindowAttention( + dim, window_size=to_2tuple(self.window_size), num_heads=num_heads, + qkv_bias=qkv_bias, qk_scale=qk_scale, attn_drop=attn_drop, proj_drop=drop) + + self.drop_path = DropPath(drop_path) if drop_path > 0. else nn.Identity() + self.norm2 = norm_layer(dim) + mlp_hidden_dim = int(dim * mlp_ratio) + self.mlp = Mlp(in_features=dim, hidden_features=mlp_hidden_dim, act_layer=act_layer, drop=drop) + + self.H = None + self.W = None + + def forward(self, x, mask_matrix): + """ Forward function. + Args: + x: Input feature, tensor size (B, H*W, C). + H, W: Spatial resolution of the input feature. + mask_matrix: Attention mask for cyclic shift. + """ + B, L, C = x.shape + H, W = self.H, self.W + assert L == H * W, "input feature has wrong size" + + shortcut = x + x = self.norm1(x) + x = x.view(B, H, W, C) + + # pad feature maps to multiples of window size + pad_l = pad_t = 0 + pad_r = (self.window_size - W % self.window_size) % self.window_size + pad_b = (self.window_size - H % self.window_size) % self.window_size + x = F.pad(x, (0, 0, pad_l, pad_r, pad_t, pad_b)) + _, Hp, Wp, _ = x.shape + + # cyclic shift + if self.shift_size > 0: + shifted_x = torch.roll(x, shifts=(-self.shift_size, -self.shift_size), dims=(1, 2)) + attn_mask = mask_matrix + else: + shifted_x = x + attn_mask = None + + # partition windows + x_windows = window_partition(shifted_x, self.window_size) # nW*B, window_size, window_size, C + x_windows = x_windows.view(-1, self.window_size * self.window_size, C) # nW*B, window_size*window_size, C + + # W-MSA/SW-MSA + attn_windows = self.attn(x_windows, mask=attn_mask) # nW*B, window_size*window_size, C + + # merge windows + attn_windows = attn_windows.view(-1, self.window_size, self.window_size, C) + shifted_x = window_reverse(attn_windows, self.window_size, Hp, Wp) # B H' W' C + + # reverse cyclic shift + if self.shift_size > 0: + x = torch.roll(shifted_x, shifts=(self.shift_size, self.shift_size), dims=(1, 2)) + else: + x = shifted_x + + if pad_r > 0 or pad_b > 0: + x = x[:, :H, :W, :].contiguous() + + x = x.view(B, H * W, C) + + # FFN + x = shortcut + self.drop_path(x) + x = x + self.drop_path(self.mlp(self.norm2(x))) + + return x + + +class PatchMerging(nn.Module): + """ Patch Merging Layer + Args: + dim (int): Number of input channels. + norm_layer (nn.Module, optional): Normalization layer. Default: nn.LayerNorm + """ + + def __init__(self, dim, norm_layer=nn.LayerNorm): + super().__init__() + self.dim = dim + self.reduction = nn.Linear(4 * dim, 2 * dim, bias=False) + self.norm = norm_layer(4 * dim) + + def forward(self, x, H, W): + """ Forward function. + Args: + x: Input feature, tensor size (B, H*W, C). + H, W: Spatial resolution of the input feature. + """ + B, L, C = x.shape + assert L == H * W, "input feature has wrong size" + + x = x.view(B, H, W, C) + + # padding + pad_input = (H % 2 == 1) or (W % 2 == 1) + if pad_input: + x = F.pad(x, (0, 0, 0, W % 2, 0, H % 2)) + + x0 = x[:, 0::2, 0::2, :] # B H/2 W/2 C + x1 = x[:, 1::2, 0::2, :] # B H/2 W/2 C + x2 = x[:, 0::2, 1::2, :] # B H/2 W/2 C + x3 = x[:, 1::2, 1::2, :] # B H/2 W/2 C + x = torch.cat([x0, x1, x2, x3], -1) # B H/2 W/2 4*C + x = x.view(B, -1, 4 * C) # B H/2*W/2 4*C + + x = self.norm(x) + x = self.reduction(x) + + return x + + +class BasicLayer(nn.Module): + """ A basic Swin Transformer layer for one stage. + Args: + dim (int): Number of feature channels + depth (int): Depths of this stage. + num_heads (int): Number of attention head. + window_size (int): Local window size. Default: 7. + mlp_ratio (float): Ratio of mlp hidden dim to embedding dim. Default: 4. + qkv_bias (bool, optional): If True, add a learnable bias to query, key, value. Default: True + qk_scale (float | None, optional): Override default qk scale of head_dim ** -0.5 if set. + drop (float, optional): Dropout rate. Default: 0.0 + attn_drop (float, optional): Attention dropout rate. Default: 0.0 + drop_path (float | tuple[float], optional): Stochastic depth rate. Default: 0.0 + norm_layer (nn.Module, optional): Normalization layer. Default: nn.LayerNorm + downsample (nn.Module | None, optional): Downsample layer at the end of the layer. Default: None + use_checkpoint (bool): Whether to use checkpointing to save memory. Default: False. + """ + + def __init__(self, + dim, + depth, + num_heads, + window_size=7, + mlp_ratio=4., + qkv_bias=True, + qk_scale=None, + drop=0., + attn_drop=0., + drop_path=0., + norm_layer=nn.LayerNorm, + downsample=None, + use_checkpoint=False): + super().__init__() + self.window_size = window_size + self.shift_size = window_size // 2 + self.depth = depth + self.use_checkpoint = use_checkpoint + + # build blocks + self.blocks = nn.ModuleList([ + SwinTransformerBlock( + dim=dim, + num_heads=num_heads, + window_size=window_size, + shift_size=0 if (i % 2 == 0) else window_size // 2, + mlp_ratio=mlp_ratio, + qkv_bias=qkv_bias, + qk_scale=qk_scale, + drop=drop, + attn_drop=attn_drop, + drop_path=drop_path[i] if isinstance(drop_path, list) else drop_path, + norm_layer=norm_layer) + for i in range(depth)]) + + # patch merging layer + if downsample is not None: + self.downsample = downsample(dim=dim, norm_layer=norm_layer) + else: + self.downsample = None + + def forward(self, x, H, W): + """ Forward function. + Args: + x: Input feature, tensor size (B, H*W, C). + H, W: Spatial resolution of the input feature. + """ + + # calculate attention mask for SW-MSA + Hp = int(np.ceil(H / self.window_size)) * self.window_size + Wp = int(np.ceil(W / self.window_size)) * self.window_size + img_mask = torch.zeros((1, Hp, Wp, 1), device=x.device) # 1 Hp Wp 1 + h_slices = (slice(0, -self.window_size), + slice(-self.window_size, -self.shift_size), + slice(-self.shift_size, None)) + w_slices = (slice(0, -self.window_size), + slice(-self.window_size, -self.shift_size), + slice(-self.shift_size, None)) + cnt = 0 + for h in h_slices: + for w in w_slices: + img_mask[:, h, w, :] = cnt + cnt += 1 + + mask_windows = window_partition(img_mask, self.window_size) # nW, window_size, window_size, 1 + mask_windows = mask_windows.view(-1, self.window_size * self.window_size) + attn_mask = mask_windows.unsqueeze(1) - mask_windows.unsqueeze(2) + attn_mask = attn_mask.masked_fill(attn_mask != 0, float(-100.0)).masked_fill(attn_mask == 0, float(0.0)) + + for blk in self.blocks: + blk.H, blk.W = H, W + if self.use_checkpoint: + x = checkpoint.checkpoint(blk, x, attn_mask) + else: + x = blk(x, attn_mask) + if self.downsample is not None: + x_down = self.downsample(x, H, W) + Wh, Ww = (H + 1) // 2, (W + 1) // 2 + return x, H, W, x_down, Wh, Ww + else: + return x, H, W, x, H, W + + +class PatchEmbed(nn.Module): + """ Image to Patch Embedding + Args: + patch_size (int): Patch token size. Default: 4. + in_chans (int): Number of input image channels. Default: 3. + embed_dim (int): Number of linear projection output channels. Default: 96. + norm_layer (nn.Module, optional): Normalization layer. Default: None + """ + + def __init__(self, patch_size=4, in_chans=3, embed_dim=96, norm_layer=None): + super().__init__() + patch_size = to_2tuple(patch_size) + self.patch_size = patch_size + + self.in_chans = in_chans + self.embed_dim = embed_dim + + self.proj = nn.Conv2d(in_chans, embed_dim, kernel_size=patch_size, stride=patch_size) + if norm_layer is not None: + self.norm = norm_layer(embed_dim) + else: + self.norm = None + + def forward(self, x): + """Forward function.""" + # padding + _, _, H, W = x.size() + if W % self.patch_size[1] != 0: + x = F.pad(x, (0, self.patch_size[1] - W % self.patch_size[1])) + if H % self.patch_size[0] != 0: + x = F.pad(x, (0, 0, 0, self.patch_size[0] - H % self.patch_size[0])) + + x = self.proj(x) # B C Wh Ww + if self.norm is not None: + Wh, Ww = x.size(2), x.size(3) + x = x.flatten(2).transpose(1, 2) + x = self.norm(x) + x = x.transpose(1, 2).view(-1, self.embed_dim, Wh, Ww) + + return x + + +class SwinTransformer(nn.Module): + """ Swin Transformer backbone. + A PyTorch impl of : `Swin Transformer: Hierarchical Vision Transformer using Shifted Windows` - + https://arxiv.org/pdf/2103.14030 + Args: + pretrain_img_size (int): Input image size for training the pretrained model, + used in absolute postion embedding. Default 224. + patch_size (int | tuple(int)): Patch size. Default: 4. + in_chans (int): Number of input image channels. Default: 3. + embed_dim (int): Number of linear projection output channels. Default: 96. + depths (tuple[int]): Depths of each Swin Transformer stage. + num_heads (tuple[int]): Number of attention head of each stage. + window_size (int): Window size. Default: 7. + mlp_ratio (float): Ratio of mlp hidden dim to embedding dim. Default: 4. + qkv_bias (bool): If True, add a learnable bias to query, key, value. Default: True + qk_scale (float): Override default qk scale of head_dim ** -0.5 if set. + drop_rate (float): Dropout rate. + attn_drop_rate (float): Attention dropout rate. Default: 0. + drop_path_rate (float): Stochastic depth rate. Default: 0.2. + norm_layer (nn.Module): Normalization layer. Default: nn.LayerNorm. + ape (bool): If True, add absolute position embedding to the patch embedding. Default: False. + patch_norm (bool): If True, add normalization after patch embedding. Default: True. + out_indices (Sequence[int]): Output from which stages. + frozen_stages (int): Stages to be frozen (stop grad and set eval mode). + -1 means not freezing any parameters. + use_checkpoint (bool): Whether to use checkpointing to save memory. Default: False. + """ + + def __init__(self, + pretrain_img_size=224, + patch_size=4, + in_chans=3, + embed_dim=96, + depths=[2, 2, 6, 2], + num_heads=[3, 6, 12, 24], + window_size=7, + mlp_ratio=4., + qkv_bias=True, + qk_scale=None, + drop_rate=0., + attn_drop_rate=0., + drop_path_rate=0.2, + norm_layer=nn.LayerNorm, + ape=False, + patch_norm=True, + out_indices=(0, 1, 2, 3), + frozen_stages=-1, + use_checkpoint=False): + super().__init__() + + self.pretrain_img_size = pretrain_img_size + self.num_layers = len(depths) + self.embed_dim = embed_dim + self.ape = ape + self.patch_norm = patch_norm + self.out_indices = out_indices + self.frozen_stages = frozen_stages + + # split image into non-overlapping patches + self.patch_embed = PatchEmbed( + patch_size=patch_size, in_chans=in_chans, embed_dim=embed_dim, + norm_layer=norm_layer if self.patch_norm else None) + + # absolute position embedding + if self.ape: + pretrain_img_size = to_2tuple(pretrain_img_size) + patch_size = to_2tuple(patch_size) + patches_resolution = [pretrain_img_size[0] // patch_size[0], pretrain_img_size[1] // patch_size[1]] + + self.absolute_pos_embed = nn.Parameter( + torch.zeros(1, embed_dim, patches_resolution[0], patches_resolution[1])) + trunc_normal_(self.absolute_pos_embed, std=.02) + + self.pos_drop = nn.Dropout(p=drop_rate) + + # stochastic depth + dpr = [x.item() for x in torch.linspace(0, drop_path_rate, sum(depths))] # stochastic depth decay rule + + # build layers + self.layers = nn.ModuleList() + for i_layer in range(self.num_layers): + layer = BasicLayer( + dim=int(embed_dim * 2 ** i_layer), + depth=depths[i_layer], + num_heads=num_heads[i_layer], + window_size=window_size, + mlp_ratio=mlp_ratio, + qkv_bias=qkv_bias, + qk_scale=qk_scale, + drop=drop_rate, + attn_drop=attn_drop_rate, + drop_path=dpr[sum(depths[:i_layer]):sum(depths[:i_layer + 1])], + norm_layer=norm_layer, + downsample=PatchMerging if (i_layer < self.num_layers - 1) else None, + use_checkpoint=use_checkpoint) + self.layers.append(layer) + + num_features = [int(embed_dim * 2 ** i) for i in range(self.num_layers)] + self.num_features = num_features + + # add a norm layer for each output + for i_layer in out_indices: + layer = norm_layer(num_features[i_layer]) + layer_name = f'norm{i_layer}' + self.add_module(layer_name, layer) + + self._freeze_stages() + + def _freeze_stages(self): + if self.frozen_stages >= 0: + self.patch_embed.eval() + for param in self.patch_embed.parameters(): + param.requires_grad = False + + if self.frozen_stages >= 1 and self.ape: + self.absolute_pos_embed.requires_grad = False + + if self.frozen_stages >= 2: + self.pos_drop.eval() + for i in range(0, self.frozen_stages - 1): + m = self.layers[i] + m.eval() + for param in m.parameters(): + param.requires_grad = False + + def forward(self, x): + + x = self.patch_embed(x) + + Wh, Ww = x.size(2), x.size(3) + if self.ape: + # interpolate the position embedding to the corresponding size + absolute_pos_embed = F.interpolate(self.absolute_pos_embed, size=(Wh, Ww), mode='bicubic') + x = (x + absolute_pos_embed) # B Wh*Ww C + + outs = [x.contiguous()] + x = x.flatten(2).transpose(1, 2) + x = self.pos_drop(x) + + for i in range(self.num_layers): + layer = self.layers[i] + x_out, H, W, x, Wh, Ww = layer(x, Wh, Ww) + + if i in self.out_indices: + norm_layer = getattr(self, f'norm{i}') + x_out = norm_layer(x_out) + + out = x_out.view(-1, H, W, self.num_features[i]).permute(0, 3, 1, 2).contiguous() + outs.append(out) + + return tuple(outs) + + +def get_activation_fn(activation): + """Return an activation function given a string""" + if activation == "gelu": + return F.gelu + + raise RuntimeError(F"activation should be gelu, not {activation}.") + + +def make_cbr(in_dim, out_dim): + return nn.Sequential(nn.Conv2d(in_dim, out_dim, kernel_size=3, padding=1), nn.InstanceNorm2d(out_dim), nn.GELU()) + + +def make_cbg(in_dim, out_dim): + return nn.Sequential(nn.Conv2d(in_dim, out_dim, kernel_size=3, padding=1), nn.InstanceNorm2d(out_dim), nn.GELU()) + + +def rescale_to(x, scale_factor: float = 2, interpolation='nearest'): + return F.interpolate(x, scale_factor=scale_factor, mode=interpolation) + + +def resize_as(x, y, interpolation='bilinear'): + return F.interpolate(x, size=y.shape[-2:], mode=interpolation) + + +def image2patches(x): + """b c (hg h) (wg w) -> (hg wg b) c h w""" + x = rearrange(x, 'b c (hg h) (wg w) -> (hg wg b) c h w', hg=2, wg=2) + return x + + +def patches2image(x): + """(hg wg b) c h w -> b c (hg h) (wg w)""" + x = rearrange(x, '(hg wg b) c h w -> b c (hg h) (wg w)', hg=2, wg=2) + return x + + +class PositionEmbeddingSine: + def __init__(self, num_pos_feats=64, temperature=10000, normalize=False, scale=None): + super().__init__() + self.num_pos_feats = num_pos_feats + self.temperature = temperature + self.normalize = normalize + if scale is not None and normalize is False: + raise ValueError("normalize should be True if scale is passed") + if scale is None: + scale = 2 * math.pi + self.scale = scale + self.dim_t = torch.arange(0, self.num_pos_feats, dtype=torch.float32) + + def __call__(self, b, h, w): + device = self.dim_t.device + mask = torch.zeros([b, h, w], dtype=torch.bool, device=device) + assert mask is not None + not_mask = ~mask + y_embed = not_mask.cumsum(dim=1, dtype=torch.float32) + x_embed = not_mask.cumsum(dim=2, dtype=torch.float32) + if self.normalize: + eps = 1e-6 + y_embed = (y_embed - 0.5) / (y_embed[:, -1:, :] + eps) * self.scale + x_embed = (x_embed - 0.5) / (x_embed[:, :, -1:] + eps) * self.scale + + dim_t = self.temperature ** (2 * (self.dim_t.to(device) // 2) / self.num_pos_feats) + pos_x = x_embed[:, :, :, None] / dim_t + pos_y = y_embed[:, :, :, None] / dim_t + + pos_x = torch.stack((pos_x[:, :, :, 0::2].sin(), pos_x[:, :, :, 1::2].cos()), dim=4).flatten(3) + pos_y = torch.stack((pos_y[:, :, :, 0::2].sin(), pos_y[:, :, :, 1::2].cos()), dim=4).flatten(3) + + return torch.cat((pos_y, pos_x), dim=3).permute(0, 3, 1, 2) + + +class PositionEmbeddingSine: + def __init__(self, num_pos_feats=64, temperature=10000, normalize=False, scale=None): + super().__init__() + self.num_pos_feats = num_pos_feats + self.temperature = temperature + self.normalize = normalize + if scale is not None and normalize is False: + raise ValueError("normalize should be True if scale is passed") + if scale is None: + scale = 2 * math.pi + self.scale = scale + self.dim_t = torch.arange(0, self.num_pos_feats, dtype=torch.float32) + + def __call__(self, b, h, w): + device = self.dim_t.device + mask = torch.zeros([b, h, w], dtype=torch.bool, device=device) + assert mask is not None + not_mask = ~mask + y_embed = not_mask.cumsum(dim=1, dtype=torch.float32) + x_embed = not_mask.cumsum(dim=2, dtype=torch.float32) + if self.normalize: + eps = 1e-6 + y_embed = (y_embed - 0.5) / (y_embed[:, -1:, :] + eps) * self.scale + x_embed = (x_embed - 0.5) / (x_embed[:, :, -1:] + eps) * self.scale + + dim_t = self.temperature ** (2 * (self.dim_t.to(device) // 2) / self.num_pos_feats) + pos_x = x_embed[:, :, :, None] / dim_t + pos_y = y_embed[:, :, :, None] / dim_t + + pos_x = torch.stack((pos_x[:, :, :, 0::2].sin(), pos_x[:, :, :, 1::2].cos()), dim=4).flatten(3) + pos_y = torch.stack((pos_y[:, :, :, 0::2].sin(), pos_y[:, :, :, 1::2].cos()), dim=4).flatten(3) + + return torch.cat((pos_y, pos_x), dim=3).permute(0, 3, 1, 2) + + +class MCLM(nn.Module): + def __init__(self, d_model, num_heads, pool_ratios=[1, 4, 8]): + super(MCLM, self).__init__() + self.attention = nn.ModuleList([ + nn.MultiheadAttention(d_model, num_heads, dropout=0.1), + nn.MultiheadAttention(d_model, num_heads, dropout=0.1), + nn.MultiheadAttention(d_model, num_heads, dropout=0.1), + nn.MultiheadAttention(d_model, num_heads, dropout=0.1), + nn.MultiheadAttention(d_model, num_heads, dropout=0.1) + ]) + + self.linear1 = nn.Linear(d_model, d_model * 2) + self.linear2 = nn.Linear(d_model * 2, d_model) + self.linear3 = nn.Linear(d_model, d_model * 2) + self.linear4 = nn.Linear(d_model * 2, d_model) + self.norm1 = nn.LayerNorm(d_model) + self.norm2 = nn.LayerNorm(d_model) + self.dropout = nn.Dropout(0.1) + self.dropout1 = nn.Dropout(0.1) + self.dropout2 = nn.Dropout(0.1) + self.activation = get_activation_fn('gelu') + self.pool_ratios = pool_ratios + self.p_poses = [] + self.g_pos = None + self.positional_encoding = PositionEmbeddingSine(num_pos_feats=d_model // 2, normalize=True) + + def forward(self, l, g): + """ + l: 4,c,h,w + g: 1,c,h,w + """ + self.p_poses = [] + self.g_pos = None + b, c, h, w = l.size() + # 4,c,h,w -> 1,c,2h,2w + concated_locs = rearrange(l, '(hg wg b) c h w -> b c (hg h) (wg w)', hg=2, wg=2) + + pools = [] + for pool_ratio in self.pool_ratios: + # b,c,h,w + tgt_hw = (round(h / pool_ratio), round(w / pool_ratio)) + pool = F.adaptive_avg_pool2d(concated_locs, tgt_hw) + pools.append(rearrange(pool, 'b c h w -> (h w) b c')) + if self.g_pos is None: + pos_emb = self.positional_encoding(pool.shape[0], pool.shape[2], pool.shape[3]) + pos_emb = rearrange(pos_emb, 'b c h w -> (h w) b c') + self.p_poses.append(pos_emb) + pools = torch.cat(pools, 0) + if self.g_pos is None: + self.p_poses = torch.cat(self.p_poses, dim=0) + pos_emb = self.positional_encoding(g.shape[0], g.shape[2], g.shape[3]) + self.g_pos = rearrange(pos_emb, 'b c h w -> (h w) b c') + + device = pools.device + self.p_poses = self.p_poses.to(device) + self.g_pos = self.g_pos.to(device) + + # attention between glb (q) & multisensory concated-locs (k,v) + g_hw_b_c = rearrange(g, 'b c h w -> (h w) b c') + + g_hw_b_c = g_hw_b_c + self.dropout1(self.attention[0](g_hw_b_c + self.g_pos, pools + self.p_poses, pools)[0]) + g_hw_b_c = self.norm1(g_hw_b_c) + g_hw_b_c = g_hw_b_c + self.dropout2(self.linear2(self.dropout(self.activation(self.linear1(g_hw_b_c)).clone()))) + g_hw_b_c = self.norm2(g_hw_b_c) + + # attention between origin locs (q) & freashed glb (k,v) + l_hw_b_c = rearrange(l, "b c h w -> (h w) b c") + _g_hw_b_c = rearrange(g_hw_b_c, '(h w) b c -> h w b c', h=h, w=w) + _g_hw_b_c = rearrange(_g_hw_b_c, "(ng h) (nw w) b c -> (h w) (ng nw b) c", ng=2, nw=2) + outputs_re = [] + for i, (_l, _g) in enumerate(zip(l_hw_b_c.chunk(4, dim=1), _g_hw_b_c.chunk(4, dim=1))): + outputs_re.append(self.attention[i + 1](_l, _g, _g)[0]) # (h w) 1 c + outputs_re = torch.cat(outputs_re, 1) # (h w) 4 c + + l_hw_b_c = l_hw_b_c + self.dropout1(outputs_re) + l_hw_b_c = self.norm1(l_hw_b_c) + l_hw_b_c = l_hw_b_c + self.dropout2(self.linear4(self.dropout(self.activation(self.linear3(l_hw_b_c)).clone()))) + l_hw_b_c = self.norm2(l_hw_b_c) + + l = torch.cat((l_hw_b_c, g_hw_b_c), 1) # hw,b(5),c + return rearrange(l, "(h w) b c -> b c h w", h=h, w=w) ## (5,c,h*w) + + +class MCRM(nn.Module): + def __init__(self, d_model, num_heads, pool_ratios=[4, 8, 16], h=None): + super(MCRM, self).__init__() + self.attention = nn.ModuleList([ + nn.MultiheadAttention(d_model, num_heads, dropout=0.1), + nn.MultiheadAttention(d_model, num_heads, dropout=0.1), + nn.MultiheadAttention(d_model, num_heads, dropout=0.1), + nn.MultiheadAttention(d_model, num_heads, dropout=0.1) + ]) + self.linear3 = nn.Linear(d_model, d_model * 2) + self.linear4 = nn.Linear(d_model * 2, d_model) + self.norm1 = nn.LayerNorm(d_model) + self.norm2 = nn.LayerNorm(d_model) + self.dropout = nn.Dropout(0.1) + self.dropout1 = nn.Dropout(0.1) + self.dropout2 = nn.Dropout(0.1) + self.sigmoid = nn.Sigmoid() + self.activation = get_activation_fn('gelu') + self.sal_conv = nn.Conv2d(d_model, 1, 1) + self.pool_ratios = pool_ratios + + def forward(self, x): + device = x.device + b, c, h, w = x.size() + loc, glb = x.split([4, 1], dim=0) # 4,c,h,w; 1,c,h,w + + patched_glb = rearrange(glb, 'b c (hg h) (wg w) -> (hg wg b) c h w', hg=2, wg=2) + + token_attention_map = self.sigmoid(self.sal_conv(glb)) + token_attention_map = F.interpolate(token_attention_map, size=patches2image(loc).shape[-2:], mode='nearest') + loc = loc * rearrange(token_attention_map, 'b c (hg h) (wg w) -> (hg wg b) c h w', hg=2, wg=2) + + pools = [] + for pool_ratio in self.pool_ratios: + tgt_hw = (round(h / pool_ratio), round(w / pool_ratio)) + pool = F.adaptive_avg_pool2d(patched_glb, tgt_hw) + pools.append(rearrange(pool, 'nl c h w -> nl c (h w)')) # nl(4),c,hw + + pools = rearrange(torch.cat(pools, 2), "nl c nphw -> nl nphw 1 c") + loc_ = rearrange(loc, 'nl c h w -> nl (h w) 1 c') + + outputs = [] + for i, q in enumerate(loc_.unbind(dim=0)): # traverse all local patches + v = pools[i] + k = v + outputs.append(self.attention[i](q, k, v)[0]) + + outputs = torch.cat(outputs, 1) + src = loc.view(4, c, -1).permute(2, 0, 1) + self.dropout1(outputs) + src = self.norm1(src) + src = src + self.dropout2(self.linear4(self.dropout(self.activation(self.linear3(src)).clone()))) + src = self.norm2(src) + src = src.permute(1, 2, 0).reshape(4, c, h, w) # freshed loc + glb = glb + F.interpolate(patches2image(src), size=glb.shape[-2:], mode='nearest') # freshed glb + + return torch.cat((src, glb), 0), token_attention_map + + +class BEN_Base(nn.Module): + def __init__(self): + super().__init__() + + self.backbone = SwinTransformer(embed_dim=128, depths=[2, 2, 18, 2], num_heads=[4, 8, 16, 32], window_size=12) + emb_dim = 128 + self.sideout5 = nn.Sequential(nn.Conv2d(emb_dim, 1, kernel_size=3, padding=1)) + self.sideout4 = nn.Sequential(nn.Conv2d(emb_dim, 1, kernel_size=3, padding=1)) + self.sideout3 = nn.Sequential(nn.Conv2d(emb_dim, 1, kernel_size=3, padding=1)) + self.sideout2 = nn.Sequential(nn.Conv2d(emb_dim, 1, kernel_size=3, padding=1)) + self.sideout1 = nn.Sequential(nn.Conv2d(emb_dim, 1, kernel_size=3, padding=1)) + + self.output5 = make_cbr(1024, emb_dim) + self.output4 = make_cbr(512, emb_dim) + self.output3 = make_cbr(256, emb_dim) + self.output2 = make_cbr(128, emb_dim) + self.output1 = make_cbr(128, emb_dim) + + self.multifieldcrossatt = MCLM(emb_dim, 1, [1, 4, 8]) + self.conv1 = make_cbr(emb_dim, emb_dim) + self.conv2 = make_cbr(emb_dim, emb_dim) + self.conv3 = make_cbr(emb_dim, emb_dim) + self.conv4 = make_cbr(emb_dim, emb_dim) + self.dec_blk1 = MCRM(emb_dim, 1, [2, 4, 8]) + self.dec_blk2 = MCRM(emb_dim, 1, [2, 4, 8]) + self.dec_blk3 = MCRM(emb_dim, 1, [2, 4, 8]) + self.dec_blk4 = MCRM(emb_dim, 1, [2, 4, 8]) + + self.insmask_head = nn.Sequential( + nn.Conv2d(emb_dim, 384, kernel_size=3, padding=1), + nn.InstanceNorm2d(384), + nn.GELU(), + nn.Conv2d(384, 384, kernel_size=3, padding=1), + nn.InstanceNorm2d(384), + nn.GELU(), + nn.Conv2d(384, emb_dim, kernel_size=3, padding=1) + ) + + self.shallow = nn.Sequential(nn.Conv2d(3, emb_dim, kernel_size=3, padding=1)) + self.upsample1 = make_cbg(emb_dim, emb_dim) + self.upsample2 = make_cbg(emb_dim, emb_dim) + self.output = nn.Sequential(nn.Conv2d(emb_dim, 1, kernel_size=3, padding=1)) + + for m in self.modules(): + if isinstance(m, nn.GELU) or isinstance(m, nn.Dropout): + m.inplace = True + + @torch.inference_mode() + @torch.autocast(device_type="cuda", dtype=torch.float16) + def forward(self, x): + real_batch = x.size(0) + + shallow_batch = self.shallow(x) + glb_batch = rescale_to(x, scale_factor=0.5, interpolation='bilinear') + + final_input = None + for i in range(real_batch): + start = i * 4 + end = (i + 1) * 4 + loc_batch = image2patches(x[i, :, :, :].unsqueeze(dim=0)) + input_ = torch.cat((loc_batch, glb_batch[i, :, :, :].unsqueeze(dim=0)), dim=0) + + if final_input == None: + final_input = input_ + else: + final_input = torch.cat((final_input, input_), dim=0) + + features = self.backbone(final_input) + outputs = [] + + for i in range(real_batch): + start = i * 5 + end = (i + 1) * 5 + + f4 = features[4][start:end, :, :, :] # shape: [5, C, H, W] + f3 = features[3][start:end, :, :, :] + f2 = features[2][start:end, :, :, :] + f1 = features[1][start:end, :, :, :] + f0 = features[0][start:end, :, :, :] + e5 = self.output5(f4) + e4 = self.output4(f3) + e3 = self.output3(f2) + e2 = self.output2(f1) + e1 = self.output1(f0) + loc_e5, glb_e5 = e5.split([4, 1], dim=0) + e5 = self.multifieldcrossatt(loc_e5, glb_e5) # (4,128,16,16) + + e4, tokenattmap4 = self.dec_blk4(e4 + resize_as(e5, e4)) + e4 = self.conv4(e4) + e3, tokenattmap3 = self.dec_blk3(e3 + resize_as(e4, e3)) + e3 = self.conv3(e3) + e2, tokenattmap2 = self.dec_blk2(e2 + resize_as(e3, e2)) + e2 = self.conv2(e2) + e1, tokenattmap1 = self.dec_blk1(e1 + resize_as(e2, e1)) + e1 = self.conv1(e1) + + loc_e1, glb_e1 = e1.split([4, 1], dim=0) + + output1_cat = patches2image(loc_e1) # (1,128,256,256) + + # add glb feat in + output1_cat = output1_cat + resize_as(glb_e1, output1_cat) + # merge + final_output = self.insmask_head(output1_cat) # (1,128,256,256) + # shallow feature merge + shallow = shallow_batch[i, :, :, :].unsqueeze(dim=0) + final_output = final_output + resize_as(shallow, final_output) + final_output = self.upsample1(rescale_to(final_output)) + final_output = rescale_to(final_output + resize_as(shallow, final_output)) + final_output = self.upsample2(final_output) + final_output = self.output(final_output) + mask = final_output.sigmoid() + outputs.append(mask) + + return torch.cat(outputs, dim=0) + + def loadcheckpoints(self, model_path): + model_dict = torch.load(model_path, map_location="cpu", weights_only=True) + self.load_state_dict(model_dict['model_state_dict'], strict=True) + del model_path + + def inference(self, image, refine_foreground=False): + + set_random_seed(9) + # image = ImageOps.exif_transpose(image) + if isinstance(image, Image.Image): + image, h, w, original_image = rgb_loader_refiner(image) + if torch.cuda.is_available(): + + img_tensor = img_transform(image).unsqueeze(0).to(next(self.parameters()).device) + else: + img_tensor = img_transform32(image).unsqueeze(0).to(next(self.parameters()).device) + + with torch.no_grad(): + res = self.forward(img_tensor) + + # Show Results + if refine_foreground == True: + + pred_pil = transforms.ToPILImage()(res.squeeze()) + image_masked = refine_foreground_process(original_image, pred_pil) + mask = pred_pil.resize(original_image.size) + image_masked.putalpha(mask) + return mask, image_masked + + else: + alpha = postprocess_image(res, im_size=[w, h]) + pred_pil = transforms.ToPILImage()(alpha) + mask = pred_pil.resize(original_image.size) + original_image.putalpha(mask) + # mask = Image.fromarray(alpha) + + return mask, original_image + + def segment_video(self, video_path, output_path="./", fps=0, refine_foreground=False, batch=1, + print_frames_processed=True, webm=False, rgb_value=(0, 255, 0)): + + """ + Segments the given video to extract the foreground (with alpha) from each frame + and saves the result as either a WebM video (with alpha channel) or MP4 (with a + color background). + Args: + video_path (str): + Path to the input video file. + output_path (str, optional): + Directory (or full path) where the output video and/or files will be saved. + Defaults to "./". + fps (int, optional): + The frames per second (FPS) to use for the output video. If 0 (default), the + original FPS of the input video is used. Otherwise, overrides it. + refine_foreground (bool, optional): + Whether to run an additional “refine foreground” process on each frame. + Defaults to False. + batch (int, optional): + Number of frames to process at once (inference batch size). Large batch sizes + may require more GPU memory. Defaults to 1. + print_frames_processed (bool, optional): + If True (default), prints progress (how many frames have been processed) to + the console. + webm (bool, optional): + If True (default), exports a WebM video with alpha channel (VP9 / yuva420p). + If False, exports an MP4 video composited over a solid color background. + rgb_value (tuple, optional): + The RGB background color (e.g., green screen) used to composite frames when + saving to MP4. Defaults to (0, 255, 0). + Returns: + None. Writes the output video(s) to disk in the specified format. + """ + + cap = cv2.VideoCapture(video_path) + if not cap.isOpened(): + raise IOError(f"Cannot open video: {video_path}") + + original_fps = cap.get(cv2.CAP_PROP_FPS) + original_fps = 30 if original_fps == 0 else original_fps + fps = original_fps if fps == 0 else fps + + ret, first_frame = cap.read() + if not ret: + raise ValueError("No frames found in the video.") + height, width = first_frame.shape[:2] + cap.set(cv2.CAP_PROP_POS_FRAMES, 0) + + foregrounds = [] + frame_idx = 0 + processed_count = 0 + batch_frames = [] + total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) + + while True: + ret, frame = cap.read() + if not ret: + if batch_frames: + batch_results = self.inference(batch_frames, refine_foreground) + if isinstance(batch_results, Image.Image): + foregrounds.append(batch_results) + else: + foregrounds.extend(batch_results) + if print_frames_processed: + print(f"Processed frames {frame_idx - len(batch_frames) + 1} to {frame_idx} of {total_frames}") + break + + # Process every frame instead of using intervals + frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) + pil_frame = Image.fromarray(frame_rgb) + batch_frames.append(pil_frame) + + if len(batch_frames) == batch: + batch_results = self.inference(batch_frames, refine_foreground) + if isinstance(batch_results, Image.Image): + foregrounds.append(batch_results) + else: + foregrounds.extend(batch_results) + if print_frames_processed: + print(f"Processed frames {frame_idx - batch + 1} to {frame_idx} of {total_frames}") + batch_frames = [] + processed_count += batch + + frame_idx += 1 + + if webm: + alpha_webm_path = os.path.join(output_path, "foreground.webm") + pil_images_to_webm_alpha(foregrounds, alpha_webm_path, fps=original_fps) + + else: + cap.release() + fg_output = os.path.join(output_path, 'foreground.mp4') + + pil_images_to_mp4(foregrounds, fg_output, fps=original_fps, rgb_value=rgb_value) + cv2.destroyAllWindows() + + try: + fg_audio_output = os.path.join(output_path, 'foreground_output_with_audio.mp4') + add_audio_to_video(fg_output, video_path, fg_audio_output) + except Exception as e: + print("No audio found in the original video") + print(e) + + +def rgb_loader_refiner(original_image): + h, w = original_image.size + + image = original_image + # Convert to RGB if necessary + if image.mode != 'RGB': + image = image.convert('RGB') + + # Resize the image + image = image.resize((1024, 1024), resample=Image.LANCZOS) + + return image.convert('RGB'), h, w, original_image + + +# Define the image transformation +img_transform = transforms.Compose([ + transforms.ToTensor(), + transforms.ConvertImageDtype(torch.float16), + transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) +]) + +img_transform32 = transforms.Compose([ + transforms.ToTensor(), + transforms.ConvertImageDtype(torch.float32), + transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) +]) + + +def pil_images_to_mp4(images, output_path, fps=24, rgb_value=(0, 255, 0)): + """ + Converts an array of PIL images to an MP4 video. + + Args: + images: List of PIL images + output_path: Path to save the MP4 file + fps: Frames per second (default: 24) + rgb_value: Background RGB color tuple (default: green (0, 255, 0)) + """ + if not images: + raise ValueError("No images provided to convert to MP4.") + + width, height = images[0].size + fourcc = cv2.VideoWriter_fourcc(*'mp4v') + video_writer = cv2.VideoWriter(output_path, fourcc, fps, (width, height)) + + for image in images: + # If image has alpha channel, composite onto the specified background color + if image.mode == 'RGBA': + # Create background image with specified RGB color + background = Image.new('RGB', image.size, rgb_value) + background = background.convert('RGBA') + # Composite the image onto the background + image = Image.alpha_composite(background, image) + image = image.convert('RGB') + else: + # Ensure RGB format for non-alpha images + image = image.convert('RGB') + + # Convert to OpenCV format and write + open_cv_image = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR) + video_writer.write(open_cv_image) + + video_writer.release() + + +def pil_images_to_webm_alpha(images, output_path, fps=30): + """ + Converts a list of PIL RGBA images to a VP9 .webm video with alpha channel. + NOTE: Not all players will display alpha in WebM. + Browsers like Chrome/Firefox typically do support VP9 alpha. + """ + if not images: + raise ValueError("No images provided for WebM with alpha.") + + # Ensure output directory exists + os.makedirs(os.path.dirname(output_path), exist_ok=True) + + with tempfile.TemporaryDirectory() as tmpdir: + # Save frames as PNG (with alpha) + for idx, img in enumerate(images): + if img.mode != "RGBA": + img = img.convert("RGBA") + out_path = os.path.join(tmpdir, f"{idx:06d}.png") + img.save(out_path, "PNG") + + # Construct ffmpeg command + # -c:v libvpx-vp9 => VP9 encoder + # -pix_fmt yuva420p => alpha-enabled pixel format + # -auto-alt-ref 0 => helps preserve alpha frames (libvpx quirk) + ffmpeg_cmd = [ + "ffmpeg", "-y", + "-framerate", str(fps), + "-i", os.path.join(tmpdir, "%06d.png"), + "-c:v", "libvpx-vp9", + "-pix_fmt", "yuva420p", + "-auto-alt-ref", "0", + output_path + ] + + subprocess.run(ffmpeg_cmd, check=True) + + print(f"WebM with alpha saved to {output_path}") + + +def add_audio_to_video(video_without_audio_path, original_video_path, output_path): + """ + Check if the original video has an audio stream. If yes, add it. If not, skip. + """ + # 1) Probe original video for audio streams + probe_command = [ + 'ffprobe', '-v', 'error', + '-select_streams', 'a:0', + '-show_entries', 'stream=index', + '-of', 'csv=p=0', + original_video_path + ] + result = subprocess.run(probe_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + + # result.stdout is empty if no audio stream found + if not result.stdout.strip(): + print("No audio track found in original video, skipping audio addition.") + return + + print("Audio track detected; proceeding to mux audio.") + # 2) If audio found, run ffmpeg to add it + command = [ + 'ffmpeg', '-y', + '-i', video_without_audio_path, + '-i', original_video_path, + '-c', 'copy', + '-map', '0:v:0', + '-map', '1:a:0', # we know there's an audio track now + output_path + ] + subprocess.run(command, check=True) + print(f"Audio added successfully => {output_path}") + + +### Thanks to the source: https://huggingface.co/ZhengPeng7/BiRefNet/blob/main/handler.py +def refine_foreground_process(image, mask, r=90): + if mask.size != image.size: + mask = mask.resize(image.size) + image = np.array(image) / 255.0 + mask = np.array(mask) / 255.0 + estimated_foreground = FB_blur_fusion_foreground_estimator_2(image, mask, r=r) + image_masked = Image.fromarray((estimated_foreground * 255.0).astype(np.uint8)) + return image_masked + + +def FB_blur_fusion_foreground_estimator_2(image, alpha, r=90): + # Thanks to the source: https://github.com/Photoroom/fast-foreground-estimation + alpha = alpha[:, :, None] + F, blur_B = FB_blur_fusion_foreground_estimator(image, image, image, alpha, r) + return FB_blur_fusion_foreground_estimator(image, F, blur_B, alpha, r=6)[0] + + +def FB_blur_fusion_foreground_estimator(image, F, B, alpha, r=90): + if isinstance(image, Image.Image): + image = np.array(image) / 255.0 + blurred_alpha = cv2.blur(alpha, (r, r))[:, :, None] + + blurred_FA = cv2.blur(F * alpha, (r, r)) + blurred_F = blurred_FA / (blurred_alpha + 1e-5) + + blurred_B1A = cv2.blur(B * (1 - alpha), (r, r)) + blurred_B = blurred_B1A / ((1 - blurred_alpha) + 1e-5) + F = blurred_F + alpha * \ + (image - alpha * blurred_F - (1 - alpha) * blurred_B) + F = np.clip(F, 0, 1) + return F, blurred_B + + +def postprocess_image(result: torch.Tensor, im_size: list) -> np.ndarray: + result = torch.squeeze(F.interpolate(result, size=im_size, mode='bilinear'), 0) + ma = torch.max(result) + mi = torch.min(result) + result = (result - mi) / (ma - mi) + im_array = (result * 255).permute(1, 2, 0).cpu().data.numpy().astype(np.uint8) + im_array = np.squeeze(im_array) + return im_array + + +def rgb_loader_refiner(original_image): + h, w = original_image.size + # # Apply EXIF orientation + + image = ImageOps.exif_transpose(original_image) + + if original_image.mode != 'RGB': + original_image = original_image.convert('RGB') + + image = original_image + # Convert to RGB if necessary + + # Resize the image + image = image.resize((1024, 1024), resample=Image.LANCZOS) + + return image, h, w, original_image + diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/bitsandbytes_NF4/__init__.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/bitsandbytes_NF4/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..5193596851c28a70b1854602a5785131d0ddec3c --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/bitsandbytes_NF4/__init__.py @@ -0,0 +1,167 @@ +#credit to comfyanonymous for this module +#from https://github.com/comfyanonymous/ComfyUI_bitsandbytes_NF4 +import comfy.ops +import torch +import folder_paths +from ...libs.utils import install_package + +try: + from bitsandbytes.nn.modules import Params4bit, QuantState +except ImportError: + Params4bit = torch.nn.Parameter + raise ImportError("Please install bitsandbytes>=0.43.3") + +def functional_linear_4bits(x, weight, bias): + try: + install_package("bitsandbytes", "0.43.3", True, "0.43.3") + import bitsandbytes as bnb + except ImportError: + raise ImportError("Please install bitsandbytes>=0.43.3") + + out = bnb.matmul_4bit(x, weight.t(), bias=bias, quant_state=weight.quant_state) + out = out.to(x) + return out + + +def copy_quant_state(state, device: torch.device = None): + if state is None: + return None + + device = device or state.absmax.device + + state2 = ( + QuantState( + absmax=state.state2.absmax.to(device), + shape=state.state2.shape, + code=state.state2.code.to(device), + blocksize=state.state2.blocksize, + quant_type=state.state2.quant_type, + dtype=state.state2.dtype, + ) + if state.nested + else None + ) + + return QuantState( + absmax=state.absmax.to(device), + shape=state.shape, + code=state.code.to(device), + blocksize=state.blocksize, + quant_type=state.quant_type, + dtype=state.dtype, + offset=state.offset.to(device) if state.nested else None, + state2=state2, + ) + + +class ForgeParams4bit(Params4bit): + + def to(self, *args, **kwargs): + device, dtype, non_blocking, convert_to_format = torch._C._nn._parse_to(*args, **kwargs) + if device is not None and device.type == "cuda" and not self.bnb_quantized: + return self._quantize(device) + else: + n = ForgeParams4bit( + torch.nn.Parameter.to(self, device=device, dtype=dtype, non_blocking=non_blocking), + requires_grad=self.requires_grad, + quant_state=copy_quant_state(self.quant_state, device), + blocksize=self.blocksize, + compress_statistics=self.compress_statistics, + quant_type=self.quant_type, + quant_storage=self.quant_storage, + bnb_quantized=self.bnb_quantized, + module=self.module + ) + self.module.quant_state = n.quant_state + self.data = n.data + self.quant_state = n.quant_state + return n + +class ForgeLoader4Bit(torch.nn.Module): + def __init__(self, *, device, dtype, quant_type, **kwargs): + super().__init__() + self.dummy = torch.nn.Parameter(torch.empty(1, device=device, dtype=dtype)) + self.weight = None + self.quant_state = None + self.bias = None + self.quant_type = quant_type + + def _save_to_state_dict(self, destination, prefix, keep_vars): + super()._save_to_state_dict(destination, prefix, keep_vars) + quant_state = getattr(self.weight, "quant_state", None) + if quant_state is not None: + for k, v in quant_state.as_dict(packed=True).items(): + destination[prefix + "weight." + k] = v if keep_vars else v.detach() + return + + def _load_from_state_dict(self, state_dict, prefix, local_metadata, strict, missing_keys, unexpected_keys, error_msgs): + quant_state_keys = {k[len(prefix + "weight."):] for k in state_dict.keys() if k.startswith(prefix + "weight.")} + + if any('bitsandbytes' in k for k in quant_state_keys): + quant_state_dict = {k: state_dict[prefix + "weight." + k] for k in quant_state_keys} + + self.weight = ForgeParams4bit().from_prequantized( + data=state_dict[prefix + 'weight'], + quantized_stats=quant_state_dict, + requires_grad=False, + device=self.dummy.device, + module=self + ) + self.quant_state = self.weight.quant_state + + if prefix + 'bias' in state_dict: + self.bias = torch.nn.Parameter(state_dict[prefix + 'bias'].to(self.dummy)) + + del self.dummy + elif hasattr(self, 'dummy'): + if prefix + 'weight' in state_dict: + self.weight = ForgeParams4bit( + state_dict[prefix + 'weight'].to(self.dummy), + requires_grad=False, + compress_statistics=True, + quant_type=self.quant_type, + quant_storage=torch.uint8, + module=self, + ) + self.quant_state = self.weight.quant_state + + if prefix + 'bias' in state_dict: + self.bias = torch.nn.Parameter(state_dict[prefix + 'bias'].to(self.dummy)) + + del self.dummy + else: + super()._load_from_state_dict(state_dict, prefix, local_metadata, strict, missing_keys, unexpected_keys, error_msgs) + +current_device = None +current_dtype = None +current_manual_cast_enabled = False +current_bnb_dtype = None + +class OPS(comfy.ops.manual_cast): + class Linear(ForgeLoader4Bit): + def __init__(self, *args, device=None, dtype=None, **kwargs): + super().__init__(device=device, dtype=dtype, quant_type=current_bnb_dtype) + self.parameters_manual_cast = current_manual_cast_enabled + + def forward(self, x): + self.weight.quant_state = self.quant_state + + if self.bias is not None and self.bias.dtype != x.dtype: + # Maybe this can also be set to all non-bnb ops since the cost is very low. + # And it only invokes one time, and most linear does not have bias + self.bias.data = self.bias.data.to(x.dtype) + + if not self.parameters_manual_cast: + return functional_linear_4bits(x, self.weight, self.bias) + elif not self.weight.bnb_quantized: + assert x.device.type == 'cuda', 'BNB Must Use CUDA as Computation Device!' + layer_original_device = self.weight.device + self.weight = self.weight._quantize(x.device) + bias = self.bias.to(x.device) if self.bias is not None else None + out = functional_linear_4bits(x, self.weight, bias) + self.weight = self.weight.to(layer_original_device) + return out + else: + weight, bias, signal = weights_manual_cast(self, x, skip_weight_dtype=True, skip_bias_dtype=True) + with main_stream_worker(weight, bias, signal): + return functional_linear_4bits(x, weight, bias) diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/briaai/__init__.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/briaai/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/briaai/rembg.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/briaai/rembg.py new file mode 100644 index 0000000000000000000000000000000000000000..4689d1494f0be21909a82171ec091462f078738c --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/briaai/rembg.py @@ -0,0 +1,475 @@ +import torch +import torch.nn as nn +import torch.nn.functional as F +from torchvision.transforms.functional import normalize +import numpy as np + + +class REBNCONV(nn.Module): + def __init__(self,in_ch=3,out_ch=3,dirate=1,stride=1): + super(REBNCONV,self).__init__() + + self.conv_s1 = nn.Conv2d(in_ch,out_ch,3,padding=1*dirate,dilation=1*dirate,stride=stride) + self.bn_s1 = nn.BatchNorm2d(out_ch) + self.relu_s1 = nn.ReLU(inplace=True) + + def forward(self,x): + + hx = x + xout = self.relu_s1(self.bn_s1(self.conv_s1(hx))) + + return xout + +## upsample tensor 'src' to have the same spatial size with tensor 'tar' +def _upsample_like(src,tar): + + src = F.interpolate(src,size=tar.shape[2:],mode='bilinear') + + return src + + +### RSU-7 ### +class RSU7(nn.Module): + + def __init__(self, in_ch=3, mid_ch=12, out_ch=3, img_size=512): + super(RSU7,self).__init__() + + self.in_ch = in_ch + self.mid_ch = mid_ch + self.out_ch = out_ch + + self.rebnconvin = REBNCONV(in_ch,out_ch,dirate=1) ## 1 -> 1/2 + + self.rebnconv1 = REBNCONV(out_ch,mid_ch,dirate=1) + self.pool1 = nn.MaxPool2d(2,stride=2,ceil_mode=True) + + self.rebnconv2 = REBNCONV(mid_ch,mid_ch,dirate=1) + self.pool2 = nn.MaxPool2d(2,stride=2,ceil_mode=True) + + self.rebnconv3 = REBNCONV(mid_ch,mid_ch,dirate=1) + self.pool3 = nn.MaxPool2d(2,stride=2,ceil_mode=True) + + self.rebnconv4 = REBNCONV(mid_ch,mid_ch,dirate=1) + self.pool4 = nn.MaxPool2d(2,stride=2,ceil_mode=True) + + self.rebnconv5 = REBNCONV(mid_ch,mid_ch,dirate=1) + self.pool5 = nn.MaxPool2d(2,stride=2,ceil_mode=True) + + self.rebnconv6 = REBNCONV(mid_ch,mid_ch,dirate=1) + + self.rebnconv7 = REBNCONV(mid_ch,mid_ch,dirate=2) + + self.rebnconv6d = REBNCONV(mid_ch*2,mid_ch,dirate=1) + self.rebnconv5d = REBNCONV(mid_ch*2,mid_ch,dirate=1) + self.rebnconv4d = REBNCONV(mid_ch*2,mid_ch,dirate=1) + self.rebnconv3d = REBNCONV(mid_ch*2,mid_ch,dirate=1) + self.rebnconv2d = REBNCONV(mid_ch*2,mid_ch,dirate=1) + self.rebnconv1d = REBNCONV(mid_ch*2,out_ch,dirate=1) + + def forward(self,x): + b, c, h, w = x.shape + + hx = x + hxin = self.rebnconvin(hx) + + hx1 = self.rebnconv1(hxin) + hx = self.pool1(hx1) + + hx2 = self.rebnconv2(hx) + hx = self.pool2(hx2) + + hx3 = self.rebnconv3(hx) + hx = self.pool3(hx3) + + hx4 = self.rebnconv4(hx) + hx = self.pool4(hx4) + + hx5 = self.rebnconv5(hx) + hx = self.pool5(hx5) + + hx6 = self.rebnconv6(hx) + + hx7 = self.rebnconv7(hx6) + + hx6d = self.rebnconv6d(torch.cat((hx7,hx6),1)) + hx6dup = _upsample_like(hx6d,hx5) + + hx5d = self.rebnconv5d(torch.cat((hx6dup,hx5),1)) + hx5dup = _upsample_like(hx5d,hx4) + + hx4d = self.rebnconv4d(torch.cat((hx5dup,hx4),1)) + hx4dup = _upsample_like(hx4d,hx3) + + hx3d = self.rebnconv3d(torch.cat((hx4dup,hx3),1)) + hx3dup = _upsample_like(hx3d,hx2) + + hx2d = self.rebnconv2d(torch.cat((hx3dup,hx2),1)) + hx2dup = _upsample_like(hx2d,hx1) + + hx1d = self.rebnconv1d(torch.cat((hx2dup,hx1),1)) + + return hx1d + hxin + + +### RSU-6 ### +class RSU6(nn.Module): + + def __init__(self, in_ch=3, mid_ch=12, out_ch=3): + super(RSU6,self).__init__() + + self.rebnconvin = REBNCONV(in_ch,out_ch,dirate=1) + + self.rebnconv1 = REBNCONV(out_ch,mid_ch,dirate=1) + self.pool1 = nn.MaxPool2d(2,stride=2,ceil_mode=True) + + self.rebnconv2 = REBNCONV(mid_ch,mid_ch,dirate=1) + self.pool2 = nn.MaxPool2d(2,stride=2,ceil_mode=True) + + self.rebnconv3 = REBNCONV(mid_ch,mid_ch,dirate=1) + self.pool3 = nn.MaxPool2d(2,stride=2,ceil_mode=True) + + self.rebnconv4 = REBNCONV(mid_ch,mid_ch,dirate=1) + self.pool4 = nn.MaxPool2d(2,stride=2,ceil_mode=True) + + self.rebnconv5 = REBNCONV(mid_ch,mid_ch,dirate=1) + + self.rebnconv6 = REBNCONV(mid_ch,mid_ch,dirate=2) + + self.rebnconv5d = REBNCONV(mid_ch*2,mid_ch,dirate=1) + self.rebnconv4d = REBNCONV(mid_ch*2,mid_ch,dirate=1) + self.rebnconv3d = REBNCONV(mid_ch*2,mid_ch,dirate=1) + self.rebnconv2d = REBNCONV(mid_ch*2,mid_ch,dirate=1) + self.rebnconv1d = REBNCONV(mid_ch*2,out_ch,dirate=1) + + def forward(self,x): + + hx = x + + hxin = self.rebnconvin(hx) + + hx1 = self.rebnconv1(hxin) + hx = self.pool1(hx1) + + hx2 = self.rebnconv2(hx) + hx = self.pool2(hx2) + + hx3 = self.rebnconv3(hx) + hx = self.pool3(hx3) + + hx4 = self.rebnconv4(hx) + hx = self.pool4(hx4) + + hx5 = self.rebnconv5(hx) + + hx6 = self.rebnconv6(hx5) + + + hx5d = self.rebnconv5d(torch.cat((hx6,hx5),1)) + hx5dup = _upsample_like(hx5d,hx4) + + hx4d = self.rebnconv4d(torch.cat((hx5dup,hx4),1)) + hx4dup = _upsample_like(hx4d,hx3) + + hx3d = self.rebnconv3d(torch.cat((hx4dup,hx3),1)) + hx3dup = _upsample_like(hx3d,hx2) + + hx2d = self.rebnconv2d(torch.cat((hx3dup,hx2),1)) + hx2dup = _upsample_like(hx2d,hx1) + + hx1d = self.rebnconv1d(torch.cat((hx2dup,hx1),1)) + + return hx1d + hxin + +### RSU-5 ### +class RSU5(nn.Module): + + def __init__(self, in_ch=3, mid_ch=12, out_ch=3): + super(RSU5,self).__init__() + + self.rebnconvin = REBNCONV(in_ch,out_ch,dirate=1) + + self.rebnconv1 = REBNCONV(out_ch,mid_ch,dirate=1) + self.pool1 = nn.MaxPool2d(2,stride=2,ceil_mode=True) + + self.rebnconv2 = REBNCONV(mid_ch,mid_ch,dirate=1) + self.pool2 = nn.MaxPool2d(2,stride=2,ceil_mode=True) + + self.rebnconv3 = REBNCONV(mid_ch,mid_ch,dirate=1) + self.pool3 = nn.MaxPool2d(2,stride=2,ceil_mode=True) + + self.rebnconv4 = REBNCONV(mid_ch,mid_ch,dirate=1) + + self.rebnconv5 = REBNCONV(mid_ch,mid_ch,dirate=2) + + self.rebnconv4d = REBNCONV(mid_ch*2,mid_ch,dirate=1) + self.rebnconv3d = REBNCONV(mid_ch*2,mid_ch,dirate=1) + self.rebnconv2d = REBNCONV(mid_ch*2,mid_ch,dirate=1) + self.rebnconv1d = REBNCONV(mid_ch*2,out_ch,dirate=1) + + def forward(self,x): + + hx = x + + hxin = self.rebnconvin(hx) + + hx1 = self.rebnconv1(hxin) + hx = self.pool1(hx1) + + hx2 = self.rebnconv2(hx) + hx = self.pool2(hx2) + + hx3 = self.rebnconv3(hx) + hx = self.pool3(hx3) + + hx4 = self.rebnconv4(hx) + + hx5 = self.rebnconv5(hx4) + + hx4d = self.rebnconv4d(torch.cat((hx5,hx4),1)) + hx4dup = _upsample_like(hx4d,hx3) + + hx3d = self.rebnconv3d(torch.cat((hx4dup,hx3),1)) + hx3dup = _upsample_like(hx3d,hx2) + + hx2d = self.rebnconv2d(torch.cat((hx3dup,hx2),1)) + hx2dup = _upsample_like(hx2d,hx1) + + hx1d = self.rebnconv1d(torch.cat((hx2dup,hx1),1)) + + return hx1d + hxin + +### RSU-4 ### +class RSU4(nn.Module): + + def __init__(self, in_ch=3, mid_ch=12, out_ch=3): + super(RSU4,self).__init__() + + self.rebnconvin = REBNCONV(in_ch,out_ch,dirate=1) + + self.rebnconv1 = REBNCONV(out_ch,mid_ch,dirate=1) + self.pool1 = nn.MaxPool2d(2,stride=2,ceil_mode=True) + + self.rebnconv2 = REBNCONV(mid_ch,mid_ch,dirate=1) + self.pool2 = nn.MaxPool2d(2,stride=2,ceil_mode=True) + + self.rebnconv3 = REBNCONV(mid_ch,mid_ch,dirate=1) + + self.rebnconv4 = REBNCONV(mid_ch,mid_ch,dirate=2) + + self.rebnconv3d = REBNCONV(mid_ch*2,mid_ch,dirate=1) + self.rebnconv2d = REBNCONV(mid_ch*2,mid_ch,dirate=1) + self.rebnconv1d = REBNCONV(mid_ch*2,out_ch,dirate=1) + + def forward(self,x): + + hx = x + + hxin = self.rebnconvin(hx) + + hx1 = self.rebnconv1(hxin) + hx = self.pool1(hx1) + + hx2 = self.rebnconv2(hx) + hx = self.pool2(hx2) + + hx3 = self.rebnconv3(hx) + + hx4 = self.rebnconv4(hx3) + + hx3d = self.rebnconv3d(torch.cat((hx4,hx3),1)) + hx3dup = _upsample_like(hx3d,hx2) + + hx2d = self.rebnconv2d(torch.cat((hx3dup,hx2),1)) + hx2dup = _upsample_like(hx2d,hx1) + + hx1d = self.rebnconv1d(torch.cat((hx2dup,hx1),1)) + + return hx1d + hxin + +### RSU-4F ### +class RSU4F(nn.Module): + + def __init__(self, in_ch=3, mid_ch=12, out_ch=3): + super(RSU4F,self).__init__() + + self.rebnconvin = REBNCONV(in_ch,out_ch,dirate=1) + + self.rebnconv1 = REBNCONV(out_ch,mid_ch,dirate=1) + self.rebnconv2 = REBNCONV(mid_ch,mid_ch,dirate=2) + self.rebnconv3 = REBNCONV(mid_ch,mid_ch,dirate=4) + + self.rebnconv4 = REBNCONV(mid_ch,mid_ch,dirate=8) + + self.rebnconv3d = REBNCONV(mid_ch*2,mid_ch,dirate=4) + self.rebnconv2d = REBNCONV(mid_ch*2,mid_ch,dirate=2) + self.rebnconv1d = REBNCONV(mid_ch*2,out_ch,dirate=1) + + def forward(self,x): + + hx = x + + hxin = self.rebnconvin(hx) + + hx1 = self.rebnconv1(hxin) + hx2 = self.rebnconv2(hx1) + hx3 = self.rebnconv3(hx2) + + hx4 = self.rebnconv4(hx3) + + hx3d = self.rebnconv3d(torch.cat((hx4,hx3),1)) + hx2d = self.rebnconv2d(torch.cat((hx3d,hx2),1)) + hx1d = self.rebnconv1d(torch.cat((hx2d,hx1),1)) + + return hx1d + hxin + + +class myrebnconv(nn.Module): + def __init__(self, in_ch=3, + out_ch=1, + kernel_size=3, + stride=1, + padding=1, + dilation=1, + groups=1): + super(myrebnconv,self).__init__() + + self.conv = nn.Conv2d(in_ch, + out_ch, + kernel_size=kernel_size, + stride=stride, + padding=padding, + dilation=dilation, + groups=groups) + self.bn = nn.BatchNorm2d(out_ch) + self.rl = nn.ReLU(inplace=True) + + def forward(self,x): + return self.rl(self.bn(self.conv(x))) + +def preprocess_image(im, model_input_size: list) -> torch.Tensor: + # im = im.resize(model_input_size, Image.BILINEAR) + im_np = np.array(im) + im_tensor = torch.tensor(im_np, dtype=torch.float32).permute(2,0,1) + im_tensor = F.interpolate(torch.unsqueeze(im_tensor,0), size=model_input_size, mode='bilinear').type(torch.uint8) + image = torch.divide(im_tensor,255.0) + image = normalize(image,[0.5,0.5,0.5],[1.0,1.0,1.0]) + return image + +def postprocess_image(result: torch.Tensor, im_size: list)-> np.ndarray: + result = torch.squeeze(F.interpolate(result, size=im_size, mode='bilinear') ,0) + ma = torch.max(result) + mi = torch.min(result) + result = (result-mi)/(ma-mi) + im_array = (result*255).permute(1,2,0).cpu().data.numpy().astype(np.uint8) + im_array = np.squeeze(im_array) + return im_array + +class BriaRMBG(nn.Module): + + def __init__(self, config:dict={"in_ch":3,"out_ch":1}): + super(BriaRMBG,self).__init__() + in_ch = config["in_ch"] + out_ch = config["out_ch"] + self.conv_in = nn.Conv2d(in_ch,64,3,stride=2,padding=1) + self.pool_in = nn.MaxPool2d(2,stride=2,ceil_mode=True) + + self.stage1 = RSU7(64,32,64) + self.pool12 = nn.MaxPool2d(2,stride=2,ceil_mode=True) + + self.stage2 = RSU6(64,32,128) + self.pool23 = nn.MaxPool2d(2,stride=2,ceil_mode=True) + + self.stage3 = RSU5(128,64,256) + self.pool34 = nn.MaxPool2d(2,stride=2,ceil_mode=True) + + self.stage4 = RSU4(256,128,512) + self.pool45 = nn.MaxPool2d(2,stride=2,ceil_mode=True) + + self.stage5 = RSU4F(512,256,512) + self.pool56 = nn.MaxPool2d(2,stride=2,ceil_mode=True) + + self.stage6 = RSU4F(512,256,512) + + # decoder + self.stage5d = RSU4F(1024,256,512) + self.stage4d = RSU4(1024,128,256) + self.stage3d = RSU5(512,64,128) + self.stage2d = RSU6(256,32,64) + self.stage1d = RSU7(128,16,64) + + self.side1 = nn.Conv2d(64,out_ch,3,padding=1) + self.side2 = nn.Conv2d(64,out_ch,3,padding=1) + self.side3 = nn.Conv2d(128,out_ch,3,padding=1) + self.side4 = nn.Conv2d(256,out_ch,3,padding=1) + self.side5 = nn.Conv2d(512,out_ch,3,padding=1) + self.side6 = nn.Conv2d(512,out_ch,3,padding=1) + + # self.outconv = nn.Conv2d(6*out_ch,out_ch,1) + + def forward(self,x): + + hx = x + + hxin = self.conv_in(hx) + #hx = self.pool_in(hxin) + + #stage 1 + hx1 = self.stage1(hxin) + hx = self.pool12(hx1) + + #stage 2 + hx2 = self.stage2(hx) + hx = self.pool23(hx2) + + #stage 3 + hx3 = self.stage3(hx) + hx = self.pool34(hx3) + + #stage 4 + hx4 = self.stage4(hx) + hx = self.pool45(hx4) + + #stage 5 + hx5 = self.stage5(hx) + hx = self.pool56(hx5) + + #stage 6 + hx6 = self.stage6(hx) + hx6up = _upsample_like(hx6,hx5) + + #-------------------- decoder -------------------- + hx5d = self.stage5d(torch.cat((hx6up,hx5),1)) + hx5dup = _upsample_like(hx5d,hx4) + + hx4d = self.stage4d(torch.cat((hx5dup,hx4),1)) + hx4dup = _upsample_like(hx4d,hx3) + + hx3d = self.stage3d(torch.cat((hx4dup,hx3),1)) + hx3dup = _upsample_like(hx3d,hx2) + + hx2d = self.stage2d(torch.cat((hx3dup,hx2),1)) + hx2dup = _upsample_like(hx2d,hx1) + + hx1d = self.stage1d(torch.cat((hx2dup,hx1),1)) + + + #side output + d1 = self.side1(hx1d) + d1 = _upsample_like(d1,x) + + d2 = self.side2(hx2d) + d2 = _upsample_like(d2,x) + + d3 = self.side3(hx3d) + d3 = _upsample_like(d3,x) + + d4 = self.side4(hx4d) + d4 = _upsample_like(d4,x) + + d5 = self.side5(hx5d) + d5 = _upsample_like(d5,x) + + d6 = self.side6(hx6) + d6 = _upsample_like(d6,x) + + return [F.sigmoid(d1), F.sigmoid(d2), F.sigmoid(d3), F.sigmoid(d4), F.sigmoid(d5), F.sigmoid(d6)],[hx1d,hx2d,hx3d,hx4d,hx5d,hx6] \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/brushnet/__init__.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/brushnet/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..c35ea134d4406ac120b94a1d98fdf80a01e85ce6 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/brushnet/__init__.py @@ -0,0 +1,822 @@ +#credit to nullquant for this module +#from https://github.com/nullquant/ComfyUI-BrushNet + +import os +import types + +import torch +try: + from accelerate import init_empty_weights, load_checkpoint_and_dispatch +except: + init_empty_weights, load_checkpoint_and_dispatch = None, None + +import comfy + +try: + from .model import BrushNetModel, PowerPaintModel + from .model_patch import add_model_patch_option, patch_model_function_wrapper + from .powerpaint_utils import TokenizerWrapper, add_tokens +except: + BrushNetModel, PowerPaintModel = None, None + add_model_patch_option, patch_model_function_wrapper = None, None + TokenizerWrapper, add_tokens = None, None + +cwd_path = os.path.dirname(os.path.realpath(__file__)) +brushnet_config_file = os.path.join(cwd_path, 'config', 'brushnet.json') +brushnet_xl_config_file = os.path.join(cwd_path, 'config', 'brushnet_xl.json') +powerpaint_config_file = os.path.join(cwd_path, 'config', 'powerpaint.json') + +sd15_scaling_factor = 0.18215 +sdxl_scaling_factor = 0.13025 + +ModelsToUnload = [comfy.sd1_clip.SD1ClipModel, comfy.ldm.models.autoencoder.AutoencoderKL] + +class BrushNet: + + # Check models compatibility + def check_compatibilty(self, model, brushnet): + is_SDXL = False + is_PP = False + if isinstance(model.model.model_config, comfy.supported_models.SD15): + print('Base model type: SD1.5') + is_SDXL = False + if brushnet["SDXL"]: + raise Exception("Base model is SD15, but BrushNet is SDXL type") + if brushnet["PP"]: + is_PP = True + elif isinstance(model.model.model_config, comfy.supported_models.SDXL): + print('Base model type: SDXL') + is_SDXL = True + if not brushnet["SDXL"]: + raise Exception("Base model is SDXL, but BrushNet is SD15 type") + else: + print('Base model type: ', type(model.model.model_config)) + raise Exception("Unsupported model type: " + str(type(model.model.model_config))) + + return (is_SDXL, is_PP) + + def check_image_mask(self, image, mask, name): + if len(image.shape) < 4: + # image tensor shape should be [B, H, W, C], but batch somehow is missing + image = image[None, :, :, :] + + if len(mask.shape) > 3: + # mask tensor shape should be [B, H, W] but we get [B, H, W, C], image may be? + # take first mask, red channel + mask = (mask[:, :, :, 0])[:, :, :] + elif len(mask.shape) < 3: + # mask tensor shape should be [B, H, W] but batch somehow is missing + mask = mask[None, :, :] + + if image.shape[0] > mask.shape[0]: + print(name, "gets batch of images (%d) but only %d masks" % (image.shape[0], mask.shape[0])) + if mask.shape[0] == 1: + print(name, "will copy the mask to fill batch") + mask = torch.cat([mask] * image.shape[0], dim=0) + else: + print(name, "will add empty masks to fill batch") + empty_mask = torch.zeros([image.shape[0] - mask.shape[0], mask.shape[1], mask.shape[2]]) + mask = torch.cat([mask, empty_mask], dim=0) + elif image.shape[0] < mask.shape[0]: + print(name, "gets batch of images (%d) but too many (%d) masks" % (image.shape[0], mask.shape[0])) + mask = mask[:image.shape[0], :, :] + + return (image, mask) + + # Prepare image and mask + def prepare_image(self, image, mask): + + image, mask = self.check_image_mask(image, mask, 'BrushNet') + + print("BrushNet image.shape =", image.shape, "mask.shape =", mask.shape) + + if mask.shape[2] != image.shape[2] or mask.shape[1] != image.shape[1]: + raise Exception("Image and mask should be the same size") + + # As a suggestion of inferno46n2 (https://github.com/nullquant/ComfyUI-BrushNet/issues/64) + mask = mask.round() + + masked_image = image * (1.0 - mask[:, :, :, None]) + + return (masked_image, mask) + + # Get origin of the mask + def cut_with_mask(self, mask, width, height): + iy, ix = (mask == 1).nonzero(as_tuple=True) + + h0, w0 = mask.shape + + if iy.numel() == 0: + x_c = w0 / 2.0 + y_c = h0 / 2.0 + else: + x_min = ix.min().item() + x_max = ix.max().item() + y_min = iy.min().item() + y_max = iy.max().item() + + if x_max - x_min > width or y_max - y_min > height: + raise Exception("Mask is bigger than provided dimensions") + + x_c = (x_min + x_max) / 2.0 + y_c = (y_min + y_max) / 2.0 + + width2 = width / 2.0 + height2 = height / 2.0 + + if w0 <= width: + x0 = 0 + w = w0 + else: + x0 = max(0, x_c - width2) + w = width + if x0 + width > w0: + x0 = w0 - width + + if h0 <= height: + y0 = 0 + h = h0 + else: + y0 = max(0, y_c - height2) + h = height + if y0 + height > h0: + y0 = h0 - height + + return (int(x0), int(y0), int(w), int(h)) + + # Prepare conditioning_latents + @torch.inference_mode() + def get_image_latents(self, masked_image, mask, vae, scaling_factor): + processed_image = masked_image.to(vae.device) + image_latents = vae.encode(processed_image[:, :, :, :3]) * scaling_factor + processed_mask = 1. - mask[:, None, :, :] + interpolated_mask = torch.nn.functional.interpolate( + processed_mask, + size=( + image_latents.shape[-2], + image_latents.shape[-1] + ) + ) + interpolated_mask = interpolated_mask.to(image_latents.device) + + conditioning_latents = [image_latents, interpolated_mask] + + print('BrushNet CL: image_latents shape =', image_latents.shape, 'interpolated_mask shape =', + interpolated_mask.shape) + + return conditioning_latents + + def brushnet_blocks(self, sd): + brushnet_down_block = 0 + brushnet_mid_block = 0 + brushnet_up_block = 0 + for key in sd: + if 'brushnet_down_block' in key: + brushnet_down_block += 1 + if 'brushnet_mid_block' in key: + brushnet_mid_block += 1 + if 'brushnet_up_block' in key: + brushnet_up_block += 1 + return (brushnet_down_block, brushnet_mid_block, brushnet_up_block, len(sd)) + + def get_model_type(self, brushnet_file): + sd = comfy.utils.load_torch_file(brushnet_file) + brushnet_down_block, brushnet_mid_block, brushnet_up_block, keys = self.brushnet_blocks(sd) + del sd + if brushnet_down_block == 24 and brushnet_mid_block == 2 and brushnet_up_block == 30: + is_SDXL = False + if keys == 322: + is_PP = False + print('BrushNet model type: SD1.5') + else: + is_PP = True + print('PowerPaint model type: SD1.5') + elif brushnet_down_block == 18 and brushnet_mid_block == 2 and brushnet_up_block == 22: + print('BrushNet model type: Loading SDXL') + is_SDXL = True + is_PP = False + else: + raise Exception("Unknown BrushNet model") + return is_SDXL, is_PP + + def load_brushnet_model(self, brushnet_file, dtype='float16'): + is_SDXL, is_PP = self.get_model_type(brushnet_file) + with init_empty_weights(): + if is_SDXL: + brushnet_config = BrushNetModel.load_config(brushnet_xl_config_file) + brushnet_model = BrushNetModel.from_config(brushnet_config) + elif is_PP: + brushnet_config = PowerPaintModel.load_config(powerpaint_config_file) + brushnet_model = PowerPaintModel.from_config(brushnet_config) + else: + brushnet_config = BrushNetModel.load_config(brushnet_config_file) + brushnet_model = BrushNetModel.from_config(brushnet_config) + if is_PP: + print("PowerPaint model file:", brushnet_file) + else: + print("BrushNet model file:", brushnet_file) + + if dtype == 'float16': + torch_dtype = torch.float16 + elif dtype == 'bfloat16': + torch_dtype = torch.bfloat16 + elif dtype == 'float32': + torch_dtype = torch.float32 + else: + torch_dtype = torch.float64 + + brushnet_model = load_checkpoint_and_dispatch( + brushnet_model, + brushnet_file, + device_map="sequential", + max_memory=None, + offload_folder=None, + offload_state_dict=False, + dtype=torch_dtype, + force_hooks=False, + ) + + if is_PP: + print("PowerPaint model is loaded") + elif is_SDXL: + print("BrushNet SDXL model is loaded") + else: + print("BrushNet SD1.5 model is loaded") + + return ({"brushnet": brushnet_model, "SDXL": is_SDXL, "PP": is_PP, "dtype": torch_dtype},) + + def brushnet_model_update(self, model, vae, image, mask, brushnet, positive, negative, scale, start_at, end_at): + + is_SDXL, is_PP = self.check_compatibilty(model, brushnet) + + if is_PP: + raise Exception("PowerPaint model was loaded, please use PowerPaint node") + + # Make a copy of the model so that we're not patching it everywhere in the workflow. + model = model.clone() + + # prepare image and mask + # no batches for original image and mask + masked_image, mask = self.prepare_image(image, mask) + + batch = masked_image.shape[0] + width = masked_image.shape[2] + height = masked_image.shape[1] + + if hasattr(model.model.model_config, 'latent_format') and hasattr(model.model.model_config.latent_format, + 'scale_factor'): + scaling_factor = model.model.model_config.latent_format.scale_factor + elif is_SDXL: + scaling_factor = sdxl_scaling_factor + else: + scaling_factor = sd15_scaling_factor + + torch_dtype = brushnet['dtype'] + + # prepare conditioning latents + conditioning_latents = self.get_image_latents(masked_image, mask, vae, scaling_factor) + conditioning_latents[0] = conditioning_latents[0].to(dtype=torch_dtype).to(brushnet['brushnet'].device) + conditioning_latents[1] = conditioning_latents[1].to(dtype=torch_dtype).to(brushnet['brushnet'].device) + + # unload vae + del vae + # for loaded_model in comfy.model_management.current_loaded_models: + # if type(loaded_model.model.model) in ModelsToUnload: + # comfy.model_management.current_loaded_models.remove(loaded_model) + # loaded_model.model_unload() + # del loaded_model + + # prepare embeddings + prompt_embeds = positive[0][0].to(dtype=torch_dtype).to(brushnet['brushnet'].device) + negative_prompt_embeds = negative[0][0].to(dtype=torch_dtype).to(brushnet['brushnet'].device) + + max_tokens = max(prompt_embeds.shape[1], negative_prompt_embeds.shape[1]) + if prompt_embeds.shape[1] < max_tokens: + multiplier = max_tokens // 77 - prompt_embeds.shape[1] // 77 + prompt_embeds = torch.concat([prompt_embeds] + [prompt_embeds[:, -77:, :]] * multiplier, dim=1) + print('BrushNet: negative prompt more than 75 tokens:', negative_prompt_embeds.shape, + 'multiplying prompt_embeds') + if negative_prompt_embeds.shape[1] < max_tokens: + multiplier = max_tokens // 77 - negative_prompt_embeds.shape[1] // 77 + negative_prompt_embeds = torch.concat( + [negative_prompt_embeds] + [negative_prompt_embeds[:, -77:, :]] * multiplier, dim=1) + print('BrushNet: positive prompt more than 75 tokens:', prompt_embeds.shape, + 'multiplying negative_prompt_embeds') + + if len(positive[0]) > 1 and 'pooled_output' in positive[0][1] and positive[0][1]['pooled_output'] is not None: + pooled_prompt_embeds = positive[0][1]['pooled_output'].to(dtype=torch_dtype).to(brushnet['brushnet'].device) + else: + print('BrushNet: positive conditioning has not pooled_output') + if is_SDXL: + print('BrushNet will not produce correct results') + pooled_prompt_embeds = torch.empty([2, 1280], device=brushnet['brushnet'].device).to(dtype=torch_dtype) + + if len(negative[0]) > 1 and 'pooled_output' in negative[0][1] and negative[0][1]['pooled_output'] is not None: + negative_pooled_prompt_embeds = negative[0][1]['pooled_output'].to(dtype=torch_dtype).to( + brushnet['brushnet'].device) + else: + print('BrushNet: negative conditioning has not pooled_output') + if is_SDXL: + print('BrushNet will not produce correct results') + negative_pooled_prompt_embeds = torch.empty([1, pooled_prompt_embeds.shape[1]], + device=brushnet['brushnet'].device).to(dtype=torch_dtype) + + time_ids = torch.FloatTensor([[height, width, 0., 0., height, width]]).to(dtype=torch_dtype).to( + brushnet['brushnet'].device) + + if not is_SDXL: + pooled_prompt_embeds = None + negative_pooled_prompt_embeds = None + time_ids = None + + # apply patch to model + brushnet_conditioning_scale = scale + control_guidance_start = start_at + control_guidance_end = end_at + + add_brushnet_patch(model, + brushnet['brushnet'], + torch_dtype, + conditioning_latents, + (brushnet_conditioning_scale, control_guidance_start, control_guidance_end), + prompt_embeds, negative_prompt_embeds, + pooled_prompt_embeds, negative_pooled_prompt_embeds, time_ids, + False) + + latent = torch.zeros([batch, 4, conditioning_latents[0].shape[2], conditioning_latents[0].shape[3]], + device=brushnet['brushnet'].device) + + return (model, positive, negative, {"samples": latent},) + + #powperpaint + def load_powerpaint_clip(self, base_clip_file, pp_clip_file): + pp_clip = comfy.sd.load_clip(ckpt_paths=[base_clip_file]) + + print('PowerPaint base CLIP file: ', base_clip_file) + + pp_tokenizer = TokenizerWrapper(pp_clip.tokenizer.clip_l.tokenizer) + pp_text_encoder = pp_clip.patcher.model.clip_l.transformer + + add_tokens( + tokenizer=pp_tokenizer, + text_encoder=pp_text_encoder, + placeholder_tokens=["P_ctxt", "P_shape", "P_obj"], + initialize_tokens=["a", "a", "a"], + num_vectors_per_token=10, + ) + + pp_text_encoder.load_state_dict(comfy.utils.load_torch_file(pp_clip_file), strict=False) + + print('PowerPaint CLIP file: ', pp_clip_file) + + pp_clip.tokenizer.clip_l.tokenizer = pp_tokenizer + pp_clip.patcher.model.clip_l.transformer = pp_text_encoder + + return (pp_clip,) + + def powerpaint_model_update(self, model, vae, image, mask, powerpaint, clip, positive, negative, fitting, function, scale, start_at, end_at, save_memory): + is_SDXL, is_PP = self.check_compatibilty(model, powerpaint) + if not is_PP: + raise Exception("BrushNet model was loaded, please use BrushNet node") + + # Make a copy of the model so that we're not patching it everywhere in the workflow. + model = model.clone() + + # prepare image and mask + # no batches for original image and mask + masked_image, mask = self.prepare_image(image, mask) + + batch = masked_image.shape[0] + # width = masked_image.shape[2] + # height = masked_image.shape[1] + + if hasattr(model.model.model_config, 'latent_format') and hasattr(model.model.model_config.latent_format, + 'scale_factor'): + scaling_factor = model.model.model_config.latent_format.scale_factor + else: + scaling_factor = sd15_scaling_factor + + torch_dtype = powerpaint['dtype'] + + # prepare conditioning latents + conditioning_latents = self.get_image_latents(masked_image, mask, vae, scaling_factor) + conditioning_latents[0] = conditioning_latents[0].to(dtype=torch_dtype).to(powerpaint['brushnet'].device) + conditioning_latents[1] = conditioning_latents[1].to(dtype=torch_dtype).to(powerpaint['brushnet'].device) + + # prepare embeddings + + if function == "object removal": + promptA = "P_ctxt" + promptB = "P_ctxt" + negative_promptA = "P_obj" + negative_promptB = "P_obj" + print('You should add to positive prompt: "empty scene blur"') + # positive = positive + " empty scene blur" + elif function == "context aware": + promptA = "P_ctxt" + promptB = "P_ctxt" + negative_promptA = "" + negative_promptB = "" + # positive = positive + " empty scene" + print('You should add to positive prompt: "empty scene"') + elif function == "shape guided": + promptA = "P_shape" + promptB = "P_ctxt" + negative_promptA = "P_shape" + negative_promptB = "P_ctxt" + elif function == "image outpainting": + promptA = "P_ctxt" + promptB = "P_ctxt" + negative_promptA = "P_obj" + negative_promptB = "P_obj" + # positive = positive + " empty scene" + print('You should add to positive prompt: "empty scene"') + else: + promptA = "P_obj" + promptB = "P_obj" + negative_promptA = "P_obj" + negative_promptB = "P_obj" + + tokens = clip.tokenize(promptA) + prompt_embedsA = clip.encode_from_tokens(tokens, return_pooled=False) + + tokens = clip.tokenize(negative_promptA) + negative_prompt_embedsA = clip.encode_from_tokens(tokens, return_pooled=False) + + tokens = clip.tokenize(promptB) + prompt_embedsB = clip.encode_from_tokens(tokens, return_pooled=False) + + tokens = clip.tokenize(negative_promptB) + negative_prompt_embedsB = clip.encode_from_tokens(tokens, return_pooled=False) + + prompt_embeds_pp = (prompt_embedsA * fitting + (1.0 - fitting) * prompt_embedsB).to(dtype=torch_dtype).to( + powerpaint['brushnet'].device) + negative_prompt_embeds_pp = (negative_prompt_embedsA * fitting + (1.0 - fitting) * negative_prompt_embedsB).to( + dtype=torch_dtype).to(powerpaint['brushnet'].device) + + # unload vae and CLIPs + del vae + del clip + # for loaded_model in comfy.model_management.current_loaded_models: + # if type(loaded_model.model.model) in ModelsToUnload: + # comfy.model_management.current_loaded_models.remove(loaded_model) + # loaded_model.model_unload() + # del loaded_model + + # apply patch to model + + brushnet_conditioning_scale = scale + control_guidance_start = start_at + control_guidance_end = end_at + + if save_memory != 'none': + powerpaint['brushnet'].set_attention_slice(save_memory) + + add_brushnet_patch(model, + powerpaint['brushnet'], + torch_dtype, + conditioning_latents, + (brushnet_conditioning_scale, control_guidance_start, control_guidance_end), + negative_prompt_embeds_pp, prompt_embeds_pp, + None, None, None, + False) + + latent = torch.zeros([batch, 4, conditioning_latents[0].shape[2], conditioning_latents[0].shape[3]], + device=powerpaint['brushnet'].device) + + return (model, positive, negative, {"samples": latent},) +@torch.inference_mode() +def brushnet_inference(x, timesteps, transformer_options, debug): + if 'model_patch' not in transformer_options: + print('BrushNet inference: there is no model_patch key in transformer_options') + return ([], 0, []) + mp = transformer_options['model_patch'] + if 'brushnet' not in mp: + print('BrushNet inference: there is no brushnet key in mdel_patch') + return ([], 0, []) + bo = mp['brushnet'] + if 'model' not in bo: + print('BrushNet inference: there is no model key in brushnet') + return ([], 0, []) + brushnet = bo['model'] + if not (isinstance(brushnet, BrushNetModel) or isinstance(brushnet, PowerPaintModel)): + print('BrushNet model is not a BrushNetModel class') + return ([], 0, []) + + torch_dtype = bo['dtype'] + cl_list = bo['latents'] + brushnet_conditioning_scale, control_guidance_start, control_guidance_end = bo['controls'] + pe = bo['prompt_embeds'] + npe = bo['negative_prompt_embeds'] + ppe, nppe, time_ids = bo['add_embeds'] + + #do_classifier_free_guidance = mp['free_guidance'] + do_classifier_free_guidance = len(transformer_options['cond_or_uncond']) > 1 + + x = x.detach().clone() + x = x.to(torch_dtype).to(brushnet.device) + + timesteps = timesteps.detach().clone() + timesteps = timesteps.to(torch_dtype).to(brushnet.device) + + total_steps = mp['total_steps'] + step = mp['step'] + + added_cond_kwargs = {} + + if do_classifier_free_guidance and step == 0: + print('BrushNet inference: do_classifier_free_guidance is True') + + sub_idx = None + if 'ad_params' in transformer_options and 'sub_idxs' in transformer_options['ad_params']: + sub_idx = transformer_options['ad_params']['sub_idxs'] + + # we have batch input images + batch = cl_list[0].shape[0] + # we have incoming latents + latents_incoming = x.shape[0] + # and we already got some + latents_got = bo['latent_id'] + if step == 0 or batch > 1: + print('BrushNet inference, step = %d: image batch = %d, got %d latents, starting from %d' \ + % (step, batch, latents_incoming, latents_got)) + + image_latents = [] + masks = [] + prompt_embeds = [] + negative_prompt_embeds = [] + pooled_prompt_embeds = [] + negative_pooled_prompt_embeds = [] + if sub_idx: + # AnimateDiff indexes detected + if step == 0: + print('BrushNet inference: AnimateDiff indexes detected and applied') + + batch = len(sub_idx) + + if do_classifier_free_guidance: + for i in sub_idx: + image_latents.append(cl_list[0][i][None,:,:,:]) + masks.append(cl_list[1][i][None,:,:,:]) + prompt_embeds.append(pe) + negative_prompt_embeds.append(npe) + pooled_prompt_embeds.append(ppe) + negative_pooled_prompt_embeds.append(nppe) + for i in sub_idx: + image_latents.append(cl_list[0][i][None,:,:,:]) + masks.append(cl_list[1][i][None,:,:,:]) + else: + for i in sub_idx: + image_latents.append(cl_list[0][i][None,:,:,:]) + masks.append(cl_list[1][i][None,:,:,:]) + prompt_embeds.append(pe) + pooled_prompt_embeds.append(ppe) + else: + # do_classifier_free_guidance = 2 passes, 1st pass is cond, 2nd is uncond + continue_batch = True + for i in range(latents_incoming): + number = latents_got + i + if number < batch: + # 1st pass, cond + image_latents.append(cl_list[0][number][None,:,:,:]) + masks.append(cl_list[1][number][None,:,:,:]) + prompt_embeds.append(pe) + pooled_prompt_embeds.append(ppe) + elif do_classifier_free_guidance and number < batch * 2: + # 2nd pass, uncond + image_latents.append(cl_list[0][number-batch][None,:,:,:]) + masks.append(cl_list[1][number-batch][None,:,:,:]) + negative_prompt_embeds.append(npe) + negative_pooled_prompt_embeds.append(nppe) + else: + # latent batch + image_latents.append(cl_list[0][0][None,:,:,:]) + masks.append(cl_list[1][0][None,:,:,:]) + prompt_embeds.append(pe) + pooled_prompt_embeds.append(ppe) + latents_got = -i + continue_batch = False + + if continue_batch: + # we don't have full batch yet + if do_classifier_free_guidance: + if number < batch * 2 - 1: + bo['latent_id'] = number + 1 + else: + bo['latent_id'] = 0 + else: + if number < batch - 1: + bo['latent_id'] = number + 1 + else: + bo['latent_id'] = 0 + else: + bo['latent_id'] = 0 + + cl = [] + for il, m in zip(image_latents, masks): + cl.append(torch.concat([il, m], dim=1)) + cl2apply = torch.concat(cl, dim=0) + + conditioning_latents = cl2apply.to(torch_dtype).to(brushnet.device) + + prompt_embeds.extend(negative_prompt_embeds) + prompt_embeds = torch.concat(prompt_embeds, dim=0).to(torch_dtype).to(brushnet.device) + + if ppe is not None: + added_cond_kwargs = {} + added_cond_kwargs['time_ids'] = torch.concat([time_ids] * latents_incoming, dim = 0).to(torch_dtype).to(brushnet.device) + + pooled_prompt_embeds.extend(negative_pooled_prompt_embeds) + pooled_prompt_embeds = torch.concat(pooled_prompt_embeds, dim=0).to(torch_dtype).to(brushnet.device) + added_cond_kwargs['text_embeds'] = pooled_prompt_embeds + else: + added_cond_kwargs = None + + if x.shape[2] != conditioning_latents.shape[2] or x.shape[3] != conditioning_latents.shape[3]: + if step == 0: + print('BrushNet inference: image', conditioning_latents.shape, 'and latent', x.shape, 'have different size, resizing image') + conditioning_latents = torch.nn.functional.interpolate( + conditioning_latents, size=( + x.shape[2], + x.shape[3], + ), mode='bicubic', + ).to(torch_dtype).to(brushnet.device) + + if step == 0: + print('BrushNet inference: sample', x.shape, ', CL', conditioning_latents.shape, 'dtype', torch_dtype) + + if debug: print('BrushNet: step =', step) + + if step < control_guidance_start or step > control_guidance_end: + cond_scale = 0.0 + else: + cond_scale = brushnet_conditioning_scale + + return brushnet(x, + encoder_hidden_states=prompt_embeds, + brushnet_cond=conditioning_latents, + timestep = timesteps, + conditioning_scale=cond_scale, + guess_mode=False, + added_cond_kwargs=added_cond_kwargs, + return_dict=False, + debug=debug, + ) + +def add_brushnet_patch(model, brushnet, torch_dtype, conditioning_latents, + controls, + prompt_embeds, negative_prompt_embeds, + pooled_prompt_embeds, negative_pooled_prompt_embeds, time_ids, + debug): + + is_SDXL = isinstance(model.model.model_config, comfy.supported_models.SDXL) + + if model.model.model_config.custom_operations is None: + fp8 = model.model.model_config.optimizations.get("fp8", model.model.model_config.scaled_fp8 is not None) + operations = comfy.ops.pick_operations(model.model.model_config.unet_config.get("dtype", None), model.model.manual_cast_dtype, + fp8_optimizations=fp8, scaled_fp8=model.model.model_config.scaled_fp8) + else: + # such as gguf + operations = model.model.model_config.custom_operations + + if is_SDXL: + input_blocks = [[0, operations.Conv2d], + [1, comfy.ldm.modules.diffusionmodules.openaimodel.ResBlock], + [2, comfy.ldm.modules.diffusionmodules.openaimodel.ResBlock], + [3, comfy.ldm.modules.diffusionmodules.openaimodel.Downsample], + [4, comfy.ldm.modules.attention.SpatialTransformer], + [5, comfy.ldm.modules.attention.SpatialTransformer], + [6, comfy.ldm.modules.diffusionmodules.openaimodel.Downsample], + [7, comfy.ldm.modules.attention.SpatialTransformer], + [8, comfy.ldm.modules.attention.SpatialTransformer]] + middle_block = [0, comfy.ldm.modules.diffusionmodules.openaimodel.ResBlock] + output_blocks = [[0, comfy.ldm.modules.attention.SpatialTransformer], + [1, comfy.ldm.modules.attention.SpatialTransformer], + [2, comfy.ldm.modules.attention.SpatialTransformer], + [2, comfy.ldm.modules.diffusionmodules.openaimodel.Upsample], + [3, comfy.ldm.modules.attention.SpatialTransformer], + [4, comfy.ldm.modules.attention.SpatialTransformer], + [5, comfy.ldm.modules.attention.SpatialTransformer], + [5, comfy.ldm.modules.diffusionmodules.openaimodel.Upsample], + [6, comfy.ldm.modules.diffusionmodules.openaimodel.ResBlock], + [7, comfy.ldm.modules.diffusionmodules.openaimodel.ResBlock], + [8, comfy.ldm.modules.diffusionmodules.openaimodel.ResBlock]] + else: + input_blocks = [[0, operations.Conv2d], + [1, comfy.ldm.modules.attention.SpatialTransformer], + [2, comfy.ldm.modules.attention.SpatialTransformer], + [3, comfy.ldm.modules.diffusionmodules.openaimodel.Downsample], + [4, comfy.ldm.modules.attention.SpatialTransformer], + [5, comfy.ldm.modules.attention.SpatialTransformer], + [6, comfy.ldm.modules.diffusionmodules.openaimodel.Downsample], + [7, comfy.ldm.modules.attention.SpatialTransformer], + [8, comfy.ldm.modules.attention.SpatialTransformer], + [9, comfy.ldm.modules.diffusionmodules.openaimodel.Downsample], + [10, comfy.ldm.modules.diffusionmodules.openaimodel.ResBlock], + [11, comfy.ldm.modules.diffusionmodules.openaimodel.ResBlock]] + middle_block = [0, comfy.ldm.modules.diffusionmodules.openaimodel.ResBlock] + output_blocks = [[0, comfy.ldm.modules.diffusionmodules.openaimodel.ResBlock], + [1, comfy.ldm.modules.diffusionmodules.openaimodel.ResBlock], + [2, comfy.ldm.modules.diffusionmodules.openaimodel.ResBlock], + [2, comfy.ldm.modules.diffusionmodules.openaimodel.Upsample], + [3, comfy.ldm.modules.attention.SpatialTransformer], + [4, comfy.ldm.modules.attention.SpatialTransformer], + [5, comfy.ldm.modules.attention.SpatialTransformer], + [5, comfy.ldm.modules.diffusionmodules.openaimodel.Upsample], + [6, comfy.ldm.modules.attention.SpatialTransformer], + [7, comfy.ldm.modules.attention.SpatialTransformer], + [8, comfy.ldm.modules.attention.SpatialTransformer], + [8, comfy.ldm.modules.diffusionmodules.openaimodel.Upsample], + [9, comfy.ldm.modules.attention.SpatialTransformer], + [10, comfy.ldm.modules.attention.SpatialTransformer], + [11, comfy.ldm.modules.attention.SpatialTransformer]] + + def last_layer_index(block, tp): + layer_list = [] + for layer in block: + layer_list.append(type(layer)) + layer_list.reverse() + if tp not in layer_list: + return -1, layer_list.reverse() + return len(layer_list) - 1 - layer_list.index(tp), layer_list + + def brushnet_forward(model, x, timesteps, transformer_options, control): + if 'brushnet' not in transformer_options['model_patch']: + input_samples = [] + mid_sample = 0 + output_samples = [] + else: + # brushnet inference + input_samples, mid_sample, output_samples = brushnet_inference(x, timesteps, transformer_options, debug) + + # give additional samples to blocks + for i, tp in input_blocks: + idx, layer_list = last_layer_index(model.input_blocks[i], tp) + if idx < 0: + print("BrushNet can't find", tp, "layer in", i, "input block:", layer_list) + continue + model.input_blocks[i][idx].add_sample_after = input_samples.pop(0) if input_samples else 0 + + idx, layer_list = last_layer_index(model.middle_block, middle_block[1]) + if idx < 0: + print("BrushNet can't find", middle_block[1], "layer in middle block", layer_list) + model.middle_block[idx].add_sample_after = mid_sample + + for i, tp in output_blocks: + idx, layer_list = last_layer_index(model.output_blocks[i], tp) + if idx < 0: + print("BrushNet can't find", tp, "layer in", i, "outnput block:", layer_list) + continue + model.output_blocks[i][idx].add_sample_after = output_samples.pop(0) if output_samples else 0 + + patch_model_function_wrapper(model, brushnet_forward) + + to = add_model_patch_option(model) + mp = to['model_patch'] + if 'brushnet' not in mp: + mp['brushnet'] = {} + bo = mp['brushnet'] + + bo['model'] = brushnet + bo['dtype'] = torch_dtype + bo['latents'] = conditioning_latents + bo['controls'] = controls + bo['prompt_embeds'] = prompt_embeds + bo['negative_prompt_embeds'] = negative_prompt_embeds + bo['add_embeds'] = (pooled_prompt_embeds, negative_pooled_prompt_embeds, time_ids) + bo['latent_id'] = 0 + + # patch layers `forward` so we can apply brushnet + def forward_patched_by_brushnet(self, x, *args, **kwargs): + h = self.original_forward(x, *args, **kwargs) + if hasattr(self, 'add_sample_after') and type(self): + to_add = self.add_sample_after + if torch.is_tensor(to_add): + # interpolate due to RAUNet + if h.shape[2] != to_add.shape[2] or h.shape[3] != to_add.shape[3]: + to_add = torch.nn.functional.interpolate(to_add, size=(h.shape[2], h.shape[3]), mode='bicubic') + h += to_add.to(h.dtype).to(h.device) + else: + h += self.add_sample_after + self.add_sample_after = 0 + return h + + for i, block in enumerate(model.model.diffusion_model.input_blocks): + for j, layer in enumerate(block): + if not hasattr(layer, 'original_forward'): + layer.original_forward = layer.forward + layer.forward = types.MethodType(forward_patched_by_brushnet, layer) + layer.add_sample_after = 0 + + for j, layer in enumerate(model.model.diffusion_model.middle_block): + if not hasattr(layer, 'original_forward'): + layer.original_forward = layer.forward + layer.forward = types.MethodType(forward_patched_by_brushnet, layer) + layer.add_sample_after = 0 + + for i, block in enumerate(model.model.diffusion_model.output_blocks): + for j, layer in enumerate(block): + if not hasattr(layer, 'original_forward'): + layer.original_forward = layer.forward + layer.forward = types.MethodType(forward_patched_by_brushnet, layer) + layer.add_sample_after = 0 \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/brushnet/__pycache__/__init__.cpython-312.pyc b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/brushnet/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5f6941ae64f8e08f8d9e7e2aa686e2f77027a0f2 Binary files /dev/null and b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/brushnet/__pycache__/__init__.cpython-312.pyc differ diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/brushnet/__pycache__/model.cpython-312.pyc b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/brushnet/__pycache__/model.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..511c5d9713a42b9a77174840600475ec48255e4b Binary files /dev/null and b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/brushnet/__pycache__/model.cpython-312.pyc differ diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/brushnet/__pycache__/model_patch.cpython-312.pyc b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/brushnet/__pycache__/model_patch.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0fcb2c45c0d3e41e9c12096e28104e09d59f5994 Binary files /dev/null and b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/brushnet/__pycache__/model_patch.cpython-312.pyc differ diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/brushnet/__pycache__/powerpaint_utils.cpython-312.pyc b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/brushnet/__pycache__/powerpaint_utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..af541235d38a5b0ae31a6156dcca83c2e56161cd Binary files /dev/null and b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/brushnet/__pycache__/powerpaint_utils.cpython-312.pyc differ diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/brushnet/__pycache__/unet_2d_blocks.cpython-312.pyc b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/brushnet/__pycache__/unet_2d_blocks.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..75fa556e7c69f93036f20e69b52d4c8e2552fa3a --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/brushnet/__pycache__/unet_2d_blocks.cpython-312.pyc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1148210ee8f1bc384cd3fbefb1425591976f0e14d66094d494abf175fdf6f255 +size 111447 diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/brushnet/__pycache__/unet_2d_condition.cpython-312.pyc b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/brushnet/__pycache__/unet_2d_condition.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..846e3da2b823d7dc07724d2fcbe98451ed1fb21b Binary files /dev/null and b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/brushnet/__pycache__/unet_2d_condition.cpython-312.pyc differ diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/brushnet/config/brushnet.json b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/brushnet/config/brushnet.json new file mode 100644 index 0000000000000000000000000000000000000000..c22ae1573b55249f221f0f426cb1bbcada747254 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/brushnet/config/brushnet.json @@ -0,0 +1,58 @@ +{ + "_class_name": "BrushNetModel", + "_diffusers_version": "0.27.0.dev0", + "_name_or_path": "runs/logs/brushnet_randommask/checkpoint-100000", + "act_fn": "silu", + "addition_embed_type": null, + "addition_embed_type_num_heads": 64, + "addition_time_embed_dim": null, + "attention_head_dim": 8, + "block_out_channels": [ + 320, + 640, + 1280, + 1280 + ], + "brushnet_conditioning_channel_order": "rgb", + "class_embed_type": null, + "conditioning_channels": 5, + "conditioning_embedding_out_channels": [ + 16, + 32, + 96, + 256 + ], + "cross_attention_dim": 768, + "down_block_types": [ + "DownBlock2D", + "DownBlock2D", + "DownBlock2D", + "DownBlock2D" + ], + "downsample_padding": 1, + "encoder_hid_dim": null, + "encoder_hid_dim_type": null, + "flip_sin_to_cos": true, + "freq_shift": 0, + "global_pool_conditions": false, + "in_channels": 4, + "layers_per_block": 2, + "mid_block_scale_factor": 1, + "mid_block_type": "MidBlock2D", + "norm_eps": 1e-05, + "norm_num_groups": 32, + "num_attention_heads": null, + "num_class_embeds": null, + "only_cross_attention": false, + "projection_class_embeddings_input_dim": null, + "resnet_time_scale_shift": "default", + "transformer_layers_per_block": 1, + "up_block_types": [ + "UpBlock2D", + "UpBlock2D", + "UpBlock2D", + "UpBlock2D" + ], + "upcast_attention": false, + "use_linear_projection": false +} \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/brushnet/config/brushnet_xl.json b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/brushnet/config/brushnet_xl.json new file mode 100644 index 0000000000000000000000000000000000000000..1e0ebb7a3928c8ae586b330f4e38f2fe0e8d8dd7 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/brushnet/config/brushnet_xl.json @@ -0,0 +1,63 @@ +{ + "_class_name": "BrushNetModel", + "_diffusers_version": "0.27.0.dev0", + "_name_or_path": "runs/logs/brushnetsdxl_randommask/checkpoint-80000", + "act_fn": "silu", + "addition_embed_type": "text_time", + "addition_embed_type_num_heads": 64, + "addition_time_embed_dim": 256, + "attention_head_dim": [ + 5, + 10, + 20 + ], + "block_out_channels": [ + 320, + 640, + 1280 + ], + "brushnet_conditioning_channel_order": "rgb", + "class_embed_type": null, + "conditioning_channels": 5, + "conditioning_embedding_out_channels": [ + 16, + 32, + 96, + 256 + ], + "cross_attention_dim": 2048, + "down_block_types": [ + "DownBlock2D", + "DownBlock2D", + "DownBlock2D" + ], + "downsample_padding": 1, + "encoder_hid_dim": null, + "encoder_hid_dim_type": null, + "flip_sin_to_cos": true, + "freq_shift": 0, + "global_pool_conditions": false, + "in_channels": 4, + "layers_per_block": 2, + "mid_block_scale_factor": 1, + "mid_block_type": "MidBlock2D", + "norm_eps": 1e-05, + "norm_num_groups": 32, + "num_attention_heads": null, + "num_class_embeds": null, + "only_cross_attention": false, + "projection_class_embeddings_input_dim": 2816, + "resnet_time_scale_shift": "default", + "transformer_layers_per_block": [ + 1, + 2, + 10 + ], + "up_block_types": [ + "UpBlock2D", + "UpBlock2D", + "UpBlock2D" + ], + "upcast_attention": null, + "use_linear_projection": true +} \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/brushnet/config/powerpaint.json b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/brushnet/config/powerpaint.json new file mode 100644 index 0000000000000000000000000000000000000000..1ea970014ae98e5dda242202bec89ee258e1e554 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/brushnet/config/powerpaint.json @@ -0,0 +1,57 @@ +{ + "_class_name": "BrushNetModel", + "_diffusers_version": "0.27.2", + "act_fn": "silu", + "addition_embed_type": null, + "addition_embed_type_num_heads": 64, + "addition_time_embed_dim": null, + "attention_head_dim": 8, + "block_out_channels": [ + 320, + 640, + 1280, + 1280 + ], + "brushnet_conditioning_channel_order": "rgb", + "class_embed_type": null, + "conditioning_channels": 5, + "conditioning_embedding_out_channels": [ + 16, + 32, + 96, + 256 + ], + "cross_attention_dim": 768, + "down_block_types": [ + "CrossAttnDownBlock2D", + "CrossAttnDownBlock2D", + "CrossAttnDownBlock2D", + "DownBlock2D" + ], + "downsample_padding": 1, + "encoder_hid_dim": null, + "encoder_hid_dim_type": null, + "flip_sin_to_cos": true, + "freq_shift": 0, + "global_pool_conditions": false, + "in_channels": 4, + "layers_per_block": 2, + "mid_block_scale_factor": 1, + "mid_block_type": "UNetMidBlock2DCrossAttn", + "norm_eps": 1e-05, + "norm_num_groups": 32, + "num_attention_heads": null, + "num_class_embeds": null, + "only_cross_attention": false, + "projection_class_embeddings_input_dim": null, + "resnet_time_scale_shift": "default", + "transformer_layers_per_block": 1, + "up_block_types": [ + "UpBlock2D", + "CrossAttnUpBlock2D", + "CrossAttnUpBlock2D", + "CrossAttnUpBlock2D" + ], + "upcast_attention": false, + "use_linear_projection": false +} \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/brushnet/model.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/brushnet/model.py new file mode 100644 index 0000000000000000000000000000000000000000..ed256d21b0d6b21adc7627caa47bdfb0b07aeb5d --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/brushnet/model.py @@ -0,0 +1,1688 @@ +from dataclasses import dataclass +from typing import Any, Dict, List, Optional, Tuple, Union + +import torch +from torch import nn + +from ...libs.utils import install_package +try: + install_package("diffusers", "0.27.2", True, "0.25.0") + + from diffusers.configuration_utils import ConfigMixin, register_to_config + from diffusers.utils import BaseOutput, logging + from diffusers.models.attention_processor import ( + ADDED_KV_ATTENTION_PROCESSORS, + CROSS_ATTENTION_PROCESSORS, + AttentionProcessor, + AttnAddedKVProcessor, + AttnProcessor, + ) + from diffusers.models.embeddings import TextImageProjection, TextImageTimeEmbedding, TextTimeEmbedding, TimestepEmbedding, Timesteps + from diffusers.models.modeling_utils import ModelMixin + from diffusers.models.resnet import ResnetBlock2D + from diffusers.models.unets.unet_2d_condition import UNet2DConditionModel + + from diffusers.models.transformers.dual_transformer_2d import DualTransformer2DModel + from diffusers.models.transformers.transformer_2d import Transformer2DModel + + from .unet_2d_blocks import ( + CrossAttnDownBlock2D, + DownBlock2D, + get_down_block, + get_mid_block, + get_up_block, + ) + + from .unet_2d_condition import UNet2DConditionModel + + logger = logging.get_logger(__name__) + + def zero_module(module): + for p in module.parameters(): + nn.init.zeros_(p) + return module + + @dataclass + class BrushNetOutput(BaseOutput): + + up_block_res_samples: Tuple[torch.Tensor] + down_block_res_samples: Tuple[torch.Tensor] + mid_block_res_sample: torch.Tensor + + # BrushNetModel + class BrushNetModel(ModelMixin, ConfigMixin): + """A BrushNet model.""" + _supports_gradient_checkpointing = True + + @register_to_config + def __init__( + self, + in_channels: int = 4, + conditioning_channels: int = 5, + flip_sin_to_cos: bool = True, + freq_shift: int = 0, + down_block_types: Tuple[str, ...] = ( + "DownBlock2D", + "DownBlock2D", + "DownBlock2D", + "DownBlock2D", + ), + mid_block_type: Optional[str] = "UNetMidBlock2D", + up_block_types: Tuple[str, ...] = ( + "UpBlock2D", + "UpBlock2D", + "UpBlock2D", + "UpBlock2D", + ), + only_cross_attention: Union[bool, Tuple[bool]] = False, + block_out_channels: Tuple[int, ...] = (320, 640, 1280, 1280), + layers_per_block: int = 2, + downsample_padding: int = 1, + mid_block_scale_factor: float = 1, + act_fn: str = "silu", + norm_num_groups: Optional[int] = 32, + norm_eps: float = 1e-5, + cross_attention_dim: int = 1280, + transformer_layers_per_block: Union[int, Tuple[int, ...]] = 1, + encoder_hid_dim: Optional[int] = None, + encoder_hid_dim_type: Optional[str] = None, + attention_head_dim: Union[int, Tuple[int, ...]] = 8, + num_attention_heads: Optional[Union[int, Tuple[int, ...]]] = None, + use_linear_projection: bool = False, + class_embed_type: Optional[str] = None, + addition_embed_type: Optional[str] = None, + addition_time_embed_dim: Optional[int] = None, + num_class_embeds: Optional[int] = None, + upcast_attention: bool = False, + resnet_time_scale_shift: str = "default", + projection_class_embeddings_input_dim: Optional[int] = None, + brushnet_conditioning_channel_order: str = "rgb", + conditioning_embedding_out_channels: Optional[Tuple[int, ...]] = (16, 32, 96, 256), + global_pool_conditions: bool = False, + addition_embed_type_num_heads: int = 64, + ): + super().__init__() + + # If `num_attention_heads` is not defined (which is the case for most models) + # it will default to `attention_head_dim`. This looks weird upon first reading it and it is. + # The reason for this behavior is to correct for incorrectly named variables that were introduced + # when this library was created. The incorrect naming was only discovered much later in https://github.com/huggingface/diffusers/issues/2011#issuecomment-1547958131 + # Changing `attention_head_dim` to `num_attention_heads` for 40,000+ configurations is too backwards breaking + # which is why we correct for the naming here. + num_attention_heads = num_attention_heads or attention_head_dim + + # Check inputs + if len(down_block_types) != len(up_block_types): + raise ValueError( + f"Must provide the same number of `down_block_types` as `up_block_types`. `down_block_types`: {down_block_types}. `up_block_types`: {up_block_types}." + ) + + if len(block_out_channels) != len(down_block_types): + raise ValueError( + f"Must provide the same number of `block_out_channels` as `down_block_types`. `block_out_channels`: {block_out_channels}. `down_block_types`: {down_block_types}." + ) + + if not isinstance(only_cross_attention, bool) and len(only_cross_attention) != len(down_block_types): + raise ValueError( + f"Must provide the same number of `only_cross_attention` as `down_block_types`. `only_cross_attention`: {only_cross_attention}. `down_block_types`: {down_block_types}." + ) + + if not isinstance(num_attention_heads, int) and len(num_attention_heads) != len(down_block_types): + raise ValueError( + f"Must provide the same number of `num_attention_heads` as `down_block_types`. `num_attention_heads`: {num_attention_heads}. `down_block_types`: {down_block_types}." + ) + + if isinstance(transformer_layers_per_block, int): + transformer_layers_per_block = [transformer_layers_per_block] * len(down_block_types) + + # input + conv_in_kernel = 3 + conv_in_padding = (conv_in_kernel - 1) // 2 + self.conv_in_condition = nn.Conv2d( + in_channels + conditioning_channels, block_out_channels[0], kernel_size=conv_in_kernel, + padding=conv_in_padding + ) + + # time + time_embed_dim = block_out_channels[0] * 4 + self.time_proj = Timesteps(block_out_channels[0], flip_sin_to_cos, freq_shift) + timestep_input_dim = block_out_channels[0] + self.time_embedding = TimestepEmbedding( + timestep_input_dim, + time_embed_dim, + act_fn=act_fn, + ) + + if encoder_hid_dim_type is None and encoder_hid_dim is not None: + encoder_hid_dim_type = "text_proj" + self.register_to_config(encoder_hid_dim_type=encoder_hid_dim_type) + print("encoder_hid_dim_type defaults to 'text_proj' as `encoder_hid_dim` is defined.") + + if encoder_hid_dim is None and encoder_hid_dim_type is not None: + raise ValueError( + f"`encoder_hid_dim` has to be defined when `encoder_hid_dim_type` is set to {encoder_hid_dim_type}." + ) + + if encoder_hid_dim_type == "text_proj": + self.encoder_hid_proj = nn.Linear(encoder_hid_dim, cross_attention_dim) + elif encoder_hid_dim_type == "text_image_proj": + # image_embed_dim DOESN'T have to be `cross_attention_dim`. To not clutter the __init__ too much + # they are set to `cross_attention_dim` here as this is exactly the required dimension for the currently only use + # case when `addition_embed_type == "text_image_proj"` (Kadinsky 2.1)` + self.encoder_hid_proj = TextImageProjection( + text_embed_dim=encoder_hid_dim, + image_embed_dim=cross_attention_dim, + cross_attention_dim=cross_attention_dim, + ) + + elif encoder_hid_dim_type is not None: + raise ValueError( + f"encoder_hid_dim_type: {encoder_hid_dim_type} must be None, 'text_proj' or 'text_image_proj'." + ) + else: + self.encoder_hid_proj = None + + # class embedding + if class_embed_type is None and num_class_embeds is not None: + self.class_embedding = nn.Embedding(num_class_embeds, time_embed_dim) + elif class_embed_type == "timestep": + self.class_embedding = TimestepEmbedding(timestep_input_dim, time_embed_dim) + elif class_embed_type == "identity": + self.class_embedding = nn.Identity(time_embed_dim, time_embed_dim) + elif class_embed_type == "projection": + if projection_class_embeddings_input_dim is None: + raise ValueError( + "`class_embed_type`: 'projection' requires `projection_class_embeddings_input_dim` be set" + ) + # The projection `class_embed_type` is the same as the timestep `class_embed_type` except + # 1. the `class_labels` inputs are not first converted to sinusoidal embeddings + # 2. it projects from an arbitrary input dimension. + # + # Note that `TimestepEmbedding` is quite general, being mainly linear layers and activations. + # When used for embedding actual timesteps, the timesteps are first converted to sinusoidal embeddings. + # As a result, `TimestepEmbedding` can be passed arbitrary vectors. + self.class_embedding = TimestepEmbedding(projection_class_embeddings_input_dim, time_embed_dim) + else: + self.class_embedding = None + + if addition_embed_type == "text": + if encoder_hid_dim is not None: + text_time_embedding_from_dim = encoder_hid_dim + else: + text_time_embedding_from_dim = cross_attention_dim + + self.add_embedding = TextTimeEmbedding( + text_time_embedding_from_dim, time_embed_dim, num_heads=addition_embed_type_num_heads + ) + elif addition_embed_type == "text_image": + # text_embed_dim and image_embed_dim DON'T have to be `cross_attention_dim`. To not clutter the __init__ too much + # they are set to `cross_attention_dim` here as this is exactly the required dimension for the currently only use + # case when `addition_embed_type == "text_image"` (Kadinsky 2.1)` + self.add_embedding = TextImageTimeEmbedding( + text_embed_dim=cross_attention_dim, image_embed_dim=cross_attention_dim, + time_embed_dim=time_embed_dim + ) + elif addition_embed_type == "text_time": + self.add_time_proj = Timesteps(addition_time_embed_dim, flip_sin_to_cos, freq_shift) + self.add_embedding = TimestepEmbedding(projection_class_embeddings_input_dim, time_embed_dim) + + elif addition_embed_type is not None: + raise ValueError(f"addition_embed_type: {addition_embed_type} must be None, 'text' or 'text_image'.") + + self.down_blocks = nn.ModuleList([]) + self.brushnet_down_blocks = nn.ModuleList([]) + + if isinstance(only_cross_attention, bool): + only_cross_attention = [only_cross_attention] * len(down_block_types) + + if isinstance(attention_head_dim, int): + attention_head_dim = (attention_head_dim,) * len(down_block_types) + + if isinstance(num_attention_heads, int): + num_attention_heads = (num_attention_heads,) * len(down_block_types) + + # down + output_channel = block_out_channels[0] + + brushnet_block = nn.Conv2d(output_channel, output_channel, kernel_size=1) + brushnet_block = zero_module(brushnet_block) + self.brushnet_down_blocks.append(brushnet_block) + + for i, down_block_type in enumerate(down_block_types): + input_channel = output_channel + output_channel = block_out_channels[i] + is_final_block = i == len(block_out_channels) - 1 + + down_block = get_down_block( + down_block_type, + num_layers=layers_per_block, + transformer_layers_per_block=transformer_layers_per_block[i], + in_channels=input_channel, + out_channels=output_channel, + temb_channels=time_embed_dim, + add_downsample=not is_final_block, + resnet_eps=norm_eps, + resnet_act_fn=act_fn, + resnet_groups=norm_num_groups, + cross_attention_dim=cross_attention_dim, + num_attention_heads=num_attention_heads[i], + attention_head_dim=attention_head_dim[i] if attention_head_dim[i] is not None else output_channel, + downsample_padding=downsample_padding, + use_linear_projection=use_linear_projection, + only_cross_attention=only_cross_attention[i], + upcast_attention=upcast_attention, + resnet_time_scale_shift=resnet_time_scale_shift, + ) + self.down_blocks.append(down_block) + + for _ in range(layers_per_block): + brushnet_block = nn.Conv2d(output_channel, output_channel, kernel_size=1) + brushnet_block = zero_module(brushnet_block) + self.brushnet_down_blocks.append(brushnet_block) + + if not is_final_block: + brushnet_block = nn.Conv2d(output_channel, output_channel, kernel_size=1) + brushnet_block = zero_module(brushnet_block) + self.brushnet_down_blocks.append(brushnet_block) + + # mid + mid_block_channel = block_out_channels[-1] + + brushnet_block = nn.Conv2d(mid_block_channel, mid_block_channel, kernel_size=1) + brushnet_block = zero_module(brushnet_block) + self.brushnet_mid_block = brushnet_block + + self.mid_block = get_mid_block( + mid_block_type, + transformer_layers_per_block=transformer_layers_per_block[-1], + in_channels=mid_block_channel, + temb_channels=time_embed_dim, + resnet_eps=norm_eps, + resnet_act_fn=act_fn, + output_scale_factor=mid_block_scale_factor, + resnet_time_scale_shift=resnet_time_scale_shift, + cross_attention_dim=cross_attention_dim, + num_attention_heads=num_attention_heads[-1], + resnet_groups=norm_num_groups, + use_linear_projection=use_linear_projection, + upcast_attention=upcast_attention, + ) + + # count how many layers upsample the images + self.num_upsamplers = 0 + + # up + reversed_block_out_channels = list(reversed(block_out_channels)) + reversed_num_attention_heads = list(reversed(num_attention_heads)) + reversed_transformer_layers_per_block = (list(reversed(transformer_layers_per_block))) + only_cross_attention = list(reversed(only_cross_attention)) + + output_channel = reversed_block_out_channels[0] + + self.up_blocks = nn.ModuleList([]) + self.brushnet_up_blocks = nn.ModuleList([]) + + for i, up_block_type in enumerate(up_block_types): + is_final_block = i == len(block_out_channels) - 1 + + prev_output_channel = output_channel + output_channel = reversed_block_out_channels[i] + input_channel = reversed_block_out_channels[min(i + 1, len(block_out_channels) - 1)] + + # add upsample block for all BUT final layer + if not is_final_block: + add_upsample = True + self.num_upsamplers += 1 + else: + add_upsample = False + + up_block = get_up_block( + up_block_type, + num_layers=layers_per_block + 1, + transformer_layers_per_block=reversed_transformer_layers_per_block[i], + in_channels=input_channel, + out_channels=output_channel, + prev_output_channel=prev_output_channel, + temb_channels=time_embed_dim, + add_upsample=add_upsample, + resnet_eps=norm_eps, + resnet_act_fn=act_fn, + resolution_idx=i, + resnet_groups=norm_num_groups, + cross_attention_dim=cross_attention_dim, + num_attention_heads=reversed_num_attention_heads[i], + use_linear_projection=use_linear_projection, + only_cross_attention=only_cross_attention[i], + upcast_attention=upcast_attention, + resnet_time_scale_shift=resnet_time_scale_shift, + attention_head_dim=attention_head_dim[i] if attention_head_dim[i] is not None else output_channel, + ) + self.up_blocks.append(up_block) + prev_output_channel = output_channel + + for _ in range(layers_per_block + 1): + brushnet_block = nn.Conv2d(output_channel, output_channel, kernel_size=1) + brushnet_block = zero_module(brushnet_block) + self.brushnet_up_blocks.append(brushnet_block) + + if not is_final_block: + brushnet_block = nn.Conv2d(output_channel, output_channel, kernel_size=1) + brushnet_block = zero_module(brushnet_block) + self.brushnet_up_blocks.append(brushnet_block) + + @classmethod + def from_unet( + cls, + unet: UNet2DConditionModel, + brushnet_conditioning_channel_order: str = "rgb", + conditioning_embedding_out_channels: Optional[Tuple[int, ...]] = (16, 32, 96, 256), + load_weights_from_unet: bool = True, + conditioning_channels: int = 5, + ): + r""" + Instantiate a [`BrushNetModel`] from [`UNet2DConditionModel`]. + + Parameters: + unet (`UNet2DConditionModel`): + The UNet model weights to copy to the [`BrushNetModel`]. All configuration options are also copied + where applicable. + """ + transformer_layers_per_block = ( + unet.config.transformer_layers_per_block if "transformer_layers_per_block" in unet.config else 1 + ) + encoder_hid_dim = unet.config.encoder_hid_dim if "encoder_hid_dim" in unet.config else None + encoder_hid_dim_type = unet.config.encoder_hid_dim_type if "encoder_hid_dim_type" in unet.config else None + addition_embed_type = unet.config.addition_embed_type if "addition_embed_type" in unet.config else None + addition_time_embed_dim = ( + unet.config.addition_time_embed_dim if "addition_time_embed_dim" in unet.config else None + ) + + down_block_types = ["DownBlock2D" for block_name in unet.config.down_block_types] + mid_block_type = "MidBlock2D" + up_block_types = ["UpBlock2D" for block_name in unet.config.down_block_types] + + brushnet = cls( + in_channels=unet.config.in_channels, + conditioning_channels=conditioning_channels, + flip_sin_to_cos=unet.config.flip_sin_to_cos, + freq_shift=unet.config.freq_shift, + down_block_types=down_block_types, + mid_block_type=mid_block_type, + up_block_types=up_block_types, + only_cross_attention=unet.config.only_cross_attention, + block_out_channels=unet.config.block_out_channels, + layers_per_block=unet.config.layers_per_block, + downsample_padding=unet.config.downsample_padding, + mid_block_scale_factor=unet.config.mid_block_scale_factor, + act_fn=unet.config.act_fn, + norm_num_groups=unet.config.norm_num_groups, + norm_eps=unet.config.norm_eps, + cross_attention_dim=unet.config.cross_attention_dim, + transformer_layers_per_block=transformer_layers_per_block, + encoder_hid_dim=encoder_hid_dim, + encoder_hid_dim_type=encoder_hid_dim_type, + attention_head_dim=unet.config.attention_head_dim, + num_attention_heads=unet.config.num_attention_heads, + use_linear_projection=unet.config.use_linear_projection, + class_embed_type=unet.config.class_embed_type, + addition_embed_type=addition_embed_type, + addition_time_embed_dim=addition_time_embed_dim, + num_class_embeds=unet.config.num_class_embeds, + upcast_attention=unet.config.upcast_attention, + resnet_time_scale_shift=unet.config.resnet_time_scale_shift, + projection_class_embeddings_input_dim=unet.config.projection_class_embeddings_input_dim, + brushnet_conditioning_channel_order=brushnet_conditioning_channel_order, + conditioning_embedding_out_channels=conditioning_embedding_out_channels, + ) + + if load_weights_from_unet: + conv_in_condition_weight = torch.zeros_like(brushnet.conv_in_condition.weight) + conv_in_condition_weight[:, :4, ...] = unet.conv_in.weight + conv_in_condition_weight[:, 4:8, ...] = unet.conv_in.weight + brushnet.conv_in_condition.weight = torch.nn.Parameter(conv_in_condition_weight) + brushnet.conv_in_condition.bias = unet.conv_in.bias + + brushnet.time_proj.load_state_dict(unet.time_proj.state_dict()) + brushnet.time_embedding.load_state_dict(unet.time_embedding.state_dict()) + + if brushnet.class_embedding: + brushnet.class_embedding.load_state_dict(unet.class_embedding.state_dict()) + + brushnet.down_blocks.load_state_dict(unet.down_blocks.state_dict(), strict=False) + brushnet.mid_block.load_state_dict(unet.mid_block.state_dict(), strict=False) + brushnet.up_blocks.load_state_dict(unet.up_blocks.state_dict(), strict=False) + + return brushnet + + @property + # Copied from diffusers.models.unets.unet_2d_condition.UNet2DConditionModel.attn_processors + def attn_processors(self) -> Dict[str, AttentionProcessor]: + r""" + Returns: + `dict` of attention processors: A dictionary containing all attention processors used in the model with + indexed by its weight name. + """ + # set recursively + processors = {} + + def fn_recursive_add_processors(name: str, module: torch.nn.Module, + processors: Dict[str, AttentionProcessor]): + if hasattr(module, "get_processor"): + processors[f"{name}.processor"] = module.get_processor(return_deprecated_lora=True) + + for sub_name, child in module.named_children(): + fn_recursive_add_processors(f"{name}.{sub_name}", child, processors) + + return processors + + for name, module in self.named_children(): + fn_recursive_add_processors(name, module, processors) + + return processors + + # Copied from diffusers.models.unets.unet_2d_condition.UNet2DConditionModel.set_attn_processor + def set_attn_processor(self, processor: Union[AttentionProcessor, Dict[str, AttentionProcessor]]): + r""" + Sets the attention processor to use to compute attention. + + Parameters: + processor (`dict` of `AttentionProcessor` or only `AttentionProcessor`): + The instantiated processor class or a dictionary of processor classes that will be set as the processor + for **all** `Attention` layers. + + If `processor` is a dict, the key needs to define the path to the corresponding cross attention + processor. This is strongly recommended when setting trainable attention processors. + + """ + count = len(self.attn_processors.keys()) + + if isinstance(processor, dict) and len(processor) != count: + raise ValueError( + f"A dict of processors was passed, but the number of processors {len(processor)} does not match the" + f" number of attention layers: {count}. Please make sure to pass {count} processor classes." + ) + + def fn_recursive_attn_processor(name: str, module: torch.nn.Module, processor): + if hasattr(module, "set_processor"): + if not isinstance(processor, dict): + module.set_processor(processor) + else: + module.set_processor(processor.pop(f"{name}.processor")) + + for sub_name, child in module.named_children(): + fn_recursive_attn_processor(f"{name}.{sub_name}", child, processor) + + for name, module in self.named_children(): + fn_recursive_attn_processor(name, module, processor) + + # Copied from diffusers.models.unets.unet_2d_condition.UNet2DConditionModel.set_default_attn_processor + def set_default_attn_processor(self): + """ + Disables custom attention processors and sets the default attention implementation. + """ + if all(proc.__class__ in ADDED_KV_ATTENTION_PROCESSORS for proc in self.attn_processors.values()): + processor = AttnAddedKVProcessor() + elif all(proc.__class__ in CROSS_ATTENTION_PROCESSORS for proc in self.attn_processors.values()): + processor = AttnProcessor() + else: + raise ValueError( + f"Cannot call `set_default_attn_processor` when attention processors are of type {next(iter(self.attn_processors.values()))}" + ) + + self.set_attn_processor(processor) + + # Copied from diffusers.models.unets.unet_2d_condition.UNet2DConditionModel.set_attention_slice + def set_attention_slice(self, slice_size: Union[str, int, List[int]]) -> None: + r""" + Enable sliced attention computation. + + When this option is enabled, the attention module splits the input tensor in slices to compute attention in + several steps. This is useful for saving some memory in exchange for a small decrease in speed. + + Args: + slice_size (`str` or `int` or `list(int)`, *optional*, defaults to `"auto"`): + When `"auto"`, input to the attention heads is halved, so attention is computed in two steps. If + `"max"`, maximum amount of memory is saved by running only one slice at a time. If a number is + provided, uses as many slices as `attention_head_dim // slice_size`. In this case, `attention_head_dim` + must be a multiple of `slice_size`. + """ + sliceable_head_dims = [] + + def fn_recursive_retrieve_sliceable_dims(module: torch.nn.Module): + if hasattr(module, "set_attention_slice"): + sliceable_head_dims.append(module.sliceable_head_dim) + + for child in module.children(): + fn_recursive_retrieve_sliceable_dims(child) + + # retrieve number of attention layers + for module in self.children(): + fn_recursive_retrieve_sliceable_dims(module) + + num_sliceable_layers = len(sliceable_head_dims) + + if slice_size == "auto": + # half the attention head size is usually a good trade-off between + # speed and memory + slice_size = [dim // 2 for dim in sliceable_head_dims] + elif slice_size == "max": + # make smallest slice possible + slice_size = num_sliceable_layers * [1] + + slice_size = num_sliceable_layers * [slice_size] if not isinstance(slice_size, list) else slice_size + + if len(slice_size) != len(sliceable_head_dims): + raise ValueError( + f"You have provided {len(slice_size)}, but {self.config} has {len(sliceable_head_dims)} different" + f" attention layers. Make sure to match `len(slice_size)` to be {len(sliceable_head_dims)}." + ) + + for i in range(len(slice_size)): + size = slice_size[i] + dim = sliceable_head_dims[i] + if size is not None and size > dim: + raise ValueError(f"size {size} has to be smaller or equal to {dim}.") + + # Recursively walk through all the children. + # Any children which exposes the set_attention_slice method + # gets the message + def fn_recursive_set_attention_slice(module: torch.nn.Module, slice_size: List[int]): + if hasattr(module, "set_attention_slice"): + module.set_attention_slice(slice_size.pop()) + + for child in module.children(): + fn_recursive_set_attention_slice(child, slice_size) + + reversed_slice_size = list(reversed(slice_size)) + for module in self.children(): + fn_recursive_set_attention_slice(module, reversed_slice_size) + + def _set_gradient_checkpointing(self, module, value: bool = False) -> None: + if isinstance(module, (CrossAttnDownBlock2D, DownBlock2D)): + module.gradient_checkpointing = value + + def forward( + self, + sample: torch.FloatTensor, + encoder_hidden_states: torch.Tensor, + brushnet_cond: torch.FloatTensor, + timestep=None, + time_emb=None, + conditioning_scale: float = 1.0, + class_labels: Optional[torch.Tensor] = None, + timestep_cond: Optional[torch.Tensor] = None, + attention_mask: Optional[torch.Tensor] = None, + added_cond_kwargs: Optional[Dict[str, torch.Tensor]] = None, + cross_attention_kwargs: Optional[Dict[str, Any]] = None, + guess_mode: bool = False, + return_dict: bool = True, + debug=False, + ) -> Union[BrushNetOutput, Tuple[Tuple[torch.FloatTensor, ...], torch.FloatTensor]]: + + # check channel order + channel_order = self.config.brushnet_conditioning_channel_order + + if channel_order == "rgb": + # in rgb order by default + ... + elif channel_order == "bgr": + brushnet_cond = torch.flip(brushnet_cond, dims=[1]) + else: + raise ValueError(f"unknown `brushnet_conditioning_channel_order`: {channel_order}") + + # prepare attention_mask + if attention_mask is not None: + attention_mask = (1 - attention_mask.to(sample.dtype)) * -10000.0 + attention_mask = attention_mask.unsqueeze(1) + + if timestep is None and time_emb is None: + raise ValueError(f"`timestep` and `emb` are both None") + + # print("BN: sample.device", sample.device) + # print("BN: TE.device", self.time_embedding.linear_1.weight.device) + + if timestep is not None: + # 1. time + timesteps = timestep + if not torch.is_tensor(timesteps): + # TODO: this requires sync between CPU and GPU. So try to pass timesteps as tensors if you can + # This would be a good case for the `match` statement (Python 3.10+) + is_mps = sample.device.type == "mps" + if isinstance(timestep, float): + dtype = torch.float32 if is_mps else torch.float64 + else: + dtype = torch.int32 if is_mps else torch.int64 + timesteps = torch.tensor([timesteps], dtype=dtype, device=sample.device) + elif len(timesteps.shape) == 0: + timesteps = timesteps[None].to(sample.device) + + # broadcast to batch dimension in a way that's compatible with ONNX/Core ML + timesteps = timesteps.expand(sample.shape[0]) + + t_emb = self.time_proj(timesteps) + + # timesteps does not contain any weights and will always return f32 tensors + # but time_embedding might actually be running in fp16. so we need to cast here. + # there might be better ways to encapsulate this. + t_emb = t_emb.to(dtype=sample.dtype) + + # print("t_emb.device =",t_emb.device) + + emb = self.time_embedding(t_emb, timestep_cond) + aug_emb = None + + # print('emb.shape', emb.shape) + + if self.class_embedding is not None: + if class_labels is None: + raise ValueError("class_labels should be provided when num_class_embeds > 0") + + if self.config.class_embed_type == "timestep": + class_labels = self.time_proj(class_labels) + + class_emb = self.class_embedding(class_labels).to(dtype=self.dtype) + emb = emb + class_emb + + if self.config.addition_embed_type is not None: + if self.config.addition_embed_type == "text": + aug_emb = self.add_embedding(encoder_hidden_states) + + elif self.config.addition_embed_type == "text_time": + if "text_embeds" not in added_cond_kwargs: + raise ValueError( + f"{self.__class__} has the config param `addition_embed_type` set to 'text_time' which requires the keyword argument `text_embeds` to be passed in `added_cond_kwargs`" + ) + text_embeds = added_cond_kwargs.get("text_embeds") + if "time_ids" not in added_cond_kwargs: + raise ValueError( + f"{self.__class__} has the config param `addition_embed_type` set to 'text_time' which requires the keyword argument `time_ids` to be passed in `added_cond_kwargs`" + ) + time_ids = added_cond_kwargs.get("time_ids") + time_embeds = self.add_time_proj(time_ids.flatten()) + time_embeds = time_embeds.reshape((text_embeds.shape[0], -1)) + + add_embeds = torch.concat([text_embeds, time_embeds], dim=-1) + add_embeds = add_embeds.to(emb.dtype) + aug_emb = self.add_embedding(add_embeds) + + # print('text_embeds', text_embeds.shape, 'time_ids', time_ids.shape, 'time_embeds', time_embeds.shape, 'add__embeds', add_embeds.shape, 'aug_emb', aug_emb.shape) + + emb = emb + aug_emb if aug_emb is not None else emb + else: + emb = time_emb + + # 2. pre-process + + brushnet_cond = torch.concat([sample, brushnet_cond], 1) + sample = self.conv_in_condition(brushnet_cond) + + # 3. down + down_block_res_samples = (sample,) + for downsample_block in self.down_blocks: + if hasattr(downsample_block, "has_cross_attention") and downsample_block.has_cross_attention: + sample, res_samples = downsample_block( + hidden_states=sample, + temb=emb, + encoder_hidden_states=encoder_hidden_states, + attention_mask=attention_mask, + cross_attention_kwargs=cross_attention_kwargs, + ) + else: + sample, res_samples = downsample_block(hidden_states=sample, temb=emb) + + down_block_res_samples += res_samples + + # 4. PaintingNet down blocks + brushnet_down_block_res_samples = () + for down_block_res_sample, brushnet_down_block in zip(down_block_res_samples, self.brushnet_down_blocks): + down_block_res_sample = brushnet_down_block(down_block_res_sample) + brushnet_down_block_res_samples = brushnet_down_block_res_samples + (down_block_res_sample,) + + # 5. mid + if self.mid_block is not None: + if hasattr(self.mid_block, "has_cross_attention") and self.mid_block.has_cross_attention: + sample = self.mid_block( + sample, + emb, + encoder_hidden_states=encoder_hidden_states, + attention_mask=attention_mask, + cross_attention_kwargs=cross_attention_kwargs, + ) + else: + sample = self.mid_block(sample, emb) + + # 6. BrushNet mid blocks + brushnet_mid_block_res_sample = self.brushnet_mid_block(sample) + + # 7. up + up_block_res_samples = () + for i, upsample_block in enumerate(self.up_blocks): + is_final_block = i == len(self.up_blocks) - 1 + + res_samples = down_block_res_samples[-len(upsample_block.resnets):] + down_block_res_samples = down_block_res_samples[: -len(upsample_block.resnets)] + + # if we have not reached the final block and need to forward the + # upsample size, we do it here + if not is_final_block: + upsample_size = down_block_res_samples[-1].shape[2:] + + if hasattr(upsample_block, "has_cross_attention") and upsample_block.has_cross_attention: + sample, up_res_samples = upsample_block( + hidden_states=sample, + temb=emb, + res_hidden_states_tuple=res_samples, + encoder_hidden_states=encoder_hidden_states, + cross_attention_kwargs=cross_attention_kwargs, + upsample_size=upsample_size, + attention_mask=attention_mask, + return_res_samples=True + ) + else: + sample, up_res_samples = upsample_block( + hidden_states=sample, + temb=emb, + res_hidden_states_tuple=res_samples, + upsample_size=upsample_size, + return_res_samples=True + ) + + up_block_res_samples += up_res_samples + + # 8. BrushNet up blocks + brushnet_up_block_res_samples = () + for up_block_res_sample, brushnet_up_block in zip(up_block_res_samples, self.brushnet_up_blocks): + up_block_res_sample = brushnet_up_block(up_block_res_sample) + brushnet_up_block_res_samples = brushnet_up_block_res_samples + (up_block_res_sample,) + + # 6. scaling + if guess_mode and not self.config.global_pool_conditions: + scales = torch.logspace(-1, 0, + len(brushnet_down_block_res_samples) + 1 + len(brushnet_up_block_res_samples), + device=sample.device) # 0.1 to 1.0 + scales = scales * conditioning_scale + + brushnet_down_block_res_samples = [sample * scale for sample, scale in + zip(brushnet_down_block_res_samples, + scales[:len(brushnet_down_block_res_samples)])] + brushnet_mid_block_res_sample = brushnet_mid_block_res_sample * scales[ + len(brushnet_down_block_res_samples)] + brushnet_up_block_res_samples = [sample * scale for sample, scale in zip(brushnet_up_block_res_samples, + scales[ + len(brushnet_down_block_res_samples) + 1:])] + else: + brushnet_down_block_res_samples = [sample * conditioning_scale for sample in + brushnet_down_block_res_samples] + brushnet_mid_block_res_sample = brushnet_mid_block_res_sample * conditioning_scale + brushnet_up_block_res_samples = [sample * conditioning_scale for sample in + brushnet_up_block_res_samples] + + if self.config.global_pool_conditions: + brushnet_down_block_res_samples = [ + torch.mean(sample, dim=(2, 3), keepdim=True) for sample in brushnet_down_block_res_samples + ] + brushnet_mid_block_res_sample = torch.mean(brushnet_mid_block_res_sample, dim=(2, 3), keepdim=True) + brushnet_up_block_res_samples = [ + torch.mean(sample, dim=(2, 3), keepdim=True) for sample in brushnet_up_block_res_samples + ] + + if not return_dict: + return (brushnet_down_block_res_samples, brushnet_mid_block_res_sample, brushnet_up_block_res_samples) + + return BrushNetOutput( + down_block_res_samples=brushnet_down_block_res_samples, + mid_block_res_sample=brushnet_mid_block_res_sample, + up_block_res_samples=brushnet_up_block_res_samples + ) + + # PowerPaintModel + class PowerPaintModel(ModelMixin, ConfigMixin): + _supports_gradient_checkpointing = True + + @register_to_config + def __init__( + self, + in_channels: int = 4, + conditioning_channels: int = 5, + flip_sin_to_cos: bool = True, + freq_shift: int = 0, + down_block_types: Tuple[str, ...] = ( + "CrossAttnDownBlock2D", + "CrossAttnDownBlock2D", + "CrossAttnDownBlock2D", + "DownBlock2D", + ), + mid_block_type: Optional[str] = "UNetMidBlock2DCrossAttn", + up_block_types: Tuple[str, ...] = ( + "UpBlock2D", + "CrossAttnUpBlock2D", + "CrossAttnUpBlock2D", + "CrossAttnUpBlock2D", + ), + only_cross_attention: Union[bool, Tuple[bool]] = False, + block_out_channels: Tuple[int, ...] = (320, 640, 1280, 1280), + layers_per_block: int = 2, + downsample_padding: int = 1, + mid_block_scale_factor: float = 1, + act_fn: str = "silu", + norm_num_groups: Optional[int] = 32, + norm_eps: float = 1e-5, + cross_attention_dim: int = 1280, + transformer_layers_per_block: Union[int, Tuple[int, ...]] = 1, + encoder_hid_dim: Optional[int] = None, + encoder_hid_dim_type: Optional[str] = None, + attention_head_dim: Union[int, Tuple[int, ...]] = 8, + num_attention_heads: Optional[Union[int, Tuple[int, ...]]] = None, + use_linear_projection: bool = False, + class_embed_type: Optional[str] = None, + addition_embed_type: Optional[str] = None, + addition_time_embed_dim: Optional[int] = None, + num_class_embeds: Optional[int] = None, + upcast_attention: bool = False, + resnet_time_scale_shift: str = "default", + projection_class_embeddings_input_dim: Optional[int] = None, + brushnet_conditioning_channel_order: str = "rgb", + conditioning_embedding_out_channels: Optional[Tuple[int, ...]] = (16, 32, 96, 256), + global_pool_conditions: bool = False, + addition_embed_type_num_heads: int = 64, + ): + super().__init__() + + # If `num_attention_heads` is not defined (which is the case for most models) + # it will default to `attention_head_dim`. This looks weird upon first reading it and it is. + # The reason for this behavior is to correct for incorrectly named variables that were introduced + # when this library was created. The incorrect naming was only discovered much later in https://github.com/huggingface/diffusers/issues/2011#issuecomment-1547958131 + # Changing `attention_head_dim` to `num_attention_heads` for 40,000+ configurations is too backwards breaking + # which is why we correct for the naming here. + num_attention_heads = num_attention_heads or attention_head_dim + + # Check inputs + if len(down_block_types) != len(up_block_types): + raise ValueError( + f"Must provide the same number of `down_block_types` as `up_block_types`. `down_block_types`: {down_block_types}. `up_block_types`: {up_block_types}." + ) + + if len(block_out_channels) != len(down_block_types): + raise ValueError( + f"Must provide the same number of `block_out_channels` as `down_block_types`. `block_out_channels`: {block_out_channels}. `down_block_types`: {down_block_types}." + ) + + if not isinstance(only_cross_attention, bool) and len(only_cross_attention) != len(down_block_types): + raise ValueError( + f"Must provide the same number of `only_cross_attention` as `down_block_types`. `only_cross_attention`: {only_cross_attention}. `down_block_types`: {down_block_types}." + ) + + if not isinstance(num_attention_heads, int) and len(num_attention_heads) != len(down_block_types): + raise ValueError( + f"Must provide the same number of `num_attention_heads` as `down_block_types`. `num_attention_heads`: {num_attention_heads}. `down_block_types`: {down_block_types}." + ) + + if isinstance(transformer_layers_per_block, int): + transformer_layers_per_block = [transformer_layers_per_block] * len(down_block_types) + + # input + conv_in_kernel = 3 + conv_in_padding = (conv_in_kernel - 1) // 2 + self.conv_in_condition = nn.Conv2d( + in_channels + conditioning_channels, + block_out_channels[0], + kernel_size=conv_in_kernel, + padding=conv_in_padding, + ) + + # time + time_embed_dim = block_out_channels[0] * 4 + self.time_proj = Timesteps(block_out_channels[0], flip_sin_to_cos, freq_shift) + timestep_input_dim = block_out_channels[0] + self.time_embedding = TimestepEmbedding( + timestep_input_dim, + time_embed_dim, + act_fn=act_fn, + ) + + if encoder_hid_dim_type is None and encoder_hid_dim is not None: + encoder_hid_dim_type = "text_proj" + self.register_to_config(encoder_hid_dim_type=encoder_hid_dim_type) + logger.info("encoder_hid_dim_type defaults to 'text_proj' as `encoder_hid_dim` is defined.") + + if encoder_hid_dim is None and encoder_hid_dim_type is not None: + raise ValueError( + f"`encoder_hid_dim` has to be defined when `encoder_hid_dim_type` is set to {encoder_hid_dim_type}." + ) + + if encoder_hid_dim_type == "text_proj": + self.encoder_hid_proj = nn.Linear(encoder_hid_dim, cross_attention_dim) + elif encoder_hid_dim_type == "text_image_proj": + # image_embed_dim DOESN'T have to be `cross_attention_dim`. To not clutter the __init__ too much + # they are set to `cross_attention_dim` here as this is exactly the required dimension for the currently only use + # case when `addition_embed_type == "text_image_proj"` (Kadinsky 2.1)` + self.encoder_hid_proj = TextImageProjection( + text_embed_dim=encoder_hid_dim, + image_embed_dim=cross_attention_dim, + cross_attention_dim=cross_attention_dim, + ) + + elif encoder_hid_dim_type is not None: + raise ValueError( + f"encoder_hid_dim_type: {encoder_hid_dim_type} must be None, 'text_proj' or 'text_image_proj'." + ) + else: + self.encoder_hid_proj = None + + # class embedding + if class_embed_type is None and num_class_embeds is not None: + self.class_embedding = nn.Embedding(num_class_embeds, time_embed_dim) + elif class_embed_type == "timestep": + self.class_embedding = TimestepEmbedding(timestep_input_dim, time_embed_dim) + elif class_embed_type == "identity": + self.class_embedding = nn.Identity(time_embed_dim, time_embed_dim) + elif class_embed_type == "projection": + if projection_class_embeddings_input_dim is None: + raise ValueError( + "`class_embed_type`: 'projection' requires `projection_class_embeddings_input_dim` be set" + ) + # The projection `class_embed_type` is the same as the timestep `class_embed_type` except + # 1. the `class_labels` inputs are not first converted to sinusoidal embeddings + # 2. it projects from an arbitrary input dimension. + # + # Note that `TimestepEmbedding` is quite general, being mainly linear layers and activations. + # When used for embedding actual timesteps, the timesteps are first converted to sinusoidal embeddings. + # As a result, `TimestepEmbedding` can be passed arbitrary vectors. + self.class_embedding = TimestepEmbedding(projection_class_embeddings_input_dim, time_embed_dim) + else: + self.class_embedding = None + + if addition_embed_type == "text": + if encoder_hid_dim is not None: + text_time_embedding_from_dim = encoder_hid_dim + else: + text_time_embedding_from_dim = cross_attention_dim + + self.add_embedding = TextTimeEmbedding( + text_time_embedding_from_dim, time_embed_dim, num_heads=addition_embed_type_num_heads + ) + elif addition_embed_type == "text_image": + # text_embed_dim and image_embed_dim DON'T have to be `cross_attention_dim`. To not clutter the __init__ too much + # they are set to `cross_attention_dim` here as this is exactly the required dimension for the currently only use + # case when `addition_embed_type == "text_image"` (Kadinsky 2.1)` + self.add_embedding = TextImageTimeEmbedding( + text_embed_dim=cross_attention_dim, image_embed_dim=cross_attention_dim, time_embed_dim=time_embed_dim + ) + elif addition_embed_type == "text_time": + self.add_time_proj = Timesteps(addition_time_embed_dim, flip_sin_to_cos, freq_shift) + self.add_embedding = TimestepEmbedding(projection_class_embeddings_input_dim, time_embed_dim) + + elif addition_embed_type is not None: + raise ValueError(f"addition_embed_type: {addition_embed_type} must be None, 'text' or 'text_image'.") + + self.down_blocks = nn.ModuleList([]) + self.brushnet_down_blocks = nn.ModuleList([]) + + if isinstance(only_cross_attention, bool): + only_cross_attention = [only_cross_attention] * len(down_block_types) + + if isinstance(attention_head_dim, int): + attention_head_dim = (attention_head_dim,) * len(down_block_types) + + if isinstance(num_attention_heads, int): + num_attention_heads = (num_attention_heads,) * len(down_block_types) + + # down + output_channel = block_out_channels[0] + + brushnet_block = nn.Conv2d(output_channel, output_channel, kernel_size=1) + brushnet_block = zero_module(brushnet_block) + self.brushnet_down_blocks.append(brushnet_block) + + for i, down_block_type in enumerate(down_block_types): + input_channel = output_channel + output_channel = block_out_channels[i] + is_final_block = i == len(block_out_channels) - 1 + + down_block = get_down_block( + down_block_type, + num_layers=layers_per_block, + transformer_layers_per_block=transformer_layers_per_block[i], + in_channels=input_channel, + out_channels=output_channel, + temb_channels=time_embed_dim, + add_downsample=not is_final_block, + resnet_eps=norm_eps, + resnet_act_fn=act_fn, + resnet_groups=norm_num_groups, + cross_attention_dim=cross_attention_dim, + num_attention_heads=num_attention_heads[i], + attention_head_dim=attention_head_dim[i] if attention_head_dim[i] is not None else output_channel, + downsample_padding=downsample_padding, + use_linear_projection=use_linear_projection, + only_cross_attention=only_cross_attention[i], + upcast_attention=upcast_attention, + resnet_time_scale_shift=resnet_time_scale_shift, + ) + self.down_blocks.append(down_block) + + for _ in range(layers_per_block): + brushnet_block = nn.Conv2d(output_channel, output_channel, kernel_size=1) + brushnet_block = zero_module(brushnet_block) + self.brushnet_down_blocks.append(brushnet_block) + + if not is_final_block: + brushnet_block = nn.Conv2d(output_channel, output_channel, kernel_size=1) + brushnet_block = zero_module(brushnet_block) + self.brushnet_down_blocks.append(brushnet_block) + + # mid + mid_block_channel = block_out_channels[-1] + + brushnet_block = nn.Conv2d(mid_block_channel, mid_block_channel, kernel_size=1) + brushnet_block = zero_module(brushnet_block) + self.brushnet_mid_block = brushnet_block + + self.mid_block = get_mid_block( + mid_block_type, + transformer_layers_per_block=transformer_layers_per_block[-1], + in_channels=mid_block_channel, + temb_channels=time_embed_dim, + resnet_eps=norm_eps, + resnet_act_fn=act_fn, + output_scale_factor=mid_block_scale_factor, + resnet_time_scale_shift=resnet_time_scale_shift, + cross_attention_dim=cross_attention_dim, + num_attention_heads=num_attention_heads[-1], + resnet_groups=norm_num_groups, + use_linear_projection=use_linear_projection, + upcast_attention=upcast_attention, + ) + + # count how many layers upsample the images + self.num_upsamplers = 0 + + # up + reversed_block_out_channels = list(reversed(block_out_channels)) + reversed_num_attention_heads = list(reversed(num_attention_heads)) + reversed_transformer_layers_per_block = list(reversed(transformer_layers_per_block)) + only_cross_attention = list(reversed(only_cross_attention)) + + output_channel = reversed_block_out_channels[0] + + self.up_blocks = nn.ModuleList([]) + self.brushnet_up_blocks = nn.ModuleList([]) + + for i, up_block_type in enumerate(up_block_types): + is_final_block = i == len(block_out_channels) - 1 + + prev_output_channel = output_channel + output_channel = reversed_block_out_channels[i] + input_channel = reversed_block_out_channels[min(i + 1, len(block_out_channels) - 1)] + + # add upsample block for all BUT final layer + if not is_final_block: + add_upsample = True + self.num_upsamplers += 1 + else: + add_upsample = False + + up_block = get_up_block( + up_block_type, + num_layers=layers_per_block + 1, + transformer_layers_per_block=reversed_transformer_layers_per_block[i], + in_channels=input_channel, + out_channels=output_channel, + prev_output_channel=prev_output_channel, + temb_channels=time_embed_dim, + add_upsample=add_upsample, + resnet_eps=norm_eps, + resnet_act_fn=act_fn, + resolution_idx=i, + resnet_groups=norm_num_groups, + cross_attention_dim=cross_attention_dim, + num_attention_heads=reversed_num_attention_heads[i], + use_linear_projection=use_linear_projection, + only_cross_attention=only_cross_attention[i], + upcast_attention=upcast_attention, + resnet_time_scale_shift=resnet_time_scale_shift, + attention_head_dim=attention_head_dim[i] if attention_head_dim[i] is not None else output_channel, + ) + self.up_blocks.append(up_block) + prev_output_channel = output_channel + + for _ in range(layers_per_block + 1): + brushnet_block = nn.Conv2d(output_channel, output_channel, kernel_size=1) + brushnet_block = zero_module(brushnet_block) + self.brushnet_up_blocks.append(brushnet_block) + + if not is_final_block: + brushnet_block = nn.Conv2d(output_channel, output_channel, kernel_size=1) + brushnet_block = zero_module(brushnet_block) + self.brushnet_up_blocks.append(brushnet_block) + + @classmethod + def from_unet( + cls, + unet: UNet2DConditionModel, + brushnet_conditioning_channel_order: str = "rgb", + conditioning_embedding_out_channels: Optional[Tuple[int, ...]] = (16, 32, 96, 256), + load_weights_from_unet: bool = True, + conditioning_channels: int = 5, + ): + r""" + Instantiate a [`BrushNetModel`] from [`UNet2DConditionModel`]. + + Parameters: + unet (`UNet2DConditionModel`): + The UNet model weights to copy to the [`BrushNetModel`]. All configuration options are also copied + where applicable. + """ + transformer_layers_per_block = ( + unet.config.transformer_layers_per_block if "transformer_layers_per_block" in unet.config else 1 + ) + encoder_hid_dim = unet.config.encoder_hid_dim if "encoder_hid_dim" in unet.config else None + encoder_hid_dim_type = unet.config.encoder_hid_dim_type if "encoder_hid_dim_type" in unet.config else None + addition_embed_type = unet.config.addition_embed_type if "addition_embed_type" in unet.config else None + addition_time_embed_dim = ( + unet.config.addition_time_embed_dim if "addition_time_embed_dim" in unet.config else None + ) + + brushnet = cls( + in_channels=unet.config.in_channels, + conditioning_channels=conditioning_channels, + flip_sin_to_cos=unet.config.flip_sin_to_cos, + freq_shift=unet.config.freq_shift, + # down_block_types=['DownBlock2D','DownBlock2D','DownBlock2D','DownBlock2D'], + down_block_types=[ + "CrossAttnDownBlock2D", + "CrossAttnDownBlock2D", + "CrossAttnDownBlock2D", + "DownBlock2D", + ], + # mid_block_type='MidBlock2D', + mid_block_type="UNetMidBlock2DCrossAttn", + # up_block_types=['UpBlock2D','UpBlock2D','UpBlock2D','UpBlock2D'], + up_block_types=["UpBlock2D", "CrossAttnUpBlock2D", "CrossAttnUpBlock2D", "CrossAttnUpBlock2D"], + only_cross_attention=unet.config.only_cross_attention, + block_out_channels=unet.config.block_out_channels, + layers_per_block=unet.config.layers_per_block, + downsample_padding=unet.config.downsample_padding, + mid_block_scale_factor=unet.config.mid_block_scale_factor, + act_fn=unet.config.act_fn, + norm_num_groups=unet.config.norm_num_groups, + norm_eps=unet.config.norm_eps, + cross_attention_dim=unet.config.cross_attention_dim, + transformer_layers_per_block=transformer_layers_per_block, + encoder_hid_dim=encoder_hid_dim, + encoder_hid_dim_type=encoder_hid_dim_type, + attention_head_dim=unet.config.attention_head_dim, + num_attention_heads=unet.config.num_attention_heads, + use_linear_projection=unet.config.use_linear_projection, + class_embed_type=unet.config.class_embed_type, + addition_embed_type=addition_embed_type, + addition_time_embed_dim=addition_time_embed_dim, + num_class_embeds=unet.config.num_class_embeds, + upcast_attention=unet.config.upcast_attention, + resnet_time_scale_shift=unet.config.resnet_time_scale_shift, + projection_class_embeddings_input_dim=unet.config.projection_class_embeddings_input_dim, + brushnet_conditioning_channel_order=brushnet_conditioning_channel_order, + conditioning_embedding_out_channels=conditioning_embedding_out_channels, + ) + + if load_weights_from_unet: + conv_in_condition_weight = torch.zeros_like(brushnet.conv_in_condition.weight) + conv_in_condition_weight[:, :4, ...] = unet.conv_in.weight + conv_in_condition_weight[:, 4:8, ...] = unet.conv_in.weight + brushnet.conv_in_condition.weight = torch.nn.Parameter(conv_in_condition_weight) + brushnet.conv_in_condition.bias = unet.conv_in.bias + + brushnet.time_proj.load_state_dict(unet.time_proj.state_dict()) + brushnet.time_embedding.load_state_dict(unet.time_embedding.state_dict()) + + if brushnet.class_embedding: + brushnet.class_embedding.load_state_dict(unet.class_embedding.state_dict()) + + brushnet.down_blocks.load_state_dict(unet.down_blocks.state_dict(), strict=False) + brushnet.mid_block.load_state_dict(unet.mid_block.state_dict(), strict=False) + brushnet.up_blocks.load_state_dict(unet.up_blocks.state_dict(), strict=False) + + return brushnet.to(unet.dtype) + + @property + # Copied from diffusers.models.unets.unet_2d_condition.UNet2DConditionModel.attn_processors + def attn_processors(self) -> Dict[str, AttentionProcessor]: + r""" + Returns: + `dict` of attention processors: A dictionary containing all attention processors used in the model with + indexed by its weight name. + """ + # set recursively + processors = {} + + def fn_recursive_add_processors(name: str, module: torch.nn.Module, processors: Dict[str, AttentionProcessor]): + if hasattr(module, "get_processor"): + processors[f"{name}.processor"] = module.get_processor(return_deprecated_lora=True) + + for sub_name, child in module.named_children(): + fn_recursive_add_processors(f"{name}.{sub_name}", child, processors) + + return processors + + for name, module in self.named_children(): + fn_recursive_add_processors(name, module, processors) + + return processors + + # Copied from diffusers.models.unets.unet_2d_condition.UNet2DConditionModel.set_attn_processor + def set_attn_processor(self, processor: Union[AttentionProcessor, Dict[str, AttentionProcessor]]): + r""" + Sets the attention processor to use to compute attention. + + Parameters: + processor (`dict` of `AttentionProcessor` or only `AttentionProcessor`): + The instantiated processor class or a dictionary of processor classes that will be set as the processor + for **all** `Attention` layers. + + If `processor` is a dict, the key needs to define the path to the corresponding cross attention + processor. This is strongly recommended when setting trainable attention processors. + + """ + count = len(self.attn_processors.keys()) + + if isinstance(processor, dict) and len(processor) != count: + raise ValueError( + f"A dict of processors was passed, but the number of processors {len(processor)} does not match the" + f" number of attention layers: {count}. Please make sure to pass {count} processor classes." + ) + + def fn_recursive_attn_processor(name: str, module: torch.nn.Module, processor): + if hasattr(module, "set_processor"): + if not isinstance(processor, dict): + module.set_processor(processor) + else: + module.set_processor(processor.pop(f"{name}.processor")) + + for sub_name, child in module.named_children(): + fn_recursive_attn_processor(f"{name}.{sub_name}", child, processor) + + for name, module in self.named_children(): + fn_recursive_attn_processor(name, module, processor) + + # Copied from diffusers.models.unets.unet_2d_condition.UNet2DConditionModel.set_default_attn_processor + def set_default_attn_processor(self): + """ + Disables custom attention processors and sets the default attention implementation. + """ + if all(proc.__class__ in ADDED_KV_ATTENTION_PROCESSORS for proc in self.attn_processors.values()): + processor = AttnAddedKVProcessor() + elif all(proc.__class__ in CROSS_ATTENTION_PROCESSORS for proc in self.attn_processors.values()): + processor = AttnProcessor() + else: + raise ValueError( + f"Cannot call `set_default_attn_processor` when attention processors are of type {next(iter(self.attn_processors.values()))}" + ) + + self.set_attn_processor(processor) + + # Copied from diffusers.models.unets.unet_2d_condition.UNet2DConditionModel.set_attention_slice + def set_attention_slice(self, slice_size: Union[str, int, List[int]]) -> None: + r""" + Enable sliced attention computation. + + When this option is enabled, the attention module splits the input tensor in slices to compute attention in + several steps. This is useful for saving some memory in exchange for a small decrease in speed. + + Args: + slice_size (`str` or `int` or `list(int)`, *optional*, defaults to `"auto"`): + When `"auto"`, input to the attention heads is halved, so attention is computed in two steps. If + `"max"`, maximum amount of memory is saved by running only one slice at a time. If a number is + provided, uses as many slices as `attention_head_dim // slice_size`. In this case, `attention_head_dim` + must be a multiple of `slice_size`. + """ + sliceable_head_dims = [] + + def fn_recursive_retrieve_sliceable_dims(module: torch.nn.Module): + if hasattr(module, "set_attention_slice"): + sliceable_head_dims.append(module.sliceable_head_dim) + + for child in module.children(): + fn_recursive_retrieve_sliceable_dims(child) + + # retrieve number of attention layers + for module in self.children(): + fn_recursive_retrieve_sliceable_dims(module) + + num_sliceable_layers = len(sliceable_head_dims) + + if slice_size == "auto": + # half the attention head size is usually a good trade-off between + # speed and memory + slice_size = [dim // 2 for dim in sliceable_head_dims] + elif slice_size == "max": + # make smallest slice possible + slice_size = num_sliceable_layers * [1] + + slice_size = num_sliceable_layers * [slice_size] if not isinstance(slice_size, list) else slice_size + + if len(slice_size) != len(sliceable_head_dims): + raise ValueError( + f"You have provided {len(slice_size)}, but {self.config} has {len(sliceable_head_dims)} different" + f" attention layers. Make sure to match `len(slice_size)` to be {len(sliceable_head_dims)}." + ) + + for i in range(len(slice_size)): + size = slice_size[i] + dim = sliceable_head_dims[i] + if size is not None and size > dim: + raise ValueError(f"size {size} has to be smaller or equal to {dim}.") + + # Recursively walk through all the children. + # Any children which exposes the set_attention_slice method + # gets the message + def fn_recursive_set_attention_slice(module: torch.nn.Module, slice_size: List[int]): + if hasattr(module, "set_attention_slice"): + module.set_attention_slice(slice_size.pop()) + + for child in module.children(): + fn_recursive_set_attention_slice(child, slice_size) + + reversed_slice_size = list(reversed(slice_size)) + for module in self.children(): + fn_recursive_set_attention_slice(module, reversed_slice_size) + + def _set_gradient_checkpointing(self, module, value: bool = False) -> None: + if isinstance(module, (CrossAttnDownBlock2D, DownBlock2D)): + module.gradient_checkpointing = value + + def forward( + self, + sample: torch.FloatTensor, + timestep: Union[torch.Tensor, float, int], + encoder_hidden_states: torch.Tensor, + brushnet_cond: torch.FloatTensor, + conditioning_scale: float = 1.0, + class_labels: Optional[torch.Tensor] = None, + timestep_cond: Optional[torch.Tensor] = None, + attention_mask: Optional[torch.Tensor] = None, + added_cond_kwargs: Optional[Dict[str, torch.Tensor]] = None, + cross_attention_kwargs: Optional[Dict[str, Any]] = None, + guess_mode: bool = False, + return_dict: bool = True, + debug=False, + ) -> Union[BrushNetOutput, Tuple[Tuple[torch.FloatTensor, ...], torch.FloatTensor]]: + """ + The [`BrushNetModel`] forward method. + + Args: + sample (`torch.FloatTensor`): + The noisy input tensor. + timestep (`Union[torch.Tensor, float, int]`): + The number of timesteps to denoise an input. + encoder_hidden_states (`torch.Tensor`): + The encoder hidden states. + brushnet_cond (`torch.FloatTensor`): + The conditional input tensor of shape `(batch_size, sequence_length, hidden_size)`. + conditioning_scale (`float`, defaults to `1.0`): + The scale factor for BrushNet outputs. + class_labels (`torch.Tensor`, *optional*, defaults to `None`): + Optional class labels for conditioning. Their embeddings will be summed with the timestep embeddings. + timestep_cond (`torch.Tensor`, *optional*, defaults to `None`): + Additional conditional embeddings for timestep. If provided, the embeddings will be summed with the + timestep_embedding passed through the `self.time_embedding` layer to obtain the final timestep + embeddings. + attention_mask (`torch.Tensor`, *optional*, defaults to `None`): + An attention mask of shape `(batch, key_tokens)` is applied to `encoder_hidden_states`. If `1` the mask + is kept, otherwise if `0` it is discarded. Mask will be converted into a bias, which adds large + negative values to the attention scores corresponding to "discard" tokens. + added_cond_kwargs (`dict`): + Additional conditions for the Stable Diffusion XL UNet. + cross_attention_kwargs (`dict[str]`, *optional*, defaults to `None`): + A kwargs dictionary that if specified is passed along to the `AttnProcessor`. + guess_mode (`bool`, defaults to `False`): + In this mode, the BrushNet encoder tries its best to recognize the input content of the input even if + you remove all prompts. A `guidance_scale` between 3.0 and 5.0 is recommended. + return_dict (`bool`, defaults to `True`): + Whether or not to return a [`~models.brushnet.BrushNetOutput`] instead of a plain tuple. + + Returns: + [`~models.brushnet.BrushNetOutput`] **or** `tuple`: + If `return_dict` is `True`, a [`~models.brushnet.BrushNetOutput`] is returned, otherwise a tuple is + returned where the first element is the sample tensor. + """ + # check channel order + channel_order = self.config.brushnet_conditioning_channel_order + + if channel_order == "rgb": + # in rgb order by default + ... + elif channel_order == "bgr": + brushnet_cond = torch.flip(brushnet_cond, dims=[1]) + else: + raise ValueError(f"unknown `brushnet_conditioning_channel_order`: {channel_order}") + + if debug: print('BrushNet CA: attn mask') + + # prepare attention_mask + if attention_mask is not None: + attention_mask = (1 - attention_mask.to(sample.dtype)) * -10000.0 + attention_mask = attention_mask.unsqueeze(1) + + if debug: print('BrushNet CA: time') + + # 1. time + timesteps = timestep + if not torch.is_tensor(timesteps): + # TODO: this requires sync between CPU and GPU. So try to pass timesteps as tensors if you can + # This would be a good case for the `match` statement (Python 3.10+) + is_mps = sample.device.type == "mps" + if isinstance(timestep, float): + dtype = torch.float32 if is_mps else torch.float64 + else: + dtype = torch.int32 if is_mps else torch.int64 + timesteps = torch.tensor([timesteps], dtype=dtype, device=sample.device) + elif len(timesteps.shape) == 0: + timesteps = timesteps[None].to(sample.device) + + # broadcast to batch dimension in a way that's compatible with ONNX/Core ML + timesteps = timesteps.expand(sample.shape[0]) + + t_emb = self.time_proj(timesteps) + + # timesteps does not contain any weights and will always return f32 tensors + # but time_embedding might actually be running in fp16. so we need to cast here. + # there might be better ways to encapsulate this. + t_emb = t_emb.to(dtype=sample.dtype) + + emb = self.time_embedding(t_emb, timestep_cond) + aug_emb = None + + if self.class_embedding is not None: + if class_labels is None: + raise ValueError("class_labels should be provided when num_class_embeds > 0") + + if self.config.class_embed_type == "timestep": + class_labels = self.time_proj(class_labels) + + class_emb = self.class_embedding(class_labels).to(dtype=self.dtype) + emb = emb + class_emb + + if self.config.addition_embed_type is not None: + if self.config.addition_embed_type == "text": + aug_emb = self.add_embedding(encoder_hidden_states) + + elif self.config.addition_embed_type == "text_time": + if "text_embeds" not in added_cond_kwargs: + raise ValueError( + f"{self.__class__} has the config param `addition_embed_type` set to 'text_time' which requires the keyword argument `text_embeds` to be passed in `added_cond_kwargs`" + ) + text_embeds = added_cond_kwargs.get("text_embeds") + if "time_ids" not in added_cond_kwargs: + raise ValueError( + f"{self.__class__} has the config param `addition_embed_type` set to 'text_time' which requires the keyword argument `time_ids` to be passed in `added_cond_kwargs`" + ) + time_ids = added_cond_kwargs.get("time_ids") + time_embeds = self.add_time_proj(time_ids.flatten()) + time_embeds = time_embeds.reshape((text_embeds.shape[0], -1)) + + add_embeds = torch.concat([text_embeds, time_embeds], dim=-1) + add_embeds = add_embeds.to(emb.dtype) + aug_emb = self.add_embedding(add_embeds) + + emb = emb + aug_emb if aug_emb is not None else emb + + if debug: print('BrushNet CA: pre-process') + + + # 2. pre-process + brushnet_cond = torch.concat([sample, brushnet_cond], 1) + sample = self.conv_in_condition(brushnet_cond) + + if debug: print('BrushNet CA: down') + + # 3. down + down_block_res_samples = (sample,) + for downsample_block in self.down_blocks: + if hasattr(downsample_block, "has_cross_attention") and downsample_block.has_cross_attention: + if debug: print('BrushNet CA (down block with XA): ', type(downsample_block)) + sample, res_samples = downsample_block( + hidden_states=sample, + temb=emb, + encoder_hidden_states=encoder_hidden_states, + attention_mask=attention_mask, + cross_attention_kwargs=cross_attention_kwargs, + debug=debug, + ) + else: + if debug: print('BrushNet CA (down block): ', type(downsample_block)) + sample, res_samples = downsample_block(hidden_states=sample, temb=emb, debug=debug) + + down_block_res_samples += res_samples + + if debug: print('BrushNet CA: PP down') + + # 4. PaintingNet down blocks + brushnet_down_block_res_samples = () + for down_block_res_sample, brushnet_down_block in zip(down_block_res_samples, self.brushnet_down_blocks): + down_block_res_sample = brushnet_down_block(down_block_res_sample) + brushnet_down_block_res_samples = brushnet_down_block_res_samples + (down_block_res_sample,) + + if debug: print('BrushNet CA: PP mid') + + # 5. mid + if self.mid_block is not None: + if hasattr(self.mid_block, "has_cross_attention") and self.mid_block.has_cross_attention: + sample = self.mid_block( + sample, + emb, + encoder_hidden_states=encoder_hidden_states, + attention_mask=attention_mask, + cross_attention_kwargs=cross_attention_kwargs, + ) + else: + sample = self.mid_block(sample, emb) + + if debug: print('BrushNet CA: mid') + + # 6. BrushNet mid blocks + brushnet_mid_block_res_sample = self.brushnet_mid_block(sample) + + if debug: print('BrushNet CA: PP up') + + # 7. up + up_block_res_samples = () + for i, upsample_block in enumerate(self.up_blocks): + is_final_block = i == len(self.up_blocks) - 1 + + res_samples = down_block_res_samples[-len(upsample_block.resnets) :] + down_block_res_samples = down_block_res_samples[: -len(upsample_block.resnets)] + + # if we have not reached the final block and need to forward the + # upsample size, we do it here + if not is_final_block: + upsample_size = down_block_res_samples[-1].shape[2:] + + if hasattr(upsample_block, "has_cross_attention") and upsample_block.has_cross_attention: + sample, up_res_samples = upsample_block( + hidden_states=sample, + temb=emb, + res_hidden_states_tuple=res_samples, + encoder_hidden_states=encoder_hidden_states, + cross_attention_kwargs=cross_attention_kwargs, + upsample_size=upsample_size, + attention_mask=attention_mask, + return_res_samples=True, + ) + else: + sample, up_res_samples = upsample_block( + hidden_states=sample, + temb=emb, + res_hidden_states_tuple=res_samples, + upsample_size=upsample_size, + return_res_samples=True, + ) + + up_block_res_samples += up_res_samples + + if debug: print('BrushNet CA: up') + + # 8. BrushNet up blocks + brushnet_up_block_res_samples = () + for up_block_res_sample, brushnet_up_block in zip(up_block_res_samples, self.brushnet_up_blocks): + up_block_res_sample = brushnet_up_block(up_block_res_sample) + brushnet_up_block_res_samples = brushnet_up_block_res_samples + (up_block_res_sample,) + + if debug: print('BrushNet CA: scaling') + + # 6. scaling + if guess_mode and not self.config.global_pool_conditions: + scales = torch.logspace( + -1, + 0, + len(brushnet_down_block_res_samples) + 1 + len(brushnet_up_block_res_samples), + device=sample.device, + ) # 0.1 to 1.0 + scales = scales * conditioning_scale + + brushnet_down_block_res_samples = [ + sample * scale + for sample, scale in zip( + brushnet_down_block_res_samples, scales[: len(brushnet_down_block_res_samples)] + ) + ] + brushnet_mid_block_res_sample = ( + brushnet_mid_block_res_sample * scales[len(brushnet_down_block_res_samples)] + ) + brushnet_up_block_res_samples = [ + sample * scale + for sample, scale in zip( + brushnet_up_block_res_samples, scales[len(brushnet_down_block_res_samples) + 1 :] + ) + ] + else: + brushnet_down_block_res_samples = [ + sample * conditioning_scale for sample in brushnet_down_block_res_samples + ] + brushnet_mid_block_res_sample = brushnet_mid_block_res_sample * conditioning_scale + brushnet_up_block_res_samples = [sample * conditioning_scale for sample in brushnet_up_block_res_samples] + + if self.config.global_pool_conditions: + brushnet_down_block_res_samples = [ + torch.mean(sample, dim=(2, 3), keepdim=True) for sample in brushnet_down_block_res_samples + ] + brushnet_mid_block_res_sample = torch.mean(brushnet_mid_block_res_sample, dim=(2, 3), keepdim=True) + brushnet_up_block_res_samples = [ + torch.mean(sample, dim=(2, 3), keepdim=True) for sample in brushnet_up_block_res_samples + ] + + if debug: print('BrushNet CA: finish') + + if not return_dict: + return (brushnet_down_block_res_samples, brushnet_mid_block_res_sample, brushnet_up_block_res_samples) + + return BrushNetOutput( + down_block_res_samples=brushnet_down_block_res_samples, + mid_block_res_sample=brushnet_mid_block_res_sample, + up_block_res_samples=brushnet_up_block_res_samples, + ) + +except ImportError: + BrushNetModel = None + PowerPaintModel = None + # print("\33[33mModule 'diffusers' load failed. If you don't have it installed, do it:\033[0m") + # print("\33[33mpip install diffusers\033[0m") \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/brushnet/model_patch.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/brushnet/model_patch.py new file mode 100644 index 0000000000000000000000000000000000000000..fe8d3f271a3b315b16a16aab8a6814b5b51148f4 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/brushnet/model_patch.py @@ -0,0 +1,137 @@ +import torch +import comfy + +# Check and add 'model_patch' to model.model_options['transformer_options'] +def add_model_patch_option(model): + if 'transformer_options' not in model.model_options: + model.model_options['transformer_options'] = {} + to = model.model_options['transformer_options'] + if "model_patch" not in to: + to["model_patch"] = {} + return to + + +# Patch model with model_function_wrapper +def patch_model_function_wrapper(model, forward_patch, remove=False): + def brushnet_model_function_wrapper(apply_model_method, options_dict): + to = options_dict['c']['transformer_options'] + + control = None + if 'control' in options_dict['c']: + control = options_dict['c']['control'] + + x = options_dict['input'] + timestep = options_dict['timestep'] + + # check if there are patches to execute + if 'model_patch' not in to or 'forward' not in to['model_patch']: + return apply_model_method(x, timestep, **options_dict['c']) + + mp = to['model_patch'] + unet = mp['unet'] + + all_sigmas = mp['all_sigmas'] + sigma = to['sigmas'][0].item() + total_steps = all_sigmas.shape[0] - 1 + step = torch.argmin((all_sigmas - sigma).abs()).item() + + mp['step'] = step + mp['total_steps'] = total_steps + + # comfy.model_base.apply_model + xc = model.model.model_sampling.calculate_input(timestep, x) + if 'c_concat' in options_dict['c'] and options_dict['c']['c_concat'] is not None: + xc = torch.cat([xc] + [options_dict['c']['c_concat']], dim=1) + t = model.model.model_sampling.timestep(timestep).float() + # execute all patches + for method in mp['forward']: + method(unet, xc, t, to, control) + + return apply_model_method(x, timestep, **options_dict['c']) + + if "model_function_wrapper" in model.model_options and model.model_options["model_function_wrapper"]: + print('BrushNet is going to replace existing model_function_wrapper:', + model.model_options["model_function_wrapper"]) + model.set_model_unet_function_wrapper(brushnet_model_function_wrapper) + + to = add_model_patch_option(model) + mp = to['model_patch'] + + if isinstance(model.model.model_config, comfy.supported_models.SD15): + mp['SDXL'] = False + elif isinstance(model.model.model_config, comfy.supported_models.SDXL): + mp['SDXL'] = True + else: + print('Base model type: ', type(model.model.model_config)) + raise Exception("Unsupported model type: ", type(model.model.model_config)) + + if 'forward' not in mp: + mp['forward'] = [] + + if remove: + if forward_patch in mp['forward']: + mp['forward'].remove(forward_patch) + else: + mp['forward'].append(forward_patch) + + mp['unet'] = model.model.diffusion_model + mp['step'] = 0 + mp['total_steps'] = 1 + + # apply patches to code + if comfy.samplers.sample.__doc__ is None or 'BrushNet' not in comfy.samplers.sample.__doc__: + comfy.samplers.original_sample = comfy.samplers.sample + comfy.samplers.sample = modified_sample + + if comfy.ldm.modules.diffusionmodules.openaimodel.apply_control.__doc__ is None or \ + 'BrushNet' not in comfy.ldm.modules.diffusionmodules.openaimodel.apply_control.__doc__: + comfy.ldm.modules.diffusionmodules.openaimodel.original_apply_control = comfy.ldm.modules.diffusionmodules.openaimodel.apply_control + comfy.ldm.modules.diffusionmodules.openaimodel.apply_control = modified_apply_control + + +# Model needs current step number and cfg at inference step. It is possible to write a custom KSampler but I'd like to use ComfyUI's one. +# The first versions had modified_common_ksampler, but it broke custom KSampler nodes +def modified_sample(model, noise, positive, negative, cfg, device, sampler, sigmas, model_options={}, + latent_image=None, denoise_mask=None, callback=None, disable_pbar=False, seed=None): + ''' Modified by BrushNet nodes''' + cfg_guider = comfy.samplers.CFGGuider(model) + cfg_guider.set_conds(positive, negative) + cfg_guider.set_cfg(cfg) + + ### Modified part ###################################################################### + to = add_model_patch_option(model) + to['model_patch']['all_sigmas'] = sigmas + ####################################################################################### + + return cfg_guider.sample(noise, latent_image, sampler, sigmas, denoise_mask, callback, disable_pbar, seed) + +# To use Controlnet with RAUNet it is much easier to modify apply_control a little +def modified_apply_control(h, control, name): + '''Modified by BrushNet nodes''' + if control is not None and name in control and len(control[name]) > 0: + ctrl = control[name].pop() + if ctrl is not None: + if h.shape[2] != ctrl.shape[2] or h.shape[3] != ctrl.shape[3]: + ctrl = torch.nn.functional.interpolate(ctrl, size=(h.shape[2], h.shape[3]), mode='bicubic').to( + h.dtype).to(h.device) + try: + h += ctrl + except: + print.warning("warning control could not be applied {} {}".format(h.shape, ctrl.shape)) + return h + +def add_model_patch(model): + to = add_model_patch_option(model) + mp = to['model_patch'] + if "brushnet" in mp: + if isinstance(model.model.model_config, comfy.supported_models.SD15): + mp['SDXL'] = False + elif isinstance(model.model.model_config, comfy.supported_models.SDXL): + mp['SDXL'] = True + else: + print('Base model type: ', type(model.model.model_config)) + raise Exception("Unsupported model type: ", type(model.model.model_config)) + + mp['unet'] = model.model.diffusion_model + mp['step'] = 0 + mp['total_steps'] = 1 \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/brushnet/powerpaint_utils.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/brushnet/powerpaint_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..1cae2c1334c56e390121dc050e690f5cefdd5d2a --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/brushnet/powerpaint_utils.py @@ -0,0 +1,467 @@ +import copy +import random + +import torch +import torch.nn as nn +from transformers import CLIPTokenizer +from typing import Any, List, Optional, Union + + +class TokenizerWrapper: + """Tokenizer wrapper for CLIPTokenizer. Only support CLIPTokenizer + currently. This wrapper is modified from https://github.com/huggingface/dif + fusers/blob/e51f19aee82c8dd874b715a09dbc521d88835d68/src/diffusers/loaders. + py#L358 # noqa. + + Args: + from_pretrained (Union[str, os.PathLike], optional): The *model id* + of a pretrained model or a path to a *directory* containing + model weights and config. Defaults to None. + from_config (Union[str, os.PathLike], optional): The *model id* + of a pretrained model or a path to a *directory* containing + model weights and config. Defaults to None. + + *args, **kwargs: If `from_pretrained` is passed, *args and **kwargs + will be passed to `from_pretrained` function. Otherwise, *args + and **kwargs will be used to initialize the model by + `self._module_cls(*args, **kwargs)`. + """ + + def __init__(self, tokenizer: CLIPTokenizer): + self.wrapped = tokenizer + self.token_map = {} + + def __getattr__(self, name: str) -> Any: + if name in self.__dict__: + return getattr(self, name) + # if name == "wrapped": + # return getattr(self, 'wrapped')#super().__getattr__("wrapped") + + try: + return getattr(self.wrapped, name) + except AttributeError: + raise AttributeError( + "'name' cannot be found in both " + f"'{self.__class__.__name__}' and " + f"'{self.__class__.__name__}.tokenizer'." + ) + + def try_adding_tokens(self, tokens: Union[str, List[str]], *args, **kwargs): + """Attempt to add tokens to the tokenizer. + + Args: + tokens (Union[str, List[str]]): The tokens to be added. + """ + num_added_tokens = self.wrapped.add_tokens(tokens, *args, **kwargs) + assert num_added_tokens != 0, ( + f"The tokenizer already contains the token {tokens}. Please pass " + "a different `placeholder_token` that is not already in the " + "tokenizer." + ) + + def get_token_info(self, token: str) -> dict: + """Get the information of a token, including its start and end index in + the current tokenizer. + + Args: + token (str): The token to be queried. + + Returns: + dict: The information of the token, including its start and end + index in current tokenizer. + """ + token_ids = self.__call__(token).input_ids + start, end = token_ids[1], token_ids[-2] + 1 + return {"name": token, "start": start, "end": end} + + def add_placeholder_token(self, placeholder_token: str, *args, num_vec_per_token: int = 1, **kwargs): + """Add placeholder tokens to the tokenizer. + + Args: + placeholder_token (str): The placeholder token to be added. + num_vec_per_token (int, optional): The number of vectors of + the added placeholder token. + *args, **kwargs: The arguments for `self.wrapped.add_tokens`. + """ + output = [] + if num_vec_per_token == 1: + self.try_adding_tokens(placeholder_token, *args, **kwargs) + output.append(placeholder_token) + else: + output = [] + for i in range(num_vec_per_token): + ith_token = placeholder_token + f"_{i}" + self.try_adding_tokens(ith_token, *args, **kwargs) + output.append(ith_token) + + for token in self.token_map: + if token in placeholder_token: + raise ValueError( + f"The tokenizer already has placeholder token {token} " + f"that can get confused with {placeholder_token} " + "keep placeholder tokens independent" + ) + self.token_map[placeholder_token] = output + + def replace_placeholder_tokens_in_text( + self, text: Union[str, List[str]], vector_shuffle: bool = False, prop_tokens_to_load: float = 1.0 + ) -> Union[str, List[str]]: + """Replace the keywords in text with placeholder tokens. This function + will be called in `self.__call__` and `self.encode`. + + Args: + text (Union[str, List[str]]): The text to be processed. + vector_shuffle (bool, optional): Whether to shuffle the vectors. + Defaults to False. + prop_tokens_to_load (float, optional): The proportion of tokens to + be loaded. If 1.0, all tokens will be loaded. Defaults to 1.0. + + Returns: + Union[str, List[str]]: The processed text. + """ + if isinstance(text, list): + output = [] + for i in range(len(text)): + output.append(self.replace_placeholder_tokens_in_text(text[i], vector_shuffle=vector_shuffle)) + return output + + for placeholder_token in self.token_map: + if placeholder_token in text: + tokens = self.token_map[placeholder_token] + tokens = tokens[: 1 + int(len(tokens) * prop_tokens_to_load)] + if vector_shuffle: + tokens = copy.copy(tokens) + random.shuffle(tokens) + text = text.replace(placeholder_token, " ".join(tokens)) + return text + + def replace_text_with_placeholder_tokens(self, text: Union[str, List[str]]) -> Union[str, List[str]]: + """Replace the placeholder tokens in text with the original keywords. + This function will be called in `self.decode`. + + Args: + text (Union[str, List[str]]): The text to be processed. + + Returns: + Union[str, List[str]]: The processed text. + """ + if isinstance(text, list): + output = [] + for i in range(len(text)): + output.append(self.replace_text_with_placeholder_tokens(text[i])) + return output + + for placeholder_token, tokens in self.token_map.items(): + merged_tokens = " ".join(tokens) + if merged_tokens in text: + text = text.replace(merged_tokens, placeholder_token) + return text + + def __call__( + self, + text: Union[str, List[str]], + *args, + vector_shuffle: bool = False, + prop_tokens_to_load: float = 1.0, + **kwargs, + ): + """The call function of the wrapper. + + Args: + text (Union[str, List[str]]): The text to be tokenized. + vector_shuffle (bool, optional): Whether to shuffle the vectors. + Defaults to False. + prop_tokens_to_load (float, optional): The proportion of tokens to + be loaded. If 1.0, all tokens will be loaded. Defaults to 1.0 + *args, **kwargs: The arguments for `self.wrapped.__call__`. + """ + replaced_text = self.replace_placeholder_tokens_in_text( + text, vector_shuffle=vector_shuffle, prop_tokens_to_load=prop_tokens_to_load + ) + + return self.wrapped.__call__(replaced_text, *args, **kwargs) + + def encode(self, text: Union[str, List[str]], *args, **kwargs): + """Encode the passed text to token index. + + Args: + text (Union[str, List[str]]): The text to be encode. + *args, **kwargs: The arguments for `self.wrapped.__call__`. + """ + replaced_text = self.replace_placeholder_tokens_in_text(text) + return self.wrapped(replaced_text, *args, **kwargs) + + def decode(self, token_ids, return_raw: bool = False, *args, **kwargs) -> Union[str, List[str]]: + """Decode the token index to text. + + Args: + token_ids: The token index to be decoded. + return_raw: Whether keep the placeholder token in the text. + Defaults to False. + *args, **kwargs: The arguments for `self.wrapped.decode`. + + Returns: + Union[str, List[str]]: The decoded text. + """ + text = self.wrapped.decode(token_ids, *args, **kwargs) + if return_raw: + return text + replaced_text = self.replace_text_with_placeholder_tokens(text) + return replaced_text + + def __repr__(self): + """The representation of the wrapper.""" + s = super().__repr__() + prefix = f"Wrapped Module Class: {self._module_cls}\n" + prefix += f"Wrapped Module Name: {self._module_name}\n" + if self._from_pretrained: + prefix += f"From Pretrained: {self._from_pretrained}\n" + s = prefix + s + return s + + +class EmbeddingLayerWithFixes(nn.Module): + """The revised embedding layer to support external embeddings. This design + of this class is inspired by https://github.com/AUTOMATIC1111/stable- + diffusion-webui/blob/22bcc7be428c94e9408f589966c2040187245d81/modules/sd_hi + jack.py#L224 # noqa. + + Args: + wrapped (nn.Emebdding): The embedding layer to be wrapped. + external_embeddings (Union[dict, List[dict]], optional): The external + embeddings added to this layer. Defaults to None. + """ + + def __init__(self, wrapped: nn.Embedding, external_embeddings: Optional[Union[dict, List[dict]]] = None): + super().__init__() + self.wrapped = wrapped + self.num_embeddings = wrapped.weight.shape[0] + + self.external_embeddings = [] + if external_embeddings: + self.add_embeddings(external_embeddings) + + self.trainable_embeddings = nn.ParameterDict() + + @property + def weight(self): + """Get the weight of wrapped embedding layer.""" + return self.wrapped.weight + + def check_duplicate_names(self, embeddings: List[dict]): + """Check whether duplicate names exist in list of 'external + embeddings'. + + Args: + embeddings (List[dict]): A list of embedding to be check. + """ + names = [emb["name"] for emb in embeddings] + assert len(names) == len(set(names)), ( + "Found duplicated names in 'external_embeddings'. Name list: " f"'{names}'" + ) + + def check_ids_overlap(self, embeddings): + """Check whether overlap exist in token ids of 'external_embeddings'. + + Args: + embeddings (List[dict]): A list of embedding to be check. + """ + ids_range = [[emb["start"], emb["end"], emb["name"]] for emb in embeddings] + ids_range.sort() # sort by 'start' + # check if 'end' has overlapping + for idx in range(len(ids_range) - 1): + name1, name2 = ids_range[idx][-1], ids_range[idx + 1][-1] + assert ids_range[idx][1] <= ids_range[idx + 1][0], ( + f"Found ids overlapping between embeddings '{name1}' " f"and '{name2}'." + ) + + def add_embeddings(self, embeddings: Optional[Union[dict, List[dict]]]): + """Add external embeddings to this layer. + Use case: + Args: + embeddings (Union[dict, list[dict]]): The external embeddings to + be added. Each dict must contain the following 4 fields: 'name' + (the name of this embedding), 'embedding' (the embedding + tensor), 'start' (the start token id of this embedding), 'end' + (the end token id of this embedding). For example: + `{name: NAME, start: START, end: END, embedding: torch.Tensor}` + """ + if isinstance(embeddings, dict): + embeddings = [embeddings] + + self.external_embeddings += embeddings + self.check_duplicate_names(self.external_embeddings) + self.check_ids_overlap(self.external_embeddings) + + # set for trainable + added_trainable_emb_info = [] + for embedding in embeddings: + trainable = embedding.get("trainable", False) + if trainable: + name = embedding["name"] + embedding["embedding"] = torch.nn.Parameter(embedding["embedding"]) + self.trainable_embeddings[name] = embedding["embedding"] + added_trainable_emb_info.append(name) + + added_emb_info = [emb["name"] for emb in embeddings] + added_emb_info = ", ".join(added_emb_info) + print(f"Successfully add external embeddings: {added_emb_info}.", "current") + + if added_trainable_emb_info: + added_trainable_emb_info = ", ".join(added_trainable_emb_info) + print("Successfully add trainable external embeddings: " f"{added_trainable_emb_info}", "current") + + def replace_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor: + """Replace external input ids to 0. + + Args: + input_ids (torch.Tensor): The input ids to be replaced. + + Returns: + torch.Tensor: The replaced input ids. + """ + input_ids_fwd = input_ids.clone() + input_ids_fwd[input_ids_fwd >= self.num_embeddings] = 0 + return input_ids_fwd + + def replace_embeddings( + self, input_ids: torch.Tensor, embedding: torch.Tensor, external_embedding: dict + ) -> torch.Tensor: + """Replace external embedding to the embedding layer. Noted that, in + this function we use `torch.cat` to avoid inplace modification. + + Args: + input_ids (torch.Tensor): The original token ids. Shape like + [LENGTH, ]. + embedding (torch.Tensor): The embedding of token ids after + `replace_input_ids` function. + external_embedding (dict): The external embedding to be replaced. + + Returns: + torch.Tensor: The replaced embedding. + """ + new_embedding = [] + + name = external_embedding["name"] + start = external_embedding["start"] + end = external_embedding["end"] + target_ids_to_replace = [i for i in range(start, end)] + ext_emb = external_embedding["embedding"].to(embedding.device) + + # do not need to replace + if not (input_ids == start).any(): + return embedding + + # start replace + s_idx, e_idx = 0, 0 + while e_idx < len(input_ids): + if input_ids[e_idx] == start: + if e_idx != 0: + # add embedding do not need to replace + new_embedding.append(embedding[s_idx:e_idx]) + + # check if the next embedding need to replace is valid + actually_ids_to_replace = [int(i) for i in input_ids[e_idx: e_idx + end - start]] + assert actually_ids_to_replace == target_ids_to_replace, ( + f"Invalid 'input_ids' in position: {s_idx} to {e_idx}. " + f"Expect '{target_ids_to_replace}' for embedding " + f"'{name}' but found '{actually_ids_to_replace}'." + ) + + new_embedding.append(ext_emb) + + s_idx = e_idx + end - start + e_idx = s_idx + 1 + else: + e_idx += 1 + + if e_idx == len(input_ids): + new_embedding.append(embedding[s_idx:e_idx]) + + return torch.cat(new_embedding, dim=0) + + def forward(self, input_ids: torch.Tensor, external_embeddings: Optional[List[dict]] = None, out_dtype = None): + """The forward function. + + Args: + input_ids (torch.Tensor): The token ids shape like [bz, LENGTH] or + [LENGTH, ]. + external_embeddings (Optional[List[dict]]): The external + embeddings. If not passed, only `self.external_embeddings` + will be used. Defaults to None. + + input_ids: shape like [bz, LENGTH] or [LENGTH]. + """ + assert input_ids.ndim in [1, 2] + if input_ids.ndim == 1: + input_ids = input_ids.unsqueeze(0) + + if external_embeddings is None and not self.external_embeddings: + return self.wrapped(input_ids, out_dtype=out_dtype) + + input_ids_fwd = self.replace_input_ids(input_ids) + inputs_embeds = self.wrapped(input_ids_fwd) + + vecs = [] + + if external_embeddings is None: + external_embeddings = [] + elif isinstance(external_embeddings, dict): + external_embeddings = [external_embeddings] + embeddings = self.external_embeddings + external_embeddings + + for input_id, embedding in zip(input_ids, inputs_embeds): + new_embedding = embedding + for external_embedding in embeddings: + new_embedding = self.replace_embeddings(input_id, new_embedding, external_embedding) + vecs.append(new_embedding) + + return torch.stack(vecs).to(out_dtype) + + +def add_tokens( + tokenizer, text_encoder, placeholder_tokens: list, initialize_tokens: list = None, + num_vectors_per_token: int = 1 +): + """Add token for training. + + # TODO: support add tokens as dict, then we can load pretrained tokens. + """ + if initialize_tokens is not None: + assert len(initialize_tokens) == len( + placeholder_tokens + ), "placeholder_token should be the same length as initialize_token" + for ii in range(len(placeholder_tokens)): + tokenizer.add_placeholder_token(placeholder_tokens[ii], num_vec_per_token=num_vectors_per_token) + + # text_encoder.set_embedding_layer() + embedding_layer = text_encoder.text_model.embeddings.token_embedding + text_encoder.text_model.embeddings.token_embedding = EmbeddingLayerWithFixes(embedding_layer) + embedding_layer = text_encoder.text_model.embeddings.token_embedding + + assert embedding_layer is not None, ( + "Do not support get embedding layer for current text encoder. " "Please check your configuration." + ) + initialize_embedding = [] + if initialize_tokens is not None: + for ii in range(len(placeholder_tokens)): + init_id = tokenizer(initialize_tokens[ii]).input_ids[1] + temp_embedding = embedding_layer.weight[init_id] + initialize_embedding.append(temp_embedding[None, ...].repeat(num_vectors_per_token, 1)) + else: + for ii in range(len(placeholder_tokens)): + init_id = tokenizer("a").input_ids[1] + temp_embedding = embedding_layer.weight[init_id] + len_emb = temp_embedding.shape[0] + init_weight = (torch.rand(num_vectors_per_token, len_emb) - 0.5) / 2.0 + initialize_embedding.append(init_weight) + + # initialize_embedding = torch.cat(initialize_embedding,dim=0) + + token_info_all = [] + for ii in range(len(placeholder_tokens)): + token_info = tokenizer.get_token_info(placeholder_tokens[ii]) + token_info["embedding"] = initialize_embedding[ii] + token_info["trainable"] = True + token_info_all.append(token_info) + embedding_layer.add_embeddings(token_info_all) diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/brushnet/unet_2d_blocks.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/brushnet/unet_2d_blocks.py new file mode 100644 index 0000000000000000000000000000000000000000..e332bb9c2b1e4ae3c0fab577cab14c111a806c19 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/brushnet/unet_2d_blocks.py @@ -0,0 +1,3908 @@ +# Copyright 2024 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import Any, Dict, Optional, Tuple, Union + +import numpy as np +import torch +import torch.nn.functional as F +from torch import nn + +from diffusers.utils import deprecate, is_torch_version, logging +from diffusers.utils.torch_utils import apply_freeu +from diffusers.models.activations import get_activation +from diffusers.models.attention_processor import Attention, AttnAddedKVProcessor, AttnAddedKVProcessor2_0 +from diffusers.models.normalization import AdaGroupNorm +from diffusers.models.resnet import ( + Downsample2D, + FirDownsample2D, + FirUpsample2D, + KDownsample2D, + KUpsample2D, + ResnetBlock2D, + ResnetBlockCondNorm2D, + Upsample2D, +) +from diffusers.models.transformers.dual_transformer_2d import DualTransformer2DModel +from diffusers.models.transformers.transformer_2d import Transformer2DModel + +logger = logging.get_logger(__name__) # pylint: disable=invalid-name + + +def get_down_block( + down_block_type: str, + num_layers: int, + in_channels: int, + out_channels: int, + temb_channels: int, + add_downsample: bool, + resnet_eps: float, + resnet_act_fn: str, + transformer_layers_per_block: int = 1, + num_attention_heads: Optional[int] = None, + resnet_groups: Optional[int] = None, + cross_attention_dim: Optional[int] = None, + downsample_padding: Optional[int] = None, + dual_cross_attention: bool = False, + use_linear_projection: bool = False, + only_cross_attention: bool = False, + upcast_attention: bool = False, + resnet_time_scale_shift: str = "default", + attention_type: str = "default", + resnet_skip_time_act: bool = False, + resnet_out_scale_factor: float = 1.0, + cross_attention_norm: Optional[str] = None, + attention_head_dim: Optional[int] = None, + downsample_type: Optional[str] = None, + dropout: float = 0.0, +): + # If attn head dim is not defined, we default it to the number of heads + if attention_head_dim is None: + logger.warning( + f"It is recommended to provide `attention_head_dim` when calling `get_down_block`. Defaulting `attention_head_dim` to {num_attention_heads}." + ) + attention_head_dim = num_attention_heads + + down_block_type = down_block_type[7:] if down_block_type.startswith("UNetRes") else down_block_type + if down_block_type == "DownBlock2D": + return DownBlock2D( + num_layers=num_layers, + in_channels=in_channels, + out_channels=out_channels, + temb_channels=temb_channels, + dropout=dropout, + add_downsample=add_downsample, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + resnet_groups=resnet_groups, + downsample_padding=downsample_padding, + resnet_time_scale_shift=resnet_time_scale_shift, + ) + elif down_block_type == "ResnetDownsampleBlock2D": + return ResnetDownsampleBlock2D( + num_layers=num_layers, + in_channels=in_channels, + out_channels=out_channels, + temb_channels=temb_channels, + dropout=dropout, + add_downsample=add_downsample, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + resnet_groups=resnet_groups, + resnet_time_scale_shift=resnet_time_scale_shift, + skip_time_act=resnet_skip_time_act, + output_scale_factor=resnet_out_scale_factor, + ) + elif down_block_type == "AttnDownBlock2D": + if add_downsample is False: + downsample_type = None + else: + downsample_type = downsample_type or "conv" # default to 'conv' + return AttnDownBlock2D( + num_layers=num_layers, + in_channels=in_channels, + out_channels=out_channels, + temb_channels=temb_channels, + dropout=dropout, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + resnet_groups=resnet_groups, + downsample_padding=downsample_padding, + attention_head_dim=attention_head_dim, + resnet_time_scale_shift=resnet_time_scale_shift, + downsample_type=downsample_type, + ) + elif down_block_type == "CrossAttnDownBlock2D": + if cross_attention_dim is None: + raise ValueError("cross_attention_dim must be specified for CrossAttnDownBlock2D") + return CrossAttnDownBlock2D( + num_layers=num_layers, + transformer_layers_per_block=transformer_layers_per_block, + in_channels=in_channels, + out_channels=out_channels, + temb_channels=temb_channels, + dropout=dropout, + add_downsample=add_downsample, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + resnet_groups=resnet_groups, + downsample_padding=downsample_padding, + cross_attention_dim=cross_attention_dim, + num_attention_heads=num_attention_heads, + dual_cross_attention=dual_cross_attention, + use_linear_projection=use_linear_projection, + only_cross_attention=only_cross_attention, + upcast_attention=upcast_attention, + resnet_time_scale_shift=resnet_time_scale_shift, + attention_type=attention_type, + ) + elif down_block_type == "SimpleCrossAttnDownBlock2D": + if cross_attention_dim is None: + raise ValueError("cross_attention_dim must be specified for SimpleCrossAttnDownBlock2D") + return SimpleCrossAttnDownBlock2D( + num_layers=num_layers, + in_channels=in_channels, + out_channels=out_channels, + temb_channels=temb_channels, + dropout=dropout, + add_downsample=add_downsample, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + resnet_groups=resnet_groups, + cross_attention_dim=cross_attention_dim, + attention_head_dim=attention_head_dim, + resnet_time_scale_shift=resnet_time_scale_shift, + skip_time_act=resnet_skip_time_act, + output_scale_factor=resnet_out_scale_factor, + only_cross_attention=only_cross_attention, + cross_attention_norm=cross_attention_norm, + ) + elif down_block_type == "SkipDownBlock2D": + return SkipDownBlock2D( + num_layers=num_layers, + in_channels=in_channels, + out_channels=out_channels, + temb_channels=temb_channels, + dropout=dropout, + add_downsample=add_downsample, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + downsample_padding=downsample_padding, + resnet_time_scale_shift=resnet_time_scale_shift, + ) + elif down_block_type == "AttnSkipDownBlock2D": + return AttnSkipDownBlock2D( + num_layers=num_layers, + in_channels=in_channels, + out_channels=out_channels, + temb_channels=temb_channels, + dropout=dropout, + add_downsample=add_downsample, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + attention_head_dim=attention_head_dim, + resnet_time_scale_shift=resnet_time_scale_shift, + ) + elif down_block_type == "DownEncoderBlock2D": + return DownEncoderBlock2D( + num_layers=num_layers, + in_channels=in_channels, + out_channels=out_channels, + dropout=dropout, + add_downsample=add_downsample, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + resnet_groups=resnet_groups, + downsample_padding=downsample_padding, + resnet_time_scale_shift=resnet_time_scale_shift, + ) + elif down_block_type == "AttnDownEncoderBlock2D": + return AttnDownEncoderBlock2D( + num_layers=num_layers, + in_channels=in_channels, + out_channels=out_channels, + dropout=dropout, + add_downsample=add_downsample, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + resnet_groups=resnet_groups, + downsample_padding=downsample_padding, + attention_head_dim=attention_head_dim, + resnet_time_scale_shift=resnet_time_scale_shift, + ) + elif down_block_type == "KDownBlock2D": + return KDownBlock2D( + num_layers=num_layers, + in_channels=in_channels, + out_channels=out_channels, + temb_channels=temb_channels, + dropout=dropout, + add_downsample=add_downsample, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + ) + elif down_block_type == "KCrossAttnDownBlock2D": + return KCrossAttnDownBlock2D( + num_layers=num_layers, + in_channels=in_channels, + out_channels=out_channels, + temb_channels=temb_channels, + dropout=dropout, + add_downsample=add_downsample, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + cross_attention_dim=cross_attention_dim, + attention_head_dim=attention_head_dim, + add_self_attention=True if not add_downsample else False, + ) + raise ValueError(f"{down_block_type} does not exist.") + + +def get_mid_block( + mid_block_type: str, + temb_channels: int, + in_channels: int, + resnet_eps: float, + resnet_act_fn: str, + resnet_groups: int, + output_scale_factor: float = 1.0, + transformer_layers_per_block: int = 1, + num_attention_heads: Optional[int] = None, + cross_attention_dim: Optional[int] = None, + dual_cross_attention: bool = False, + use_linear_projection: bool = False, + mid_block_only_cross_attention: bool = False, + upcast_attention: bool = False, + resnet_time_scale_shift: str = "default", + attention_type: str = "default", + resnet_skip_time_act: bool = False, + cross_attention_norm: Optional[str] = None, + attention_head_dim: Optional[int] = 1, + dropout: float = 0.0, +): + if mid_block_type == "UNetMidBlock2DCrossAttn": + return UNetMidBlock2DCrossAttn( + transformer_layers_per_block=transformer_layers_per_block, + in_channels=in_channels, + temb_channels=temb_channels, + dropout=dropout, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + output_scale_factor=output_scale_factor, + resnet_time_scale_shift=resnet_time_scale_shift, + cross_attention_dim=cross_attention_dim, + num_attention_heads=num_attention_heads, + resnet_groups=resnet_groups, + dual_cross_attention=dual_cross_attention, + use_linear_projection=use_linear_projection, + upcast_attention=upcast_attention, + attention_type=attention_type, + ) + elif mid_block_type == "UNetMidBlock2DSimpleCrossAttn": + return UNetMidBlock2DSimpleCrossAttn( + in_channels=in_channels, + temb_channels=temb_channels, + dropout=dropout, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + output_scale_factor=output_scale_factor, + cross_attention_dim=cross_attention_dim, + attention_head_dim=attention_head_dim, + resnet_groups=resnet_groups, + resnet_time_scale_shift=resnet_time_scale_shift, + skip_time_act=resnet_skip_time_act, + only_cross_attention=mid_block_only_cross_attention, + cross_attention_norm=cross_attention_norm, + ) + elif mid_block_type == "UNetMidBlock2D": + return UNetMidBlock2D( + in_channels=in_channels, + temb_channels=temb_channels, + dropout=dropout, + num_layers=0, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + output_scale_factor=output_scale_factor, + resnet_groups=resnet_groups, + resnet_time_scale_shift=resnet_time_scale_shift, + add_attention=False, + ) + elif mid_block_type == "MidBlock2D": + return MidBlock2D( + in_channels=in_channels, + temb_channels=temb_channels, + dropout=dropout, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + output_scale_factor=output_scale_factor, + resnet_time_scale_shift=resnet_time_scale_shift, + resnet_groups=resnet_groups, + use_linear_projection=use_linear_projection, + ) + elif mid_block_type is None: + return None + else: + raise ValueError(f"unknown mid_block_type : {mid_block_type}") + + +def get_up_block( + up_block_type: str, + num_layers: int, + in_channels: int, + out_channels: int, + prev_output_channel: int, + temb_channels: int, + add_upsample: bool, + resnet_eps: float, + resnet_act_fn: str, + resolution_idx: Optional[int] = None, + transformer_layers_per_block: int = 1, + num_attention_heads: Optional[int] = None, + resnet_groups: Optional[int] = None, + cross_attention_dim: Optional[int] = None, + dual_cross_attention: bool = False, + use_linear_projection: bool = False, + only_cross_attention: bool = False, + upcast_attention: bool = False, + resnet_time_scale_shift: str = "default", + attention_type: str = "default", + resnet_skip_time_act: bool = False, + resnet_out_scale_factor: float = 1.0, + cross_attention_norm: Optional[str] = None, + attention_head_dim: Optional[int] = None, + upsample_type: Optional[str] = None, + dropout: float = 0.0, +) -> nn.Module: + # If attn head dim is not defined, we default it to the number of heads + if attention_head_dim is None: + logger.warning( + f"It is recommended to provide `attention_head_dim` when calling `get_up_block`. Defaulting `attention_head_dim` to {num_attention_heads}." + ) + attention_head_dim = num_attention_heads + + up_block_type = up_block_type[7:] if up_block_type.startswith("UNetRes") else up_block_type + if up_block_type == "UpBlock2D": + return UpBlock2D( + num_layers=num_layers, + in_channels=in_channels, + out_channels=out_channels, + prev_output_channel=prev_output_channel, + temb_channels=temb_channels, + resolution_idx=resolution_idx, + dropout=dropout, + add_upsample=add_upsample, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + resnet_groups=resnet_groups, + resnet_time_scale_shift=resnet_time_scale_shift, + ) + elif up_block_type == "ResnetUpsampleBlock2D": + return ResnetUpsampleBlock2D( + num_layers=num_layers, + in_channels=in_channels, + out_channels=out_channels, + prev_output_channel=prev_output_channel, + temb_channels=temb_channels, + resolution_idx=resolution_idx, + dropout=dropout, + add_upsample=add_upsample, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + resnet_groups=resnet_groups, + resnet_time_scale_shift=resnet_time_scale_shift, + skip_time_act=resnet_skip_time_act, + output_scale_factor=resnet_out_scale_factor, + ) + elif up_block_type == "CrossAttnUpBlock2D": + if cross_attention_dim is None: + raise ValueError("cross_attention_dim must be specified for CrossAttnUpBlock2D") + return CrossAttnUpBlock2D( + num_layers=num_layers, + transformer_layers_per_block=transformer_layers_per_block, + in_channels=in_channels, + out_channels=out_channels, + prev_output_channel=prev_output_channel, + temb_channels=temb_channels, + resolution_idx=resolution_idx, + dropout=dropout, + add_upsample=add_upsample, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + resnet_groups=resnet_groups, + cross_attention_dim=cross_attention_dim, + num_attention_heads=num_attention_heads, + dual_cross_attention=dual_cross_attention, + use_linear_projection=use_linear_projection, + only_cross_attention=only_cross_attention, + upcast_attention=upcast_attention, + resnet_time_scale_shift=resnet_time_scale_shift, + attention_type=attention_type, + ) + elif up_block_type == "SimpleCrossAttnUpBlock2D": + if cross_attention_dim is None: + raise ValueError("cross_attention_dim must be specified for SimpleCrossAttnUpBlock2D") + return SimpleCrossAttnUpBlock2D( + num_layers=num_layers, + in_channels=in_channels, + out_channels=out_channels, + prev_output_channel=prev_output_channel, + temb_channels=temb_channels, + resolution_idx=resolution_idx, + dropout=dropout, + add_upsample=add_upsample, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + resnet_groups=resnet_groups, + cross_attention_dim=cross_attention_dim, + attention_head_dim=attention_head_dim, + resnet_time_scale_shift=resnet_time_scale_shift, + skip_time_act=resnet_skip_time_act, + output_scale_factor=resnet_out_scale_factor, + only_cross_attention=only_cross_attention, + cross_attention_norm=cross_attention_norm, + ) + elif up_block_type == "AttnUpBlock2D": + if add_upsample is False: + upsample_type = None + else: + upsample_type = upsample_type or "conv" # default to 'conv' + + return AttnUpBlock2D( + num_layers=num_layers, + in_channels=in_channels, + out_channels=out_channels, + prev_output_channel=prev_output_channel, + temb_channels=temb_channels, + resolution_idx=resolution_idx, + dropout=dropout, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + resnet_groups=resnet_groups, + attention_head_dim=attention_head_dim, + resnet_time_scale_shift=resnet_time_scale_shift, + upsample_type=upsample_type, + ) + elif up_block_type == "SkipUpBlock2D": + return SkipUpBlock2D( + num_layers=num_layers, + in_channels=in_channels, + out_channels=out_channels, + prev_output_channel=prev_output_channel, + temb_channels=temb_channels, + resolution_idx=resolution_idx, + dropout=dropout, + add_upsample=add_upsample, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + resnet_time_scale_shift=resnet_time_scale_shift, + ) + elif up_block_type == "AttnSkipUpBlock2D": + return AttnSkipUpBlock2D( + num_layers=num_layers, + in_channels=in_channels, + out_channels=out_channels, + prev_output_channel=prev_output_channel, + temb_channels=temb_channels, + resolution_idx=resolution_idx, + dropout=dropout, + add_upsample=add_upsample, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + attention_head_dim=attention_head_dim, + resnet_time_scale_shift=resnet_time_scale_shift, + ) + elif up_block_type == "UpDecoderBlock2D": + return UpDecoderBlock2D( + num_layers=num_layers, + in_channels=in_channels, + out_channels=out_channels, + resolution_idx=resolution_idx, + dropout=dropout, + add_upsample=add_upsample, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + resnet_groups=resnet_groups, + resnet_time_scale_shift=resnet_time_scale_shift, + temb_channels=temb_channels, + ) + elif up_block_type == "AttnUpDecoderBlock2D": + return AttnUpDecoderBlock2D( + num_layers=num_layers, + in_channels=in_channels, + out_channels=out_channels, + resolution_idx=resolution_idx, + dropout=dropout, + add_upsample=add_upsample, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + resnet_groups=resnet_groups, + attention_head_dim=attention_head_dim, + resnet_time_scale_shift=resnet_time_scale_shift, + temb_channels=temb_channels, + ) + elif up_block_type == "KUpBlock2D": + return KUpBlock2D( + num_layers=num_layers, + in_channels=in_channels, + out_channels=out_channels, + temb_channels=temb_channels, + resolution_idx=resolution_idx, + dropout=dropout, + add_upsample=add_upsample, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + ) + elif up_block_type == "KCrossAttnUpBlock2D": + return KCrossAttnUpBlock2D( + num_layers=num_layers, + in_channels=in_channels, + out_channels=out_channels, + temb_channels=temb_channels, + resolution_idx=resolution_idx, + dropout=dropout, + add_upsample=add_upsample, + resnet_eps=resnet_eps, + resnet_act_fn=resnet_act_fn, + cross_attention_dim=cross_attention_dim, + attention_head_dim=attention_head_dim, + ) + + raise ValueError(f"{up_block_type} does not exist.") + + +class AutoencoderTinyBlock(nn.Module): + """ + Tiny Autoencoder block used in [`AutoencoderTiny`]. It is a mini residual module consisting of plain conv + ReLU + blocks. + + Args: + in_channels (`int`): The number of input channels. + out_channels (`int`): The number of output channels. + act_fn (`str`): + ` The activation function to use. Supported values are `"swish"`, `"mish"`, `"gelu"`, and `"relu"`. + + Returns: + `torch.FloatTensor`: A tensor with the same shape as the input tensor, but with the number of channels equal to + `out_channels`. + """ + + def __init__(self, in_channels: int, out_channels: int, act_fn: str): + super().__init__() + act_fn = get_activation(act_fn) + self.conv = nn.Sequential( + nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1), + act_fn, + nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1), + act_fn, + nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1), + ) + self.skip = ( + nn.Conv2d(in_channels, out_channels, kernel_size=1, bias=False) + if in_channels != out_channels + else nn.Identity() + ) + self.fuse = nn.ReLU() + + def forward(self, x: torch.FloatTensor) -> torch.FloatTensor: + return self.fuse(self.conv(x) + self.skip(x)) + + +class UNetMidBlock2D(nn.Module): + """ + A 2D UNet mid-block [`UNetMidBlock2D`] with multiple residual blocks and optional attention blocks. + + Args: + in_channels (`int`): The number of input channels. + temb_channels (`int`): The number of temporal embedding channels. + dropout (`float`, *optional*, defaults to 0.0): The dropout rate. + num_layers (`int`, *optional*, defaults to 1): The number of residual blocks. + resnet_eps (`float`, *optional*, 1e-6 ): The epsilon value for the resnet blocks. + resnet_time_scale_shift (`str`, *optional*, defaults to `default`): + The type of normalization to apply to the time embeddings. This can help to improve the performance of the + model on tasks with long-range temporal dependencies. + resnet_act_fn (`str`, *optional*, defaults to `swish`): The activation function for the resnet blocks. + resnet_groups (`int`, *optional*, defaults to 32): + The number of groups to use in the group normalization layers of the resnet blocks. + attn_groups (`Optional[int]`, *optional*, defaults to None): The number of groups for the attention blocks. + resnet_pre_norm (`bool`, *optional*, defaults to `True`): + Whether to use pre-normalization for the resnet blocks. + add_attention (`bool`, *optional*, defaults to `True`): Whether to add attention blocks. + attention_head_dim (`int`, *optional*, defaults to 1): + Dimension of a single attention head. The number of attention heads is determined based on this value and + the number of input channels. + output_scale_factor (`float`, *optional*, defaults to 1.0): The output scale factor. + + Returns: + `torch.FloatTensor`: The output of the last residual block, which is a tensor of shape `(batch_size, + in_channels, height, width)`. + + """ + + def __init__( + self, + in_channels: int, + temb_channels: int, + dropout: float = 0.0, + num_layers: int = 1, + resnet_eps: float = 1e-6, + resnet_time_scale_shift: str = "default", # default, spatial + resnet_act_fn: str = "swish", + resnet_groups: int = 32, + attn_groups: Optional[int] = None, + resnet_pre_norm: bool = True, + add_attention: bool = True, + attention_head_dim: int = 1, + output_scale_factor: float = 1.0, + ): + super().__init__() + resnet_groups = resnet_groups if resnet_groups is not None else min(in_channels // 4, 32) + self.add_attention = add_attention + + if attn_groups is None: + attn_groups = resnet_groups if resnet_time_scale_shift == "default" else None + + # there is always at least one resnet + if resnet_time_scale_shift == "spatial": + resnets = [ + ResnetBlockCondNorm2D( + in_channels=in_channels, + out_channels=in_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm="spatial", + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + ) + ] + else: + resnets = [ + ResnetBlock2D( + in_channels=in_channels, + out_channels=in_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + ) + ] + attentions = [] + + if attention_head_dim is None: + logger.warning( + f"It is not recommend to pass `attention_head_dim=None`. Defaulting `attention_head_dim` to `in_channels`: {in_channels}." + ) + attention_head_dim = in_channels + + for _ in range(num_layers): + if self.add_attention: + attentions.append( + Attention( + in_channels, + heads=in_channels // attention_head_dim, + dim_head=attention_head_dim, + rescale_output_factor=output_scale_factor, + eps=resnet_eps, + norm_num_groups=attn_groups, + spatial_norm_dim=temb_channels if resnet_time_scale_shift == "spatial" else None, + residual_connection=True, + bias=True, + upcast_softmax=True, + _from_deprecated_attn_block=True, + ) + ) + else: + attentions.append(None) + + if resnet_time_scale_shift == "spatial": + resnets.append( + ResnetBlockCondNorm2D( + in_channels=in_channels, + out_channels=in_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm="spatial", + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + ) + ) + else: + resnets.append( + ResnetBlock2D( + in_channels=in_channels, + out_channels=in_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + ) + ) + + self.attentions = nn.ModuleList(attentions) + self.resnets = nn.ModuleList(resnets) + + def forward(self, hidden_states: torch.FloatTensor, temb: Optional[torch.FloatTensor] = None) -> torch.FloatTensor: + hidden_states = self.resnets[0](hidden_states, temb) + for attn, resnet in zip(self.attentions, self.resnets[1:]): + if attn is not None: + hidden_states = attn(hidden_states, temb=temb) + hidden_states = resnet(hidden_states, temb) + + return hidden_states + + +class UNetMidBlock2DCrossAttn(nn.Module): + def __init__( + self, + in_channels: int, + temb_channels: int, + dropout: float = 0.0, + num_layers: int = 1, + transformer_layers_per_block: Union[int, Tuple[int]] = 1, + resnet_eps: float = 1e-6, + resnet_time_scale_shift: str = "default", + resnet_act_fn: str = "swish", + resnet_groups: int = 32, + resnet_pre_norm: bool = True, + num_attention_heads: int = 1, + output_scale_factor: float = 1.0, + cross_attention_dim: int = 1280, + dual_cross_attention: bool = False, + use_linear_projection: bool = False, + upcast_attention: bool = False, + attention_type: str = "default", + ): + super().__init__() + + self.has_cross_attention = True + self.num_attention_heads = num_attention_heads + resnet_groups = resnet_groups if resnet_groups is not None else min(in_channels // 4, 32) + + # support for variable transformer layers per block + if isinstance(transformer_layers_per_block, int): + transformer_layers_per_block = [transformer_layers_per_block] * num_layers + + # there is always at least one resnet + resnets = [ + ResnetBlock2D( + in_channels=in_channels, + out_channels=in_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + ) + ] + attentions = [] + + for i in range(num_layers): + if not dual_cross_attention: + attentions.append( + Transformer2DModel( + num_attention_heads, + in_channels // num_attention_heads, + in_channels=in_channels, + num_layers=transformer_layers_per_block[i], + cross_attention_dim=cross_attention_dim, + norm_num_groups=resnet_groups, + use_linear_projection=use_linear_projection, + upcast_attention=upcast_attention, + attention_type=attention_type, + ) + ) + else: + attentions.append( + DualTransformer2DModel( + num_attention_heads, + in_channels // num_attention_heads, + in_channels=in_channels, + num_layers=1, + cross_attention_dim=cross_attention_dim, + norm_num_groups=resnet_groups, + ) + ) + resnets.append( + ResnetBlock2D( + in_channels=in_channels, + out_channels=in_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + ) + ) + + self.attentions = nn.ModuleList(attentions) + self.resnets = nn.ModuleList(resnets) + + self.gradient_checkpointing = False + + def forward( + self, + hidden_states: torch.FloatTensor, + temb: Optional[torch.FloatTensor] = None, + encoder_hidden_states: Optional[torch.FloatTensor] = None, + attention_mask: Optional[torch.FloatTensor] = None, + cross_attention_kwargs: Optional[Dict[str, Any]] = None, + encoder_attention_mask: Optional[torch.FloatTensor] = None, + ) -> torch.FloatTensor: + if cross_attention_kwargs is not None: + if cross_attention_kwargs.get("scale", None) is not None: + logger.warning("Passing `scale` to `cross_attention_kwargs` is deprecated. `scale` will be ignored.") + + hidden_states = self.resnets[0](hidden_states, temb) + for attn, resnet in zip(self.attentions, self.resnets[1:]): + if self.training and self.gradient_checkpointing: + + def create_custom_forward(module, return_dict=None): + def custom_forward(*inputs): + if return_dict is not None: + return module(*inputs, return_dict=return_dict) + else: + return module(*inputs) + + return custom_forward + + ckpt_kwargs: Dict[str, Any] = {"use_reentrant": False} if is_torch_version(">=", "1.11.0") else {} + hidden_states = attn( + hidden_states, + encoder_hidden_states=encoder_hidden_states, + cross_attention_kwargs=cross_attention_kwargs, + attention_mask=attention_mask, + encoder_attention_mask=encoder_attention_mask, + return_dict=False, + )[0] + hidden_states = torch.utils.checkpoint.checkpoint( + create_custom_forward(resnet), + hidden_states, + temb, + **ckpt_kwargs, + ) + else: + hidden_states = attn( + hidden_states, + encoder_hidden_states=encoder_hidden_states, + cross_attention_kwargs=cross_attention_kwargs, + attention_mask=attention_mask, + encoder_attention_mask=encoder_attention_mask, + return_dict=False, + )[0] + hidden_states = resnet(hidden_states, temb) + + return hidden_states + + +class UNetMidBlock2DSimpleCrossAttn(nn.Module): + def __init__( + self, + in_channels: int, + temb_channels: int, + dropout: float = 0.0, + num_layers: int = 1, + resnet_eps: float = 1e-6, + resnet_time_scale_shift: str = "default", + resnet_act_fn: str = "swish", + resnet_groups: int = 32, + resnet_pre_norm: bool = True, + attention_head_dim: int = 1, + output_scale_factor: float = 1.0, + cross_attention_dim: int = 1280, + skip_time_act: bool = False, + only_cross_attention: bool = False, + cross_attention_norm: Optional[str] = None, + ): + super().__init__() + + self.has_cross_attention = True + + self.attention_head_dim = attention_head_dim + resnet_groups = resnet_groups if resnet_groups is not None else min(in_channels // 4, 32) + + self.num_heads = in_channels // self.attention_head_dim + + # there is always at least one resnet + resnets = [ + ResnetBlock2D( + in_channels=in_channels, + out_channels=in_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + skip_time_act=skip_time_act, + ) + ] + attentions = [] + + for _ in range(num_layers): + processor = ( + AttnAddedKVProcessor2_0() if hasattr(F, "scaled_dot_product_attention") else AttnAddedKVProcessor() + ) + + attentions.append( + Attention( + query_dim=in_channels, + cross_attention_dim=in_channels, + heads=self.num_heads, + dim_head=self.attention_head_dim, + added_kv_proj_dim=cross_attention_dim, + norm_num_groups=resnet_groups, + bias=True, + upcast_softmax=True, + only_cross_attention=only_cross_attention, + cross_attention_norm=cross_attention_norm, + processor=processor, + ) + ) + resnets.append( + ResnetBlock2D( + in_channels=in_channels, + out_channels=in_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + skip_time_act=skip_time_act, + ) + ) + + self.attentions = nn.ModuleList(attentions) + self.resnets = nn.ModuleList(resnets) + + def forward( + self, + hidden_states: torch.FloatTensor, + temb: Optional[torch.FloatTensor] = None, + encoder_hidden_states: Optional[torch.FloatTensor] = None, + attention_mask: Optional[torch.FloatTensor] = None, + cross_attention_kwargs: Optional[Dict[str, Any]] = None, + encoder_attention_mask: Optional[torch.FloatTensor] = None, + ) -> torch.FloatTensor: + cross_attention_kwargs = cross_attention_kwargs if cross_attention_kwargs is not None else {} + if cross_attention_kwargs.get("scale", None) is not None: + logger.warning("Passing `scale` to `cross_attention_kwargs` is deprecated. `scale` will be ignored.") + + if attention_mask is None: + # if encoder_hidden_states is defined: we are doing cross-attn, so we should use cross-attn mask. + mask = None if encoder_hidden_states is None else encoder_attention_mask + else: + # when attention_mask is defined: we don't even check for encoder_attention_mask. + # this is to maintain compatibility with UnCLIP, which uses 'attention_mask' param for cross-attn masks. + # TODO: UnCLIP should express cross-attn mask via encoder_attention_mask param instead of via attention_mask. + # then we can simplify this whole if/else block to: + # mask = attention_mask if encoder_hidden_states is None else encoder_attention_mask + mask = attention_mask + + hidden_states = self.resnets[0](hidden_states, temb) + for attn, resnet in zip(self.attentions, self.resnets[1:]): + # attn + hidden_states = attn( + hidden_states, + encoder_hidden_states=encoder_hidden_states, + attention_mask=mask, + **cross_attention_kwargs, + ) + + # resnet + hidden_states = resnet(hidden_states, temb) + + return hidden_states + + +class MidBlock2D(nn.Module): + def __init__( + self, + in_channels: int, + temb_channels: int, + dropout: float = 0.0, + num_layers: int = 1, + resnet_eps: float = 1e-6, + resnet_time_scale_shift: str = "default", + resnet_act_fn: str = "swish", + resnet_groups: int = 32, + resnet_pre_norm: bool = True, + output_scale_factor: float = 1.0, + use_linear_projection: bool = False, + ): + super().__init__() + + self.has_cross_attention = False + resnet_groups = resnet_groups if resnet_groups is not None else min(in_channels // 4, 32) + + # there is always at least one resnet + resnets = [ + ResnetBlock2D( + in_channels=in_channels, + out_channels=in_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + ) + ] + + for i in range(num_layers): + resnets.append( + ResnetBlock2D( + in_channels=in_channels, + out_channels=in_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + ) + ) + + self.resnets = nn.ModuleList(resnets) + + self.gradient_checkpointing = False + + def forward( + self, + hidden_states: torch.FloatTensor, + temb: Optional[torch.FloatTensor] = None, + ) -> torch.FloatTensor: + lora_scale = 1.0 + hidden_states = self.resnets[0](hidden_states, temb, scale=lora_scale) + for resnet in self.resnets[1:]: + if self.training and self.gradient_checkpointing: + + def create_custom_forward(module, return_dict=None): + def custom_forward(*inputs): + if return_dict is not None: + return module(*inputs, return_dict=return_dict) + else: + return module(*inputs) + + return custom_forward + + ckpt_kwargs: Dict[str, Any] = {"use_reentrant": False} if is_torch_version(">=", "1.11.0") else {} + hidden_states = torch.utils.checkpoint.checkpoint( + create_custom_forward(resnet), + hidden_states, + temb, + **ckpt_kwargs, + ) + else: + hidden_states = resnet(hidden_states, temb, scale=lora_scale) + + return hidden_states + + +class AttnDownBlock2D(nn.Module): + def __init__( + self, + in_channels: int, + out_channels: int, + temb_channels: int, + dropout: float = 0.0, + num_layers: int = 1, + resnet_eps: float = 1e-6, + resnet_time_scale_shift: str = "default", + resnet_act_fn: str = "swish", + resnet_groups: int = 32, + resnet_pre_norm: bool = True, + attention_head_dim: int = 1, + output_scale_factor: float = 1.0, + downsample_padding: int = 1, + downsample_type: str = "conv", + ): + super().__init__() + resnets = [] + attentions = [] + self.downsample_type = downsample_type + + if attention_head_dim is None: + logger.warning( + f"It is not recommend to pass `attention_head_dim=None`. Defaulting `attention_head_dim` to `in_channels`: {out_channels}." + ) + attention_head_dim = out_channels + + for i in range(num_layers): + in_channels = in_channels if i == 0 else out_channels + resnets.append( + ResnetBlock2D( + in_channels=in_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + ) + ) + attentions.append( + Attention( + out_channels, + heads=out_channels // attention_head_dim, + dim_head=attention_head_dim, + rescale_output_factor=output_scale_factor, + eps=resnet_eps, + norm_num_groups=resnet_groups, + residual_connection=True, + bias=True, + upcast_softmax=True, + _from_deprecated_attn_block=True, + ) + ) + + self.attentions = nn.ModuleList(attentions) + self.resnets = nn.ModuleList(resnets) + + if downsample_type == "conv": + self.downsamplers = nn.ModuleList( + [ + Downsample2D( + out_channels, use_conv=True, out_channels=out_channels, padding=downsample_padding, name="op" + ) + ] + ) + elif downsample_type == "resnet": + self.downsamplers = nn.ModuleList( + [ + ResnetBlock2D( + in_channels=out_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + down=True, + ) + ] + ) + else: + self.downsamplers = None + + def forward( + self, + hidden_states: torch.FloatTensor, + temb: Optional[torch.FloatTensor] = None, + upsample_size: Optional[int] = None, + cross_attention_kwargs: Optional[Dict[str, Any]] = None, + ) -> Tuple[torch.FloatTensor, Tuple[torch.FloatTensor, ...]]: + cross_attention_kwargs = cross_attention_kwargs if cross_attention_kwargs is not None else {} + if cross_attention_kwargs.get("scale", None) is not None: + logger.warning("Passing `scale` to `cross_attention_kwargs` is deprecated. `scale` will be ignored.") + + output_states = () + + for resnet, attn in zip(self.resnets, self.attentions): + hidden_states = resnet(hidden_states, temb) + hidden_states = attn(hidden_states, **cross_attention_kwargs) + output_states = output_states + (hidden_states,) + + if self.downsamplers is not None: + for downsampler in self.downsamplers: + if self.downsample_type == "resnet": + hidden_states = downsampler(hidden_states, temb=temb) + else: + hidden_states = downsampler(hidden_states) + + output_states += (hidden_states,) + + return hidden_states, output_states + + +class CrossAttnDownBlock2D(nn.Module): + def __init__( + self, + in_channels: int, + out_channels: int, + temb_channels: int, + dropout: float = 0.0, + num_layers: int = 1, + transformer_layers_per_block: Union[int, Tuple[int]] = 1, + resnet_eps: float = 1e-6, + resnet_time_scale_shift: str = "default", + resnet_act_fn: str = "swish", + resnet_groups: int = 32, + resnet_pre_norm: bool = True, + num_attention_heads: int = 1, + cross_attention_dim: int = 1280, + output_scale_factor: float = 1.0, + downsample_padding: int = 1, + add_downsample: bool = True, + dual_cross_attention: bool = False, + use_linear_projection: bool = False, + only_cross_attention: bool = False, + upcast_attention: bool = False, + attention_type: str = "default", + ): + super().__init__() + resnets = [] + attentions = [] + + self.has_cross_attention = True + self.num_attention_heads = num_attention_heads + if isinstance(transformer_layers_per_block, int): + transformer_layers_per_block = [transformer_layers_per_block] * num_layers + + for i in range(num_layers): + in_channels = in_channels if i == 0 else out_channels + resnets.append( + ResnetBlock2D( + in_channels=in_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + ) + ) + if not dual_cross_attention: + attentions.append( + Transformer2DModel( + num_attention_heads, + out_channels // num_attention_heads, + in_channels=out_channels, + num_layers=transformer_layers_per_block[i], + cross_attention_dim=cross_attention_dim, + norm_num_groups=resnet_groups, + use_linear_projection=use_linear_projection, + only_cross_attention=only_cross_attention, + upcast_attention=upcast_attention, + attention_type=attention_type, + ) + ) + else: + attentions.append( + DualTransformer2DModel( + num_attention_heads, + out_channels // num_attention_heads, + in_channels=out_channels, + num_layers=1, + cross_attention_dim=cross_attention_dim, + norm_num_groups=resnet_groups, + ) + ) + self.attentions = nn.ModuleList(attentions) + self.resnets = nn.ModuleList(resnets) + + if add_downsample: + self.downsamplers = nn.ModuleList( + [ + Downsample2D( + out_channels, use_conv=True, out_channels=out_channels, padding=downsample_padding, name="op" + ) + ] + ) + else: + self.downsamplers = None + + self.gradient_checkpointing = False + + def forward( + self, + hidden_states: torch.FloatTensor, + temb: Optional[torch.FloatTensor] = None, + encoder_hidden_states: Optional[torch.FloatTensor] = None, + attention_mask: Optional[torch.FloatTensor] = None, + cross_attention_kwargs: Optional[Dict[str, Any]] = None, + encoder_attention_mask: Optional[torch.FloatTensor] = None, + additional_residuals: Optional[torch.FloatTensor] = None, + down_block_add_samples: Optional[torch.FloatTensor] = None, + debug=False, + ) -> Tuple[torch.FloatTensor, Tuple[torch.FloatTensor, ...]]: + + if debug: print(' XAD2: forward') + + if cross_attention_kwargs is not None: + if cross_attention_kwargs.get("scale", None) is not None: + logger.warning("Passing `scale` to `cross_attention_kwargs` is deprecated. `scale` will be ignored.") + + output_states = () + + blocks = list(zip(self.resnets, self.attentions)) + + for i, (resnet, attn) in enumerate(blocks): + if self.training and self.gradient_checkpointing: + + def create_custom_forward(module, return_dict=None): + def custom_forward(*inputs): + if return_dict is not None: + return module(*inputs, return_dict=return_dict) + else: + return module(*inputs) + + return custom_forward + + ckpt_kwargs: Dict[str, Any] = {"use_reentrant": False} if is_torch_version(">=", "1.11.0") else {} + hidden_states = torch.utils.checkpoint.checkpoint( + create_custom_forward(resnet), + hidden_states, + temb, + **ckpt_kwargs, + ) + hidden_states = attn( + hidden_states, + encoder_hidden_states=encoder_hidden_states, + cross_attention_kwargs=cross_attention_kwargs, + attention_mask=attention_mask, + encoder_attention_mask=encoder_attention_mask, + return_dict=False, + )[0] + else: + if debug: print(' XAD2: resnet hs #', i, hidden_states.shape) + if debug and temb is not None: print(' XAD2: resnet temb #', i, temb.shape) + + hidden_states = resnet(hidden_states, temb) + + if debug: print(' XAD2: attn hs #', i, hidden_states.shape) + if debug and encoder_hidden_states is not None: print(' XAD2: attn ehs #', i, + encoder_hidden_states.shape) + + hidden_states = attn( + hidden_states, + encoder_hidden_states=encoder_hidden_states, + cross_attention_kwargs=cross_attention_kwargs, + attention_mask=attention_mask, + encoder_attention_mask=encoder_attention_mask, + return_dict=False, + )[0] + + # apply additional residuals to the output of the last pair of resnet and attention blocks + if i == len(blocks) - 1 and additional_residuals is not None: + + if debug: print(' XAD2: add res', additional_residuals.shape) + + hidden_states = hidden_states + additional_residuals + + if down_block_add_samples is not None: + + if debug: print(' XAD2: add samples', down_block_add_samples.shape) + + hidden_states = hidden_states + down_block_add_samples.pop(0) + + if debug: print(' XAD2: output', hidden_states.shape) + + output_states = output_states + (hidden_states,) + + if self.downsamplers is not None: + for downsampler in self.downsamplers: + hidden_states = downsampler(hidden_states) + + if down_block_add_samples is not None: + hidden_states = hidden_states + down_block_add_samples.pop(0) # todo: add before or after + + output_states = output_states + (hidden_states,) + + if debug: + print(' XAD2: finish') + for st in output_states: + print(' XAD2: ', st.shape) + + return hidden_states, output_states + + +class DownBlock2D(nn.Module): + def __init__( + self, + in_channels: int, + out_channels: int, + temb_channels: int, + dropout: float = 0.0, + num_layers: int = 1, + resnet_eps: float = 1e-6, + resnet_time_scale_shift: str = "default", + resnet_act_fn: str = "swish", + resnet_groups: int = 32, + resnet_pre_norm: bool = True, + output_scale_factor: float = 1.0, + add_downsample: bool = True, + downsample_padding: int = 1, + ): + super().__init__() + resnets = [] + + for i in range(num_layers): + in_channels = in_channels if i == 0 else out_channels + resnets.append( + ResnetBlock2D( + in_channels=in_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + ) + ) + + self.resnets = nn.ModuleList(resnets) + + if add_downsample: + self.downsamplers = nn.ModuleList( + [ + Downsample2D( + out_channels, use_conv=True, out_channels=out_channels, padding=downsample_padding, name="op" + ) + ] + ) + else: + self.downsamplers = None + + self.gradient_checkpointing = False + + def forward( + self, hidden_states: torch.FloatTensor, temb: Optional[torch.FloatTensor] = None, + down_block_add_samples: Optional[torch.FloatTensor] = None, *args, **kwargs + ) -> Tuple[torch.FloatTensor, Tuple[torch.FloatTensor, ...]]: + if len(args) > 0 or kwargs.get("scale", None) is not None: + deprecation_message = "The `scale` argument is deprecated and will be ignored. Please remove it, as passing it will raise an error in the future. `scale` should directly be passed while calling the underlying pipeline component i.e., via `cross_attention_kwargs`." + deprecate("scale", "1.0.0", deprecation_message) + + output_states = () + + if kwargs.get("debug", False): print(' D2: forward', hidden_states.shape) + + for resnet in self.resnets: + if self.training and self.gradient_checkpointing: + + def create_custom_forward(module): + def custom_forward(*inputs): + return module(*inputs) + + return custom_forward + + if is_torch_version(">=", "1.11.0"): + hidden_states = torch.utils.checkpoint.checkpoint( + create_custom_forward(resnet), hidden_states, temb, use_reentrant=False + ) + else: + hidden_states = torch.utils.checkpoint.checkpoint( + create_custom_forward(resnet), hidden_states, temb + ) + else: + + if kwargs.get("debug", False): print(' D2: resnet', hidden_states.shape) + + hidden_states = resnet(hidden_states, temb) + + if down_block_add_samples is not None: + hidden_states = hidden_states + down_block_add_samples.pop(0) + + output_states = output_states + (hidden_states,) + + if self.downsamplers is not None: + for downsampler in self.downsamplers: + hidden_states = downsampler(hidden_states) + + if down_block_add_samples is not None: + hidden_states = hidden_states + down_block_add_samples.pop(0) # todo: add before or after + + output_states = output_states + (hidden_states,) + + if kwargs.get("debug", False): print(' D2: finish', hidden_states.shape) + + return hidden_states, output_states + + +class DownEncoderBlock2D(nn.Module): + def __init__( + self, + in_channels: int, + out_channels: int, + dropout: float = 0.0, + num_layers: int = 1, + resnet_eps: float = 1e-6, + resnet_time_scale_shift: str = "default", + resnet_act_fn: str = "swish", + resnet_groups: int = 32, + resnet_pre_norm: bool = True, + output_scale_factor: float = 1.0, + add_downsample: bool = True, + downsample_padding: int = 1, + ): + super().__init__() + resnets = [] + + for i in range(num_layers): + in_channels = in_channels if i == 0 else out_channels + if resnet_time_scale_shift == "spatial": + resnets.append( + ResnetBlockCondNorm2D( + in_channels=in_channels, + out_channels=out_channels, + temb_channels=None, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm="spatial", + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + ) + ) + else: + resnets.append( + ResnetBlock2D( + in_channels=in_channels, + out_channels=out_channels, + temb_channels=None, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + ) + ) + + self.resnets = nn.ModuleList(resnets) + + if add_downsample: + self.downsamplers = nn.ModuleList( + [ + Downsample2D( + out_channels, use_conv=True, out_channels=out_channels, padding=downsample_padding, name="op" + ) + ] + ) + else: + self.downsamplers = None + + def forward(self, hidden_states: torch.FloatTensor, *args, **kwargs) -> torch.FloatTensor: + if len(args) > 0 or kwargs.get("scale", None) is not None: + deprecation_message = "The `scale` argument is deprecated and will be ignored. Please remove it, as passing it will raise an error in the future. `scale` should directly be passed while calling the underlying pipeline component i.e., via `cross_attention_kwargs`." + deprecate("scale", "1.0.0", deprecation_message) + + for resnet in self.resnets: + hidden_states = resnet(hidden_states, temb=None) + + if self.downsamplers is not None: + for downsampler in self.downsamplers: + hidden_states = downsampler(hidden_states) + + return hidden_states + + +class AttnDownEncoderBlock2D(nn.Module): + def __init__( + self, + in_channels: int, + out_channels: int, + dropout: float = 0.0, + num_layers: int = 1, + resnet_eps: float = 1e-6, + resnet_time_scale_shift: str = "default", + resnet_act_fn: str = "swish", + resnet_groups: int = 32, + resnet_pre_norm: bool = True, + attention_head_dim: int = 1, + output_scale_factor: float = 1.0, + add_downsample: bool = True, + downsample_padding: int = 1, + ): + super().__init__() + resnets = [] + attentions = [] + + if attention_head_dim is None: + logger.warning( + f"It is not recommend to pass `attention_head_dim=None`. Defaulting `attention_head_dim` to `in_channels`: {out_channels}." + ) + attention_head_dim = out_channels + + for i in range(num_layers): + in_channels = in_channels if i == 0 else out_channels + if resnet_time_scale_shift == "spatial": + resnets.append( + ResnetBlockCondNorm2D( + in_channels=in_channels, + out_channels=out_channels, + temb_channels=None, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm="spatial", + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + ) + ) + else: + resnets.append( + ResnetBlock2D( + in_channels=in_channels, + out_channels=out_channels, + temb_channels=None, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + ) + ) + attentions.append( + Attention( + out_channels, + heads=out_channels // attention_head_dim, + dim_head=attention_head_dim, + rescale_output_factor=output_scale_factor, + eps=resnet_eps, + norm_num_groups=resnet_groups, + residual_connection=True, + bias=True, + upcast_softmax=True, + _from_deprecated_attn_block=True, + ) + ) + + self.attentions = nn.ModuleList(attentions) + self.resnets = nn.ModuleList(resnets) + + if add_downsample: + self.downsamplers = nn.ModuleList( + [ + Downsample2D( + out_channels, use_conv=True, out_channels=out_channels, padding=downsample_padding, name="op" + ) + ] + ) + else: + self.downsamplers = None + + def forward(self, hidden_states: torch.FloatTensor, *args, **kwargs) -> torch.FloatTensor: + if len(args) > 0 or kwargs.get("scale", None) is not None: + deprecation_message = "The `scale` argument is deprecated and will be ignored. Please remove it, as passing it will raise an error in the future. `scale` should directly be passed while calling the underlying pipeline component i.e., via `cross_attention_kwargs`." + deprecate("scale", "1.0.0", deprecation_message) + + for resnet, attn in zip(self.resnets, self.attentions): + hidden_states = resnet(hidden_states, temb=None) + hidden_states = attn(hidden_states) + + if self.downsamplers is not None: + for downsampler in self.downsamplers: + hidden_states = downsampler(hidden_states) + + return hidden_states + + +class AttnSkipDownBlock2D(nn.Module): + def __init__( + self, + in_channels: int, + out_channels: int, + temb_channels: int, + dropout: float = 0.0, + num_layers: int = 1, + resnet_eps: float = 1e-6, + resnet_time_scale_shift: str = "default", + resnet_act_fn: str = "swish", + resnet_pre_norm: bool = True, + attention_head_dim: int = 1, + output_scale_factor: float = np.sqrt(2.0), + add_downsample: bool = True, + ): + super().__init__() + self.attentions = nn.ModuleList([]) + self.resnets = nn.ModuleList([]) + + if attention_head_dim is None: + logger.warning( + f"It is not recommend to pass `attention_head_dim=None`. Defaulting `attention_head_dim` to `in_channels`: {out_channels}." + ) + attention_head_dim = out_channels + + for i in range(num_layers): + in_channels = in_channels if i == 0 else out_channels + self.resnets.append( + ResnetBlock2D( + in_channels=in_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=min(in_channels // 4, 32), + groups_out=min(out_channels // 4, 32), + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + ) + ) + self.attentions.append( + Attention( + out_channels, + heads=out_channels // attention_head_dim, + dim_head=attention_head_dim, + rescale_output_factor=output_scale_factor, + eps=resnet_eps, + norm_num_groups=32, + residual_connection=True, + bias=True, + upcast_softmax=True, + _from_deprecated_attn_block=True, + ) + ) + + if add_downsample: + self.resnet_down = ResnetBlock2D( + in_channels=out_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=min(out_channels // 4, 32), + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + use_in_shortcut=True, + down=True, + kernel="fir", + ) + self.downsamplers = nn.ModuleList([FirDownsample2D(out_channels, out_channels=out_channels)]) + self.skip_conv = nn.Conv2d(3, out_channels, kernel_size=(1, 1), stride=(1, 1)) + else: + self.resnet_down = None + self.downsamplers = None + self.skip_conv = None + + def forward( + self, + hidden_states: torch.FloatTensor, + temb: Optional[torch.FloatTensor] = None, + skip_sample: Optional[torch.FloatTensor] = None, + *args, + **kwargs, + ) -> Tuple[torch.FloatTensor, Tuple[torch.FloatTensor, ...], torch.FloatTensor]: + if len(args) > 0 or kwargs.get("scale", None) is not None: + deprecation_message = "The `scale` argument is deprecated and will be ignored. Please remove it, as passing it will raise an error in the future. `scale` should directly be passed while calling the underlying pipeline component i.e., via `cross_attention_kwargs`." + deprecate("scale", "1.0.0", deprecation_message) + + output_states = () + + for resnet, attn in zip(self.resnets, self.attentions): + hidden_states = resnet(hidden_states, temb) + hidden_states = attn(hidden_states) + output_states += (hidden_states,) + + if self.downsamplers is not None: + hidden_states = self.resnet_down(hidden_states, temb) + for downsampler in self.downsamplers: + skip_sample = downsampler(skip_sample) + + hidden_states = self.skip_conv(skip_sample) + hidden_states + + output_states += (hidden_states,) + + return hidden_states, output_states, skip_sample + + +class SkipDownBlock2D(nn.Module): + def __init__( + self, + in_channels: int, + out_channels: int, + temb_channels: int, + dropout: float = 0.0, + num_layers: int = 1, + resnet_eps: float = 1e-6, + resnet_time_scale_shift: str = "default", + resnet_act_fn: str = "swish", + resnet_pre_norm: bool = True, + output_scale_factor: float = np.sqrt(2.0), + add_downsample: bool = True, + downsample_padding: int = 1, + ): + super().__init__() + self.resnets = nn.ModuleList([]) + + for i in range(num_layers): + in_channels = in_channels if i == 0 else out_channels + self.resnets.append( + ResnetBlock2D( + in_channels=in_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=min(in_channels // 4, 32), + groups_out=min(out_channels // 4, 32), + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + ) + ) + + if add_downsample: + self.resnet_down = ResnetBlock2D( + in_channels=out_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=min(out_channels // 4, 32), + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + use_in_shortcut=True, + down=True, + kernel="fir", + ) + self.downsamplers = nn.ModuleList([FirDownsample2D(out_channels, out_channels=out_channels)]) + self.skip_conv = nn.Conv2d(3, out_channels, kernel_size=(1, 1), stride=(1, 1)) + else: + self.resnet_down = None + self.downsamplers = None + self.skip_conv = None + + def forward( + self, + hidden_states: torch.FloatTensor, + temb: Optional[torch.FloatTensor] = None, + skip_sample: Optional[torch.FloatTensor] = None, + *args, + **kwargs, + ) -> Tuple[torch.FloatTensor, Tuple[torch.FloatTensor, ...], torch.FloatTensor]: + if len(args) > 0 or kwargs.get("scale", None) is not None: + deprecation_message = "The `scale` argument is deprecated and will be ignored. Please remove it, as passing it will raise an error in the future. `scale` should directly be passed while calling the underlying pipeline component i.e., via `cross_attention_kwargs`." + deprecate("scale", "1.0.0", deprecation_message) + + output_states = () + + for resnet in self.resnets: + hidden_states = resnet(hidden_states, temb) + output_states += (hidden_states,) + + if self.downsamplers is not None: + hidden_states = self.resnet_down(hidden_states, temb) + for downsampler in self.downsamplers: + skip_sample = downsampler(skip_sample) + + hidden_states = self.skip_conv(skip_sample) + hidden_states + + output_states += (hidden_states,) + + return hidden_states, output_states, skip_sample + + +class ResnetDownsampleBlock2D(nn.Module): + def __init__( + self, + in_channels: int, + out_channels: int, + temb_channels: int, + dropout: float = 0.0, + num_layers: int = 1, + resnet_eps: float = 1e-6, + resnet_time_scale_shift: str = "default", + resnet_act_fn: str = "swish", + resnet_groups: int = 32, + resnet_pre_norm: bool = True, + output_scale_factor: float = 1.0, + add_downsample: bool = True, + skip_time_act: bool = False, + ): + super().__init__() + resnets = [] + + for i in range(num_layers): + in_channels = in_channels if i == 0 else out_channels + resnets.append( + ResnetBlock2D( + in_channels=in_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + skip_time_act=skip_time_act, + ) + ) + + self.resnets = nn.ModuleList(resnets) + + if add_downsample: + self.downsamplers = nn.ModuleList( + [ + ResnetBlock2D( + in_channels=out_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + skip_time_act=skip_time_act, + down=True, + ) + ] + ) + else: + self.downsamplers = None + + self.gradient_checkpointing = False + + def forward( + self, hidden_states: torch.FloatTensor, temb: Optional[torch.FloatTensor] = None, *args, **kwargs + ) -> Tuple[torch.FloatTensor, Tuple[torch.FloatTensor, ...]]: + if len(args) > 0 or kwargs.get("scale", None) is not None: + deprecation_message = "The `scale` argument is deprecated and will be ignored. Please remove it, as passing it will raise an error in the future. `scale` should directly be passed while calling the underlying pipeline component i.e., via `cross_attention_kwargs`." + deprecate("scale", "1.0.0", deprecation_message) + + output_states = () + + for resnet in self.resnets: + if self.training and self.gradient_checkpointing: + + def create_custom_forward(module): + def custom_forward(*inputs): + return module(*inputs) + + return custom_forward + + if is_torch_version(">=", "1.11.0"): + hidden_states = torch.utils.checkpoint.checkpoint( + create_custom_forward(resnet), hidden_states, temb, use_reentrant=False + ) + else: + hidden_states = torch.utils.checkpoint.checkpoint( + create_custom_forward(resnet), hidden_states, temb + ) + else: + hidden_states = resnet(hidden_states, temb) + + output_states = output_states + (hidden_states,) + + if self.downsamplers is not None: + for downsampler in self.downsamplers: + hidden_states = downsampler(hidden_states, temb) + + output_states = output_states + (hidden_states,) + + return hidden_states, output_states + + +class SimpleCrossAttnDownBlock2D(nn.Module): + def __init__( + self, + in_channels: int, + out_channels: int, + temb_channels: int, + dropout: float = 0.0, + num_layers: int = 1, + resnet_eps: float = 1e-6, + resnet_time_scale_shift: str = "default", + resnet_act_fn: str = "swish", + resnet_groups: int = 32, + resnet_pre_norm: bool = True, + attention_head_dim: int = 1, + cross_attention_dim: int = 1280, + output_scale_factor: float = 1.0, + add_downsample: bool = True, + skip_time_act: bool = False, + only_cross_attention: bool = False, + cross_attention_norm: Optional[str] = None, + ): + super().__init__() + + self.has_cross_attention = True + + resnets = [] + attentions = [] + + self.attention_head_dim = attention_head_dim + self.num_heads = out_channels // self.attention_head_dim + + for i in range(num_layers): + in_channels = in_channels if i == 0 else out_channels + resnets.append( + ResnetBlock2D( + in_channels=in_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + skip_time_act=skip_time_act, + ) + ) + + processor = ( + AttnAddedKVProcessor2_0() if hasattr(F, "scaled_dot_product_attention") else AttnAddedKVProcessor() + ) + + attentions.append( + Attention( + query_dim=out_channels, + cross_attention_dim=out_channels, + heads=self.num_heads, + dim_head=attention_head_dim, + added_kv_proj_dim=cross_attention_dim, + norm_num_groups=resnet_groups, + bias=True, + upcast_softmax=True, + only_cross_attention=only_cross_attention, + cross_attention_norm=cross_attention_norm, + processor=processor, + ) + ) + self.attentions = nn.ModuleList(attentions) + self.resnets = nn.ModuleList(resnets) + + if add_downsample: + self.downsamplers = nn.ModuleList( + [ + ResnetBlock2D( + in_channels=out_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + skip_time_act=skip_time_act, + down=True, + ) + ] + ) + else: + self.downsamplers = None + + self.gradient_checkpointing = False + + def forward( + self, + hidden_states: torch.FloatTensor, + temb: Optional[torch.FloatTensor] = None, + encoder_hidden_states: Optional[torch.FloatTensor] = None, + attention_mask: Optional[torch.FloatTensor] = None, + cross_attention_kwargs: Optional[Dict[str, Any]] = None, + encoder_attention_mask: Optional[torch.FloatTensor] = None, + ) -> Tuple[torch.FloatTensor, Tuple[torch.FloatTensor, ...]]: + cross_attention_kwargs = cross_attention_kwargs if cross_attention_kwargs is not None else {} + if cross_attention_kwargs.get("scale", None) is not None: + logger.warning("Passing `scale` to `cross_attention_kwargs` is deprecated. `scale` will be ignored.") + + output_states = () + + if attention_mask is None: + # if encoder_hidden_states is defined: we are doing cross-attn, so we should use cross-attn mask. + mask = None if encoder_hidden_states is None else encoder_attention_mask + else: + # when attention_mask is defined: we don't even check for encoder_attention_mask. + # this is to maintain compatibility with UnCLIP, which uses 'attention_mask' param for cross-attn masks. + # TODO: UnCLIP should express cross-attn mask via encoder_attention_mask param instead of via attention_mask. + # then we can simplify this whole if/else block to: + # mask = attention_mask if encoder_hidden_states is None else encoder_attention_mask + mask = attention_mask + + for resnet, attn in zip(self.resnets, self.attentions): + if self.training and self.gradient_checkpointing: + + def create_custom_forward(module, return_dict=None): + def custom_forward(*inputs): + if return_dict is not None: + return module(*inputs, return_dict=return_dict) + else: + return module(*inputs) + + return custom_forward + + hidden_states = torch.utils.checkpoint.checkpoint(create_custom_forward(resnet), hidden_states, temb) + hidden_states = attn( + hidden_states, + encoder_hidden_states=encoder_hidden_states, + attention_mask=mask, + **cross_attention_kwargs, + ) + else: + hidden_states = resnet(hidden_states, temb) + + hidden_states = attn( + hidden_states, + encoder_hidden_states=encoder_hidden_states, + attention_mask=mask, + **cross_attention_kwargs, + ) + + output_states = output_states + (hidden_states,) + + if self.downsamplers is not None: + for downsampler in self.downsamplers: + hidden_states = downsampler(hidden_states, temb) + + output_states = output_states + (hidden_states,) + + return hidden_states, output_states + + +class KDownBlock2D(nn.Module): + def __init__( + self, + in_channels: int, + out_channels: int, + temb_channels: int, + dropout: float = 0.0, + num_layers: int = 4, + resnet_eps: float = 1e-5, + resnet_act_fn: str = "gelu", + resnet_group_size: int = 32, + add_downsample: bool = False, + ): + super().__init__() + resnets = [] + + for i in range(num_layers): + in_channels = in_channels if i == 0 else out_channels + groups = in_channels // resnet_group_size + groups_out = out_channels // resnet_group_size + + resnets.append( + ResnetBlockCondNorm2D( + in_channels=in_channels, + out_channels=out_channels, + dropout=dropout, + temb_channels=temb_channels, + groups=groups, + groups_out=groups_out, + eps=resnet_eps, + non_linearity=resnet_act_fn, + time_embedding_norm="ada_group", + conv_shortcut_bias=False, + ) + ) + + self.resnets = nn.ModuleList(resnets) + + if add_downsample: + # YiYi's comments- might be able to use FirDownsample2D, look into details later + self.downsamplers = nn.ModuleList([KDownsample2D()]) + else: + self.downsamplers = None + + self.gradient_checkpointing = False + + def forward( + self, hidden_states: torch.FloatTensor, temb: Optional[torch.FloatTensor] = None, *args, **kwargs + ) -> Tuple[torch.FloatTensor, Tuple[torch.FloatTensor, ...]]: + if len(args) > 0 or kwargs.get("scale", None) is not None: + deprecation_message = "The `scale` argument is deprecated and will be ignored. Please remove it, as passing it will raise an error in the future. `scale` should directly be passed while calling the underlying pipeline component i.e., via `cross_attention_kwargs`." + deprecate("scale", "1.0.0", deprecation_message) + + output_states = () + + for resnet in self.resnets: + if self.training and self.gradient_checkpointing: + + def create_custom_forward(module): + def custom_forward(*inputs): + return module(*inputs) + + return custom_forward + + if is_torch_version(">=", "1.11.0"): + hidden_states = torch.utils.checkpoint.checkpoint( + create_custom_forward(resnet), hidden_states, temb, use_reentrant=False + ) + else: + hidden_states = torch.utils.checkpoint.checkpoint( + create_custom_forward(resnet), hidden_states, temb + ) + else: + hidden_states = resnet(hidden_states, temb) + + output_states += (hidden_states,) + + if self.downsamplers is not None: + for downsampler in self.downsamplers: + hidden_states = downsampler(hidden_states) + + return hidden_states, output_states + + +class KCrossAttnDownBlock2D(nn.Module): + def __init__( + self, + in_channels: int, + out_channels: int, + temb_channels: int, + cross_attention_dim: int, + dropout: float = 0.0, + num_layers: int = 4, + resnet_group_size: int = 32, + add_downsample: bool = True, + attention_head_dim: int = 64, + add_self_attention: bool = False, + resnet_eps: float = 1e-5, + resnet_act_fn: str = "gelu", + ): + super().__init__() + resnets = [] + attentions = [] + + self.has_cross_attention = True + + for i in range(num_layers): + in_channels = in_channels if i == 0 else out_channels + groups = in_channels // resnet_group_size + groups_out = out_channels // resnet_group_size + + resnets.append( + ResnetBlockCondNorm2D( + in_channels=in_channels, + out_channels=out_channels, + dropout=dropout, + temb_channels=temb_channels, + groups=groups, + groups_out=groups_out, + eps=resnet_eps, + non_linearity=resnet_act_fn, + time_embedding_norm="ada_group", + conv_shortcut_bias=False, + ) + ) + attentions.append( + KAttentionBlock( + out_channels, + out_channels // attention_head_dim, + attention_head_dim, + cross_attention_dim=cross_attention_dim, + temb_channels=temb_channels, + attention_bias=True, + add_self_attention=add_self_attention, + cross_attention_norm="layer_norm", + group_size=resnet_group_size, + ) + ) + + self.resnets = nn.ModuleList(resnets) + self.attentions = nn.ModuleList(attentions) + + if add_downsample: + self.downsamplers = nn.ModuleList([KDownsample2D()]) + else: + self.downsamplers = None + + self.gradient_checkpointing = False + + def forward( + self, + hidden_states: torch.FloatTensor, + temb: Optional[torch.FloatTensor] = None, + encoder_hidden_states: Optional[torch.FloatTensor] = None, + attention_mask: Optional[torch.FloatTensor] = None, + cross_attention_kwargs: Optional[Dict[str, Any]] = None, + encoder_attention_mask: Optional[torch.FloatTensor] = None, + ) -> Tuple[torch.FloatTensor, Tuple[torch.FloatTensor, ...]]: + cross_attention_kwargs = cross_attention_kwargs if cross_attention_kwargs is not None else {} + if cross_attention_kwargs.get("scale", None) is not None: + logger.warning("Passing `scale` to `cross_attention_kwargs` is deprecated. `scale` will be ignored.") + + output_states = () + + for resnet, attn in zip(self.resnets, self.attentions): + if self.training and self.gradient_checkpointing: + + def create_custom_forward(module, return_dict=None): + def custom_forward(*inputs): + if return_dict is not None: + return module(*inputs, return_dict=return_dict) + else: + return module(*inputs) + + return custom_forward + + ckpt_kwargs: Dict[str, Any] = {"use_reentrant": False} if is_torch_version(">=", "1.11.0") else {} + hidden_states = torch.utils.checkpoint.checkpoint( + create_custom_forward(resnet), + hidden_states, + temb, + **ckpt_kwargs, + ) + hidden_states = attn( + hidden_states, + encoder_hidden_states=encoder_hidden_states, + emb=temb, + attention_mask=attention_mask, + cross_attention_kwargs=cross_attention_kwargs, + encoder_attention_mask=encoder_attention_mask, + ) + else: + hidden_states = resnet(hidden_states, temb) + hidden_states = attn( + hidden_states, + encoder_hidden_states=encoder_hidden_states, + emb=temb, + attention_mask=attention_mask, + cross_attention_kwargs=cross_attention_kwargs, + encoder_attention_mask=encoder_attention_mask, + ) + + if self.downsamplers is None: + output_states += (None,) + else: + output_states += (hidden_states,) + + if self.downsamplers is not None: + for downsampler in self.downsamplers: + hidden_states = downsampler(hidden_states) + + return hidden_states, output_states + + +class AttnUpBlock2D(nn.Module): + def __init__( + self, + in_channels: int, + prev_output_channel: int, + out_channels: int, + temb_channels: int, + resolution_idx: int = None, + dropout: float = 0.0, + num_layers: int = 1, + resnet_eps: float = 1e-6, + resnet_time_scale_shift: str = "default", + resnet_act_fn: str = "swish", + resnet_groups: int = 32, + resnet_pre_norm: bool = True, + attention_head_dim: int = 1, + output_scale_factor: float = 1.0, + upsample_type: str = "conv", + ): + super().__init__() + resnets = [] + attentions = [] + + self.upsample_type = upsample_type + + if attention_head_dim is None: + logger.warning( + f"It is not recommend to pass `attention_head_dim=None`. Defaulting `attention_head_dim` to `in_channels`: {out_channels}." + ) + attention_head_dim = out_channels + + for i in range(num_layers): + res_skip_channels = in_channels if (i == num_layers - 1) else out_channels + resnet_in_channels = prev_output_channel if i == 0 else out_channels + + resnets.append( + ResnetBlock2D( + in_channels=resnet_in_channels + res_skip_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + ) + ) + attentions.append( + Attention( + out_channels, + heads=out_channels // attention_head_dim, + dim_head=attention_head_dim, + rescale_output_factor=output_scale_factor, + eps=resnet_eps, + norm_num_groups=resnet_groups, + residual_connection=True, + bias=True, + upcast_softmax=True, + _from_deprecated_attn_block=True, + ) + ) + + self.attentions = nn.ModuleList(attentions) + self.resnets = nn.ModuleList(resnets) + + if upsample_type == "conv": + self.upsamplers = nn.ModuleList([Upsample2D(out_channels, use_conv=True, out_channels=out_channels)]) + elif upsample_type == "resnet": + self.upsamplers = nn.ModuleList( + [ + ResnetBlock2D( + in_channels=out_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + up=True, + ) + ] + ) + else: + self.upsamplers = None + + self.resolution_idx = resolution_idx + + def forward( + self, + hidden_states: torch.FloatTensor, + res_hidden_states_tuple: Tuple[torch.FloatTensor, ...], + temb: Optional[torch.FloatTensor] = None, + upsample_size: Optional[int] = None, + *args, + **kwargs, + ) -> torch.FloatTensor: + if len(args) > 0 or kwargs.get("scale", None) is not None: + deprecation_message = "The `scale` argument is deprecated and will be ignored. Please remove it, as passing it will raise an error in the future. `scale` should directly be passed while calling the underlying pipeline component i.e., via `cross_attention_kwargs`." + deprecate("scale", "1.0.0", deprecation_message) + + for resnet, attn in zip(self.resnets, self.attentions): + # pop res hidden states + res_hidden_states = res_hidden_states_tuple[-1] + res_hidden_states_tuple = res_hidden_states_tuple[:-1] + hidden_states = torch.cat([hidden_states, res_hidden_states], dim=1) + + hidden_states = resnet(hidden_states, temb) + hidden_states = attn(hidden_states) + + if self.upsamplers is not None: + for upsampler in self.upsamplers: + if self.upsample_type == "resnet": + hidden_states = upsampler(hidden_states, temb=temb) + else: + hidden_states = upsampler(hidden_states) + + return hidden_states + + +class CrossAttnUpBlock2D(nn.Module): + def __init__( + self, + in_channels: int, + out_channels: int, + prev_output_channel: int, + temb_channels: int, + resolution_idx: Optional[int] = None, + dropout: float = 0.0, + num_layers: int = 1, + transformer_layers_per_block: Union[int, Tuple[int]] = 1, + resnet_eps: float = 1e-6, + resnet_time_scale_shift: str = "default", + resnet_act_fn: str = "swish", + resnet_groups: int = 32, + resnet_pre_norm: bool = True, + num_attention_heads: int = 1, + cross_attention_dim: int = 1280, + output_scale_factor: float = 1.0, + add_upsample: bool = True, + dual_cross_attention: bool = False, + use_linear_projection: bool = False, + only_cross_attention: bool = False, + upcast_attention: bool = False, + attention_type: str = "default", + ): + super().__init__() + resnets = [] + attentions = [] + + self.has_cross_attention = True + self.num_attention_heads = num_attention_heads + + if isinstance(transformer_layers_per_block, int): + transformer_layers_per_block = [transformer_layers_per_block] * num_layers + + for i in range(num_layers): + res_skip_channels = in_channels if (i == num_layers - 1) else out_channels + resnet_in_channels = prev_output_channel if i == 0 else out_channels + + resnets.append( + ResnetBlock2D( + in_channels=resnet_in_channels + res_skip_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + ) + ) + if not dual_cross_attention: + attentions.append( + Transformer2DModel( + num_attention_heads, + out_channels // num_attention_heads, + in_channels=out_channels, + num_layers=transformer_layers_per_block[i], + cross_attention_dim=cross_attention_dim, + norm_num_groups=resnet_groups, + use_linear_projection=use_linear_projection, + only_cross_attention=only_cross_attention, + upcast_attention=upcast_attention, + attention_type=attention_type, + ) + ) + else: + attentions.append( + DualTransformer2DModel( + num_attention_heads, + out_channels // num_attention_heads, + in_channels=out_channels, + num_layers=1, + cross_attention_dim=cross_attention_dim, + norm_num_groups=resnet_groups, + ) + ) + self.attentions = nn.ModuleList(attentions) + self.resnets = nn.ModuleList(resnets) + + if add_upsample: + self.upsamplers = nn.ModuleList([Upsample2D(out_channels, use_conv=True, out_channels=out_channels)]) + else: + self.upsamplers = None + + self.gradient_checkpointing = False + self.resolution_idx = resolution_idx + + def forward( + self, + hidden_states: torch.FloatTensor, + res_hidden_states_tuple: Tuple[torch.FloatTensor, ...], + temb: Optional[torch.FloatTensor] = None, + encoder_hidden_states: Optional[torch.FloatTensor] = None, + cross_attention_kwargs: Optional[Dict[str, Any]] = None, + upsample_size: Optional[int] = None, + attention_mask: Optional[torch.FloatTensor] = None, + encoder_attention_mask: Optional[torch.FloatTensor] = None, + return_res_samples: Optional[bool] = False, + up_block_add_samples: Optional[torch.FloatTensor] = None, + ) -> torch.FloatTensor: + if cross_attention_kwargs is not None: + if cross_attention_kwargs.get("scale", None) is not None: + logger.warning("Passing `scale` to `cross_attention_kwargs` is deprecated. `scale` will be ignored.") + + is_freeu_enabled = ( + getattr(self, "s1", None) + and getattr(self, "s2", None) + and getattr(self, "b1", None) + and getattr(self, "b2", None) + ) + if return_res_samples: + output_states = () + + for resnet, attn in zip(self.resnets, self.attentions): + # pop res hidden states + res_hidden_states = res_hidden_states_tuple[-1] + res_hidden_states_tuple = res_hidden_states_tuple[:-1] + + # FreeU: Only operate on the first two stages + if is_freeu_enabled: + hidden_states, res_hidden_states = apply_freeu( + self.resolution_idx, + hidden_states, + res_hidden_states, + s1=self.s1, + s2=self.s2, + b1=self.b1, + b2=self.b2, + ) + + hidden_states = torch.cat([hidden_states, res_hidden_states], dim=1) + + if self.training and self.gradient_checkpointing: + + def create_custom_forward(module, return_dict=None): + def custom_forward(*inputs): + if return_dict is not None: + return module(*inputs, return_dict=return_dict) + else: + return module(*inputs) + + return custom_forward + + ckpt_kwargs: Dict[str, Any] = {"use_reentrant": False} if is_torch_version(">=", "1.11.0") else {} + hidden_states = torch.utils.checkpoint.checkpoint( + create_custom_forward(resnet), + hidden_states, + temb, + **ckpt_kwargs, + ) + hidden_states = attn( + hidden_states, + encoder_hidden_states=encoder_hidden_states, + cross_attention_kwargs=cross_attention_kwargs, + attention_mask=attention_mask, + encoder_attention_mask=encoder_attention_mask, + return_dict=False, + )[0] + else: + hidden_states = resnet(hidden_states, temb) + hidden_states = attn( + hidden_states, + encoder_hidden_states=encoder_hidden_states, + cross_attention_kwargs=cross_attention_kwargs, + attention_mask=attention_mask, + encoder_attention_mask=encoder_attention_mask, + return_dict=False, + )[0] + if return_res_samples: + output_states = output_states + (hidden_states,) + if up_block_add_samples is not None: + hidden_states = hidden_states + up_block_add_samples.pop(0) + + if self.upsamplers is not None: + for upsampler in self.upsamplers: + hidden_states = upsampler(hidden_states, upsample_size) + if return_res_samples: + output_states = output_states + (hidden_states,) + if up_block_add_samples is not None: + hidden_states = hidden_states + up_block_add_samples.pop(0) + + if return_res_samples: + return hidden_states, output_states + else: + return hidden_states + + +class UpBlock2D(nn.Module): + def __init__( + self, + in_channels: int, + prev_output_channel: int, + out_channels: int, + temb_channels: int, + resolution_idx: Optional[int] = None, + dropout: float = 0.0, + num_layers: int = 1, + resnet_eps: float = 1e-6, + resnet_time_scale_shift: str = "default", + resnet_act_fn: str = "swish", + resnet_groups: int = 32, + resnet_pre_norm: bool = True, + output_scale_factor: float = 1.0, + add_upsample: bool = True, + ): + super().__init__() + resnets = [] + + for i in range(num_layers): + res_skip_channels = in_channels if (i == num_layers - 1) else out_channels + resnet_in_channels = prev_output_channel if i == 0 else out_channels + + resnets.append( + ResnetBlock2D( + in_channels=resnet_in_channels + res_skip_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + ) + ) + + self.resnets = nn.ModuleList(resnets) + + if add_upsample: + self.upsamplers = nn.ModuleList([Upsample2D(out_channels, use_conv=True, out_channels=out_channels)]) + else: + self.upsamplers = None + + self.gradient_checkpointing = False + self.resolution_idx = resolution_idx + + def forward( + self, + hidden_states: torch.FloatTensor, + res_hidden_states_tuple: Tuple[torch.FloatTensor, ...], + temb: Optional[torch.FloatTensor] = None, + upsample_size: Optional[int] = None, + return_res_samples: Optional[bool] = False, + up_block_add_samples: Optional[torch.FloatTensor] = None, + *args, + **kwargs, + ) -> torch.FloatTensor: + if len(args) > 0 or kwargs.get("scale", None) is not None: + deprecation_message = "The `scale` argument is deprecated and will be ignored. Please remove it, as passing it will raise an error in the future. `scale` should directly be passed while calling the underlying pipeline component i.e., via `cross_attention_kwargs`." + deprecate("scale", "1.0.0", deprecation_message) + + is_freeu_enabled = ( + getattr(self, "s1", None) + and getattr(self, "s2", None) + and getattr(self, "b1", None) + and getattr(self, "b2", None) + ) + if return_res_samples: + output_states = () + + for resnet in self.resnets: + # pop res hidden states + res_hidden_states = res_hidden_states_tuple[-1] + res_hidden_states_tuple = res_hidden_states_tuple[:-1] + + # FreeU: Only operate on the first two stages + if is_freeu_enabled: + hidden_states, res_hidden_states = apply_freeu( + self.resolution_idx, + hidden_states, + res_hidden_states, + s1=self.s1, + s2=self.s2, + b1=self.b1, + b2=self.b2, + ) + + hidden_states = torch.cat([hidden_states, res_hidden_states], dim=1) + + if self.training and self.gradient_checkpointing: + + def create_custom_forward(module): + def custom_forward(*inputs): + return module(*inputs) + + return custom_forward + + if is_torch_version(">=", "1.11.0"): + hidden_states = torch.utils.checkpoint.checkpoint( + create_custom_forward(resnet), hidden_states, temb, use_reentrant=False + ) + else: + hidden_states = torch.utils.checkpoint.checkpoint( + create_custom_forward(resnet), hidden_states, temb + ) + else: + hidden_states = resnet(hidden_states, temb) + + if return_res_samples: + output_states = output_states + (hidden_states,) + if up_block_add_samples is not None: + hidden_states = hidden_states + up_block_add_samples.pop(0) # todo: add before or after + + if self.upsamplers is not None: + for upsampler in self.upsamplers: + hidden_states = upsampler(hidden_states, upsample_size) + + if return_res_samples: + output_states = output_states + (hidden_states,) + if up_block_add_samples is not None: + hidden_states = hidden_states + up_block_add_samples.pop(0) # todo: add before or after + + if return_res_samples: + return hidden_states, output_states + else: + return hidden_states + + +class UpDecoderBlock2D(nn.Module): + def __init__( + self, + in_channels: int, + out_channels: int, + resolution_idx: Optional[int] = None, + dropout: float = 0.0, + num_layers: int = 1, + resnet_eps: float = 1e-6, + resnet_time_scale_shift: str = "default", # default, spatial + resnet_act_fn: str = "swish", + resnet_groups: int = 32, + resnet_pre_norm: bool = True, + output_scale_factor: float = 1.0, + add_upsample: bool = True, + temb_channels: Optional[int] = None, + ): + super().__init__() + resnets = [] + + for i in range(num_layers): + input_channels = in_channels if i == 0 else out_channels + + if resnet_time_scale_shift == "spatial": + resnets.append( + ResnetBlockCondNorm2D( + in_channels=input_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm="spatial", + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + ) + ) + else: + resnets.append( + ResnetBlock2D( + in_channels=input_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + ) + ) + + self.resnets = nn.ModuleList(resnets) + + if add_upsample: + self.upsamplers = nn.ModuleList([Upsample2D(out_channels, use_conv=True, out_channels=out_channels)]) + else: + self.upsamplers = None + + self.resolution_idx = resolution_idx + + def forward(self, hidden_states: torch.FloatTensor, temb: Optional[torch.FloatTensor] = None) -> torch.FloatTensor: + for resnet in self.resnets: + hidden_states = resnet(hidden_states, temb=temb) + + if self.upsamplers is not None: + for upsampler in self.upsamplers: + hidden_states = upsampler(hidden_states) + + return hidden_states + + +class AttnUpDecoderBlock2D(nn.Module): + def __init__( + self, + in_channels: int, + out_channels: int, + resolution_idx: Optional[int] = None, + dropout: float = 0.0, + num_layers: int = 1, + resnet_eps: float = 1e-6, + resnet_time_scale_shift: str = "default", + resnet_act_fn: str = "swish", + resnet_groups: int = 32, + resnet_pre_norm: bool = True, + attention_head_dim: int = 1, + output_scale_factor: float = 1.0, + add_upsample: bool = True, + temb_channels: Optional[int] = None, + ): + super().__init__() + resnets = [] + attentions = [] + + if attention_head_dim is None: + logger.warning( + f"It is not recommend to pass `attention_head_dim=None`. Defaulting `attention_head_dim` to `out_channels`: {out_channels}." + ) + attention_head_dim = out_channels + + for i in range(num_layers): + input_channels = in_channels if i == 0 else out_channels + + if resnet_time_scale_shift == "spatial": + resnets.append( + ResnetBlockCondNorm2D( + in_channels=input_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm="spatial", + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + ) + ) + else: + resnets.append( + ResnetBlock2D( + in_channels=input_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + ) + ) + + attentions.append( + Attention( + out_channels, + heads=out_channels // attention_head_dim, + dim_head=attention_head_dim, + rescale_output_factor=output_scale_factor, + eps=resnet_eps, + norm_num_groups=resnet_groups if resnet_time_scale_shift != "spatial" else None, + spatial_norm_dim=temb_channels if resnet_time_scale_shift == "spatial" else None, + residual_connection=True, + bias=True, + upcast_softmax=True, + _from_deprecated_attn_block=True, + ) + ) + + self.attentions = nn.ModuleList(attentions) + self.resnets = nn.ModuleList(resnets) + + if add_upsample: + self.upsamplers = nn.ModuleList([Upsample2D(out_channels, use_conv=True, out_channels=out_channels)]) + else: + self.upsamplers = None + + self.resolution_idx = resolution_idx + + def forward(self, hidden_states: torch.FloatTensor, temb: Optional[torch.FloatTensor] = None) -> torch.FloatTensor: + for resnet, attn in zip(self.resnets, self.attentions): + hidden_states = resnet(hidden_states, temb=temb) + hidden_states = attn(hidden_states, temb=temb) + + if self.upsamplers is not None: + for upsampler in self.upsamplers: + hidden_states = upsampler(hidden_states) + + return hidden_states + + +class AttnSkipUpBlock2D(nn.Module): + def __init__( + self, + in_channels: int, + prev_output_channel: int, + out_channels: int, + temb_channels: int, + resolution_idx: Optional[int] = None, + dropout: float = 0.0, + num_layers: int = 1, + resnet_eps: float = 1e-6, + resnet_time_scale_shift: str = "default", + resnet_act_fn: str = "swish", + resnet_pre_norm: bool = True, + attention_head_dim: int = 1, + output_scale_factor: float = np.sqrt(2.0), + add_upsample: bool = True, + ): + super().__init__() + self.attentions = nn.ModuleList([]) + self.resnets = nn.ModuleList([]) + + for i in range(num_layers): + res_skip_channels = in_channels if (i == num_layers - 1) else out_channels + resnet_in_channels = prev_output_channel if i == 0 else out_channels + + self.resnets.append( + ResnetBlock2D( + in_channels=resnet_in_channels + res_skip_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=min(resnet_in_channels + res_skip_channels // 4, 32), + groups_out=min(out_channels // 4, 32), + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + ) + ) + + if attention_head_dim is None: + logger.warning( + f"It is not recommend to pass `attention_head_dim=None`. Defaulting `attention_head_dim` to `out_channels`: {out_channels}." + ) + attention_head_dim = out_channels + + self.attentions.append( + Attention( + out_channels, + heads=out_channels // attention_head_dim, + dim_head=attention_head_dim, + rescale_output_factor=output_scale_factor, + eps=resnet_eps, + norm_num_groups=32, + residual_connection=True, + bias=True, + upcast_softmax=True, + _from_deprecated_attn_block=True, + ) + ) + + self.upsampler = FirUpsample2D(in_channels, out_channels=out_channels) + if add_upsample: + self.resnet_up = ResnetBlock2D( + in_channels=out_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=min(out_channels // 4, 32), + groups_out=min(out_channels // 4, 32), + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + use_in_shortcut=True, + up=True, + kernel="fir", + ) + self.skip_conv = nn.Conv2d(out_channels, 3, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) + self.skip_norm = torch.nn.GroupNorm( + num_groups=min(out_channels // 4, 32), num_channels=out_channels, eps=resnet_eps, affine=True + ) + self.act = nn.SiLU() + else: + self.resnet_up = None + self.skip_conv = None + self.skip_norm = None + self.act = None + + self.resolution_idx = resolution_idx + + def forward( + self, + hidden_states: torch.FloatTensor, + res_hidden_states_tuple: Tuple[torch.FloatTensor, ...], + temb: Optional[torch.FloatTensor] = None, + skip_sample=None, + *args, + **kwargs, + ) -> Tuple[torch.FloatTensor, torch.FloatTensor]: + if len(args) > 0 or kwargs.get("scale", None) is not None: + deprecation_message = "The `scale` argument is deprecated and will be ignored. Please remove it, as passing it will raise an error in the future. `scale` should directly be passed while calling the underlying pipeline component i.e., via `cross_attention_kwargs`." + deprecate("scale", "1.0.0", deprecation_message) + + for resnet in self.resnets: + # pop res hidden states + res_hidden_states = res_hidden_states_tuple[-1] + res_hidden_states_tuple = res_hidden_states_tuple[:-1] + hidden_states = torch.cat([hidden_states, res_hidden_states], dim=1) + + hidden_states = resnet(hidden_states, temb) + + hidden_states = self.attentions[0](hidden_states) + + if skip_sample is not None: + skip_sample = self.upsampler(skip_sample) + else: + skip_sample = 0 + + if self.resnet_up is not None: + skip_sample_states = self.skip_norm(hidden_states) + skip_sample_states = self.act(skip_sample_states) + skip_sample_states = self.skip_conv(skip_sample_states) + + skip_sample = skip_sample + skip_sample_states + + hidden_states = self.resnet_up(hidden_states, temb) + + return hidden_states, skip_sample + + +class SkipUpBlock2D(nn.Module): + def __init__( + self, + in_channels: int, + prev_output_channel: int, + out_channels: int, + temb_channels: int, + resolution_idx: Optional[int] = None, + dropout: float = 0.0, + num_layers: int = 1, + resnet_eps: float = 1e-6, + resnet_time_scale_shift: str = "default", + resnet_act_fn: str = "swish", + resnet_pre_norm: bool = True, + output_scale_factor: float = np.sqrt(2.0), + add_upsample: bool = True, + upsample_padding: int = 1, + ): + super().__init__() + self.resnets = nn.ModuleList([]) + + for i in range(num_layers): + res_skip_channels = in_channels if (i == num_layers - 1) else out_channels + resnet_in_channels = prev_output_channel if i == 0 else out_channels + + self.resnets.append( + ResnetBlock2D( + in_channels=resnet_in_channels + res_skip_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=min((resnet_in_channels + res_skip_channels) // 4, 32), + groups_out=min(out_channels // 4, 32), + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + ) + ) + + self.upsampler = FirUpsample2D(in_channels, out_channels=out_channels) + if add_upsample: + self.resnet_up = ResnetBlock2D( + in_channels=out_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=min(out_channels // 4, 32), + groups_out=min(out_channels // 4, 32), + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + use_in_shortcut=True, + up=True, + kernel="fir", + ) + self.skip_conv = nn.Conv2d(out_channels, 3, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) + self.skip_norm = torch.nn.GroupNorm( + num_groups=min(out_channels // 4, 32), num_channels=out_channels, eps=resnet_eps, affine=True + ) + self.act = nn.SiLU() + else: + self.resnet_up = None + self.skip_conv = None + self.skip_norm = None + self.act = None + + self.resolution_idx = resolution_idx + + def forward( + self, + hidden_states: torch.FloatTensor, + res_hidden_states_tuple: Tuple[torch.FloatTensor, ...], + temb: Optional[torch.FloatTensor] = None, + skip_sample=None, + *args, + **kwargs, + ) -> Tuple[torch.FloatTensor, torch.FloatTensor]: + if len(args) > 0 or kwargs.get("scale", None) is not None: + deprecation_message = "The `scale` argument is deprecated and will be ignored. Please remove it, as passing it will raise an error in the future. `scale` should directly be passed while calling the underlying pipeline component i.e., via `cross_attention_kwargs`." + deprecate("scale", "1.0.0", deprecation_message) + + for resnet in self.resnets: + # pop res hidden states + res_hidden_states = res_hidden_states_tuple[-1] + res_hidden_states_tuple = res_hidden_states_tuple[:-1] + hidden_states = torch.cat([hidden_states, res_hidden_states], dim=1) + + hidden_states = resnet(hidden_states, temb) + + if skip_sample is not None: + skip_sample = self.upsampler(skip_sample) + else: + skip_sample = 0 + + if self.resnet_up is not None: + skip_sample_states = self.skip_norm(hidden_states) + skip_sample_states = self.act(skip_sample_states) + skip_sample_states = self.skip_conv(skip_sample_states) + + skip_sample = skip_sample + skip_sample_states + + hidden_states = self.resnet_up(hidden_states, temb) + + return hidden_states, skip_sample + + +class ResnetUpsampleBlock2D(nn.Module): + def __init__( + self, + in_channels: int, + prev_output_channel: int, + out_channels: int, + temb_channels: int, + resolution_idx: Optional[int] = None, + dropout: float = 0.0, + num_layers: int = 1, + resnet_eps: float = 1e-6, + resnet_time_scale_shift: str = "default", + resnet_act_fn: str = "swish", + resnet_groups: int = 32, + resnet_pre_norm: bool = True, + output_scale_factor: float = 1.0, + add_upsample: bool = True, + skip_time_act: bool = False, + ): + super().__init__() + resnets = [] + + for i in range(num_layers): + res_skip_channels = in_channels if (i == num_layers - 1) else out_channels + resnet_in_channels = prev_output_channel if i == 0 else out_channels + + resnets.append( + ResnetBlock2D( + in_channels=resnet_in_channels + res_skip_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + skip_time_act=skip_time_act, + ) + ) + + self.resnets = nn.ModuleList(resnets) + + if add_upsample: + self.upsamplers = nn.ModuleList( + [ + ResnetBlock2D( + in_channels=out_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + skip_time_act=skip_time_act, + up=True, + ) + ] + ) + else: + self.upsamplers = None + + self.gradient_checkpointing = False + self.resolution_idx = resolution_idx + + def forward( + self, + hidden_states: torch.FloatTensor, + res_hidden_states_tuple: Tuple[torch.FloatTensor, ...], + temb: Optional[torch.FloatTensor] = None, + upsample_size: Optional[int] = None, + *args, + **kwargs, + ) -> torch.FloatTensor: + if len(args) > 0 or kwargs.get("scale", None) is not None: + deprecation_message = "The `scale` argument is deprecated and will be ignored. Please remove it, as passing it will raise an error in the future. `scale` should directly be passed while calling the underlying pipeline component i.e., via `cross_attention_kwargs`." + deprecate("scale", "1.0.0", deprecation_message) + + for resnet in self.resnets: + # pop res hidden states + res_hidden_states = res_hidden_states_tuple[-1] + res_hidden_states_tuple = res_hidden_states_tuple[:-1] + hidden_states = torch.cat([hidden_states, res_hidden_states], dim=1) + + if self.training and self.gradient_checkpointing: + + def create_custom_forward(module): + def custom_forward(*inputs): + return module(*inputs) + + return custom_forward + + if is_torch_version(">=", "1.11.0"): + hidden_states = torch.utils.checkpoint.checkpoint( + create_custom_forward(resnet), hidden_states, temb, use_reentrant=False + ) + else: + hidden_states = torch.utils.checkpoint.checkpoint( + create_custom_forward(resnet), hidden_states, temb + ) + else: + hidden_states = resnet(hidden_states, temb) + + if self.upsamplers is not None: + for upsampler in self.upsamplers: + hidden_states = upsampler(hidden_states, temb) + + return hidden_states + + +class SimpleCrossAttnUpBlock2D(nn.Module): + def __init__( + self, + in_channels: int, + out_channels: int, + prev_output_channel: int, + temb_channels: int, + resolution_idx: Optional[int] = None, + dropout: float = 0.0, + num_layers: int = 1, + resnet_eps: float = 1e-6, + resnet_time_scale_shift: str = "default", + resnet_act_fn: str = "swish", + resnet_groups: int = 32, + resnet_pre_norm: bool = True, + attention_head_dim: int = 1, + cross_attention_dim: int = 1280, + output_scale_factor: float = 1.0, + add_upsample: bool = True, + skip_time_act: bool = False, + only_cross_attention: bool = False, + cross_attention_norm: Optional[str] = None, + ): + super().__init__() + resnets = [] + attentions = [] + + self.has_cross_attention = True + self.attention_head_dim = attention_head_dim + + self.num_heads = out_channels // self.attention_head_dim + + for i in range(num_layers): + res_skip_channels = in_channels if (i == num_layers - 1) else out_channels + resnet_in_channels = prev_output_channel if i == 0 else out_channels + + resnets.append( + ResnetBlock2D( + in_channels=resnet_in_channels + res_skip_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + skip_time_act=skip_time_act, + ) + ) + + processor = ( + AttnAddedKVProcessor2_0() if hasattr(F, "scaled_dot_product_attention") else AttnAddedKVProcessor() + ) + + attentions.append( + Attention( + query_dim=out_channels, + cross_attention_dim=out_channels, + heads=self.num_heads, + dim_head=self.attention_head_dim, + added_kv_proj_dim=cross_attention_dim, + norm_num_groups=resnet_groups, + bias=True, + upcast_softmax=True, + only_cross_attention=only_cross_attention, + cross_attention_norm=cross_attention_norm, + processor=processor, + ) + ) + self.attentions = nn.ModuleList(attentions) + self.resnets = nn.ModuleList(resnets) + + if add_upsample: + self.upsamplers = nn.ModuleList( + [ + ResnetBlock2D( + in_channels=out_channels, + out_channels=out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=resnet_groups, + dropout=dropout, + time_embedding_norm=resnet_time_scale_shift, + non_linearity=resnet_act_fn, + output_scale_factor=output_scale_factor, + pre_norm=resnet_pre_norm, + skip_time_act=skip_time_act, + up=True, + ) + ] + ) + else: + self.upsamplers = None + + self.gradient_checkpointing = False + self.resolution_idx = resolution_idx + + def forward( + self, + hidden_states: torch.FloatTensor, + res_hidden_states_tuple: Tuple[torch.FloatTensor, ...], + temb: Optional[torch.FloatTensor] = None, + encoder_hidden_states: Optional[torch.FloatTensor] = None, + upsample_size: Optional[int] = None, + attention_mask: Optional[torch.FloatTensor] = None, + cross_attention_kwargs: Optional[Dict[str, Any]] = None, + encoder_attention_mask: Optional[torch.FloatTensor] = None, + ) -> torch.FloatTensor: + cross_attention_kwargs = cross_attention_kwargs if cross_attention_kwargs is not None else {} + if cross_attention_kwargs.get("scale", None) is not None: + logger.warning("Passing `scale` to `cross_attention_kwargs` is deprecated. `scale` will be ignored.") + + if attention_mask is None: + # if encoder_hidden_states is defined: we are doing cross-attn, so we should use cross-attn mask. + mask = None if encoder_hidden_states is None else encoder_attention_mask + else: + # when attention_mask is defined: we don't even check for encoder_attention_mask. + # this is to maintain compatibility with UnCLIP, which uses 'attention_mask' param for cross-attn masks. + # TODO: UnCLIP should express cross-attn mask via encoder_attention_mask param instead of via attention_mask. + # then we can simplify this whole if/else block to: + # mask = attention_mask if encoder_hidden_states is None else encoder_attention_mask + mask = attention_mask + + for resnet, attn in zip(self.resnets, self.attentions): + # resnet + # pop res hidden states + res_hidden_states = res_hidden_states_tuple[-1] + res_hidden_states_tuple = res_hidden_states_tuple[:-1] + hidden_states = torch.cat([hidden_states, res_hidden_states], dim=1) + + if self.training and self.gradient_checkpointing: + + def create_custom_forward(module, return_dict=None): + def custom_forward(*inputs): + if return_dict is not None: + return module(*inputs, return_dict=return_dict) + else: + return module(*inputs) + + return custom_forward + + hidden_states = torch.utils.checkpoint.checkpoint(create_custom_forward(resnet), hidden_states, temb) + hidden_states = attn( + hidden_states, + encoder_hidden_states=encoder_hidden_states, + attention_mask=mask, + **cross_attention_kwargs, + ) + else: + hidden_states = resnet(hidden_states, temb) + + hidden_states = attn( + hidden_states, + encoder_hidden_states=encoder_hidden_states, + attention_mask=mask, + **cross_attention_kwargs, + ) + + if self.upsamplers is not None: + for upsampler in self.upsamplers: + hidden_states = upsampler(hidden_states, temb) + + return hidden_states + + +class KUpBlock2D(nn.Module): + def __init__( + self, + in_channels: int, + out_channels: int, + temb_channels: int, + resolution_idx: int, + dropout: float = 0.0, + num_layers: int = 5, + resnet_eps: float = 1e-5, + resnet_act_fn: str = "gelu", + resnet_group_size: Optional[int] = 32, + add_upsample: bool = True, + ): + super().__init__() + resnets = [] + k_in_channels = 2 * out_channels + k_out_channels = in_channels + num_layers = num_layers - 1 + + for i in range(num_layers): + in_channels = k_in_channels if i == 0 else out_channels + groups = in_channels // resnet_group_size + groups_out = out_channels // resnet_group_size + + resnets.append( + ResnetBlockCondNorm2D( + in_channels=in_channels, + out_channels=k_out_channels if (i == num_layers - 1) else out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=groups, + groups_out=groups_out, + dropout=dropout, + non_linearity=resnet_act_fn, + time_embedding_norm="ada_group", + conv_shortcut_bias=False, + ) + ) + + self.resnets = nn.ModuleList(resnets) + + if add_upsample: + self.upsamplers = nn.ModuleList([KUpsample2D()]) + else: + self.upsamplers = None + + self.gradient_checkpointing = False + self.resolution_idx = resolution_idx + + def forward( + self, + hidden_states: torch.FloatTensor, + res_hidden_states_tuple: Tuple[torch.FloatTensor, ...], + temb: Optional[torch.FloatTensor] = None, + upsample_size: Optional[int] = None, + *args, + **kwargs, + ) -> torch.FloatTensor: + if len(args) > 0 or kwargs.get("scale", None) is not None: + deprecation_message = "The `scale` argument is deprecated and will be ignored. Please remove it, as passing it will raise an error in the future. `scale` should directly be passed while calling the underlying pipeline component i.e., via `cross_attention_kwargs`." + deprecate("scale", "1.0.0", deprecation_message) + + res_hidden_states_tuple = res_hidden_states_tuple[-1] + if res_hidden_states_tuple is not None: + hidden_states = torch.cat([hidden_states, res_hidden_states_tuple], dim=1) + + for resnet in self.resnets: + if self.training and self.gradient_checkpointing: + + def create_custom_forward(module): + def custom_forward(*inputs): + return module(*inputs) + + return custom_forward + + if is_torch_version(">=", "1.11.0"): + hidden_states = torch.utils.checkpoint.checkpoint( + create_custom_forward(resnet), hidden_states, temb, use_reentrant=False + ) + else: + hidden_states = torch.utils.checkpoint.checkpoint( + create_custom_forward(resnet), hidden_states, temb + ) + else: + hidden_states = resnet(hidden_states, temb) + + if self.upsamplers is not None: + for upsampler in self.upsamplers: + hidden_states = upsampler(hidden_states) + + return hidden_states + + +class KCrossAttnUpBlock2D(nn.Module): + def __init__( + self, + in_channels: int, + out_channels: int, + temb_channels: int, + resolution_idx: int, + dropout: float = 0.0, + num_layers: int = 4, + resnet_eps: float = 1e-5, + resnet_act_fn: str = "gelu", + resnet_group_size: int = 32, + attention_head_dim: int = 1, # attention dim_head + cross_attention_dim: int = 768, + add_upsample: bool = True, + upcast_attention: bool = False, + ): + super().__init__() + resnets = [] + attentions = [] + + is_first_block = in_channels == out_channels == temb_channels + is_middle_block = in_channels != out_channels + add_self_attention = True if is_first_block else False + + self.has_cross_attention = True + self.attention_head_dim = attention_head_dim + + # in_channels, and out_channels for the block (k-unet) + k_in_channels = out_channels if is_first_block else 2 * out_channels + k_out_channels = in_channels + + num_layers = num_layers - 1 + + for i in range(num_layers): + in_channels = k_in_channels if i == 0 else out_channels + groups = in_channels // resnet_group_size + groups_out = out_channels // resnet_group_size + + if is_middle_block and (i == num_layers - 1): + conv_2d_out_channels = k_out_channels + else: + conv_2d_out_channels = None + + resnets.append( + ResnetBlockCondNorm2D( + in_channels=in_channels, + out_channels=out_channels, + conv_2d_out_channels=conv_2d_out_channels, + temb_channels=temb_channels, + eps=resnet_eps, + groups=groups, + groups_out=groups_out, + dropout=dropout, + non_linearity=resnet_act_fn, + time_embedding_norm="ada_group", + conv_shortcut_bias=False, + ) + ) + attentions.append( + KAttentionBlock( + k_out_channels if (i == num_layers - 1) else out_channels, + k_out_channels // attention_head_dim + if (i == num_layers - 1) + else out_channels // attention_head_dim, + attention_head_dim, + cross_attention_dim=cross_attention_dim, + temb_channels=temb_channels, + attention_bias=True, + add_self_attention=add_self_attention, + cross_attention_norm="layer_norm", + upcast_attention=upcast_attention, + ) + ) + + self.resnets = nn.ModuleList(resnets) + self.attentions = nn.ModuleList(attentions) + + if add_upsample: + self.upsamplers = nn.ModuleList([KUpsample2D()]) + else: + self.upsamplers = None + + self.gradient_checkpointing = False + self.resolution_idx = resolution_idx + + def forward( + self, + hidden_states: torch.FloatTensor, + res_hidden_states_tuple: Tuple[torch.FloatTensor, ...], + temb: Optional[torch.FloatTensor] = None, + encoder_hidden_states: Optional[torch.FloatTensor] = None, + cross_attention_kwargs: Optional[Dict[str, Any]] = None, + upsample_size: Optional[int] = None, + attention_mask: Optional[torch.FloatTensor] = None, + encoder_attention_mask: Optional[torch.FloatTensor] = None, + ) -> torch.FloatTensor: + res_hidden_states_tuple = res_hidden_states_tuple[-1] + if res_hidden_states_tuple is not None: + hidden_states = torch.cat([hidden_states, res_hidden_states_tuple], dim=1) + + for resnet, attn in zip(self.resnets, self.attentions): + if self.training and self.gradient_checkpointing: + + def create_custom_forward(module, return_dict=None): + def custom_forward(*inputs): + if return_dict is not None: + return module(*inputs, return_dict=return_dict) + else: + return module(*inputs) + + return custom_forward + + ckpt_kwargs: Dict[str, Any] = {"use_reentrant": False} if is_torch_version(">=", "1.11.0") else {} + hidden_states = torch.utils.checkpoint.checkpoint( + create_custom_forward(resnet), + hidden_states, + temb, + **ckpt_kwargs, + ) + hidden_states = attn( + hidden_states, + encoder_hidden_states=encoder_hidden_states, + emb=temb, + attention_mask=attention_mask, + cross_attention_kwargs=cross_attention_kwargs, + encoder_attention_mask=encoder_attention_mask, + ) + else: + hidden_states = resnet(hidden_states, temb) + hidden_states = attn( + hidden_states, + encoder_hidden_states=encoder_hidden_states, + emb=temb, + attention_mask=attention_mask, + cross_attention_kwargs=cross_attention_kwargs, + encoder_attention_mask=encoder_attention_mask, + ) + + if self.upsamplers is not None: + for upsampler in self.upsamplers: + hidden_states = upsampler(hidden_states) + + return hidden_states + + +# can potentially later be renamed to `No-feed-forward` attention +class KAttentionBlock(nn.Module): + r""" + A basic Transformer block. + + Parameters: + dim (`int`): The number of channels in the input and output. + num_attention_heads (`int`): The number of heads to use for multi-head attention. + attention_head_dim (`int`): The number of channels in each head. + dropout (`float`, *optional*, defaults to 0.0): The dropout probability to use. + cross_attention_dim (`int`, *optional*): The size of the encoder_hidden_states vector for cross attention. + attention_bias (`bool`, *optional*, defaults to `False`): + Configure if the attention layers should contain a bias parameter. + upcast_attention (`bool`, *optional*, defaults to `False`): + Set to `True` to upcast the attention computation to `float32`. + temb_channels (`int`, *optional*, defaults to 768): + The number of channels in the token embedding. + add_self_attention (`bool`, *optional*, defaults to `False`): + Set to `True` to add self-attention to the block. + cross_attention_norm (`str`, *optional*, defaults to `None`): + The type of normalization to use for the cross attention. Can be `None`, `layer_norm`, or `group_norm`. + group_size (`int`, *optional*, defaults to 32): + The number of groups to separate the channels into for group normalization. + """ + + def __init__( + self, + dim: int, + num_attention_heads: int, + attention_head_dim: int, + dropout: float = 0.0, + cross_attention_dim: Optional[int] = None, + attention_bias: bool = False, + upcast_attention: bool = False, + temb_channels: int = 768, # for ada_group_norm + add_self_attention: bool = False, + cross_attention_norm: Optional[str] = None, + group_size: int = 32, + ): + super().__init__() + self.add_self_attention = add_self_attention + + # 1. Self-Attn + if add_self_attention: + self.norm1 = AdaGroupNorm(temb_channels, dim, max(1, dim // group_size)) + self.attn1 = Attention( + query_dim=dim, + heads=num_attention_heads, + dim_head=attention_head_dim, + dropout=dropout, + bias=attention_bias, + cross_attention_dim=None, + cross_attention_norm=None, + ) + + # 2. Cross-Attn + self.norm2 = AdaGroupNorm(temb_channels, dim, max(1, dim // group_size)) + self.attn2 = Attention( + query_dim=dim, + cross_attention_dim=cross_attention_dim, + heads=num_attention_heads, + dim_head=attention_head_dim, + dropout=dropout, + bias=attention_bias, + upcast_attention=upcast_attention, + cross_attention_norm=cross_attention_norm, + ) + + def _to_3d(self, hidden_states: torch.FloatTensor, height: int, weight: int) -> torch.FloatTensor: + return hidden_states.permute(0, 2, 3, 1).reshape(hidden_states.shape[0], height * weight, -1) + + def _to_4d(self, hidden_states: torch.FloatTensor, height: int, weight: int) -> torch.FloatTensor: + return hidden_states.permute(0, 2, 1).reshape(hidden_states.shape[0], -1, height, weight) + + def forward( + self, + hidden_states: torch.FloatTensor, + encoder_hidden_states: Optional[torch.FloatTensor] = None, + # TODO: mark emb as non-optional (self.norm2 requires it). + # requires assessing impact of change to positional param interface. + emb: Optional[torch.FloatTensor] = None, + attention_mask: Optional[torch.FloatTensor] = None, + cross_attention_kwargs: Optional[Dict[str, Any]] = None, + encoder_attention_mask: Optional[torch.FloatTensor] = None, + ) -> torch.FloatTensor: + cross_attention_kwargs = cross_attention_kwargs if cross_attention_kwargs is not None else {} + if cross_attention_kwargs.get("scale", None) is not None: + logger.warning("Passing `scale` to `cross_attention_kwargs` is deprecated. `scale` will be ignored.") + + # 1. Self-Attention + if self.add_self_attention: + norm_hidden_states = self.norm1(hidden_states, emb) + + height, weight = norm_hidden_states.shape[2:] + norm_hidden_states = self._to_3d(norm_hidden_states, height, weight) + + attn_output = self.attn1( + norm_hidden_states, + encoder_hidden_states=None, + attention_mask=attention_mask, + **cross_attention_kwargs, + ) + attn_output = self._to_4d(attn_output, height, weight) + + hidden_states = attn_output + hidden_states + + # 2. Cross-Attention/None + norm_hidden_states = self.norm2(hidden_states, emb) + + height, weight = norm_hidden_states.shape[2:] + norm_hidden_states = self._to_3d(norm_hidden_states, height, weight) + attn_output = self.attn2( + norm_hidden_states, + encoder_hidden_states=encoder_hidden_states, + attention_mask=attention_mask if encoder_hidden_states is None else encoder_attention_mask, + **cross_attention_kwargs, + ) + attn_output = self._to_4d(attn_output, height, weight) + + hidden_states = attn_output + hidden_states + + return hidden_states diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/brushnet/unet_2d_condition.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/brushnet/unet_2d_condition.py new file mode 100644 index 0000000000000000000000000000000000000000..103cd08d02e92753bce82af557dfc460f7406924 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/brushnet/unet_2d_condition.py @@ -0,0 +1,1359 @@ +# Copyright 2024 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from dataclasses import dataclass +from typing import Any, Dict, List, Optional, Tuple, Union + +import torch +import torch.nn as nn +import torch.utils.checkpoint + +from diffusers.configuration_utils import ConfigMixin, register_to_config +from diffusers.loaders import PeftAdapterMixin, UNet2DConditionLoadersMixin +from diffusers.utils import USE_PEFT_BACKEND, BaseOutput, deprecate, logging, scale_lora_layers, unscale_lora_layers +from diffusers.models.activations import get_activation +from diffusers.models.attention_processor import ( + ADDED_KV_ATTENTION_PROCESSORS, + CROSS_ATTENTION_PROCESSORS, + Attention, + AttentionProcessor, + AttnAddedKVProcessor, + AttnProcessor, +) +from diffusers.models.embeddings import ( + GaussianFourierProjection, + GLIGENTextBoundingboxProjection, + ImageHintTimeEmbedding, + ImageProjection, + ImageTimeEmbedding, + TextImageProjection, + TextImageTimeEmbedding, + TextTimeEmbedding, + TimestepEmbedding, + Timesteps, +) +from diffusers.models.modeling_utils import ModelMixin +from .unet_2d_blocks import ( + get_down_block, + get_mid_block, + get_up_block, +) + +logger = logging.get_logger(__name__) # pylint: disable=invalid-name + + +@dataclass +class UNet2DConditionOutput(BaseOutput): + """ + The output of [`UNet2DConditionModel`]. + + Args: + sample (`torch.FloatTensor` of shape `(batch_size, num_channels, height, width)`): + The hidden states output conditioned on `encoder_hidden_states` input. Output of last layer of model. + """ + + sample: torch.FloatTensor = None + + +class UNet2DConditionModel(ModelMixin, ConfigMixin, UNet2DConditionLoadersMixin, PeftAdapterMixin): + r""" + A conditional 2D UNet model that takes a noisy sample, conditional state, and a timestep and returns a sample + shaped output. + + This model inherits from [`ModelMixin`]. Check the superclass documentation for it's generic methods implemented + for all models (such as downloading or saving). + + Parameters: + sample_size (`int` or `Tuple[int, int]`, *optional*, defaults to `None`): + Height and width of input/output sample. + in_channels (`int`, *optional*, defaults to 4): Number of channels in the input sample. + out_channels (`int`, *optional*, defaults to 4): Number of channels in the output. + center_input_sample (`bool`, *optional*, defaults to `False`): Whether to center the input sample. + flip_sin_to_cos (`bool`, *optional*, defaults to `True`): + Whether to flip the sin to cos in the time embedding. + freq_shift (`int`, *optional*, defaults to 0): The frequency shift to apply to the time embedding. + down_block_types (`Tuple[str]`, *optional*, defaults to `("CrossAttnDownBlock2D", "CrossAttnDownBlock2D", "CrossAttnDownBlock2D", "DownBlock2D")`): + The tuple of downsample blocks to use. + mid_block_type (`str`, *optional*, defaults to `"UNetMidBlock2DCrossAttn"`): + Block type for middle of UNet, it can be one of `UNetMidBlock2DCrossAttn`, `UNetMidBlock2D`, or + `UNetMidBlock2DSimpleCrossAttn`. If `None`, the mid block layer is skipped. + up_block_types (`Tuple[str]`, *optional*, defaults to `("UpBlock2D", "CrossAttnUpBlock2D", "CrossAttnUpBlock2D", "CrossAttnUpBlock2D")`): + The tuple of upsample blocks to use. + only_cross_attention(`bool` or `Tuple[bool]`, *optional*, default to `False`): + Whether to include self-attention in the basic transformer blocks, see + [`~models.attention.BasicTransformerBlock`]. + block_out_channels (`Tuple[int]`, *optional*, defaults to `(320, 640, 1280, 1280)`): + The tuple of output channels for each block. + layers_per_block (`int`, *optional*, defaults to 2): The number of layers per block. + downsample_padding (`int`, *optional*, defaults to 1): The padding to use for the downsampling convolution. + mid_block_scale_factor (`float`, *optional*, defaults to 1.0): The scale factor to use for the mid block. + dropout (`float`, *optional*, defaults to 0.0): The dropout probability to use. + act_fn (`str`, *optional*, defaults to `"silu"`): The activation function to use. + norm_num_groups (`int`, *optional*, defaults to 32): The number of groups to use for the normalization. + If `None`, normalization and activation layers is skipped in post-processing. + norm_eps (`float`, *optional*, defaults to 1e-5): The epsilon to use for the normalization. + cross_attention_dim (`int` or `Tuple[int]`, *optional*, defaults to 1280): + The dimension of the cross attention features. + transformer_layers_per_block (`int`, `Tuple[int]`, or `Tuple[Tuple]` , *optional*, defaults to 1): + The number of transformer blocks of type [`~models.attention.BasicTransformerBlock`]. Only relevant for + [`~models.unet_2d_blocks.CrossAttnDownBlock2D`], [`~models.unet_2d_blocks.CrossAttnUpBlock2D`], + [`~models.unet_2d_blocks.UNetMidBlock2DCrossAttn`]. + reverse_transformer_layers_per_block : (`Tuple[Tuple]`, *optional*, defaults to None): + The number of transformer blocks of type [`~models.attention.BasicTransformerBlock`], in the upsampling + blocks of the U-Net. Only relevant if `transformer_layers_per_block` is of type `Tuple[Tuple]` and for + [`~models.unet_2d_blocks.CrossAttnDownBlock2D`], [`~models.unet_2d_blocks.CrossAttnUpBlock2D`], + [`~models.unet_2d_blocks.UNetMidBlock2DCrossAttn`]. + encoder_hid_dim (`int`, *optional*, defaults to None): + If `encoder_hid_dim_type` is defined, `encoder_hidden_states` will be projected from `encoder_hid_dim` + dimension to `cross_attention_dim`. + encoder_hid_dim_type (`str`, *optional*, defaults to `None`): + If given, the `encoder_hidden_states` and potentially other embeddings are down-projected to text + embeddings of dimension `cross_attention` according to `encoder_hid_dim_type`. + attention_head_dim (`int`, *optional*, defaults to 8): The dimension of the attention heads. + num_attention_heads (`int`, *optional*): + The number of attention heads. If not defined, defaults to `attention_head_dim` + resnet_time_scale_shift (`str`, *optional*, defaults to `"default"`): Time scale shift config + for ResNet blocks (see [`~models.resnet.ResnetBlock2D`]). Choose from `default` or `scale_shift`. + class_embed_type (`str`, *optional*, defaults to `None`): + The type of class embedding to use which is ultimately summed with the time embeddings. Choose from `None`, + `"timestep"`, `"identity"`, `"projection"`, or `"simple_projection"`. + addition_embed_type (`str`, *optional*, defaults to `None`): + Configures an optional embedding which will be summed with the time embeddings. Choose from `None` or + "text". "text" will use the `TextTimeEmbedding` layer. + addition_time_embed_dim: (`int`, *optional*, defaults to `None`): + Dimension for the timestep embeddings. + num_class_embeds (`int`, *optional*, defaults to `None`): + Input dimension of the learnable embedding matrix to be projected to `time_embed_dim`, when performing + class conditioning with `class_embed_type` equal to `None`. + time_embedding_type (`str`, *optional*, defaults to `positional`): + The type of position embedding to use for timesteps. Choose from `positional` or `fourier`. + time_embedding_dim (`int`, *optional*, defaults to `None`): + An optional override for the dimension of the projected time embedding. + time_embedding_act_fn (`str`, *optional*, defaults to `None`): + Optional activation function to use only once on the time embeddings before they are passed to the rest of + the UNet. Choose from `silu`, `mish`, `gelu`, and `swish`. + timestep_post_act (`str`, *optional*, defaults to `None`): + The second activation function to use in timestep embedding. Choose from `silu`, `mish` and `gelu`. + time_cond_proj_dim (`int`, *optional*, defaults to `None`): + The dimension of `cond_proj` layer in the timestep embedding. + conv_in_kernel (`int`, *optional*, default to `3`): The kernel size of `conv_in` layer. + conv_out_kernel (`int`, *optional*, default to `3`): The kernel size of `conv_out` layer. + projection_class_embeddings_input_dim (`int`, *optional*): The dimension of the `class_labels` input when + `class_embed_type="projection"`. Required when `class_embed_type="projection"`. + class_embeddings_concat (`bool`, *optional*, defaults to `False`): Whether to concatenate the time + embeddings with the class embeddings. + mid_block_only_cross_attention (`bool`, *optional*, defaults to `None`): + Whether to use cross attention with the mid block when using the `UNetMidBlock2DSimpleCrossAttn`. If + `only_cross_attention` is given as a single boolean and `mid_block_only_cross_attention` is `None`, the + `only_cross_attention` value is used as the value for `mid_block_only_cross_attention`. Default to `False` + otherwise. + """ + + _supports_gradient_checkpointing = True + + @register_to_config + def __init__( + self, + sample_size: Optional[int] = None, + in_channels: int = 4, + out_channels: int = 4, + center_input_sample: bool = False, + flip_sin_to_cos: bool = True, + freq_shift: int = 0, + down_block_types: Tuple[str] = ( + "CrossAttnDownBlock2D", + "CrossAttnDownBlock2D", + "CrossAttnDownBlock2D", + "DownBlock2D", + ), + mid_block_type: Optional[str] = "UNetMidBlock2DCrossAttn", + up_block_types: Tuple[str] = ( + "UpBlock2D", "CrossAttnUpBlock2D", "CrossAttnUpBlock2D", "CrossAttnUpBlock2D"), + only_cross_attention: Union[bool, Tuple[bool]] = False, + block_out_channels: Tuple[int] = (320, 640, 1280, 1280), + layers_per_block: Union[int, Tuple[int]] = 2, + downsample_padding: int = 1, + mid_block_scale_factor: float = 1, + dropout: float = 0.0, + act_fn: str = "silu", + norm_num_groups: Optional[int] = 32, + norm_eps: float = 1e-5, + cross_attention_dim: Union[int, Tuple[int]] = 1280, + transformer_layers_per_block: Union[int, Tuple[int], Tuple[Tuple]] = 1, + reverse_transformer_layers_per_block: Optional[Tuple[Tuple[int]]] = None, + encoder_hid_dim: Optional[int] = None, + encoder_hid_dim_type: Optional[str] = None, + attention_head_dim: Union[int, Tuple[int]] = 8, + num_attention_heads: Optional[Union[int, Tuple[int]]] = None, + dual_cross_attention: bool = False, + use_linear_projection: bool = False, + class_embed_type: Optional[str] = None, + addition_embed_type: Optional[str] = None, + addition_time_embed_dim: Optional[int] = None, + num_class_embeds: Optional[int] = None, + upcast_attention: bool = False, + resnet_time_scale_shift: str = "default", + resnet_skip_time_act: bool = False, + resnet_out_scale_factor: float = 1.0, + time_embedding_type: str = "positional", + time_embedding_dim: Optional[int] = None, + time_embedding_act_fn: Optional[str] = None, + timestep_post_act: Optional[str] = None, + time_cond_proj_dim: Optional[int] = None, + conv_in_kernel: int = 3, + conv_out_kernel: int = 3, + projection_class_embeddings_input_dim: Optional[int] = None, + attention_type: str = "default", + class_embeddings_concat: bool = False, + mid_block_only_cross_attention: Optional[bool] = None, + cross_attention_norm: Optional[str] = None, + addition_embed_type_num_heads: int = 64, + ): + super().__init__() + + self.sample_size = sample_size + + if num_attention_heads is not None: + raise ValueError( + "At the moment it is not possible to define the number of attention heads via `num_attention_heads` because of a naming issue as described in https://github.com/huggingface/diffusers/issues/2011#issuecomment-1547958131. Passing `num_attention_heads` will only be supported in diffusers v0.19." + ) + + # If `num_attention_heads` is not defined (which is the case for most models) + # it will default to `attention_head_dim`. This looks weird upon first reading it and it is. + # The reason for this behavior is to correct for incorrectly named variables that were introduced + # when this library was created. The incorrect naming was only discovered much later in https://github.com/huggingface/diffusers/issues/2011#issuecomment-1547958131 + # Changing `attention_head_dim` to `num_attention_heads` for 40,000+ configurations is too backwards breaking + # which is why we correct for the naming here. + num_attention_heads = num_attention_heads or attention_head_dim + + # Check inputs + self._check_config( + down_block_types=down_block_types, + up_block_types=up_block_types, + only_cross_attention=only_cross_attention, + block_out_channels=block_out_channels, + layers_per_block=layers_per_block, + cross_attention_dim=cross_attention_dim, + transformer_layers_per_block=transformer_layers_per_block, + reverse_transformer_layers_per_block=reverse_transformer_layers_per_block, + attention_head_dim=attention_head_dim, + num_attention_heads=num_attention_heads, + ) + + # input + conv_in_padding = (conv_in_kernel - 1) // 2 + self.conv_in = nn.Conv2d( + in_channels, block_out_channels[0], kernel_size=conv_in_kernel, padding=conv_in_padding + ) + + # time + time_embed_dim, timestep_input_dim = self._set_time_proj( + time_embedding_type, + block_out_channels=block_out_channels, + flip_sin_to_cos=flip_sin_to_cos, + freq_shift=freq_shift, + time_embedding_dim=time_embedding_dim, + ) + + self.time_embedding = TimestepEmbedding( + timestep_input_dim, + time_embed_dim, + act_fn=act_fn, + post_act_fn=timestep_post_act, + cond_proj_dim=time_cond_proj_dim, + ) + + self._set_encoder_hid_proj( + encoder_hid_dim_type, + cross_attention_dim=cross_attention_dim, + encoder_hid_dim=encoder_hid_dim, + ) + + # class embedding + self._set_class_embedding( + class_embed_type, + act_fn=act_fn, + num_class_embeds=num_class_embeds, + projection_class_embeddings_input_dim=projection_class_embeddings_input_dim, + time_embed_dim=time_embed_dim, + timestep_input_dim=timestep_input_dim, + ) + + self._set_add_embedding( + addition_embed_type, + addition_embed_type_num_heads=addition_embed_type_num_heads, + addition_time_embed_dim=addition_time_embed_dim, + cross_attention_dim=cross_attention_dim, + encoder_hid_dim=encoder_hid_dim, + flip_sin_to_cos=flip_sin_to_cos, + freq_shift=freq_shift, + projection_class_embeddings_input_dim=projection_class_embeddings_input_dim, + time_embed_dim=time_embed_dim, + ) + + if time_embedding_act_fn is None: + self.time_embed_act = None + else: + self.time_embed_act = get_activation(time_embedding_act_fn) + + self.down_blocks = nn.ModuleList([]) + self.up_blocks = nn.ModuleList([]) + + if isinstance(only_cross_attention, bool): + if mid_block_only_cross_attention is None: + mid_block_only_cross_attention = only_cross_attention + + only_cross_attention = [only_cross_attention] * len(down_block_types) + + if mid_block_only_cross_attention is None: + mid_block_only_cross_attention = False + + if isinstance(num_attention_heads, int): + num_attention_heads = (num_attention_heads,) * len(down_block_types) + + if isinstance(attention_head_dim, int): + attention_head_dim = (attention_head_dim,) * len(down_block_types) + + if isinstance(cross_attention_dim, int): + cross_attention_dim = (cross_attention_dim,) * len(down_block_types) + + if isinstance(layers_per_block, int): + layers_per_block = [layers_per_block] * len(down_block_types) + + if isinstance(transformer_layers_per_block, int): + transformer_layers_per_block = [transformer_layers_per_block] * len(down_block_types) + + if class_embeddings_concat: + # The time embeddings are concatenated with the class embeddings. The dimension of the + # time embeddings passed to the down, middle, and up blocks is twice the dimension of the + # regular time embeddings + blocks_time_embed_dim = time_embed_dim * 2 + else: + blocks_time_embed_dim = time_embed_dim + + # down + output_channel = block_out_channels[0] + for i, down_block_type in enumerate(down_block_types): + input_channel = output_channel + output_channel = block_out_channels[i] + is_final_block = i == len(block_out_channels) - 1 + + down_block = get_down_block( + down_block_type, + num_layers=layers_per_block[i], + transformer_layers_per_block=transformer_layers_per_block[i], + in_channels=input_channel, + out_channels=output_channel, + temb_channels=blocks_time_embed_dim, + add_downsample=not is_final_block, + resnet_eps=norm_eps, + resnet_act_fn=act_fn, + resnet_groups=norm_num_groups, + cross_attention_dim=cross_attention_dim[i], + num_attention_heads=num_attention_heads[i], + downsample_padding=downsample_padding, + dual_cross_attention=dual_cross_attention, + use_linear_projection=use_linear_projection, + only_cross_attention=only_cross_attention[i], + upcast_attention=upcast_attention, + resnet_time_scale_shift=resnet_time_scale_shift, + attention_type=attention_type, + resnet_skip_time_act=resnet_skip_time_act, + resnet_out_scale_factor=resnet_out_scale_factor, + cross_attention_norm=cross_attention_norm, + attention_head_dim=attention_head_dim[i] if attention_head_dim[i] is not None else output_channel, + dropout=dropout, + ) + self.down_blocks.append(down_block) + + # mid + self.mid_block = get_mid_block( + mid_block_type, + temb_channels=blocks_time_embed_dim, + in_channels=block_out_channels[-1], + resnet_eps=norm_eps, + resnet_act_fn=act_fn, + resnet_groups=norm_num_groups, + output_scale_factor=mid_block_scale_factor, + transformer_layers_per_block=transformer_layers_per_block[-1], + num_attention_heads=num_attention_heads[-1], + cross_attention_dim=cross_attention_dim[-1], + dual_cross_attention=dual_cross_attention, + use_linear_projection=use_linear_projection, + mid_block_only_cross_attention=mid_block_only_cross_attention, + upcast_attention=upcast_attention, + resnet_time_scale_shift=resnet_time_scale_shift, + attention_type=attention_type, + resnet_skip_time_act=resnet_skip_time_act, + cross_attention_norm=cross_attention_norm, + attention_head_dim=attention_head_dim[-1], + dropout=dropout, + ) + + # count how many layers upsample the images + self.num_upsamplers = 0 + + # up + reversed_block_out_channels = list(reversed(block_out_channels)) + reversed_num_attention_heads = list(reversed(num_attention_heads)) + reversed_layers_per_block = list(reversed(layers_per_block)) + reversed_cross_attention_dim = list(reversed(cross_attention_dim)) + reversed_transformer_layers_per_block = ( + list(reversed(transformer_layers_per_block)) + if reverse_transformer_layers_per_block is None + else reverse_transformer_layers_per_block + ) + only_cross_attention = list(reversed(only_cross_attention)) + + output_channel = reversed_block_out_channels[0] + for i, up_block_type in enumerate(up_block_types): + is_final_block = i == len(block_out_channels) - 1 + + prev_output_channel = output_channel + output_channel = reversed_block_out_channels[i] + input_channel = reversed_block_out_channels[min(i + 1, len(block_out_channels) - 1)] + + # add upsample block for all BUT final layer + if not is_final_block: + add_upsample = True + self.num_upsamplers += 1 + else: + add_upsample = False + + up_block = get_up_block( + up_block_type, + num_layers=reversed_layers_per_block[i] + 1, + transformer_layers_per_block=reversed_transformer_layers_per_block[i], + in_channels=input_channel, + out_channels=output_channel, + prev_output_channel=prev_output_channel, + temb_channels=blocks_time_embed_dim, + add_upsample=add_upsample, + resnet_eps=norm_eps, + resnet_act_fn=act_fn, + resolution_idx=i, + resnet_groups=norm_num_groups, + cross_attention_dim=reversed_cross_attention_dim[i], + num_attention_heads=reversed_num_attention_heads[i], + dual_cross_attention=dual_cross_attention, + use_linear_projection=use_linear_projection, + only_cross_attention=only_cross_attention[i], + upcast_attention=upcast_attention, + resnet_time_scale_shift=resnet_time_scale_shift, + attention_type=attention_type, + resnet_skip_time_act=resnet_skip_time_act, + resnet_out_scale_factor=resnet_out_scale_factor, + cross_attention_norm=cross_attention_norm, + attention_head_dim=attention_head_dim[i] if attention_head_dim[i] is not None else output_channel, + dropout=dropout, + ) + self.up_blocks.append(up_block) + prev_output_channel = output_channel + + # out + if norm_num_groups is not None: + self.conv_norm_out = nn.GroupNorm( + num_channels=block_out_channels[0], num_groups=norm_num_groups, eps=norm_eps + ) + + self.conv_act = get_activation(act_fn) + + else: + self.conv_norm_out = None + self.conv_act = None + + conv_out_padding = (conv_out_kernel - 1) // 2 + self.conv_out = nn.Conv2d( + block_out_channels[0], out_channels, kernel_size=conv_out_kernel, padding=conv_out_padding + ) + + self._set_pos_net_if_use_gligen(attention_type=attention_type, cross_attention_dim=cross_attention_dim) + + def _check_config( + self, + down_block_types: Tuple[str], + up_block_types: Tuple[str], + only_cross_attention: Union[bool, Tuple[bool]], + block_out_channels: Tuple[int], + layers_per_block: Union[int, Tuple[int]], + cross_attention_dim: Union[int, Tuple[int]], + transformer_layers_per_block: Union[int, Tuple[int], Tuple[Tuple[int]]], + reverse_transformer_layers_per_block: bool, + attention_head_dim: int, + num_attention_heads: Optional[Union[int, Tuple[int]]], + ): + if len(down_block_types) != len(up_block_types): + raise ValueError( + f"Must provide the same number of `down_block_types` as `up_block_types`. `down_block_types`: {down_block_types}. `up_block_types`: {up_block_types}." + ) + + if len(block_out_channels) != len(down_block_types): + raise ValueError( + f"Must provide the same number of `block_out_channels` as `down_block_types`. `block_out_channels`: {block_out_channels}. `down_block_types`: {down_block_types}." + ) + + if not isinstance(only_cross_attention, bool) and len(only_cross_attention) != len(down_block_types): + raise ValueError( + f"Must provide the same number of `only_cross_attention` as `down_block_types`. `only_cross_attention`: {only_cross_attention}. `down_block_types`: {down_block_types}." + ) + + if not isinstance(num_attention_heads, int) and len(num_attention_heads) != len(down_block_types): + raise ValueError( + f"Must provide the same number of `num_attention_heads` as `down_block_types`. `num_attention_heads`: {num_attention_heads}. `down_block_types`: {down_block_types}." + ) + + if not isinstance(attention_head_dim, int) and len(attention_head_dim) != len(down_block_types): + raise ValueError( + f"Must provide the same number of `attention_head_dim` as `down_block_types`. `attention_head_dim`: {attention_head_dim}. `down_block_types`: {down_block_types}." + ) + + if isinstance(cross_attention_dim, list) and len(cross_attention_dim) != len(down_block_types): + raise ValueError( + f"Must provide the same number of `cross_attention_dim` as `down_block_types`. `cross_attention_dim`: {cross_attention_dim}. `down_block_types`: {down_block_types}." + ) + + if not isinstance(layers_per_block, int) and len(layers_per_block) != len(down_block_types): + raise ValueError( + f"Must provide the same number of `layers_per_block` as `down_block_types`. `layers_per_block`: {layers_per_block}. `down_block_types`: {down_block_types}." + ) + if isinstance(transformer_layers_per_block, list) and reverse_transformer_layers_per_block is None: + for layer_number_per_block in transformer_layers_per_block: + if isinstance(layer_number_per_block, list): + raise ValueError("Must provide 'reverse_transformer_layers_per_block` if using asymmetrical UNet.") + + def _set_time_proj( + self, + time_embedding_type: str, + block_out_channels: int, + flip_sin_to_cos: bool, + freq_shift: float, + time_embedding_dim: int, + ) -> Tuple[int, int]: + if time_embedding_type == "fourier": + time_embed_dim = time_embedding_dim or block_out_channels[0] * 2 + if time_embed_dim % 2 != 0: + raise ValueError(f"`time_embed_dim` should be divisible by 2, but is {time_embed_dim}.") + self.time_proj = GaussianFourierProjection( + time_embed_dim // 2, set_W_to_weight=False, log=False, flip_sin_to_cos=flip_sin_to_cos + ) + timestep_input_dim = time_embed_dim + elif time_embedding_type == "positional": + time_embed_dim = time_embedding_dim or block_out_channels[0] * 4 + + self.time_proj = Timesteps(block_out_channels[0], flip_sin_to_cos, freq_shift) + timestep_input_dim = block_out_channels[0] + else: + raise ValueError( + f"{time_embedding_type} does not exist. Please make sure to use one of `fourier` or `positional`." + ) + + return time_embed_dim, timestep_input_dim + + def _set_encoder_hid_proj( + self, + encoder_hid_dim_type: Optional[str], + cross_attention_dim: Union[int, Tuple[int]], + encoder_hid_dim: Optional[int], + ): + if encoder_hid_dim_type is None and encoder_hid_dim is not None: + encoder_hid_dim_type = "text_proj" + self.register_to_config(encoder_hid_dim_type=encoder_hid_dim_type) + logger.info("encoder_hid_dim_type defaults to 'text_proj' as `encoder_hid_dim` is defined.") + + if encoder_hid_dim is None and encoder_hid_dim_type is not None: + raise ValueError( + f"`encoder_hid_dim` has to be defined when `encoder_hid_dim_type` is set to {encoder_hid_dim_type}." + ) + + if encoder_hid_dim_type == "text_proj": + self.encoder_hid_proj = nn.Linear(encoder_hid_dim, cross_attention_dim) + elif encoder_hid_dim_type == "text_image_proj": + # image_embed_dim DOESN'T have to be `cross_attention_dim`. To not clutter the __init__ too much + # they are set to `cross_attention_dim` here as this is exactly the required dimension for the currently only use + # case when `addition_embed_type == "text_image_proj"` (Kandinsky 2.1)` + self.encoder_hid_proj = TextImageProjection( + text_embed_dim=encoder_hid_dim, + image_embed_dim=cross_attention_dim, + cross_attention_dim=cross_attention_dim, + ) + elif encoder_hid_dim_type == "image_proj": + # Kandinsky 2.2 + self.encoder_hid_proj = ImageProjection( + image_embed_dim=encoder_hid_dim, + cross_attention_dim=cross_attention_dim, + ) + elif encoder_hid_dim_type is not None: + raise ValueError( + f"encoder_hid_dim_type: {encoder_hid_dim_type} must be None, 'text_proj' or 'text_image_proj'." + ) + else: + self.encoder_hid_proj = None + + def _set_class_embedding( + self, + class_embed_type: Optional[str], + act_fn: str, + num_class_embeds: Optional[int], + projection_class_embeddings_input_dim: Optional[int], + time_embed_dim: int, + timestep_input_dim: int, + ): + if class_embed_type is None and num_class_embeds is not None: + self.class_embedding = nn.Embedding(num_class_embeds, time_embed_dim) + elif class_embed_type == "timestep": + self.class_embedding = TimestepEmbedding(timestep_input_dim, time_embed_dim, act_fn=act_fn) + elif class_embed_type == "identity": + self.class_embedding = nn.Identity(time_embed_dim, time_embed_dim) + elif class_embed_type == "projection": + if projection_class_embeddings_input_dim is None: + raise ValueError( + "`class_embed_type`: 'projection' requires `projection_class_embeddings_input_dim` be set" + ) + # The projection `class_embed_type` is the same as the timestep `class_embed_type` except + # 1. the `class_labels` inputs are not first converted to sinusoidal embeddings + # 2. it projects from an arbitrary input dimension. + # + # Note that `TimestepEmbedding` is quite general, being mainly linear layers and activations. + # When used for embedding actual timesteps, the timesteps are first converted to sinusoidal embeddings. + # As a result, `TimestepEmbedding` can be passed arbitrary vectors. + self.class_embedding = TimestepEmbedding(projection_class_embeddings_input_dim, time_embed_dim) + elif class_embed_type == "simple_projection": + if projection_class_embeddings_input_dim is None: + raise ValueError( + "`class_embed_type`: 'simple_projection' requires `projection_class_embeddings_input_dim` be set" + ) + self.class_embedding = nn.Linear(projection_class_embeddings_input_dim, time_embed_dim) + else: + self.class_embedding = None + + def _set_add_embedding( + self, + addition_embed_type: str, + addition_embed_type_num_heads: int, + addition_time_embed_dim: Optional[int], + flip_sin_to_cos: bool, + freq_shift: float, + cross_attention_dim: Optional[int], + encoder_hid_dim: Optional[int], + projection_class_embeddings_input_dim: Optional[int], + time_embed_dim: int, + ): + if addition_embed_type == "text": + if encoder_hid_dim is not None: + text_time_embedding_from_dim = encoder_hid_dim + else: + text_time_embedding_from_dim = cross_attention_dim + + self.add_embedding = TextTimeEmbedding( + text_time_embedding_from_dim, time_embed_dim, num_heads=addition_embed_type_num_heads + ) + elif addition_embed_type == "text_image": + # text_embed_dim and image_embed_dim DON'T have to be `cross_attention_dim`. To not clutter the __init__ too much + # they are set to `cross_attention_dim` here as this is exactly the required dimension for the currently only use + # case when `addition_embed_type == "text_image"` (Kandinsky 2.1)` + self.add_embedding = TextImageTimeEmbedding( + text_embed_dim=cross_attention_dim, image_embed_dim=cross_attention_dim, time_embed_dim=time_embed_dim + ) + elif addition_embed_type == "text_time": + self.add_time_proj = Timesteps(addition_time_embed_dim, flip_sin_to_cos, freq_shift) + self.add_embedding = TimestepEmbedding(projection_class_embeddings_input_dim, time_embed_dim) + elif addition_embed_type == "image": + # Kandinsky 2.2 + self.add_embedding = ImageTimeEmbedding(image_embed_dim=encoder_hid_dim, time_embed_dim=time_embed_dim) + elif addition_embed_type == "image_hint": + # Kandinsky 2.2 ControlNet + self.add_embedding = ImageHintTimeEmbedding(image_embed_dim=encoder_hid_dim, time_embed_dim=time_embed_dim) + elif addition_embed_type is not None: + raise ValueError(f"addition_embed_type: {addition_embed_type} must be None, 'text' or 'text_image'.") + + def _set_pos_net_if_use_gligen(self, attention_type: str, cross_attention_dim: int): + if attention_type in ["gated", "gated-text-image"]: + positive_len = 768 + if isinstance(cross_attention_dim, int): + positive_len = cross_attention_dim + elif isinstance(cross_attention_dim, tuple) or isinstance(cross_attention_dim, list): + positive_len = cross_attention_dim[0] + + feature_type = "text-only" if attention_type == "gated" else "text-image" + self.position_net = GLIGENTextBoundingboxProjection( + positive_len=positive_len, out_dim=cross_attention_dim, feature_type=feature_type + ) + + @property + def attn_processors(self) -> Dict[str, AttentionProcessor]: + r""" + Returns: + `dict` of attention processors: A dictionary containing all attention processors used in the model with + indexed by its weight name. + """ + # set recursively + processors = {} + + def fn_recursive_add_processors(name: str, module: torch.nn.Module, processors: Dict[str, AttentionProcessor]): + if hasattr(module, "get_processor"): + processors[f"{name}.processor"] = module.get_processor(return_deprecated_lora=True) + + for sub_name, child in module.named_children(): + fn_recursive_add_processors(f"{name}.{sub_name}", child, processors) + + return processors + + for name, module in self.named_children(): + fn_recursive_add_processors(name, module, processors) + + return processors + + def set_attn_processor(self, processor: Union[AttentionProcessor, Dict[str, AttentionProcessor]]): + r""" + Sets the attention processor to use to compute attention. + + Parameters: + processor (`dict` of `AttentionProcessor` or only `AttentionProcessor`): + The instantiated processor class or a dictionary of processor classes that will be set as the processor + for **all** `Attention` layers. + + If `processor` is a dict, the key needs to define the path to the corresponding cross attention + processor. This is strongly recommended when setting trainable attention processors. + + """ + count = len(self.attn_processors.keys()) + + if isinstance(processor, dict) and len(processor) != count: + raise ValueError( + f"A dict of processors was passed, but the number of processors {len(processor)} does not match the" + f" number of attention layers: {count}. Please make sure to pass {count} processor classes." + ) + + def fn_recursive_attn_processor(name: str, module: torch.nn.Module, processor): + if hasattr(module, "set_processor"): + if not isinstance(processor, dict): + module.set_processor(processor) + else: + module.set_processor(processor.pop(f"{name}.processor")) + + for sub_name, child in module.named_children(): + fn_recursive_attn_processor(f"{name}.{sub_name}", child, processor) + + for name, module in self.named_children(): + fn_recursive_attn_processor(name, module, processor) + + def set_default_attn_processor(self): + """ + Disables custom attention processors and sets the default attention implementation. + """ + if all(proc.__class__ in ADDED_KV_ATTENTION_PROCESSORS for proc in self.attn_processors.values()): + processor = AttnAddedKVProcessor() + elif all(proc.__class__ in CROSS_ATTENTION_PROCESSORS for proc in self.attn_processors.values()): + processor = AttnProcessor() + else: + raise ValueError( + f"Cannot call `set_default_attn_processor` when attention processors are of type {next(iter(self.attn_processors.values()))}" + ) + + self.set_attn_processor(processor) + + def set_attention_slice(self, slice_size: Union[str, int, List[int]] = "auto"): + r""" + Enable sliced attention computation. + + When this option is enabled, the attention module splits the input tensor in slices to compute attention in + several steps. This is useful for saving some memory in exchange for a small decrease in speed. + + Args: + slice_size (`str` or `int` or `list(int)`, *optional*, defaults to `"auto"`): + When `"auto"`, input to the attention heads is halved, so attention is computed in two steps. If + `"max"`, maximum amount of memory is saved by running only one slice at a time. If a number is + provided, uses as many slices as `attention_head_dim // slice_size`. In this case, `attention_head_dim` + must be a multiple of `slice_size`. + """ + sliceable_head_dims = [] + + def fn_recursive_retrieve_sliceable_dims(module: torch.nn.Module): + if hasattr(module, "set_attention_slice"): + sliceable_head_dims.append(module.sliceable_head_dim) + + for child in module.children(): + fn_recursive_retrieve_sliceable_dims(child) + + # retrieve number of attention layers + for module in self.children(): + fn_recursive_retrieve_sliceable_dims(module) + + num_sliceable_layers = len(sliceable_head_dims) + + if slice_size == "auto": + # half the attention head size is usually a good trade-off between + # speed and memory + slice_size = [dim // 2 for dim in sliceable_head_dims] + elif slice_size == "max": + # make smallest slice possible + slice_size = num_sliceable_layers * [1] + + slice_size = num_sliceable_layers * [slice_size] if not isinstance(slice_size, list) else slice_size + + if len(slice_size) != len(sliceable_head_dims): + raise ValueError( + f"You have provided {len(slice_size)}, but {self.config} has {len(sliceable_head_dims)} different" + f" attention layers. Make sure to match `len(slice_size)` to be {len(sliceable_head_dims)}." + ) + + for i in range(len(slice_size)): + size = slice_size[i] + dim = sliceable_head_dims[i] + if size is not None and size > dim: + raise ValueError(f"size {size} has to be smaller or equal to {dim}.") + + # Recursively walk through all the children. + # Any children which exposes the set_attention_slice method + # gets the message + def fn_recursive_set_attention_slice(module: torch.nn.Module, slice_size: List[int]): + if hasattr(module, "set_attention_slice"): + module.set_attention_slice(slice_size.pop()) + + for child in module.children(): + fn_recursive_set_attention_slice(child, slice_size) + + reversed_slice_size = list(reversed(slice_size)) + for module in self.children(): + fn_recursive_set_attention_slice(module, reversed_slice_size) + + def _set_gradient_checkpointing(self, module, value=False): + if hasattr(module, "gradient_checkpointing"): + module.gradient_checkpointing = value + + def enable_freeu(self, s1: float, s2: float, b1: float, b2: float): + r"""Enables the FreeU mechanism from https://arxiv.org/abs/2309.11497. + + The suffixes after the scaling factors represent the stage blocks where they are being applied. + + Please refer to the [official repository](https://github.com/ChenyangSi/FreeU) for combinations of values that + are known to work well for different pipelines such as Stable Diffusion v1, v2, and Stable Diffusion XL. + + Args: + s1 (`float`): + Scaling factor for stage 1 to attenuate the contributions of the skip features. This is done to + mitigate the "oversmoothing effect" in the enhanced denoising process. + s2 (`float`): + Scaling factor for stage 2 to attenuate the contributions of the skip features. This is done to + mitigate the "oversmoothing effect" in the enhanced denoising process. + b1 (`float`): Scaling factor for stage 1 to amplify the contributions of backbone features. + b2 (`float`): Scaling factor for stage 2 to amplify the contributions of backbone features. + """ + for i, upsample_block in enumerate(self.up_blocks): + setattr(upsample_block, "s1", s1) + setattr(upsample_block, "s2", s2) + setattr(upsample_block, "b1", b1) + setattr(upsample_block, "b2", b2) + + def disable_freeu(self): + """Disables the FreeU mechanism.""" + freeu_keys = {"s1", "s2", "b1", "b2"} + for i, upsample_block in enumerate(self.up_blocks): + for k in freeu_keys: + if hasattr(upsample_block, k) or getattr(upsample_block, k, None) is not None: + setattr(upsample_block, k, None) + + def fuse_qkv_projections(self): + """ + Enables fused QKV projections. For self-attention modules, all projection matrices (i.e., query, key, value) + are fused. For cross-attention modules, key and value projection matrices are fused. + + + + This API is 🧪 experimental. + + + """ + self.original_attn_processors = None + + for _, attn_processor in self.attn_processors.items(): + if "Added" in str(attn_processor.__class__.__name__): + raise ValueError("`fuse_qkv_projections()` is not supported for models having added KV projections.") + + self.original_attn_processors = self.attn_processors + + for module in self.modules(): + if isinstance(module, Attention): + module.fuse_projections(fuse=True) + + def unfuse_qkv_projections(self): + """Disables the fused QKV projection if enabled. + + + + This API is 🧪 experimental. + + + + """ + if self.original_attn_processors is not None: + self.set_attn_processor(self.original_attn_processors) + + def unload_lora(self): + """Unloads LoRA weights.""" + deprecate( + "unload_lora", + "0.28.0", + "Calling `unload_lora()` is deprecated and will be removed in a future version. Please install `peft` and then call `disable_adapters().", + ) + for module in self.modules(): + if hasattr(module, "set_lora_layer"): + module.set_lora_layer(None) + + def get_time_embed( + self, sample: torch.Tensor, timestep: Union[torch.Tensor, float, int] + ) -> Optional[torch.Tensor]: + timesteps = timestep + if not torch.is_tensor(timesteps): + # TODO: this requires sync between CPU and GPU. So try to pass timesteps as tensors if you can + # This would be a good case for the `match` statement (Python 3.10+) + is_mps = sample.device.type == "mps" + if isinstance(timestep, float): + dtype = torch.float32 if is_mps else torch.float64 + else: + dtype = torch.int32 if is_mps else torch.int64 + timesteps = torch.tensor([timesteps], dtype=dtype, device=sample.device) + elif len(timesteps.shape) == 0: + timesteps = timesteps[None].to(sample.device) + + # broadcast to batch dimension in a way that's compatible with ONNX/Core ML + timesteps = timesteps.expand(sample.shape[0]) + + t_emb = self.time_proj(timesteps) + # `Timesteps` does not contain any weights and will always return f32 tensors + # but time_embedding might actually be running in fp16. so we need to cast here. + # there might be better ways to encapsulate this. + t_emb = t_emb.to(dtype=sample.dtype) + return t_emb + + def get_class_embed(self, sample: torch.Tensor, class_labels: Optional[torch.Tensor]) -> Optional[torch.Tensor]: + class_emb = None + if self.class_embedding is not None: + if class_labels is None: + raise ValueError("class_labels should be provided when num_class_embeds > 0") + + if self.config.class_embed_type == "timestep": + class_labels = self.time_proj(class_labels) + + # `Timesteps` does not contain any weights and will always return f32 tensors + # there might be better ways to encapsulate this. + class_labels = class_labels.to(dtype=sample.dtype) + + class_emb = self.class_embedding(class_labels).to(dtype=sample.dtype) + return class_emb + + def get_aug_embed( + self, emb: torch.Tensor, encoder_hidden_states: torch.Tensor, added_cond_kwargs: Dict[str, Any] + ) -> Optional[torch.Tensor]: + aug_emb = None + if self.config.addition_embed_type == "text": + aug_emb = self.add_embedding(encoder_hidden_states) + elif self.config.addition_embed_type == "text_image": + # Kandinsky 2.1 - style + if "image_embeds" not in added_cond_kwargs: + raise ValueError( + f"{self.__class__} has the config param `addition_embed_type` set to 'text_image' which requires the keyword argument `image_embeds` to be passed in `added_cond_kwargs`" + ) + + image_embs = added_cond_kwargs.get("image_embeds") + text_embs = added_cond_kwargs.get("text_embeds", encoder_hidden_states) + aug_emb = self.add_embedding(text_embs, image_embs) + elif self.config.addition_embed_type == "text_time": + # SDXL - style + if "text_embeds" not in added_cond_kwargs: + raise ValueError( + f"{self.__class__} has the config param `addition_embed_type` set to 'text_time' which requires the keyword argument `text_embeds` to be passed in `added_cond_kwargs`" + ) + text_embeds = added_cond_kwargs.get("text_embeds") + if "time_ids" not in added_cond_kwargs: + raise ValueError( + f"{self.__class__} has the config param `addition_embed_type` set to 'text_time' which requires the keyword argument `time_ids` to be passed in `added_cond_kwargs`" + ) + time_ids = added_cond_kwargs.get("time_ids") + time_embeds = self.add_time_proj(time_ids.flatten()) + time_embeds = time_embeds.reshape((text_embeds.shape[0], -1)) + add_embeds = torch.concat([text_embeds, time_embeds], dim=-1) + add_embeds = add_embeds.to(emb.dtype) + aug_emb = self.add_embedding(add_embeds) + elif self.config.addition_embed_type == "image": + # Kandinsky 2.2 - style + if "image_embeds" not in added_cond_kwargs: + raise ValueError( + f"{self.__class__} has the config param `addition_embed_type` set to 'image' which requires the keyword argument `image_embeds` to be passed in `added_cond_kwargs`" + ) + image_embs = added_cond_kwargs.get("image_embeds") + aug_emb = self.add_embedding(image_embs) + elif self.config.addition_embed_type == "image_hint": + # Kandinsky 2.2 - style + if "image_embeds" not in added_cond_kwargs or "hint" not in added_cond_kwargs: + raise ValueError( + f"{self.__class__} has the config param `addition_embed_type` set to 'image_hint' which requires the keyword arguments `image_embeds` and `hint` to be passed in `added_cond_kwargs`" + ) + image_embs = added_cond_kwargs.get("image_embeds") + hint = added_cond_kwargs.get("hint") + aug_emb = self.add_embedding(image_embs, hint) + return aug_emb + + def process_encoder_hidden_states( + self, encoder_hidden_states: torch.Tensor, added_cond_kwargs: Dict[str, Any] + ) -> torch.Tensor: + if self.encoder_hid_proj is not None and self.config.encoder_hid_dim_type == "text_proj": + encoder_hidden_states = self.encoder_hid_proj(encoder_hidden_states) + elif self.encoder_hid_proj is not None and self.config.encoder_hid_dim_type == "text_image_proj": + # Kandinsky 2.1 - style + if "image_embeds" not in added_cond_kwargs: + raise ValueError( + f"{self.__class__} has the config param `encoder_hid_dim_type` set to 'text_image_proj' which requires the keyword argument `image_embeds` to be passed in `added_conditions`" + ) + + image_embeds = added_cond_kwargs.get("image_embeds") + encoder_hidden_states = self.encoder_hid_proj(encoder_hidden_states, image_embeds) + elif self.encoder_hid_proj is not None and self.config.encoder_hid_dim_type == "image_proj": + # Kandinsky 2.2 - style + if "image_embeds" not in added_cond_kwargs: + raise ValueError( + f"{self.__class__} has the config param `encoder_hid_dim_type` set to 'image_proj' which requires the keyword argument `image_embeds` to be passed in `added_conditions`" + ) + image_embeds = added_cond_kwargs.get("image_embeds") + encoder_hidden_states = self.encoder_hid_proj(image_embeds) + elif self.encoder_hid_proj is not None and self.config.encoder_hid_dim_type == "ip_image_proj": + if "image_embeds" not in added_cond_kwargs: + raise ValueError( + f"{self.__class__} has the config param `encoder_hid_dim_type` set to 'ip_image_proj' which requires the keyword argument `image_embeds` to be passed in `added_conditions`" + ) + image_embeds = added_cond_kwargs.get("image_embeds") + image_embeds = self.encoder_hid_proj(image_embeds) + encoder_hidden_states = (encoder_hidden_states, image_embeds) + return encoder_hidden_states + + def forward( + self, + sample: torch.FloatTensor, + timestep: Union[torch.Tensor, float, int], + encoder_hidden_states: torch.Tensor, + class_labels: Optional[torch.Tensor] = None, + timestep_cond: Optional[torch.Tensor] = None, + attention_mask: Optional[torch.Tensor] = None, + cross_attention_kwargs: Optional[Dict[str, Any]] = None, + added_cond_kwargs: Optional[Dict[str, torch.Tensor]] = None, + down_block_additional_residuals: Optional[Tuple[torch.Tensor]] = None, + mid_block_additional_residual: Optional[torch.Tensor] = None, + down_intrablock_additional_residuals: Optional[Tuple[torch.Tensor]] = None, + encoder_attention_mask: Optional[torch.Tensor] = None, + return_dict: bool = True, + down_block_add_samples: Optional[Tuple[torch.Tensor]] = None, + mid_block_add_sample: Optional[Tuple[torch.Tensor]] = None, + up_block_add_samples: Optional[Tuple[torch.Tensor]] = None, + ) -> Union[UNet2DConditionOutput, Tuple]: + r""" + The [`UNet2DConditionModel`] forward method. + + Args: + sample (`torch.FloatTensor`): + The noisy input tensor with the following shape `(batch, channel, height, width)`. + timestep (`torch.FloatTensor` or `float` or `int`): The number of timesteps to denoise an input. + encoder_hidden_states (`torch.FloatTensor`): + The encoder hidden states with shape `(batch, sequence_length, feature_dim)`. + class_labels (`torch.Tensor`, *optional*, defaults to `None`): + Optional class labels for conditioning. Their embeddings will be summed with the timestep embeddings. + timestep_cond: (`torch.Tensor`, *optional*, defaults to `None`): + Conditional embeddings for timestep. If provided, the embeddings will be summed with the samples passed + through the `self.time_embedding` layer to obtain the timestep embeddings. + attention_mask (`torch.Tensor`, *optional*, defaults to `None`): + An attention mask of shape `(batch, key_tokens)` is applied to `encoder_hidden_states`. If `1` the mask + is kept, otherwise if `0` it is discarded. Mask will be converted into a bias, which adds large + negative values to the attention scores corresponding to "discard" tokens. + cross_attention_kwargs (`dict`, *optional*): + A kwargs dictionary that if specified is passed along to the `AttentionProcessor` as defined under + `self.processor` in + [diffusers.models.attention_processor](https://github.com/huggingface/diffusers/blob/main/src/diffusers/models/attention_processor.py). + added_cond_kwargs: (`dict`, *optional*): + A kwargs dictionary containing additional embeddings that if specified are added to the embeddings that + are passed along to the UNet blocks. + down_block_additional_residuals: (`tuple` of `torch.Tensor`, *optional*): + A tuple of tensors that if specified are added to the residuals of down unet blocks. + mid_block_additional_residual: (`torch.Tensor`, *optional*): + A tensor that if specified is added to the residual of the middle unet block. + down_intrablock_additional_residuals (`tuple` of `torch.Tensor`, *optional*): + additional residuals to be added within UNet down blocks, for example from T2I-Adapter side model(s) + encoder_attention_mask (`torch.Tensor`): + A cross-attention mask of shape `(batch, sequence_length)` is applied to `encoder_hidden_states`. If + `True` the mask is kept, otherwise if `False` it is discarded. Mask will be converted into a bias, + which adds large negative values to the attention scores corresponding to "discard" tokens. + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`~models.unets.unet_2d_condition.UNet2DConditionOutput`] instead of a plain + tuple. + + Returns: + [`~models.unets.unet_2d_condition.UNet2DConditionOutput`] or `tuple`: + If `return_dict` is True, an [`~models.unets.unet_2d_condition.UNet2DConditionOutput`] is returned, + otherwise a `tuple` is returned where the first element is the sample tensor. + """ + # By default samples have to be AT least a multiple of the overall upsampling factor. + # The overall upsampling factor is equal to 2 ** (# num of upsampling layers). + # However, the upsampling interpolation output size can be forced to fit any upsampling size + # on the fly if necessary. + default_overall_up_factor = 2 ** self.num_upsamplers + + # upsample size should be forwarded when sample is not a multiple of `default_overall_up_factor` + forward_upsample_size = False + upsample_size = None + + for dim in sample.shape[-2:]: + if dim % default_overall_up_factor != 0: + # Forward upsample size to force interpolation output size. + forward_upsample_size = True + break + + # ensure attention_mask is a bias, and give it a singleton query_tokens dimension + # expects mask of shape: + # [batch, key_tokens] + # adds singleton query_tokens dimension: + # [batch, 1, key_tokens] + # this helps to broadcast it as a bias over attention scores, which will be in one of the following shapes: + # [batch, heads, query_tokens, key_tokens] (e.g. torch sdp attn) + # [batch * heads, query_tokens, key_tokens] (e.g. xformers or classic attn) + if attention_mask is not None: + # assume that mask is expressed as: + # (1 = keep, 0 = discard) + # convert mask into a bias that can be added to attention scores: + # (keep = +0, discard = -10000.0) + attention_mask = (1 - attention_mask.to(sample.dtype)) * -10000.0 + attention_mask = attention_mask.unsqueeze(1) + + # convert encoder_attention_mask to a bias the same way we do for attention_mask + if encoder_attention_mask is not None: + encoder_attention_mask = (1 - encoder_attention_mask.to(sample.dtype)) * -10000.0 + encoder_attention_mask = encoder_attention_mask.unsqueeze(1) + + # 0. center input if necessary + if self.config.center_input_sample: + sample = 2 * sample - 1.0 + + # 1. time + t_emb = self.get_time_embed(sample=sample, timestep=timestep) + emb = self.time_embedding(t_emb, timestep_cond) + aug_emb = None + + class_emb = self.get_class_embed(sample=sample, class_labels=class_labels) + if class_emb is not None: + if self.config.class_embeddings_concat: + emb = torch.cat([emb, class_emb], dim=-1) + else: + emb = emb + class_emb + + aug_emb = self.get_aug_embed( + emb=emb, encoder_hidden_states=encoder_hidden_states, added_cond_kwargs=added_cond_kwargs + ) + if self.config.addition_embed_type == "image_hint": + aug_emb, hint = aug_emb + sample = torch.cat([sample, hint], dim=1) + + emb = emb + aug_emb if aug_emb is not None else emb + + if self.time_embed_act is not None: + emb = self.time_embed_act(emb) + + encoder_hidden_states = self.process_encoder_hidden_states( + encoder_hidden_states=encoder_hidden_states, added_cond_kwargs=added_cond_kwargs + ) + + # 2. pre-process + sample = self.conv_in(sample) + + # 2.5 GLIGEN position net + if cross_attention_kwargs is not None and cross_attention_kwargs.get("gligen", None) is not None: + cross_attention_kwargs = cross_attention_kwargs.copy() + gligen_args = cross_attention_kwargs.pop("gligen") + cross_attention_kwargs["gligen"] = {"objs": self.position_net(**gligen_args)} + + # 3. down + # we're popping the `scale` instead of getting it because otherwise `scale` will be propagated + # to the internal blocks and will raise deprecation warnings. this will be confusing for our users. + if cross_attention_kwargs is not None: + cross_attention_kwargs = cross_attention_kwargs.copy() + lora_scale = cross_attention_kwargs.pop("scale", 1.0) + else: + lora_scale = 1.0 + + if USE_PEFT_BACKEND: + # weight the lora layers by setting `lora_scale` for each PEFT layer + scale_lora_layers(self, lora_scale) + + is_controlnet = mid_block_additional_residual is not None and down_block_additional_residuals is not None + # using new arg down_intrablock_additional_residuals for T2I-Adapters, to distinguish from controlnets + is_adapter = down_intrablock_additional_residuals is not None + # maintain backward compatibility for legacy usage, where + # T2I-Adapter and ControlNet both use down_block_additional_residuals arg + # but can only use one or the other + is_brushnet = down_block_add_samples is not None and mid_block_add_sample is not None and up_block_add_samples is not None + if not is_adapter and mid_block_additional_residual is None and down_block_additional_residuals is not None: + deprecate( + "T2I should not use down_block_additional_residuals", + "1.3.0", + "Passing intrablock residual connections with `down_block_additional_residuals` is deprecated \ + and will be removed in diffusers 1.3.0. `down_block_additional_residuals` should only be used \ + for ControlNet. Please make sure use `down_intrablock_additional_residuals` instead. ", + standard_warn=False, + ) + down_intrablock_additional_residuals = down_block_additional_residuals + is_adapter = True + + down_block_res_samples = (sample,) + + if is_brushnet: + sample = sample + down_block_add_samples.pop(0) + + for downsample_block in self.down_blocks: + if hasattr(downsample_block, "has_cross_attention") and downsample_block.has_cross_attention: + # For t2i-adapter CrossAttnDownBlock2D + additional_residuals = {} + if is_adapter and len(down_intrablock_additional_residuals) > 0: + additional_residuals["additional_residuals"] = down_intrablock_additional_residuals.pop(0) + + i = len(down_block_add_samples) + + if is_brushnet and len(down_block_add_samples) > 0: + additional_residuals["down_block_add_samples"] = [down_block_add_samples.pop(0) + for _ in range( + len(downsample_block.resnets) + (downsample_block.downsamplers != None))] + + sample, res_samples = downsample_block( + hidden_states=sample, + temb=emb, + encoder_hidden_states=encoder_hidden_states, + attention_mask=attention_mask, + cross_attention_kwargs=cross_attention_kwargs, + encoder_attention_mask=encoder_attention_mask, + **additional_residuals, + ) + else: + additional_residuals = {} + + i = len(down_block_add_samples) + + if is_brushnet and len(down_block_add_samples) > 0: + additional_residuals["down_block_add_samples"] = [down_block_add_samples.pop(0) + for _ in range( + len(downsample_block.resnets) + (downsample_block.downsamplers != None))] + + sample, res_samples = downsample_block(hidden_states=sample, temb=emb, **additional_residuals) + if is_adapter and len(down_intrablock_additional_residuals) > 0: + sample += down_intrablock_additional_residuals.pop(0) + + down_block_res_samples += res_samples + + if is_controlnet: + new_down_block_res_samples = () + + for down_block_res_sample, down_block_additional_residual in zip( + down_block_res_samples, down_block_additional_residuals + ): + down_block_res_sample = down_block_res_sample + down_block_additional_residual + new_down_block_res_samples = new_down_block_res_samples + (down_block_res_sample,) + + down_block_res_samples = new_down_block_res_samples + + # 4. mid + if self.mid_block is not None: + if hasattr(self.mid_block, "has_cross_attention") and self.mid_block.has_cross_attention: + sample = self.mid_block( + sample, + emb, + encoder_hidden_states=encoder_hidden_states, + attention_mask=attention_mask, + cross_attention_kwargs=cross_attention_kwargs, + encoder_attention_mask=encoder_attention_mask, + ) + else: + sample = self.mid_block(sample, emb) + + # To support T2I-Adapter-XL + if ( + is_adapter + and len(down_intrablock_additional_residuals) > 0 + and sample.shape == down_intrablock_additional_residuals[0].shape + ): + sample += down_intrablock_additional_residuals.pop(0) + + if is_controlnet: + sample = sample + mid_block_additional_residual + + if is_brushnet: + sample = sample + mid_block_add_sample + + # 5. up + for i, upsample_block in enumerate(self.up_blocks): + is_final_block = i == len(self.up_blocks) - 1 + + res_samples = down_block_res_samples[-len(upsample_block.resnets):] + down_block_res_samples = down_block_res_samples[: -len(upsample_block.resnets)] + + # if we have not reached the final block and need to forward the + # upsample size, we do it here + if not is_final_block and forward_upsample_size: + upsample_size = down_block_res_samples[-1].shape[2:] + + if hasattr(upsample_block, "has_cross_attention") and upsample_block.has_cross_attention: + additional_residuals = {} + + i = len(up_block_add_samples) + + if is_brushnet and len(up_block_add_samples) > 0: + additional_residuals["up_block_add_samples"] = [up_block_add_samples.pop(0) + for _ in range( + len(upsample_block.resnets) + (upsample_block.upsamplers != None))] + + sample = upsample_block( + hidden_states=sample, + temb=emb, + res_hidden_states_tuple=res_samples, + encoder_hidden_states=encoder_hidden_states, + cross_attention_kwargs=cross_attention_kwargs, + upsample_size=upsample_size, + attention_mask=attention_mask, + encoder_attention_mask=encoder_attention_mask, + **additional_residuals, + ) + else: + additional_residuals = {} + + i = len(up_block_add_samples) + + if is_brushnet and len(up_block_add_samples) > 0: + additional_residuals["up_block_add_samples"] = [up_block_add_samples.pop(0) + for _ in range( + len(upsample_block.resnets) + (upsample_block.upsamplers != None))] + + sample = upsample_block( + hidden_states=sample, + temb=emb, + res_hidden_states_tuple=res_samples, + upsample_size=upsample_size, + **additional_residuals, + ) + + # 6. post-process + if self.conv_norm_out: + sample = self.conv_norm_out(sample) + sample = self.conv_act(sample) + sample = self.conv_out(sample) + + if USE_PEFT_BACKEND: + # remove `lora_scale` from each PEFT layer + unscale_lora_layers(self, lora_scale) + + if not return_dict: + return (sample,) + + return UNet2DConditionOutput(sample=sample) diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/dit/__init__.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/dit/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..329a7347f7cf662fbab366e11bd8f42a2cb14990 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/dit/__init__.py @@ -0,0 +1,2 @@ +#credit to city96 for this module +#from https://github.com/city96/ComfyUI_ExtraModels/ \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/dit/__pycache__/__init__.cpython-312.pyc b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/dit/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bcccec87a69315c9e483ab8190b8742f622f63b2 Binary files /dev/null and b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/dit/__pycache__/__init__.cpython-312.pyc differ diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/dit/config.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/dit/config.py new file mode 100644 index 0000000000000000000000000000000000000000..07d94105cccbeeb7ccef21864ea5523509e86b20 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/dit/config.py @@ -0,0 +1,120 @@ +""" +List of all DiT model types / settings +""" +sampling_settings = { + "beta_schedule" : "sqrt_linear", + "linear_start" : 0.0001, + "linear_end" : 0.02, + "timesteps" : 1000, +} + +dit_conf = { + "XL/2": { # DiT_XL_2 + "unet_config": { + "depth" : 28, + "num_heads" : 16, + "patch_size" : 2, + "hidden_size" : 1152, + }, + "sampling_settings" : sampling_settings, + }, + "XL/4": { # DiT_XL_4 + "unet_config": { + "depth" : 28, + "num_heads" : 16, + "patch_size" : 4, + "hidden_size" : 1152, + }, + "sampling_settings" : sampling_settings, + }, + "XL/8": { # DiT_XL_8 + "unet_config": { + "depth" : 28, + "num_heads" : 16, + "patch_size" : 8, + "hidden_size" : 1152, + }, + "sampling_settings" : sampling_settings, + }, + "L/2": { # DiT_L_2 + "unet_config": { + "depth" : 24, + "num_heads" : 16, + "patch_size" : 2, + "hidden_size" : 1024, + }, + "sampling_settings" : sampling_settings, + }, + "L/4": { # DiT_L_4 + "unet_config": { + "depth" : 24, + "num_heads" : 16, + "patch_size" : 4, + "hidden_size" : 1024, + }, + "sampling_settings" : sampling_settings, + }, + "L/8": { # DiT_L_8 + "unet_config": { + "depth" : 24, + "num_heads" : 16, + "patch_size" : 8, + "hidden_size" : 1024, + }, + "sampling_settings" : sampling_settings, + }, + "B/2": { # DiT_B_2 + "unet_config": { + "depth" : 12, + "num_heads" : 12, + "patch_size" : 2, + "hidden_size" : 768, + }, + "sampling_settings" : sampling_settings, + }, + "B/4": { # DiT_B_4 + "unet_config": { + "depth" : 12, + "num_heads" : 12, + "patch_size" : 4, + "hidden_size" : 768, + }, + "sampling_settings" : sampling_settings, + }, + "B/8": { # DiT_B_8 + "unet_config": { + "depth" : 12, + "num_heads" : 12, + "patch_size" : 8, + "hidden_size" : 768, + }, + "sampling_settings" : sampling_settings, + }, + "S/2": { # DiT_S_2 + "unet_config": { + "depth" : 12, + "num_heads" : 6, + "patch_size" : 2, + "hidden_size" : 384, + }, + "sampling_settings" : sampling_settings, + }, + "S/4": { # DiT_S_4 + "unet_config": { + "depth" : 12, + "num_heads" : 6, + "patch_size" : 4, + "hidden_size" : 384, + }, + "sampling_settings" : sampling_settings, + }, + "S/8": { # DiT_S_8 + "unet_config": { + "depth" : 12, + "num_heads" : 6, + "patch_size" : 8, + "hidden_size" : 384, + }, + "sampling_settings" : sampling_settings, + }, +} \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/dit/pixArt/LICENSE-Pixart b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/dit/pixArt/LICENSE-Pixart new file mode 100644 index 0000000000000000000000000000000000000000..29ebfa545f5580919a4e884d7014d7a3eb2df762 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/dit/pixArt/LICENSE-Pixart @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/dit/pixArt/__init__.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/dit/pixArt/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/dit/pixArt/__pycache__/__init__.cpython-312.pyc b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/dit/pixArt/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d498836e27e306d5147b04a40418d06e53fce97d Binary files /dev/null and b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/dit/pixArt/__pycache__/__init__.cpython-312.pyc differ diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/dit/pixArt/__pycache__/config.cpython-312.pyc b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/dit/pixArt/__pycache__/config.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..29f3c7e95e71cfeb4b22cd1030967fd8c87b5b56 Binary files /dev/null and b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/dit/pixArt/__pycache__/config.cpython-312.pyc differ diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/dit/pixArt/__pycache__/diffusers_convert.cpython-312.pyc b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/dit/pixArt/__pycache__/diffusers_convert.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e870ebd34b63baa427e513e34367d9ff4920e581 Binary files /dev/null and b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/dit/pixArt/__pycache__/diffusers_convert.cpython-312.pyc differ diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/dit/pixArt/__pycache__/loader.cpython-312.pyc b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/dit/pixArt/__pycache__/loader.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1ed33d8dd4ba08c1ad674b598cb4f7b61c545a20 Binary files /dev/null and b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/dit/pixArt/__pycache__/loader.cpython-312.pyc differ diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/dit/pixArt/config.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/dit/pixArt/config.py new file mode 100644 index 0000000000000000000000000000000000000000..47f521a7162941286852ecd7f2537116fc07210a --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/dit/pixArt/config.py @@ -0,0 +1,139 @@ +""" +List of all PixArt model types / settings +""" +sampling_settings = { + "beta_schedule" : "sqrt_linear", + "linear_start" : 0.0001, + "linear_end" : 0.02, + "timesteps" : 1000, +} + +pixart_conf = { + "PixArtMS_XL_2": { # models/PixArtMS + "target": "PixArtMS", + "unet_config": { + "input_size" : 1024//8, + "depth" : 28, + "num_heads" : 16, + "patch_size" : 2, + "hidden_size" : 1152, + "pe_interpolation": 2, + }, + "sampling_settings" : sampling_settings, + }, + "PixArtMS_Sigma_XL_2": { + "target": "PixArtMSSigma", + "unet_config": { + "input_size" : 1024//8, + "token_num" : 300, + "depth" : 28, + "num_heads" : 16, + "patch_size" : 2, + "hidden_size" : 1152, + "micro_condition": False, + "pe_interpolation": 2, + "model_max_length": 300, + }, + "sampling_settings" : sampling_settings, + }, + "PixArtMS_Sigma_XL_2_900M": { + "target": "PixArtMSSigma", + "unet_config": { + "input_size": 1024 // 8, + "token_num": 300, + "depth": 42, + "num_heads": 16, + "patch_size": 2, + "hidden_size": 1152, + "micro_condition": False, + "pe_interpolation": 2, + "model_max_length": 300, + }, + "sampling_settings": sampling_settings, + }, + "PixArtMS_Sigma_XL_2_2K": { + "target": "PixArtMSSigma", + "unet_config": { + "input_size" : 2048//8, + "token_num" : 300, + "depth" : 28, + "num_heads" : 16, + "patch_size" : 2, + "hidden_size" : 1152, + "micro_condition": False, + "pe_interpolation": 4, + "model_max_length": 300, + }, + "sampling_settings" : sampling_settings, + }, + "PixArt_XL_2": { # models/PixArt + "target": "PixArt", + "unet_config": { + "input_size" : 512//8, + "token_num" : 120, + "depth" : 28, + "num_heads" : 16, + "patch_size" : 2, + "hidden_size" : 1152, + "pe_interpolation": 1, + }, + "sampling_settings" : sampling_settings, + }, +} + +pixart_conf.update({ # controlnet models + "ControlPixArtHalf": { + "target": "ControlPixArtHalf", + "unet_config": pixart_conf["PixArt_XL_2"]["unet_config"], + "sampling_settings": pixart_conf["PixArt_XL_2"]["sampling_settings"], + }, + "ControlPixArtMSHalf": { + "target": "ControlPixArtMSHalf", + "unet_config": pixart_conf["PixArtMS_XL_2"]["unet_config"], + "sampling_settings": pixart_conf["PixArtMS_XL_2"]["sampling_settings"], + } +}) + +pixart_res = { + "PixArtMS_XL_2": { # models/PixArtMS 1024x1024 + '0.25': [512, 2048], '0.26': [512, 1984], '0.27': [512, 1920], '0.28': [512, 1856], + '0.32': [576, 1792], '0.33': [576, 1728], '0.35': [576, 1664], '0.40': [640, 1600], + '0.42': [640, 1536], '0.48': [704, 1472], '0.50': [704, 1408], '0.52': [704, 1344], + '0.57': [768, 1344], '0.60': [768, 1280], '0.68': [832, 1216], '0.72': [832, 1152], + '0.78': [896, 1152], '0.82': [896, 1088], '0.88': [960, 1088], '0.94': [960, 1024], + '1.00': [1024,1024], '1.07': [1024, 960], '1.13': [1088, 960], '1.21': [1088, 896], + '1.29': [1152, 896], '1.38': [1152, 832], '1.46': [1216, 832], '1.67': [1280, 768], + '1.75': [1344, 768], '2.00': [1408, 704], '2.09': [1472, 704], '2.40': [1536, 640], + '2.50': [1600, 640], '2.89': [1664, 576], '3.00': [1728, 576], '3.11': [1792, 576], + '3.62': [1856, 512], '3.75': [1920, 512], '3.88': [1984, 512], '4.00': [2048, 512], + }, + "PixArt_XL_2": { # models/PixArt 512x512 + '0.25': [256,1024], '0.26': [256, 992], '0.27': [256, 960], '0.28': [256, 928], + '0.32': [288, 896], '0.33': [288, 864], '0.35': [288, 832], '0.40': [320, 800], + '0.42': [320, 768], '0.48': [352, 736], '0.50': [352, 704], '0.52': [352, 672], + '0.57': [384, 672], '0.60': [384, 640], '0.68': [416, 608], '0.72': [416, 576], + '0.78': [448, 576], '0.82': [448, 544], '0.88': [480, 544], '0.94': [480, 512], + '1.00': [512, 512], '1.07': [512, 480], '1.13': [544, 480], '1.21': [544, 448], + '1.29': [576, 448], '1.38': [576, 416], '1.46': [608, 416], '1.67': [640, 384], + '1.75': [672, 384], '2.00': [704, 352], '2.09': [736, 352], '2.40': [768, 320], + '2.50': [800, 320], '2.89': [832, 288], '3.00': [864, 288], '3.11': [896, 288], + '3.62': [928, 256], '3.75': [960, 256], '3.88': [992, 256], '4.00': [1024,256] + }, + "PixArtMS_Sigma_XL_2_2K": { + '0.25': [1024, 4096], '0.26': [1024, 3968], '0.27': [1024, 3840], '0.28': [1024, 3712], + '0.32': [1152, 3584], '0.33': [1152, 3456], '0.35': [1152, 3328], '0.40': [1280, 3200], + '0.42': [1280, 3072], '0.48': [1408, 2944], '0.50': [1408, 2816], '0.52': [1408, 2688], + '0.57': [1536, 2688], '0.60': [1536, 2560], '0.68': [1664, 2432], '0.72': [1664, 2304], + '0.78': [1792, 2304], '0.82': [1792, 2176], '0.88': [1920, 2176], '0.94': [1920, 2048], + '1.00': [2048, 2048], '1.07': [2048, 1920], '1.13': [2176, 1920], '1.21': [2176, 1792], + '1.29': [2304, 1792], '1.38': [2304, 1664], '1.46': [2432, 1664], '1.67': [2560, 1536], + '1.75': [2688, 1536], '2.00': [2816, 1408], '2.09': [2944, 1408], '2.40': [3072, 1280], + '2.50': [3200, 1280], '2.89': [3328, 1152], '3.00': [3456, 1152], '3.11': [3584, 1152], + '3.62': [3712, 1024], '3.75': [3840, 1024], '3.88': [3968, 1024], '4.00': [4096, 1024] + } +} +# These should be the same +pixart_res.update({ + "PixArtMS_Sigma_XL_2": pixart_res["PixArtMS_XL_2"], + "PixArtMS_Sigma_XL_2_512": pixart_res["PixArt_XL_2"], +}) \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/dit/pixArt/diffusers_convert.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/dit/pixArt/diffusers_convert.py new file mode 100644 index 0000000000000000000000000000000000000000..b5ca0ca07f0226b5c8d06500560c88588dff3fb7 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/dit/pixArt/diffusers_convert.py @@ -0,0 +1,216 @@ +# For using the diffusers format weights +# Based on the original ComfyUI function + +# https://github.com/PixArt-alpha/PixArt-alpha/blob/master/tools/convert_pixart_alpha_to_diffusers.py +import torch + +conversion_map_ms = [ # for multi_scale_train (MS) + # Resolution + ("csize_embedder.mlp.0.weight", "adaln_single.emb.resolution_embedder.linear_1.weight"), + ("csize_embedder.mlp.0.bias", "adaln_single.emb.resolution_embedder.linear_1.bias"), + ("csize_embedder.mlp.2.weight", "adaln_single.emb.resolution_embedder.linear_2.weight"), + ("csize_embedder.mlp.2.bias", "adaln_single.emb.resolution_embedder.linear_2.bias"), + # Aspect ratio + ("ar_embedder.mlp.0.weight", "adaln_single.emb.aspect_ratio_embedder.linear_1.weight"), + ("ar_embedder.mlp.0.bias", "adaln_single.emb.aspect_ratio_embedder.linear_1.bias"), + ("ar_embedder.mlp.2.weight", "adaln_single.emb.aspect_ratio_embedder.linear_2.weight"), + ("ar_embedder.mlp.2.bias", "adaln_single.emb.aspect_ratio_embedder.linear_2.bias"), +] + + +def get_depth(state_dict): + return sum(key.endswith('.attn1.to_k.bias') for key in state_dict.keys()) + + +def get_lora_depth(state_dict): + return sum(key.endswith('.attn1.to_k.lora_A.weight') for key in state_dict.keys()) + + +def get_conversion_map(state_dict): + conversion_map = [ # main SD conversion map (PixArt reference, HF Diffusers) + # Patch embeddings + ("x_embedder.proj.weight", "pos_embed.proj.weight"), + ("x_embedder.proj.bias", "pos_embed.proj.bias"), + # Caption projection + ("y_embedder.y_embedding", "caption_projection.y_embedding"), + ("y_embedder.y_proj.fc1.weight", "caption_projection.linear_1.weight"), + ("y_embedder.y_proj.fc1.bias", "caption_projection.linear_1.bias"), + ("y_embedder.y_proj.fc2.weight", "caption_projection.linear_2.weight"), + ("y_embedder.y_proj.fc2.bias", "caption_projection.linear_2.bias"), + # AdaLN-single LN + ("t_embedder.mlp.0.weight", "adaln_single.emb.timestep_embedder.linear_1.weight"), + ("t_embedder.mlp.0.bias", "adaln_single.emb.timestep_embedder.linear_1.bias"), + ("t_embedder.mlp.2.weight", "adaln_single.emb.timestep_embedder.linear_2.weight"), + ("t_embedder.mlp.2.bias", "adaln_single.emb.timestep_embedder.linear_2.bias"), + # Shared norm + ("t_block.1.weight", "adaln_single.linear.weight"), + ("t_block.1.bias", "adaln_single.linear.bias"), + # Final block + ("final_layer.linear.weight", "proj_out.weight"), + ("final_layer.linear.bias", "proj_out.bias"), + ("final_layer.scale_shift_table", "scale_shift_table"), + ] + + # Add actual transformer blocks + for depth in range(get_depth(state_dict)): + # Transformer blocks + conversion_map += [ + (f"blocks.{depth}.scale_shift_table", f"transformer_blocks.{depth}.scale_shift_table"), + # Projection + (f"blocks.{depth}.attn.proj.weight", f"transformer_blocks.{depth}.attn1.to_out.0.weight"), + (f"blocks.{depth}.attn.proj.bias", f"transformer_blocks.{depth}.attn1.to_out.0.bias"), + # Feed-forward + (f"blocks.{depth}.mlp.fc1.weight", f"transformer_blocks.{depth}.ff.net.0.proj.weight"), + (f"blocks.{depth}.mlp.fc1.bias", f"transformer_blocks.{depth}.ff.net.0.proj.bias"), + (f"blocks.{depth}.mlp.fc2.weight", f"transformer_blocks.{depth}.ff.net.2.weight"), + (f"blocks.{depth}.mlp.fc2.bias", f"transformer_blocks.{depth}.ff.net.2.bias"), + # Cross-attention (proj) + (f"blocks.{depth}.cross_attn.proj.weight", f"transformer_blocks.{depth}.attn2.to_out.0.weight"), + (f"blocks.{depth}.cross_attn.proj.bias", f"transformer_blocks.{depth}.attn2.to_out.0.bias"), + ] + return conversion_map + + +def find_prefix(state_dict, target_key): + prefix = "" + for k in state_dict.keys(): + if k.endswith(target_key): + prefix = k.split(target_key)[0] + break + return prefix + + +def convert_state_dict(state_dict): + if "adaln_single.emb.resolution_embedder.linear_1.weight" in state_dict.keys(): + cmap = get_conversion_map(state_dict) + conversion_map_ms + else: + cmap = get_conversion_map(state_dict) + + missing = [k for k, v in cmap if v not in state_dict] + new_state_dict = {k: state_dict[v] for k, v in cmap if k not in missing} + matched = list(v for k, v in cmap if v in state_dict.keys()) + + for depth in range(get_depth(state_dict)): + for wb in ["weight", "bias"]: + # Self Attention + key = lambda a: f"transformer_blocks.{depth}.attn1.to_{a}.{wb}" + new_state_dict[f"blocks.{depth}.attn.qkv.{wb}"] = torch.cat(( + state_dict[key('q')], state_dict[key('k')], state_dict[key('v')] + ), dim=0) + matched += [key('q'), key('k'), key('v')] + + # Cross-attention (linear) + key = lambda a: f"transformer_blocks.{depth}.attn2.to_{a}.{wb}" + new_state_dict[f"blocks.{depth}.cross_attn.q_linear.{wb}"] = state_dict[key('q')] + new_state_dict[f"blocks.{depth}.cross_attn.kv_linear.{wb}"] = torch.cat(( + state_dict[key('k')], state_dict[key('v')] + ), dim=0) + matched += [key('q'), key('k'), key('v')] + + if len(matched) < len(state_dict): + print(f"PixArt: UNET conversion has leftover keys! ({len(matched)} vs {len(state_dict)})") + print(list(set(state_dict.keys()) - set(matched))) + + if len(missing) > 0: + print(f"PixArt: UNET conversion has missing keys!") + print(missing) + + return new_state_dict + + +# Same as above but for LoRA weights: +def convert_lora_state_dict(state_dict, peft=True): + # koyha + rep_ak = lambda x: x.replace(".weight", ".lora_down.weight") + rep_bk = lambda x: x.replace(".weight", ".lora_up.weight") + rep_pk = lambda x: x.replace(".weight", ".alpha") + if peft: # peft + rep_ap = lambda x: x.replace(".weight", ".lora_A.weight") + rep_bp = lambda x: x.replace(".weight", ".lora_B.weight") + rep_pp = lambda x: x.replace(".weight", ".alpha") + + prefix = find_prefix(state_dict, "adaln_single.linear.lora_A.weight") + state_dict = {k[len(prefix):]: v for k, v in state_dict.items()} + else: # OneTrainer + rep_ap = lambda x: x.replace(".", "_")[:-7] + ".lora_down.weight" + rep_bp = lambda x: x.replace(".", "_")[:-7] + ".lora_up.weight" + rep_pp = lambda x: x.replace(".", "_")[:-7] + ".alpha" + + prefix = "lora_transformer_" + t5_marker = "lora_te_encoder" + t5_keys = [] + for key in list(state_dict.keys()): + if key.startswith(prefix): + state_dict[key[len(prefix):]] = state_dict.pop(key) + elif t5_marker in key: + t5_keys.append(state_dict.pop(key)) + if len(t5_keys) > 0: + print(f"Text Encoder not supported for PixArt LoRA, ignoring {len(t5_keys)} keys") + + cmap = [] + cmap_unet = get_conversion_map(state_dict) + conversion_map_ms # todo: 512 model + for k, v in cmap_unet: + if v.endswith(".weight"): + cmap.append((rep_ak(k), rep_ap(v))) + cmap.append((rep_bk(k), rep_bp(v))) + if not peft: + cmap.append((rep_pk(k), rep_pp(v))) + + missing = [k for k, v in cmap if v not in state_dict] + new_state_dict = {k: state_dict[v] for k, v in cmap if k not in missing} + matched = list(v for k, v in cmap if v in state_dict.keys()) + + lora_depth = get_lora_depth(state_dict) + for fp, fk in ((rep_ap, rep_ak), (rep_bp, rep_bk)): + for depth in range(lora_depth): + # Self Attention + key = lambda a: fp(f"transformer_blocks.{depth}.attn1.to_{a}.weight") + new_state_dict[fk(f"blocks.{depth}.attn.qkv.weight")] = torch.cat(( + state_dict[key('q')], state_dict[key('k')], state_dict[key('v')] + ), dim=0) + + matched += [key('q'), key('k'), key('v')] + if not peft: + akey = lambda a: rep_pp(f"transformer_blocks.{depth}.attn1.to_{a}.weight") + new_state_dict[rep_pk((f"blocks.{depth}.attn.qkv.weight"))] = state_dict[akey("q")] + matched += [akey('q'), akey('k'), akey('v')] + + # Self Attention projection? + key = lambda a: fp(f"transformer_blocks.{depth}.attn1.to_{a}.weight") + new_state_dict[fk(f"blocks.{depth}.attn.proj.weight")] = state_dict[key('out.0')] + matched += [key('out.0')] + + # Cross-attention (linear) + key = lambda a: fp(f"transformer_blocks.{depth}.attn2.to_{a}.weight") + new_state_dict[fk(f"blocks.{depth}.cross_attn.q_linear.weight")] = state_dict[key('q')] + new_state_dict[fk(f"blocks.{depth}.cross_attn.kv_linear.weight")] = torch.cat(( + state_dict[key('k')], state_dict[key('v')] + ), dim=0) + matched += [key('q'), key('k'), key('v')] + if not peft: + akey = lambda a: rep_pp(f"transformer_blocks.{depth}.attn2.to_{a}.weight") + new_state_dict[rep_pk((f"blocks.{depth}.cross_attn.q_linear.weight"))] = state_dict[akey("q")] + new_state_dict[rep_pk((f"blocks.{depth}.cross_attn.kv_linear.weight"))] = state_dict[akey("k")] + matched += [akey('q'), akey('k'), akey('v')] + + # Cross Attention projection? + key = lambda a: fp(f"transformer_blocks.{depth}.attn2.to_{a}.weight") + new_state_dict[fk(f"blocks.{depth}.cross_attn.proj.weight")] = state_dict[key('out.0')] + matched += [key('out.0')] + + key = fp(f"transformer_blocks.{depth}.ff.net.0.proj.weight") + new_state_dict[fk(f"blocks.{depth}.mlp.fc1.weight")] = state_dict[key] + matched += [key] + + key = fp(f"transformer_blocks.{depth}.ff.net.2.weight") + new_state_dict[fk(f"blocks.{depth}.mlp.fc2.weight")] = state_dict[key] + matched += [key] + + if len(matched) < len(state_dict): + print(f"PixArt: LoRA conversion has leftover keys! ({len(matched)} vs {len(state_dict)})") + print(list(set(state_dict.keys()) - set(matched))) + + if len(missing) > 0: + print(f"PixArt: LoRA conversion has missing keys! (probably)") + print(missing) + + return new_state_dict \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/dit/pixArt/loader.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/dit/pixArt/loader.py new file mode 100644 index 0000000000000000000000000000000000000000..d59a0fff30b6e3b73085cec60c481a8d41b6c5c1 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/dit/pixArt/loader.py @@ -0,0 +1,331 @@ +import os +import json +import copy +import torch +import math +import comfy.supported_models_base +import comfy.latent_formats +import comfy.model_patcher +import comfy.model_base +import comfy.utils +import comfy.conds +from comfy import model_management +from .diffusers_convert import convert_state_dict, convert_lora_state_dict + +# checkpointbf +class EXM_PixArt(comfy.supported_models_base.BASE): + unet_config = {} + unet_extra_config = {} + latent_format = comfy.latent_formats.SD15 + + def __init__(self, model_conf): + self.model_target = model_conf.get("target") + self.unet_config = model_conf.get("unet_config", {}) + self.sampling_settings = model_conf.get("sampling_settings", {}) + self.latent_format = self.latent_format() + # UNET is handled by extension + self.unet_config["disable_unet_model_creation"] = True + + def model_type(self, state_dict, prefix=""): + return comfy.model_base.ModelType.EPS + + +class EXM_PixArt_Model(comfy.model_base.BaseModel): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def extra_conds(self, **kwargs): + out = super().extra_conds(**kwargs) + + img_hw = kwargs.get("img_hw", None) + if img_hw is not None: + out["img_hw"] = comfy.conds.CONDRegular(torch.tensor(img_hw)) + + aspect_ratio = kwargs.get("aspect_ratio", None) + if aspect_ratio is not None: + out["aspect_ratio"] = comfy.conds.CONDRegular(torch.tensor(aspect_ratio)) + + cn_hint = kwargs.get("cn_hint", None) + if cn_hint is not None: + out["cn_hint"] = comfy.conds.CONDRegular(cn_hint) + + return out + + +def load_pixart(model_path, model_conf=None): + state_dict = comfy.utils.load_torch_file(model_path) + state_dict = state_dict.get("model", state_dict) + + # prefix + for prefix in ["model.diffusion_model.", ]: + if any(True for x in state_dict if x.startswith(prefix)): + state_dict = {k[len(prefix):]: v for k, v in state_dict.items()} + + # diffusers + if "adaln_single.linear.weight" in state_dict: + state_dict = convert_state_dict(state_dict) # Diffusers + + # guess auto config + if model_conf is None: + model_conf = guess_pixart_config(state_dict) + + parameters = comfy.utils.calculate_parameters(state_dict) + unet_dtype = model_management.unet_dtype(model_params=parameters) + load_device = comfy.model_management.get_torch_device() + offload_device = comfy.model_management.unet_offload_device() + + # ignore fp8/etc and use directly for now + manual_cast_dtype = model_management.unet_manual_cast(unet_dtype, load_device) + if manual_cast_dtype: + print(f"PixArt: falling back to {manual_cast_dtype}") + unet_dtype = manual_cast_dtype + + model_conf = EXM_PixArt(model_conf) # convert to object + model = EXM_PixArt_Model( # same as comfy.model_base.BaseModel + model_conf, + model_type=comfy.model_base.ModelType.EPS, + device=model_management.get_torch_device() + ) + + if model_conf.model_target == "PixArtMS": + from .models.PixArtMS import PixArtMS + model.diffusion_model = PixArtMS(**model_conf.unet_config) + elif model_conf.model_target == "PixArt": + from .models.PixArt import PixArt + model.diffusion_model = PixArt(**model_conf.unet_config) + elif model_conf.model_target == "PixArtMSSigma": + from .models.PixArtMS import PixArtMS + model.diffusion_model = PixArtMS(**model_conf.unet_config) + model.latent_format = comfy.latent_formats.SDXL() + elif model_conf.model_target == "ControlPixArtMSHalf": + from .models.PixArtMS import PixArtMS + from .models.pixart_controlnet import ControlPixArtMSHalf + model.diffusion_model = PixArtMS(**model_conf.unet_config) + model.diffusion_model = ControlPixArtMSHalf(model.diffusion_model) + elif model_conf.model_target == "ControlPixArtHalf": + from .models.PixArt import PixArt + from .models.pixart_controlnet import ControlPixArtHalf + model.diffusion_model = PixArt(**model_conf.unet_config) + model.diffusion_model = ControlPixArtHalf(model.diffusion_model) + else: + raise NotImplementedError(f"Unknown model target '{model_conf.model_target}'") + + m, u = model.diffusion_model.load_state_dict(state_dict, strict=False) + if len(m) > 0: print("Missing UNET keys", m) + if len(u) > 0: print("Leftover UNET keys", u) + model.diffusion_model.dtype = unet_dtype + model.diffusion_model.eval() + model.diffusion_model.to(unet_dtype) + + model_patcher = comfy.model_patcher.ModelPatcher( + model, + load_device=load_device, + offload_device=offload_device, + ) + return model_patcher + + +def guess_pixart_config(sd): + """ + Guess config based on converted state dict. + """ + # Shared settings based on DiT_XL_2 - could be enumerated + config = { + "num_heads": 16, # get from attention + "patch_size": 2, # final layer I guess? + "hidden_size": 1152, # pos_embed.shape[2] + } + config["depth"] = sum([key.endswith(".attn.proj.weight") for key in sd.keys()]) or 28 + + try: + # this is not present in the diffusers version for sigma? + config["model_max_length"] = sd["y_embedder.y_embedding"].shape[0] + except KeyError: + # need better logic to guess this + config["model_max_length"] = 300 + + if "pos_embed" in sd: + config["input_size"] = int(math.sqrt(sd["pos_embed"].shape[1])) * config["patch_size"] + config["pe_interpolation"] = config["input_size"] // (512 // 8) # dumb guess + + target_arch = "PixArtMS" + if config["model_max_length"] == 300: + # Sigma + target_arch = "PixArtMSSigma" + config["micro_condition"] = False + if "input_size" not in config: + # The diffusers weights for 1K/2K are exactly the same...? + # replace patch embed logic with HyDiT? + print(f"PixArt: diffusers weights - 2K model will be broken, use manual loading!") + config["input_size"] = 1024 // 8 + else: + # Alpha + if "csize_embedder.mlp.0.weight" in sd: + # MS (microconds) + target_arch = "PixArtMS" + config["micro_condition"] = True + if "input_size" not in config: + config["input_size"] = 1024 // 8 + config["pe_interpolation"] = 2 + else: + # PixArt + target_arch = "PixArt" + if "input_size" not in config: + config["input_size"] = 512 // 8 + config["pe_interpolation"] = 1 + + print("PixArt guessed config:", target_arch, config) + return { + "target": target_arch, + "unet_config": config, + "sampling_settings": { + "beta_schedule": "sqrt_linear", + "linear_start": 0.0001, + "linear_end": 0.02, + "timesteps": 1000, + } + } + +# lora +class EXM_PixArt_ModelPatcher(comfy.model_patcher.ModelPatcher): + def calculate_weight(self, patches, weight, key): + """ + This is almost the same as the comfy function, but stripped down to just the LoRA patch code. + The problem with the original code is the q/k/v keys being combined into one for the attention. + In the diffusers code, they're treated as separate keys, but in the reference code they're recombined (q+kv|qkv). + This means, for example, that the [1152,1152] weights become [3456,1152] in the state dict. + The issue with this is that the LoRA weights are [128,1152],[1152,128] and become [384,1162],[3456,128] instead. + + This is the best thing I could think of that would fix that, but it's very fragile. + - Check key shape to determine if it needs the fallback logic + - Cut the input into parts based on the shape (undoing the torch.cat) + - Do the matrix multiplication logic + - Recombine them to match the expected shape + """ + for p in patches: + alpha = p[0] + v = p[1] + strength_model = p[2] + if strength_model != 1.0: + weight *= strength_model + + if isinstance(v, list): + v = (self.calculate_weight(v[1:], v[0].clone(), key),) + + if len(v) == 2: + patch_type = v[0] + v = v[1] + + if patch_type == "lora": + mat1 = comfy.model_management.cast_to_device(v[0], weight.device, torch.float32) + mat2 = comfy.model_management.cast_to_device(v[1], weight.device, torch.float32) + if v[2] is not None: + alpha *= v[2] / mat2.shape[0] + try: + mat1 = mat1.flatten(start_dim=1) + mat2 = mat2.flatten(start_dim=1) + + ch1 = mat1.shape[0] // mat2.shape[1] + ch2 = mat2.shape[0] // mat1.shape[1] + ### Fallback logic for shape mismatch ### + if mat1.shape[0] != mat2.shape[1] and ch1 == ch2 and (mat1.shape[0] / mat2.shape[1]) % 1 == 0: + mat1 = mat1.chunk(ch1, dim=0) + mat2 = mat2.chunk(ch1, dim=0) + weight += torch.cat( + [alpha * torch.mm(mat1[x], mat2[x]) for x in range(ch1)], + dim=0, + ).reshape(weight.shape).type(weight.dtype) + else: + weight += (alpha * torch.mm(mat1, mat2)).reshape(weight.shape).type(weight.dtype) + except Exception as e: + print("ERROR", key, e) + return weight + + def clone(self): + n = EXM_PixArt_ModelPatcher(self.model, self.load_device, self.offload_device, self.size, self.current_device, + weight_inplace_update=self.weight_inplace_update) + n.patches = {} + for k in self.patches: + n.patches[k] = self.patches[k][:] + + n.object_patches = self.object_patches.copy() + n.model_options = copy.deepcopy(self.model_options) + n.model_keys = self.model_keys + return n + + +def replace_model_patcher(model): + n = EXM_PixArt_ModelPatcher( + model=model.model, + size=model.size, + load_device=model.load_device, + offload_device=model.offload_device, + current_device=model.current_device, + weight_inplace_update=model.weight_inplace_update, + ) + n.patches = {} + for k in model.patches: + n.patches[k] = model.patches[k][:] + + n.object_patches = model.object_patches.copy() + n.model_options = copy.deepcopy(model.model_options) + return n + + +def find_peft_alpha(path): + def load_json(json_path): + with open(json_path) as f: + data = json.load(f) + alpha = data.get("lora_alpha") + alpha = alpha or data.get("alpha") + if not alpha: + print(" Found config but `lora_alpha` is missing!") + else: + print(f" Found config at {json_path} [alpha:{alpha}]") + return alpha + + # For some weird reason peft doesn't include the alpha in the actual model + print("PixArt: Warning! This is a PEFT LoRA. Trying to find config...") + files = [ + f"{os.path.splitext(path)[0]}.json", + f"{os.path.splitext(path)[0]}.config.json", + os.path.join(os.path.dirname(path), "adapter_config.json"), + ] + for file in files: + if os.path.isfile(file): + return load_json(file) + + print(" Missing config/alpha! assuming alpha of 8. Consider converting it/adding a config json to it.") + return 8.0 + + +def load_pixart_lora(model, lora, lora_path, strength): + k_back = lambda x: x.replace(".lora_up.weight", "") + # need to convert the actual weights for this to work. + if any(True for x in lora.keys() if x.endswith("adaln_single.linear.lora_A.weight")): + lora = convert_lora_state_dict(lora, peft=True) + alpha = find_peft_alpha(lora_path) + lora.update({f"{k_back(x)}.alpha": torch.tensor(alpha) for x in lora.keys() if "lora_up" in x}) + else: # OneTrainer + lora = convert_lora_state_dict(lora, peft=False) + + key_map = {k_back(x): f"diffusion_model.{k_back(x)}.weight" for x in lora.keys() if "lora_up" in x} # fake + + loaded = comfy.lora.load_lora(lora, key_map) + if model is not None: + # switch to custom model patcher when using LoRAs + if isinstance(model, EXM_PixArt_ModelPatcher): + new_modelpatcher = model.clone() + else: + new_modelpatcher = replace_model_patcher(model) + k = new_modelpatcher.add_patches(loaded, strength) + else: + k = () + new_modelpatcher = None + + k = set(k) + for x in loaded: + if (x not in k): + print("NOT LOADED", x) + + return new_modelpatcher \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/dit/pixArt/models/PixArt.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/dit/pixArt/models/PixArt.py new file mode 100644 index 0000000000000000000000000000000000000000..4d6cf93be017348dcafc42754552ca132112bd9d --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/dit/pixArt/models/PixArt.py @@ -0,0 +1,250 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. + +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. +# -------------------------------------------------------- +# References: +# GLIDE: https://github.com/openai/glide-text2im +# MAE: https://github.com/facebookresearch/mae/blob/main/models_mae.py +# -------------------------------------------------------- +import math +import torch +import torch.nn as nn +import os +import numpy as np +from timm.models.layers import DropPath +from timm.models.vision_transformer import PatchEmbed, Mlp + + +from .utils import auto_grad_checkpoint, to_2tuple +from .PixArt_blocks import t2i_modulate, CaptionEmbedder, AttentionKVCompress, MultiHeadCrossAttention, T2IFinalLayer, TimestepEmbedder, LabelEmbedder, FinalLayer + + +class PixArtBlock(nn.Module): + """ + A PixArt block with adaptive layer norm (adaLN-single) conditioning. + """ + def __init__(self, hidden_size, num_heads, mlp_ratio=4.0, drop_path=0, input_size=None, sampling=None, sr_ratio=1, qk_norm=False, **block_kwargs): + super().__init__() + self.norm1 = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6) + self.attn = AttentionKVCompress( + hidden_size, num_heads=num_heads, qkv_bias=True, sampling=sampling, sr_ratio=sr_ratio, + qk_norm=qk_norm, **block_kwargs + ) + self.cross_attn = MultiHeadCrossAttention(hidden_size, num_heads, **block_kwargs) + self.norm2 = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6) + # to be compatible with lower version pytorch + approx_gelu = lambda: nn.GELU(approximate="tanh") + self.mlp = Mlp(in_features=hidden_size, hidden_features=int(hidden_size * mlp_ratio), act_layer=approx_gelu, drop=0) + self.drop_path = DropPath(drop_path) if drop_path > 0. else nn.Identity() + self.scale_shift_table = nn.Parameter(torch.randn(6, hidden_size) / hidden_size ** 0.5) + self.sampling = sampling + self.sr_ratio = sr_ratio + + def forward(self, x, y, t, mask=None, **kwargs): + B, N, C = x.shape + + shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp = (self.scale_shift_table[None] + t.reshape(B, 6, -1)).chunk(6, dim=1) + x = x + self.drop_path(gate_msa * self.attn(t2i_modulate(self.norm1(x), shift_msa, scale_msa)).reshape(B, N, C)) + x = x + self.cross_attn(x, y, mask) + x = x + self.drop_path(gate_mlp * self.mlp(t2i_modulate(self.norm2(x), shift_mlp, scale_mlp))) + + return x + + +### Core PixArt Model ### +class PixArt(nn.Module): + """ + Diffusion model with a Transformer backbone. + """ + def __init__( + self, + input_size=32, + patch_size=2, + in_channels=4, + hidden_size=1152, + depth=28, + num_heads=16, + mlp_ratio=4.0, + class_dropout_prob=0.1, + pred_sigma=True, + drop_path: float = 0., + caption_channels=4096, + pe_interpolation=1.0, + pe_precision=None, + config=None, + model_max_length=120, + qk_norm=False, + kv_compress_config=None, + **kwargs, + ): + super().__init__() + self.pred_sigma = pred_sigma + self.in_channels = in_channels + self.out_channels = in_channels * 2 if pred_sigma else in_channels + self.patch_size = patch_size + self.num_heads = num_heads + self.pe_interpolation = pe_interpolation + self.pe_precision = pe_precision + self.depth = depth + + self.x_embedder = PatchEmbed(input_size, patch_size, in_channels, hidden_size, bias=True) + self.t_embedder = TimestepEmbedder(hidden_size) + num_patches = self.x_embedder.num_patches + self.base_size = input_size // self.patch_size + # Will use fixed sin-cos embedding: + self.register_buffer("pos_embed", torch.zeros(1, num_patches, hidden_size)) + + approx_gelu = lambda: nn.GELU(approximate="tanh") + self.t_block = nn.Sequential( + nn.SiLU(), + nn.Linear(hidden_size, 6 * hidden_size, bias=True) + ) + self.y_embedder = CaptionEmbedder( + in_channels=caption_channels, hidden_size=hidden_size, uncond_prob=class_dropout_prob, + act_layer=approx_gelu, token_num=model_max_length + ) + drop_path = [x.item() for x in torch.linspace(0, drop_path, depth)] # stochastic depth decay rule + self.kv_compress_config = kv_compress_config + if kv_compress_config is None: + self.kv_compress_config = { + 'sampling': None, + 'scale_factor': 1, + 'kv_compress_layer': [], + } + self.blocks = nn.ModuleList([ + PixArtBlock( + hidden_size, num_heads, mlp_ratio=mlp_ratio, drop_path=drop_path[i], + input_size=(input_size // patch_size, input_size // patch_size), + sampling=self.kv_compress_config['sampling'], + sr_ratio=int( + self.kv_compress_config['scale_factor'] + ) if i in self.kv_compress_config['kv_compress_layer'] else 1, + qk_norm=qk_norm, + ) + for i in range(depth) + ]) + self.final_layer = T2IFinalLayer(hidden_size, patch_size, self.out_channels) + + def forward_raw(self, x, t, y, mask=None, data_info=None): + """ + Original forward pass of PixArt. + x: (N, C, H, W) tensor of spatial inputs (images or latent representations of images) + t: (N,) tensor of diffusion timesteps + y: (N, 1, 120, C) tensor of class labels + """ + x = x.to(self.dtype) + timestep = t.to(self.dtype) + y = y.to(self.dtype) + pos_embed = self.pos_embed.to(self.dtype) + self.h, self.w = x.shape[-2]//self.patch_size, x.shape[-1]//self.patch_size + x = self.x_embedder(x) + pos_embed # (N, T, D), where T = H * W / patch_size ** 2 + t = self.t_embedder(timestep.to(x.dtype)) # (N, D) + t0 = self.t_block(t) + y = self.y_embedder(y, self.training) # (N, 1, L, D) + if mask is not None: + if mask.shape[0] != y.shape[0]: + mask = mask.repeat(y.shape[0] // mask.shape[0], 1) + mask = mask.squeeze(1).squeeze(1) + y = y.squeeze(1).masked_select(mask.unsqueeze(-1) != 0).view(1, -1, x.shape[-1]) + y_lens = mask.sum(dim=1).tolist() + else: + y_lens = [y.shape[2]] * y.shape[0] + y = y.squeeze(1).view(1, -1, x.shape[-1]) + for block in self.blocks: + x = auto_grad_checkpoint(block, x, y, t0, y_lens) # (N, T, D) #support grad checkpoint + x = self.final_layer(x, t) # (N, T, patch_size ** 2 * out_channels) + x = self.unpatchify(x) # (N, out_channels, H, W) + return x + + def forward(self, x, timesteps, context, y=None, **kwargs): + """ + Forward pass that adapts comfy input to original forward function + x: (N, C, H, W) tensor of spatial inputs (images or latent representations of images) + timesteps: (N,) tensor of diffusion timesteps + context: (N, 1, 120, C) conditioning + y: extra conditioning. + """ + ## Still accepts the input w/o that dim but returns garbage + if len(context.shape) == 3: + context = context.unsqueeze(1) + + ## run original forward pass + out = self.forward_raw( + x = x.to(self.dtype), + t = timesteps.to(self.dtype), + y = context.to(self.dtype), + ) + + ## only return EPS + out = out.to(torch.float) + eps, rest = out[:, :self.in_channels], out[:, self.in_channels:] + return eps + + def unpatchify(self, x): + """ + x: (N, T, patch_size**2 * C) + imgs: (N, H, W, C) + """ + c = self.out_channels + p = self.x_embedder.patch_size[0] + h = w = int(x.shape[1] ** 0.5) + assert h * w == x.shape[1] + + x = x.reshape(shape=(x.shape[0], h, w, p, p, c)) + x = torch.einsum('nhwpqc->nchpwq', x) + imgs = x.reshape(shape=(x.shape[0], c, h * p, h * p)) + return imgs + + +def get_2d_sincos_pos_embed(embed_dim, grid_size, cls_token=False, extra_tokens=0, pe_interpolation=1.0, base_size=16): + """ + grid_size: int of the grid height and width + return: + pos_embed: [grid_size*grid_size, embed_dim] or [1+grid_size*grid_size, embed_dim] (w/ or w/o cls_token) + """ + if isinstance(grid_size, int): + grid_size = to_2tuple(grid_size) + grid_h = np.arange(grid_size[0], dtype=np.float32) / (grid_size[0]/base_size) / pe_interpolation + grid_w = np.arange(grid_size[1], dtype=np.float32) / (grid_size[1]/base_size) / pe_interpolation + grid = np.meshgrid(grid_w, grid_h) # here w goes first + grid = np.stack(grid, axis=0) + grid = grid.reshape([2, 1, grid_size[1], grid_size[0]]) + + pos_embed = get_2d_sincos_pos_embed_from_grid(embed_dim, grid) + if cls_token and extra_tokens > 0: + pos_embed = np.concatenate([np.zeros([extra_tokens, embed_dim]), pos_embed], axis=0) + return pos_embed.astype(np.float32) + + +def get_2d_sincos_pos_embed_from_grid(embed_dim, grid): + assert embed_dim % 2 == 0 + + # use half of dimensions to encode grid_h + emb_h = get_1d_sincos_pos_embed_from_grid(embed_dim // 2, grid[0]) # (H*W, D/2) + emb_w = get_1d_sincos_pos_embed_from_grid(embed_dim // 2, grid[1]) # (H*W, D/2) + + emb = np.concatenate([emb_h, emb_w], axis=1) # (H*W, D) + return emb + + +def get_1d_sincos_pos_embed_from_grid(embed_dim, pos): + """ + embed_dim: output dimension for each position + pos: a list of positions to be encoded: size (M,) + out: (M, D) + """ + assert embed_dim % 2 == 0 + omega = np.arange(embed_dim // 2, dtype=np.float64) + omega /= embed_dim / 2. + omega = 1. / 10000 ** omega # (D/2,) + + pos = pos.reshape(-1) # (M,) + out = np.einsum('m,d->md', pos, omega) # (M, D/2), outer product + + emb_sin = np.sin(out) # (M, D/2) + emb_cos = np.cos(out) # (M, D/2) + + emb = np.concatenate([emb_sin, emb_cos], axis=1) # (M, D) + return emb diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/dit/pixArt/models/PixArtMS.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/dit/pixArt/models/PixArtMS.py new file mode 100644 index 0000000000000000000000000000000000000000..34ada90ed128a22ad1050a7fe14a286e7bae4051 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/dit/pixArt/models/PixArtMS.py @@ -0,0 +1,273 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. + +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. +# -------------------------------------------------------- +# References: +# GLIDE: https://github.com/openai/glide-text2im +# MAE: https://github.com/facebookresearch/mae/blob/main/models_mae.py +# -------------------------------------------------------- +import torch +import torch.nn as nn +from tqdm import tqdm +from timm.models.layers import DropPath +from timm.models.vision_transformer import Mlp + +from .utils import auto_grad_checkpoint, to_2tuple +from .PixArt_blocks import t2i_modulate, CaptionEmbedder, AttentionKVCompress, MultiHeadCrossAttention, T2IFinalLayer, TimestepEmbedder, SizeEmbedder +from .PixArt import PixArt, get_2d_sincos_pos_embed + + +class PatchEmbed(nn.Module): + """ + 2D Image to Patch Embedding + """ + def __init__( + self, + patch_size=16, + in_chans=3, + embed_dim=768, + norm_layer=None, + flatten=True, + bias=True, + ): + super().__init__() + patch_size = to_2tuple(patch_size) + self.patch_size = patch_size + self.flatten = flatten + self.proj = nn.Conv2d(in_chans, embed_dim, kernel_size=patch_size, stride=patch_size, bias=bias) + self.norm = norm_layer(embed_dim) if norm_layer else nn.Identity() + + def forward(self, x): + x = self.proj(x) + if self.flatten: + x = x.flatten(2).transpose(1, 2) # BCHW -> BNC + x = self.norm(x) + return x + + +class PixArtMSBlock(nn.Module): + """ + A PixArt block with adaptive layer norm zero (adaLN-Zero) conditioning. + """ + def __init__(self, hidden_size, num_heads, mlp_ratio=4.0, drop_path=0., input_size=None, + sampling=None, sr_ratio=1, qk_norm=False, **block_kwargs): + super().__init__() + self.hidden_size = hidden_size + self.norm1 = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6) + self.attn = AttentionKVCompress( + hidden_size, num_heads=num_heads, qkv_bias=True, sampling=sampling, sr_ratio=sr_ratio, + qk_norm=qk_norm, **block_kwargs + ) + self.cross_attn = MultiHeadCrossAttention(hidden_size, num_heads, **block_kwargs) + self.norm2 = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6) + # to be compatible with lower version pytorch + approx_gelu = lambda: nn.GELU(approximate="tanh") + self.mlp = Mlp(in_features=hidden_size, hidden_features=int(hidden_size * mlp_ratio), act_layer=approx_gelu, drop=0) + self.drop_path = DropPath(drop_path) if drop_path > 0. else nn.Identity() + self.scale_shift_table = nn.Parameter(torch.randn(6, hidden_size) / hidden_size ** 0.5) + + def forward(self, x, y, t, mask=None, HW=None, **kwargs): + B, N, C = x.shape + + shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp = (self.scale_shift_table[None] + t.reshape(B, 6, -1)).chunk(6, dim=1) + x = x + self.drop_path(gate_msa * self.attn(t2i_modulate(self.norm1(x), shift_msa, scale_msa), HW=HW)) + x = x + self.cross_attn(x, y, mask) + x = x + self.drop_path(gate_mlp * self.mlp(t2i_modulate(self.norm2(x), shift_mlp, scale_mlp))) + + return x + + +### Core PixArt Model ### +class PixArtMS(PixArt): + """ + Diffusion model with a Transformer backbone. + """ + def __init__( + self, + input_size=32, + patch_size=2, + in_channels=4, + hidden_size=1152, + depth=28, + num_heads=16, + mlp_ratio=4.0, + class_dropout_prob=0.1, + learn_sigma=True, + pred_sigma=True, + drop_path: float = 0., + caption_channels=4096, + pe_interpolation=None, + pe_precision=None, + config=None, + model_max_length=120, + micro_condition=True, + qk_norm=False, + kv_compress_config=None, + **kwargs, + ): + super().__init__( + input_size=input_size, + patch_size=patch_size, + in_channels=in_channels, + hidden_size=hidden_size, + depth=depth, + num_heads=num_heads, + mlp_ratio=mlp_ratio, + class_dropout_prob=class_dropout_prob, + learn_sigma=learn_sigma, + pred_sigma=pred_sigma, + drop_path=drop_path, + pe_interpolation=pe_interpolation, + config=config, + model_max_length=model_max_length, + qk_norm=qk_norm, + kv_compress_config=kv_compress_config, + **kwargs, + ) + self.dtype = torch.get_default_dtype() + self.h = self.w = 0 + approx_gelu = lambda: nn.GELU(approximate="tanh") + self.t_block = nn.Sequential( + nn.SiLU(), + nn.Linear(hidden_size, 6 * hidden_size, bias=True) + ) + self.x_embedder = PatchEmbed(patch_size, in_channels, hidden_size, bias=True) + self.y_embedder = CaptionEmbedder(in_channels=caption_channels, hidden_size=hidden_size, uncond_prob=class_dropout_prob, act_layer=approx_gelu, token_num=model_max_length) + self.micro_conditioning = micro_condition + if self.micro_conditioning: + self.csize_embedder = SizeEmbedder(hidden_size//3) # c_size embed + self.ar_embedder = SizeEmbedder(hidden_size//3) # aspect ratio embed + drop_path = [x.item() for x in torch.linspace(0, drop_path, depth)] # stochastic depth decay rule + if kv_compress_config is None: + kv_compress_config = { + 'sampling': None, + 'scale_factor': 1, + 'kv_compress_layer': [], + } + self.blocks = nn.ModuleList([ + PixArtMSBlock( + hidden_size, num_heads, mlp_ratio=mlp_ratio, drop_path=drop_path[i], + input_size=(input_size // patch_size, input_size // patch_size), + sampling=kv_compress_config['sampling'], + sr_ratio=int(kv_compress_config['scale_factor']) if i in kv_compress_config['kv_compress_layer'] else 1, + qk_norm=qk_norm, + ) + for i in range(depth) + ]) + self.final_layer = T2IFinalLayer(hidden_size, patch_size, self.out_channels) + + def forward_raw(self, x, t, y, mask=None, data_info=None, **kwargs): + """ + Original forward pass of PixArt. + x: (N, C, H, W) tensor of spatial inputs (images or latent representations of images) + t: (N,) tensor of diffusion timesteps + y: (N, 1, 120, C) tensor of class labels + """ + bs = x.shape[0] + x = x.to(self.dtype) + timestep = t.to(self.dtype) + y = y.to(self.dtype) + + pe_interpolation = self.pe_interpolation + if pe_interpolation is None or self.pe_precision is not None: + # calculate pe_interpolation on-the-fly + pe_interpolation = round((x.shape[-1]+x.shape[-2])/2.0 / (512/8.0), self.pe_precision or 0) + + self.h, self.w = x.shape[-2]//self.patch_size, x.shape[-1]//self.patch_size + pos_embed = torch.from_numpy( + get_2d_sincos_pos_embed( + self.pos_embed.shape[-1], (self.h, self.w), pe_interpolation=pe_interpolation, + base_size=self.base_size + ) + ).unsqueeze(0).to(device=x.device, dtype=self.dtype) + + x = self.x_embedder(x) + pos_embed # (N, T, D), where T = H * W / patch_size ** 2 + t = self.t_embedder(timestep) # (N, D) + + if self.micro_conditioning: + c_size, ar = data_info['img_hw'].to(self.dtype), data_info['aspect_ratio'].to(self.dtype) + csize = self.csize_embedder(c_size, bs) # (N, D) + ar = self.ar_embedder(ar, bs) # (N, D) + t = t + torch.cat([csize, ar], dim=1) + + t0 = self.t_block(t) + y = self.y_embedder(y, self.training) # (N, D) + + if mask is not None: + if mask.shape[0] != y.shape[0]: + mask = mask.repeat(y.shape[0] // mask.shape[0], 1) + mask = mask.squeeze(1).squeeze(1) + y = y.squeeze(1).masked_select(mask.unsqueeze(-1) != 0).view(1, -1, x.shape[-1]) + y_lens = mask.sum(dim=1).tolist() + else: + y_lens = [y.shape[2]] * y.shape[0] + y = y.squeeze(1).view(1, -1, x.shape[-1]) + for block in self.blocks: + x = auto_grad_checkpoint(block, x, y, t0, y_lens, (self.h, self.w), **kwargs) # (N, T, D) #support grad checkpoint + + x = self.final_layer(x, t) # (N, T, patch_size ** 2 * out_channels) + x = self.unpatchify(x) # (N, out_channels, H, W) + + return x + + def forward(self, x, timesteps, context, img_hw=None, aspect_ratio=None, **kwargs): + """ + Forward pass that adapts comfy input to original forward function + x: (N, C, H, W) tensor of spatial inputs (images or latent representations of images) + timesteps: (N,) tensor of diffusion timesteps + context: (N, 1, 120, C) conditioning + img_hw: height|width conditioning + aspect_ratio: aspect ratio conditioning + """ + ## size/ar from cond with fallback based on the latent image shape. + bs = x.shape[0] + data_info = {} + if img_hw is None: + data_info["img_hw"] = torch.tensor( + [[x.shape[2]*8, x.shape[3]*8]], + dtype=self.dtype, + device=x.device + ).repeat(bs, 1) + else: + data_info["img_hw"] = img_hw.to(dtype=x.dtype, device=x.device) + if aspect_ratio is None or True: + data_info["aspect_ratio"] = torch.tensor( + [[x.shape[2]/x.shape[3]]], + dtype=self.dtype, + device=x.device + ).repeat(bs, 1) + else: + data_info["aspect_ratio"] = aspect_ratio.to(dtype=x.dtype, device=x.device) + + ## Still accepts the input w/o that dim but returns garbage + if len(context.shape) == 3: + context = context.unsqueeze(1) + + ## run original forward pass + out = self.forward_raw( + x = x.to(self.dtype), + t = timesteps.to(self.dtype), + y = context.to(self.dtype), + data_info=data_info, + ) + + ## only return EPS + out = out.to(torch.float) + eps, rest = out[:, :self.in_channels], out[:, self.in_channels:] + return eps + + def unpatchify(self, x): + """ + x: (N, T, patch_size**2 * C) + imgs: (N, H, W, C) + """ + c = self.out_channels + p = self.x_embedder.patch_size[0] + assert self.h * self.w == x.shape[1] + + x = x.reshape(shape=(x.shape[0], self.h, self.w, p, p, c)) + x = torch.einsum('nhwpqc->nchpwq', x) + imgs = x.reshape(shape=(x.shape[0], c, self.h * p, self.w * p)) + return imgs diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/dit/pixArt/models/PixArt_blocks.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/dit/pixArt/models/PixArt_blocks.py new file mode 100644 index 0000000000000000000000000000000000000000..52cf847c9f3672f6d10c6a3efe08ae67a881f898 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/dit/pixArt/models/PixArt_blocks.py @@ -0,0 +1,477 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. + +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. +# -------------------------------------------------------- +# References: +# GLIDE: https://github.com/openai/glide-text2im +# MAE: https://github.com/facebookresearch/mae/blob/main/models_mae.py +# -------------------------------------------------------- +import math +import torch +import torch.nn as nn +import torch.nn.functional as F +from timm.models.vision_transformer import Mlp, Attention as Attention_ +from einops import rearrange + +from comfy import model_management +if model_management.xformers_enabled(): + import xformers + import xformers.ops +else: + print(""" +######################################## + PixArt: Not using xformers! + Expect images to be non-deterministic! + Batch sizes > 1 are most likely broken +######################################## +""") + +def modulate(x, shift, scale): + return x * (1 + scale.unsqueeze(1)) + shift.unsqueeze(1) + +def t2i_modulate(x, shift, scale): + return x * (1 + scale) + shift + +class MultiHeadCrossAttention(nn.Module): + def __init__(self, d_model, num_heads, attn_drop=0., proj_drop=0., **block_kwargs): + super(MultiHeadCrossAttention, self).__init__() + assert d_model % num_heads == 0, "d_model must be divisible by num_heads" + + self.d_model = d_model + self.num_heads = num_heads + self.head_dim = d_model // num_heads + + self.q_linear = nn.Linear(d_model, d_model) + self.kv_linear = nn.Linear(d_model, d_model*2) + self.attn_drop = nn.Dropout(attn_drop) + self.proj = nn.Linear(d_model, d_model) + self.proj_drop = nn.Dropout(proj_drop) + + def forward(self, x, cond, mask=None): + # query/value: img tokens; key: condition; mask: if padding tokens + B, N, C = x.shape + + q = self.q_linear(x).view(1, -1, self.num_heads, self.head_dim) + kv = self.kv_linear(cond).view(1, -1, 2, self.num_heads, self.head_dim) + k, v = kv.unbind(2) + + if model_management.xformers_enabled(): + attn_bias = None + if mask is not None: + attn_bias = xformers.ops.fmha.BlockDiagonalMask.from_seqlens([N] * B, mask) + x = xformers.ops.memory_efficient_attention( + q, k, v, + p=self.attn_drop.p, + attn_bias=attn_bias + ) + else: + q, k, v = map(lambda t: t.permute(0, 2, 1, 3),(q, k, v),) + attn_mask = None + if mask is not None and len(mask) > 1: + + # Create equivalent of xformer diagonal block mask, still only correct for square masks + # But depth doesn't matter as tensors can expand in that dimension + attn_mask_template = torch.ones( + [q.shape[2] // B, mask[0]], + dtype=torch.bool, + device=q.device + ) + attn_mask = torch.block_diag(attn_mask_template) + + # create a mask on the diagonal for each mask in the batch + for n in range(B - 1): + attn_mask = torch.block_diag(attn_mask, attn_mask_template) + + x = torch.nn.functional.scaled_dot_product_attention( + q, k, v, + attn_mask=attn_mask, + dropout_p=self.attn_drop.p + ).permute(0, 2, 1, 3).contiguous() + x = x.view(B, -1, C) + x = self.proj(x) + x = self.proj_drop(x) + return x + + +class AttentionKVCompress(Attention_): + """Multi-head Attention block with KV token compression and qk norm.""" + + def __init__( + self, + dim, + num_heads=8, + qkv_bias=True, + sampling='conv', + sr_ratio=1, + qk_norm=False, + **block_kwargs, + ): + """ + Args: + dim (int): Number of input channels. + num_heads (int): Number of attention heads. + qkv_bias (bool: If True, add a learnable bias to query, key, value. + """ + super().__init__(dim, num_heads=num_heads, qkv_bias=qkv_bias, **block_kwargs) + + self.sampling=sampling # ['conv', 'ave', 'uniform', 'uniform_every'] + self.sr_ratio = sr_ratio + if sr_ratio > 1 and sampling == 'conv': + # Avg Conv Init. + self.sr = nn.Conv2d(dim, dim, groups=dim, kernel_size=sr_ratio, stride=sr_ratio) + self.sr.weight.data.fill_(1/sr_ratio**2) + self.sr.bias.data.zero_() + self.norm = nn.LayerNorm(dim) + if qk_norm: + self.q_norm = nn.LayerNorm(dim) + self.k_norm = nn.LayerNorm(dim) + else: + self.q_norm = nn.Identity() + self.k_norm = nn.Identity() + + def downsample_2d(self, tensor, H, W, scale_factor, sampling=None): + if sampling is None or scale_factor == 1: + return tensor + B, N, C = tensor.shape + + if sampling == 'uniform_every': + return tensor[:, ::scale_factor], int(N // scale_factor) + + tensor = tensor.reshape(B, H, W, C).permute(0, 3, 1, 2) + new_H, new_W = int(H / scale_factor), int(W / scale_factor) + new_N = new_H * new_W + + if sampling == 'ave': + tensor = F.interpolate( + tensor, scale_factor=1 / scale_factor, mode='nearest' + ).permute(0, 2, 3, 1) + elif sampling == 'uniform': + tensor = tensor[:, :, ::scale_factor, ::scale_factor].permute(0, 2, 3, 1) + elif sampling == 'conv': + tensor = self.sr(tensor).reshape(B, C, -1).permute(0, 2, 1) + tensor = self.norm(tensor) + else: + raise ValueError + + return tensor.reshape(B, new_N, C).contiguous(), new_N + + def forward(self, x, mask=None, HW=None, block_id=None): + B, N, C = x.shape # 2 4096 1152 + new_N = N + if HW is None: + H = W = int(N ** 0.5) + else: + H, W = HW + qkv = self.qkv(x).reshape(B, N, 3, C) + + q, k, v = qkv.unbind(2) + dtype = q.dtype + q = self.q_norm(q) + k = self.k_norm(k) + + # KV compression + if self.sr_ratio > 1: + k, new_N = self.downsample_2d(k, H, W, self.sr_ratio, sampling=self.sampling) + v, new_N = self.downsample_2d(v, H, W, self.sr_ratio, sampling=self.sampling) + + q = q.reshape(B, N, self.num_heads, C // self.num_heads).to(dtype) + k = k.reshape(B, new_N, self.num_heads, C // self.num_heads).to(dtype) + v = v.reshape(B, new_N, self.num_heads, C // self.num_heads).to(dtype) + + attn_bias = None + if mask is not None: + attn_bias = torch.zeros([B * self.num_heads, q.shape[1], k.shape[1]], dtype=q.dtype, device=q.device) + attn_bias.masked_fill_(mask.squeeze(1).repeat(self.num_heads, 1, 1) == 0, float('-inf')) + # Switch between torch / xformers attention + if model_management.xformers_enabled(): + x = xformers.ops.memory_efficient_attention( + q, k, v, + p=self.attn_drop.p, + attn_bias=attn_bias + ) + else: + q, k, v = map(lambda t: t.transpose(1, 2),(q, k, v),) + x = torch.nn.functional.scaled_dot_product_attention( + q, k, v, + dropout_p=self.attn_drop.p, + attn_mask=attn_bias + ).transpose(1, 2).contiguous() + x = x.view(B, N, C) + x = self.proj(x) + x = self.proj_drop(x) + return x + + +################################################################################# +# AMP attention with fp32 softmax to fix loss NaN problem during training # +################################################################################# +class Attention(Attention_): + def forward(self, x): + B, N, C = x.shape + qkv = self.qkv(x).reshape(B, N, 3, self.num_heads, C // self.num_heads).permute(2, 0, 3, 1, 4) + q, k, v = qkv.unbind(0) # make torchscript happy (cannot use tensor as tuple) + use_fp32_attention = getattr(self, 'fp32_attention', False) + if use_fp32_attention: + q, k = q.float(), k.float() + with torch.cuda.amp.autocast(enabled=not use_fp32_attention): + attn = (q @ k.transpose(-2, -1)) * self.scale + attn = attn.softmax(dim=-1) + + attn = self.attn_drop(attn) + + x = (attn @ v).transpose(1, 2).reshape(B, N, C) + x = self.proj(x) + x = self.proj_drop(x) + return x + + +class FinalLayer(nn.Module): + """ + The final layer of PixArt. + """ + + def __init__(self, hidden_size, patch_size, out_channels): + super().__init__() + self.norm_final = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6) + self.linear = nn.Linear(hidden_size, patch_size * patch_size * out_channels, bias=True) + self.adaLN_modulation = nn.Sequential( + nn.SiLU(), + nn.Linear(hidden_size, 2 * hidden_size, bias=True) + ) + + def forward(self, x, c): + shift, scale = self.adaLN_modulation(c).chunk(2, dim=1) + x = modulate(self.norm_final(x), shift, scale) + x = self.linear(x) + return x + + +class T2IFinalLayer(nn.Module): + """ + The final layer of PixArt. + """ + + def __init__(self, hidden_size, patch_size, out_channels): + super().__init__() + self.norm_final = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6) + self.linear = nn.Linear(hidden_size, patch_size * patch_size * out_channels, bias=True) + self.scale_shift_table = nn.Parameter(torch.randn(2, hidden_size) / hidden_size ** 0.5) + self.out_channels = out_channels + + def forward(self, x, t): + shift, scale = (self.scale_shift_table[None] + t[:, None]).chunk(2, dim=1) + x = t2i_modulate(self.norm_final(x), shift, scale) + x = self.linear(x) + return x + + +class MaskFinalLayer(nn.Module): + """ + The final layer of PixArt. + """ + + def __init__(self, final_hidden_size, c_emb_size, patch_size, out_channels): + super().__init__() + self.norm_final = nn.LayerNorm(final_hidden_size, elementwise_affine=False, eps=1e-6) + self.linear = nn.Linear(final_hidden_size, patch_size * patch_size * out_channels, bias=True) + self.adaLN_modulation = nn.Sequential( + nn.SiLU(), + nn.Linear(c_emb_size, 2 * final_hidden_size, bias=True) + ) + def forward(self, x, t): + shift, scale = self.adaLN_modulation(t).chunk(2, dim=1) + x = modulate(self.norm_final(x), shift, scale) + x = self.linear(x) + return x + + +class DecoderLayer(nn.Module): + """ + The final layer of PixArt. + """ + + def __init__(self, hidden_size, decoder_hidden_size): + super().__init__() + self.norm_decoder = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6) + self.linear = nn.Linear(hidden_size, decoder_hidden_size, bias=True) + self.adaLN_modulation = nn.Sequential( + nn.SiLU(), + nn.Linear(hidden_size, 2 * hidden_size, bias=True) + ) + def forward(self, x, t): + shift, scale = self.adaLN_modulation(t).chunk(2, dim=1) + x = modulate(self.norm_decoder(x), shift, scale) + x = self.linear(x) + return x + + +################################################################################# +# Embedding Layers for Timesteps and Class Labels # +################################################################################# +class TimestepEmbedder(nn.Module): + """ + Embeds scalar timesteps into vector representations. + """ + + def __init__(self, hidden_size, frequency_embedding_size=256): + super().__init__() + self.mlp = nn.Sequential( + nn.Linear(frequency_embedding_size, hidden_size, bias=True), + nn.SiLU(), + nn.Linear(hidden_size, hidden_size, bias=True), + ) + self.frequency_embedding_size = frequency_embedding_size + + @staticmethod + def timestep_embedding(t, dim, max_period=10000): + """ + Create sinusoidal timestep embeddings. + :param t: a 1-D Tensor of N indices, one per batch element. + These may be fractional. + :param dim: the dimension of the output. + :param max_period: controls the minimum frequency of the embeddings. + :return: an (N, D) Tensor of positional embeddings. + """ + # https://github.com/openai/glide-text2im/blob/main/glide_text2im/nn.py + half = dim // 2 + freqs = torch.exp( + -math.log(max_period) * torch.arange(start=0, end=half, dtype=torch.float32, device=t.device) / half) + args = t[:, None].float() * freqs[None] + embedding = torch.cat([torch.cos(args), torch.sin(args)], dim=-1) + if dim % 2: + embedding = torch.cat([embedding, torch.zeros_like(embedding[:, :1])], dim=-1) + return embedding + + def forward(self, t): + t_freq = self.timestep_embedding(t, self.frequency_embedding_size) + t_emb = self.mlp(t_freq.to(t.dtype)) + return t_emb + + +class SizeEmbedder(TimestepEmbedder): + """ + Embeds scalar timesteps into vector representations. + """ + + def __init__(self, hidden_size, frequency_embedding_size=256): + super().__init__(hidden_size=hidden_size, frequency_embedding_size=frequency_embedding_size) + self.mlp = nn.Sequential( + nn.Linear(frequency_embedding_size, hidden_size, bias=True), + nn.SiLU(), + nn.Linear(hidden_size, hidden_size, bias=True), + ) + self.frequency_embedding_size = frequency_embedding_size + self.outdim = hidden_size + + def forward(self, s, bs): + if s.ndim == 1: + s = s[:, None] + assert s.ndim == 2 + if s.shape[0] != bs: + s = s.repeat(bs//s.shape[0], 1) + assert s.shape[0] == bs + b, dims = s.shape[0], s.shape[1] + s = rearrange(s, "b d -> (b d)") + s_freq = self.timestep_embedding(s, self.frequency_embedding_size) + s_emb = self.mlp(s_freq.to(s.dtype)) + s_emb = rearrange(s_emb, "(b d) d2 -> b (d d2)", b=b, d=dims, d2=self.outdim) + return s_emb + + +class LabelEmbedder(nn.Module): + """ + Embeds class labels into vector representations. Also handles label dropout for classifier-free guidance. + """ + + def __init__(self, num_classes, hidden_size, dropout_prob): + super().__init__() + use_cfg_embedding = dropout_prob > 0 + self.embedding_table = nn.Embedding(num_classes + use_cfg_embedding, hidden_size) + self.num_classes = num_classes + self.dropout_prob = dropout_prob + + def token_drop(self, labels, force_drop_ids=None): + """ + Drops labels to enable classifier-free guidance. + """ + if force_drop_ids is None: + drop_ids = torch.rand(labels.shape[0]).cuda() < self.dropout_prob + else: + drop_ids = force_drop_ids == 1 + labels = torch.where(drop_ids, self.num_classes, labels) + return labels + + def forward(self, labels, train, force_drop_ids=None): + use_dropout = self.dropout_prob > 0 + if (train and use_dropout) or (force_drop_ids is not None): + labels = self.token_drop(labels, force_drop_ids) + embeddings = self.embedding_table(labels) + return embeddings + + +class CaptionEmbedder(nn.Module): + """ + Embeds class labels into vector representations. Also handles label dropout for classifier-free guidance. + """ + + def __init__(self, in_channels, hidden_size, uncond_prob, act_layer=nn.GELU(approximate='tanh'), token_num=120): + super().__init__() + self.y_proj = Mlp(in_features=in_channels, hidden_features=hidden_size, out_features=hidden_size, act_layer=act_layer, drop=0) + self.register_buffer("y_embedding", nn.Parameter(torch.randn(token_num, in_channels) / in_channels ** 0.5)) + self.uncond_prob = uncond_prob + + def token_drop(self, caption, force_drop_ids=None): + """ + Drops labels to enable classifier-free guidance. + """ + if force_drop_ids is None: + drop_ids = torch.rand(caption.shape[0]).cuda() < self.uncond_prob + else: + drop_ids = force_drop_ids == 1 + caption = torch.where(drop_ids[:, None, None, None], self.y_embedding, caption) + return caption + + def forward(self, caption, train, force_drop_ids=None): + if train: + assert caption.shape[2:] == self.y_embedding.shape + use_dropout = self.uncond_prob > 0 + if (train and use_dropout) or (force_drop_ids is not None): + caption = self.token_drop(caption, force_drop_ids) + caption = self.y_proj(caption) + return caption + + +class CaptionEmbedderDoubleBr(nn.Module): + """ + Embeds class labels into vector representations. Also handles label dropout for classifier-free guidance. + """ + + def __init__(self, in_channels, hidden_size, uncond_prob, act_layer=nn.GELU(approximate='tanh'), token_num=120): + super().__init__() + self.proj = Mlp(in_features=in_channels, hidden_features=hidden_size, out_features=hidden_size, act_layer=act_layer, drop=0) + self.embedding = nn.Parameter(torch.randn(1, in_channels) / 10 ** 0.5) + self.y_embedding = nn.Parameter(torch.randn(token_num, in_channels) / 10 ** 0.5) + self.uncond_prob = uncond_prob + + def token_drop(self, global_caption, caption, force_drop_ids=None): + """ + Drops labels to enable classifier-free guidance. + """ + if force_drop_ids is None: + drop_ids = torch.rand(global_caption.shape[0]).cuda() < self.uncond_prob + else: + drop_ids = force_drop_ids == 1 + global_caption = torch.where(drop_ids[:, None], self.embedding, global_caption) + caption = torch.where(drop_ids[:, None, None, None], self.y_embedding, caption) + return global_caption, caption + + def forward(self, caption, train, force_drop_ids=None): + assert caption.shape[2: ] == self.y_embedding.shape + global_caption = caption.mean(dim=2).squeeze() + use_dropout = self.uncond_prob > 0 + if (train and use_dropout) or (force_drop_ids is not None): + global_caption, caption = self.token_drop(global_caption, caption, force_drop_ids) + y_embed = self.proj(global_caption) + return y_embed, caption diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/dit/pixArt/models/__init__.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/dit/pixArt/models/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/dit/pixArt/models/pixart_controlnet.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/dit/pixArt/models/pixart_controlnet.py new file mode 100644 index 0000000000000000000000000000000000000000..37fa4c1dcc0e6de44719c7fe897e2ecb0653eedb --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/dit/pixArt/models/pixart_controlnet.py @@ -0,0 +1,312 @@ +import re +import torch +import torch.nn as nn + +from copy import deepcopy +from torch import Tensor +from torch.nn import Module, Linear, init +from typing import Any, Mapping + +from .PixArt import PixArt, get_2d_sincos_pos_embed +from .PixArtMS import PixArtMSBlock, PixArtMS +from .utils import auto_grad_checkpoint + +# The implementation of ControlNet-Half architrecture +# https://github.com/lllyasviel/ControlNet/discussions/188 +class ControlT2IDitBlockHalf(Module): + def __init__(self, base_block: PixArtMSBlock, block_index: 0) -> None: + super().__init__() + self.copied_block = deepcopy(base_block) + self.block_index = block_index + + for p in self.copied_block.parameters(): + p.requires_grad_(True) + + self.copied_block.load_state_dict(base_block.state_dict()) + self.copied_block.train() + + self.hidden_size = hidden_size = base_block.hidden_size + if self.block_index == 0: + self.before_proj = Linear(hidden_size, hidden_size) + init.zeros_(self.before_proj.weight) + init.zeros_(self.before_proj.bias) + self.after_proj = Linear(hidden_size, hidden_size) + init.zeros_(self.after_proj.weight) + init.zeros_(self.after_proj.bias) + + def forward(self, x, y, t, mask=None, c=None): + + if self.block_index == 0: + # the first block + c = self.before_proj(c) + c = self.copied_block(x + c, y, t, mask) + c_skip = self.after_proj(c) + else: + # load from previous c and produce the c for skip connection + c = self.copied_block(c, y, t, mask) + c_skip = self.after_proj(c) + + return c, c_skip + + +# The implementation of ControlPixArtHalf net +class ControlPixArtHalf(Module): + # only support single res model + def __init__(self, base_model: PixArt, copy_blocks_num: int = 13) -> None: + super().__init__() + self.dtype = torch.get_default_dtype() + self.base_model = base_model.eval() + self.controlnet = [] + self.copy_blocks_num = copy_blocks_num + self.total_blocks_num = len(base_model.blocks) + for p in self.base_model.parameters(): + p.requires_grad_(False) + + # Copy first copy_blocks_num block + for i in range(copy_blocks_num): + self.controlnet.append(ControlT2IDitBlockHalf(base_model.blocks[i], i)) + self.controlnet = nn.ModuleList(self.controlnet) + + def __getattr__(self, name: str) -> Tensor or Module: + if name in ['forward', 'forward_with_dpmsolver', 'forward_with_cfg', 'forward_c', 'load_state_dict']: + return self.__dict__[name] + elif name in ['base_model', 'controlnet']: + return super().__getattr__(name) + else: + return getattr(self.base_model, name) + + def forward_c(self, c): + self.h, self.w = c.shape[-2]//self.patch_size, c.shape[-1]//self.patch_size + pos_embed = torch.from_numpy(get_2d_sincos_pos_embed(self.pos_embed.shape[-1], (self.h, self.w), lewei_scale=self.lewei_scale, base_size=self.base_size)).unsqueeze(0).to(c.device).to(self.dtype) + return self.x_embedder(c) + pos_embed if c is not None else c + + # def forward(self, x, t, c, **kwargs): + # return self.base_model(x, t, c=self.forward_c(c), **kwargs) + def forward_raw(self, x, timestep, y, mask=None, data_info=None, c=None, **kwargs): + # modify the original PixArtMS forward function + if c is not None: + c = c.to(self.dtype) + c = self.forward_c(c) + """ + Forward pass of PixArt. + x: (N, C, H, W) tensor of spatial inputs (images or latent representations of images) + t: (N,) tensor of diffusion timesteps + y: (N, 1, 120, C) tensor of class labels + """ + x = x.to(self.dtype) + timestep = timestep.to(self.dtype) + y = y.to(self.dtype) + pos_embed = self.pos_embed.to(self.dtype) + self.h, self.w = x.shape[-2]//self.patch_size, x.shape[-1]//self.patch_size + x = self.x_embedder(x) + pos_embed # (N, T, D), where T = H * W / patch_size ** 2 + t = self.t_embedder(timestep.to(x.dtype)) # (N, D) + t0 = self.t_block(t) + y = self.y_embedder(y, self.training) # (N, 1, L, D) + if mask is not None: + if mask.shape[0] != y.shape[0]: + mask = mask.repeat(y.shape[0] // mask.shape[0], 1) + mask = mask.squeeze(1).squeeze(1) + y = y.squeeze(1).masked_select(mask.unsqueeze(-1) != 0).view(1, -1, x.shape[-1]) + y_lens = mask.sum(dim=1).tolist() + else: + y_lens = [y.shape[2]] * y.shape[0] + y = y.squeeze(1).view(1, -1, x.shape[-1]) + + # define the first layer + x = auto_grad_checkpoint(self.base_model.blocks[0], x, y, t0, y_lens, **kwargs) # (N, T, D) #support grad checkpoint + + if c is not None: + # update c + for index in range(1, self.copy_blocks_num + 1): + c, c_skip = auto_grad_checkpoint(self.controlnet[index - 1], x, y, t0, y_lens, c, **kwargs) + x = auto_grad_checkpoint(self.base_model.blocks[index], x + c_skip, y, t0, y_lens, **kwargs) + + # update x + for index in range(self.copy_blocks_num + 1, self.total_blocks_num): + x = auto_grad_checkpoint(self.base_model.blocks[index], x, y, t0, y_lens, **kwargs) + else: + for index in range(1, self.total_blocks_num): + x = auto_grad_checkpoint(self.base_model.blocks[index], x, y, t0, y_lens, **kwargs) + + x = self.final_layer(x, t) # (N, T, patch_size ** 2 * out_channels) + x = self.unpatchify(x) # (N, out_channels, H, W) + return x + + def forward(self, x, timesteps, context, cn_hint=None, **kwargs): + """ + Forward pass that adapts comfy input to original forward function + x: (N, C, H, W) tensor of spatial inputs (images or latent representations of images) + timesteps: (N,) tensor of diffusion timesteps + context: (N, 1, 120, C) conditioning + cn_hint: controlnet hint + """ + ## Still accepts the input w/o that dim but returns garbage + if len(context.shape) == 3: + context = context.unsqueeze(1) + + ## run original forward pass + out = self.forward_raw( + x = x.to(self.dtype), + timestep = timesteps.to(self.dtype), + y = context.to(self.dtype), + c = cn_hint, + ) + + ## only return EPS + out = out.to(torch.float) + eps, rest = out[:, :self.in_channels], out[:, self.in_channels:] + return eps + + def forward_with_dpmsolver(self, x, t, y, data_info, c, **kwargs): + model_out = self.forward_raw(x, t, y, data_info=data_info, c=c, **kwargs) + return model_out.chunk(2, dim=1)[0] + + # def forward_with_dpmsolver(self, x, t, y, data_info, c, **kwargs): + # return self.base_model.forward_with_dpmsolver(x, t, y, data_info=data_info, c=self.forward_c(c), **kwargs) + + def forward_with_cfg(self, x, t, y, cfg_scale, data_info, c, **kwargs): + return self.base_model.forward_with_cfg(x, t, y, cfg_scale, data_info, c=self.forward_c(c), **kwargs) + + def load_state_dict(self, state_dict: Mapping[str, Any], strict: bool = True): + if all((k.startswith('base_model') or k.startswith('controlnet')) for k in state_dict.keys()): + return super().load_state_dict(state_dict, strict) + else: + new_key = {} + for k in state_dict.keys(): + new_key[k] = re.sub(r"(blocks\.\d+)(.*)", r"\1.base_block\2", k) + for k, v in new_key.items(): + if k != v: + print(f"replace {k} to {v}") + state_dict[v] = state_dict.pop(k) + + return self.base_model.load_state_dict(state_dict, strict) + + def unpatchify(self, x): + """ + x: (N, T, patch_size**2 * C) + imgs: (N, H, W, C) + """ + c = self.out_channels + p = self.x_embedder.patch_size[0] + assert self.h * self.w == x.shape[1] + + x = x.reshape(shape=(x.shape[0], self.h, self.w, p, p, c)) + x = torch.einsum('nhwpqc->nchpwq', x) + imgs = x.reshape(shape=(x.shape[0], c, self.h * p, self.w * p)) + return imgs + + # @property + # def dtype(self): + ## 返回模型参数的数据类型 + # return next(self.parameters()).dtype + + +# The implementation for PixArtMS_Half + 1024 resolution +class ControlPixArtMSHalf(ControlPixArtHalf): + # support multi-scale res model (multi-scale model can also be applied to single reso training & inference) + def __init__(self, base_model: PixArtMS, copy_blocks_num: int = 13) -> None: + super().__init__(base_model=base_model, copy_blocks_num=copy_blocks_num) + + def forward_raw(self, x, timestep, y, mask=None, data_info=None, c=None, **kwargs): + # modify the original PixArtMS forward function + """ + Forward pass of PixArt. + x: (N, C, H, W) tensor of spatial inputs (images or latent representations of images) + t: (N,) tensor of diffusion timesteps + y: (N, 1, 120, C) tensor of class labels + """ + if c is not None: + c = c.to(self.dtype) + c = self.forward_c(c) + bs = x.shape[0] + x = x.to(self.dtype) + timestep = timestep.to(self.dtype) + y = y.to(self.dtype) + c_size, ar = data_info['img_hw'].to(self.dtype), data_info['aspect_ratio'].to(self.dtype) + self.h, self.w = x.shape[-2]//self.patch_size, x.shape[-1]//self.patch_size + + pos_embed = torch.from_numpy(get_2d_sincos_pos_embed(self.pos_embed.shape[-1], (self.h, self.w), lewei_scale=self.lewei_scale, base_size=self.base_size)).unsqueeze(0).to(x.device).to(self.dtype) + x = self.x_embedder(x) + pos_embed # (N, T, D), where T = H * W / patch_size ** 2 + t = self.t_embedder(timestep) # (N, D) + csize = self.csize_embedder(c_size, bs) # (N, D) + ar = self.ar_embedder(ar, bs) # (N, D) + t = t + torch.cat([csize, ar], dim=1) + t0 = self.t_block(t) + y = self.y_embedder(y, self.training) # (N, D) + if mask is not None: + if mask.shape[0] != y.shape[0]: + mask = mask.repeat(y.shape[0] // mask.shape[0], 1) + mask = mask.squeeze(1).squeeze(1) + y = y.squeeze(1).masked_select(mask.unsqueeze(-1) != 0).view(1, -1, x.shape[-1]) + y_lens = mask.sum(dim=1).tolist() + else: + y_lens = [y.shape[2]] * y.shape[0] + y = y.squeeze(1).view(1, -1, x.shape[-1]) + + # define the first layer + x = auto_grad_checkpoint(self.base_model.blocks[0], x, y, t0, y_lens, **kwargs) # (N, T, D) #support grad checkpoint + + if c is not None: + # update c + for index in range(1, self.copy_blocks_num + 1): + c, c_skip = auto_grad_checkpoint(self.controlnet[index - 1], x, y, t0, y_lens, c, **kwargs) + x = auto_grad_checkpoint(self.base_model.blocks[index], x + c_skip, y, t0, y_lens, **kwargs) + + # update x + for index in range(self.copy_blocks_num + 1, self.total_blocks_num): + x = auto_grad_checkpoint(self.base_model.blocks[index], x, y, t0, y_lens, **kwargs) + else: + for index in range(1, self.total_blocks_num): + x = auto_grad_checkpoint(self.base_model.blocks[index], x, y, t0, y_lens, **kwargs) + + x = self.final_layer(x, t) # (N, T, patch_size ** 2 * out_channels) + x = self.unpatchify(x) # (N, out_channels, H, W) + return x + + def forward(self, x, timesteps, context, img_hw=None, aspect_ratio=None, cn_hint=None, **kwargs): + """ + Forward pass that adapts comfy input to original forward function + x: (N, C, H, W) tensor of spatial inputs (images or latent representations of images) + timesteps: (N,) tensor of diffusion timesteps + context: (N, 1, 120, C) conditioning + img_hw: height|width conditioning + aspect_ratio: aspect ratio conditioning + cn_hint: controlnet hint + """ + ## size/ar from cond with fallback based on the latent image shape. + bs = x.shape[0] + data_info = {} + if img_hw is None: + data_info["img_hw"] = torch.tensor( + [[x.shape[2]*8, x.shape[3]*8]], + dtype=self.dtype, + device=x.device + ).repeat(bs, 1) + else: + data_info["img_hw"] = img_hw.to(x.dtype) + if aspect_ratio is None or True: + data_info["aspect_ratio"] = torch.tensor( + [[x.shape[2]/x.shape[3]]], + dtype=self.dtype, + device=x.device + ).repeat(bs, 1) + else: + data_info["aspect_ratio"] = aspect_ratio.to(x.dtype) + + ## Still accepts the input w/o that dim but returns garbage + if len(context.shape) == 3: + context = context.unsqueeze(1) + + ## run original forward pass + out = self.forward_raw( + x = x.to(self.dtype), + timestep = timesteps.to(self.dtype), + y = context.to(self.dtype), + c = cn_hint, + data_info=data_info, + ) + + ## only return EPS + out = out.to(torch.float) + eps, rest = out[:, :self.in_channels], out[:, self.in_channels:] + return eps diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/dit/pixArt/models/utils.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/dit/pixArt/models/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..9f7762148226a32b7c20d8527fa144a14acc275c --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/dit/pixArt/models/utils.py @@ -0,0 +1,122 @@ +import torch +import torch.nn as nn +import torch.nn.functional as F +from torch.utils.checkpoint import checkpoint, checkpoint_sequential +from collections.abc import Iterable +from itertools import repeat + +def _ntuple(n): + def parse(x): + if isinstance(x, Iterable) and not isinstance(x, str): + return x + return tuple(repeat(x, n)) + return parse + +to_1tuple = _ntuple(1) +to_2tuple = _ntuple(2) + +def set_grad_checkpoint(model, use_fp32_attention=False, gc_step=1): + assert isinstance(model, nn.Module) + + def set_attr(module): + module.grad_checkpointing = True + module.fp32_attention = use_fp32_attention + module.grad_checkpointing_step = gc_step + model.apply(set_attr) + +def auto_grad_checkpoint(module, *args, **kwargs): + if getattr(module, 'grad_checkpointing', False): + if isinstance(module, Iterable): + gc_step = module[0].grad_checkpointing_step + return checkpoint_sequential(module, gc_step, *args, **kwargs) + else: + return checkpoint(module, *args, **kwargs) + return module(*args, **kwargs) + +def checkpoint_sequential(functions, step, input, *args, **kwargs): + + # Hack for keyword-only parameter in a python 2.7-compliant way + preserve = kwargs.pop('preserve_rng_state', True) + if kwargs: + raise ValueError("Unexpected keyword arguments: " + ",".join(arg for arg in kwargs)) + + def run_function(start, end, functions): + def forward(input): + for j in range(start, end + 1): + input = functions[j](input, *args) + return input + return forward + + if isinstance(functions, torch.nn.Sequential): + functions = list(functions.children()) + + # the last chunk has to be non-volatile + end = -1 + segment = len(functions) // step + for start in range(0, step * (segment - 1), step): + end = start + step - 1 + input = checkpoint(run_function(start, end, functions), input, preserve_rng_state=preserve) + return run_function(end + 1, len(functions) - 1, functions)(input) + +def get_rel_pos(q_size, k_size, rel_pos): + """ + Get relative positional embeddings according to the relative positions of + query and key sizes. + Args: + q_size (int): size of query q. + k_size (int): size of key k. + rel_pos (Tensor): relative position embeddings (L, C). + + Returns: + Extracted positional embeddings according to relative positions. + """ + max_rel_dist = int(2 * max(q_size, k_size) - 1) + # Interpolate rel pos if needed. + if rel_pos.shape[0] != max_rel_dist: + # Interpolate rel pos. + rel_pos_resized = F.interpolate( + rel_pos.reshape(1, rel_pos.shape[0], -1).permute(0, 2, 1), + size=max_rel_dist, + mode="linear", + ) + rel_pos_resized = rel_pos_resized.reshape(-1, max_rel_dist).permute(1, 0) + else: + rel_pos_resized = rel_pos + + # Scale the coords with short length if shapes for q and k are different. + q_coords = torch.arange(q_size)[:, None] * max(k_size / q_size, 1.0) + k_coords = torch.arange(k_size)[None, :] * max(q_size / k_size, 1.0) + relative_coords = (q_coords - k_coords) + (k_size - 1) * max(q_size / k_size, 1.0) + + return rel_pos_resized[relative_coords.long()] + +def add_decomposed_rel_pos(attn, q, rel_pos_h, rel_pos_w, q_size, k_size): + """ + Calculate decomposed Relative Positional Embeddings from :paper:`mvitv2`. + https://github.com/facebookresearch/mvit/blob/19786631e330df9f3622e5402b4a419a263a2c80/mvit/models/attention.py # noqa B950 + Args: + attn (Tensor): attention map. + q (Tensor): query q in the attention layer with shape (B, q_h * q_w, C). + rel_pos_h (Tensor): relative position embeddings (Lh, C) for height axis. + rel_pos_w (Tensor): relative position embeddings (Lw, C) for width axis. + q_size (Tuple): spatial sequence size of query q with (q_h, q_w). + k_size (Tuple): spatial sequence size of key k with (k_h, k_w). + + Returns: + attn (Tensor): attention map with added relative positional embeddings. + """ + q_h, q_w = q_size + k_h, k_w = k_size + Rh = get_rel_pos(q_h, k_h, rel_pos_h) + Rw = get_rel_pos(q_w, k_w, rel_pos_w) + + B, _, dim = q.shape + r_q = q.reshape(B, q_h, q_w, dim) + rel_h = torch.einsum("bhwc,hkc->bhwk", r_q, Rh) + rel_w = torch.einsum("bhwc,wkc->bhwk", r_q, Rw) + + attn = ( + attn.view(B, q_h, q_w, k_h, k_w) + rel_h[:, :, :, :, None] + rel_w[:, :, :, None, :] + ).view(B, q_h * q_w, k_h * k_w) + + return attn diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/dit/utils.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/dit/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..22bf8e9e4f54f538167f841980ebbdea11407b3f --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/dit/utils.py @@ -0,0 +1,38 @@ +import torch +from comfy import model_management + +def string_to_dtype(s="none", mode=None): + s = s.lower().strip() + if s in ["default", "as-is"]: + return None + elif s in ["auto", "auto (comfy)"]: + if mode == "vae": + return model_management.vae_device() + elif mode == "text_encoder": + return model_management.text_encoder_dtype() + elif mode == "unet": + return model_management.unet_dtype() + else: + raise NotImplementedError(f"Unknown dtype mode '{mode}'") + elif s in ["none", "auto (hf)", "auto (hf/bnb)"]: + return None + elif s in ["fp32", "float32", "float"]: + return torch.float32 + elif s in ["bf16", "bfloat16"]: + return torch.bfloat16 + elif s in ["fp16", "float16", "half"]: + return torch.float16 + elif "fp8" in s or "float8" in s: + if "e5m2" in s: + return torch.float8_e5m2 + elif "e4m3" in s: + return torch.float8_e4m3fn + else: + raise NotImplementedError(f"Unknown 8bit dtype '{s}'") + elif "bnb" in s: + assert s in ["bnb8bit", "bnb4bit"], f"Unknown bnb mode '{s}'" + return s + elif s is None: + return None + else: + raise NotImplementedError(f"Unknown dtype '{s}'") \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/fooocus/__init__.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/fooocus/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..4ab3c199ff592f4dabebca84d7453afc9ebb2b63 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/fooocus/__init__.py @@ -0,0 +1,139 @@ +#credit to Acly for this module +#from https://github.com/Acly/comfyui-inpaint-nodes +import torch +import torch.nn.functional as F +import comfy +from comfy.model_base import BaseModel +from comfy.model_patcher import ModelPatcher +from comfy.model_management import cast_to_device + +from ...libs.log import log_node_warn, log_node_error, log_node_info + +class InpaintHead(torch.nn.Module): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.head = torch.nn.Parameter(torch.empty(size=(320, 5, 3, 3), device="cpu")) + + def __call__(self, x): + x = F.pad(x, (1, 1, 1, 1), "replicate") + return F.conv2d(x, weight=self.head) + +# injected_model_patcher_calculate_weight = False +# original_calculate_weight = None + +class applyFooocusInpaint: + def calculate_weight_patched(self, patches, weight, key, intermediate_dtype=torch.float32): + remaining = [] + + for p in patches: + alpha = p[0] + v = p[1] + + is_fooocus_patch = isinstance(v, tuple) and len(v) == 2 and v[0] == "fooocus" + if not is_fooocus_patch: + remaining.append(p) + continue + + if alpha != 0.0: + v = v[1] + w1 = cast_to_device(v[0], weight.device, torch.float32) + if w1.shape == weight.shape: + w_min = cast_to_device(v[1], weight.device, torch.float32) + w_max = cast_to_device(v[2], weight.device, torch.float32) + w1 = (w1 / 255.0) * (w_max - w_min) + w_min + weight += alpha * cast_to_device(w1, weight.device, weight.dtype) + else: + print( + f"[ApplyFooocusInpaint] Shape mismatch {key}, weight not merged ({w1.shape} != {weight.shape})" + ) + + if len(remaining) > 0: + return self.original_calculate_weight(remaining, weight, key, intermediate_dtype) + return weight + + def __enter__(self): + try: + print("[comfyui-easy-use] Injecting patched comfy.lora.calculate_weight.calculate_weight") + self.original_calculate_weight = comfy.lora.calculate_weight + comfy.lora.calculate_weight = self.calculate_weight_patched + except AttributeError: + print("[comfyui-easy-use] Injecting patched comfy.model_patcher.ModelPatcher.calculate_weight") + self.original_calculate_weight = ModelPatcher.calculate_weight + ModelPatcher.calculate_weight = self.calculate_weight_patched + + def __exit__(self, exc_type, exc_value, traceback): + try: + comfy.lora.calculate_weight = self.original_calculate_weight + except: + ModelPatcher.calculate_weight = self.original_calculate_weight + +# def inject_patched_calculate_weight(): +# global injected_model_patcher_calculate_weight +# if not injected_model_patcher_calculate_weight: +# try: +# print("[comfyui-easy-use] Injecting patched comfy.lora.calculate_weight.calculate_weight") +# original_calculate_weight = comfy.lora.calculate_weight +# comfy.lora.original_calculate_weight = original_calculate_weight +# comfy.lora.calculate_weight = calculate_weight_patched +# except AttributeError: +# print("[comfyui-easy-use] Injecting patched comfy.model_patcher.ModelPatcher.calculate_weight") +# original_calculate_weight = ModelPatcher.calculate_weight +# ModelPatcher.original_calculate_weight = original_calculate_weight +# ModelPatcher.calculate_weight = calculate_weight_patched +# injected_model_patcher_calculate_weight = True + + +class InpaintWorker: + def __init__(self, node_name): + self.node_name = node_name if node_name is not None else "" + + def load_fooocus_patch(self, lora: dict, to_load: dict): + patch_dict = {} + loaded_keys = set() + for key in to_load.values(): + if value := lora.get(key, None): + patch_dict[key] = ("fooocus", value) + loaded_keys.add(key) + + not_loaded = sum(1 for x in lora if x not in loaded_keys) + if not_loaded > 0: + log_node_info(self.node_name, + f"{len(loaded_keys)} Lora keys loaded, {not_loaded} remaining keys not found in model." + ) + return patch_dict + + def _input_block_patch(self, h: torch.Tensor, transformer_options: dict): + if transformer_options["block"][1] == 0: + if self._inpaint_block is None or self._inpaint_block.shape != h.shape: + assert self._inpaint_head_feature is not None + batch = h.shape[0] // self._inpaint_head_feature.shape[0] + self._inpaint_block = self._inpaint_head_feature.to(h).repeat(batch, 1, 1, 1) + h = h + self._inpaint_block + return h + + def patch(self, model, latent, patch): + base_model: BaseModel = model.model + latent_pixels = base_model.process_latent_in(latent["samples"]) + noise_mask = latent["noise_mask"].round() + latent_mask = F.max_pool2d(noise_mask, (8, 8)).round().to(latent_pixels) + + inpaint_head_model, inpaint_lora = patch + feed = torch.cat([latent_mask, latent_pixels], dim=1) + inpaint_head_model.to(device=feed.device, dtype=feed.dtype) + self._inpaint_head_feature = inpaint_head_model(feed) + self._inpaint_block = None + + lora_keys = comfy.lora.model_lora_keys_unet(model.model, {}) + lora_keys.update({x: x for x in base_model.state_dict().keys()}) + loaded_lora = self.load_fooocus_patch(inpaint_lora, lora_keys) + + m = model.clone() + m.set_model_input_block_patch(self._input_block_patch) + patched = m.add_patches(loaded_lora, 1.0) + m.model_options['transformer_options']['fooocus'] = True + not_patched_count = sum(1 for x in loaded_lora if x not in patched) + if not_patched_count > 0: + log_node_error(self.node_name, f"Failed to patch {not_patched_count} keys") + + # inject_patched_calculate_weight() + return (m,) \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/human_parsing/__init__.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/human_parsing/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/human_parsing/parsing_api.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/human_parsing/parsing_api.py new file mode 100644 index 0000000000000000000000000000000000000000..610d1c3cd045f2d75f40cd03d30b962595345add --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/human_parsing/parsing_api.py @@ -0,0 +1,156 @@ +import torch +import numpy as np +import cv2 +import torchvision.transforms as transforms +from torch.utils.data import DataLoader +from .simple_extractor_dataset import SimpleFolderDataset +from .transforms import transform_logits +from tqdm import tqdm +from PIL import Image + +def get_palette(num_cls): + """ Returns the color map for visualizing the segmentation mask. + Args: + num_cls: Number of classes + Returns: + The color map + """ + n = num_cls + palette = [0] * (n * 3) + for j in range(0, n): + lab = j + palette[j * 3 + 0] = 0 + palette[j * 3 + 1] = 0 + palette[j * 3 + 2] = 0 + i = 0 + while lab: + palette[j * 3 + 0] |= (((lab >> 0) & 1) << (7 - i)) + palette[j * 3 + 1] |= (((lab >> 1) & 1) << (7 - i)) + palette[j * 3 + 2] |= (((lab >> 2) & 1) << (7 - i)) + i += 1 + lab >>= 3 + return palette + + +def delete_irregular(logits_result): + parsing_result = np.argmax(logits_result, axis=2) + upper_cloth = np.where(parsing_result == 4, 255, 0) + contours, hierarchy = cv2.findContours(upper_cloth.astype(np.uint8), + cv2.RETR_CCOMP, cv2.CHAIN_APPROX_TC89_L1) + area = [] + for i in range(len(contours)): + a = cv2.contourArea(contours[i], True) + area.append(abs(a)) + if len(area) != 0: + top = area.index(max(area)) + M = cv2.moments(contours[top]) + cY = int(M["m01"] / M["m00"]) + + dresses = np.where(parsing_result == 7, 255, 0) + contours_dress, hierarchy_dress = cv2.findContours(dresses.astype(np.uint8), + cv2.RETR_CCOMP, cv2.CHAIN_APPROX_TC89_L1) + area_dress = [] + for j in range(len(contours_dress)): + a_d = cv2.contourArea(contours_dress[j], True) + area_dress.append(abs(a_d)) + if len(area_dress) != 0: + top_dress = area_dress.index(max(area_dress)) + M_dress = cv2.moments(contours_dress[top_dress]) + cY_dress = int(M_dress["m01"] / M_dress["m00"]) + wear_type = "dresses" + if len(area) != 0: + if len(area_dress) != 0 and cY_dress > cY: + irregular_list = np.array([4, 5, 6]) + logits_result[:, :, irregular_list] = -1 + else: + irregular_list = np.array([5, 6, 7, 8, 9, 10, 12, 13]) + logits_result[:cY, :, irregular_list] = -1 + wear_type = "cloth_pant" + parsing_result = np.argmax(logits_result, axis=2) + # pad border + parsing_result = np.pad(parsing_result, pad_width=1, mode='constant', constant_values=0) + return parsing_result, wear_type + + + +def hole_fill(img): + img_copy = img.copy() + mask = np.zeros((img.shape[0] + 2, img.shape[1] + 2), dtype=np.uint8) + cv2.floodFill(img, mask, (0, 0), 255) + img_inverse = cv2.bitwise_not(img) + dst = cv2.bitwise_or(img_copy, img_inverse) + return dst + +def refine_mask(mask): + contours, hierarchy = cv2.findContours(mask.astype(np.uint8), + cv2.RETR_CCOMP, cv2.CHAIN_APPROX_TC89_L1) + area = [] + for j in range(len(contours)): + a_d = cv2.contourArea(contours[j], True) + area.append(abs(a_d)) + refine_mask = np.zeros_like(mask).astype(np.uint8) + if len(area) != 0: + i = area.index(max(area)) + cv2.drawContours(refine_mask, contours, i, color=255, thickness=-1) + # keep large area in skin case + for j in range(len(area)): + if j != i and area[i] > 2000: + cv2.drawContours(refine_mask, contours, j, color=255, thickness=-1) + return refine_mask + +def refine_hole(parsing_result_filled, parsing_result, arm_mask): + filled_hole = cv2.bitwise_and(np.where(parsing_result_filled == 4, 255, 0), + np.where(parsing_result != 4, 255, 0)) - arm_mask * 255 + contours, hierarchy = cv2.findContours(filled_hole, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_TC89_L1) + refine_hole_mask = np.zeros_like(parsing_result).astype(np.uint8) + for i in range(len(contours)): + a = cv2.contourArea(contours[i], True) + # keep hole > 2000 pixels + if abs(a) > 2000: + cv2.drawContours(refine_hole_mask, contours, i, color=255, thickness=-1) + return refine_hole_mask + arm_mask + +def onnx_inference(lip_session, input_dir, mask_components=[0]): + + transform = transforms.Compose([ + transforms.ToTensor(), + transforms.Normalize(mean=[0.406, 0.456, 0.485], std=[0.225, 0.224, 0.229]) + ]) + input_size = [473, 473] + + dataset_lip = SimpleFolderDataset(root=input_dir, input_size=input_size, transform=transform) + dataloader_lip = DataLoader(dataset_lip) + palette = get_palette(20) + with torch.no_grad(): + for _, batch in enumerate(tqdm(dataloader_lip)): + image, meta = batch + c = meta['center'].numpy()[0] + s = meta['scale'].numpy()[0] + w = meta['width'].numpy()[0] + h = meta['height'].numpy()[0] + + output = lip_session.run(None, {"input.1": image.numpy().astype(np.float32)}) + upsample = torch.nn.Upsample(size=input_size, mode='bilinear', align_corners=True) + upsample_output = upsample(torch.from_numpy(output[1][0]).unsqueeze(0)) + upsample_output = upsample_output.squeeze() + upsample_output = upsample_output.permute(1, 2, 0) # CHW -> HWC + logits_result_lip = transform_logits(upsample_output.data.cpu().numpy(), c, s, w, h, + input_size=input_size) + parsing_result = np.argmax(logits_result_lip, axis=2) + + output_img = Image.fromarray(np.asarray(parsing_result, dtype=np.uint8)) + output_img.putpalette(palette) + + mask = np.isin(output_img, mask_components).astype(np.uint8) + mask_image = Image.fromarray(mask * 255) + mask_image = mask_image.convert("RGB") + mask_image = torch.from_numpy(np.array(mask_image).astype(np.float32) / 255.0).unsqueeze(0) + + output_img = output_img.convert('RGB') + output_img = torch.from_numpy(np.array(output_img).astype(np.float32) / 255.0).unsqueeze(0) + + return output_img, mask_image + + + + diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/human_parsing/run_parsing.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/human_parsing/run_parsing.py new file mode 100644 index 0000000000000000000000000000000000000000..ebdf1556b2722092003d8e8d8b6ec89154574cf8 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/human_parsing/run_parsing.py @@ -0,0 +1,109 @@ +import numpy as np +import torch +from PIL import Image +from .parsing_api import onnx_inference +from ...libs.utils import install_package + +class HumanParsing: + def __init__(self, model_path): + self.model_path = model_path + self.session = None + + def __call__(self, input_image, mask_components): + if self.session is None: + install_package('onnxruntime') + import onnxruntime as ort + + session_options = ort.SessionOptions() + session_options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL + session_options.execution_mode = ort.ExecutionMode.ORT_SEQUENTIAL + # session_options.add_session_config_entry('gpu_id', str(gpu_id)) + self.session = ort.InferenceSession(self.model_path, sess_options=session_options, + providers=['CUDAExecutionProvider', 'CPUExecutionProvider']) + + parsed_image, mask = onnx_inference(self.session, input_image, mask_components) + return parsed_image, mask + + +class HumanParts: + + def __init__(self, model_path): + self.model_path = model_path + self.session = None + # self.classes_dict = { + # "background": 0, + # "hair": 2, + # "glasses": 4, + # "top-clothes": 5, + # "bottom-clothes": 9, + # "torso-skin": 10, + # "face": 13, + # "left-arm": 14, + # "right-arm": 15, + # "left-leg": 16, + # "right-leg": 17, + # "left-foot": 18, + # "right-foot": 19, + # }, + self.classes = [0, 13, 2, 4, 5, 9, 10, 14, 15, 16, 17, 18, 19] + + + def __call__(self, input_image, mask_components): + if self.session is None: + install_package('onnxruntime') + import onnxruntime as ort + + self.session = ort.InferenceSession(self.model_path, providers=['TensorrtExecutionProvider', 'CUDAExecutionProvider', 'CPUExecutionProvider']) + + mask, = self.get_mask(self.session, input_image, 0, mask_components) + return mask + + def get_mask(self, model, image, rotation, mask_components): + image = image.squeeze(0) + image_np = image.numpy() * 255 + + pil_image = Image.fromarray(image_np.astype(np.uint8)) + original_size = pil_image.size # to resize the mask later + # resize to 512x512 as the model expects + pil_image = pil_image.resize((512, 512)) + center = (256, 256) + + if rotation != 0: + pil_image = pil_image.rotate(rotation, center=center) + + # normalize the image + image_np = np.array(pil_image).astype(np.float32) / 127.5 - 1 + image_np = np.expand_dims(image_np, axis=0) + + # use the onnx model to get the mask + input_name = model.get_inputs()[0].name + output_name = model.get_outputs()[0].name + result = model.run([output_name], {input_name: image_np}) + result = np.array(result[0]).argmax(axis=3).squeeze(0) + + score: int = 0 + + mask = np.zeros_like(result) + for class_index in mask_components: + detected = result == self.classes[class_index] + mask[detected] = 255 + score += mask.sum() + + # back to the original size + mask_image = Image.fromarray(mask.astype(np.uint8), mode="L") + if rotation != 0: + mask_image = mask_image.rotate(-rotation, center=center) + + mask_image = mask_image.resize(original_size) + + # and back to numpy... + mask = np.array(mask_image).astype(np.float32) / 255 + + # add 2 dimensions to match the expected output + mask = np.expand_dims(mask, axis=0) + mask = np.expand_dims(mask, axis=0) + # ensure to return a "binary mask_image" + + del image_np, result # free up memory, maybe not necessary + + return (torch.from_numpy(mask.astype(np.uint8)),) \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/human_parsing/simple_extractor_dataset.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/human_parsing/simple_extractor_dataset.py new file mode 100644 index 0000000000000000000000000000000000000000..5604b1eda242f3214a123684b727f2fe0768ce1f --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/human_parsing/simple_extractor_dataset.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- + +""" +@Author : Peike Li +@Contact : peike.li@yahoo.com +@File : dataset.py +@Time : 8/30/19 9:12 PM +@Desc : Dataset Definition +@License : This source code is licensed under the license found in the + LICENSE file in the root directory of this source tree. +""" + +import os + +import cv2 +import numpy as np +from PIL import Image +from torch.utils import data +from .transforms import get_affine_transform + + +class SimpleFolderDataset(data.Dataset): + def __init__(self, root, input_size=[512, 512], transform=None): + self.root = root + self.input_size = input_size + self.transform = transform + self.aspect_ratio = input_size[1] * 1.0 / input_size[0] + self.input_size = np.asarray(input_size) + self.is_pil_image = False + if isinstance(root, Image.Image): + self.file_list = [root] + self.is_pil_image = True + elif os.path.isfile(root): + self.file_list = [os.path.basename(root)] + self.root = os.path.dirname(root) + else: + self.file_list = os.listdir(self.root) + + def __len__(self): + return len(self.file_list) + + def _box2cs(self, box): + x, y, w, h = box[:4] + return self._xywh2cs(x, y, w, h) + + def _xywh2cs(self, x, y, w, h): + center = np.zeros((2), dtype=np.float32) + center[0] = x + w * 0.5 + center[1] = y + h * 0.5 + if w > self.aspect_ratio * h: + h = w * 1.0 / self.aspect_ratio + elif w < self.aspect_ratio * h: + w = h * self.aspect_ratio + scale = np.array([w, h], dtype=np.float32) + return center, scale + + def __getitem__(self, index): + if self.is_pil_image: + img = np.asarray(self.file_list[index])[:, :, [2, 1, 0]] + else: + img_name = self.file_list[index] + img_path = os.path.join(self.root, img_name) + img = cv2.imread(img_path, cv2.IMREAD_COLOR) + h, w, _ = img.shape + + # Get person center and scale + person_center, s = self._box2cs([0, 0, w - 1, h - 1]) + r = 0 + trans = get_affine_transform(person_center, s, r, self.input_size) + input = cv2.warpAffine( + img, + trans, + (int(self.input_size[1]), int(self.input_size[0])), + flags=cv2.INTER_LINEAR, + borderMode=cv2.BORDER_CONSTANT, + borderValue=(0, 0, 0)) + + input = self.transform(input) + meta = { + 'center': person_center, + 'height': h, + 'width': w, + 'scale': s, + 'rotation': r + } + + return input, meta diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/human_parsing/transforms.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/human_parsing/transforms.py new file mode 100644 index 0000000000000000000000000000000000000000..1442a728938ca19fcb4ac21ae6588266df45631c --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/human_parsing/transforms.py @@ -0,0 +1,167 @@ +# ------------------------------------------------------------------------------ +# Copyright (c) Microsoft +# Licensed under the MIT License. +# Written by Bin Xiao (Bin.Xiao@microsoft.com) +# ------------------------------------------------------------------------------ + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import numpy as np +import cv2 +import torch + +class BRG2Tensor_transform(object): + def __call__(self, pic): + img = torch.from_numpy(pic.transpose((2, 0, 1))) + if isinstance(img, torch.ByteTensor): + return img.float() + else: + return img + +class BGR2RGB_transform(object): + def __call__(self, tensor): + return tensor[[2,1,0],:,:] + +def flip_back(output_flipped, matched_parts): + ''' + ouput_flipped: numpy.ndarray(batch_size, num_joints, height, width) + ''' + assert output_flipped.ndim == 4,\ + 'output_flipped should be [batch_size, num_joints, height, width]' + + output_flipped = output_flipped[:, :, :, ::-1] + + for pair in matched_parts: + tmp = output_flipped[:, pair[0], :, :].copy() + output_flipped[:, pair[0], :, :] = output_flipped[:, pair[1], :, :] + output_flipped[:, pair[1], :, :] = tmp + + return output_flipped + + +def fliplr_joints(joints, joints_vis, width, matched_parts): + """ + flip coords + """ + # Flip horizontal + joints[:, 0] = width - joints[:, 0] - 1 + + # Change left-right parts + for pair in matched_parts: + joints[pair[0], :], joints[pair[1], :] = \ + joints[pair[1], :], joints[pair[0], :].copy() + joints_vis[pair[0], :], joints_vis[pair[1], :] = \ + joints_vis[pair[1], :], joints_vis[pair[0], :].copy() + + return joints*joints_vis, joints_vis + + +def transform_preds(coords, center, scale, input_size): + target_coords = np.zeros(coords.shape) + trans = get_affine_transform(center, scale, 0, input_size, inv=1) + for p in range(coords.shape[0]): + target_coords[p, 0:2] = affine_transform(coords[p, 0:2], trans) + return target_coords + +def transform_parsing(pred, center, scale, width, height, input_size): + + trans = get_affine_transform(center, scale, 0, input_size, inv=1) + target_pred = cv2.warpAffine( + pred, + trans, + (int(width), int(height)), #(int(width), int(height)), + flags=cv2.INTER_NEAREST, + borderMode=cv2.BORDER_CONSTANT, + borderValue=(0)) + + return target_pred + +def transform_logits(logits, center, scale, width, height, input_size): + + trans = get_affine_transform(center, scale, 0, input_size, inv=1) + channel = logits.shape[2] + target_logits = [] + for i in range(channel): + target_logit = cv2.warpAffine( + logits[:,:,i], + trans, + (int(width), int(height)), #(int(width), int(height)), + flags=cv2.INTER_LINEAR, + borderMode=cv2.BORDER_CONSTANT, + borderValue=(0)) + target_logits.append(target_logit) + target_logits = np.stack(target_logits,axis=2) + + return target_logits + + +def get_affine_transform(center, + scale, + rot, + output_size, + shift=np.array([0, 0], dtype=np.float32), + inv=0): + if not isinstance(scale, np.ndarray) and not isinstance(scale, list): + print(scale) + scale = np.array([scale, scale]) + + scale_tmp = scale + + src_w = scale_tmp[0] + dst_w = output_size[1] + dst_h = output_size[0] + + rot_rad = np.pi * rot / 180 + src_dir = get_dir([0, src_w * -0.5], rot_rad) + dst_dir = np.array([0, (dst_w-1) * -0.5], np.float32) + + src = np.zeros((3, 2), dtype=np.float32) + dst = np.zeros((3, 2), dtype=np.float32) + src[0, :] = center + scale_tmp * shift + src[1, :] = center + src_dir + scale_tmp * shift + dst[0, :] = [(dst_w-1) * 0.5, (dst_h-1) * 0.5] + dst[1, :] = np.array([(dst_w-1) * 0.5, (dst_h-1) * 0.5]) + dst_dir + + src[2:, :] = get_3rd_point(src[0, :], src[1, :]) + dst[2:, :] = get_3rd_point(dst[0, :], dst[1, :]) + + if inv: + trans = cv2.getAffineTransform(np.float32(dst), np.float32(src)) + else: + trans = cv2.getAffineTransform(np.float32(src), np.float32(dst)) + + return trans + + +def affine_transform(pt, t): + new_pt = np.array([pt[0], pt[1], 1.]).T + new_pt = np.dot(t, new_pt) + return new_pt[:2] + + +def get_3rd_point(a, b): + direct = a - b + return b + np.array([-direct[1], direct[0]], dtype=np.float32) + + +def get_dir(src_point, rot_rad): + sn, cs = np.sin(rot_rad), np.cos(rot_rad) + + src_result = [0, 0] + src_result[0] = src_point[0] * cs - src_point[1] * sn + src_result[1] = src_point[0] * sn + src_point[1] * cs + + return src_result + + +def crop(img, center, scale, output_size, rot=0): + trans = get_affine_transform(center, scale, rot, output_size) + + dst_img = cv2.warpAffine(img, + trans, + (int(output_size[1]), int(output_size[0])), + flags=cv2.INTER_LINEAR) + + return dst_img diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/ic_light/__init__.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/ic_light/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a9a95abcdda2a22ef855e1c11b83330295ef45b4 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/ic_light/__init__.py @@ -0,0 +1,184 @@ +#credit to huchenlei for this module +#from https://github.com/huchenlei/ComfyUI-IC-Light-Native +import torch +import numpy as np +from typing import Tuple, TypedDict, Callable + +import comfy.model_management +from comfy.sd import load_unet +from comfy.ldm.models.autoencoder import AutoencoderKL +from comfy.model_base import BaseModel +from comfy.model_patcher import ModelPatcher +from PIL import Image +from nodes import VAEEncode +from ...libs.image import np2tensor, pil2tensor + +class UnetParams(TypedDict): + input: torch.Tensor + timestep: torch.Tensor + c: dict + cond_or_uncond: torch.Tensor + +class VAEEncodeArgMax(VAEEncode): + def encode(self, vae, pixels): + assert isinstance( + vae.first_stage_model, AutoencoderKL + ), "ArgMax only supported for AutoencoderKL" + original_sample_mode = vae.first_stage_model.regularization.sample + vae.first_stage_model.regularization.sample = False + ret = super().encode(vae, pixels) + vae.first_stage_model.regularization.sample = original_sample_mode + return ret + +class ICLight: + + @staticmethod + def apply_c_concat(params: UnetParams, concat_conds) -> UnetParams: + """Apply c_concat on unet call.""" + sample = params["input"] + params["c"]["c_concat"] = torch.cat( + ( + [concat_conds.to(sample.device)] + * (sample.shape[0] // concat_conds.shape[0]) + ), + dim=0, + ) + return params + + @staticmethod + def create_custom_conv( + original_conv: torch.nn.Module, + dtype: torch.dtype, + device=torch.device, + ) -> torch.nn.Module: + with torch.no_grad(): + new_conv_in = torch.nn.Conv2d( + 8, + original_conv.out_channels, + original_conv.kernel_size, + original_conv.stride, + original_conv.padding, + ) + new_conv_in.weight.zero_() + new_conv_in.weight[:, :4, :, :].copy_(original_conv.weight) + new_conv_in.bias = original_conv.bias + return new_conv_in.to(dtype=dtype, device=device) + + def generate_lighting_image(self, original_image, direction): + _, image_height, image_width, _ = original_image.shape + if direction == 'Left Light': + gradient = np.linspace(255, 0, image_width) + image = np.tile(gradient, (image_height, 1)) + input_bg = np.stack((image,) * 3, axis=-1).astype(np.uint8) + return np2tensor(input_bg) + elif direction == 'Right Light': + gradient = np.linspace(0, 255, image_width) + image = np.tile(gradient, (image_height, 1)) + input_bg = np.stack((image,) * 3, axis=-1).astype(np.uint8) + return np2tensor(input_bg) + elif direction == 'Top Light': + gradient = np.linspace(255, 0, image_height)[:, None] + image = np.tile(gradient, (1, image_width)) + input_bg = np.stack((image,) * 3, axis=-1).astype(np.uint8) + return np2tensor(input_bg) + elif direction == 'Bottom Light': + gradient = np.linspace(0, 255, image_height)[:, None] + image = np.tile(gradient, (1, image_width)) + input_bg = np.stack((image,) * 3, axis=-1).astype(np.uint8) + return np2tensor(input_bg) + elif direction == 'Circle Light': + x = np.linspace(-1, 1, image_width) + y = np.linspace(-1, 1, image_height) + x, y = np.meshgrid(x, y) + r = np.sqrt(x ** 2 + y ** 2) + r = r / r.max() + color1 = np.array([0, 0, 0])[np.newaxis, np.newaxis, :] + color2 = np.array([255, 255, 255])[np.newaxis, np.newaxis, :] + gradient = (color1 * r[..., np.newaxis] + color2 * (1 - r)[..., np.newaxis]).astype(np.uint8) + image = pil2tensor(Image.fromarray(gradient)) + return image + else: + image = pil2tensor(Image.new('RGB', (1, 1), (0, 0, 0))) + return image + + def generate_source_image(self, original_image, source): + batch_size, image_height, image_width, _ = original_image.shape + if source == 'Use Flipped Background Image': + if batch_size < 2: + raise ValueError('Must be at least 2 image to use flipped background image.') + original_image = [img.unsqueeze(0) for img in original_image] + image = torch.flip(original_image[1], [2]) + return image + elif source == 'Ambient': + input_bg = np.zeros(shape=(image_height, image_width, 3), dtype=np.uint8) + 64 + return np2tensor(input_bg) + elif source == 'Left Light': + gradient = np.linspace(224, 32, image_width) + image = np.tile(gradient, (image_height, 1)) + input_bg = np.stack((image,) * 3, axis=-1).astype(np.uint8) + return np2tensor(input_bg) + elif source == 'Right Light': + gradient = np.linspace(32, 224, image_width) + image = np.tile(gradient, (image_height, 1)) + input_bg = np.stack((image,) * 3, axis=-1).astype(np.uint8) + return np2tensor(input_bg) + elif source == 'Top Light': + gradient = np.linspace(224, 32, image_height)[:, None] + image = np.tile(gradient, (1, image_width)) + input_bg = np.stack((image,) * 3, axis=-1).astype(np.uint8) + return np2tensor(input_bg) + elif source == 'Bottom Light': + gradient = np.linspace(32, 224, image_height)[:, None] + image = np.tile(gradient, (1, image_width)) + input_bg = np.stack((image,) * 3, axis=-1).astype(np.uint8) + return np2tensor(input_bg) + else: + image = pil2tensor(Image.new('RGB', (1, 1), (0, 0, 0))) + return image + + + def apply(self, ic_model_path, model, c_concat: dict, ic_model=None) -> Tuple[ModelPatcher]: + device = comfy.model_management.get_torch_device() + dtype = comfy.model_management.unet_dtype() + work_model = model.clone() + + # Apply scale factor. + base_model: BaseModel = work_model.model + scale_factor = base_model.model_config.latent_format.scale_factor + + # [B, 4, H, W] + concat_conds: torch.Tensor = c_concat["samples"] * scale_factor + # [1, 4 * B, H, W] + concat_conds = torch.cat([c[None, ...] for c in concat_conds], dim=1) + + def unet_dummy_apply(unet_apply: Callable, params: UnetParams): + """A dummy unet apply wrapper serving as the endpoint of wrapper + chain.""" + return unet_apply(x=params["input"], t=params["timestep"], **params["c"]) + + existing_wrapper = work_model.model_options.get( + "model_function_wrapper", unet_dummy_apply + ) + + def wrapper_func(unet_apply: Callable, params: UnetParams): + return existing_wrapper(unet_apply, params=self.apply_c_concat(params, concat_conds)) + + work_model.set_model_unet_function_wrapper(wrapper_func) + if not ic_model: + ic_model = load_unet(ic_model_path) + ic_model_state_dict = ic_model.model.diffusion_model.state_dict() + + work_model.add_patches( + patches={ + ("diffusion_model." + key): ( + 'diff', + [ + value.to(dtype=dtype, device=device), + {"pad_weight": key == 'input_blocks.0.0.weight'} + ] + ) + for key, value in ic_model_state_dict.items() + } + ) + + return (work_model, ic_model) \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/ic_light/__pycache__/__init__.cpython-312.pyc b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/ic_light/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ddb36328331a4ed3bad490fa9833867ae77474dc Binary files /dev/null and b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/ic_light/__pycache__/__init__.cpython-312.pyc differ diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/ipadapter/__init__.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/ipadapter/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a03e4fba8b302f5d9090a3a44081ac439fe81c26 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/ipadapter/__init__.py @@ -0,0 +1,268 @@ +#credit to shakker-labs and instantX for this module +#from https://github.com/Shakker-Labs/ComfyUI-IPAdapter-Flux +import torch +from PIL import Image +import numpy as np +from .attention_processor import IPAFluxAttnProcessor2_0 +from .utils import is_model_pathched, FluxUpdateModules +from .sd3.resampler import TimeResampler +from .sd3.joinblock import JointBlockIPWrapper, IPAttnProcessor + +image_proj_model = None +class MLPProjModel(torch.nn.Module): + def __init__(self, cross_attention_dim=768, id_embeddings_dim=512, num_tokens=4): + super().__init__() + + self.cross_attention_dim = cross_attention_dim + self.num_tokens = num_tokens + + self.proj = torch.nn.Sequential( + torch.nn.Linear(id_embeddings_dim, id_embeddings_dim * 2), + torch.nn.GELU(), + torch.nn.Linear(id_embeddings_dim * 2, cross_attention_dim * num_tokens), + ) + self.norm = torch.nn.LayerNorm(cross_attention_dim) + + def forward(self, id_embeds): + x = self.proj(id_embeds) + x = x.reshape(-1, self.num_tokens, self.cross_attention_dim) + x = self.norm(x) + return x + +class InstantXFluxIpadapterApply: + def __init__(self, num_tokens=128): + self.device = None + self.dtype = torch.float16 + self.num_tokens = num_tokens + self.ip_ckpt = None + self.clip_vision = None + self.image_encoder = None + self.clip_image_processor = None + # state_dict + self.state_dict = None + self.joint_attention_dim = 4096 + self.hidden_size = 3072 + + def set_ip_adapter(self, flux_model, weight, timestep_percent_range=(0.0, 1.0)): + s = flux_model.model_sampling + percent_to_timestep_function = lambda a: s.percent_to_sigma(a) + timestep_range = (percent_to_timestep_function(timestep_percent_range[0]), + percent_to_timestep_function(timestep_percent_range[1])) + ip_attn_procs = {} # 19+38=57 + dsb_count = len(flux_model.diffusion_model.double_blocks) + for i in range(dsb_count): + name = f"double_blocks.{i}" + ip_attn_procs[name] = IPAFluxAttnProcessor2_0( + hidden_size=self.hidden_size, + cross_attention_dim=self.joint_attention_dim, + num_tokens=self.num_tokens, + scale=weight, + timestep_range=timestep_range + ).to(self.device, dtype=self.dtype) + ssb_count = len(flux_model.diffusion_model.single_blocks) + for i in range(ssb_count): + name = f"single_blocks.{i}" + ip_attn_procs[name] = IPAFluxAttnProcessor2_0( + hidden_size=self.hidden_size, + cross_attention_dim=self.joint_attention_dim, + num_tokens=self.num_tokens, + scale=weight, + timestep_range=timestep_range + ).to(self.device, dtype=self.dtype) + return ip_attn_procs + + def load_ip_adapter(self, flux_model, weight, timestep_percent_range=(0.0, 1.0)): + global image_proj_model + image_proj_model.load_state_dict(self.state_dict["image_proj"], strict=True) + ip_attn_procs = self.set_ip_adapter(flux_model, weight, timestep_percent_range) + ip_layers = torch.nn.ModuleList(ip_attn_procs.values()) + ip_layers.load_state_dict(self.state_dict["ip_adapter"], strict=True) + return ip_attn_procs + + def get_image_embeds(self, pil_image=None, clip_image_embeds=None): + # outputs = self.clip_vision.encode_image(pil_image) + # clip_image_embeds = outputs['image_embeds'] + # clip_image_embeds = clip_image_embeds.to(self.device, dtype=self.dtype) + # image_prompt_embeds = self.image_proj_model(clip_image_embeds) + if pil_image is not None: + if isinstance(pil_image, Image.Image): + pil_image = [pil_image] + clip_image = self.clip_image_processor(images=pil_image, return_tensors="pt").pixel_values + clip_image_embeds = self.image_encoder( + clip_image.to(self.device, dtype=self.image_encoder.dtype)).pooler_output + clip_image_embeds = clip_image_embeds.to(dtype=self.dtype) + else: + clip_image_embeds = clip_image_embeds.to(self.device, dtype=self.dtype) + global image_proj_model + image_prompt_embeds = image_proj_model(clip_image_embeds) + return image_prompt_embeds + + def apply_ipadapter(self, model, ipadapter, image, weight, start_at, end_at, provider=None, use_tiled=False): + self.device = provider.lower() + if "clipvision" in ipadapter: + # self.clip_vision = ipadapter["clipvision"]['model'] + self.image_encoder = ipadapter["clipvision"]['model']['image_encoder'].to(self.device, dtype=self.dtype) + self.clip_image_processor = ipadapter["clipvision"]['model']['clip_image_processor'] + if "ipadapter" in ipadapter: + self.ip_ckpt = ipadapter["ipadapter"]['file'] + self.state_dict = ipadapter["ipadapter"]['model'] + + # process image + pil_image = image.numpy()[0] * 255.0 + pil_image = Image.fromarray(pil_image.astype(np.uint8)) + # initialize ipadapter + global image_proj_model + if image_proj_model is None: + image_proj_model = MLPProjModel( + cross_attention_dim=self.joint_attention_dim, # 4096 + id_embeddings_dim=1152, + num_tokens=self.num_tokens, + ) + image_proj_model.to(self.device, dtype=self.dtype) + ip_attn_procs = self.load_ip_adapter(model.model, weight, (start_at, end_at)) + # process control image + image_prompt_embeds = self.get_image_embeds(pil_image=pil_image, clip_image_embeds=None) + # set model + # is_patched = is_model_pathched(model.model) + bi = model.clone() + FluxUpdateModules(bi, ip_attn_procs, image_prompt_embeds) + + return (bi, image) + + +def patch_sd3( + patcher, + ip_procs, + resampler: TimeResampler, + clip_embeds, + weight=1.0, + start=0.0, + end=1.0, +): + """ + Patches a model_sampler to add the ipadapter + """ + mmdit = patcher.model.diffusion_model + timestep_schedule_max = patcher.model.model_config.sampling_settings.get( + "timesteps", 1000 + ) + # hook the model's forward function + # so that when it gets called, we can grab the timestep and send it to the resampler + ip_options = { + "hidden_states": None, + "t_emb": None, + "weight": weight, + } + + def ddit_wrapper(forward, args): + # this is between 0 and 1, so the adapters can calculate start_point and end_point + # actually, do we need to get the sigma value instead? + t_percent = 1 - args["timestep"].flatten()[0].cpu().item() + if start <= t_percent <= end: + batch_size = args["input"].shape[0] // len(args["cond_or_uncond"]) + # if we're only doing cond or only doing uncond, only pass one of them through the resampler + embeds = clip_embeds[args["cond_or_uncond"]] + # slight efficiency optimization todo: pass the embeds through and then afterwards + # repeat to the batch size + embeds = torch.repeat_interleave(embeds, batch_size, dim=0) + # the resampler wants between 0 and MAX_STEPS + timestep = args["timestep"] * timestep_schedule_max + image_emb, t_emb = resampler(embeds, timestep, need_temb=True) + # these will need to be accessible to the IPAdapters + ip_options["hidden_states"] = image_emb + ip_options["t_emb"] = t_emb + else: + ip_options["hidden_states"] = None + ip_options["t_emb"] = None + + return forward(args["input"], args["timestep"], **args["c"]) + + patcher.set_model_unet_function_wrapper(ddit_wrapper) + # patch each dit block + for i, block in enumerate(mmdit.joint_blocks): + wrapper = JointBlockIPWrapper(block, ip_procs[i], ip_options) + patcher.set_model_patch_replace(wrapper, "dit", "double_block", i) + +class InstantXSD3IpadapterApply: + def __init__(self): + self.device = None + self.dtype = torch.float16 + self.clip_image_processor = None + self.image_encoder = None + self.resampler = None + self.procs = None + + @torch.inference_mode() + def encode(self, image): + clip_image = self.clip_image_processor.image_processor(image, return_tensors="pt", do_rescale=False).pixel_values + clip_image_embeds = self.image_encoder( + clip_image.to(self.device, dtype=self.image_encoder.dtype), + output_hidden_states=True, + ).hidden_states[-2] + clip_image_embeds = torch.cat( + [clip_image_embeds, torch.zeros_like(clip_image_embeds)], dim=0 + ) + clip_image_embeds = clip_image_embeds.to(dtype=torch.float16) + return clip_image_embeds + + def apply_ipadapter(self, model, ipadapter, image, weight, start_at, end_at, provider=None, use_tiled=False): + self.device = provider.lower() + if "clipvision" in ipadapter: + self.image_encoder = ipadapter["clipvision"]['model']['image_encoder'].to(self.device, dtype=self.dtype) + self.clip_image_processor = ipadapter["clipvision"]['model']['clip_image_processor'] + if "ipadapter" in ipadapter: + self.ip_ckpt = ipadapter["ipadapter"]['file'] + self.state_dict = ipadapter["ipadapter"]['model'] + + self.resampler = TimeResampler( + dim=1280, + depth=4, + dim_head=64, + heads=20, + num_queries=64, + embedding_dim=1152, + output_dim=2432, + ff_mult=4, + timestep_in_dim=320, + timestep_flip_sin_to_cos=True, + timestep_freq_shift=0, + ) + self.resampler.eval() + self.resampler.to(self.device, dtype=self.dtype) + self.resampler.load_state_dict(self.state_dict["image_proj"]) + + # now we'll create the attention processors + # ip_adapter.keys looks like [0.proj, 0.to_k, ..., 1.proj, 1.to_k, ...] + n_procs = len( + set(x.split(".")[0] for x in self.state_dict["ip_adapter"].keys()) + ) + self.procs = torch.nn.ModuleList( + [ + # this is hardcoded for SD3.5L + IPAttnProcessor( + hidden_size=2432, + cross_attention_dim=2432, + ip_hidden_states_dim=2432, + ip_encoder_hidden_states_dim=2432, + head_dim=64, + timesteps_emb_dim=1280, + ).to(self.device, dtype=torch.float16) + for _ in range(n_procs) + ] + ) + self.procs.load_state_dict(self.state_dict["ip_adapter"]) + + work_model = model.clone() + embeds = self.encode(image) + + patch_sd3( + work_model, + self.procs, + self.resampler, + embeds, + weight, + start_at, + end_at, + ) + + return (work_model, image) \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/ipadapter/attention_processor.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/ipadapter/attention_processor.py new file mode 100644 index 0000000000000000000000000000000000000000..4eb1b153773f1ee893a76ed64dedbdfd163cea48 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/ipadapter/attention_processor.py @@ -0,0 +1,87 @@ +import numbers +from typing import Dict, Optional, Tuple +import torch +import torch.nn as nn +import torch.nn.functional as F +from einops import rearrange + +class RMSNorm(nn.Module): + def __init__(self, dim, eps: float, elementwise_affine: bool = True): + super().__init__() + + self.eps = eps + + if isinstance(dim, numbers.Integral): + dim = (dim,) + + self.dim = torch.Size(dim) + + if elementwise_affine: + self.weight = nn.Parameter(torch.ones(dim)) + else: + self.weight = None + + def forward(self, hidden_states): + input_dtype = hidden_states.dtype + variance = hidden_states.to(torch.float32).pow(2).mean(-1, keepdim=True) + hidden_states = hidden_states * torch.rsqrt(variance + self.eps) + + if self.weight is not None: + # convert into half-precision if necessary + if self.weight.dtype in [torch.float16, torch.bfloat16]: + hidden_states = hidden_states.to(self.weight.dtype) + hidden_states = hidden_states * self.weight + else: + hidden_states = hidden_states.to(input_dtype) + + return hidden_states + +class IPAFluxAttnProcessor2_0(nn.Module): + """Attention processor used typically in processing the SD3-like self-attention projections.""" + + def __init__(self, hidden_size, cross_attention_dim=None, scale=1.0, num_tokens=4, timestep_range=None): + super().__init__() + + self.hidden_size = hidden_size # 3072 + self.cross_attention_dim = cross_attention_dim # 4096 + self.scale = scale + self.num_tokens = num_tokens + + self.to_k_ip = nn.Linear(cross_attention_dim or hidden_size, hidden_size, bias=False) + self.to_v_ip = nn.Linear(cross_attention_dim or hidden_size, hidden_size, bias=False) + + self.norm_added_k = RMSNorm(128, eps=1e-5, elementwise_affine=False) + self.norm_added_v = RMSNorm(128, eps=1e-5, elementwise_affine=False) + self.timestep_range = timestep_range + + def __call__( + self, + num_heads, + query, + image_emb: torch.FloatTensor, + t: torch.FloatTensor + ) -> torch.FloatTensor: + # only apply IPA if timestep is within range + if self.timestep_range is not None: + if t[0] > self.timestep_range[0] or t[0] < self.timestep_range[1]: + return None + # `ip-adapter` projections + ip_hidden_states = image_emb + ip_hidden_states_key_proj = self.to_k_ip(ip_hidden_states) + ip_hidden_states_value_proj = self.to_v_ip(ip_hidden_states) + + ip_hidden_states_key_proj = rearrange(ip_hidden_states_key_proj, 'B L (H D) -> B H L D', H=num_heads) + ip_hidden_states_value_proj = rearrange(ip_hidden_states_value_proj, 'B L (H D) -> B H L D', H=num_heads) + + ip_hidden_states_key_proj = self.norm_added_k(ip_hidden_states_key_proj) + ip_hidden_states_value_proj = self.norm_added_v(ip_hidden_states_value_proj) + + ip_hidden_states = F.scaled_dot_product_attention(query.to(image_emb.device).to(image_emb.dtype), + ip_hidden_states_key_proj, + ip_hidden_states_value_proj, + dropout_p=0.0, is_causal=False) + + ip_hidden_states = rearrange(ip_hidden_states, "B H L D -> B L (H D)", H=num_heads) + ip_hidden_states = ip_hidden_states.to(query.dtype).to(query.device) + + return self.scale * ip_hidden_states \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/ipadapter/flux/__init__.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/ipadapter/flux/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/ipadapter/flux/layers.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/ipadapter/flux/layers.py new file mode 100644 index 0000000000000000000000000000000000000000..9eeeb49fb0ba668abbb39719e40f87ba96e1169d --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/ipadapter/flux/layers.py @@ -0,0 +1,153 @@ +import torch +from torch import Tensor, nn + +from .math import attention +from ..attention_processor import IPAFluxAttnProcessor2_0 +from comfy.ldm.flux.layers import DoubleStreamBlock, SingleStreamBlock +from comfy import model_management as mm + +class DoubleStreamBlockIPA(nn.Module): + def __init__(self, original_block: DoubleStreamBlock, ip_adapter, image_emb): + super().__init__() + + mlp_hidden_dim = original_block.img_mlp[0].out_features + mlp_ratio = mlp_hidden_dim / original_block.hidden_size + mlp_hidden_dim = int(original_block.hidden_size * mlp_ratio) + self.num_heads = original_block.num_heads + self.hidden_size = original_block.hidden_size + self.img_mod = original_block.img_mod + self.img_norm1 = original_block.img_norm1 + self.img_attn = original_block.img_attn + + self.img_norm2 = original_block.img_norm2 + self.img_mlp = original_block.img_mlp + + self.txt_mod = original_block.txt_mod + self.txt_norm1 = original_block.txt_norm1 + self.txt_attn = original_block.txt_attn + + self.txt_norm2 = original_block.txt_norm2 + self.txt_mlp = original_block.txt_mlp + self.flipped_img_txt = getattr(original_block, 'flipped_img_txt', False) + + self.ip_adapter = ip_adapter + self.image_emb = image_emb + self.device = mm.get_torch_device() + + def forward(self, img: Tensor, txt: Tensor, vec: Tensor, pe: Tensor, t: Tensor, attn_mask=None): + img_mod1, img_mod2 = self.img_mod(vec) + txt_mod1, txt_mod2 = self.txt_mod(vec) + + # prepare image for attention + img_modulated = self.img_norm1(img) + img_modulated = (1 + img_mod1.scale) * img_modulated + img_mod1.shift + img_qkv = self.img_attn.qkv(img_modulated) + img_q, img_k, img_v = img_qkv.view(img_qkv.shape[0], img_qkv.shape[1], 3, self.num_heads, -1).permute(2, 0, 3, + 1, 4) + img_q, img_k = self.img_attn.norm(img_q, img_k, img_v) + + # prepare txt for attention + txt_modulated = self.txt_norm1(txt) + txt_modulated = (1 + txt_mod1.scale) * txt_modulated + txt_mod1.shift + txt_qkv = self.txt_attn.qkv(txt_modulated) + txt_q, txt_k, txt_v = txt_qkv.view(txt_qkv.shape[0], txt_qkv.shape[1], 3, self.num_heads, -1).permute(2, 0, 3, + 1, 4) + txt_q, txt_k = self.txt_attn.norm(txt_q, txt_k, txt_v) + + if self.flipped_img_txt: + # run actual attention + attn = attention(torch.cat((img_q, txt_q), dim=2), + torch.cat((img_k, txt_k), dim=2), + torch.cat((img_v, txt_v), dim=2), + pe=pe, mask=attn_mask) + + img_attn, txt_attn = attn[:, : img.shape[1]], attn[:, img.shape[1]:] + else: + # run actual attention + attn = attention(torch.cat((txt_q, img_q), dim=2), + torch.cat((txt_k, img_k), dim=2), + torch.cat((txt_v, img_v), dim=2), + pe=pe, mask=attn_mask) + + txt_attn, img_attn = attn[:, : txt.shape[1]], attn[:, txt.shape[1]:] + + for adapter, image in zip(self.ip_adapter, self.image_emb): + # this does a separate attention for each adapter + ip_hidden_states = adapter(self.num_heads, img_q, image, t) + if ip_hidden_states is not None: + ip_hidden_states = ip_hidden_states.to(self.device) + img_attn = img_attn + ip_hidden_states + + # calculate the img bloks + img = img + img_mod1.gate * self.img_attn.proj(img_attn) + img = img + img_mod2.gate * self.img_mlp((1 + img_mod2.scale) * self.img_norm2(img) + img_mod2.shift) + + # calculate the txt bloks + txt += txt_mod1.gate * self.txt_attn.proj(txt_attn) + txt += txt_mod2.gate * self.txt_mlp((1 + txt_mod2.scale) * self.txt_norm2(txt) + txt_mod2.shift) + + if txt.dtype == torch.float16: + txt = torch.nan_to_num(txt, nan=0.0, posinf=65504, neginf=-65504) + + return img, txt + + +class SingleStreamBlockIPA(nn.Module): + """ + A DiT block with parallel linear layers as described in + https://arxiv.org/abs/2302.05442 and adapted modulation interface. + """ + + def __init__(self, original_block: SingleStreamBlock, ip_adapter, image_emb): + super().__init__() + self.hidden_dim = original_block.hidden_size + self.num_heads = original_block.num_heads + self.scale = original_block.scale + + self.mlp_hidden_dim = original_block.mlp_hidden_dim + # qkv and mlp_in + self.linear1 = original_block.linear1 + # proj and mlp_out + self.linear2 = original_block.linear2 + + self.norm = original_block.norm + + self.hidden_size = original_block.hidden_size + self.pre_norm = original_block.pre_norm + + self.mlp_act = original_block.mlp_act + self.modulation = original_block.modulation + + self.ip_adapter = ip_adapter + self.image_emb = image_emb + self.device = mm.get_torch_device() + + def add_adapter(self, ip_adapter: IPAFluxAttnProcessor2_0, image_emb): + self.ip_adapter.append(ip_adapter) + self.image_emb.append(image_emb) + + def forward(self, x: Tensor, vec: Tensor, pe: Tensor, t: Tensor, attn_mask=None) -> Tensor: + mod, _ = self.modulation(vec) + x_mod = (1 + mod.scale) * self.pre_norm(x) + mod.shift + qkv, mlp = torch.split(self.linear1(x_mod), [3 * self.hidden_size, self.mlp_hidden_dim], dim=-1) + + q, k, v = qkv.view(qkv.shape[0], qkv.shape[1], 3, self.num_heads, -1).permute(2, 0, 3, 1, 4) + q, k = self.norm(q, k, v) + + # compute attention + attn = attention(q, k, v, pe=pe, mask=attn_mask) + + for adapter, image in zip(self.ip_adapter, self.image_emb): + # this does a separate attention for each adapter + # maybe we want a single joint attention call for all adapters? + ip_hidden_states = adapter(self.num_heads, q, image, t) + if ip_hidden_states is not None: + ip_hidden_states = ip_hidden_states.to(self.device) + attn = attn + ip_hidden_states + + # compute activation in mlp stream, cat again and run second linear layer + output = self.linear2(torch.cat((attn, self.mlp_act(mlp)), 2)) + x += mod.gate * output + if x.dtype == torch.float16: + x = torch.nan_to_num(x, nan=0.0, posinf=65504, neginf=-65504) + return x \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/ipadapter/flux/math.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/ipadapter/flux/math.py new file mode 100644 index 0000000000000000000000000000000000000000..1f58b1811d0ae6c224df73b0cf5f89163e8cd35f --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/ipadapter/flux/math.py @@ -0,0 +1,35 @@ +import torch +from einops import rearrange +from torch import Tensor +from comfy.ldm.modules.attention import optimized_attention +import comfy.model_management + +def attention(q: Tensor, k: Tensor, v: Tensor, pe: Tensor, mask=None) -> Tensor: + q, k = apply_rope(q, k, pe) + + heads = q.shape[1] + x = optimized_attention(q, k, v, heads, skip_reshape=True, mask=mask) + return x + + +def rope(pos: Tensor, dim: int, theta: int) -> Tensor: + assert dim % 2 == 0 + if comfy.model_management.is_device_mps(pos.device) or comfy.model_management.is_intel_xpu(): + device = torch.device("cpu") + else: + device = pos.device + + scale = torch.linspace(0, (dim - 2) / dim, steps=dim//2, dtype=torch.float64, device=device) + omega = 1.0 / (theta**scale) + out = torch.einsum("...n,d->...nd", pos.to(dtype=torch.float32, device=device), omega) + out = torch.stack([torch.cos(out), -torch.sin(out), torch.sin(out), torch.cos(out)], dim=-1) + out = rearrange(out, "b n d (i j) -> b n d i j", i=2, j=2) + return out.to(dtype=torch.float32, device=pos.device) + + +def apply_rope(xq: Tensor, xk: Tensor, freqs_cis: Tensor): + xq_ = xq.float().reshape(*xq.shape[:-1], -1, 1, 2) + xk_ = xk.float().reshape(*xk.shape[:-1], -1, 1, 2) + xq_out = freqs_cis[..., 0] * xq_[..., 0] + freqs_cis[..., 1] * xq_[..., 1] + xk_out = freqs_cis[..., 0] * xk_[..., 0] + freqs_cis[..., 1] * xk_[..., 1] + return xq_out.reshape(*xq.shape).type_as(xq), xk_out.reshape(*xk.shape).type_as(xk) \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/ipadapter/sd3/__init__.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/ipadapter/sd3/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/ipadapter/sd3/joinblock.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/ipadapter/sd3/joinblock.py new file mode 100644 index 0000000000000000000000000000000000000000..95bbf26b440165e0f85c8303753bf48fcf7d60c9 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/ipadapter/sd3/joinblock.py @@ -0,0 +1,219 @@ +import torch +from torch import nn +from torch.nn import functional as F +from einops import rearrange + +from comfy.ldm.modules.attention import optimized_attention +from comfy.ldm.modules.diffusionmodules.mmdit import (RMSNorm, JointBlock,) + + +class AdaLayerNorm(nn.Module): + """ + Norm layer adaptive layer norm zero (adaLN-Zero). + + Parameters: + embedding_dim (`int`): The size of each embedding vector. + num_embeddings (`int`): The size of the embeddings dictionary. + """ + + def __init__(self, embedding_dim: int, time_embedding_dim=None, mode="normal"): + super().__init__() + + self.silu = nn.SiLU() + num_params_dict = dict( + zero=6, + normal=2, + ) + num_params = num_params_dict[mode] + self.linear = nn.Linear( + time_embedding_dim or embedding_dim, num_params * embedding_dim, bias=True + ) + self.norm = nn.LayerNorm(embedding_dim, elementwise_affine=False, eps=1e-6) + self.mode = mode + + def forward( + self, + x, + hidden_dtype=None, + emb=None, + ): + emb = self.linear(self.silu(emb)) + if self.mode == "normal": + shift_msa, scale_msa = emb.chunk(2, dim=1) + x = self.norm(x) * (1 + scale_msa[:, None]) + shift_msa[:, None] + return x + + elif self.mode == "zero": + shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp = emb.chunk( + 6, dim=1 + ) + x = self.norm(x) * (1 + scale_msa[:, None]) + shift_msa[:, None] + return x, gate_msa, shift_mlp, scale_mlp, gate_mlp + + +class IPAttnProcessor(nn.Module): + + def __init__( + self, + hidden_size=None, + cross_attention_dim=None, + ip_hidden_states_dim=None, + ip_encoder_hidden_states_dim=None, + head_dim=None, + timesteps_emb_dim=1280, + ): + super().__init__() + + self.norm_ip = AdaLayerNorm( + ip_hidden_states_dim, time_embedding_dim=timesteps_emb_dim + ) + self.to_k_ip = nn.Linear(ip_hidden_states_dim, hidden_size, bias=False) + self.to_v_ip = nn.Linear(ip_hidden_states_dim, hidden_size, bias=False) + self.norm_q = RMSNorm(head_dim, 1e-6) + self.norm_k = RMSNorm(head_dim, 1e-6) + self.norm_ip_k = RMSNorm(head_dim, 1e-6) + + def forward( + self, + ip_hidden_states, + img_query, + img_key=None, + img_value=None, + t_emb=None, + n_heads=1, + ): + if ip_hidden_states is None: + return None + + if not hasattr(self, "to_k_ip") or not hasattr(self, "to_v_ip"): + return None + + # norm ip input + norm_ip_hidden_states = self.norm_ip(ip_hidden_states, emb=t_emb) + + # to k and v + ip_key = self.to_k_ip(norm_ip_hidden_states) + ip_value = self.to_v_ip(norm_ip_hidden_states) + + # reshape + img_query = rearrange(img_query, "b l (h d) -> b h l d", h=n_heads) + img_key = rearrange(img_key, "b l (h d) -> b h l d", h=n_heads) + # note that the image is in a different shape: b l h d + # so we transpose to b h l d + # or do we have to transpose here? + img_value = torch.transpose(img_value, 1, 2) + ip_key = rearrange(ip_key, "b l (h d) -> b h l d", h=n_heads) + ip_value = rearrange(ip_value, "b l (h d) -> b h l d", h=n_heads) + + # norm + img_query = self.norm_q(img_query) + img_key = self.norm_k(img_key) + ip_key = self.norm_ip_k(ip_key) + + # cat img + key = torch.cat([img_key, ip_key], dim=2) + value = torch.cat([img_value, ip_value], dim=2) + + # + ip_hidden_states = F.scaled_dot_product_attention( + img_query, key, value, dropout_p=0.0, is_causal=False + ) + ip_hidden_states = rearrange(ip_hidden_states, "b h l d -> b l (h d)") + ip_hidden_states = ip_hidden_states.to(img_query.dtype) + return ip_hidden_states + + +class JointBlockIPWrapper: + """To be used as a patch_replace with Comfy""" + + def __init__( + self, + original_block: JointBlock, + adapter: IPAttnProcessor, + ip_options=None, + ): + self.original_block = original_block + self.adapter = adapter + if ip_options is None: + ip_options = {} + self.ip_options = ip_options + + def block_mixing(self, context, x, context_block, x_block, c): + """ + Comes from mmdit.py. Modified to add ipadapter attention. + """ + context_qkv, context_intermediates = context_block.pre_attention(context, c) + + if x_block.x_block_self_attn: + x_qkv, x_qkv2, x_intermediates = x_block.pre_attention_x(x, c) + else: + x_qkv, x_intermediates = x_block.pre_attention(x, c) + + qkv = tuple(torch.cat((context_qkv[j], x_qkv[j]), dim=1) for j in range(3)) + + attn = optimized_attention( + qkv[0], + qkv[1], + qkv[2], + heads=x_block.attn.num_heads, + ) + context_attn, x_attn = ( + attn[:, : context_qkv[0].shape[1]], + attn[:, context_qkv[0].shape[1] :], + ) + # if the current timestep is not in the ipadapter enabling range, then the resampler wasn't run + # and the hidden states will be None + if ( + self.ip_options["hidden_states"] is not None + and self.ip_options["t_emb"] is not None + ): + # IP-Adapter + ip_attn = self.adapter( + self.ip_options["hidden_states"], + *x_qkv, + self.ip_options["t_emb"], + x_block.attn.num_heads, + ) + x_attn = x_attn + ip_attn * self.ip_options["weight"] + + # Everything else is unchanged + if not context_block.pre_only: + context = context_block.post_attention(context_attn, *context_intermediates) + + else: + context = None + if x_block.x_block_self_attn: + attn2 = optimized_attention( + x_qkv2[0], + x_qkv2[1], + x_qkv2[2], + heads=x_block.attn2.num_heads, + ) + x = x_block.post_attention_x(x_attn, attn2, *x_intermediates) + else: + x = x_block.post_attention(x_attn, *x_intermediates) + return context, x + + def __call__(self, args, _): + # Code from mmdit.py: + # in this case, we're blocks_replace[("double_block", i)] + # note that although we're passed the original block, + # we can't actually get it from inside its wrapper + # (which would simplify the whole code...) + # ``` + # def block_wrap(args): + # out = {} + # out["txt"], out["img"] = self.joint_blocks[i](args["txt"], args["img"], c=args["vec"]) + # return out + # out = blocks_replace[("double_block", i)]({"img": x, "txt": context, "vec": c_mod}, {"original_block": block_wrap}) + # context = out["txt"] + # x = out["img"] + # ``` + c, x = self.block_mixing( + args["txt"], + args["img"], + self.original_block.context_block, + self.original_block.x_block, + c=args["vec"], + ) + return {"txt": c, "img": x} \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/ipadapter/sd3/resampler.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/ipadapter/sd3/resampler.py new file mode 100644 index 0000000000000000000000000000000000000000..d8b68a079427050378ec56622bd483359f4671a7 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/ipadapter/sd3/resampler.py @@ -0,0 +1,385 @@ +# modified from https://github.com/mlfoundations/open_flamingo/blob/main/open_flamingo/src/helpers.py +import math + +import torch +import torch.nn as nn +from typing import Optional + + +ACTIVATION_FUNCTIONS = { + "swish": nn.SiLU(), + "silu": nn.SiLU(), + "mish": nn.Mish(), + "gelu": nn.GELU(), + "relu": nn.ReLU(), +} +def get_activation(act_fn: str) -> nn.Module: + """Helper function to get activation function from string. + + Args: + act_fn (str): Name of activation function. + + Returns: + nn.Module: Activation function. + """ + + act_fn = act_fn.lower() + if act_fn in ACTIVATION_FUNCTIONS: + return ACTIVATION_FUNCTIONS[act_fn] + else: + raise ValueError(f"Unsupported activation function: {act_fn}") + +def get_timestep_embedding( + timesteps: torch.Tensor, + embedding_dim: int, + flip_sin_to_cos: bool = False, + downscale_freq_shift: float = 1, + scale: float = 1, + max_period: int = 10000, +): + """ + This matches the implementation in Denoising Diffusion Probabilistic Models: Create sinusoidal timestep embeddings. + + Args + timesteps (torch.Tensor): + a 1-D Tensor of N indices, one per batch element. These may be fractional. + embedding_dim (int): + the dimension of the output. + flip_sin_to_cos (bool): + Whether the embedding order should be `cos, sin` (if True) or `sin, cos` (if False) + downscale_freq_shift (float): + Controls the delta between frequencies between dimensions + scale (float): + Scaling factor applied to the embeddings. + max_period (int): + Controls the maximum frequency of the embeddings + Returns + torch.Tensor: an [N x dim] Tensor of positional embeddings. + """ + assert len(timesteps.shape) == 1, "Timesteps should be a 1d-array" + + half_dim = embedding_dim // 2 + exponent = -math.log(max_period) * torch.arange( + start=0, end=half_dim, dtype=torch.float32, device=timesteps.device + ) + exponent = exponent / (half_dim - downscale_freq_shift) + + emb = torch.exp(exponent) + emb = timesteps[:, None].float() * emb[None, :] + + # scale embeddings + emb = scale * emb + + # concat sine and cosine embeddings + emb = torch.cat([torch.sin(emb), torch.cos(emb)], dim=-1) + + # flip sine and cosine embeddings + if flip_sin_to_cos: + emb = torch.cat([emb[:, half_dim:], emb[:, :half_dim]], dim=-1) + + # zero pad + if embedding_dim % 2 == 1: + emb = torch.nn.functional.pad(emb, (0, 1, 0, 0)) + return emb + +class Timesteps(nn.Module): + def __init__(self, num_channels: int, flip_sin_to_cos: bool, downscale_freq_shift: float, scale: int = 1): + super().__init__() + self.num_channels = num_channels + self.flip_sin_to_cos = flip_sin_to_cos + self.downscale_freq_shift = downscale_freq_shift + self.scale = scale + + def forward(self, timesteps): + t_emb = get_timestep_embedding( + timesteps, + self.num_channels, + flip_sin_to_cos=self.flip_sin_to_cos, + downscale_freq_shift=self.downscale_freq_shift, + scale=self.scale, + ) + return t_emb + +class TimestepEmbedding(nn.Module): + def __init__( + self, + in_channels: int, + time_embed_dim: int, + act_fn: str = "silu", + out_dim: int = None, + post_act_fn: Optional[str] = None, + cond_proj_dim=None, + sample_proj_bias=True, + ): + super().__init__() + + self.linear_1 = nn.Linear(in_channels, time_embed_dim, sample_proj_bias) + + if cond_proj_dim is not None: + self.cond_proj = nn.Linear(cond_proj_dim, in_channels, bias=False) + else: + self.cond_proj = None + + self.act = get_activation(act_fn) + + if out_dim is not None: + time_embed_dim_out = out_dim + else: + time_embed_dim_out = time_embed_dim + self.linear_2 = nn.Linear(time_embed_dim, time_embed_dim_out, sample_proj_bias) + + if post_act_fn is None: + self.post_act = None + else: + self.post_act = get_activation(post_act_fn) + + def forward(self, sample, condition=None): + if condition is not None: + sample = sample + self.cond_proj(condition) + sample = self.linear_1(sample) + + if self.act is not None: + sample = self.act(sample) + + sample = self.linear_2(sample) + + if self.post_act is not None: + sample = self.post_act(sample) + return sample + + +# FFN +def FeedForward(dim, mult=4): + inner_dim = int(dim * mult) + return nn.Sequential( + nn.LayerNorm(dim), + nn.Linear(dim, inner_dim, bias=False), + nn.GELU(), + nn.Linear(inner_dim, dim, bias=False), + ) + + +def reshape_tensor(x, heads): + bs, length, width = x.shape + # (bs, length, width) --> (bs, length, n_heads, dim_per_head) + x = x.view(bs, length, heads, -1) + # (bs, length, n_heads, dim_per_head) --> (bs, n_heads, length, dim_per_head) + x = x.transpose(1, 2) + # (bs, n_heads, length, dim_per_head) --> (bs*n_heads, length, dim_per_head) + x = x.reshape(bs, heads, length, -1) + return x + + +class PerceiverAttention(nn.Module): + def __init__(self, *, dim, dim_head=64, heads=8): + super().__init__() + self.scale = dim_head**-0.5 + self.dim_head = dim_head + self.heads = heads + inner_dim = dim_head * heads + + self.norm1 = nn.LayerNorm(dim) + self.norm2 = nn.LayerNorm(dim) + + self.to_q = nn.Linear(dim, inner_dim, bias=False) + self.to_kv = nn.Linear(dim, inner_dim * 2, bias=False) + self.to_out = nn.Linear(inner_dim, dim, bias=False) + + def forward(self, x, latents, shift=None, scale=None): + """ + Args: + x (torch.Tensor): image features + shape (b, n1, D) + latent (torch.Tensor): latent features + shape (b, n2, D) + """ + x = self.norm1(x) + latents = self.norm2(latents) + + if shift is not None and scale is not None: + latents = latents * (1 + scale.unsqueeze(1)) + shift.unsqueeze(1) + + b, l, _ = latents.shape + + q = self.to_q(latents) + kv_input = torch.cat((x, latents), dim=-2) + k, v = self.to_kv(kv_input).chunk(2, dim=-1) + + q = reshape_tensor(q, self.heads) + k = reshape_tensor(k, self.heads) + v = reshape_tensor(v, self.heads) + + # attention + scale = 1 / math.sqrt(math.sqrt(self.dim_head)) + weight = (q * scale) @ (k * scale).transpose( + -2, -1 + ) # More stable with f16 than dividing afterwards + weight = torch.softmax(weight.float(), dim=-1).type(weight.dtype) + out = weight @ v + + out = out.permute(0, 2, 1, 3).reshape(b, l, -1) + + return self.to_out(out) + + +class Resampler(nn.Module): + def __init__( + self, + dim=1024, + depth=8, + dim_head=64, + heads=16, + num_queries=8, + embedding_dim=768, + output_dim=1024, + ff_mult=4, + *args, + **kwargs, + ): + super().__init__() + + self.latents = nn.Parameter(torch.randn(1, num_queries, dim) / dim**0.5) + + self.proj_in = nn.Linear(embedding_dim, dim) + + self.proj_out = nn.Linear(dim, output_dim) + self.norm_out = nn.LayerNorm(output_dim) + + self.layers = nn.ModuleList([]) + for _ in range(depth): + self.layers.append( + nn.ModuleList( + [ + PerceiverAttention(dim=dim, dim_head=dim_head, heads=heads), + FeedForward(dim=dim, mult=ff_mult), + ] + ) + ) + + def forward(self, x): + + latents = self.latents.repeat(x.size(0), 1, 1) + + x = self.proj_in(x) + + for attn, ff in self.layers: + latents = attn(x, latents) + latents + latents = ff(latents) + latents + + latents = self.proj_out(latents) + return self.norm_out(latents) + + +class TimeResampler(nn.Module): + def __init__( + self, + dim=1024, + depth=8, + dim_head=64, + heads=16, + num_queries=8, + embedding_dim=768, + output_dim=1024, + ff_mult=4, + timestep_in_dim=320, + timestep_flip_sin_to_cos=True, + timestep_freq_shift=0, + ): + super().__init__() + + self.latents = nn.Parameter(torch.randn(1, num_queries, dim) / dim**0.5) + + self.proj_in = nn.Linear(embedding_dim, dim) + + self.proj_out = nn.Linear(dim, output_dim) + self.norm_out = nn.LayerNorm(output_dim) + + self.layers = nn.ModuleList([]) + for _ in range(depth): + self.layers.append( + nn.ModuleList( + [ + # msa + PerceiverAttention(dim=dim, dim_head=dim_head, heads=heads), + # ff + FeedForward(dim=dim, mult=ff_mult), + # adaLN + nn.Sequential(nn.SiLU(), nn.Linear(dim, 4 * dim, bias=True)), + ] + ) + ) + + # time + self.time_proj = Timesteps( + timestep_in_dim, timestep_flip_sin_to_cos, timestep_freq_shift + ) + self.time_embedding = TimestepEmbedding(timestep_in_dim, dim, act_fn="silu") + + # adaLN + # self.adaLN_modulation = nn.Sequential( + # nn.SiLU(), + # nn.Linear(timestep_out_dim, 6 * timestep_out_dim, bias=True) + # ) + + def forward(self, x, timestep, need_temb=False): + timestep_emb = self.embedding_time(x, timestep) # bs, dim + + latents = self.latents.repeat(x.size(0), 1, 1) + + x = self.proj_in(x) + x = x + timestep_emb[:, None] + + for attn, ff, adaLN_modulation in self.layers: + shift_msa, scale_msa, shift_mlp, scale_mlp = adaLN_modulation( + timestep_emb + ).chunk(4, dim=1) + latents = attn(x, latents, shift_msa, scale_msa) + latents + + res = latents + for idx_ff in range(len(ff)): + layer_ff = ff[idx_ff] + latents = layer_ff(latents) + if idx_ff == 0 and isinstance(layer_ff, nn.LayerNorm): # adaLN + latents = latents * ( + 1 + scale_mlp.unsqueeze(1) + ) + shift_mlp.unsqueeze(1) + latents = latents + res + + # latents = ff(latents) + latents + + latents = self.proj_out(latents) + latents = self.norm_out(latents) + + if need_temb: + return latents, timestep_emb + else: + return latents + + def embedding_time(self, sample, timestep): + + # 1. time + timesteps = timestep + if not torch.is_tensor(timesteps): + # TODO: this requires sync between CPU and GPU. So try to pass timesteps as tensors if you can + # This would be a good case for the `match` statement (Python 3.10+) + is_mps = sample.device.type == "mps" + if isinstance(timestep, float): + dtype = torch.float32 if is_mps else torch.float64 + else: + dtype = torch.int32 if is_mps else torch.int64 + timesteps = torch.tensor([timesteps], dtype=dtype, device=sample.device) + elif len(timesteps.shape) == 0: + timesteps = timesteps[None].to(sample.device) + + # broadcast to batch dimension in a way that's compatible with ONNX/Core ML + timesteps = timesteps.expand(sample.shape[0]) + + t_emb = self.time_proj(timesteps) + + # timesteps does not contain any weights and will always return f32 tensors + # but time_embedding might actually be running in fp16. so we need to cast here. + # there might be better ways to encapsulate this. + t_emb = t_emb.to(dtype=sample.dtype) + + emb = self.time_embedding(t_emb, None) + return emb \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/ipadapter/utils.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/ipadapter/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..4556dde3dfe8e6026d7cb291984e0dc3ee6b6af3 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/ipadapter/utils.py @@ -0,0 +1,136 @@ +import torch +from torch import Tensor +from .flux.layers import DoubleStreamBlockIPA, SingleStreamBlockIPA +from comfy.ldm.flux.layers import timestep_embedding +from types import MethodType + +def FluxUpdateModules(bi, ip_attn_procs, image_emb): + flux_model = bi.model + bi.add_object_patch(f"diffusion_model.forward_orig", MethodType(forward_orig_ipa, flux_model.diffusion_model)) + for i, original in enumerate(flux_model.diffusion_model.double_blocks): + patch_name = f"double_blocks.{i}" + maybe_patched_layer = bi.get_model_object(f"diffusion_model.{patch_name}") + # if there's already a patch there, collect its adapters and replace it + procs = [ip_attn_procs[patch_name]] + embs = [image_emb] + if isinstance(maybe_patched_layer, DoubleStreamBlockIPA): + procs = maybe_patched_layer.ip_adapter + procs + embs = maybe_patched_layer.image_emb + embs + # initial ipa models with image embeddings + new_layer = DoubleStreamBlockIPA(original, procs, embs) + # for example, ComfyUI internally uses model.add_patches to add loras + bi.add_object_patch(f"diffusion_model.{patch_name}", new_layer) + for i, original in enumerate(flux_model.diffusion_model.single_blocks): + patch_name = f"single_blocks.{i}" + maybe_patched_layer = bi.get_model_object(f"diffusion_model.{patch_name}") + procs = [ip_attn_procs[patch_name]] + embs = [image_emb] + if isinstance(maybe_patched_layer, SingleStreamBlockIPA): + procs = maybe_patched_layer.ip_adapter + procs + embs = maybe_patched_layer.image_emb + embs + # initial ipa models with image embeddings + new_layer = SingleStreamBlockIPA(original, procs, embs) + bi.add_object_patch(f"diffusion_model.{patch_name}", new_layer) + +def is_model_pathched(model): + def test(mod): + if isinstance(mod, DoubleStreamBlockIPA): + return True + else: + for p in mod.children(): + if test(p): + return True + return False + + result = test(model) + return result + + +def forward_orig_ipa( + self, + img: Tensor, + img_ids: Tensor, + txt: Tensor, + txt_ids: Tensor, + timesteps: Tensor, + y: Tensor, + guidance: Tensor|None = None, + control=None, + transformer_options={}, + attn_mask: Tensor = None, +) -> Tensor: + patches_replace = transformer_options.get("patches_replace", {}) + if img.ndim != 3 or txt.ndim != 3: + raise ValueError("Input img and txt tensors must have 3 dimensions.") + + # running on sequences img + img = self.img_in(img) + vec = self.time_in(timestep_embedding(timesteps, 256).to(img.dtype)) + if self.params.guidance_embed: + if guidance is None: + raise ValueError("Didn't get guidance strength for guidance distilled model.") + vec = vec + self.guidance_in(timestep_embedding(guidance, 256).to(img.dtype)) + + vec = vec + self.vector_in(y[:,:self.params.vec_in_dim]) + txt = self.txt_in(txt) + + ids = torch.cat((txt_ids, img_ids), dim=1) + pe = self.pe_embedder(ids) + + blocks_replace = patches_replace.get("dit", {}) + for i, block in enumerate(self.double_blocks): + if ("double_block", i) in blocks_replace: + def block_wrap(args): + out = {} + if isinstance(block, DoubleStreamBlockIPA): # ipadaper + out["img"], out["txt"] = block(img=args["img"], txt=args["txt"], vec=args["vec"], pe=args["pe"], t=args["timesteps"], attn_mask=args.get("attn_mask")) + else: + out["img"], out["txt"] = block(img=args["img"], txt=args["txt"], vec=args["vec"], pe=args["pe"], attn_mask=args.get("attn_mask")) + return out + out = blocks_replace[("double_block", i)]({"img": img, "txt": txt, "vec": vec, "pe": pe, "timesteps": timesteps, "attn_mask": attn_mask}, {"original_block": block_wrap}) + txt = out["txt"] + img = out["img"] + else: + if isinstance(block, DoubleStreamBlockIPA): # ipadaper + img, txt = block(img=img, txt=txt, vec=vec, pe=pe, t=timesteps, attn_mask=attn_mask) + else: + img, txt = block(img=img, txt=txt, vec=vec, pe=pe, attn_mask=attn_mask) + + if control is not None: # Controlnet + control_i = control.get("input") + if i < len(control_i): + add = control_i[i] + if add is not None: + img += add + + img = torch.cat((txt, img), 1) + + for i, block in enumerate(self.single_blocks): + if ("single_block", i) in blocks_replace: + def block_wrap(args): + out = {} + if isinstance(block, SingleStreamBlockIPA): # ipadaper + out["img"] = block(args["img"], vec=args["vec"], pe=args["pe"], t=args["timesteps"], attn_mask=args.get("attn_mask")) + else: + out["img"] = block(args["img"], vec=args["vec"], pe=args["pe"], attn_mask=args.get("attn_mask")) + return out + + out = blocks_replace[("single_block", i)]({"img": img, "vec": vec, "pe": pe, "timesteps": timesteps, "attn_mask": attn_mask}, {"original_block": block_wrap}) + img = out["img"] + else: + if isinstance(block, SingleStreamBlockIPA): # ipadaper + img = block(img, vec=vec, pe=pe, t=timesteps, attn_mask=attn_mask) + else: + img = block(img, vec=vec, pe=pe, attn_mask=attn_mask) + + if control is not None: # Controlnet + control_o = control.get("output") + if i < len(control_o): + add = control_o[i] + if add is not None: + img[:, txt.shape[1] :, ...] += add + + img = img[:, txt.shape[1] :, ...] + + img = self.final_layer(img, vec) # (N, T, patch_size ** 2 * out_channels) + return img \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/kolors/__init__.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/kolors/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/kolors/__pycache__/__init__.cpython-312.pyc b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/kolors/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b2f142e795d596ac8f7486ce3a42ef9a31b478a2 Binary files /dev/null and b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/kolors/__pycache__/__init__.cpython-312.pyc differ diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/kolors/__pycache__/text_encode.cpython-312.pyc b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/kolors/__pycache__/text_encode.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..517415089cc9286f9a5da12894d2790c493cd78e Binary files /dev/null and b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/kolors/__pycache__/text_encode.cpython-312.pyc differ diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/kolors/chatglm/__init__.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/kolors/chatglm/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/kolors/chatglm/config_chatglm.json b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/kolors/chatglm/config_chatglm.json new file mode 100644 index 0000000000000000000000000000000000000000..c6e19300822b25ae0a07125bbc171c6581dbeda4 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/kolors/chatglm/config_chatglm.json @@ -0,0 +1,42 @@ +{ + "_name_or_path": "THUDM/chatglm3-6b-base", + "model_type": "chatglm", + "architectures": [ + "ChatGLMModel" + ], + "auto_map": { + "AutoConfig": "configuration_chatglm.ChatGLMConfig", + "AutoModel": "modeling_chatglm.ChatGLMForConditionalGeneration", + "AutoModelForCausalLM": "modeling_chatglm.ChatGLMForConditionalGeneration", + "AutoModelForSeq2SeqLM": "modeling_chatglm.ChatGLMForConditionalGeneration", + "AutoModelForSequenceClassification": "modeling_chatglm.ChatGLMForSequenceClassification" + }, + "add_bias_linear": false, + "add_qkv_bias": true, + "apply_query_key_layer_scaling": true, + "apply_residual_connection_post_layernorm": false, + "attention_dropout": 0.0, + "attention_softmax_in_fp32": true, + "bias_dropout_fusion": true, + "ffn_hidden_size": 13696, + "fp32_residual_connection": false, + "hidden_dropout": 0.0, + "hidden_size": 4096, + "kv_channels": 128, + "layernorm_epsilon": 1e-05, + "multi_query_attention": true, + "multi_query_group_num": 2, + "num_attention_heads": 32, + "num_layers": 28, + "original_rope": true, + "padded_vocab_size": 65024, + "post_layer_norm": true, + "rmsnorm": true, + "seq_length": 32768, + "use_cache": true, + "torch_dtype": "float16", + "transformers_version": "4.30.2", + "tie_word_embeddings": false, + "eos_token_id": 2, + "pad_token_id": 0 +} \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/kolors/chatglm/configuration_chatglm.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/kolors/chatglm/configuration_chatglm.py new file mode 100644 index 0000000000000000000000000000000000000000..7a0e01bceb1982e9bff680823f97c93e191ddaab --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/kolors/chatglm/configuration_chatglm.py @@ -0,0 +1,60 @@ +from transformers import PretrainedConfig + +class ChatGLMConfig(PretrainedConfig): + model_type = "chatglm" + def __init__( + self, + num_layers=28, + padded_vocab_size=65024, + hidden_size=4096, + ffn_hidden_size=13696, + kv_channels=128, + num_attention_heads=32, + seq_length=2048, + hidden_dropout=0.0, + classifier_dropout=None, + attention_dropout=0.0, + layernorm_epsilon=1e-5, + rmsnorm=True, + apply_residual_connection_post_layernorm=False, + post_layer_norm=True, + add_bias_linear=False, + add_qkv_bias=False, + bias_dropout_fusion=True, + multi_query_attention=False, + multi_query_group_num=1, + apply_query_key_layer_scaling=True, + attention_softmax_in_fp32=True, + fp32_residual_connection=False, + quantization_bit=0, + pre_seq_len=None, + prefix_projection=False, + **kwargs + ): + self.num_layers = num_layers + self.vocab_size = padded_vocab_size + self.padded_vocab_size = padded_vocab_size + self.hidden_size = hidden_size + self.ffn_hidden_size = ffn_hidden_size + self.kv_channels = kv_channels + self.num_attention_heads = num_attention_heads + self.seq_length = seq_length + self.hidden_dropout = hidden_dropout + self.classifier_dropout = classifier_dropout + self.attention_dropout = attention_dropout + self.layernorm_epsilon = layernorm_epsilon + self.rmsnorm = rmsnorm + self.apply_residual_connection_post_layernorm = apply_residual_connection_post_layernorm + self.post_layer_norm = post_layer_norm + self.add_bias_linear = add_bias_linear + self.add_qkv_bias = add_qkv_bias + self.bias_dropout_fusion = bias_dropout_fusion + self.multi_query_attention = multi_query_attention + self.multi_query_group_num = multi_query_group_num + self.apply_query_key_layer_scaling = apply_query_key_layer_scaling + self.attention_softmax_in_fp32 = attention_softmax_in_fp32 + self.fp32_residual_connection = fp32_residual_connection + self.quantization_bit = quantization_bit + self.pre_seq_len = pre_seq_len + self.prefix_projection = prefix_projection + super().__init__(**kwargs) \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/kolors/chatglm/modeling_chatglm.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/kolors/chatglm/modeling_chatglm.py new file mode 100644 index 0000000000000000000000000000000000000000..e25704c7a71b361255a319ac1657afb0e762b45f --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/kolors/chatglm/modeling_chatglm.py @@ -0,0 +1,1298 @@ +""" PyTorch ChatGLM model. """ + +import math +import copy +import warnings +import re +import sys + +import torch +import torch.utils.checkpoint +import torch.nn.functional as F +from torch import nn +from torch.nn import CrossEntropyLoss, LayerNorm +from torch.nn import CrossEntropyLoss, LayerNorm, MSELoss, BCEWithLogitsLoss +from torch.nn.utils import skip_init +from typing import Optional, Tuple, Union, List, Callable, Dict, Any +from copy import deepcopy + +from transformers.modeling_outputs import ( + BaseModelOutputWithPast, + CausalLMOutputWithPast, + SequenceClassifierOutputWithPast, +) +from transformers.modeling_utils import PreTrainedModel +from transformers.utils import logging +from transformers.generation.logits_process import LogitsProcessor +from transformers.generation.utils import LogitsProcessorList, StoppingCriteriaList, GenerationConfig, ModelOutput + +try: + from .configuration_chatglm import ChatGLMConfig +except: + from .configuration_chatglm import ChatGLMConfig + + +# flags required to enable jit fusion kernels + +if sys.platform != 'darwin': + torch._C._jit_set_profiling_mode(False) + torch._C._jit_set_profiling_executor(False) + torch._C._jit_override_can_fuse_on_cpu(True) + torch._C._jit_override_can_fuse_on_gpu(True) + +logger = logging.get_logger(__name__) + +_CHECKPOINT_FOR_DOC = "THUDM/ChatGLM" +_CONFIG_FOR_DOC = "ChatGLM6BConfig" + +CHATGLM_6B_PRETRAINED_MODEL_ARCHIVE_LIST = [ + "THUDM/chatglm3-6b-base", + # See all ChatGLM models at https://huggingface.co/models?filter=chatglm +] + + +def default_init(cls, *args, **kwargs): + return cls(*args, **kwargs) + + +class InvalidScoreLogitsProcessor(LogitsProcessor): + def __call__(self, input_ids: torch.LongTensor, scores: torch.FloatTensor) -> torch.FloatTensor: + if torch.isnan(scores).any() or torch.isinf(scores).any(): + scores.zero_() + scores[..., 5] = 5e4 + return scores + + +class PrefixEncoder(torch.nn.Module): + """ + The torch.nn model to encode the prefix + Input shape: (batch-size, prefix-length) + Output shape: (batch-size, prefix-length, 2*layers*hidden) + """ + + def __init__(self, config: ChatGLMConfig): + super().__init__() + self.prefix_projection = config.prefix_projection + if self.prefix_projection: + # Use a two-layer MLP to encode the prefix + kv_size = config.num_layers * config.kv_channels * config.multi_query_group_num * 2 + self.embedding = torch.nn.Embedding(config.pre_seq_len, kv_size) + self.trans = torch.nn.Sequential( + torch.nn.Linear(kv_size, config.hidden_size), + torch.nn.Tanh(), + torch.nn.Linear(config.hidden_size, kv_size) + ) + else: + self.embedding = torch.nn.Embedding(config.pre_seq_len, + config.num_layers * config.kv_channels * config.multi_query_group_num * 2) + + def forward(self, prefix: torch.Tensor): + if self.prefix_projection: + prefix_tokens = self.embedding(prefix) + past_key_values = self.trans(prefix_tokens) + else: + past_key_values = self.embedding(prefix) + return past_key_values + + +def split_tensor_along_last_dim( + tensor: torch.Tensor, + num_partitions: int, + contiguous_split_chunks: bool = False, +) -> List[torch.Tensor]: + """Split a tensor along its last dimension. + + Arguments: + tensor: input tensor. + num_partitions: number of partitions to split the tensor + contiguous_split_chunks: If True, make each chunk contiguous + in memory. + + Returns: + A list of Tensors + """ + # Get the size and dimension. + last_dim = tensor.dim() - 1 + last_dim_size = tensor.size()[last_dim] // num_partitions + # Split. + tensor_list = torch.split(tensor, last_dim_size, dim=last_dim) + # Note: torch.split does not create contiguous tensors by default. + if contiguous_split_chunks: + return tuple(chunk.contiguous() for chunk in tensor_list) + + return tensor_list + + +class RotaryEmbedding(nn.Module): + def __init__(self, dim, original_impl=False, device=None, dtype=None): + super().__init__() + inv_freq = 1.0 / (10000 ** (torch.arange(0, dim, 2, device=device).to(dtype=dtype) / dim)) + self.register_buffer("inv_freq", inv_freq) + self.dim = dim + self.original_impl = original_impl + + def forward_impl( + self, seq_len: int, n_elem: int, dtype: torch.dtype, device: torch.device, base: int = 10000 + ): + """Enhanced Transformer with Rotary Position Embedding. + + Derived from: https://github.com/labmlai/annotated_deep_learning_paper_implementations/blob/master/labml_nn/ + transformers/rope/__init__.py. MIT License: + https://github.com/labmlai/annotated_deep_learning_paper_implementations/blob/master/license. + """ + # $\Theta = {\theta_i = 10000^{\frac{2(i-1)}{d}}, i \in [1, 2, ..., \frac{d}{2}]}$ + theta = 1.0 / (base ** (torch.arange(0, n_elem, 2, dtype=torch.float, device=device) / n_elem)) + + # Create position indexes `[0, 1, ..., seq_len - 1]` + seq_idx = torch.arange(seq_len, dtype=torch.float, device=device) + + # Calculate the product of position index and $\theta_i$ + idx_theta = torch.outer(seq_idx, theta).float() + + cache = torch.stack([torch.cos(idx_theta), torch.sin(idx_theta)], dim=-1) + + # this is to mimic the behaviour of complex32, else we will get different results + if dtype in (torch.float16, torch.bfloat16, torch.int8): + cache = cache.bfloat16() if dtype == torch.bfloat16 else cache.half() + return cache + + def forward(self, max_seq_len, offset=0): + return self.forward_impl( + max_seq_len, self.dim, dtype=self.inv_freq.dtype, device=self.inv_freq.device + ) + + +@torch.jit.script +def apply_rotary_pos_emb(x: torch.Tensor, rope_cache: torch.Tensor) -> torch.Tensor: + # x: [sq, b, np, hn] + sq, b, np, hn = x.size(0), x.size(1), x.size(2), x.size(3) + rot_dim = rope_cache.shape[-2] * 2 + x, x_pass = x[..., :rot_dim], x[..., rot_dim:] + # truncate to support variable sizes + rope_cache = rope_cache[:sq] + xshaped = x.reshape(sq, -1, np, rot_dim // 2, 2) + rope_cache = rope_cache.view(sq, -1, 1, xshaped.size(3), 2) + x_out2 = torch.stack( + [ + xshaped[..., 0] * rope_cache[..., 0] - xshaped[..., 1] * rope_cache[..., 1], + xshaped[..., 1] * rope_cache[..., 0] + xshaped[..., 0] * rope_cache[..., 1], + ], + -1, + ) + x_out2 = x_out2.flatten(3) + return torch.cat((x_out2, x_pass), dim=-1) + + +class RMSNorm(torch.nn.Module): + def __init__(self, normalized_shape, eps=1e-5, device=None, dtype=None, **kwargs): + super().__init__() + self.weight = torch.nn.Parameter(torch.empty(normalized_shape, device=device, dtype=dtype)) + self.eps = eps + + def forward(self, hidden_states: torch.Tensor): + input_dtype = hidden_states.dtype + variance = hidden_states.to(torch.float32).pow(2).mean(-1, keepdim=True) + hidden_states = hidden_states * torch.rsqrt(variance + self.eps) + + return (self.weight * hidden_states).to(input_dtype) + + +class CoreAttention(torch.nn.Module): + def __init__(self, config: ChatGLMConfig, layer_number): + super(CoreAttention, self).__init__() + + self.apply_query_key_layer_scaling = config.apply_query_key_layer_scaling + self.attention_softmax_in_fp32 = config.attention_softmax_in_fp32 + if self.apply_query_key_layer_scaling: + self.attention_softmax_in_fp32 = True + self.layer_number = max(1, layer_number) + + projection_size = config.kv_channels * config.num_attention_heads + + # Per attention head and per partition values. + self.hidden_size_per_partition = projection_size + self.hidden_size_per_attention_head = projection_size // config.num_attention_heads + self.num_attention_heads_per_partition = config.num_attention_heads + + coeff = None + self.norm_factor = math.sqrt(self.hidden_size_per_attention_head) + if self.apply_query_key_layer_scaling: + coeff = self.layer_number + self.norm_factor *= coeff + self.coeff = coeff + + self.attention_dropout = torch.nn.Dropout(config.attention_dropout) + + def forward(self, query_layer, key_layer, value_layer, attention_mask): + pytorch_major_version = int(torch.__version__.split('.')[0]) + if pytorch_major_version >= 2: + query_layer, key_layer, value_layer = [k.permute(1, 2, 0, 3) for k in [query_layer, key_layer, value_layer]] + if attention_mask is None and query_layer.shape[2] == key_layer.shape[2]: + context_layer = torch.nn.functional.scaled_dot_product_attention(query_layer, key_layer, value_layer, + is_causal=True) + else: + if attention_mask is not None: + attention_mask = ~attention_mask + context_layer = torch.nn.functional.scaled_dot_product_attention(query_layer, key_layer, value_layer, + attention_mask) + context_layer = context_layer.permute(2, 0, 1, 3) + new_context_layer_shape = context_layer.size()[:-2] + (self.hidden_size_per_partition,) + context_layer = context_layer.reshape(*new_context_layer_shape) + else: + # Raw attention scores + + # [b, np, sq, sk] + output_size = (query_layer.size(1), query_layer.size(2), query_layer.size(0), key_layer.size(0)) + + # [sq, b, np, hn] -> [sq, b * np, hn] + query_layer = query_layer.view(output_size[2], output_size[0] * output_size[1], -1) + # [sk, b, np, hn] -> [sk, b * np, hn] + key_layer = key_layer.view(output_size[3], output_size[0] * output_size[1], -1) + + # preallocting input tensor: [b * np, sq, sk] + matmul_input_buffer = torch.empty( + output_size[0] * output_size[1], output_size[2], output_size[3], dtype=query_layer.dtype, + device=query_layer.device + ) + + # Raw attention scores. [b * np, sq, sk] + matmul_result = torch.baddbmm( + matmul_input_buffer, + query_layer.transpose(0, 1), # [b * np, sq, hn] + key_layer.transpose(0, 1).transpose(1, 2), # [b * np, hn, sk] + beta=0.0, + alpha=(1.0 / self.norm_factor), + ) + + # change view to [b, np, sq, sk] + attention_scores = matmul_result.view(*output_size) + + # =========================== + # Attention probs and dropout + # =========================== + + # attention scores and attention mask [b, np, sq, sk] + if self.attention_softmax_in_fp32: + attention_scores = attention_scores.float() + if self.coeff is not None: + attention_scores = attention_scores * self.coeff + if attention_mask is None and attention_scores.shape[2] == attention_scores.shape[3]: + attention_mask = torch.ones(output_size[0], 1, output_size[2], output_size[3], + device=attention_scores.device, dtype=torch.bool) + attention_mask.tril_() + attention_mask = ~attention_mask + if attention_mask is not None: + attention_scores = attention_scores.masked_fill(attention_mask, float("-inf")) + attention_probs = F.softmax(attention_scores, dim=-1) + attention_probs = attention_probs.type_as(value_layer) + + # This is actually dropping out entire tokens to attend to, which might + # seem a bit unusual, but is taken from the original Transformer paper. + attention_probs = self.attention_dropout(attention_probs) + # ========================= + # Context layer. [sq, b, hp] + # ========================= + + # value_layer -> context layer. + # [sk, b, np, hn] --> [b, np, sq, hn] + + # context layer shape: [b, np, sq, hn] + output_size = (value_layer.size(1), value_layer.size(2), query_layer.size(0), value_layer.size(3)) + # change view [sk, b * np, hn] + value_layer = value_layer.view(value_layer.size(0), output_size[0] * output_size[1], -1) + # change view [b * np, sq, sk] + attention_probs = attention_probs.view(output_size[0] * output_size[1], output_size[2], -1) + # matmul: [b * np, sq, hn] + context_layer = torch.bmm(attention_probs, value_layer.transpose(0, 1)) + # change view [b, np, sq, hn] + context_layer = context_layer.view(*output_size) + # [b, np, sq, hn] --> [sq, b, np, hn] + context_layer = context_layer.permute(2, 0, 1, 3).contiguous() + # [sq, b, np, hn] --> [sq, b, hp] + new_context_layer_shape = context_layer.size()[:-2] + (self.hidden_size_per_partition,) + context_layer = context_layer.view(*new_context_layer_shape) + + return context_layer + + +class SelfAttention(torch.nn.Module): + """Parallel self-attention layer abstract class. + + Self-attention layer takes input with size [s, b, h] + and returns output of the same size. + """ + + def __init__(self, config: ChatGLMConfig, layer_number, device=None): + super(SelfAttention, self).__init__() + self.layer_number = max(1, layer_number) + + self.projection_size = config.kv_channels * config.num_attention_heads + + # Per attention head and per partition values. + self.hidden_size_per_attention_head = self.projection_size // config.num_attention_heads + self.num_attention_heads_per_partition = config.num_attention_heads + + self.multi_query_attention = config.multi_query_attention + self.qkv_hidden_size = 3 * self.projection_size + if self.multi_query_attention: + self.num_multi_query_groups_per_partition = config.multi_query_group_num + self.qkv_hidden_size = ( + self.projection_size + 2 * self.hidden_size_per_attention_head * config.multi_query_group_num + ) + self.query_key_value = nn.Linear(config.hidden_size, self.qkv_hidden_size, + bias=config.add_bias_linear or config.add_qkv_bias, + device=device, **_config_to_kwargs(config) + ) + + self.core_attention = CoreAttention(config, self.layer_number) + + # Output. + self.dense = nn.Linear(self.projection_size, config.hidden_size, bias=config.add_bias_linear, + device=device, **_config_to_kwargs(config) + ) + + def _allocate_memory(self, inference_max_sequence_len, batch_size, device=None, dtype=None): + if self.multi_query_attention: + num_attention_heads = self.num_multi_query_groups_per_partition + else: + num_attention_heads = self.num_attention_heads_per_partition + return torch.empty( + inference_max_sequence_len, + batch_size, + num_attention_heads, + self.hidden_size_per_attention_head, + dtype=dtype, + device=device, + ) + + def forward( + self, hidden_states, attention_mask, rotary_pos_emb, kv_cache=None, use_cache=True + ): + # hidden_states: [sq, b, h] + + # ================================================= + # Pre-allocate memory for key-values for inference. + # ================================================= + # ===================== + # Query, Key, and Value + # ===================== + + # Attention heads [sq, b, h] --> [sq, b, (np * 3 * hn)] + mixed_x_layer = self.query_key_value(hidden_states) + + if self.multi_query_attention: + (query_layer, key_layer, value_layer) = mixed_x_layer.split( + [ + self.num_attention_heads_per_partition * self.hidden_size_per_attention_head, + self.num_multi_query_groups_per_partition * self.hidden_size_per_attention_head, + self.num_multi_query_groups_per_partition * self.hidden_size_per_attention_head, + ], + dim=-1, + ) + query_layer = query_layer.view( + query_layer.size()[:-1] + (self.num_attention_heads_per_partition, self.hidden_size_per_attention_head) + ) + key_layer = key_layer.view( + key_layer.size()[:-1] + (self.num_multi_query_groups_per_partition, self.hidden_size_per_attention_head) + ) + value_layer = value_layer.view( + value_layer.size()[:-1] + + (self.num_multi_query_groups_per_partition, self.hidden_size_per_attention_head) + ) + else: + new_tensor_shape = mixed_x_layer.size()[:-1] + \ + (self.num_attention_heads_per_partition, + 3 * self.hidden_size_per_attention_head) + mixed_x_layer = mixed_x_layer.view(*new_tensor_shape) + + # [sq, b, np, 3 * hn] --> 3 [sq, b, np, hn] + (query_layer, key_layer, value_layer) = split_tensor_along_last_dim(mixed_x_layer, 3) + + # apply relative positional encoding (rotary embedding) + if rotary_pos_emb is not None: + query_layer = apply_rotary_pos_emb(query_layer, rotary_pos_emb) + key_layer = apply_rotary_pos_emb(key_layer, rotary_pos_emb) + + # adjust key and value for inference + if kv_cache is not None: + cache_k, cache_v = kv_cache + key_layer = torch.cat((cache_k, key_layer), dim=0) + value_layer = torch.cat((cache_v, value_layer), dim=0) + if use_cache: + kv_cache = (key_layer, value_layer) + else: + kv_cache = None + + if self.multi_query_attention: + key_layer = key_layer.unsqueeze(-2) + key_layer = key_layer.expand( + -1, -1, -1, self.num_attention_heads_per_partition // self.num_multi_query_groups_per_partition, -1 + ) + key_layer = key_layer.contiguous().view( + key_layer.size()[:2] + (self.num_attention_heads_per_partition, self.hidden_size_per_attention_head) + ) + value_layer = value_layer.unsqueeze(-2) + value_layer = value_layer.expand( + -1, -1, -1, self.num_attention_heads_per_partition // self.num_multi_query_groups_per_partition, -1 + ) + value_layer = value_layer.contiguous().view( + value_layer.size()[:2] + (self.num_attention_heads_per_partition, self.hidden_size_per_attention_head) + ) + + # ================================== + # core attention computation + # ================================== + + context_layer = self.core_attention(query_layer, key_layer, value_layer, attention_mask) + + # ================= + # Output. [sq, b, h] + # ================= + + output = self.dense(context_layer) + + return output, kv_cache + + +def _config_to_kwargs(args): + common_kwargs = { + "dtype": args.torch_dtype, + } + return common_kwargs + + +class MLP(torch.nn.Module): + """MLP. + + MLP will take the input with h hidden state, project it to 4*h + hidden dimension, perform nonlinear transformation, and project the + state back into h hidden dimension. + """ + + def __init__(self, config: ChatGLMConfig, device=None): + super(MLP, self).__init__() + + self.add_bias = config.add_bias_linear + + # Project to 4h. If using swiglu double the output width, see https://arxiv.org/pdf/2002.05202.pdf + self.dense_h_to_4h = nn.Linear( + config.hidden_size, + config.ffn_hidden_size * 2, + bias=self.add_bias, + device=device, + **_config_to_kwargs(config) + ) + + def swiglu(x): + x = torch.chunk(x, 2, dim=-1) + return F.silu(x[0]) * x[1] + + self.activation_func = swiglu + + # Project back to h. + self.dense_4h_to_h = nn.Linear( + config.ffn_hidden_size, + config.hidden_size, + bias=self.add_bias, + device=device, + **_config_to_kwargs(config) + ) + + def forward(self, hidden_states): + # [s, b, 4hp] + intermediate_parallel = self.dense_h_to_4h(hidden_states) + intermediate_parallel = self.activation_func(intermediate_parallel) + # [s, b, h] + output = self.dense_4h_to_h(intermediate_parallel) + return output + + +class GLMBlock(torch.nn.Module): + """A single transformer layer. + + Transformer layer takes input with size [s, b, h] and returns an + output of the same size. + """ + + def __init__(self, config: ChatGLMConfig, layer_number, device=None): + super(GLMBlock, self).__init__() + self.layer_number = layer_number + + self.apply_residual_connection_post_layernorm = config.apply_residual_connection_post_layernorm + + self.fp32_residual_connection = config.fp32_residual_connection + + LayerNormFunc = RMSNorm if config.rmsnorm else LayerNorm + # Layernorm on the input data. + self.input_layernorm = LayerNormFunc(config.hidden_size, eps=config.layernorm_epsilon, device=device, + dtype=config.torch_dtype) + + # Self attention. + self.self_attention = SelfAttention(config, layer_number, device=device) + self.hidden_dropout = config.hidden_dropout + + # Layernorm on the attention output + self.post_attention_layernorm = LayerNormFunc(config.hidden_size, eps=config.layernorm_epsilon, device=device, + dtype=config.torch_dtype) + + # MLP + self.mlp = MLP(config, device=device) + + def forward( + self, hidden_states, attention_mask, rotary_pos_emb, kv_cache=None, use_cache=True, + ): + # hidden_states: [s, b, h] + + # Layer norm at the beginning of the transformer layer. + layernorm_output = self.input_layernorm(hidden_states) + # Self attention. + attention_output, kv_cache = self.self_attention( + layernorm_output, + attention_mask, + rotary_pos_emb, + kv_cache=kv_cache, + use_cache=use_cache + ) + + # Residual connection. + if self.apply_residual_connection_post_layernorm: + residual = layernorm_output + else: + residual = hidden_states + + layernorm_input = torch.nn.functional.dropout(attention_output, p=self.hidden_dropout, training=self.training) + layernorm_input = residual + layernorm_input + + # Layer norm post the self attention. + layernorm_output = self.post_attention_layernorm(layernorm_input) + + # MLP. + mlp_output = self.mlp(layernorm_output) + + # Second residual connection. + if self.apply_residual_connection_post_layernorm: + residual = layernorm_output + else: + residual = layernorm_input + + output = torch.nn.functional.dropout(mlp_output, p=self.hidden_dropout, training=self.training) + output = residual + output + + return output, kv_cache + + +class GLMTransformer(torch.nn.Module): + """Transformer class.""" + + def __init__(self, config: ChatGLMConfig, device=None): + super(GLMTransformer, self).__init__() + + self.fp32_residual_connection = config.fp32_residual_connection + self.post_layer_norm = config.post_layer_norm + + # Number of layers. + self.num_layers = config.num_layers + + # Transformer layers. + def build_layer(layer_number): + return GLMBlock(config, layer_number, device=device) + + self.layers = torch.nn.ModuleList([build_layer(i + 1) for i in range(self.num_layers)]) + + if self.post_layer_norm: + LayerNormFunc = RMSNorm if config.rmsnorm else LayerNorm + # Final layer norm before output. + self.final_layernorm = LayerNormFunc(config.hidden_size, eps=config.layernorm_epsilon, device=device, + dtype=config.torch_dtype) + + self.gradient_checkpointing = False + + def _get_layer(self, layer_number): + return self.layers[layer_number] + + def forward( + self, hidden_states, attention_mask, rotary_pos_emb, kv_caches=None, + use_cache: Optional[bool] = True, + output_hidden_states: Optional[bool] = False, + ): + if not kv_caches: + kv_caches = [None for _ in range(self.num_layers)] + presents = () if use_cache else None + if self.gradient_checkpointing and self.training: + if use_cache: + logger.warning_once( + "`use_cache=True` is incompatible with gradient checkpointing. Setting `use_cache=False`..." + ) + use_cache = False + + all_self_attentions = None + all_hidden_states = () if output_hidden_states else None + for index in range(self.num_layers): + if output_hidden_states: + all_hidden_states = all_hidden_states + (hidden_states,) + + layer = self._get_layer(index) + if self.gradient_checkpointing and self.training: + layer_ret = torch.utils.checkpoint.checkpoint( + layer, + hidden_states, + attention_mask, + rotary_pos_emb, + kv_caches[index], + use_cache + ) + else: + layer_ret = layer( + hidden_states, + attention_mask, + rotary_pos_emb, + kv_cache=kv_caches[index], + use_cache=use_cache + ) + hidden_states, kv_cache = layer_ret + if use_cache: + presents = presents + (kv_cache,) + + if output_hidden_states: + all_hidden_states = all_hidden_states + (hidden_states,) + + # Final layer norm. + if self.post_layer_norm: + hidden_states = self.final_layernorm(hidden_states) + + return hidden_states, presents, all_hidden_states, all_self_attentions + + +class ChatGLMPreTrainedModel(PreTrainedModel): + """ + An abstract class to handle weights initialization and + a simple interface for downloading and loading pretrained models. + """ + + is_parallelizable = False + supports_gradient_checkpointing = True + config_class = ChatGLMConfig + base_model_prefix = "transformer" + _no_split_modules = ["GLMBlock"] + + def _init_weights(self, module: nn.Module): + """Initialize the weights.""" + return + + def get_masks(self, input_ids, past_key_values, padding_mask=None): + batch_size, seq_length = input_ids.shape + full_attention_mask = torch.ones(batch_size, seq_length, seq_length, device=input_ids.device) + full_attention_mask.tril_() + past_length = 0 + if past_key_values: + past_length = past_key_values[0][0].shape[0] + if past_length: + full_attention_mask = torch.cat((torch.ones(batch_size, seq_length, past_length, + device=input_ids.device), full_attention_mask), dim=-1) + if padding_mask is not None: + full_attention_mask = full_attention_mask * padding_mask.unsqueeze(1) + if not past_length and padding_mask is not None: + full_attention_mask -= padding_mask.unsqueeze(-1) - 1 + full_attention_mask = (full_attention_mask < 0.5).bool() + full_attention_mask.unsqueeze_(1) + return full_attention_mask + + def get_position_ids(self, input_ids, device): + batch_size, seq_length = input_ids.shape + position_ids = torch.arange(seq_length, dtype=torch.long, device=device).unsqueeze(0).repeat(batch_size, 1) + return position_ids + + def _set_gradient_checkpointing(self, module, value=False): + if isinstance(module, GLMTransformer): + module.gradient_checkpointing = value + + +class Embedding(torch.nn.Module): + """Language model embeddings.""" + + def __init__(self, config: ChatGLMConfig, device=None): + super(Embedding, self).__init__() + + self.hidden_size = config.hidden_size + # Word embeddings (parallel). + self.word_embeddings = nn.Embedding( + config.padded_vocab_size, + self.hidden_size, + dtype=config.torch_dtype, + device=device + ) + self.fp32_residual_connection = config.fp32_residual_connection + + def forward(self, input_ids): + # Embeddings. + words_embeddings = self.word_embeddings(input_ids) + embeddings = words_embeddings + # Data format change to avoid explicit tranposes : [b s h] --> [s b h]. + embeddings = embeddings.transpose(0, 1).contiguous() + # If the input flag for fp32 residual connection is set, convert for float. + if self.fp32_residual_connection: + embeddings = embeddings.float() + return embeddings + + +class ChatGLMModel(ChatGLMPreTrainedModel): + def __init__(self, config: ChatGLMConfig, device=None, empty_init=True): + super().__init__(config) + if empty_init: + init_method = skip_init + else: + init_method = default_init + init_kwargs = {} + if device is not None: + init_kwargs["device"] = device + self.embedding = init_method(Embedding, config, **init_kwargs) + self.num_layers = config.num_layers + self.multi_query_group_num = config.multi_query_group_num + self.kv_channels = config.kv_channels + + # Rotary positional embeddings + self.seq_length = config.seq_length + rotary_dim = ( + config.hidden_size // config.num_attention_heads if config.kv_channels is None else config.kv_channels + ) + + self.rotary_pos_emb = RotaryEmbedding(rotary_dim // 2, original_impl=config.original_rope, device=device, + dtype=config.torch_dtype) + self.encoder = init_method(GLMTransformer, config, **init_kwargs) + self.output_layer = init_method(nn.Linear, config.hidden_size, config.padded_vocab_size, bias=False, + dtype=config.torch_dtype, **init_kwargs) + self.pre_seq_len = config.pre_seq_len + self.prefix_projection = config.prefix_projection + if self.pre_seq_len is not None: + for param in self.parameters(): + param.requires_grad = False + self.prefix_tokens = torch.arange(self.pre_seq_len).long() + self.prefix_encoder = PrefixEncoder(config) + self.dropout = torch.nn.Dropout(0.1) + + def get_input_embeddings(self): + return self.embedding.word_embeddings + + def get_prompt(self, batch_size, device, dtype=torch.half): + prefix_tokens = self.prefix_tokens.unsqueeze(0).expand(batch_size, -1).to(device) + past_key_values = self.prefix_encoder(prefix_tokens).type(dtype) + past_key_values = past_key_values.view( + batch_size, + self.pre_seq_len, + self.num_layers * 2, + self.multi_query_group_num, + self.kv_channels + ) + # seq_len, b, nh, hidden_size + past_key_values = self.dropout(past_key_values) + past_key_values = past_key_values.permute([2, 1, 0, 3, 4]).split(2) + return past_key_values + + def forward( + self, + input_ids, + position_ids: Optional[torch.Tensor] = None, + attention_mask: Optional[torch.BoolTensor] = None, + full_attention_mask: Optional[torch.BoolTensor] = None, + past_key_values: Optional[Tuple[Tuple[torch.Tensor, torch.Tensor], ...]] = None, + inputs_embeds: Optional[torch.Tensor] = None, + use_cache: Optional[bool] = None, + output_hidden_states: Optional[bool] = None, + return_dict: Optional[bool] = None, + ): + output_hidden_states = ( + output_hidden_states if output_hidden_states is not None else self.config.output_hidden_states + ) + use_cache = use_cache if use_cache is not None else self.config.use_cache + return_dict = return_dict if return_dict is not None else self.config.use_return_dict + + batch_size, seq_length = input_ids.shape + + if inputs_embeds is None: + inputs_embeds = self.embedding(input_ids) + + if self.pre_seq_len is not None: + if past_key_values is None: + past_key_values = self.get_prompt(batch_size=batch_size, device=input_ids.device, + dtype=inputs_embeds.dtype) + if attention_mask is not None: + attention_mask = torch.cat([attention_mask.new_ones((batch_size, self.pre_seq_len)), + attention_mask], dim=-1) + + if full_attention_mask is None: + if (attention_mask is not None and not attention_mask.all()) or (past_key_values and seq_length != 1): + full_attention_mask = self.get_masks(input_ids, past_key_values, padding_mask=attention_mask) + + # Rotary positional embeddings + rotary_pos_emb = self.rotary_pos_emb(self.seq_length) + if position_ids is not None: + rotary_pos_emb = rotary_pos_emb[position_ids] + else: + rotary_pos_emb = rotary_pos_emb[None, :seq_length] + rotary_pos_emb = rotary_pos_emb.transpose(0, 1).contiguous() + + # Run encoder. + hidden_states, presents, all_hidden_states, all_self_attentions = self.encoder( + inputs_embeds, full_attention_mask, rotary_pos_emb=rotary_pos_emb, + kv_caches=past_key_values, use_cache=use_cache, output_hidden_states=output_hidden_states + ) + + if not return_dict: + return tuple(v for v in [hidden_states, presents, all_hidden_states, all_self_attentions] if v is not None) + + return BaseModelOutputWithPast( + last_hidden_state=hidden_states, + past_key_values=presents, + hidden_states=all_hidden_states, + attentions=all_self_attentions, + ) + + def quantize(self, weight_bit_width: int): + from .quantization import quantize + quantize(self.encoder, weight_bit_width) + return self + + +class ChatGLMForConditionalGeneration(ChatGLMPreTrainedModel): + def __init__(self, config: ChatGLMConfig, empty_init=True, device=None): + super().__init__(config) + + self.max_sequence_length = config.max_length + self.transformer = ChatGLMModel(config, empty_init=empty_init, device=device) + self.config = config + self.quantized = False + + if self.config.quantization_bit: + self.quantize(self.config.quantization_bit, empty_init=True) + + def _update_model_kwargs_for_generation( + self, + outputs: ModelOutput, + model_kwargs: Dict[str, Any], + is_encoder_decoder: bool = False, + standardize_cache_format: bool = False, + ) -> Dict[str, Any]: + # update past_key_values + model_kwargs["past_key_values"] = self._extract_past_from_model_output( + outputs, standardize_cache_format=standardize_cache_format + ) + + # update attention mask + if "attention_mask" in model_kwargs: + attention_mask = model_kwargs["attention_mask"] + model_kwargs["attention_mask"] = torch.cat( + [attention_mask, attention_mask.new_ones((attention_mask.shape[0], 1))], dim=-1 + ) + + # update position ids + if "position_ids" in model_kwargs: + position_ids = model_kwargs["position_ids"] + new_position_id = position_ids[..., -1:].clone() + new_position_id += 1 + model_kwargs["position_ids"] = torch.cat( + [position_ids, new_position_id], dim=-1 + ) + + model_kwargs["is_first_forward"] = False + return model_kwargs + + def prepare_inputs_for_generation( + self, + input_ids: torch.LongTensor, + past_key_values: Optional[torch.Tensor] = None, + attention_mask: Optional[torch.Tensor] = None, + position_ids: Optional[torch.Tensor] = None, + use_cache: Optional[bool] = None, + is_first_forward: bool = True, + **kwargs + ) -> dict: + # only last token for input_ids if past is not None + if position_ids is None: + position_ids = self.get_position_ids(input_ids, device=input_ids.device) + if not is_first_forward: + if past_key_values is not None: + position_ids = position_ids[..., -1:] + input_ids = input_ids[:, -1:] + return { + "input_ids": input_ids, + "past_key_values": past_key_values, + "position_ids": position_ids, + "attention_mask": attention_mask, + "return_last_logit": True, + "use_cache": use_cache + } + + def forward( + self, + input_ids: Optional[torch.Tensor] = None, + position_ids: Optional[torch.Tensor] = None, + attention_mask: Optional[torch.Tensor] = None, + past_key_values: Optional[Tuple[torch.FloatTensor]] = None, + inputs_embeds: Optional[torch.Tensor] = None, + labels: Optional[torch.Tensor] = None, + use_cache: Optional[bool] = None, + output_attentions: Optional[bool] = None, + output_hidden_states: Optional[bool] = None, + return_dict: Optional[bool] = None, + return_last_logit: Optional[bool] = False, + ): + use_cache = use_cache if use_cache is not None else self.config.use_cache + return_dict = return_dict if return_dict is not None else self.config.use_return_dict + + transformer_outputs = self.transformer( + input_ids=input_ids, + position_ids=position_ids, + attention_mask=attention_mask, + past_key_values=past_key_values, + inputs_embeds=inputs_embeds, + use_cache=use_cache, + output_hidden_states=output_hidden_states, + return_dict=return_dict, + ) + + hidden_states = transformer_outputs[0] + if return_last_logit: + hidden_states = hidden_states[-1:] + lm_logits = self.transformer.output_layer(hidden_states) + lm_logits = lm_logits.transpose(0, 1).contiguous() + + loss = None + if labels is not None: + lm_logits = lm_logits.to(torch.float32) + + # Shift so that tokens < n predict n + shift_logits = lm_logits[..., :-1, :].contiguous() + shift_labels = labels[..., 1:].contiguous() + # Flatten the tokens + loss_fct = CrossEntropyLoss(ignore_index=-100) + loss = loss_fct(shift_logits.view(-1, shift_logits.size(-1)), shift_labels.view(-1)) + + lm_logits = lm_logits.to(hidden_states.dtype) + loss = loss.to(hidden_states.dtype) + + if not return_dict: + output = (lm_logits,) + transformer_outputs[1:] + return ((loss,) + output) if loss is not None else output + + return CausalLMOutputWithPast( + loss=loss, + logits=lm_logits, + past_key_values=transformer_outputs.past_key_values, + hidden_states=transformer_outputs.hidden_states, + attentions=transformer_outputs.attentions, + ) + + @staticmethod + def _reorder_cache( + past: Tuple[Tuple[torch.Tensor, torch.Tensor], ...], beam_idx: torch.LongTensor + ) -> Tuple[Tuple[torch.Tensor, torch.Tensor], ...]: + """ + This function is used to re-order the `past_key_values` cache if [`~PreTrainedModel.beam_search`] or + [`~PreTrainedModel.beam_sample`] is called. This is required to match `past_key_values` with the correct + beam_idx at every generation step. + + Output shares the same memory storage as `past`. + """ + return tuple( + ( + layer_past[0].index_select(1, beam_idx.to(layer_past[0].device)), + layer_past[1].index_select(1, beam_idx.to(layer_past[1].device)), + ) + for layer_past in past + ) + + def process_response(self, output, history): + content = "" + history = deepcopy(history) + for response in output.split("<|assistant|>"): + metadata, content = response.split("\n", maxsplit=1) + if not metadata.strip(): + content = content.strip() + history.append({"role": "assistant", "metadata": metadata, "content": content}) + content = content.replace("[[训练时间]]", "2023年") + else: + history.append({"role": "assistant", "metadata": metadata, "content": content}) + if history[0]["role"] == "system" and "tools" in history[0]: + content = "\n".join(content.split("\n")[1:-1]) + def tool_call(**kwargs): + return kwargs + parameters = eval(content) + content = {"name": metadata.strip(), "parameters": parameters} + else: + content = {"name": metadata.strip(), "content": content} + return content, history + + @torch.inference_mode() + def chat(self, tokenizer, query: str, history: List[Tuple[str, str]] = None, role: str = "user", + max_length: int = 8192, num_beams=1, do_sample=True, top_p=0.8, temperature=0.8, logits_processor=None, + **kwargs): + if history is None: + history = [] + if logits_processor is None: + logits_processor = LogitsProcessorList() + logits_processor.append(InvalidScoreLogitsProcessor()) + gen_kwargs = {"max_length": max_length, "num_beams": num_beams, "do_sample": do_sample, "top_p": top_p, + "temperature": temperature, "logits_processor": logits_processor, **kwargs} + inputs = tokenizer.build_chat_input(query, history=history, role=role) + inputs = inputs.to(self.device) + eos_token_id = [tokenizer.eos_token_id, tokenizer.get_command("<|user|>"), + tokenizer.get_command("<|observation|>")] + outputs = self.generate(**inputs, **gen_kwargs, eos_token_id=eos_token_id) + outputs = outputs.tolist()[0][len(inputs["input_ids"][0]):-1] + response = tokenizer.decode(outputs) + history.append({"role": role, "content": query}) + response, history = self.process_response(response, history) + return response, history + + @torch.inference_mode() + def stream_chat(self, tokenizer, query: str, history: List[Tuple[str, str]] = None, role: str = "user", + past_key_values=None,max_length: int = 8192, do_sample=True, top_p=0.8, temperature=0.8, + logits_processor=None, return_past_key_values=False, **kwargs): + if history is None: + history = [] + if logits_processor is None: + logits_processor = LogitsProcessorList() + logits_processor.append(InvalidScoreLogitsProcessor()) + eos_token_id = [tokenizer.eos_token_id, tokenizer.get_command("<|user|>"), + tokenizer.get_command("<|observation|>")] + gen_kwargs = {"max_length": max_length, "do_sample": do_sample, "top_p": top_p, + "temperature": temperature, "logits_processor": logits_processor, **kwargs} + if past_key_values is None: + inputs = tokenizer.build_chat_input(query, history=history, role=role) + else: + inputs = tokenizer.build_chat_input(query, role=role) + inputs = inputs.to(self.device) + if past_key_values is not None: + past_length = past_key_values[0][0].shape[0] + if self.transformer.pre_seq_len is not None: + past_length -= self.transformer.pre_seq_len + inputs.position_ids += past_length + attention_mask = inputs.attention_mask + attention_mask = torch.cat((attention_mask.new_ones(1, past_length), attention_mask), dim=1) + inputs['attention_mask'] = attention_mask + history.append({"role": role, "content": query}) + for outputs in self.stream_generate(**inputs, past_key_values=past_key_values, + eos_token_id=eos_token_id, return_past_key_values=return_past_key_values, + **gen_kwargs): + if return_past_key_values: + outputs, past_key_values = outputs + outputs = outputs.tolist()[0][len(inputs["input_ids"][0]):-1] + response = tokenizer.decode(outputs) + if response and response[-1] != "�": + response, new_history = self.process_response(response, history) + if return_past_key_values: + yield response, new_history, past_key_values + else: + yield response, new_history + + @torch.inference_mode() + def stream_generate( + self, + input_ids, + generation_config: Optional[GenerationConfig] = None, + logits_processor: Optional[LogitsProcessorList] = None, + stopping_criteria: Optional[StoppingCriteriaList] = None, + prefix_allowed_tokens_fn: Optional[Callable[[int, torch.Tensor], List[int]]] = None, + return_past_key_values=False, + **kwargs, + ): + batch_size, input_ids_seq_length = input_ids.shape[0], input_ids.shape[-1] + + if generation_config is None: + generation_config = self.generation_config + generation_config = copy.deepcopy(generation_config) + model_kwargs = generation_config.update(**kwargs) + model_kwargs["use_cache"] = generation_config.use_cache + bos_token_id, eos_token_id = generation_config.bos_token_id, generation_config.eos_token_id + + if isinstance(eos_token_id, int): + eos_token_id = [eos_token_id] + eos_token_id_tensor = torch.tensor(eos_token_id).to(input_ids.device) if eos_token_id is not None else None + + has_default_max_length = kwargs.get("max_length") is None and generation_config.max_length is not None + if has_default_max_length and generation_config.max_new_tokens is None: + warnings.warn( + f"Using `max_length`'s default ({generation_config.max_length}) to control the generation length. " + "This behaviour is deprecated and will be removed from the config in v5 of Transformers -- we" + " recommend using `max_new_tokens` to control the maximum length of the generation.", + UserWarning, + ) + elif generation_config.max_new_tokens is not None: + generation_config.max_length = generation_config.max_new_tokens + input_ids_seq_length + if not has_default_max_length: + logger.warn( + f"Both `max_new_tokens` (={generation_config.max_new_tokens}) and `max_length`(=" + f"{generation_config.max_length}) seem to have been set. `max_new_tokens` will take precedence. " + "Please refer to the documentation for more information. " + "(https://huggingface.co/docs/transformers/main/en/main_classes/text_generation)", + UserWarning, + ) + + if input_ids_seq_length >= generation_config.max_length: + input_ids_string = "decoder_input_ids" if self.config.is_encoder_decoder else "input_ids" + logger.warning( + f"Input length of {input_ids_string} is {input_ids_seq_length}, but `max_length` is set to" + f" {generation_config.max_length}. This can lead to unexpected behavior. You should consider" + " increasing `max_new_tokens`." + ) + + # 2. Set generation parameters if not already defined + logits_processor = logits_processor if logits_processor is not None else LogitsProcessorList() + stopping_criteria = stopping_criteria if stopping_criteria is not None else StoppingCriteriaList() + + logits_processor = self._get_logits_processor( + generation_config=generation_config, + input_ids_seq_length=input_ids_seq_length, + encoder_input_ids=input_ids, + prefix_allowed_tokens_fn=prefix_allowed_tokens_fn, + logits_processor=logits_processor, + ) + + stopping_criteria = self._get_stopping_criteria( + generation_config=generation_config, stopping_criteria=stopping_criteria + ) + logits_warper = self._get_logits_warper(generation_config) + + unfinished_sequences = input_ids.new(input_ids.shape[0]).fill_(1) + scores = None + while True: + model_inputs = self.prepare_inputs_for_generation(input_ids, **model_kwargs) + # forward pass to get next token + outputs = self( + **model_inputs, + return_dict=True, + output_attentions=False, + output_hidden_states=False, + ) + + next_token_logits = outputs.logits[:, -1, :] + + # pre-process distribution + next_token_scores = logits_processor(input_ids, next_token_logits) + next_token_scores = logits_warper(input_ids, next_token_scores) + + # sample + probs = nn.functional.softmax(next_token_scores, dim=-1) + if generation_config.do_sample: + next_tokens = torch.multinomial(probs, num_samples=1).squeeze(1) + else: + next_tokens = torch.argmax(probs, dim=-1) + # update generated ids, model inputs, and length for next step + input_ids = torch.cat([input_ids, next_tokens[:, None]], dim=-1) + model_kwargs = self._update_model_kwargs_for_generation( + outputs, model_kwargs, is_encoder_decoder=self.config.is_encoder_decoder + ) + unfinished_sequences = unfinished_sequences.mul( + next_tokens.tile(eos_token_id_tensor.shape[0], 1).ne(eos_token_id_tensor.unsqueeze(1)).prod(dim=0) + ) + if return_past_key_values: + yield input_ids, outputs.past_key_values + else: + yield input_ids + # stop when each sentence is finished, or if we exceed the maximum length + if unfinished_sequences.max() == 0 or stopping_criteria(input_ids, scores): + break + + def quantize(self, bits: int, empty_init=False, device=None, **kwargs): + if bits == 0: + return + + from .quantization import quantize + + if self.quantized: + logger.info("Already quantized.") + return self + + self.quantized = True + + self.config.quantization_bit = bits + + self.transformer.encoder = quantize(self.transformer.encoder, bits, empty_init=empty_init, device=device, + **kwargs) + return self + + +class ChatGLMForSequenceClassification(ChatGLMPreTrainedModel): + def __init__(self, config: ChatGLMConfig, empty_init=True, device=None): + super().__init__(config) + + self.num_labels = config.num_labels + self.transformer = ChatGLMModel(config, empty_init=empty_init, device=device) + + self.classifier_head = nn.Linear(config.hidden_size, config.num_labels, bias=True, dtype=torch.half) + if config.classifier_dropout is not None: + self.dropout = nn.Dropout(config.classifier_dropout) + else: + self.dropout = None + self.config = config + + if self.config.quantization_bit: + self.quantize(self.config.quantization_bit, empty_init=True) + + def forward( + self, + input_ids: Optional[torch.LongTensor] = None, + position_ids: Optional[torch.LongTensor] = None, + attention_mask: Optional[torch.Tensor] = None, + full_attention_mask: Optional[torch.Tensor] = None, + past_key_values: Optional[Tuple[Tuple[torch.Tensor, torch.Tensor], ...]] = None, + inputs_embeds: Optional[torch.LongTensor] = None, + labels: Optional[torch.LongTensor] = None, + use_cache: Optional[bool] = None, + output_hidden_states: Optional[bool] = None, + return_dict: Optional[bool] = None, + ) -> Union[Tuple[torch.Tensor, ...], SequenceClassifierOutputWithPast]: + return_dict = return_dict if return_dict is not None else self.config.use_return_dict + + transformer_outputs = self.transformer( + input_ids=input_ids, + position_ids=position_ids, + attention_mask=attention_mask, + full_attention_mask=full_attention_mask, + past_key_values=past_key_values, + inputs_embeds=inputs_embeds, + use_cache=use_cache, + output_hidden_states=output_hidden_states, + return_dict=return_dict, + ) + + hidden_states = transformer_outputs[0] + pooled_hidden_states = hidden_states[-1] + if self.dropout is not None: + pooled_hidden_states = self.dropout(pooled_hidden_states) + logits = self.classifier_head(pooled_hidden_states) + + loss = None + if labels is not None: + if self.config.problem_type is None: + if self.num_labels == 1: + self.config.problem_type = "regression" + elif self.num_labels > 1 and (labels.dtype == torch.long or labels.dtype == torch.int): + self.config.problem_type = "single_label_classification" + else: + self.config.problem_type = "multi_label_classification" + + if self.config.problem_type == "regression": + loss_fct = MSELoss() + if self.num_labels == 1: + loss = loss_fct(logits.squeeze().float(), labels.squeeze()) + else: + loss = loss_fct(logits.float(), labels) + elif self.config.problem_type == "single_label_classification": + loss_fct = CrossEntropyLoss() + loss = loss_fct(logits.view(-1, self.num_labels).float(), labels.view(-1)) + elif self.config.problem_type == "multi_label_classification": + loss_fct = BCEWithLogitsLoss() + loss = loss_fct(logits.float(), labels.view(-1, self.num_labels)) + + if not return_dict: + output = (logits,) + transformer_outputs[1:] + return ((loss,) + output) if loss is not None else output + + return SequenceClassifierOutputWithPast( + loss=loss, + logits=logits, + past_key_values=transformer_outputs.past_key_values, + hidden_states=transformer_outputs.hidden_states, + attentions=transformer_outputs.attentions, + ) \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/kolors/chatglm/quantization.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/kolors/chatglm/quantization.py new file mode 100644 index 0000000000000000000000000000000000000000..c3bb3926ffd6d4f18804097997eddda8d84bfb1d --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/kolors/chatglm/quantization.py @@ -0,0 +1,190 @@ +from torch.nn import Linear +from torch.nn.parameter import Parameter + +import bz2 +import torch +import base64 +import ctypes +from transformers.utils import logging + +from typing import List +from functools import partial + +logger = logging.get_logger(__name__) + +try: + from cpm_kernels.kernels.base import LazyKernelCModule, KernelFunction, round_up + + class Kernel: + def __init__(self, code: bytes, function_names: List[str]): + self.code = code + self._function_names = function_names + self._cmodule = LazyKernelCModule(self.code) + + for name in self._function_names: + setattr(self, name, KernelFunction(self._cmodule, name)) + + quantization_code = "$QlpoOTFBWSZTWU9yuJUAQHN//////////f/n/8/n///n//bt4dTidcVx8X3V9FV/92/v4B7/AD5FBQFAAAChSgKpFCFAFVSigUAAAEKhSgUUqgFBKigqVREQAABQBQIANDTTIGI00BkZBkNGE0A0BkBkGQGRkaNAaAGQNBoGgDIAAYIGTI0DQAQAaGmmQMRpoDIyDIaMJoBoDIDIMgMjI0aA0AMgaDQNAGQAAwQMmRoGgAgA0NNMgYjTQGRkGQ0YTQDQGQGQZAZGRo0BoAZA0GgaAMgABggZMjQNABABoaaZAxGmgMjIMhowmgGgMgMgyAyMjRoDQAyBoNA0AZAADBAyZGgaAAmqU1NEgJqnptU/Sn4jRR6J6epk2pqb1Q/SgAPUGgyNNGjQ2SBpoAZAAGg0NB6mgDIAAAAA2oaApSREBNAARhGiYEaEwU8pvImlP0k2aam1GaGqbFNM1MHpTwmkepmyU9R6nqPKekHqNNPUxNGhp6n6p6QaZ6o9TG1GMqcoV9ly6nRanHlq6zPNbnGZNi6HSug+2nPiZ13XcnFYZW+45W11CumhzYhchOJ2GLLV1OBjBjGf4TptOddTSOcVxhqYZMYwZXZZY00zI1paX5X9J+b+f4e+x43RXSxXPOdquiGpduatGyXneN696M9t4HU2eR5XX/kPhP261NTx3JO1Ow7LyuDmeo9a7d351T1ZxnvnrvYnrXv/hXxPCeuYx2XsNmO003eg9J3Z6U7b23meJ4ri01OdzTk9BNO96brz+qT5nuvvH3ds/G+m/JcG/F2XYuhXlvO+jP7U3XgrzPN/lr8Sf1n6j4j7jZs+s/T0tNaNNYzTs12rxjwztHlnire3Nzc3N1wuBwOBwXBvZfoHpD7rFmR99V5vj3aXza3xdBbXMalubTg/jIv5dfAi54Pdc75j4z412n3Npj3Ld/ENm7a3b/Cod6h/ret1/5vn/C+l+gdslMvgPSLJ8d8q+U66fevYn/tW1chleEtNTGlcHCbLRlq0tHzF5tsbbZZfHjjLgZu42XCuC3NrdjTasZGNzgxPIrGqp7r3p7L2p5XjnpPSmTd5XtzqnB6U87zzg1Ol0zd0zsLszxR6lkxp35u6/teL0L0W922cR7Lu1lpL9CsHirzuM2T+BgsyViT6LHcm0/Vr6U/7LGGyJeqTEjt0PHWhF5mCT7R9mtlDwriYv0Tyr/OxYt6qp5r0mPVT0608TqnqMZaarU2nFwrTzzlrs1ed7z1ux60wyr4ydCaTi3enW8x68x0zU7tXSlcmPSW1mGpWJMg4zmPC2lK96tp0OE80y4MfEvnZj8zGluR6b22ki1Ou9V2nCd9xovcPvcYMZYy0lvN60ScZ45vN6yeCeeXFb1lVjnnCar5fwXwE2bzJ4HI1XVPXfXZMm44GUsMpYsmLB65TuVdm0cl0b+i/wGNN66XjeV7zuPpHcnK/juhhjdfId5jMdE5nN0dGmmm2zZs2cexD5n9p/dY352XsvXHaZNWWsmmS1atjR452nYudzvqv2HMRyvNNnlMcDl3R2+yx2uVrBubTW9icHDVtbNXlZm7jma1rM4VurZZd2y6nUau7ZXZ7bVU+mnoOVxZGMrVmvX60605JwmzGZhhhjTWtaaaMaaGTGmNMZasY0iX8VMUl8eepaIrzGSpemWOQyZORk2bNpjUybMmxqYmknCGCFynutfksaZpjTNMaaatM0xsxcGR0sociNqxNSmhhR1ZJPbsn8qyF0t2qH6iYBclclalbtTTcHTDsPaX6rlnElph2Jyumumtynv2Kk8GI7rsvXbIcJgHJOSaSXnnGaI3m87RtVXJOZ/YtgdTE6Wpha6ZlE8ayXkef1fh602r2WwvfMXtMdLlkfnLFdYYwYso+bWqm7yJqHXZGw2nrS5ZanSYnWlxBxMF1V940K2wdrI7R6OYf7DGGamMmTSbRhlS45xmVOumF1EyPCmHrrN8wwZOOrdNtLeMtzFzDlWnfTBxMk2NaXIZHBYxYLD4w8yju0ao65Vz1OIXoS9dLanwCe1PWrYuWMqf1if1z2k2yYfKJ741PDgno1ZQ8DRqvUny3mNoWTzGO6m1DkrJI8JiR5cSd+vZdGOO8nrMoc5+NDUFsMSXaZJeNlMmGLtJsovOsUp7I9S5VojKxF6bTVEelXqlfJobQr3LozSh2Jk7VcrVMfhXqszGWMzNqGhqZY0OadxkyyMssKugZR0KNFXBHlqwmJgTE/BNVMk6ItJXZMR0H47GpXv/DMOvNkmVuaV1PRfEdxuqc7Hcd+ZV/zTLaRxWk0nl9CdCeM6mn5rstHIBcpiuwmUZXeq81DacHI2rmrZ5SuE5mOZd6LQrZg9mx32TprA8BMo5jKN6yLTCi3WzQaZSuhzTtM1fUTGVpG8Tw+KXI0tjEpiWxtLYynOlktSbVlaI5kxP8TDH8kx50xoxi5KcA4pcja8KWLRlO/Ks6q06ergnvm1ca3Tq8Uw7LTUsmWyctXPWmpitl/uvGcWTGXGuAXDfhqazGmjkxcJW5hMMMMpYsXl2TZYtVOddG3XCarUt6Ptq9CZXSNzyuRzqRZOjsxdBbFVz6OA5HI43r1jityVlVpVkxmOsyaYWE1NTGq1sOVh36mHMcxtSvcy70edG0ZGR3I1Go1GRlV7mWWo1G0ZGRqlvH40l7o4m5xMWLLLYyNjnqc8556mdPqLJ31n/1nWOncxzG1tizrHs/Z+d2vP/B/l8wdJ6rHUn2nbbDq4p6htFtYzMMMTaZis1K5GKzGNmxhmUx2DDlZ/qNnIx41xnaMfCZWYaZWtNLTNW8ND4Fw1MyZOCdM428suKG1ehW8TesOydg7J+YYcD4cYR+8dFK6M4E3HM9ZfRNNL+Sn6rsl4DsrDl2HpPCnfxjGXtbZtYys1ttlyJ4T+BvexjGWRjMszK4Jpc77D3GyuVD7q0+G8m9G+2+rGm7cOR2y7FdtY2XUYx/oNlfRYxhMYyYZkyyg55enna9Kt/FFi6GMMwYwdwxWgxGMLKYmUyGExTKMZkMFhkymKuh0NOBNnBu+23LdwDoZYYzGGMxtORaTU1pjTGWTTGGtMrNWUsyyTTLLG1qy2ZjbK2DBllWqxMtBMaYZQmcE7zvvRcTkclUwdkxTaSdyySt/7fpL+T1v516Ji97fwr5JbLu305zMn5+GMTTZ9F+y7ExwmGVfG44yxn3dLv6l5i+Wth1jCrDq21nW9LqvvDzz3Vf3LLH/O/32TJ/erx3bXftO4eF+G956D952K/An4NfvOpjFjExjevP/UmE0fIoZXx6/w6lX/no3D0bLt+ixjieBM6ksRd0yB4Lt2SwYNE+gd1detlZWUnpiZfGfFaK+4PyCa/v18V8X75pe9fLXzp7l3VjF76vWZmHwGz1IZNWT7b8yddJ4q5kyrVdfru6atWc7bVYztL9Jf4GXvT+Y8m9/YsXP6H018a8D4XVOqvfzqeR+6yZOD8dPv0+U7/q5Pl+2dNb0MjzGVH5p6MNQ7cOWvw62U9aHE8DprDek+McLyvDz+te+9Zhq5+YTruufMcWMabqysTmZVWjKPfnK0wyVcrsuhjZRdLkHNvD72b9abriOSGIxiLixMOoalNPXzy+wT/tf+U6HHONfsz+xe8ufHBdQWWGWLA9if0rsnmrxK5LvRZQeWsTCsrmOYy8VteVfuRfcVTtDLItLIsMYxZLdU/DbtSemxF6Z6Zo5WBXE4tFdCyVMMXMTEMZXVlS6Xec2T4e0tHsRcEuWshcJ2YsNF5rUx1E8ifCq6Z+ZP7qdCeu/aTwFd53l16/o0NOw6O3dLavP4Hbi4RdmuDk6DoYaninC0+o4uZjbJ7Rxeu0/FbuFg+q7DVS6fQe0rZ6NDGUNNU6DEqOaLTicKnYZMnBWruljQxoaS3dZhocDge0bSTyOvdAbG5hxe2xji7E/L55xX13wWNDi6HCekcFxfCPGxY0MXC+s7afWaMdDyjyr+o8Rudm/NabOZvdl274zH4f5XK9z6On1Pe/K5TdPAslg77BjuO6Y3eO7GqvOPG/stknp1leyvLL0Z7bl9I4noMvLkzytLhWYzrOZzLXCORe028rORzOg4N/L0HlMOQ3Pgmnbb6KczlabORpu980q37TBqRu0/p3PO6234Bl03Ynuz+9W7gnsEcmvYaYY3aMYY0wx3pYd+ujsXauWdaY5Xkbtl23fPzFHiDB/QMo0yFjBllYxTQYYyxkrwn7JufwJ/PfgJ+C83X69ni6zvXcnyXabv0ncbLwsceS+RNlyN2mnneJtX0ngYO0+e+0+UnA+Wch3ji8hj5an4h+i6XBySU4n+R0roVcbw5yvHrmr4Yw8Y7x6c+9POPYHI5HI5HI5HI5HGXGww4nE4nrVyOR8XeqPEO7PLOiukYa3Novk5hV4cdtYZLI93e+uxff2jRo0aNGjRo0aNG1bVtW1dy3m83m8+tQ5ZzHw3nObwOu8La9Rc1dtkdS8A3eTk823tnktXWlxN6Oixe06zrN70Isd9jiOgZFq9yfkPqP/SLhN2Myl8jDM43bl1nbcb4cO57jlh8Jow6pzXZdL4dyODTuuhu77FyO27DdwdRxmvO+O+3N2+BdqyTwLHVczDVY4UPE4O66/ZO2cx1LFzVdSXtF7G4HMbrauOHRw6c8FdZ5m9fHZHYZXfTlZquyynSyTTKke6vcffSD9pzPA/G7n7jxPmuhc1DHMynPMrGL6AdewYmwu5ko+UUyTwrMv27rPH1v1nGqd87+p6N6LU8k3NEng53xXyHS97+44OSg/sy/hn+Se6yfYNjW0/uTgP+PvWYzLMmjhcLB/gGpri6H83/84eUXWT6T9Hsv7785z/7z4icpW+zfXypuR7rx/gMdZb1/wC678pcs8/2a3mDitGHxl9mfPlll5MafWWqxk/eYuTDgcNMzDGWLWvsuglNxs53GtN6uWpktlW1tZZYcuinMMWmnNnJydze3b2Y1McBxrBkXw799izLMZZYyy0TkbsGM4p03S2uVu5s/XXUdSdec6smVxZYYGpVmT8A+8ajuEyV5FatkvVru2x6uxGXXbH4A+jvgP4GMYy3iPLXzq/6z65+E005ey+cwMZD3fZcqc6xpjTFjQ0P3U+e++cPYmTIwj0nrK5NPTfl3WvpfLtXDcb2HQMudYOxFXQBor4L4T6vrOauFctYXJQ++NUWmJe5bmx1jDiZS1dTqWxo4GR8jm3fttpmPHppk9PEyv4/y8/sO07XacOmcqc0x2Vi9BvNJvN5oW8x4mOsydpidRxMYJPx06m1bqPzq9KtK8sxXNXFodD/+MYYaJTLwOhc9brCsV18oOR1i4tXChyTkq4lf4y1Ke+9axjDHqs1mfBbMXuP4Hzi+X7t8vzv7bHerrUPgPCxhjre4fXdfLNtNM+Jd+Zdh8xd8wP87uNPoPgv4W7/5P2BuxfsMabNnMnza+54Pdi5U671GPZY8CehX8Voeoo7FHpkeEc6715FwHZrIrUrHaviPUbPZHND+IhczrP6FcYvhOZ0Di/ETt0OI+YwNWR9r7tpf6WDeZKZDB1+z2IthOl1mPyb5FluvEx9h9d0NnM0Y1XPFkWIsk1WotJ0PBMmkvjvQTd0e71tfeV+8r8lQ/tpzpsmxJ+InrI/dj2UajUajVTUajatRqNRtGo1Go1Go4wjeMpZFMVV9CHbofPraLsJ3JpWV2XOoanCuFky4y3PPNxucK2uKC1Lbdb1eo+m5XomN6HfeZsabHLHRX/K+offtNGGmHWctcVcG44MdSqsOLY9VzX+Zxfxn2HPdWTpzWvkrtJ8M5zorrKcquRytJ5N5DZmcaW02l76nWO+BqPXm1A2Ry/0q71dH/mqrqeFjkYxjEXtsX8qubTk67rGycyqsdm4tZx5D6D5hhi0waaWmiaMP81Yjii5qxPlPuU/GfTL1Y5E6Jyfiq63qTa39A4J0sOGDgO9WF9bOXl0XfPRbsY2bPNKPy1YrFYrFYmRhhlTIyMjJWJYZHXuCXI8OoXsvfljGLFicNifpp2XunoPiG1wtx3p1Tah+/DD66OnVtVXP9rKbVxOnL0tR/rHtqB5UDErUVcl11D4qqvjpOcxX7armUNJB3LpW6bxVvD08e8h3odKKvyCFZBdSh2FVcST9xV3n3T8t1j7Kr9qgrqXg+13Pt5U7JCvFXVIV1YG5lRhkVYZJYYDDD4KOIMoHCp26WS8GB7uBh2zIdgq/PKyInjV2STShuoapUdCpX1yTwqq/z1VvET7Kh5nVPkO8YyxjLt2MaaMmWTLQvx3qnzltnXW0p2jxgbEtSny/Osv8Y9pLMXYoHVPAhkVdWVeODhR6q9/Sxe2liwwZWMVvFXfRkeIDxAePUPIrdJ4ey6yquzH+PD/bUOWAu05qVHtFd8rrKHSoeNIOUqrYr3FXyToqfYJgwmJdKpXXOwYYegNNGMzfZPp/t3t/DVs4zjNTN61rRqaWaa4NYbRjTa0tWwy2Y2tGN8ZO8ofNKq4j9SL7I+cSm4/6ovLV5HNXLI0jJidwrtk6ynCaP6Z++GjRlWS3tLeW129Mi9evxU9mtz6s5J3Z7M2ngTgnKvmpomxpaLCzPfmx0JWE+m3NLDDGOX47RctdYYNK5jakdqLkRlI39n590T5zctGSwwZZDJj6kW8XSi6ot2MmWWJ0DUT3nuvebBudScjZ79g8cWJ8av0k+/bE5WKd5MdbFpbDVMxu1DVMmtNZGJvq1mtRbn6M+g/kP0FwDwr7quZs7xosNGpbscyxhhd9TyJyFwbLcxlTasg75vW7TsV5K7ji44XPMMrdoj+Y3rT0Hie62nlYV/pwczzOmdLqLhYkzGMzCZWGMQzGMSsZYY6Di1t4nlJ+Em63mJxrVLxPbYxNEdgc1dU2iOKyoYYWjNrEeHTYybVk0atSa7ehuwsWMWTqn1TrnS6hYsi71d1+s+k+ic70e20fzE/VaTdxT9ZtU4GIXdeNx3X77guYYfpHeTQjaMX6brOu4OY4K7Y2d9mbHarI5ox3p4GpJ2Vd/Tst60f7j999pppjR+Q/Qf8J/VaORs3cji7FfFuN61+ui9s8hix1OCh5KGVV23BPXvZfz3CLyHpix+exi8z/KnCnosY2eunor+cxyPO/xJ0vKey9OvE9VjqaYu0x3Z3jd6o2b1T12D+F8l232lwaaacD5LE8LBxu7WTlbWraWpew8Xexjel3E+wWD4APITdNqR8F3R3T0lunCQ4GaE9R37DxeCYfcHi4xci5ovKfxVs55y2hf+65E/Xdp6jR5nrebTmi5incpkyOjs50JvrZwstbbW6kfuuQw+2mykf/EXNFzxfKTrxew929TR6bWnGL//F3JFOFCQT3K4lQ" + + kernels = Kernel( + bz2.decompress(base64.b64decode(quantization_code)), + [ + "int4WeightCompression", + "int4WeightExtractionFloat", + "int4WeightExtractionHalf", + "int8WeightExtractionFloat", + "int8WeightExtractionHalf", + ], + ) +except Exception as exception: + kernels = None + logger.warning("Failed to load cpm_kernels:" + str(exception)) + + +class W8A16Linear(torch.autograd.Function): + @staticmethod + def forward(ctx, inp: torch.Tensor, quant_w: torch.Tensor, scale_w: torch.Tensor, weight_bit_width): + ctx.inp_shape = inp.size() + ctx.weight_bit_width = weight_bit_width + out_features = quant_w.size(0) + inp = inp.contiguous().view(-1, inp.size(-1)) + weight = extract_weight_to_half(quant_w, scale_w, weight_bit_width) + ctx.weight_shape = weight.size() + output = inp.mm(weight.t()) + ctx.save_for_backward(inp, quant_w, scale_w) + return output.view(*(ctx.inp_shape[:-1] + (out_features,))) + + @staticmethod + def backward(ctx, grad_output: torch.Tensor): + inp, quant_w, scale_w = ctx.saved_tensors + weight = extract_weight_to_half(quant_w, scale_w, ctx.weight_bit_width) + grad_output = grad_output.contiguous().view(-1, weight.size(0)) + grad_input = grad_output.mm(weight) + grad_weight = grad_output.t().mm(inp) + return grad_input.view(ctx.inp_shape), grad_weight.view(ctx.weight_shape), None, None + + +def compress_int4_weight(weight: torch.Tensor): # (n, m) + with torch.cuda.device(weight.device): + n, m = weight.size(0), weight.size(1) + assert m % 2 == 0 + m = m // 2 + out = torch.empty(n, m, dtype=torch.int8, device="cuda") + stream = torch.cuda.current_stream() + + gridDim = (n, 1, 1) + blockDim = (min(round_up(m, 32), 1024), 1, 1) + + kernels.int4WeightCompression( + gridDim, + blockDim, + 0, + stream, + [ctypes.c_void_p(weight.data_ptr()), ctypes.c_void_p(out.data_ptr()), ctypes.c_int32(n), ctypes.c_int32(m)], + ) + return out + + +def extract_weight_to_half(weight: torch.Tensor, scale_list: torch.Tensor, source_bit_width: int): + assert scale_list.dtype in [torch.half, torch.bfloat16] + assert weight.dtype in [torch.int8] + if source_bit_width == 8: + return weight.to(scale_list.dtype) * scale_list[:, None] + elif source_bit_width == 4: + func = ( + kernels.int4WeightExtractionHalf if scale_list.dtype == torch.half else kernels.int4WeightExtractionBFloat16 + ) + else: + assert False, "Unsupported bit-width" + + with torch.cuda.device(weight.device): + n, m = weight.size(0), weight.size(1) + out = torch.empty(n, m * (8 // source_bit_width), dtype=scale_list.dtype, device="cuda") + stream = torch.cuda.current_stream() + + gridDim = (n, 1, 1) + blockDim = (min(round_up(m, 32), 1024), 1, 1) + + func( + gridDim, + blockDim, + 0, + stream, + [ + ctypes.c_void_p(weight.data_ptr()), + ctypes.c_void_p(scale_list.data_ptr()), + ctypes.c_void_p(out.data_ptr()), + ctypes.c_int32(n), + ctypes.c_int32(m), + ], + ) + return out + + +class QuantizedLinear(torch.nn.Module): + def __init__(self, weight_bit_width: int, weight, bias=None, device="cpu", dtype=None, empty_init=False, *args, + **kwargs): + super().__init__() + self.weight_bit_width = weight_bit_width + + shape = weight.shape + + if weight is None or empty_init: + self.weight = torch.empty(shape[0], shape[1] * weight_bit_width // 8, dtype=torch.int8, device=device) + self.weight_scale = torch.empty(shape[0], dtype=dtype, device=device) + else: + self.weight_scale = weight.abs().max(dim=-1).values / ((2 ** (weight_bit_width - 1)) - 1) + self.weight = torch.round(weight / self.weight_scale[:, None]).to(torch.int8) + if weight_bit_width == 4: + self.weight = compress_int4_weight(self.weight) + try: + self.weight = Parameter(self.weight.to(device), requires_grad=False) + except: + self.weight.to(device, dtype=self.weight.dtype) + self.weight_scale = Parameter(self.weight_scale.to(device), requires_grad=False) + self.bias = Parameter(bias.to(device), requires_grad=False) if bias is not None else None + + def forward(self, input): + output = W8A16Linear.apply(input, self.weight, self.weight_scale, self.weight_bit_width) + if self.bias is not None: + output = output + self.bias + return output + + +def quantize(model, weight_bit_width, empty_init=False, device=None): + """Replace fp16 linear with quantized linear""" + for layer in model.layers: + layer.self_attention.query_key_value = QuantizedLinear( + weight_bit_width=weight_bit_width, + weight=layer.self_attention.query_key_value.weight.to(torch.cuda.current_device()), + bias=layer.self_attention.query_key_value.bias, + dtype=layer.self_attention.query_key_value.weight.dtype, + device=layer.self_attention.query_key_value.weight.device if device is None else device, + empty_init=empty_init + ) + layer.self_attention.dense = QuantizedLinear( + weight_bit_width=weight_bit_width, + weight=layer.self_attention.dense.weight.to(torch.cuda.current_device()), + bias=layer.self_attention.dense.bias, + dtype=layer.self_attention.dense.weight.dtype, + device=layer.self_attention.dense.weight.device if device is None else device, + empty_init=empty_init + ) + layer.mlp.dense_h_to_4h = QuantizedLinear( + weight_bit_width=weight_bit_width, + weight=layer.mlp.dense_h_to_4h.weight.to(torch.cuda.current_device()), + bias=layer.mlp.dense_h_to_4h.bias, + dtype=layer.mlp.dense_h_to_4h.weight.dtype, + device=layer.mlp.dense_h_to_4h.weight.device if device is None else device, + empty_init=empty_init + ) + layer.mlp.dense_4h_to_h = QuantizedLinear( + weight_bit_width=weight_bit_width, + weight=layer.mlp.dense_4h_to_h.weight.to(torch.cuda.current_device()), + bias=layer.mlp.dense_4h_to_h.bias, + dtype=layer.mlp.dense_4h_to_h.weight.dtype, + device=layer.mlp.dense_4h_to_h.weight.device if device is None else device, + empty_init=empty_init + ) + + return model \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/kolors/chatglm/tokenization_chatglm.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/kolors/chatglm/tokenization_chatglm.py new file mode 100644 index 0000000000000000000000000000000000000000..0d8b7e56f5382c4db85e6e636ab5012c817cd0a3 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/kolors/chatglm/tokenization_chatglm.py @@ -0,0 +1,300 @@ +import json +import os +import re +from typing import List, Optional, Union, Dict +from sentencepiece import SentencePieceProcessor +from transformers import PreTrainedTokenizer +from transformers.utils import logging, PaddingStrategy +from transformers.tokenization_utils_base import EncodedInput, BatchEncoding + +class SPTokenizer: + def __init__(self, model_path: str): + # reload tokenizer + assert os.path.isfile(model_path), model_path + self.sp_model = SentencePieceProcessor(model_file=model_path) + + # BOS / EOS token IDs + self.n_words: int = self.sp_model.vocab_size() + self.bos_id: int = self.sp_model.bos_id() + self.eos_id: int = self.sp_model.eos_id() + self.pad_id: int = self.sp_model.unk_id() + assert self.sp_model.vocab_size() == self.sp_model.get_piece_size() + + role_special_tokens = ["<|system|>", "<|user|>", "<|assistant|>", "<|observation|>"] + special_tokens = ["[MASK]", "[gMASK]", "[sMASK]", "sop", "eop"] + role_special_tokens + self.special_tokens = {} + self.index_special_tokens = {} + for token in special_tokens: + self.special_tokens[token] = self.n_words + self.index_special_tokens[self.n_words] = token + self.n_words += 1 + self.role_special_token_expression = "|".join([re.escape(token) for token in role_special_tokens]) + + def tokenize(self, s: str, encode_special_tokens=False): + if encode_special_tokens: + last_index = 0 + t = [] + for match in re.finditer(self.role_special_token_expression, s): + if last_index < match.start(): + t.extend(self.sp_model.EncodeAsPieces(s[last_index:match.start()])) + t.append(s[match.start():match.end()]) + last_index = match.end() + if last_index < len(s): + t.extend(self.sp_model.EncodeAsPieces(s[last_index:])) + return t + else: + return self.sp_model.EncodeAsPieces(s) + + def encode(self, s: str, bos: bool = False, eos: bool = False) -> List[int]: + assert type(s) is str + t = self.sp_model.encode(s) + if bos: + t = [self.bos_id] + t + if eos: + t = t + [self.eos_id] + return t + + def decode(self, t: List[int]) -> str: + text, buffer = "", [] + for token in t: + if token in self.index_special_tokens: + if buffer: + text += self.sp_model.decode(buffer) + buffer = [] + text += self.index_special_tokens[token] + else: + buffer.append(token) + if buffer: + text += self.sp_model.decode(buffer) + return text + + def decode_tokens(self, tokens: List[str]) -> str: + text = self.sp_model.DecodePieces(tokens) + return text + + def convert_token_to_id(self, token): + """ Converts a token (str) in an id using the vocab. """ + if token in self.special_tokens: + return self.special_tokens[token] + return self.sp_model.PieceToId(token) + + def convert_id_to_token(self, index): + """Converts an index (integer) in a token (str) using the vocab.""" + if index in self.index_special_tokens: + return self.index_special_tokens[index] + if index in [self.eos_id, self.bos_id, self.pad_id] or index < 0: + return "" + return self.sp_model.IdToPiece(index) + + +class ChatGLMTokenizer(PreTrainedTokenizer): + vocab_files_names = {"vocab_file": "tokenizer.model"} + + model_input_names = ["input_ids", "attention_mask", "position_ids"] + + def __init__(self, vocab_file, padding_side="left", clean_up_tokenization_spaces=False, encode_special_tokens=False, + **kwargs): + self.name = "GLMTokenizer" + + self.vocab_file = vocab_file + self.tokenizer = SPTokenizer(vocab_file) + self.special_tokens = { + "": self.tokenizer.bos_id, + "": self.tokenizer.eos_id, + "": self.tokenizer.pad_id + } + self.encode_special_tokens = encode_special_tokens + super().__init__(padding_side=padding_side, clean_up_tokenization_spaces=clean_up_tokenization_spaces, + encode_special_tokens=encode_special_tokens, + **kwargs) + + def get_command(self, token): + if token in self.special_tokens: + return self.special_tokens[token] + assert token in self.tokenizer.special_tokens, f"{token} is not a special token for {self.name}" + return self.tokenizer.special_tokens[token] + + @property + def unk_token(self) -> str: + return "" + + @property + def pad_token(self) -> str: + return "" + + @property + def pad_token_id(self): + return self.get_command("") + + @property + def eos_token(self) -> str: + return "" + + @property + def eos_token_id(self): + return self.get_command("") + + @property + def vocab_size(self): + return self.tokenizer.n_words + + def get_vocab(self): + """ Returns vocab as a dict """ + vocab = {self._convert_id_to_token(i): i for i in range(self.vocab_size)} + vocab.update(self.added_tokens_encoder) + return vocab + + def _tokenize(self, text, **kwargs): + return self.tokenizer.tokenize(text, encode_special_tokens=self.encode_special_tokens) + + def _convert_token_to_id(self, token): + """ Converts a token (str) in an id using the vocab. """ + return self.tokenizer.convert_token_to_id(token) + + def _convert_id_to_token(self, index): + """Converts an index (integer) in a token (str) using the vocab.""" + return self.tokenizer.convert_id_to_token(index) + + def convert_tokens_to_string(self, tokens: List[str]) -> str: + return self.tokenizer.decode_tokens(tokens) + + def save_vocabulary(self, save_directory, filename_prefix=None): + """ + Save the vocabulary and special tokens file to a directory. + + Args: + save_directory (`str`): + The directory in which to save the vocabulary. + filename_prefix (`str`, *optional*): + An optional prefix to add to the named of the saved files. + + Returns: + `Tuple(str)`: Paths to the files saved. + """ + if os.path.isdir(save_directory): + vocab_file = os.path.join( + save_directory, self.vocab_files_names["vocab_file"] + ) + else: + vocab_file = save_directory + + with open(self.vocab_file, 'rb') as fin: + proto_str = fin.read() + + with open(vocab_file, "wb") as writer: + writer.write(proto_str) + + return (vocab_file,) + + def get_prefix_tokens(self): + prefix_tokens = [self.get_command("[gMASK]"), self.get_command("sop")] + return prefix_tokens + + def build_single_message(self, role, metadata, message): + assert role in ["system", "user", "assistant", "observation"], role + role_tokens = [self.get_command(f"<|{role}|>")] + self.tokenizer.encode(f"{metadata}\n") + message_tokens = self.tokenizer.encode(message) + tokens = role_tokens + message_tokens + return tokens + + def build_chat_input(self, query, history=None, role="user"): + if history is None: + history = [] + input_ids = [] + for item in history: + content = item["content"] + if item["role"] == "system" and "tools" in item: + content = content + "\n" + json.dumps(item["tools"], indent=4, ensure_ascii=False) + input_ids.extend(self.build_single_message(item["role"], item.get("metadata", ""), content)) + input_ids.extend(self.build_single_message(role, "", query)) + input_ids.extend([self.get_command("<|assistant|>")]) + return self.batch_encode_plus([input_ids], return_tensors="pt", is_split_into_words=True) + + def build_inputs_with_special_tokens( + self, token_ids_0: List[int], token_ids_1: Optional[List[int]] = None + ) -> List[int]: + """ + Build model inputs from a sequence or a pair of sequence for sequence classification tasks by concatenating and + adding special tokens. A BERT sequence has the following format: + + - single sequence: `[CLS] X [SEP]` + - pair of sequences: `[CLS] A [SEP] B [SEP]` + + Args: + token_ids_0 (`List[int]`): + List of IDs to which the special tokens will be added. + token_ids_1 (`List[int]`, *optional*): + Optional second list of IDs for sequence pairs. + + Returns: + `List[int]`: List of [input IDs](../glossary#input-ids) with the appropriate special tokens. + """ + prefix_tokens = self.get_prefix_tokens() + token_ids_0 = prefix_tokens + token_ids_0 + if token_ids_1 is not None: + token_ids_0 = token_ids_0 + token_ids_1 + [self.get_command("")] + return token_ids_0 + + def _pad( + self, + encoded_inputs: Union[Dict[str, EncodedInput], BatchEncoding], + max_length: Optional[int] = None, + padding_strategy: PaddingStrategy = PaddingStrategy.DO_NOT_PAD, + pad_to_multiple_of: Optional[int] = None, + return_attention_mask: Optional[bool] = None, + **kwargs + ) -> dict: + """ + Pad encoded inputs (on left/right and up to predefined length or max length in the batch) + + Args: + encoded_inputs: + Dictionary of tokenized inputs (`List[int]`) or batch of tokenized inputs (`List[List[int]]`). + max_length: maximum length of the returned list and optionally padding length (see below). + Will truncate by taking into account the special tokens. + padding_strategy: PaddingStrategy to use for padding. + + - PaddingStrategy.LONGEST Pad to the longest sequence in the batch + - PaddingStrategy.MAX_LENGTH: Pad to the max length (default) + - PaddingStrategy.DO_NOT_PAD: Do not pad + The tokenizer padding sides are defined in self.padding_side: + + - 'left': pads on the left of the sequences + - 'right': pads on the right of the sequences + pad_to_multiple_of: (optional) Integer if set will pad the sequence to a multiple of the provided value. + This is especially useful to enable the use of Tensor Core on NVIDIA hardware with compute capability + `>= 7.5` (Volta). + return_attention_mask: + (optional) Set to False to avoid returning attention mask (default: set to model specifics) + """ + # Load from model defaults + assert self.padding_side == "left" + + required_input = encoded_inputs[self.model_input_names[0]] + seq_length = len(required_input) + + if padding_strategy == PaddingStrategy.LONGEST: + max_length = len(required_input) + + if max_length is not None and pad_to_multiple_of is not None and (max_length % pad_to_multiple_of != 0): + max_length = ((max_length // pad_to_multiple_of) + 1) * pad_to_multiple_of + + needs_to_be_padded = padding_strategy != PaddingStrategy.DO_NOT_PAD and len(required_input) != max_length + + # Initialize attention mask if not present. + if "attention_mask" not in encoded_inputs: + encoded_inputs["attention_mask"] = [1] * seq_length + + if "position_ids" not in encoded_inputs: + encoded_inputs["position_ids"] = list(range(seq_length)) + + if needs_to_be_padded: + difference = max_length - len(required_input) + + if "attention_mask" in encoded_inputs: + encoded_inputs["attention_mask"] = [0] * difference + encoded_inputs["attention_mask"] + if "position_ids" in encoded_inputs: + encoded_inputs["position_ids"] = [0] * difference + encoded_inputs["position_ids"] + encoded_inputs[self.model_input_names[0]] = [self.pad_token_id] * difference + required_input + + return encoded_inputs \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/kolors/chatglm/tokenizer/tokenizer.model b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/kolors/chatglm/tokenizer/tokenizer.model new file mode 100644 index 0000000000000000000000000000000000000000..8a8007697b7cc3d3868dcffbbebf8c1f2bd690ba --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/kolors/chatglm/tokenizer/tokenizer.model @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e7dc4c393423b76e4373e5157ddc34803a0189ba96b21ddbb40269d31468a6f2 +size 1018370 diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/kolors/chatglm/tokenizer/tokenizer_config.json b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/kolors/chatglm/tokenizer/tokenizer_config.json new file mode 100644 index 0000000000000000000000000000000000000000..f6f13c88707490cebd8023da86e8bf7a56fa21e3 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/kolors/chatglm/tokenizer/tokenizer_config.json @@ -0,0 +1,12 @@ +{ + "name_or_path": "THUDM/chatglm3-6b-base", + "remove_space": false, + "do_lower_case": false, + "tokenizer_class": "ChatGLMTokenizer", + "auto_map": { + "AutoTokenizer": [ + "tokenization_chatglm.ChatGLMTokenizer", + null + ] + } +} diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/kolors/chatglm/tokenizer/vocab.txt b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/kolors/chatglm/tokenizer/vocab.txt new file mode 100644 index 0000000000000000000000000000000000000000..8a8007697b7cc3d3868dcffbbebf8c1f2bd690ba --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/kolors/chatglm/tokenizer/vocab.txt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e7dc4c393423b76e4373e5157ddc34803a0189ba96b21ddbb40269d31468a6f2 +size 1018370 diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/kolors/clip_vision_config_vitl_336.json b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/kolors/clip_vision_config_vitl_336.json new file mode 100644 index 0000000000000000000000000000000000000000..20a019142e8d20db3cd149b4609a460c5295fd0f --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/kolors/clip_vision_config_vitl_336.json @@ -0,0 +1,18 @@ +{ + "attention_dropout": 0.0, + "dropout": 0.0, + "hidden_act": "quick_gelu", + "hidden_size": 1024, + "image_size": 336, + "initializer_factor": 1.0, + "initializer_range": 0.02, + "intermediate_size": 4096, + "layer_norm_eps": 1e-05, + "model_type": "clip_vision_model", + "num_attention_heads": 16, + "num_channels": 3, + "num_hidden_layers": 24, + "patch_size": 14, + "projection_dim": 768, + "torch_dtype": "float32" +} diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/kolors/loader.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/kolors/loader.py new file mode 100644 index 0000000000000000000000000000000000000000..9902286a671821c1df1f01c2b9d3c9b09da900eb --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/kolors/loader.py @@ -0,0 +1,303 @@ +import json +import os +import torch +import subprocess +import sys +import comfy.supported_models +import comfy.model_patcher +import comfy.model_management +import comfy.model_detection as model_detection +import comfy.model_base as model_base +from comfy.model_base import sdxl_pooled, CLIPEmbeddingNoiseAugmentation, Timestep, ModelType +from comfy.ldm.modules.diffusionmodules.openaimodel import UNetModel +from comfy.clip_vision import ClipVisionModel, Output +from comfy.utils import load_torch_file +from .chatglm.modeling_chatglm import ChatGLMModel, ChatGLMConfig +from .chatglm.tokenization_chatglm import ChatGLMTokenizer + +class KolorsUNetModel(UNetModel): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.encoder_hid_proj = torch.nn.Linear(4096, 2048, bias=True) + + def forward(self, *args, **kwargs): + with torch.cuda.amp.autocast(enabled=True): + if "context" in kwargs: + kwargs["context"] = self.encoder_hid_proj(kwargs["context"]) + result = super().forward(*args, **kwargs) + return result + +class KolorsSDXL(model_base.SDXL): + def __init__(self, model_config, model_type=ModelType.EPS, device=None): + model_base.BaseModel.__init__(self, model_config, model_type, device=device, unet_model=KolorsUNetModel) + self.embedder = Timestep(256) + self.noise_augmentor = CLIPEmbeddingNoiseAugmentation(**{"noise_schedule_config": {"timesteps": 1000, "beta_schedule": "squaredcos_cap_v2"}, "timestep_dim": 1280}) + + def encode_adm(self, **kwargs): + clip_pooled = sdxl_pooled(kwargs, self.noise_augmentor) + width = kwargs.get("width", 768) + height = kwargs.get("height", 768) + crop_w = kwargs.get("crop_w", 0) + crop_h = kwargs.get("crop_h", 0) + target_width = kwargs.get("target_width", width) + target_height = kwargs.get("target_height", height) + + out = [] + out.append(self.embedder(torch.Tensor([height]))) + out.append(self.embedder(torch.Tensor([width]))) + out.append(self.embedder(torch.Tensor([crop_h]))) + out.append(self.embedder(torch.Tensor([crop_w]))) + out.append(self.embedder(torch.Tensor([target_height]))) + out.append(self.embedder(torch.Tensor([target_width]))) + flat = torch.flatten(torch.cat(out)).unsqueeze( + dim=0).repeat(clip_pooled.shape[0], 1) + return torch.cat((clip_pooled.to(flat.device), flat), dim=1) + +class Kolors(comfy.supported_models.SDXL): + unet_config = { + "model_channels": 320, + "use_linear_in_transformer": True, + "transformer_depth": [0, 0, 2, 2, 10, 10], + "context_dim": 2048, + "adm_in_channels": 5632, + "use_temporal_attention": False, + } + + def get_model(self, state_dict, prefix="", device=None): + out = KolorsSDXL(self, model_type=self.model_type(state_dict, prefix), device=device, ) + out.__class__ = model_base.SDXL + if self.inpaint_model(): + out.set_inpaint() + return out + +def kolors_unet_config_from_diffusers_unet(state_dict, dtype=None): + match = {} + transformer_depth = [] + + attn_res = 1 + count_blocks = model_detection.count_blocks + down_blocks = count_blocks(state_dict, "down_blocks.{}") + for i in range(down_blocks): + attn_blocks = count_blocks( + state_dict, "down_blocks.{}.attentions.".format(i) + '{}') + res_blocks = count_blocks( + state_dict, "down_blocks.{}.resnets.".format(i) + '{}') + for ab in range(attn_blocks): + transformer_count = count_blocks( + state_dict, "down_blocks.{}.attentions.{}.transformer_blocks.".format(i, ab) + '{}') + transformer_depth.append(transformer_count) + if transformer_count > 0: + match["context_dim"] = state_dict["down_blocks.{}.attentions.{}.transformer_blocks.0.attn2.to_k.weight".format( + i, ab)].shape[1] + + attn_res *= 2 + if attn_blocks == 0: + for i in range(res_blocks): + transformer_depth.append(0) + + match["transformer_depth"] = transformer_depth + + match["model_channels"] = state_dict["conv_in.weight"].shape[0] + match["in_channels"] = state_dict["conv_in.weight"].shape[1] + match["adm_in_channels"] = None + if "class_embedding.linear_1.weight" in state_dict: + match["adm_in_channels"] = state_dict["class_embedding.linear_1.weight"].shape[1] + elif "add_embedding.linear_1.weight" in state_dict: + match["adm_in_channels"] = state_dict["add_embedding.linear_1.weight"].shape[1] + + Kolors = {'use_checkpoint': False, 'image_size': 32, 'out_channels': 4, 'use_spatial_transformer': True, 'legacy': False, + 'num_classes': 'sequential', 'adm_in_channels': 5632, 'dtype': dtype, 'in_channels': 4, 'model_channels': 320, + 'num_res_blocks': [2, 2, 2], 'transformer_depth': [0, 0, 2, 2, 10, 10], 'channel_mult': [1, 2, 4], 'transformer_depth_middle': 10, + 'use_linear_in_transformer': True, 'context_dim': 2048, 'num_head_channels': 64, 'transformer_depth_output': [0, 0, 0, 2, 2, 2, 10, 10, 10], + 'use_temporal_attention': False, 'use_temporal_resblock': False} + + Kolors_inpaint = {'use_checkpoint': False, 'image_size': 32, 'out_channels': 4, 'use_spatial_transformer': True, + 'legacy': False, + 'num_classes': 'sequential', 'adm_in_channels': 5632, 'dtype': dtype, 'in_channels': 9, + 'model_channels': 320, + 'num_res_blocks': [2, 2, 2], 'transformer_depth': [0, 0, 2, 2, 10, 10], 'channel_mult': [1, 2, 4], + 'transformer_depth_middle': 10, + 'use_linear_in_transformer': True, 'context_dim': 2048, 'num_head_channels': 64, + 'transformer_depth_output': [0, 0, 0, 2, 2, 2, 10, 10, 10], + 'use_temporal_attention': False, 'use_temporal_resblock': False} + + Kolors_ip2p = {'use_checkpoint': False, 'image_size': 32, 'out_channels': 4, 'use_spatial_transformer': True, + 'legacy': False, + 'num_classes': 'sequential', 'adm_in_channels': 5632, 'dtype': dtype, 'in_channels': 8, + 'model_channels': 320, + 'num_res_blocks': [2, 2, 2], 'transformer_depth': [0, 0, 2, 2, 10, 10], 'channel_mult': [1, 2, 4], + 'transformer_depth_middle': 10, + 'use_linear_in_transformer': True, 'context_dim': 2048, 'num_head_channels': 64, + 'transformer_depth_output': [0, 0, 0, 2, 2, 2, 10, 10, 10], + 'use_temporal_attention': False, 'use_temporal_resblock': False} + + SDXL = {'use_checkpoint': False, 'image_size': 32, 'out_channels': 4, 'use_spatial_transformer': True, + 'legacy': False, + 'num_classes': 'sequential', 'adm_in_channels': 2816, 'dtype': dtype, 'in_channels': 4, + 'model_channels': 320, + 'num_res_blocks': [2, 2, 2], 'transformer_depth': [0, 0, 2, 2, 10, 10], 'channel_mult': [1, 2, 4], + 'transformer_depth_middle': 10, + 'use_linear_in_transformer': True, 'context_dim': 2048, 'num_head_channels': 64, + 'transformer_depth_output': [0, 0, 0, 2, 2, 2, 10, 10, 10], + 'use_temporal_attention': False, 'use_temporal_resblock': False} + + SDXL_mid_cnet = {'use_checkpoint': False, 'image_size': 32, 'out_channels': 4, 'use_spatial_transformer': True, + 'legacy': False, + 'num_classes': 'sequential', 'adm_in_channels': 2816, 'dtype': dtype, 'in_channels': 4, + 'model_channels': 320, + 'num_res_blocks': [2, 2, 2], 'transformer_depth': [0, 0, 0, 0, 1, 1], 'channel_mult': [1, 2, 4], + 'transformer_depth_middle': 1, + 'use_linear_in_transformer': True, 'context_dim': 2048, 'num_head_channels': 64, + 'transformer_depth_output': [0, 0, 0, 0, 0, 0, 1, 1, 1], + 'use_temporal_attention': False, 'use_temporal_resblock': False} + + SDXL_small_cnet = {'use_checkpoint': False, 'image_size': 32, 'out_channels': 4, 'use_spatial_transformer': True, + 'legacy': False, + 'num_classes': 'sequential', 'adm_in_channels': 2816, 'dtype': dtype, 'in_channels': 4, + 'model_channels': 320, + 'num_res_blocks': [2, 2, 2], 'transformer_depth': [0, 0, 0, 0, 0, 0], 'channel_mult': [1, 2, 4], + 'transformer_depth_middle': 0, + 'use_linear_in_transformer': True, 'num_head_channels': 64, 'context_dim': 1, + 'transformer_depth_output': [0, 0, 0, 0, 0, 0, 0, 0, 0], + 'use_temporal_attention': False, 'use_temporal_resblock': False} + + supported_models = [Kolors, Kolors_inpaint, + Kolors_ip2p, SDXL, SDXL_mid_cnet, SDXL_small_cnet] + + + for unet_config in supported_models: + matches = True + for k in match: + if match[k] != unet_config[k]: + # print("key {} does not match".format(k), match[k], "||", unet_config[k]) + matches = False + break + if matches: + return model_detection.convert_config(unet_config) + return None + +# chatglm3 model +class chatGLM3Model(torch.nn.Module): + def __init__(self, textmodel_json_config=None, device='cpu', offload_device='cpu', model_path=None): + super().__init__() + if model_path is None: + raise ValueError("model_path is required") + self.device = device + if textmodel_json_config is None: + textmodel_json_config = os.path.join( + os.path.dirname(os.path.realpath(__file__)), + "chatglm", + "config_chatglm.json" + ) + with open(textmodel_json_config, 'r') as file: + config = json.load(file) + textmodel_json_config = ChatGLMConfig(**config) + is_accelerate_available = False + try: + from accelerate import init_empty_weights + from accelerate.utils import set_module_tensor_to_device + is_accelerate_available = True + except: + pass + + from contextlib import nullcontext + with (init_empty_weights() if is_accelerate_available else nullcontext()): + with torch.no_grad(): + print('torch version:', torch.__version__) + self.text_encoder = ChatGLMModel(textmodel_json_config).eval() + if '4bit' in model_path: + try: + import cpm_kernels + except ImportError: + print("Installing cpm_kernels...") + subprocess.run([sys.executable, "-m", "pip", "install", "cpm_kernels"], check=True) + pass + self.text_encoder.quantize(4) + elif '8bit' in model_path: + self.text_encoder.quantize(8) + + sd = load_torch_file(model_path) + if is_accelerate_available: + for key in sd: + set_module_tensor_to_device(self.text_encoder, key, device=offload_device, value=sd[key]) + else: + print("WARNING: Accelerate not available, use load_state_dict load model") + self.text_encoder.load_state_dict() + +def load_chatglm3(model_path=None): + if model_path is None: + return + + load_device = comfy.model_management.text_encoder_device() + offload_device = comfy.model_management.text_encoder_offload_device() + + glm3model = chatGLM3Model( + device=load_device, + offload_device=offload_device, + model_path=model_path + ) + tokenizer_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'chatglm', "tokenizer") + tokenizer = ChatGLMTokenizer.from_pretrained(tokenizer_path) + text_encoder = glm3model.text_encoder + return {"text_encoder":text_encoder, "tokenizer":tokenizer} + + +# clipvision model +def load_clipvision_vitl_336(path): + sd = load_torch_file(path) + if "vision_model.encoder.layers.22.layer_norm1.weight" in sd: + json_config = os.path.join(os.path.dirname(os.path.realpath(__file__)), "clip_vision_config_vitl_336.json") + else: + raise Exception("Unsupported clip vision model") + clip = ClipVisionModel(json_config) + m, u = clip.load_sd(sd) + if len(m) > 0: + print("missing clip vision: {}".format(m)) + u = set(u) + keys = list(sd.keys()) + for k in keys: + if k not in u: + t = sd.pop(k) + del t + return clip + +class applyKolorsUnet: + def __enter__(self): + import comfy.ldm.modules.diffusionmodules.openaimodel + import comfy.utils + import comfy.clip_vision + + self.original_UNET_MAP_BASIC = comfy.utils.UNET_MAP_BASIC.copy() + comfy.utils.UNET_MAP_BASIC.add(("encoder_hid_proj.weight", "encoder_hid_proj.weight"),) + comfy.utils.UNET_MAP_BASIC.add(("encoder_hid_proj.bias", "encoder_hid_proj.bias"),) + + self.original_unet_config_from_diffusers_unet = model_detection.unet_config_from_diffusers_unet + model_detection.unet_config_from_diffusers_unet = kolors_unet_config_from_diffusers_unet + + import comfy.supported_models + self.original_supported_models = comfy.supported_models.models + comfy.supported_models.models = [Kolors] + + self.original_load_clipvision_from_sd = comfy.clip_vision.load_clipvision_from_sd + comfy.clip_vision.load_clipvision_from_sd = load_clipvision_vitl_336 + + def __exit__(self, type, value, traceback): + import comfy.ldm.modules.diffusionmodules.openaimodel + import comfy.utils + import comfy.supported_models + import comfy.clip_vision + + comfy.utils.UNET_MAP_BASIC = self.original_UNET_MAP_BASIC + + model_detection.unet_config_from_diffusers_unet = self.original_unet_config_from_diffusers_unet + comfy.supported_models.models = self.original_supported_models + + comfy.clip_vision.load_clipvision_from_sd = self.original_load_clipvision_from_sd + + +def is_kolors_model(model): + unet_config = model.model.model_config.unet_config if hasattr(model, 'model') else None + if unet_config and "adm_in_channels" in unet_config and unet_config["adm_in_channels"] == 5632: + return True + else: + return False \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/kolors/model_patch.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/kolors/model_patch.py new file mode 100644 index 0000000000000000000000000000000000000000..7900a0a13d8e1639d8bcb3302d02c360d81c44fb --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/kolors/model_patch.py @@ -0,0 +1,66 @@ +import torch +from torch.nn import Linear +from types import MethodType +import comfy.model_management +import comfy.samplers +from comfy.cldm.cldm import ControlNet +from comfy.controlnet import ControlLora + +def patch_controlnet(model, control_net): + import comfy.controlnet + if isinstance(control_net, ControlLora): + del_keys = [] + for k in control_net.control_weights: + if k.startswith("label_emb.0.0."): + del_keys.append(k) + + for k in del_keys: + control_net.control_weights.pop(k) + + super_pre_run = ControlLora.pre_run + super_copy = ControlLora.copy + + super_forward = ControlNet.forward + + def KolorsControlNet_forward(self, x, hint, timesteps, context, **kwargs): + with torch.cuda.amp.autocast(enabled=True): + context = model.model.diffusion_model.encoder_hid_proj(context) + return super_forward(self, x, hint, timesteps, context, **kwargs) + + def KolorsControlLora_pre_run(self, *args, **kwargs): + result = super_pre_run(self, *args, **kwargs) + + if hasattr(self, "control_model"): + self.control_model.forward = MethodType( + KolorsControlNet_forward, self.control_model) + return result + + control_net.pre_run = MethodType( + KolorsControlLora_pre_run, control_net) + + def KolorsControlLora_copy(self, *args, **kwargs): + c = super_copy(self, *args, **kwargs) + c.pre_run = MethodType( + KolorsControlLora_pre_run, c) + return c + + control_net.copy = MethodType(KolorsControlLora_copy, control_net) + + elif isinstance(control_net, comfy.controlnet.ControlNet): + model_label_emb = model.model.diffusion_model.label_emb + control_net.control_model.label_emb = model_label_emb + control_net.control_model_wrapped.model.label_emb = model_label_emb + super_forward = ControlNet.forward + + def KolorsControlNet_forward(self, x, hint, timesteps, context, **kwargs): + with torch.cuda.amp.autocast(enabled=True): + context = model.model.diffusion_model.encoder_hid_proj(context) + return super_forward(self, x, hint, timesteps, context, **kwargs) + + control_net.control_model.forward = MethodType( + KolorsControlNet_forward, control_net.control_model) + + else: + raise NotImplementedError(f"Type {control_net} not supported for KolorsControlNetPatch") + + return control_net diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/kolors/text_encode.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/kolors/text_encode.py new file mode 100644 index 0000000000000000000000000000000000000000..b52e1bb250a73cc8b42b0fcb10b1246944699993 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/kolors/text_encode.py @@ -0,0 +1,105 @@ +import re +import random +import gc +import comfy.model_management as mm +from nodes import ConditioningConcat, ConditioningZeroOut, ConditioningSetTimestepRange, ConditioningCombine + +def chatglm3_text_encode(chatglm3_model, prompt, clean_gpu=False): + device = mm.get_torch_device() + offload_device = mm.unet_offload_device() + if clean_gpu: + mm.unload_all_models() + mm.soft_empty_cache() + # Function to randomly select an option from the brackets + + def choose_random_option(match): + options = match.group(1).split('|') + return random.choice(options) + + prompt = re.sub(r'\{([^{}]*)\}', choose_random_option, prompt) + + if "|" in prompt: + prompt = prompt.split("|") + + if prompt is not None and isinstance(prompt, str): + batch_size = 1 + elif prompt is not None and isinstance(prompt, list): + batch_size = len(prompt) + + # Define tokenizers and text encoders + tokenizer = chatglm3_model['tokenizer'] + text_encoder = chatglm3_model['text_encoder'] + text_encoder.to(device) + text_inputs = tokenizer( + prompt, + padding="max_length", + max_length=256, + truncation=True, + return_tensors="pt", + ).to(device) + + output = text_encoder( + input_ids=text_inputs['input_ids'], + attention_mask=text_inputs['attention_mask'], + position_ids=text_inputs['position_ids'], + output_hidden_states=True) + + # [batch_size, 77, 4096] + prompt_embeds = output.hidden_states[-2].permute(1, 0, 2).clone() + text_proj = output.hidden_states[-1][-1, :, :].clone() # [batch_size, 4096] + bs_embed, seq_len, _ = prompt_embeds.shape + prompt_embeds = prompt_embeds.repeat(1, 1, 1) + prompt_embeds = prompt_embeds.view(bs_embed, seq_len, -1) + + bs_embed = text_proj.shape[0] + text_proj = text_proj.repeat(1, 1).view(bs_embed, -1) + text_encoder.to(offload_device) + if clean_gpu: + mm.soft_empty_cache() + gc.collect() + return [[prompt_embeds, {"pooled_output": text_proj},]] + +def chatglm3_adv_text_encode(chatglm3_model, text, clean_gpu=False): + time_start = 0 + time_end = 1 + match = re.search(r'TIMESTEP.*$', text) + if match: + timestep = match.group() + timestep = timestep.split(' ') + timestep = timestep[0] + text = text.replace(timestep, '') + value = timestep.split(':') + if len(value) >= 3: + time_start = float(value[1]) + time_end = float(value[2]) + elif len(value) == 2: + time_start = float(value[1]) + time_end = 1 + elif len(value) == 1: + time_start = 0.1 + time_end = 1 + + + pass3 = [x.strip() for x in text.split("BREAK")] + pass3 = [x for x in pass3 if x != ''] + + if len(pass3) == 0: + pass3 = [''] + + conditioning = None + + for text in pass3: + cond = chatglm3_text_encode(chatglm3_model, text, clean_gpu) + if conditioning is not None: + conditioning = ConditioningConcat().concat(conditioning, cond)[0] + else: + conditioning = cond + + # setTimeStepRange + if time_start > 0 or time_end < 1: + conditioning_2, = ConditioningSetTimestepRange().set_range(conditioning, 0, time_start) + conditioning_1, = ConditioningZeroOut().zero_out(conditioning) + conditioning_1, = ConditioningSetTimestepRange().set_range(conditioning_1, time_start, time_end) + conditioning, = ConditioningCombine().combine(conditioning_1, conditioning_2) + + return conditioning \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/layer_diffuse/__init__.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/layer_diffuse/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..871daf0ca7f9c051adab179d8712409bdd6b75df --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/layer_diffuse/__init__.py @@ -0,0 +1,219 @@ +#credit to huchenlei for this module +#from https://github.com/huchenlei/ComfyUI-layerdiffuse +import torch +import comfy.model_management +import comfy.lora +import copy +from typing import Optional +from enum import Enum +from comfy.utils import load_torch_file +from comfy.conds import CONDRegular +from comfy_extras.nodes_compositing import JoinImageWithAlpha +try: + from .model import ModelPatcher, TransparentVAEDecoder, calculate_weight_adjust_channel +except: + ModelPatcher, TransparentVAEDecoder, calculate_weight_adjust_channel = None, None, None +from .attension_sharing import AttentionSharingPatcher +from ...config import LAYER_DIFFUSION, LAYER_DIFFUSION_DIR, LAYER_DIFFUSION_VAE +from ...libs.utils import to_lora_patch_dict, get_local_filepath, get_sd_version + +load_layer_model_state_dict = load_torch_file +class LayerMethod(Enum): + FG_ONLY_ATTN = "Attention Injection" + FG_ONLY_CONV = "Conv Injection" + FG_TO_BLEND = "Foreground" + FG_BLEND_TO_BG = "Foreground to Background" + BG_TO_BLEND = "Background" + BG_BLEND_TO_FG = "Background to Foreground" + EVERYTHING = "Everything" + +class LayerDiffuse: + + def __init__(self) -> None: + self.vae_transparent_decoder = None + self.frames = 1 + + def get_layer_diffusion_method(self, method, has_blend_latent): + method = LayerMethod(method) + if method == LayerMethod.BG_TO_BLEND and has_blend_latent: + method = LayerMethod.BG_BLEND_TO_FG + elif method == LayerMethod.FG_TO_BLEND and has_blend_latent: + method = LayerMethod.FG_BLEND_TO_BG + return method + + def apply_layer_c_concat(self, cond, uncond, c_concat): + def write_c_concat(cond): + new_cond = [] + for t in cond: + n = [t[0], t[1].copy()] + if "model_conds" not in n[1]: + n[1]["model_conds"] = {} + n[1]["model_conds"]["c_concat"] = CONDRegular(c_concat) + new_cond.append(n) + return new_cond + + return (write_c_concat(cond), write_c_concat(uncond)) + + def apply_layer_diffusion(self, model, method, weight, samples, blend_samples, positive, negative, image=None, additional_cond=(None, None, None)): + control_img: Optional[torch.TensorType] = None + sd_version = get_sd_version(model) + model_url = LAYER_DIFFUSION[method.value][sd_version]["model_url"] + + if image is not None: + image = image.movedim(-1, 1) + + try: + if hasattr(comfy.lora, "calculate_weight"): + comfy.lora.calculate_weight = calculate_weight_adjust_channel(comfy.lora.calculate_weight) + else: + ModelPatcher.calculate_weight = calculate_weight_adjust_channel(ModelPatcher.calculate_weight) + except: + pass + + if method in [LayerMethod.FG_ONLY_CONV, LayerMethod.FG_ONLY_ATTN] and sd_version == 'sd1': + self.frames = 1 + elif method in [LayerMethod.BG_TO_BLEND, LayerMethod.FG_TO_BLEND, LayerMethod.BG_BLEND_TO_FG, LayerMethod.FG_BLEND_TO_BG] and sd_version == 'sd1': + self.frames = 2 + batch_size, _, height, width = samples['samples'].shape + if batch_size % 2 != 0: + raise Exception(f"The batch size should be a multiple of 2. 批次大小需为2的倍数") + control_img = image + elif method == LayerMethod.EVERYTHING and sd_version == 'sd1': + batch_size, _, height, width = samples['samples'].shape + self.frames = 3 + if batch_size % 3 != 0: + raise Exception(f"The batch size should be a multiple of 3. 批次大小需为3的倍数") + if model_url is None: + raise Exception(f"{method.value} is not supported for {sd_version} model") + + model_path = get_local_filepath(model_url, LAYER_DIFFUSION_DIR) + layer_lora_state_dict = load_layer_model_state_dict(model_path) + work_model = model.clone() + if sd_version == 'sd1': + patcher = AttentionSharingPatcher( + work_model, self.frames, use_control=control_img is not None + ) + patcher.load_state_dict(layer_lora_state_dict, strict=True) + if control_img is not None: + patcher.set_control(control_img) + else: + layer_lora_patch_dict = to_lora_patch_dict(layer_lora_state_dict) + work_model.add_patches(layer_lora_patch_dict, weight) + + # cond_contact + if method in [LayerMethod.FG_ONLY_ATTN, LayerMethod.FG_ONLY_CONV]: + samp_model = work_model + elif sd_version == 'sdxl': + if method in [LayerMethod.BG_TO_BLEND, LayerMethod.FG_TO_BLEND]: + c_concat = model.model.latent_format.process_in(samples["samples"]) + else: + c_concat = model.model.latent_format.process_in(torch.cat([samples["samples"], blend_samples["samples"]], dim=1)) + samp_model, positive, negative = (work_model,) + self.apply_layer_c_concat(positive, negative, c_concat) + elif sd_version == 'sd1': + if method in [LayerMethod.BG_TO_BLEND, LayerMethod.BG_BLEND_TO_FG]: + additional_cond = (additional_cond[0], None) + elif method in [LayerMethod.FG_TO_BLEND, LayerMethod.FG_BLEND_TO_BG]: + additional_cond = (additional_cond[1], None) + + work_model.model_options.setdefault("transformer_options", {}) + work_model.model_options["transformer_options"]["cond_overwrite"] = [ + cond[0][0] if cond is not None else None + for cond in additional_cond + ] + samp_model = work_model + + return samp_model, positive, negative + + def join_image_with_alpha(self, image, alpha): + out = image.movedim(-1, 1) + if out.shape[1] == 3: # RGB + out = torch.cat([out, torch.ones_like(out[:, :1, :, :])], dim=1) + for i in range(out.shape[0]): + out[i, 3, :, :] = alpha + return out.movedim(1, -1) + + def image_to_alpha(self, image, latent): + pixel = image.movedim(-1, 1) # [B, H, W, C] => [B, C, H, W] + decoded = [] + sub_batch_size = 16 + for start_idx in range(0, latent.shape[0], sub_batch_size): + decoded.append( + self.vae_transparent_decoder.decode_pixel( + pixel[start_idx: start_idx + sub_batch_size], + latent[start_idx: start_idx + sub_batch_size], + ) + ) + pixel_with_alpha = torch.cat(decoded, dim=0) + # [B, C, H, W] => [B, H, W, C] + pixel_with_alpha = pixel_with_alpha.movedim(1, -1) + image = pixel_with_alpha[..., 1:] + alpha = pixel_with_alpha[..., 0] + + alpha = 1.0 - alpha + try: + new_images, = JoinImageWithAlpha().execute(image, alpha) + except: + new_images, = JoinImageWithAlpha().join_image_with_alpha(image, alpha) + return new_images, alpha + + def make_3d_mask(self, mask): + if len(mask.shape) == 4: + return mask.squeeze(0) + + elif len(mask.shape) == 2: + return mask.unsqueeze(0) + + return mask + + def masks_to_list(self, masks): + if masks is None: + empty_mask = torch.zeros((64, 64), dtype=torch.float32, device="cpu") + return ([empty_mask],) + + res = [] + + for mask in masks: + res.append(mask) + + return [self.make_3d_mask(x) for x in res] + + def layer_diffusion_decode(self, layer_diffusion_method, latent, blend_samples, samp_images, model): + alpha = [] + if layer_diffusion_method is not None: + sd_version = get_sd_version(model) + if sd_version not in ['sdxl', 'sd1']: + raise Exception(f"Only SDXL and SD1.5 model supported for Layer Diffusion") + method = self.get_layer_diffusion_method(layer_diffusion_method, blend_samples is not None) + sd15_allow = True if sd_version == 'sd1' and method in [LayerMethod.FG_ONLY_ATTN, LayerMethod.EVERYTHING, LayerMethod.BG_TO_BLEND, LayerMethod.BG_BLEND_TO_FG] else False + sdxl_allow = True if sd_version == 'sdxl' and method in [LayerMethod.FG_ONLY_CONV, LayerMethod.FG_ONLY_ATTN, LayerMethod.BG_BLEND_TO_FG] else False + if sdxl_allow or sd15_allow: + if self.vae_transparent_decoder is None: + model_url = LAYER_DIFFUSION_VAE['decode'][sd_version]["model_url"] + if model_url is None: + raise Exception(f"{method.value} is not supported for {sd_version} model") + decoder_file = get_local_filepath(model_url, LAYER_DIFFUSION_DIR) + self.vae_transparent_decoder = TransparentVAEDecoder( + load_torch_file(decoder_file), + device=comfy.model_management.get_torch_device(), + dtype=(torch.float16 if comfy.model_management.should_use_fp16() else torch.float32), + ) + if method in [LayerMethod.EVERYTHING, LayerMethod.BG_BLEND_TO_FG, LayerMethod.BG_TO_BLEND]: + new_images = [] + sliced_samples = copy.copy({"samples": latent}) + for index in range(len(samp_images)): + if index % self.frames == 0: + img = samp_images[index::self.frames] + alpha_images, _alpha = self.image_to_alpha(img, sliced_samples["samples"][index::self.frames]) + alpha.append(self.make_3d_mask(_alpha[0])) + new_images.append(alpha_images[0]) + else: + new_images.append(samp_images[index]) + else: + new_images, alpha = self.image_to_alpha(samp_images, latent) + else: + new_images = samp_images + else: + new_images = samp_images + + + return (new_images, samp_images, alpha) \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/layer_diffuse/__pycache__/__init__.cpython-312.pyc b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/layer_diffuse/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4241883e0fb9c06e0548aeee946f5121bf7d966d Binary files /dev/null and b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/layer_diffuse/__pycache__/__init__.cpython-312.pyc differ diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/layer_diffuse/__pycache__/attension_sharing.cpython-312.pyc b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/layer_diffuse/__pycache__/attension_sharing.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8f4252576964311bc996b3babb8830baea9e3e57 Binary files /dev/null and b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/layer_diffuse/__pycache__/attension_sharing.cpython-312.pyc differ diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/layer_diffuse/__pycache__/model.cpython-312.pyc b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/layer_diffuse/__pycache__/model.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..677dd4b26249ae53059b6628beaea9f8bd0aa69b Binary files /dev/null and b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/layer_diffuse/__pycache__/model.cpython-312.pyc differ diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/layer_diffuse/attension_sharing.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/layer_diffuse/attension_sharing.py new file mode 100644 index 0000000000000000000000000000000000000000..89904eecfc2354823428eac5782747997b260bc7 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/layer_diffuse/attension_sharing.py @@ -0,0 +1,359 @@ +# Currently only sd15 + +import functools +import torch +import einops + +from comfy import model_management, utils +from comfy.ldm.modules.attention import optimized_attention + +module_mapping_sd15 = { + 0: "input_blocks.1.1.transformer_blocks.0.attn1", + 1: "input_blocks.1.1.transformer_blocks.0.attn2", + 2: "input_blocks.2.1.transformer_blocks.0.attn1", + 3: "input_blocks.2.1.transformer_blocks.0.attn2", + 4: "input_blocks.4.1.transformer_blocks.0.attn1", + 5: "input_blocks.4.1.transformer_blocks.0.attn2", + 6: "input_blocks.5.1.transformer_blocks.0.attn1", + 7: "input_blocks.5.1.transformer_blocks.0.attn2", + 8: "input_blocks.7.1.transformer_blocks.0.attn1", + 9: "input_blocks.7.1.transformer_blocks.0.attn2", + 10: "input_blocks.8.1.transformer_blocks.0.attn1", + 11: "input_blocks.8.1.transformer_blocks.0.attn2", + 12: "output_blocks.3.1.transformer_blocks.0.attn1", + 13: "output_blocks.3.1.transformer_blocks.0.attn2", + 14: "output_blocks.4.1.transformer_blocks.0.attn1", + 15: "output_blocks.4.1.transformer_blocks.0.attn2", + 16: "output_blocks.5.1.transformer_blocks.0.attn1", + 17: "output_blocks.5.1.transformer_blocks.0.attn2", + 18: "output_blocks.6.1.transformer_blocks.0.attn1", + 19: "output_blocks.6.1.transformer_blocks.0.attn2", + 20: "output_blocks.7.1.transformer_blocks.0.attn1", + 21: "output_blocks.7.1.transformer_blocks.0.attn2", + 22: "output_blocks.8.1.transformer_blocks.0.attn1", + 23: "output_blocks.8.1.transformer_blocks.0.attn2", + 24: "output_blocks.9.1.transformer_blocks.0.attn1", + 25: "output_blocks.9.1.transformer_blocks.0.attn2", + 26: "output_blocks.10.1.transformer_blocks.0.attn1", + 27: "output_blocks.10.1.transformer_blocks.0.attn2", + 28: "output_blocks.11.1.transformer_blocks.0.attn1", + 29: "output_blocks.11.1.transformer_blocks.0.attn2", + 30: "middle_block.1.transformer_blocks.0.attn1", + 31: "middle_block.1.transformer_blocks.0.attn2", +} + + +def compute_cond_mark(cond_or_uncond, sigmas): + cond_or_uncond_size = int(sigmas.shape[0]) + + cond_mark = [] + for cx in cond_or_uncond: + cond_mark += [cx] * cond_or_uncond_size + + cond_mark = torch.Tensor(cond_mark).to(sigmas) + return cond_mark + + +class LoRALinearLayer(torch.nn.Module): + def __init__(self, in_features: int, out_features: int, rank: int = 256, org=None): + super().__init__() + self.down = torch.nn.Linear(in_features, rank, bias=False) + self.up = torch.nn.Linear(rank, out_features, bias=False) + self.org = [org] + + def forward(self, h): + org_weight = self.org[0].weight.to(h) + org_bias = self.org[0].bias.to(h) if self.org[0].bias is not None else None + down_weight = self.down.weight + up_weight = self.up.weight + final_weight = org_weight + torch.mm(up_weight, down_weight) + return torch.nn.functional.linear(h, final_weight, org_bias) + + +class AttentionSharingUnit(torch.nn.Module): + # `transformer_options` passed to the most recent BasicTransformerBlock.forward + # call. + transformer_options: dict = {} + + def __init__(self, module, frames=2, use_control=True, rank=256): + super().__init__() + + self.heads = module.heads + self.frames = frames + self.original_module = [module] + q_in_channels, q_out_channels = ( + module.to_q.in_features, + module.to_q.out_features, + ) + k_in_channels, k_out_channels = ( + module.to_k.in_features, + module.to_k.out_features, + ) + v_in_channels, v_out_channels = ( + module.to_v.in_features, + module.to_v.out_features, + ) + o_in_channels, o_out_channels = ( + module.to_out[0].in_features, + module.to_out[0].out_features, + ) + + hidden_size = k_out_channels + + self.to_q_lora = [ + LoRALinearLayer(q_in_channels, q_out_channels, rank, module.to_q) + for _ in range(self.frames) + ] + self.to_k_lora = [ + LoRALinearLayer(k_in_channels, k_out_channels, rank, module.to_k) + for _ in range(self.frames) + ] + self.to_v_lora = [ + LoRALinearLayer(v_in_channels, v_out_channels, rank, module.to_v) + for _ in range(self.frames) + ] + self.to_out_lora = [ + LoRALinearLayer(o_in_channels, o_out_channels, rank, module.to_out[0]) + for _ in range(self.frames) + ] + + self.to_q_lora = torch.nn.ModuleList(self.to_q_lora) + self.to_k_lora = torch.nn.ModuleList(self.to_k_lora) + self.to_v_lora = torch.nn.ModuleList(self.to_v_lora) + self.to_out_lora = torch.nn.ModuleList(self.to_out_lora) + + self.temporal_i = torch.nn.Linear( + in_features=hidden_size, out_features=hidden_size + ) + self.temporal_n = torch.nn.LayerNorm( + hidden_size, elementwise_affine=True, eps=1e-6 + ) + self.temporal_q = torch.nn.Linear( + in_features=hidden_size, out_features=hidden_size + ) + self.temporal_k = torch.nn.Linear( + in_features=hidden_size, out_features=hidden_size + ) + self.temporal_v = torch.nn.Linear( + in_features=hidden_size, out_features=hidden_size + ) + self.temporal_o = torch.nn.Linear( + in_features=hidden_size, out_features=hidden_size + ) + + self.control_convs = None + + if use_control: + self.control_convs = [ + torch.nn.Sequential( + torch.nn.Conv2d(256, 256, kernel_size=3, padding=1, stride=1), + torch.nn.SiLU(), + torch.nn.Conv2d(256, hidden_size, kernel_size=1), + ) + for _ in range(self.frames) + ] + self.control_convs = torch.nn.ModuleList(self.control_convs) + + self.control_signals = None + + def forward(self, h, context=None, value=None): + transformer_options = self.transformer_options + + modified_hidden_states = einops.rearrange( + h, "(b f) d c -> f b d c", f=self.frames + ) + + if self.control_convs is not None: + context_dim = int(modified_hidden_states.shape[2]) + control_outs = [] + for f in range(self.frames): + control_signal = self.control_signals[context_dim].to( + modified_hidden_states + ) + control = self.control_convs[f](control_signal) + control = einops.rearrange(control, "b c h w -> b (h w) c") + control_outs.append(control) + control_outs = torch.stack(control_outs, dim=0) + modified_hidden_states = modified_hidden_states + control_outs.to( + modified_hidden_states + ) + + if context is None: + framed_context = modified_hidden_states + else: + framed_context = einops.rearrange( + context, "(b f) d c -> f b d c", f=self.frames + ) + + framed_cond_mark = einops.rearrange( + compute_cond_mark( + transformer_options["cond_or_uncond"], + transformer_options["sigmas"], + ), + "(b f) -> f b", + f=self.frames, + ).to(modified_hidden_states) + + attn_outs = [] + for f in range(self.frames): + fcf = framed_context[f] + + if context is not None: + cond_overwrite = transformer_options.get("cond_overwrite", []) + if len(cond_overwrite) > f: + cond_overwrite = cond_overwrite[f] + else: + cond_overwrite = None + if cond_overwrite is not None: + cond_mark = framed_cond_mark[f][:, None, None] + fcf = cond_overwrite.to(fcf) * (1.0 - cond_mark) + fcf * cond_mark + + q = self.to_q_lora[f](modified_hidden_states[f]) + k = self.to_k_lora[f](fcf) + v = self.to_v_lora[f](fcf) + o = optimized_attention(q, k, v, self.heads) + o = self.to_out_lora[f](o) + o = self.original_module[0].to_out[1](o) + attn_outs.append(o) + + attn_outs = torch.stack(attn_outs, dim=0) + modified_hidden_states = modified_hidden_states + attn_outs.to( + modified_hidden_states + ) + modified_hidden_states = einops.rearrange( + modified_hidden_states, "f b d c -> (b f) d c", f=self.frames + ) + + x = modified_hidden_states + x = self.temporal_n(x) + x = self.temporal_i(x) + d = x.shape[1] + + x = einops.rearrange(x, "(b f) d c -> (b d) f c", f=self.frames) + + q = self.temporal_q(x) + k = self.temporal_k(x) + v = self.temporal_v(x) + + x = optimized_attention(q, k, v, self.heads) + x = self.temporal_o(x) + x = einops.rearrange(x, "(b d) f c -> (b f) d c", d=d) + + modified_hidden_states = modified_hidden_states + x + + return modified_hidden_states - h + + @classmethod + def hijack_transformer_block(cls): + def register_get_transformer_options(func): + @functools.wraps(func) + def forward(self, x, context=None, transformer_options={}): + cls.transformer_options = transformer_options + return func(self, x, context, transformer_options) + + return forward + + from comfy.ldm.modules.attention import BasicTransformerBlock + + BasicTransformerBlock.forward = register_get_transformer_options( + BasicTransformerBlock.forward + ) + + +AttentionSharingUnit.hijack_transformer_block() + + +class AdditionalAttentionCondsEncoder(torch.nn.Module): + def __init__(self): + super().__init__() + + self.blocks_0 = torch.nn.Sequential( + torch.nn.Conv2d(3, 32, kernel_size=3, padding=1, stride=1), + torch.nn.SiLU(), + torch.nn.Conv2d(32, 32, kernel_size=3, padding=1, stride=1), + torch.nn.SiLU(), + torch.nn.Conv2d(32, 64, kernel_size=3, padding=1, stride=2), + torch.nn.SiLU(), + torch.nn.Conv2d(64, 64, kernel_size=3, padding=1, stride=1), + torch.nn.SiLU(), + torch.nn.Conv2d(64, 128, kernel_size=3, padding=1, stride=2), + torch.nn.SiLU(), + torch.nn.Conv2d(128, 128, kernel_size=3, padding=1, stride=1), + torch.nn.SiLU(), + torch.nn.Conv2d(128, 256, kernel_size=3, padding=1, stride=2), + torch.nn.SiLU(), + torch.nn.Conv2d(256, 256, kernel_size=3, padding=1, stride=1), + torch.nn.SiLU(), + ) # 64*64*256 + + self.blocks_1 = torch.nn.Sequential( + torch.nn.Conv2d(256, 256, kernel_size=3, padding=1, stride=2), + torch.nn.SiLU(), + torch.nn.Conv2d(256, 256, kernel_size=3, padding=1, stride=1), + torch.nn.SiLU(), + ) # 32*32*256 + + self.blocks_2 = torch.nn.Sequential( + torch.nn.Conv2d(256, 256, kernel_size=3, padding=1, stride=2), + torch.nn.SiLU(), + torch.nn.Conv2d(256, 256, kernel_size=3, padding=1, stride=1), + torch.nn.SiLU(), + ) # 16*16*256 + + self.blocks_3 = torch.nn.Sequential( + torch.nn.Conv2d(256, 256, kernel_size=3, padding=1, stride=2), + torch.nn.SiLU(), + torch.nn.Conv2d(256, 256, kernel_size=3, padding=1, stride=1), + torch.nn.SiLU(), + ) # 8*8*256 + + self.blks = [self.blocks_0, self.blocks_1, self.blocks_2, self.blocks_3] + + def __call__(self, h): + results = {} + for b in self.blks: + h = b(h) + results[int(h.shape[2]) * int(h.shape[3])] = h + return results + + +class HookerLayers(torch.nn.Module): + def __init__(self, layer_list): + super().__init__() + self.layers = torch.nn.ModuleList(layer_list) + + +class AttentionSharingPatcher(torch.nn.Module): + def __init__(self, unet, frames=2, use_control=True, rank=256): + super().__init__() + model_management.unload_model_clones(unet) + + units = [] + for i in range(32): + real_key = module_mapping_sd15[i] + attn_module = utils.get_attr(unet.model.diffusion_model, real_key) + u = AttentionSharingUnit( + attn_module, frames=frames, use_control=use_control, rank=rank + ) + units.append(u) + unet.add_object_patch("diffusion_model." + real_key, u) + + self.hookers = HookerLayers(units) + + if use_control: + self.kwargs_encoder = AdditionalAttentionCondsEncoder() + else: + self.kwargs_encoder = None + + self.dtype = torch.float32 + if model_management.should_use_fp16(model_management.get_torch_device()): + self.dtype = torch.float16 + self.hookers.half() + return + + def set_control(self, img): + img = img.cpu().float() * 2.0 - 1.0 + signals = self.kwargs_encoder(img) + for m in self.hookers.layers: + m.control_signals = signals + return diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/layer_diffuse/model.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/layer_diffuse/model.py new file mode 100644 index 0000000000000000000000000000000000000000..14c9d1a69bf9cf06783cfda50409ab33b8fab4e1 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/modules/layer_diffuse/model.py @@ -0,0 +1,390 @@ +import torch.nn as nn +import torch +import cv2 +import numpy as np +import comfy.model_management + +from comfy.model_patcher import ModelPatcher +from tqdm import tqdm +from typing import Optional, Tuple +from ...libs.utils import install_package +from packaging import version + +try: + install_package("diffusers", "0.27.2", True, "0.25.0") + + from diffusers.configuration_utils import ConfigMixin, register_to_config + from diffusers.models.modeling_utils import ModelMixin + from diffusers import __version__ + if __version__: + if version.parse(__version__) < version.parse("0.26.0"): + from diffusers.models.unet_2d_blocks import UNetMidBlock2D, get_down_block, get_up_block + else: + from diffusers.models.unets.unet_2d_blocks import UNetMidBlock2D, get_down_block, get_up_block + + import functools + + def zero_module(module): + """ + Zero out the parameters of a module and return it. + """ + for p in module.parameters(): + p.detach().zero_() + return module + + + class LatentTransparencyOffsetEncoder(torch.nn.Module): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.blocks = torch.nn.Sequential( + torch.nn.Conv2d(4, 32, kernel_size=3, padding=1, stride=1), + nn.SiLU(), + torch.nn.Conv2d(32, 32, kernel_size=3, padding=1, stride=1), + nn.SiLU(), + torch.nn.Conv2d(32, 64, kernel_size=3, padding=1, stride=2), + nn.SiLU(), + torch.nn.Conv2d(64, 64, kernel_size=3, padding=1, stride=1), + nn.SiLU(), + torch.nn.Conv2d(64, 128, kernel_size=3, padding=1, stride=2), + nn.SiLU(), + torch.nn.Conv2d(128, 128, kernel_size=3, padding=1, stride=1), + nn.SiLU(), + torch.nn.Conv2d(128, 256, kernel_size=3, padding=1, stride=2), + nn.SiLU(), + torch.nn.Conv2d(256, 256, kernel_size=3, padding=1, stride=1), + nn.SiLU(), + zero_module(torch.nn.Conv2d(256, 4, kernel_size=3, padding=1, stride=1)), + ) + + def __call__(self, x): + return self.blocks(x) + + + # 1024 * 1024 * 3 -> 16 * 16 * 512 -> 1024 * 1024 * 3 + class UNet1024(ModelMixin, ConfigMixin): + @register_to_config + def __init__( + self, + in_channels: int = 3, + out_channels: int = 3, + down_block_types: Tuple[str] = ( + "DownBlock2D", + "DownBlock2D", + "DownBlock2D", + "DownBlock2D", + "AttnDownBlock2D", + "AttnDownBlock2D", + "AttnDownBlock2D", + ), + up_block_types: Tuple[str] = ( + "AttnUpBlock2D", + "AttnUpBlock2D", + "AttnUpBlock2D", + "UpBlock2D", + "UpBlock2D", + "UpBlock2D", + "UpBlock2D", + ), + block_out_channels: Tuple[int] = (32, 32, 64, 128, 256, 512, 512), + layers_per_block: int = 2, + mid_block_scale_factor: float = 1, + downsample_padding: int = 1, + downsample_type: str = "conv", + upsample_type: str = "conv", + dropout: float = 0.0, + act_fn: str = "silu", + attention_head_dim: Optional[int] = 8, + norm_num_groups: int = 4, + norm_eps: float = 1e-5, + ): + super().__init__() + + # input + self.conv_in = nn.Conv2d( + in_channels, block_out_channels[0], kernel_size=3, padding=(1, 1) + ) + self.latent_conv_in = zero_module( + nn.Conv2d(4, block_out_channels[2], kernel_size=1) + ) + + self.down_blocks = nn.ModuleList([]) + self.mid_block = None + self.up_blocks = nn.ModuleList([]) + + # down + output_channel = block_out_channels[0] + for i, down_block_type in enumerate(down_block_types): + input_channel = output_channel + output_channel = block_out_channels[i] + is_final_block = i == len(block_out_channels) - 1 + + down_block = get_down_block( + down_block_type, + num_layers=layers_per_block, + in_channels=input_channel, + out_channels=output_channel, + temb_channels=None, + add_downsample=not is_final_block, + resnet_eps=norm_eps, + resnet_act_fn=act_fn, + resnet_groups=norm_num_groups, + attention_head_dim=( + attention_head_dim + if attention_head_dim is not None + else output_channel + ), + downsample_padding=downsample_padding, + resnet_time_scale_shift="default", + downsample_type=downsample_type, + dropout=dropout, + ) + self.down_blocks.append(down_block) + + # mid + self.mid_block = UNetMidBlock2D( + in_channels=block_out_channels[-1], + temb_channels=None, + dropout=dropout, + resnet_eps=norm_eps, + resnet_act_fn=act_fn, + output_scale_factor=mid_block_scale_factor, + resnet_time_scale_shift="default", + attention_head_dim=( + attention_head_dim + if attention_head_dim is not None + else block_out_channels[-1] + ), + resnet_groups=norm_num_groups, + attn_groups=None, + add_attention=True, + ) + + # up + reversed_block_out_channels = list(reversed(block_out_channels)) + output_channel = reversed_block_out_channels[0] + for i, up_block_type in enumerate(up_block_types): + prev_output_channel = output_channel + output_channel = reversed_block_out_channels[i] + input_channel = reversed_block_out_channels[ + min(i + 1, len(block_out_channels) - 1) + ] + + is_final_block = i == len(block_out_channels) - 1 + + up_block = get_up_block( + up_block_type, + num_layers=layers_per_block + 1, + in_channels=input_channel, + out_channels=output_channel, + prev_output_channel=prev_output_channel, + temb_channels=None, + add_upsample=not is_final_block, + resnet_eps=norm_eps, + resnet_act_fn=act_fn, + resnet_groups=norm_num_groups, + attention_head_dim=( + attention_head_dim + if attention_head_dim is not None + else output_channel + ), + resnet_time_scale_shift="default", + upsample_type=upsample_type, + dropout=dropout, + ) + self.up_blocks.append(up_block) + prev_output_channel = output_channel + + # out + self.conv_norm_out = nn.GroupNorm( + num_channels=block_out_channels[0], num_groups=norm_num_groups, eps=norm_eps + ) + self.conv_act = nn.SiLU() + self.conv_out = nn.Conv2d( + block_out_channels[0], out_channels, kernel_size=3, padding=1 + ) + + def forward(self, x, latent): + sample_latent = self.latent_conv_in(latent) + sample = self.conv_in(x) + emb = None + + down_block_res_samples = (sample,) + for i, downsample_block in enumerate(self.down_blocks): + if i == 3: + sample = sample + sample_latent + + sample, res_samples = downsample_block(hidden_states=sample, temb=emb) + down_block_res_samples += res_samples + + sample = self.mid_block(sample, emb) + + for upsample_block in self.up_blocks: + res_samples = down_block_res_samples[-len(upsample_block.resnets):] + down_block_res_samples = down_block_res_samples[ + : -len(upsample_block.resnets) + ] + sample = upsample_block(sample, res_samples, emb) + + sample = self.conv_norm_out(sample) + sample = self.conv_act(sample) + sample = self.conv_out(sample) + return sample + + + def checkerboard(shape): + return np.indices(shape).sum(axis=0) % 2 + + + def fill_checkerboard_bg(y: torch.Tensor) -> torch.Tensor: + alpha = y[..., :1] + fg = y[..., 1:] + B, H, W, C = fg.shape + cb = checkerboard(shape=(H // 64, W // 64)) + cb = cv2.resize(cb, (W, H), interpolation=cv2.INTER_NEAREST) + cb = (0.5 + (cb - 0.5) * 0.1)[None, ..., None] + cb = torch.from_numpy(cb).to(fg) + vis = fg * alpha + cb * (1 - alpha) + return vis + + + class TransparentVAEDecoder: + def __init__(self, sd, device, dtype): + self.load_device = device + self.dtype = dtype + + model = UNet1024(in_channels=3, out_channels=4) + model.load_state_dict(sd, strict=True) + model.to(self.load_device, dtype=self.dtype) + model.eval() + self.model = model + + @torch.no_grad() + def estimate_single_pass(self, pixel, latent): + y = self.model(pixel, latent) + return y + + @torch.no_grad() + def estimate_augmented(self, pixel, latent): + args = [ + [False, 0], + [False, 1], + [False, 2], + [False, 3], + [True, 0], + [True, 1], + [True, 2], + [True, 3], + ] + + result = [] + + for flip, rok in tqdm(args): + feed_pixel = pixel.clone() + feed_latent = latent.clone() + + if flip: + feed_pixel = torch.flip(feed_pixel, dims=(3,)) + feed_latent = torch.flip(feed_latent, dims=(3,)) + + feed_pixel = torch.rot90(feed_pixel, k=rok, dims=(2, 3)) + feed_latent = torch.rot90(feed_latent, k=rok, dims=(2, 3)) + + eps = self.estimate_single_pass(feed_pixel, feed_latent).clip(0, 1) + eps = torch.rot90(eps, k=-rok, dims=(2, 3)) + + if flip: + eps = torch.flip(eps, dims=(3,)) + + result += [eps] + + result = torch.stack(result, dim=0) + median = torch.median(result, dim=0).values + return median + + @torch.no_grad() + def decode_pixel( + self, pixel: torch.TensorType, latent: torch.TensorType + ) -> torch.TensorType: + # pixel.shape = [B, C=3, H, W] + assert pixel.shape[1] == 3 + pixel_device = pixel.device + pixel_dtype = pixel.dtype + + pixel = pixel.to(device=self.load_device, dtype=self.dtype) + latent = latent.to(device=self.load_device, dtype=self.dtype) + # y.shape = [B, C=4, H, W] + y = self.estimate_augmented(pixel, latent) + y = y.clip(0, 1) + assert y.shape[1] == 4 + # Restore image to original device of input image. + return y.to(pixel_device, dtype=pixel_dtype) + + + def calculate_weight_adjust_channel(func): + """Patches ComfyUI's LoRA weight application to accept multi-channel inputs.""" + @functools.wraps(func) + def calculate_weight( + patches, weight: torch.Tensor, key: str, intermediate_type=torch.float32 + ) -> torch.Tensor: + weight = func(patches, weight, key, intermediate_type) + + for p in patches: + alpha = p[0] + v = p[1] + + # The recursion call should be handled in the main func call. + if isinstance(v, list): + continue + + if len(v) == 1: + patch_type = "diff" + elif len(v) == 2: + patch_type = v[0] + v = v[1] + + if patch_type == "diff": + w1 = v[0] + if all( + ( + alpha != 0.0, + w1.shape != weight.shape, + w1.ndim == weight.ndim == 4, + ) + ): + new_shape = [max(n, m) for n, m in zip(weight.shape, w1.shape)] + print( + f"Merged with {key} channel changed from {weight.shape} to {new_shape}" + ) + new_diff = alpha * comfy.model_management.cast_to_device( + w1, weight.device, weight.dtype + ) + new_weight = torch.zeros(size=new_shape).to(weight) + new_weight[ + : weight.shape[0], + : weight.shape[1], + : weight.shape[2], + : weight.shape[3], + ] = weight + new_weight[ + : new_diff.shape[0], + : new_diff.shape[1], + : new_diff.shape[2], + : new_diff.shape[3], + ] += new_diff + new_weight = new_weight.contiguous().clone() + weight = new_weight + return weight + + return calculate_weight + + +except ImportError: + ModelMixin = None + ConfigMixin = None + TransparentVAEDecoder = None + calculate_weight_adjust_channel = None + print("\33[33mModule 'diffusers' load failed. If you don't have it installed, do it:\033[0m") + print("\33[33mpip install diffusers\033[0m") + + + diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/__pycache__/adapter.cpython-312.pyc b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/__pycache__/adapter.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7fa0d3a4a17c052752dfa2ce153528486f7a35b9 Binary files /dev/null and b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/__pycache__/adapter.cpython-312.pyc differ diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/__pycache__/api.cpython-312.pyc b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/__pycache__/api.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2861e9ff23985709d449f37ed4963a7e59401bac Binary files /dev/null and b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/__pycache__/api.cpython-312.pyc differ diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/__pycache__/deprecated.cpython-312.pyc b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/__pycache__/deprecated.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b28829dd12b2e4057b7e47c3edf84d3116d72172 Binary files /dev/null and b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/__pycache__/deprecated.cpython-312.pyc differ diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/__pycache__/fix.cpython-312.pyc b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/__pycache__/fix.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ee8c2bfc3105ca62d3aaa67fc0e9c0470e37405b Binary files /dev/null and b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/__pycache__/fix.cpython-312.pyc differ diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/__pycache__/image.cpython-312.pyc b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/__pycache__/image.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f5f3adeeea75537b71a7b3981a4619d2bce7ec7a Binary files /dev/null and b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/__pycache__/image.cpython-312.pyc differ diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/__pycache__/inpaint.cpython-312.pyc b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/__pycache__/inpaint.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dd6aea897d69c0790356b9727b0b22b37ea550bb Binary files /dev/null and b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/__pycache__/inpaint.cpython-312.pyc differ diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/__pycache__/loaders.cpython-312.pyc b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/__pycache__/loaders.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9345b5147e21c1a8e8947ac28af620cd601746e7 Binary files /dev/null and b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/__pycache__/loaders.cpython-312.pyc differ diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/__pycache__/logic.cpython-312.pyc b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/__pycache__/logic.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0a4f286ea1b98d9759458a82f22112b42dc5f3a4 Binary files /dev/null and b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/__pycache__/logic.cpython-312.pyc differ diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/__pycache__/pipe.cpython-312.pyc b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/__pycache__/pipe.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d4890e1c4f2fa7d956a849e733a6a9bb5d366066 Binary files /dev/null and b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/__pycache__/pipe.cpython-312.pyc differ diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/__pycache__/preSampling.cpython-312.pyc b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/__pycache__/preSampling.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bd19c2332c1593f101a42d69aa06f9134c90d6f0 Binary files /dev/null and b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/__pycache__/preSampling.cpython-312.pyc differ diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/__pycache__/prompt.cpython-312.pyc b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/__pycache__/prompt.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9809977cc8cfc0a9d4041ab48dcedb9bbac9e802 Binary files /dev/null and b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/__pycache__/prompt.cpython-312.pyc differ diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/__pycache__/samplers.cpython-312.pyc b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/__pycache__/samplers.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e4de2e8d30e36cd20ef5c8ec94f8ee712a081255 Binary files /dev/null and b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/__pycache__/samplers.cpython-312.pyc differ diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/__pycache__/seed.cpython-312.pyc b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/__pycache__/seed.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f28779932ea06c44650ed5c865a322d60719e05b Binary files /dev/null and b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/__pycache__/seed.cpython-312.pyc differ diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/__pycache__/util.cpython-312.pyc b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/__pycache__/util.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..889856bb9f983e6debb1e413bb229d449473ad5a Binary files /dev/null and b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/__pycache__/util.cpython-312.pyc differ diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/__pycache__/xyplot.cpython-312.pyc b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/__pycache__/xyplot.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ef0e473839a57b683ffbd1801c7cb2cf54837dd7 Binary files /dev/null and b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/__pycache__/xyplot.cpython-312.pyc differ diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/adapter.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/adapter.py new file mode 100644 index 0000000000000000000000000000000000000000..8f28c2a9fc49980ab7d7c069f52d28090899b8eb --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/adapter.py @@ -0,0 +1,1358 @@ +import re +import torch +import folder_paths +import comfy.utils, comfy.sample, comfy.samplers, comfy.controlnet, comfy.model_base, comfy.model_management, comfy.sampler_helpers, comfy.supported_models +from comfy_extras.nodes_compositing import JoinImageWithAlpha +from comfy.clip_vision import load as load_clip_vision + +from nodes import NODE_CLASS_MAPPINGS as ALL_NODE_CLASS_MAPPINGS +from ..config import * + +from ..libs.log import log_node_info, log_node_warn +from ..libs.utils import get_local_filepath, get_sd_version +from ..libs.wildcards import process_with_loras +from ..libs.controlnet import easyControlnet +from ..libs.conditioning import prompt_to_cond +from ..libs import cache as backend_cache + +from .. import easyCache + +class applyLoraPrompt: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "model": ("MODEL",), + "clip": ("CLIP",), + "positive": ("STRING", {"default": "", "forceInput": True}), + }, + "optional": { + "negative": ("STRING", {"default": "", "forceInput": True}), + } + } + + RETURN_TYPES = ("MODEL", "CLIP", "STRING", "STRING") + RETURN_NAMES = ("model", "clip", "positive", "negative") + CATEGORY = "EasyUse/Adapter" + FUNCTION = "apply" + + def apply(self, model, clip, positive, negative=None): + model, clip, positive, _, _, _ = process_with_loras(positive, model, clip, 'Positive', easyCache=easyCache) + if negative is not None: + model, clip, negative, _, _, _ = process_with_loras(negative, model, clip, 'Negative', easyCache=easyCache) + + return (model, clip, positive, negative if negative is not None else "") + +class applyLoraStack: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "lora_stack": ("LORA_STACK",), + "model": ("MODEL",), + }, + "optional": { + "optional_clip": ("CLIP",), + } + } + + RETURN_TYPES = ("MODEL", "CLIP") + RETURN_NAMES = ("model", "clip") + CATEGORY = "EasyUse/Adapter" + FUNCTION = "apply" + + def apply(self, lora_stack, model, optional_clip=None): + clip = None + if lora_stack is not None and len(lora_stack) > 0: + for lora in lora_stack: + lora = {"lora_name": lora[0], "model": model, "clip": optional_clip, "model_strength": lora[1], + "clip_strength": lora[2]} + model, clip = easyCache.load_lora(lora, model, optional_clip, use_cache=False) + return (model, optional_clip if clip is None else clip) + +class applyControlnetStack: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "controlnet_stack": ("CONTROL_NET_STACK",), + "pipe": ("PIPE_LINE",), + }, + "optional": { + } + } + + RETURN_TYPES = ("PIPE_LINE",) + RETURN_NAMES = ("pipe",) + CATEGORY = "EasyUse/Adapter" + FUNCTION = "apply" + + def apply(self, controlnet_stack, pipe): + + positive = pipe['positive'] + negative = pipe['negative'] + model = pipe['model'] + vae = pipe['vae'] + + if controlnet_stack is not None and len(controlnet_stack) >0: + for controlnet in controlnet_stack: + positive, negative = easyControlnet().apply(controlnet[0], controlnet[5], positive, negative, controlnet[1], start_percent=controlnet[2], end_percent=controlnet[3], control_net=None, scale_soft_weights=controlnet[4], mask=None, easyCache=easyCache, use_cache=False, model=model, vae=vae) + + new_pipe = { + **pipe, + "positive": positive, + "negetive": negative, + } + del pipe + + return (new_pipe,) + +# 风格对齐 +from ..libs.styleAlign import styleAlignBatch, SHARE_NORM_OPTIONS, SHARE_ATTN_OPTIONS +class styleAlignedBatchAlign: + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "model": ("MODEL",), + "share_norm": (SHARE_NORM_OPTIONS,), + "share_attn": (SHARE_ATTN_OPTIONS,), + "scale": ("FLOAT", {"default": 1, "min": 0, "max": 1.0, "step": 0.1}), + } + } + + RETURN_TYPES = ("MODEL",) + FUNCTION = "align" + CATEGORY = "EasyUse/Adapter" + + def align(self, model, share_norm, share_attn, scale): + return (styleAlignBatch(model, share_norm, share_attn, scale),) + +# 光照对齐 +from ..modules.ic_light import ICLight, VAEEncodeArgMax +class icLightApply: + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "mode": (list(IC_LIGHT_MODELS.keys()),), + "model": ("MODEL",), + "image": ("IMAGE",), + "vae": ("VAE",), + "lighting": (['None', 'Left Light', 'Right Light', 'Top Light', 'Bottom Light', 'Circle Light'],{"default": "None"}), + "source": (['Use Background Image', 'Use Flipped Background Image', 'Left Light', 'Right Light', 'Top Light', 'Bottom Light', 'Ambient'],{"default": "Use Background Image"}), + "remove_bg": ("BOOLEAN", {"default": True}), + }, + } + + RETURN_TYPES = ("MODEL", "IMAGE") + RETURN_NAMES = ("model", "lighting_image") + FUNCTION = "apply" + CATEGORY = "EasyUse/Adapter" + + def batch(self, image1, image2): + if image1.shape[1:] != image2.shape[1:]: + image2 = comfy.utils.common_upscale(image2.movedim(-1, 1), image1.shape[2], image1.shape[1], "bilinear", + "center").movedim(1, -1) + s = torch.cat((image1, image2), dim=0) + return s + + def removebg(self, image): + if "easy imageRemBg" not in ALL_NODE_CLASS_MAPPINGS: + raise Exception("Please re-install ComfyUI-Easy-Use") + cls = ALL_NODE_CLASS_MAPPINGS['easy imageRemBg'] + results = cls().remove('RMBG-1.4', image, 'Hide', 'ComfyUI') + if "result" in results: + image, _ = results['result'] + return image + + def apply(self, mode, model, image, vae, lighting, source, remove_bg): + model_type = get_sd_version(model) + if model_type == 'sdxl': + raise Exception("IC Light model is not supported for SDXL now") + + batch_size, height, width, channel = image.shape + if channel == 3: + # remove bg + if mode == 'Foreground' or batch_size == 1: + if remove_bg: + image = self.removebg(image) + else: + mask = torch.full((1, height, width), 1.0, dtype=torch.float32, device="cpu") + try: + image, = JoinImageWithAlpha().execute(image, mask) + except: + image, = JoinImageWithAlpha().join_image_with_alpha(image, mask) + + iclight = ICLight() + if mode == 'Foreground': + lighting_image = iclight.generate_lighting_image(image, lighting) + else: + lighting_image = iclight.generate_source_image(image, source) + if source not in ['Use Background Image', 'Use Flipped Background Image']: + _, height, width, _ = lighting_image.shape + mask = torch.full((1, height, width), 1.0, dtype=torch.float32, device="cpu") + try: + lighting_image, = JoinImageWithAlpha().execute(lighting_image, mask) + except: + lighting_image, = JoinImageWithAlpha().join_image_with_alpha(lighting_image, mask) + if batch_size < 2: + image = self.batch(image, lighting_image) + else: + original_image = [img.unsqueeze(0) for img in image] + original_image = self.removebg(original_image[0]) + image = self.batch(original_image, lighting_image) + + latent, = VAEEncodeArgMax().encode(vae, image) + key = 'iclight_' + mode + '_' + model_type + model_path = get_local_filepath(IC_LIGHT_MODELS[mode]['sd1']["model_url"], + os.path.join(folder_paths.models_dir, "unet")) + ic_model = None + if key in backend_cache.cache: + log_node_info("easy icLightApply", f"Using icLightModel {mode+'_'+model_type} Cached") + _, ic_model = backend_cache.cache[key][1] + m, _ = iclight.apply(model_path, model, latent, ic_model) + else: + m, ic_model = iclight.apply(model_path, model, latent, ic_model) + backend_cache.update_cache(key, 'iclight', (False, ic_model)) + return (m, lighting_image) + + +def insightface_loader(provider, name='buffalo_l'): + try: + from insightface.app import FaceAnalysis + except ImportError as e: + raise Exception(e) + path = os.path.join(folder_paths.models_dir, "insightface") + model = FaceAnalysis(name=name, root=path, providers=[provider + 'ExecutionProvider', ]) + model.prepare(ctx_id=0, det_size=(640, 640)) + return model + +# Apply Ipadapter +class ipadapter: + + def __init__(self): + self.normal_presets = [ + 'LIGHT - SD1.5 only (low strength)', + 'STANDARD (medium strength)', + 'VIT-G (medium strength)', + 'PLUS (high strength)', + 'PLUS (kolors genernal)', + 'REGULAR - FLUX and SD3.5 only (high strength)', + 'PLUS FACE (portraits)', + 'FULL FACE - SD1.5 only (portraits stronger)', + 'COMPOSITION' + ] + self.faceid_presets = [ + 'FACEID', + 'FACEID PLUS - SD1.5 only', + "FACEID PLUS KOLORS", + 'FACEID PLUS V2', + 'FACEID PORTRAIT (style transfer)', + 'FACEID PORTRAIT UNNORM - SDXL only (strong)' + ] + self.weight_types = ["linear", "ease in", "ease out", 'ease in-out', 'reverse in-out', 'weak input', 'weak output', 'weak middle', 'strong middle', 'style transfer', 'composition', 'strong style transfer', 'style and composition', 'style transfer precise'] + self.presets = self.normal_presets + self.faceid_presets + + + def error(self): + raise Exception(f"[ERROR] To use ipadapterApply, you need to install 'ComfyUI_IPAdapter_plus'") + + def get_clipvision_file(self, preset, node_name): + preset = preset.lower() + clipvision_list = folder_paths.get_filename_list("clip_vision") + if preset.startswith("regular"): + # pattern = 'sigclip.vision.patch14.384' + pattern = 'siglip.so400m.patch14.384' + elif preset.startswith("plus (kolors") or preset.startswith("faceid plus kolors"): + pattern = 'Vit.Large.patch14.336.(bin|safetensors)$' + elif preset.startswith("vit-g"): + pattern = '(ViT.bigG.14.*39B.b160k|ipadapter.*sdxl|sdxl.*model.(bin|safetensors))' + else: + pattern = '(ViT.H.14.*s32B.b79K|ipadapter.*sd15|sd1.?5.*model.(bin|safetensors))' + clipvision_files = [e for e in clipvision_list if re.search(pattern, e, re.IGNORECASE)] + clipvision_name = clipvision_files[0] if len(clipvision_files)>0 else None + clipvision_file = folder_paths.get_full_path("clip_vision", clipvision_name) if clipvision_name else None + # if clipvision_name is not None: + # log_node_info(node_name, f"Using {clipvision_name}") + return clipvision_file, clipvision_name + + def get_ipadapter_file(self, preset, model_type, node_name): + preset = preset.lower() + ipadapter_list = folder_paths.get_filename_list("ipadapter") + is_insightface = False + lora_pattern = None + is_sdxl = model_type == 'sdxl' + is_flux = model_type == 'flux' + + if preset.startswith("light"): + if is_sdxl: + raise Exception("light model is not supported for SDXL") + pattern = 'sd15.light.v11.(safetensors|bin)$' + # if light model v11 is not found, try with the old version + if not [e for e in ipadapter_list if re.search(pattern, e, re.IGNORECASE)]: + pattern = 'sd15.light.(safetensors|bin)$' + elif preset.startswith("standard"): + if is_sdxl: + pattern = 'ip.adapter.sdxl.vit.h.(safetensors|bin)$' + else: + pattern = 'ip.adapter.sd15.(safetensors|bin)$' + elif preset.startswith("vit-g"): + if is_sdxl: + pattern = 'ip.adapter.sdxl.(safetensors|bin)$' + else: + pattern = 'sd15.vit.g.(safetensors|bin)$' + elif preset.startswith("regular"): + if is_flux: + pattern = 'ip.adapter.flux.1.dev.(safetensors|bin)$' + else: + pattern = 'ip.adapter.sd35.(safetensors|bin)$' + elif preset.startswith("plus (high"): + if is_sdxl: + pattern = 'plus.sdxl.vit.h.(safetensors|bin)$' + else: + pattern = 'ip.adapter.plus.sd15.(safetensors|bin)$' + elif preset.startswith("plus (kolors"): + if is_sdxl: + pattern = 'plus.gener(nal|al).(safetensors|bin)$' + else: + raise Exception("kolors model is not supported for SD15") + elif preset.startswith("plus face"): + if is_sdxl: + pattern = 'plus.face.sdxl.vit.h.(safetensors|bin)$' + else: + pattern = 'plus.face.sd15.(safetensors|bin)$' + elif preset.startswith("full"): + if is_sdxl: + raise Exception("full face model is not supported for SDXL") + pattern = 'full.face.sd15.(safetensors|bin)$' + elif preset.startswith("composition"): + if is_sdxl: + pattern = 'plus.composition.sdxl.(safetensors|bin)$' + else: + pattern = 'plus.composition.sd15.(safetensors|bin)$' + elif preset.startswith("faceid portrait ("): + if is_sdxl: + pattern = 'portrait.sdxl.(safetensors|bin)$' + else: + pattern = 'portrait.v11.sd15.(safetensors|bin)$' + # if v11 is not found, try with the old version + if not [e for e in ipadapter_list if re.search(pattern, e, re.IGNORECASE)]: + pattern = 'portrait.sd15.(safetensors|bin)$' + is_insightface = True + elif preset.startswith("faceid portrait unnorm"): + if is_sdxl: + pattern = r'portrait.sdxl.unnorm.(safetensors|bin)$' + else: + raise Exception("portrait unnorm model is not supported for SD1.5") + is_insightface = True + elif preset == "faceid": + if is_sdxl: + pattern = 'faceid.sdxl.(safetensors|bin)$' + lora_pattern = 'faceid.sdxl.lora.safetensors$' + else: + pattern = 'faceid.sd15.(safetensors|bin)$' + lora_pattern = 'faceid.sd15.lora.safetensors$' + is_insightface = True + elif preset.startswith("faceid plus kolors"): + if is_sdxl: + pattern = '(kolors.ip.adapter.faceid.plus|ipa.faceid.plus).(safetensors|bin)$' + else: + raise Exception("faceid plus kolors model is not supported for SD1.5") + is_insightface = True + elif preset.startswith("faceid plus -"): + if is_sdxl: + raise Exception("faceid plus model is not supported for SDXL") + pattern = 'faceid.plus.sd15.(safetensors|bin)$' + lora_pattern = 'faceid.plus.sd15.lora.safetensors$' + is_insightface = True + elif preset.startswith("faceid plus v2"): + if is_sdxl: + pattern = 'faceid.plusv2.sdxl.(safetensors|bin)$' + lora_pattern = 'faceid.plusv2.sdxl.lora.safetensors$' + else: + pattern = 'faceid.plusv2.sd15.(safetensors|bin)$' + lora_pattern = 'faceid.plusv2.sd15.lora.safetensors$' + is_insightface = True + else: + raise Exception(f"invalid type '{preset}'") + + ipadapter_files = [e for e in ipadapter_list if re.search(pattern, e, re.IGNORECASE)] + ipadapter_name = ipadapter_files[0] if len(ipadapter_files)>0 else None + ipadapter_file = folder_paths.get_full_path("ipadapter", ipadapter_name) if ipadapter_name else None + # if ipadapter_name is not None: + # log_node_info(node_name, f"Using {ipadapter_name}") + + return ipadapter_file, ipadapter_name, is_insightface, lora_pattern + + def get_lora_pattern(self, file): + basename = os.path.basename(file) + lora_pattern = None + if re.search(r'faceid.sdxl.(safetensors|bin)$', basename, re.IGNORECASE): + lora_pattern = 'faceid.sdxl.lora.safetensors$' + elif re.search(r'faceid.sd15.(safetensors|bin)$', basename, re.IGNORECASE): + lora_pattern = 'faceid.sd15.lora.safetensors$' + elif re.search(r'faceid.plus.sd15.(safetensors|bin)$', basename, re.IGNORECASE): + lora_pattern = 'faceid.plus.sd15.lora.safetensors$' + elif re.search(r'faceid.plusv2.sdxl.(safetensors|bin)$', basename, re.IGNORECASE): + lora_pattern = 'faceid.plusv2.sdxl.lora.safetensors$' + elif re.search(r'faceid.plusv2.sd15.(safetensors|bin)$', basename, re.IGNORECASE): + lora_pattern = 'faceid.plusv2.sd15.lora.safetensors$' + + return lora_pattern + + def get_lora_file(self, preset, pattern, model_type, model, model_strength, clip_strength, clip=None): + lora_list = folder_paths.get_filename_list("loras") + lora_files = [e for e in lora_list if re.search(pattern, e, re.IGNORECASE)] + lora_name = lora_files[0] if lora_files else None + if lora_name: + return easyCache.load_lora({"model": model, "clip": clip, "lora_name": lora_name, "model_strength":model_strength, "clip_strength":clip_strength},) + else: + if "lora_url" in IPADAPTER_MODELS[preset][model_type]: + lora_name = get_local_filepath(IPADAPTER_MODELS[preset][model_type]["lora_url"], os.path.join(folder_paths.models_dir, "loras")) + return easyCache.load_lora({"model": model, "clip": clip, "lora_name": lora_name, "model_strength":model_strength, "clip_strength":clip_strength},) + return (model, clip) + + def ipadapter_model_loader(self, file): + model = comfy.utils.load_torch_file(file, safe_load=False) + + if file.lower().endswith(".safetensors"): + st_model = {"image_proj": {}, "ip_adapter": {}} + for key in model.keys(): + if key.startswith("image_proj."): + st_model["image_proj"][key.replace("image_proj.", "")] = model[key] + elif key.startswith("ip_adapter."): + st_model["ip_adapter"][key.replace("ip_adapter.", "")] = model[key] + model = st_model + del st_model + + model_keys = model.keys() + if "adapter_modules" in model_keys: + model["ip_adapter"] = model["adapter_modules"] + model["faceidplusv2"] = True + del model['adapter_modules'] + + if not "ip_adapter" in model_keys or not model["ip_adapter"]: + raise Exception("invalid IPAdapter model {}".format(file)) + + if 'plusv2' in file.lower(): + model["faceidplusv2"] = True + + if 'unnorm' in file.lower(): + model["portraitunnorm"] = True + + return model + + def load_model(self, model, preset, lora_model_strength, provider="CPU", clip_vision=None, optional_ipadapter=None, cache_mode='none', node_name='easy ipadapterApply'): + pipeline = {"clipvision": {'file': None, 'model': None}, "ipadapter": {'file': None, 'model': None}, + "insightface": {'provider': None, 'model': None}} + ipadapter, insightface, is_insightface, lora_pattern = None, None, None, None + if optional_ipadapter is not None: + pipeline = optional_ipadapter + if not clip_vision: + clip_vision = pipeline['clipvision']['model'] + ipadapter = pipeline['ipadapter']['model'] + if 'insightface' in pipeline: + insightface = pipeline['insightface']['model'] + lora_pattern = self.get_lora_pattern(pipeline['ipadapter']['file']) + + # 1. Load the clipvision model + if not clip_vision: + clipvision_file, clipvision_name = self.get_clipvision_file(preset, node_name) + if clipvision_file is None: + if preset.lower().startswith("regular"): + # model_url = IPADAPTER_CLIPVISION_MODELS["sigclip_vision_patch14_384"]["model_url"] + # clipvision_file = get_local_filepath(model_url, IPADAPTER_DIR, "sigclip_vision_patch14_384.bin") + from huggingface_hub import snapshot_download + import shutil + CLIP_PATH = os.path.join(folder_paths.models_dir, "clip_vision", "google--siglip-so400m-patch14-384") + print("CLIP_VISION not found locally. Downloading google/siglip-so400m-patch14-384...") + try: + snapshot_download( + repo_id="google/siglip-so400m-patch14-384", + local_dir=os.path.join(folder_paths.models_dir, "clip_vision", + "cache--google--siglip-so400m-patch14-384"), + local_dir_use_symlinks=False, + resume_download=True + ) + shutil.move(os.path.join(folder_paths.models_dir, "clip_vision", + "cache--google--siglip-so400m-patch14-384"), CLIP_PATH) + print(f"CLIP_VISION has been downloaded to {CLIP_PATH}") + except Exception as e: + print(f"Error downloading CLIP model: {e}") + raise + clipvision_file = CLIP_PATH + elif preset.lower().startswith("plus (kolors"): + model_url = IPADAPTER_CLIPVISION_MODELS["clip-vit-large-patch14-336"]["model_url"] + clipvision_file = get_local_filepath(model_url, IPADAPTER_DIR, "clip-vit-large-patch14-336.bin") + else: + model_url = IPADAPTER_CLIPVISION_MODELS["clip-vit-h-14-laion2B-s32B-b79K"]["model_url"] + clipvision_file = get_local_filepath(model_url, IPADAPTER_DIR, "clip-vit-h-14-laion2B-s32B-b79K.safetensors") + clipvision_name = os.path.basename(model_url) + if clipvision_file == pipeline['clipvision']['file']: + clip_vision = pipeline['clipvision']['model'] + elif cache_mode in ["all", "clip_vision only"] and clipvision_name in backend_cache.cache: + log_node_info("easy ipadapterApply", f"Using ClipVisonModel {clipvision_name} Cached") + _, clip_vision = backend_cache.cache[clipvision_name][1] + else: + if preset.lower().startswith("regular"): + from transformers import SiglipVisionModel, AutoProcessor + image_encoder_path = os.path.dirname(clipvision_file) + image_encoder = SiglipVisionModel.from_pretrained(image_encoder_path) + clip_image_processor = AutoProcessor.from_pretrained(image_encoder_path) + clip_vision = { + 'image_encoder': image_encoder, + 'clip_image_processor': clip_image_processor + } + else: + clip_vision = load_clip_vision(clipvision_file) + log_node_info("easy ipadapterApply", f"Using ClipVisonModel {clipvision_name}") + if cache_mode in ["all", "clip_vision only"]: + backend_cache.update_cache(clipvision_name, 'clip_vision', (False, clip_vision)) + pipeline['clipvision']['file'] = clipvision_file + pipeline['clipvision']['model'] = clip_vision + # 2. Load the ipadapter model + model_type = get_sd_version(model) + if not ipadapter: + ipadapter_file, ipadapter_name, is_insightface, lora_pattern = self.get_ipadapter_file(preset, model_type, node_name) + if ipadapter_file is None: + model_url = IPADAPTER_MODELS[preset][model_type]["model_url"] + local_file_name = IPADAPTER_MODELS[preset][model_type]['model_file_name'] if "model_file_name" in IPADAPTER_MODELS[preset][model_type] else None + ipadapter_file = get_local_filepath(model_url, IPADAPTER_DIR, local_file_name) + ipadapter_name = os.path.basename(model_url) + if ipadapter_file == pipeline['ipadapter']['file']: + ipadapter = pipeline['ipadapter']['model'] + elif cache_mode in ["all", "ipadapter only"] and ipadapter_name in backend_cache.cache: + log_node_info("easy ipadapterApply", f"Using IpAdapterModel {ipadapter_name} Cached") + _, ipadapter = backend_cache.cache[ipadapter_name][1] + else: + ipadapter = self.ipadapter_model_loader(ipadapter_file) + pipeline['ipadapter']['file'] = ipadapter_file + log_node_info("easy ipadapterApply", f"Using IpAdapterModel {ipadapter_name}") + if cache_mode in ["all", "ipadapter only"]: + backend_cache.update_cache(ipadapter_name, 'ipadapter', (False, ipadapter)) + + pipeline['ipadapter']['model'] = ipadapter + + # 3. Load the lora model if needed + if lora_pattern is not None: + if lora_model_strength > 0: + model, _ = self.get_lora_file(preset, lora_pattern, model_type, model, lora_model_strength, 1) + + # 4. Load the insightface model if needed + if is_insightface: + if not insightface: + icache_key = 'insightface-' + provider + if provider == pipeline['insightface']['provider']: + insightface = pipeline['insightface']['model'] + elif cache_mode in ["all", "insightface only"] and icache_key in backend_cache.cache: + log_node_info("easy ipadapterApply", f"Using InsightFaceModel {icache_key} Cached") + _, insightface = backend_cache.cache[icache_key][1] + else: + insightface = insightface_loader(provider, 'antelopev2' if preset == 'FACEID PLUS KOLORS' else 'buffalo_l') + if cache_mode in ["all", "insightface only"]: + backend_cache.update_cache(icache_key, 'insightface',(False, insightface)) + pipeline['insightface']['provider'] = provider + pipeline['insightface']['model'] = insightface + + return (model, pipeline,) + +class ipadapterApply(ipadapter): + def __init__(self): + super().__init__() + pass + + @classmethod + def INPUT_TYPES(cls): + presets = cls().presets + return { + "required": { + "model": ("MODEL",), + "image": ("IMAGE",), + "preset": (presets,), + "lora_strength": ("FLOAT", {"default": 0.6, "min": 0, "max": 1, "step": 0.01}), + "provider": (["CPU", "CUDA", "ROCM", "DirectML", "OpenVINO", "CoreML"], {"default": "CUDA"}), + "weight": ("FLOAT", {"default": 1.0, "min": -1, "max": 3, "step": 0.05}), + "weight_faceidv2": ("FLOAT", { "default": 1.0, "min": -1, "max": 5.0, "step": 0.05 }), + "start_at": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001}), + "end_at": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.001}), + "cache_mode": (["insightface only", "clip_vision only", "ipadapter only", "all", "none"], {"default": "all"},), + "use_tiled": ("BOOLEAN", {"default": False},), + }, + + "optional": { + "attn_mask": ("MASK",), + "optional_ipadapter": ("IPADAPTER",), + } + } + + RETURN_TYPES = ("MODEL", "IMAGE", "MASK", "IPADAPTER",) + RETURN_NAMES = ("model", "images", "masks", "ipadapter", ) + CATEGORY = "EasyUse/Adapter" + FUNCTION = "apply" + + def apply(self, model, image, preset, lora_strength, provider, weight, weight_faceidv2, start_at, end_at, cache_mode, use_tiled, attn_mask=None, optional_ipadapter=None, weight_kolors=None): + images, masks = image, [None] + model, ipadapter = self.load_model(model, preset, lora_strength, provider, clip_vision=None, optional_ipadapter=optional_ipadapter, cache_mode=cache_mode) + if preset == 'REGULAR - FLUX and SD3.5 only (high strength)': + from ..modules.ipadapter import InstantXFluxIpadapterApply, InstantXSD3IpadapterApply + model_type = get_sd_version(model) + if model_type == 'flux': + model, images = InstantXFluxIpadapterApply().apply_ipadapter(model, ipadapter, image, weight, start_at, end_at, provider) + elif model_type == 'sd3': + model, images = InstantXSD3IpadapterApply().apply_ipadapter(model, ipadapter, image, weight, start_at, end_at, provider) + elif use_tiled and preset not in self.faceid_presets: + if "IPAdapterTiled" not in ALL_NODE_CLASS_MAPPINGS: + self.error() + cls = ALL_NODE_CLASS_MAPPINGS["IPAdapterTiled"] + model, images, masks = cls().apply_tiled(model, ipadapter, image, weight, "linear", start_at, end_at, sharpening=0.0, combine_embeds="concat", image_negative=None, attn_mask=attn_mask, clip_vision=None, embeds_scaling='V only') + else: + if preset in ['FACEID PLUS KOLORS', 'FACEID PLUS V2', 'FACEID PORTRAIT (style transfer)']: + if "IPAdapterAdvanced" not in ALL_NODE_CLASS_MAPPINGS: + self.error() + cls = ALL_NODE_CLASS_MAPPINGS["IPAdapterAdvanced"] + if weight_kolors is None: + weight_kolors = weight + model, images = cls().apply_ipadapter(model, ipadapter, start_at=start_at, end_at=end_at, weight=weight, weight_type="linear", combine_embeds="concat", weight_faceidv2=weight_faceidv2, image=image, image_negative=None, clip_vision=None, attn_mask=attn_mask, insightface=None, embeds_scaling='V only', weight_kolors=weight_kolors) + else: + if "IPAdapter" not in ALL_NODE_CLASS_MAPPINGS: + self.error() + cls = ALL_NODE_CLASS_MAPPINGS["IPAdapter"] + model, images = cls().apply_ipadapter(model, ipadapter, image, weight, start_at, end_at, weight_type='standard', attn_mask=attn_mask) + if images is None: + images = image + return (model, images, masks, ipadapter,) + +class ipadapterApplyAdvanced(ipadapter): + def __init__(self): + super().__init__() + pass + + @classmethod + def INPUT_TYPES(cls): + ipa_cls = cls() + presets = ipa_cls.presets + weight_types = ipa_cls.weight_types + return { + "required": { + "model": ("MODEL",), + "image": ("IMAGE",), + "preset": (presets,), + "lora_strength": ("FLOAT", {"default": 0.6, "min": 0, "max": 1, "step": 0.01}), + "provider": (["CPU", "CUDA", "ROCM", "DirectML", "OpenVINO", "CoreML"], {"default": "CUDA"}), + "weight": ("FLOAT", {"default": 1.0, "min": -1, "max": 3, "step": 0.05}), + "weight_faceidv2": ("FLOAT", {"default": 1.0, "min": -1, "max": 5.0, "step": 0.05 }), + "weight_type": (weight_types,), + "combine_embeds": (["concat", "add", "subtract", "average", "norm average"],), + "start_at": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001}), + "end_at": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.001}), + "embeds_scaling": (['V only', 'K+V', 'K+V w/ C penalty', 'K+mean(V) w/ C penalty'],), + "cache_mode": (["insightface only", "clip_vision only","ipadapter only", "all", "none"], {"default": "all"},), + "use_tiled": ("BOOLEAN", {"default": False},), + "use_batch": ("BOOLEAN", {"default": False},), + "sharpening": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.05}), + }, + + "optional": { + "image_negative": ("IMAGE",), + "attn_mask": ("MASK",), + "clip_vision": ("CLIP_VISION",), + "optional_ipadapter": ("IPADAPTER",), + "layer_weights": ("STRING", {"default": "", "multiline": True, "placeholder": "Mad Scientist Layer Weights"}), + } + } + + RETURN_TYPES = ("MODEL", "IMAGE", "MASK", "IPADAPTER",) + RETURN_NAMES = ("model", "images", "masks", "ipadapter", ) + CATEGORY = "EasyUse/Adapter" + FUNCTION = "apply" + + def apply(self, model, image, preset, lora_strength, provider, weight, weight_faceidv2, weight_type, combine_embeds, start_at, end_at, embeds_scaling, cache_mode, use_tiled, use_batch, sharpening, weight_style=1.0, weight_composition=1.0, image_style=None, image_composition=None, expand_style=False, image_negative=None, clip_vision=None, attn_mask=None, optional_ipadapter=None, layer_weights=None, weight_kolors=None): + images, masks = image, [None] + model, ipadapter = self.load_model(model, preset, lora_strength, provider, clip_vision=clip_vision, optional_ipadapter=optional_ipadapter, cache_mode=cache_mode) + + if weight_kolors is None: + weight_kolors = weight + + if layer_weights: + if "IPAdapterMS" not in ALL_NODE_CLASS_MAPPINGS: + self.error() + cls = ALL_NODE_CLASS_MAPPINGS["IPAdapterAdvanced"] + model, images = cls().apply_ipadapter(model, ipadapter, weight=weight, weight_type=weight_type, start_at=start_at, end_at=end_at, combine_embeds=combine_embeds, weight_faceidv2=weight_faceidv2, image=image, image_negative=image_negative, weight_style=weight_style, weight_composition=weight_composition, image_style=image_style, image_composition=image_composition, expand_style=expand_style, clip_vision=clip_vision, attn_mask=attn_mask, insightface=None, embeds_scaling=embeds_scaling, layer_weights=layer_weights, weight_kolors=weight_kolors) + elif use_tiled: + if use_batch: + if "IPAdapterTiledBatch" not in ALL_NODE_CLASS_MAPPINGS: + self.error() + cls = ALL_NODE_CLASS_MAPPINGS["IPAdapterTiledBatch"] + else: + if "IPAdapterTiled" not in ALL_NODE_CLASS_MAPPINGS: + self.error() + cls = ALL_NODE_CLASS_MAPPINGS["IPAdapterTiled"] + model, images, masks = cls().apply_tiled(model, ipadapter, image=image, weight=weight, weight_type=weight_type, start_at=start_at, end_at=end_at, sharpening=sharpening, combine_embeds=combine_embeds, image_negative=image_negative, attn_mask=attn_mask, clip_vision=clip_vision, embeds_scaling=embeds_scaling) + else: + if use_batch: + if "IPAdapterBatch" not in ALL_NODE_CLASS_MAPPINGS: + self.error() + cls = ALL_NODE_CLASS_MAPPINGS["IPAdapterBatch"] + else: + if "IPAdapterAdvanced" not in ALL_NODE_CLASS_MAPPINGS: + self.error() + cls = ALL_NODE_CLASS_MAPPINGS["IPAdapterAdvanced"] + model, images = cls().apply_ipadapter(model, ipadapter, weight=weight, weight_type=weight_type, start_at=start_at, end_at=end_at, combine_embeds=combine_embeds, weight_faceidv2=weight_faceidv2, image=image, image_negative=image_negative, weight_style=1.0, weight_composition=1.0, image_style=image_style, image_composition=image_composition, expand_style=expand_style, clip_vision=clip_vision, attn_mask=attn_mask, insightface=None, embeds_scaling=embeds_scaling, weight_kolors=weight_kolors) + if images is None: + images = image + return (model, images, masks, ipadapter) + +class ipadapterApplyFaceIDKolors(ipadapterApplyAdvanced): + + @classmethod + def INPUT_TYPES(cls): + ipa_cls = cls() + presets = ipa_cls.presets + weight_types = ipa_cls.weight_types + return { + "required": { + "model": ("MODEL",), + "image": ("IMAGE",), + "preset": (['FACEID PLUS KOLORS'], {"default":"FACEID PLUS KOLORS"}), + "lora_strength": ("FLOAT", {"default": 0.6, "min": 0, "max": 1, "step": 0.01}), + "provider": (["CPU", "CUDA", "ROCM", "DirectML", "OpenVINO", "CoreML"], {"default": "CUDA"}), + "weight": ("FLOAT", {"default": 0.8, "min": -1, "max": 3, "step": 0.05}), + "weight_faceidv2": ("FLOAT", {"default": 1.0, "min": -1, "max": 5.0, "step": 0.05}), + "weight_kolors": ("FLOAT", {"default": 0.8, "min": -1, "max": 5.0, "step": 0.05}), + "weight_type": (weight_types,), + "combine_embeds": (["concat", "add", "subtract", "average", "norm average"],), + "start_at": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001}), + "end_at": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.001}), + "embeds_scaling": (['V only', 'K+V', 'K+V w/ C penalty', 'K+mean(V) w/ C penalty'],), + "cache_mode": (["insightface only", "clip_vision only", "ipadapter only", "all", "none"], {"default": "all"},), + "use_tiled": ("BOOLEAN", {"default": False},), + "use_batch": ("BOOLEAN", {"default": False},), + "sharpening": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.05}), + }, + + "optional": { + "image_negative": ("IMAGE",), + "attn_mask": ("MASK",), + "clip_vision": ("CLIP_VISION",), + "optional_ipadapter": ("IPADAPTER",), + } + } + + +class ipadapterStyleComposition(ipadapter): + def __init__(self): + super().__init__() + pass + + @classmethod + def INPUT_TYPES(cls): + ipa_cls = cls() + normal_presets = ipa_cls.normal_presets + weight_types = ipa_cls.weight_types + return { + "required": { + "model": ("MODEL",), + "image_style": ("IMAGE",), + "preset": (normal_presets,), + "weight_style": ("FLOAT", {"default": 1.0, "min": -1, "max": 5, "step": 0.05}), + "weight_composition": ("FLOAT", {"default": 1.0, "min": -1, "max": 5, "step": 0.05}), + "expand_style": ("BOOLEAN", {"default": False}), + "combine_embeds": (["concat", "add", "subtract", "average", "norm average"], {"default": "average"}), + "start_at": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001}), + "end_at": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.001}), + "embeds_scaling": (['V only', 'K+V', 'K+V w/ C penalty', 'K+mean(V) w/ C penalty'],), + "cache_mode": (["insightface only", "clip_vision only", "ipadapter only", "all", "none"], + {"default": "all"},), + }, + "optional": { + "image_composition": ("IMAGE",), + "image_negative": ("IMAGE",), + "attn_mask": ("MASK",), + "clip_vision": ("CLIP_VISION",), + "optional_ipadapter": ("IPADAPTER",), + } + } + + CATEGORY = "EasyUse/Adapter" + + RETURN_TYPES = ("MODEL", "IPADAPTER",) + RETURN_NAMES = ("model", "ipadapter",) + CATEGORY = "EasyUse/Adapter" + FUNCTION = "apply" + + def apply(self, model, preset, weight_style, weight_composition, expand_style, combine_embeds, start_at, end_at, embeds_scaling, cache_mode, image_style=None , image_composition=None, image_negative=None, clip_vision=None, attn_mask=None, optional_ipadapter=None): + model, ipadapter = self.load_model(model, preset, 0, 'CPU', clip_vision=None, optional_ipadapter=optional_ipadapter, cache_mode=cache_mode) + + if "IPAdapterAdvanced" not in ALL_NODE_CLASS_MAPPINGS: + self.error() + cls = ALL_NODE_CLASS_MAPPINGS["IPAdapterAdvanced"] + + model, image = cls().apply_ipadapter(model, ipadapter, start_at=start_at, end_at=end_at, weight_style=weight_style, weight_composition=weight_composition, weight_type='linear', combine_embeds=combine_embeds, weight_faceidv2=weight_composition, image_style=image_style, image_composition=image_composition, image_negative=image_negative, expand_style=expand_style, clip_vision=clip_vision, attn_mask=attn_mask, insightface=None, embeds_scaling=embeds_scaling) + return (model, ipadapter) + +class ipadapterApplyEncoder(ipadapter): + def __init__(self): + super().__init__() + pass + + @classmethod + def INPUT_TYPES(cls): + ipa_cls = cls() + normal_presets = ipa_cls.normal_presets + max_embeds_num = 4 + inputs = { + "required": { + "model": ("MODEL",), + "clip_vision": ("CLIP_VISION",), + "image1": ("IMAGE",), + "preset": (normal_presets,), + "num_embeds": ("INT", {"default": 2, "min": 1, "max": max_embeds_num}), + }, + "optional": {} + } + + for i in range(1, max_embeds_num + 1): + if i > 1: + inputs["optional"][f"image{i}"] = ("IMAGE",) + for i in range(1, max_embeds_num + 1): + inputs["optional"][f"mask{i}"] = ("MASK",) + inputs["optional"][f"weight{i}"] = ("FLOAT", {"default": 1.0, "min": -1, "max": 3, "step": 0.05}) + inputs["optional"]["combine_method"] = (["concat", "add", "subtract", "average", "norm average", "max", "min"],) + inputs["optional"]["optional_ipadapter"] = ("IPADAPTER",) + inputs["optional"]["pos_embeds"] = ("EMBEDS",) + inputs["optional"]["neg_embeds"] = ("EMBEDS",) + return inputs + + RETURN_TYPES = ("MODEL", "CLIP_VISION","IPADAPTER", "EMBEDS", "EMBEDS", ) + RETURN_NAMES = ("model", "clip_vision","ipadapter", "pos_embed", "neg_embed",) + CATEGORY = "EasyUse/Adapter" + FUNCTION = "apply" + + def batch(self, embeds, method): + if method == 'concat' and len(embeds) == 1: + return (embeds[0],) + + embeds = [embed for embed in embeds if embed is not None] + embeds = torch.cat(embeds, dim=0) + + if method == "add": + embeds = torch.sum(embeds, dim=0).unsqueeze(0) + elif method == "subtract": + embeds = embeds[0] - torch.mean(embeds[1:], dim=0) + embeds = embeds.unsqueeze(0) + elif method == "average": + embeds = torch.mean(embeds, dim=0).unsqueeze(0) + elif method == "norm average": + embeds = torch.mean(embeds / torch.norm(embeds, dim=0, keepdim=True), dim=0).unsqueeze(0) + elif method == "max": + embeds = torch.max(embeds, dim=0).values.unsqueeze(0) + elif method == "min": + embeds = torch.min(embeds, dim=0).values.unsqueeze(0) + + return embeds + + def apply(self, **kwargs): + model = kwargs['model'] + clip_vision = kwargs['clip_vision'] + preset = kwargs['preset'] + if 'optional_ipadapter' in kwargs: + ipadapter = kwargs['optional_ipadapter'] + else: + model, ipadapter = self.load_model(model, preset, 0, 'CPU', clip_vision=clip_vision, optional_ipadapter=None, cache_mode='none') + + if "IPAdapterEncoder" not in ALL_NODE_CLASS_MAPPINGS: + self.error() + encoder_cls = ALL_NODE_CLASS_MAPPINGS["IPAdapterEncoder"] + pos_embeds = kwargs["pos_embeds"] if "pos_embeds" in kwargs else [] + neg_embeds = kwargs["neg_embeds"] if "neg_embeds" in kwargs else [] + for i in range(1, kwargs['num_embeds'] + 1): + if f"image{i}" not in kwargs: + raise Exception(f"image{i} is required") + kwargs[f"mask{i}"] = kwargs[f"mask{i}"] if f"mask{i}" in kwargs else None + kwargs[f"weight{i}"] = kwargs[f"weight{i}"] if f"weight{i}" in kwargs else 1.0 + + pos, neg = encoder_cls().encode(ipadapter, kwargs[f"image{i}"], kwargs[f"weight{i}"], kwargs[f"mask{i}"], clip_vision=clip_vision) + pos_embeds.append(pos) + neg_embeds.append(neg) + + pos_embeds = self.batch(pos_embeds, kwargs['combine_method']) + neg_embeds = self.batch(neg_embeds, kwargs['combine_method']) + + return (model,clip_vision, ipadapter, pos_embeds, neg_embeds) + +class ipadapterApplyEmbeds(ipadapter): + def __init__(self): + super().__init__() + pass + + @classmethod + def INPUT_TYPES(cls): + ipa_cls = cls() + weight_types = ipa_cls.weight_types + return { + "required": { + "model": ("MODEL",), + "clip_vision": ("CLIP_VISION",), + "ipadapter": ("IPADAPTER",), + "pos_embed": ("EMBEDS",), + "weight": ("FLOAT", {"default": 1.0, "min": -1, "max": 3, "step": 0.05}), + "weight_type": (weight_types,), + "start_at": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001}), + "end_at": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.001}), + "embeds_scaling": (['V only', 'K+V', 'K+V w/ C penalty', 'K+mean(V) w/ C penalty'],), + }, + + "optional": { + "neg_embed": ("EMBEDS",), + "attn_mask": ("MASK",), + } + } + + RETURN_TYPES = ("MODEL", "IPADAPTER",) + RETURN_NAMES = ("model", "ipadapter", ) + CATEGORY = "EasyUse/Adapter" + FUNCTION = "apply" + + def apply(self, model, ipadapter, clip_vision, pos_embed, weight, weight_type, start_at, end_at, embeds_scaling, attn_mask=None, neg_embed=None,): + if "IPAdapterEmbeds" not in ALL_NODE_CLASS_MAPPINGS: + self.error() + + cls = ALL_NODE_CLASS_MAPPINGS["IPAdapterEmbeds"] + model, image = cls().apply_ipadapter(model, ipadapter, pos_embed, weight, weight_type, start_at, end_at, neg_embed=neg_embed, attn_mask=attn_mask, clip_vision=clip_vision, embeds_scaling=embeds_scaling) + + return (model, ipadapter) + +class ipadapterApplyRegional(ipadapter): + def __init__(self): + super().__init__() + pass + + @classmethod + def INPUT_TYPES(cls): + ipa_cls = cls() + weight_types = ipa_cls.weight_types + return { + "required": { + "pipe": ("PIPE_LINE",), + "image": ("IMAGE",), + "positive": ("STRING", {"default": "", "placeholder": "positive", "multiline": True}), + "negative": ("STRING", {"default": "", "placeholder": "negative", "multiline": True}), + "image_weight": ("FLOAT", {"default": 1.0, "min": -1.0, "max": 3.0, "step": 0.05}), + "prompt_weight": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.05}), + "weight_type": (weight_types,), + "start_at": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001}), + "end_at": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.001}), + }, + + "optional": { + "mask": ("MASK",), + "optional_ipadapter_params": ("IPADAPTER_PARAMS",), + }, + "hidden": {"prompt": "PROMPT", "my_unique_id": "UNIQUE_ID"} + } + + RETURN_TYPES = ("PIPE_LINE", "IPADAPTER_PARAMS", "CONDITIONING", "CONDITIONING") + RETURN_NAMES = ("pipe", "ipadapter_params", "positive", "negative") + CATEGORY = "EasyUse/Adapter" + FUNCTION = "apply" + + def apply(self, pipe, image, positive, negative, image_weight, prompt_weight, weight_type, start_at, end_at, mask=None, optional_ipadapter_params=None, prompt=None, my_unique_id=None): + model = pipe['model'] + + if positive == '': + positive = pipe['loader_settings']['positive'] + if negative == '': + negative = pipe['loader_settings']['negative'] + + if "clip" not in pipe or not pipe['clip']: + if "chatglm3_model" in pipe: + from ..modules.kolors.text_encode import chatglm3_adv_text_encode + chatglm3_model = pipe['chatglm3_model'] + # text encode + log_node_warn("Positive encoding...") + positive_embeddings_final = chatglm3_adv_text_encode(chatglm3_model, positive, False) + log_node_warn("Negative encoding...") + negative_embeddings_final = chatglm3_adv_text_encode(chatglm3_model, negative, False) + else: + clip = pipe['clip'] + clip_skip = pipe['loader_settings']['clip_skip'] + a1111_prompt_style = pipe['loader_settings']['a1111_prompt_style'] + pipe_lora_stack = pipe['loader_settings']['lora_stack'] + positive_token_normalization = pipe['loader_settings']['positive_token_normalization'] + positive_weight_interpretation = pipe['loader_settings']['positive_weight_interpretation'] + negative_token_normalization = pipe['loader_settings']['negative_token_normalization'] + negative_weight_interpretation = pipe['loader_settings']['negative_weight_interpretation'] + + positive_embeddings_final, positive_wildcard_prompt, model, clip = prompt_to_cond('positive', model, clip, clip_skip, pipe_lora_stack, positive, positive_token_normalization, positive_weight_interpretation, a1111_prompt_style, my_unique_id, prompt, easyCache) + negative_embeddings_final, negative_wildcard_prompt, model, clip = prompt_to_cond('negative', model, clip, clip_skip, pipe_lora_stack, negative, negative_token_normalization, negative_weight_interpretation, a1111_prompt_style, my_unique_id, prompt, easyCache) + + #ipadapter regional + if "IPAdapterRegionalConditioning" not in ALL_NODE_CLASS_MAPPINGS: + self.error() + + cls = ALL_NODE_CLASS_MAPPINGS["IPAdapterRegionalConditioning"] + ipadapter_params, new_positive_embeds, new_negative_embeds = cls().conditioning(image, image_weight, prompt_weight, weight_type, start_at, end_at, mask=mask, positive=positive_embeddings_final, negative=negative_embeddings_final) + + if optional_ipadapter_params is not None: + positive_embeds = pipe['positive'] + new_positive_embeds + negative_embeds = pipe['negative'] + new_negative_embeds + _ipadapter_params = { + "image": optional_ipadapter_params["image"] + ipadapter_params["image"], + "attn_mask": optional_ipadapter_params["attn_mask"] + ipadapter_params["attn_mask"], + "weight": optional_ipadapter_params["weight"] + ipadapter_params["weight"], + "weight_type": optional_ipadapter_params["weight_type"] + ipadapter_params["weight_type"], + "start_at": optional_ipadapter_params["start_at"] + ipadapter_params["start_at"], + "end_at": optional_ipadapter_params["end_at"] + ipadapter_params["end_at"], + } + ipadapter_params = _ipadapter_params + del _ipadapter_params + else: + positive_embeds = new_positive_embeds + negative_embeds = new_negative_embeds + + new_pipe = { + **pipe, + "positive": positive_embeds, + "negative": negative_embeds, + } + + del pipe + + return (new_pipe, ipadapter_params, positive_embeds, negative_embeds) + +class ipadapterApplyFromParams(ipadapter): + def __init__(self): + super().__init__() + pass + + @classmethod + def INPUT_TYPES(cls): + ipa_cls = cls() + normal_presets = ipa_cls.normal_presets + return { + "required": { + "model": ("MODEL",), + "preset": (normal_presets,), + "ipadapter_params": ("IPADAPTER_PARAMS",), + "combine_embeds": (["concat", "add", "subtract", "average", "norm average", "max", "min"],), + "embeds_scaling": (['V only', 'K+V', 'K+V w/ C penalty', 'K+mean(V) w/ C penalty'],), + "cache_mode": (["insightface only", "clip_vision only", "ipadapter only", "all", "none"], + {"default": "insightface only"}), + }, + + "optional": { + "optional_ipadapter": ("IPADAPTER",), + "image_negative": ("IMAGE",), + } + } + + RETURN_TYPES = ("MODEL", "IPADAPTER",) + RETURN_NAMES = ("model", "ipadapter", ) + CATEGORY = "EasyUse/Adapter" + FUNCTION = "apply" + + def apply(self, model, preset, ipadapter_params, combine_embeds, embeds_scaling, cache_mode, optional_ipadapter=None, image_negative=None,): + model, ipadapter = self.load_model(model, preset, 0, 'CPU', clip_vision=None, optional_ipadapter=optional_ipadapter, cache_mode=cache_mode) + if "IPAdapterFromParams" not in ALL_NODE_CLASS_MAPPINGS: + self.error() + cls = ALL_NODE_CLASS_MAPPINGS["IPAdapterFromParams"] + model, image = cls().apply_ipadapter(model, ipadapter, clip_vision=None, combine_embeds=combine_embeds, embeds_scaling=embeds_scaling, image_negative=image_negative, ipadapter_params=ipadapter_params) + + return (model, ipadapter) + +#Apply InstantID +class instantID: + + def error(self): + raise Exception(f"[ERROR] To use instantIDApply, you need to install 'ComfyUI_InstantID'") + + def run(self, pipe, image, instantid_file, insightface, control_net_name, cn_strength, cn_soft_weights, weight, start_at, end_at, noise, image_kps=None, mask=None, control_net=None, positive=None, negative=None, prompt=None, extra_pnginfo=None, my_unique_id=None): + instantid_model, insightface_model, face_embeds = None, None, None + model = pipe['model'] + # Load InstantID + cache_key = 'instantID' + if cache_key in backend_cache.cache: + log_node_info("easy instantIDApply","Using InstantIDModel Cached") + _, instantid_model = backend_cache.cache[cache_key][1] + if "InstantIDModelLoader" in ALL_NODE_CLASS_MAPPINGS: + load_instant_cls = ALL_NODE_CLASS_MAPPINGS["InstantIDModelLoader"] + instantid_model, = load_instant_cls().load_model(instantid_file) + backend_cache.update_cache(cache_key, 'instantid', (False, instantid_model)) + else: + self.error() + icache_key = 'insightface-' + insightface + if icache_key in backend_cache.cache: + log_node_info("easy instantIDApply", f"Using InsightFaceModel {insightface} Cached") + _, insightface_model = backend_cache.cache[icache_key][1] + elif "InstantIDFaceAnalysis" in ALL_NODE_CLASS_MAPPINGS: + load_insightface_cls = ALL_NODE_CLASS_MAPPINGS["InstantIDFaceAnalysis"] + insightface_model, = load_insightface_cls().load_insight_face(insightface) + backend_cache.update_cache(icache_key, 'insightface', (False, insightface_model)) + else: + self.error() + + # Apply InstantID + if "ApplyInstantID" in ALL_NODE_CLASS_MAPPINGS: + instantid_apply = ALL_NODE_CLASS_MAPPINGS['ApplyInstantID'] + if control_net is None: + control_net = easyCache.load_controlnet(control_net_name, cn_soft_weights) + model, positive, negative = instantid_apply().apply_instantid(instantid_model, insightface_model, control_net, image, model, positive, negative, start_at, end_at, weight=weight, ip_weight=None, cn_strength=cn_strength, noise=noise, image_kps=image_kps, mask=mask) + else: + self.error() + + new_pipe = { + "model": model, + "positive": positive, + "negative": negative, + "vae": pipe['vae'], + "clip": pipe['clip'], + + "samples": pipe["samples"], + "images": pipe["images"], + "seed": 0, + + "loader_settings": pipe["loader_settings"] + } + + del pipe + + return (new_pipe, model, positive, negative) + +class instantIDApply(instantID): + + def __init__(self): + super().__init__() + pass + + @classmethod + def INPUT_TYPES(cls): + return { + "required":{ + "pipe": ("PIPE_LINE",), + "image": ("IMAGE",), + "instantid_file": (folder_paths.get_filename_list("instantid"),), + "insightface": (["CPU", "CUDA", "ROCM"],), + "control_net_name": (folder_paths.get_filename_list("controlnet"),), + "cn_strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}), + "cn_soft_weights": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.001},), + "weight": ("FLOAT", {"default": .8, "min": 0.0, "max": 5.0, "step": 0.01, }), + "start_at": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001, }), + "end_at": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.001, }), + "noise": ("FLOAT", {"default": 0.35, "min": 0.0, "max": 1.0, "step": 0.05, }), + }, + "optional": { + "image_kps": ("IMAGE",), + "mask": ("MASK",), + "control_net": ("CONTROL_NET",), + }, + "hidden": { + "prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID" + }, + } + + RETURN_TYPES = ("PIPE_LINE", "MODEL", "CONDITIONING", "CONDITIONING") + RETURN_NAMES = ("pipe", "model", "positive", "negative") + + FUNCTION = "apply" + CATEGORY = "EasyUse/Adapter" + + + def apply(self, pipe, image, instantid_file, insightface, control_net_name, cn_strength, cn_soft_weights, weight, start_at, end_at, noise, image_kps=None, mask=None, control_net=None, prompt=None, extra_pnginfo=None, my_unique_id=None): + positive = pipe['positive'] + negative = pipe['negative'] + return self.run(pipe, image, instantid_file, insightface, control_net_name, cn_strength, cn_soft_weights, weight, start_at, end_at, noise, image_kps, mask, control_net, positive, negative, prompt, extra_pnginfo, my_unique_id) + +#Apply InstantID Advanced +class instantIDApplyAdvanced(instantID): + + def __init__(self): + super().__init__() + pass + + @classmethod + def INPUT_TYPES(cls): + return { + "required":{ + "pipe": ("PIPE_LINE",), + "image": ("IMAGE",), + "instantid_file": (folder_paths.get_filename_list("instantid"),), + "insightface": (["CPU", "CUDA", "ROCM"],), + "control_net_name": (folder_paths.get_filename_list("controlnet"),), + "cn_strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}), + "cn_soft_weights": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.001},), + "weight": ("FLOAT", {"default": .8, "min": 0.0, "max": 5.0, "step": 0.01, }), + "start_at": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001, }), + "end_at": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.001, }), + "noise": ("FLOAT", {"default": 0.35, "min": 0.0, "max": 1.0, "step": 0.05, }), + }, + "optional": { + "image_kps": ("IMAGE",), + "mask": ("MASK",), + "control_net": ("CONTROL_NET",), + "positive": ("CONDITIONING",), + "negative": ("CONDITIONING",), + }, + "hidden": { + "prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID" + }, + } + + RETURN_TYPES = ("PIPE_LINE", "MODEL", "CONDITIONING", "CONDITIONING") + RETURN_NAMES = ("pipe", "model", "positive", "negative") + + FUNCTION = "apply_advanced" + CATEGORY = "EasyUse/Adapter" + + def apply_advanced(self, pipe, image, instantid_file, insightface, control_net_name, cn_strength, cn_soft_weights, weight, start_at, end_at, noise, image_kps=None, mask=None, control_net=None, positive=None, negative=None, prompt=None, extra_pnginfo=None, my_unique_id=None): + + positive = positive if positive is not None else pipe['positive'] + negative = negative if negative is not None else pipe['negative'] + + return self.run(pipe, image, instantid_file, insightface, control_net_name, cn_strength, cn_soft_weights, weight, start_at, end_at, noise, image_kps, mask, control_net, positive, negative, prompt, extra_pnginfo, my_unique_id) + +class applyPulID: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "model": ("MODEL",), + "pulid_file": (folder_paths.get_filename_list("pulid"),), + "insightface": (["CPU", "CUDA", "ROCM"],), + "image": ("IMAGE",), + "method": (["fidelity", "style", "neutral"],), + "weight": ("FLOAT", {"default": 1.0, "min": -1.0, "max": 5.0, "step": 0.05}), + "start_at": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001}), + "end_at": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.001}), + }, + "optional": { + "attn_mask": ("MASK",), + }, + } + + RETURN_TYPES = ("MODEL",) + RETURN_NAMES = ("model",) + + FUNCTION = "run" + CATEGORY = "EasyUse/Adapter" + + def error(self): + raise Exception(f"[ERROR] To use pulIDApply, you need to install 'ComfyUI_PulID'") + + def run(self, model, image, pulid_file, insightface, weight, start_at, end_at, method=None, noise=0.0, fidelity=None, projection=None, attn_mask=None, prompt=None, extra_pnginfo=None, my_unique_id=None): + pulid_model, insightface_model, eva_clip = None, None, None + # Load PulID + cache_key = 'pulID' + if cache_key in backend_cache.cache: + log_node_info("easy pulIDApply","Using InstantIDModel Cached") + _, pulid_model = backend_cache.cache[cache_key][1] + if "PulidModelLoader" in ALL_NODE_CLASS_MAPPINGS: + load_pulid_cls = ALL_NODE_CLASS_MAPPINGS["PulidModelLoader"] + pulid_model, = load_pulid_cls().load_model(pulid_file) + backend_cache.update_cache(cache_key, 'pulid', (False, pulid_model)) + else: + self.error() + # Load Insightface + icache_key = 'insightface-' + insightface + if icache_key in backend_cache.cache: + log_node_info("easy pulIDApply", f"Using InsightFaceModel {insightface} Cached") + _, insightface_model = backend_cache.cache[icache_key][1] + elif "PulidInsightFaceLoader" in ALL_NODE_CLASS_MAPPINGS: + load_insightface_cls = ALL_NODE_CLASS_MAPPINGS["PulidInsightFaceLoader"] + insightface_model, = load_insightface_cls().load_insightface(insightface) + backend_cache.update_cache(icache_key, 'insightface', (False, insightface_model)) + else: + self.error() + # Load Eva clip + ecache_key = 'eva_clip' + if ecache_key in backend_cache.cache: + log_node_info("easy pulIDApply", f"Using EVAClipModel Cached") + _, eva_clip = backend_cache.cache[ecache_key][1] + elif "PulidEvaClipLoader" in ALL_NODE_CLASS_MAPPINGS: + load_evaclip_cls = ALL_NODE_CLASS_MAPPINGS["PulidEvaClipLoader"] + eva_clip, = load_evaclip_cls().load_eva_clip() + backend_cache.update_cache(ecache_key, 'eva_clip', (False, eva_clip)) + else: + self.error() + + # Apply PulID + if method is not None: + if "ApplyPulid" in ALL_NODE_CLASS_MAPPINGS: + cls = ALL_NODE_CLASS_MAPPINGS['ApplyPulid'] + model, = cls().apply_pulid(model, pulid=pulid_model, eva_clip=eva_clip, face_analysis=insightface_model, image=image, weight=weight, method=method, start_at=start_at, end_at=end_at, attn_mask=attn_mask) + else: + self.error() + else: + if "ApplyPulidAdvanced" in ALL_NODE_CLASS_MAPPINGS: + cls = ALL_NODE_CLASS_MAPPINGS['ApplyPulidAdvanced'] + model, = cls().apply_pulid(model, pulid=pulid_model, eva_clip=eva_clip, face_analysis=insightface_model, image=image, weight=weight, projection=projection, fidelity=fidelity, noise=noise, start_at=start_at, end_at=end_at, attn_mask=attn_mask) + else: + self.error() + + return (model,) + +class applyPulIDADV(applyPulID): + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "model": ("MODEL",), + "pulid_file": (folder_paths.get_filename_list("pulid"),), + "insightface": (["CPU", "CUDA", "ROCM"],), + "image": ("IMAGE",), + "weight": ("FLOAT", {"default": 1.0, "min": -1.0, "max": 5.0, "step": 0.05}), + "projection": (["ortho_v2", "ortho", "none"], {"default":"ortho_v2"}), + "fidelity": ("INT", {"default": 8, "min": 0, "max": 32, "step": 1}), + "noise": ("FLOAT", {"default": 0.0, "min": -1.0, "max": 1.0, "step": 0.1}), + "start_at": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001}), + "end_at": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.001}), + }, + "optional": { + "attn_mask": ("MASK",), + }, + } + + + +NODE_CLASS_MAPPINGS = { + "easy loraPromptApply": applyLoraPrompt, + "easy loraStackApply": applyLoraStack, + "easy controlnetStackApply": applyControlnetStack, + "easy ipadapterApply": ipadapterApply, + "easy ipadapterApplyADV": ipadapterApplyAdvanced, + "easy ipadapterApplyFaceIDKolors": ipadapterApplyFaceIDKolors, + "easy ipadapterApplyEncoder": ipadapterApplyEncoder, + "easy ipadapterApplyEmbeds": ipadapterApplyEmbeds, + "easy ipadapterApplyRegional": ipadapterApplyRegional, + "easy ipadapterApplyFromParams": ipadapterApplyFromParams, + "easy ipadapterStyleComposition": ipadapterStyleComposition, + "easy instantIDApply": instantIDApply, + "easy instantIDApplyADV": instantIDApplyAdvanced, + "easy pulIDApply": applyPulID, + "easy pulIDApplyADV": applyPulIDADV, + "easy styleAlignedBatchAlign": styleAlignedBatchAlign, + "easy icLightApply": icLightApply +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "easy loraPromptApply": "Easy Apply LoraPrompt", + "easy loraStackApply": "Easy Apply LoraStack", + "easy controlnetStackApply": "Easy Apply CnetStack", + "easy ipadapterApply": "Easy Apply IPAdapter", + "easy ipadapterApplyADV": "Easy Apply IPAdapter (Advanced)", + "easy ipadapterApplyFaceIDKolors": "Easy Apply IPAdapter (FaceID Kolors)", + "easy ipadapterStyleComposition": "Easy Apply IPAdapter (StyleComposition)", + "easy ipadapterApplyEncoder": "Easy Apply IPAdapter (Encoder)", + "easy ipadapterApplyRegional": "Easy Apply IPAdapter (Regional)", + "easy ipadapterApplyEmbeds": "Easy Apply IPAdapter (Embeds)", + "easy ipadapterApplyFromParams": "Easy Apply IPAdapter (From Params)", + "easy instantIDApply": "Easy Apply InstantID", + "easy instantIDApplyADV": "Easy Apply InstantID (Advanced)", + "easy pulIDApply": "Easy Apply PuLID", + "easy pulIDApplyADV": "Easy Apply PuLID (Advanced)", + "easy styleAlignedBatchAlign": "Easy Apply StyleAlign", + "easy icLightApply": "Easy Apply ICLight" +} \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/api.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/api.py new file mode 100644 index 0000000000000000000000000000000000000000..baee49ef16e10106461550f9f6964cb6b2618e6c --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/api.py @@ -0,0 +1,139 @@ +import comfy.utils +from ..libs.api.fluxai import fluxaiAPI +from ..libs.api.bizyair import bizyairAPI, encode_data +from nodes import NODE_CLASS_MAPPINGS as ALL_NODE_CLASS_MAPPINGS + +class joyCaption2API: + API_URL = f"/supernode/joycaption2" + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "image": ("IMAGE",), + "do_sample": ([True, False],), + "temperature": ( + "FLOAT", + { + "default": 0.5, + "min": 0.0, + "max": 2.0, + "step": 0.01, + "round": 0.001, + "display": "number", + }, + ), + "max_tokens": ( + "INT", + { + "default": 256, + "min": 16, + "max": 512, + "step": 16, + "display": "number", + }, + ), + "caption_type": ( + [ + "Descriptive", + "Descriptive (Informal)", + "Training Prompt", + "MidJourney", + "Booru tag list", + "Booru-like tag list", + "Art Critic", + "Product Listing", + "Social Media Post", + ], + ), + "caption_length": ( + ["any", "very short", "short", "medium-length", "long", "very long"] + + [str(i) for i in range(20, 261, 10)], + ), + "extra_options": ( + "STRING", + { + "placeholder": "Extra options(e.g):\nIf there is a person/character in the image you must refer to them as {name}.", + "tooltip": "Extra options for the model", + "multiline": True, + }, + ), + "name_input": ( + "STRING", + { + "default": "", + "tooltip": "Name input is only used if an Extra Option is selected that requires it.", + }, + ), + "custom_prompt": ( + "STRING", + { + "default": "", + "multiline": True, + }, + ), + }, + "optional":{ + "apikey_override": ("STRING", {"default": "", "forceInput": True, "tooltip":"Override the API key in the local config"}), + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("caption",) + + FUNCTION = "joycaption" + OUTPUT_NODE = False + + CATEGORY = "EasyUse/API" + + def joycaption( + self, + image, + do_sample, + temperature, + max_tokens, + caption_type, + caption_length, + extra_options, + name_input, + custom_prompt, + apikey_override=None + ): + pbar = comfy.utils.ProgressBar(100) + pbar.update_absolute(10) + SIZE_LIMIT = 1536 + _, w, h, c = image.shape + if w > SIZE_LIMIT or h > SIZE_LIMIT: + node_class = ALL_NODE_CLASS_MAPPINGS['easy imageScaleDownToSize'] + image, = node_class().image_scale_down_to_size(image, SIZE_LIMIT, True) + + payload = { + "image": None, + "do_sample": do_sample == True, + "temperature": temperature, + "max_new_tokens": max_tokens, + "caption_type": caption_type, + "caption_length": caption_length, + "extra_options": [extra_options], + "name_input": name_input, + "custom_prompt": custom_prompt, + } + + pbar.update_absolute(30) + caption = bizyairAPI.joyCaption(payload, image, apikey_override, API_URL=self.API_URL) + + pbar.update_absolute(100) + return (caption,) + +class joyCaption3API(joyCaption2API): + API_URL = f"/supernode/joycaption3" + +NODE_CLASS_MAPPINGS = { + "easy joyCaption2API": joyCaption2API, + "easy joyCaption3API": joyCaption3API, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "easy joyCaption2API": "JoyCaption2 (BizyAIR)", + "easy joyCaption3API": "JoyCaption3 (BizyAIR)", +} \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/deprecated.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/deprecated.py new file mode 100644 index 0000000000000000000000000000000000000000..ae89659c2fad8b130fcdb329bcdd9f8c15a93ed3 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/deprecated.py @@ -0,0 +1,521 @@ +import numpy as np +import os +import json +import torch +import folder_paths +import comfy +import comfy.model_management +from PIL import Image +from PIL.PngImagePlugin import PngInfo +from nodes import ConditioningSetMask, RepeatLatentBatch +from comfy_extras.nodes_mask import LatentCompositeMasked +from ..libs.log import log_node_info, log_node_warn +from ..libs.adv_encode import advanced_encode +from ..libs.utils import AlwaysEqualProxy +any_type = AlwaysEqualProxy("*") + + +class If: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "any": (any_type,), + "if": (any_type,), + "else": (any_type,), + }, + } + + RETURN_TYPES = (any_type,) + RETURN_NAMES = ("?",) + FUNCTION = "execute" + CATEGORY = "EasyUse/🚫 Deprecated" + DEPRECATED = True + + def execute(self, *args, **kwargs): + return (kwargs['if'] if kwargs['any'] else kwargs['else'],) + + +class poseEditor: + @classmethod + def INPUT_TYPES(s): + return {"required": { + "image": ("STRING", {"default": ""}) + }} + + FUNCTION = "output_pose" + CATEGORY = "EasyUse/🚫 Deprecated" + DEPRECATED = True + RETURN_TYPES = () + RETURN_NAMES = () + + def output_pose(self, image): + return () + + +class imageToMask: + @classmethod + def INPUT_TYPES(s): + return {"required": { + "image": ("IMAGE",), + "channel": (['red', 'green', 'blue'],), + } + } + + RETURN_TYPES = ("MASK",) + FUNCTION = "convert" + CATEGORY = "EasyUse/🚫 Deprecated" + DEPRECATED = True + + def convert_to_single_channel(self, image, channel='red'): + from PIL import Image + # Convert to RGB mode to access individual channels + image = image.convert('RGB') + + # Extract the desired channel and convert to greyscale + if channel == 'red': + channel_img = image.split()[0].convert('L') + elif channel == 'green': + channel_img = image.split()[1].convert('L') + elif channel == 'blue': + channel_img = image.split()[2].convert('L') + else: + raise ValueError( + "Invalid channel option. Please choose 'red', 'green', or 'blue'.") + + # Convert the greyscale channel back to RGB mode + channel_img = Image.merge( + 'RGB', (channel_img, channel_img, channel_img)) + + return channel_img + + def convert(self, image, channel='red'): + from ..libs.image import pil2tensor, tensor2pil + image = self.convert_to_single_channel(tensor2pil(image), channel) + image = pil2tensor(image) + return (image.squeeze().mean(2),) + +# 显示推理时间 +class showSpentTime: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "pipe": ("PIPE_LINE",), + }, + "hidden": { + "unique_id": "UNIQUE_ID", + "extra_pnginfo": "EXTRA_PNGINFO", + }, + } + + FUNCTION = "notify" + OUTPUT_NODE = True + CATEGORY = "EasyUse/🚫 Deprecated" + DEPRECATED = True + RETURN_TYPES = () + RETURN_NAMES = () + + def notify(self, pipe, spent_time=None, unique_id=None, extra_pnginfo=None): + if unique_id and extra_pnginfo and "workflow" in extra_pnginfo: + workflow = extra_pnginfo["workflow"] + node = next((x for x in workflow["nodes"] if str(x["id"]) == unique_id), None) + if node: + spent_time = pipe['loader_settings']['spent_time'] if 'spent_time' in pipe['loader_settings'] else '' + node["widgets_values"] = [spent_time] + + return {"ui": {"text": [spent_time]}, "result": {}} + + +# 潜空间sigma相乘 +class latentNoisy: + @classmethod + def INPUT_TYPES(s): + return {"required": { + "sampler_name": (comfy.samplers.KSampler.SAMPLERS,), + "scheduler": (comfy.samplers.KSampler.SCHEDULERS,), + "steps": ("INT", {"default": 10000, "min": 0, "max": 10000}), + "start_at_step": ("INT", {"default": 0, "min": 0, "max": 10000}), + "end_at_step": ("INT", {"default": 10000, "min": 1, "max": 10000}), + "source": (["CPU", "GPU"],), + "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), + }, + "optional": { + "pipe": ("PIPE_LINE",), + "optional_model": ("MODEL",), + "optional_latent": ("LATENT",) + }} + + RETURN_TYPES = ("PIPE_LINE", "LATENT", "FLOAT",) + RETURN_NAMES = ("pipe", "latent", "sigma",) + FUNCTION = "run" + DEPRECATED = True + + CATEGORY = "EasyUse/🚫 Deprecated" + + def run(self, sampler_name, scheduler, steps, start_at_step, end_at_step, source, seed, pipe=None, optional_model=None, optional_latent=None): + model = optional_model if optional_model is not None else pipe["model"] + batch_size = pipe["loader_settings"]["batch_size"] + empty_latent_height = pipe["loader_settings"]["empty_latent_height"] + empty_latent_width = pipe["loader_settings"]["empty_latent_width"] + + if optional_latent is not None: + samples = optional_latent + else: + torch.manual_seed(seed) + if source == "CPU": + device = "cpu" + else: + device = comfy.model_management.get_torch_device() + noise = torch.randn((batch_size, 4, empty_latent_height // 8, empty_latent_width // 8), dtype=torch.float32, + device=device).cpu() + + samples = {"samples": noise} + + device = comfy.model_management.get_torch_device() + end_at_step = min(steps, end_at_step) + start_at_step = min(start_at_step, end_at_step) + comfy.model_management.load_model_gpu(model) + model_patcher = comfy.model_patcher.ModelPatcher(model.model, load_device=device, offload_device=comfy.model_management.unet_offload_device()) + sampler = comfy.samplers.KSampler(model_patcher, steps=steps, device=device, sampler=sampler_name, + scheduler=scheduler, denoise=1.0, model_options=model.model_options) + sigmas = sampler.sigmas + sigma = sigmas[start_at_step] - sigmas[end_at_step] + sigma /= model.model.latent_format.scale_factor + sigma = sigma.cpu().numpy() + + samples_out = samples.copy() + + s1 = samples["samples"] + samples_out["samples"] = s1 * sigma + + if pipe is None: + pipe = {} + new_pipe = { + **pipe, + "samples": samples_out + } + del pipe + + return (new_pipe, samples_out, sigma) + +# Latent遮罩复合 +class latentCompositeMaskedWithCond: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "pipe": ("PIPE_LINE",), + "text_combine": ("LIST",), + "source_latent": ("LATENT",), + "source_mask": ("MASK",), + "destination_mask": ("MASK",), + "text_combine_mode": (["add", "replace", "cover"], {"default": "add"}), + "replace_text": ("STRING", {"default": ""}) + }, + "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID"}, + } + + OUTPUT_IS_LIST = (False, False, True) + + RETURN_TYPES = ("PIPE_LINE", "LATENT", "CONDITIONING") + RETURN_NAMES = ("pipe", "latent", "conditioning",) + FUNCTION = "run" + + CATEGORY = "EasyUse/🚫 Deprecated" + DEPRECATED = True + + def run(self, pipe, text_combine, source_latent, source_mask, destination_mask, text_combine_mode, replace_text, prompt=None, extra_pnginfo=None, my_unique_id=None): + positive = None + clip = pipe["clip"] + destination_latent = pipe["samples"] + + conds = [] + + for text in text_combine: + if text_combine_mode == 'cover': + positive = text + elif text_combine_mode == 'replace' and replace_text != '': + positive = pipe["loader_settings"]["positive"].replace(replace_text, text) + else: + positive = pipe["loader_settings"]["positive"] + ',' + text + positive_token_normalization = pipe["loader_settings"]["positive_token_normalization"] + positive_weight_interpretation = pipe["loader_settings"]["positive_weight_interpretation"] + a1111_prompt_style = pipe["loader_settings"]["a1111_prompt_style"] + positive_cond = pipe["positive"] + + log_node_warn("Positive encoding...") + steps = pipe["loader_settings"]["steps"] if "steps" in pipe["loader_settings"] else 1 + positive_embeddings_final = advanced_encode(clip, positive, + positive_token_normalization, + positive_weight_interpretation, w_max=1.0, + apply_to_pooled='enable', a1111_prompt_style=a1111_prompt_style, steps=steps) + + # source cond + (cond_1,) = ConditioningSetMask().append(positive_cond, source_mask, "default", 1) + (cond_2,) = ConditioningSetMask().append(positive_embeddings_final, destination_mask, "default", 1) + positive_cond = cond_1 + cond_2 + + conds.append(positive_cond) + # latent composite masked + (samples,) = LatentCompositeMasked().composite(destination_latent, source_latent, 0, 0, False) + + new_pipe = { + **pipe, + "samples": samples, + "loader_settings": { + **pipe["loader_settings"], + "positive": positive, + } + } + + del pipe + + return (new_pipe, samples, conds) + +# 噪声注入到潜空间 +class injectNoiseToLatent: + @classmethod + def INPUT_TYPES(s): + return {"required": { + "strength": ("FLOAT", {"default": 0.1, "min": 0.0, "max": 200.0, "step": 0.0001}), + "normalize": ("BOOLEAN", {"default": False}), + "average": ("BOOLEAN", {"default": False}), + }, + "optional": { + "pipe_to_noise": ("PIPE_LINE",), + "image_to_latent": ("IMAGE",), + "latent": ("LATENT",), + "noise": ("LATENT",), + "mask": ("MASK",), + "mix_randn_amount": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1000.0, "step": 0.001}), + "seed": ("INT", {"default": 123, "min": 0, "max": 0xffffffffffffffff, "step": 1}), + } + } + + RETURN_TYPES = ("LATENT",) + FUNCTION = "inject" + CATEGORY = "EasyUse/🚫 Deprecated" + DEPRECATED = True + + + def inject(self,strength, normalize, average, pipe_to_noise=None, noise=None, image_to_latent=None, latent=None, mix_randn_amount=0, mask=None, seed=None): + + vae = pipe_to_noise["vae"] if pipe_to_noise is not None else pipe_to_noise["vae"] + batch_size = pipe_to_noise["loader_settings"]["batch_size"] if pipe_to_noise is not None and "batch_size" in pipe_to_noise["loader_settings"] else 1 + if noise is None and pipe_to_noise is not None: + noise = pipe_to_noise["samples"] + elif noise is None: + raise Exception("InjectNoiseToLatent: No noise provided") + + if image_to_latent is not None and vae is not None: + samples = {"samples": vae.encode(image_to_latent[:, :, :, :3])} + latents = RepeatLatentBatch().repeat(samples, batch_size)[0] + elif latent is not None: + latents = latent + else: + latents = {"samples": noise["samples"].clone()} + + samples = latents.copy() + if latents["samples"].shape != noise["samples"].shape: + raise ValueError("InjectNoiseToLatent: Latent and noise must have the same shape") + if average: + noised = (samples["samples"].clone() + noise["samples"].clone()) / 2 + else: + noised = samples["samples"].clone() + noise["samples"].clone() * strength + if normalize: + noised = noised / noised.std() + if mask is not None: + mask = torch.nn.functional.interpolate(mask.reshape((-1, 1, mask.shape[-2], mask.shape[-1])), + size=(noised.shape[2], noised.shape[3]), mode="bilinear") + mask = mask.expand((-1, noised.shape[1], -1, -1)) + if mask.shape[0] < noised.shape[0]: + mask = mask.repeat((noised.shape[0] - 1) // mask.shape[0] + 1, 1, 1, 1)[:noised.shape[0]] + noised = mask * noised + (1 - mask) * latents["samples"] + if mix_randn_amount > 0: + if seed is not None: + torch.manual_seed(seed) + rand_noise = torch.randn_like(noised) + noised = ((1 - mix_randn_amount) * noised + mix_randn_amount * + rand_noise) / ((mix_randn_amount ** 2 + (1 - mix_randn_amount) ** 2) ** 0.5) + samples["samples"] = noised + return (samples,) + + +from ..libs.api.stability import stableAPI +class stableDiffusion3API: + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "positive": ("STRING", {"default": "", "placeholder": "Positive", "multiline": True}), + "negative": ("STRING", {"default": "", "placeholder": "Negative", "multiline": True}), + "model": (["sd3", "sd3-turbo"],), + "aspect_ratio": (['16:9', '1:1', '21:9', '2:3', '3:2', '4:5', '5:4', '9:16', '9:21'],), + "seed": ("INT", {"default": 0, "min": 0, "max": 4294967294}), + "denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0}), + }, + "optional": { + "optional_image": ("IMAGE",), + }, + "hidden": { + "unique_id": "UNIQUE_ID", + "extra_pnginfo": "EXTRA_PNGINFO", + }, + } + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("image",) + + FUNCTION = "generate" + OUTPUT_NODE = False + + CATEGORY = "EasyUse/🚫 Deprecated" + DEPRECATED = True + + + def generate(self, positive, negative, model, aspect_ratio, seed, denoise, optional_image=None, unique_id=None, extra_pnginfo=None): + stableAPI.getAPIKeys() + mode = 'text-to-image' + if optional_image is not None: + mode = 'image-to-image' + output_image = stableAPI.generate_sd3_image(positive, negative, aspect_ratio, seed=seed, mode=mode, model=model, strength=denoise, image=optional_image) + return (output_image,) + + +class saveImageLazy(): + def __init__(self): + self.output_dir = folder_paths.get_output_directory() + self.type = "output" + self.compress_level = 4 + + @classmethod + def INPUT_TYPES(s): + return {"required": + {"images": ("IMAGE",), + "filename_prefix": ("STRING", {"default": "ComfyUI"}), + "save_metadata": ("BOOLEAN", {"default": True}), + }, + "optional":{}, + "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"}, + } + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("images",) + OUTPUT_NODE = False + FUNCTION = "save" + + DEPRECATED = True + CATEGORY = "EasyUse/🚫 Deprecated" + + def save(self, images, filename_prefix, save_metadata, prompt=None, extra_pnginfo=None): + extension = 'png' + + full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path( + filename_prefix, self.output_dir, images[0].shape[1], images[0].shape[0]) + + results = list() + for (batch_number, image) in enumerate(images): + i = 255. * image.cpu().numpy() + img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8)) + metadata = None + + filename_with_batch_num = filename.replace( + "%batch_num%", str(batch_number)) + + counter = 1 + + if os.path.exists(full_output_folder) and os.listdir(full_output_folder): + filtered_filenames = list(filter( + lambda filename: filename.startswith( + filename_with_batch_num + "_") + and filename[len(filename_with_batch_num) + 1:-4].isdigit(), + os.listdir(full_output_folder) + )) + + if filtered_filenames: + max_counter = max( + int(filename[len(filename_with_batch_num) + 1:-4]) + for filename in filtered_filenames + ) + counter = max_counter + 1 + + file = f"{filename_with_batch_num}_{counter:05}.{extension}" + + save_path = os.path.join(full_output_folder, file) + + if save_metadata: + metadata = PngInfo() + if prompt is not None: + metadata.add_text("prompt", json.dumps(prompt)) + if extra_pnginfo is not None: + for x in extra_pnginfo: + metadata.add_text( + x, json.dumps(extra_pnginfo[x])) + + img.save(save_path, pnginfo=metadata) + + results.append({ + "filename": file, + "subfolder": subfolder, + "type": self.type + }) + + return {"ui": {"images": results} , "result": (images,)} + +from .logic import saveText, showAnything + +class showAnythingLazy(showAnything): + @classmethod + def INPUT_TYPES(s): + return {"required": {}, "optional": {"anything": (any_type, {}), }, + "hidden": {"unique_id": "UNIQUE_ID", "extra_pnginfo": "EXTRA_PNGINFO", + }} + + RETURN_TYPES = (any_type,) + RETURN_NAMES = ('output',) + INPUT_IS_LIST = True + OUTPUT_NODE = False + OUTPUT_IS_LIST = (False,) + DEPRECATED = True + FUNCTION = "log_input" + CATEGORY = "EasyUse/🚫 Deprecated" + +class saveTextLazy(saveText): + + RETURN_TYPES = ("STRING", "IMAGE") + RETURN_NAMES = ("text", 'image',) + + FUNCTION = "save_text" + OUTPUT_NODE = False + DEPRECATED = True + CATEGORY = "EasyUse/🚫 Deprecated" + +NODE_CLASS_MAPPINGS = { + "easy if": If, + "easy poseEditor": poseEditor, + "easy imageToMask": imageToMask, + "easy showSpentTime": showSpentTime, + "easy latentNoisy": latentNoisy, + "easy latentCompositeMaskedWithCond": latentCompositeMaskedWithCond, + "easy injectNoiseToLatent": injectNoiseToLatent, + "easy stableDiffusion3API": stableDiffusion3API, + "easy saveImageLazy": saveImageLazy, + "easy saveTextLazy": saveTextLazy, + "easy showAnythingLazy": showAnythingLazy, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "easy if": "If (🚫Deprecated)", + "easy poseEditor": "PoseEditor (🚫Deprecated)", + "easy imageToMask": "ImageToMask (🚫Deprecated)", + "easy showSpentTime": "Show Spent Time (🚫Deprecated)", + "easy latentNoisy": "LatentNoisy (🚫Deprecated)", + "easy latentCompositeMaskedWithCond": "LatentCompositeMaskedWithCond (🚫Deprecated)", + "easy injectNoiseToLatent": "InjectNoiseToLatent (🚫Deprecated)", + "easy stableDiffusion3API": "StableDiffusion3API (🚫Deprecated)", + "easy saveImageLazy": "SaveImageLazy (🚫Deprecated)", + "easy saveTextLazy": "SaveTextLazy (🚫Deprecated)", + "easy showAnythingLazy": "ShowAnythingLazy (🚫Deprecated)", +} \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/fix.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/fix.py new file mode 100644 index 0000000000000000000000000000000000000000..c54220c4016c597558cdcf09120c1596795b6fb3 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/fix.py @@ -0,0 +1,647 @@ +import sys +import time +import comfy +import torch +import folder_paths + +from comfy_extras.chainner_models import model_loading + +from server import PromptServer +from nodes import MAX_RESOLUTION, NODE_CLASS_MAPPINGS as ALL_NODE_CLASS_MAPPINGS + +from ..libs.utils import easySave, get_sd_version +from ..libs.sampler import easySampler +from .. import easyCache, sampler + +class hiresFix: + upscale_methods = ["nearest-exact", "bilinear", "area", "bicubic", "lanczos", "bislerp"] + crop_methods = ["disabled", "center"] + + @classmethod + def INPUT_TYPES(s): + return {"required": { + "model_name": (folder_paths.get_filename_list("upscale_models"),), + "rescale_after_model": ([False, True], {"default": True}), + "rescale_method": (s.upscale_methods,), + "rescale": (["by percentage", "to Width/Height", 'to longer side - maintain aspect'],), + "percent": ("INT", {"default": 50, "min": 0, "max": 1000, "step": 1}), + "width": ("INT", {"default": 1024, "min": 64, "max": MAX_RESOLUTION, "step": 8}), + "height": ("INT", {"default": 1024, "min": 64, "max": MAX_RESOLUTION, "step": 8}), + "longer_side": ("INT", {"default": 1024, "min": 64, "max": MAX_RESOLUTION, "step": 8}), + "crop": (s.crop_methods,), + "image_output": (["Hide", "Preview", "Save", "Hide&Save", "Sender", "Sender&Save"],{"default": "Preview"}), + "link_id": ("INT", {"default": 0, "min": 0, "max": sys.maxsize, "step": 1}), + "save_prefix": ("STRING", {"default": "ComfyUI"}), + }, + "optional": { + "pipe": ("PIPE_LINE",), + "image": ("IMAGE",), + "vae": ("VAE",), + }, + "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID", + }, + } + + RETURN_TYPES = ("PIPE_LINE", "IMAGE", "LATENT", ) + RETURN_NAMES = ('pipe', 'image', "latent", ) + + FUNCTION = "upscale" + CATEGORY = "EasyUse/Fix" + OUTPUT_NODE = True + + def vae_encode_crop_pixels(self, pixels): + x = (pixels.shape[1] // 8) * 8 + y = (pixels.shape[2] // 8) * 8 + if pixels.shape[1] != x or pixels.shape[2] != y: + x_offset = (pixels.shape[1] % 8) // 2 + y_offset = (pixels.shape[2] % 8) // 2 + pixels = pixels[:, x_offset:x + x_offset, y_offset:y + y_offset, :] + return pixels + + def upscale(self, model_name, rescale_after_model, rescale_method, rescale, percent, width, height, + longer_side, crop, image_output, link_id, save_prefix, pipe=None, image=None, vae=None, prompt=None, + extra_pnginfo=None, my_unique_id=None): + + new_pipe = {} + if pipe is not None: + image = image if image is not None else pipe["images"] + vae = vae if vae is not None else pipe.get("vae") + elif image is None or vae is None: + raise ValueError("pipe or image or vae missing.") + # Load Model + model_path = folder_paths.get_full_path("upscale_models", model_name) + sd = comfy.utils.load_torch_file(model_path, safe_load=True) + upscale_model = model_loading.load_state_dict(sd).eval() + + # Model upscale + device = comfy.model_management.get_torch_device() + upscale_model.to(device) + in_img = image.movedim(-1, -3).to(device) + + tile = 128 + 64 + overlap = 8 + steps = in_img.shape[0] * comfy.utils.get_tiled_scale_steps(in_img.shape[3], in_img.shape[2], tile_x=tile, + tile_y=tile, overlap=overlap) + pbar = comfy.utils.ProgressBar(steps) + s = comfy.utils.tiled_scale(in_img, lambda a: upscale_model(a), tile_x=tile, tile_y=tile, overlap=overlap, + upscale_amount=upscale_model.scale, pbar=pbar) + upscale_model.cpu() + s = torch.clamp(s.movedim(-3, -1), min=0, max=1.0) + + # Post Model Rescale + if rescale_after_model == True: + samples = s.movedim(-1, 1) + orig_height = samples.shape[2] + orig_width = samples.shape[3] + if rescale == "by percentage" and percent != 0: + height = percent / 100 * orig_height + width = percent / 100 * orig_width + if (width > MAX_RESOLUTION): + width = MAX_RESOLUTION + if (height > MAX_RESOLUTION): + height = MAX_RESOLUTION + + width = easySampler.enforce_mul_of_64(width) + height = easySampler.enforce_mul_of_64(height) + elif rescale == "to longer side - maintain aspect": + longer_side = easySampler.enforce_mul_of_64(longer_side) + if orig_width > orig_height: + width, height = longer_side, easySampler.enforce_mul_of_64(longer_side * orig_height / orig_width) + else: + width, height = easySampler.enforce_mul_of_64(longer_side * orig_width / orig_height), longer_side + + s = comfy.utils.common_upscale(samples, width, height, rescale_method, crop) + s = s.movedim(1, -1) + + # vae encode + pixels = self.vae_encode_crop_pixels(s) + t = vae.encode(pixels[:, :, :, :3]) + + if pipe is not None: + new_pipe = { + "model": pipe['model'], + "positive": pipe['positive'], + "negative": pipe['negative'], + "vae": vae, + "clip": pipe['clip'], + + "samples": {"samples": t}, + "images": s, + "seed": pipe['seed'], + + "loader_settings": { + **pipe["loader_settings"], + } + } + del pipe + else: + new_pipe = {} + + results = easySave(s, save_prefix, image_output, prompt, extra_pnginfo) + + if image_output in ("Sender", "Sender&Save"): + PromptServer.instance.send_sync("img-send", {"link_id": link_id, "images": results}) + + if image_output in ("Hide", "Hide&Save"): + return (new_pipe, s, {"samples": t},) + + return {"ui": {"images": results}, + "result": (new_pipe, s, {"samples": t},)} + +# 预细节修复 +class preDetailerFix: + @classmethod + def INPUT_TYPES(s): + return {"required": { + "pipe": ("PIPE_LINE",), + "guide_size": ("FLOAT", {"default": 256, "min": 64, "max": MAX_RESOLUTION, "step": 8}), + "guide_size_for": ("BOOLEAN", {"default": True, "label_on": "bbox", "label_off": "crop_region"}), + "max_size": ("FLOAT", {"default": 768, "min": 64, "max": MAX_RESOLUTION, "step": 8}), + "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), + "steps": ("INT", {"default": 20, "min": 1, "max": 10000}), + "cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0}), + "sampler_name": (comfy.samplers.KSampler.SAMPLERS,), + "scheduler": (comfy.samplers.KSampler.SCHEDULERS + ['align_your_steps'],), + "denoise": ("FLOAT", {"default": 0.5, "min": 0.0001, "max": 1.0, "step": 0.01}), + "feather": ("INT", {"default": 5, "min": 0, "max": 100, "step": 1}), + "noise_mask": ("BOOLEAN", {"default": True, "label_on": "enabled", "label_off": "disabled"}), + "force_inpaint": ("BOOLEAN", {"default": True, "label_on": "enabled", "label_off": "disabled"}), + "drop_size": ("INT", {"min": 1, "max": MAX_RESOLUTION, "step": 1, "default": 10}), + "wildcard": ("STRING", {"multiline": True, "dynamicPrompts": False}), + "cycle": ("INT", {"default": 1, "min": 1, "max": 10, "step": 1}), + }, + "optional": { + "bbox_segm_pipe": ("PIPE_LINE",), + "sam_pipe": ("PIPE_LINE",), + "optional_image": ("IMAGE",), + }, + } + + RETURN_TYPES = ("PIPE_LINE",) + RETURN_NAMES = ("pipe",) + OUTPUT_IS_LIST = (False,) + FUNCTION = "doit" + + CATEGORY = "EasyUse/Fix" + + def doit(self, pipe, guide_size, guide_size_for, max_size, seed, steps, cfg, sampler_name, scheduler, denoise, feather, noise_mask, force_inpaint, drop_size, wildcard, cycle, bbox_segm_pipe=None, sam_pipe=None, optional_image=None): + + model = pipe["model"] if "model" in pipe else None + if model is None: + raise Exception(f"[ERROR] pipe['model'] is missing") + clip = pipe["clip"] if"clip" in pipe else None + if clip is None: + raise Exception(f"[ERROR] pipe['clip'] is missing") + vae = pipe["vae"] if "vae" in pipe else None + if vae is None: + raise Exception(f"[ERROR] pipe['vae'] is missing") + if optional_image is not None: + images = optional_image + else: + images = pipe["images"] if "images" in pipe else None + if images is None: + raise Exception(f"[ERROR] pipe['image'] is missing") + positive = pipe["positive"] if "positive" in pipe else None + if positive is None: + raise Exception(f"[ERROR] pipe['positive'] is missing") + negative = pipe["negative"] if "negative" in pipe else None + if negative is None: + raise Exception(f"[ERROR] pipe['negative'] is missing") + bbox_segm_pipe = bbox_segm_pipe or (pipe["bbox_segm_pipe"] if pipe and "bbox_segm_pipe" in pipe else None) + if bbox_segm_pipe is None: + raise Exception(f"[ERROR] bbox_segm_pipe or pipe['bbox_segm_pipe'] is missing") + sam_pipe = sam_pipe or (pipe["sam_pipe"] if pipe and "sam_pipe" in pipe else None) + if sam_pipe is None: + raise Exception(f"[ERROR] sam_pipe or pipe['sam_pipe'] is missing") + + loader_settings = pipe["loader_settings"] if "loader_settings" in pipe else {} + + if(scheduler == 'align_your_steps'): + model_version = get_sd_version(model) + if model_version == 'sdxl': + scheduler = 'AYS SDXL' + elif model_version == 'svd': + scheduler = 'AYS SVD' + else: + scheduler = 'AYS SD1' + + new_pipe = { + "images": images, + "model": model, + "clip": clip, + "vae": vae, + "positive": positive, + "negative": negative, + "seed": seed, + + "bbox_segm_pipe": bbox_segm_pipe, + "sam_pipe": sam_pipe, + + "loader_settings": loader_settings, + + "detail_fix_settings": { + "guide_size": guide_size, + "guide_size_for": guide_size_for, + "max_size": max_size, + "seed": seed, + "steps": steps, + "cfg": cfg, + "sampler_name": sampler_name, + "scheduler": scheduler, + "denoise": denoise, + "feather": feather, + "noise_mask": noise_mask, + "force_inpaint": force_inpaint, + "drop_size": drop_size, + "wildcard": wildcard, + "cycle": cycle + } + } + + + del bbox_segm_pipe + del sam_pipe + + return (new_pipe,) + +# 预遮罩细节修复 +class preMaskDetailerFix: + @classmethod + def INPUT_TYPES(s): + return {"required": { + "pipe": ("PIPE_LINE",), + "mask": ("MASK",), + + "guide_size": ("FLOAT", {"default": 384, "min": 64, "max": MAX_RESOLUTION, "step": 8}), + "guide_size_for": ("BOOLEAN", {"default": True, "label_on": "bbox", "label_off": "crop_region"}), + "max_size": ("FLOAT", {"default": 1024, "min": 64, "max": MAX_RESOLUTION, "step": 8}), + "mask_mode": ("BOOLEAN", {"default": True, "label_on": "masked only", "label_off": "whole"}), + + "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), + "steps": ("INT", {"default": 20, "min": 1, "max": 10000}), + "cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0}), + "sampler_name": (comfy.samplers.KSampler.SAMPLERS,), + "scheduler": (comfy.samplers.KSampler.SCHEDULERS,), + "denoise": ("FLOAT", {"default": 0.5, "min": 0.0001, "max": 1.0, "step": 0.01}), + + "feather": ("INT", {"default": 5, "min": 0, "max": 100, "step": 1}), + "crop_factor": ("FLOAT", {"default": 3.0, "min": 1.0, "max": 10, "step": 0.1}), + "drop_size": ("INT", {"min": 1, "max": MAX_RESOLUTION, "step": 1, "default": 10}), + "refiner_ratio": ("FLOAT", {"default": 0.2, "min": 0.0, "max": 1.0}), + "batch_size": ("INT", {"default": 1, "min": 1, "max": 100}), + "cycle": ("INT", {"default": 1, "min": 1, "max": 10, "step": 1}), + }, + "optional": { + # "patch": ("INPAINT_PATCH",), + "optional_image": ("IMAGE",), + "inpaint_model": ("BOOLEAN", {"default": False, "label_on": "enabled", "label_off": "disabled"}), + "noise_mask_feather": ("INT", {"default": 20, "min": 0, "max": 100, "step": 1}), + }, + } + + RETURN_TYPES = ("PIPE_LINE",) + RETURN_NAMES = ("pipe",) + OUTPUT_IS_LIST = (False,) + FUNCTION = "doit" + + CATEGORY = "EasyUse/Fix" + + def doit(self, pipe, mask, guide_size, guide_size_for, max_size, mask_mode, seed, steps, cfg, sampler_name, scheduler, denoise, feather, crop_factor, drop_size,refiner_ratio, batch_size, cycle, optional_image=None, inpaint_model=False, noise_mask_feather=20): + + model = pipe["model"] if "model" in pipe else None + if model is None: + raise Exception(f"[ERROR] pipe['model'] is missing") + clip = pipe["clip"] if"clip" in pipe else None + if clip is None: + raise Exception(f"[ERROR] pipe['clip'] is missing") + vae = pipe["vae"] if "vae" in pipe else None + if vae is None: + raise Exception(f"[ERROR] pipe['vae'] is missing") + if optional_image is not None: + images = optional_image + else: + images = pipe["images"] if "images" in pipe else None + if images is None: + raise Exception(f"[ERROR] pipe['image'] is missing") + positive = pipe["positive"] if "positive" in pipe else None + if positive is None: + raise Exception(f"[ERROR] pipe['positive'] is missing") + negative = pipe["negative"] if "negative" in pipe else None + if negative is None: + raise Exception(f"[ERROR] pipe['negative'] is missing") + latent = pipe["samples"] if "samples" in pipe else None + if latent is None: + raise Exception(f"[ERROR] pipe['samples'] is missing") + + if 'noise_mask' not in latent: + if images is None: + raise Exception("No Images found") + if vae is None: + raise Exception("No VAE found") + x = (images.shape[1] // 8) * 8 + y = (images.shape[2] // 8) * 8 + mask = torch.nn.functional.interpolate(mask.reshape((-1, 1, mask.shape[-2], mask.shape[-1])), + size=(images.shape[1], images.shape[2]), mode="bilinear") + + pixels = images.clone() + if pixels.shape[1] != x or pixels.shape[2] != y: + x_offset = (pixels.shape[1] % 8) // 2 + y_offset = (pixels.shape[2] % 8) // 2 + pixels = pixels[:, x_offset:x + x_offset, y_offset:y + y_offset, :] + mask = mask[:, :, x_offset:x + x_offset, y_offset:y + y_offset] + + mask_erosion = mask + + m = (1.0 - mask.round()).squeeze(1) + for i in range(3): + pixels[:, :, :, i] -= 0.5 + pixels[:, :, :, i] *= m + pixels[:, :, :, i] += 0.5 + t = vae.encode(pixels) + + latent = {"samples": t, "noise_mask": (mask_erosion[:, :, :x, :y].round())} + # when patch was linked + # if patch is not None: + # worker = InpaintWorker(node_name="easy kSamplerInpainting") + # model, = worker.patch(model, latent, patch) + + loader_settings = pipe["loader_settings"] if "loader_settings" in pipe else {} + + new_pipe = { + "images": images, + "model": model, + "clip": clip, + "vae": vae, + "positive": positive, + "negative": negative, + "seed": seed, + "mask": mask, + + "loader_settings": loader_settings, + + "detail_fix_settings": { + "guide_size": guide_size, + "guide_size_for": guide_size_for, + "max_size": max_size, + "seed": seed, + "steps": steps, + "cfg": cfg, + "sampler_name": sampler_name, + "scheduler": scheduler, + "denoise": denoise, + "feather": feather, + "crop_factor": crop_factor, + "drop_size": drop_size, + "refiner_ratio": refiner_ratio, + "batch_size": batch_size, + "cycle": cycle + }, + + "mask_settings": { + "mask_mode": mask_mode, + "inpaint_model": inpaint_model, + "noise_mask_feather": noise_mask_feather + } + } + + del pipe + + return (new_pipe,) + +# 细节修复 +class detailerFix: + @classmethod + def INPUT_TYPES(s): + return {"required": { + "pipe": ("PIPE_LINE",), + "image_output": (["Hide", "Preview", "Save", "Hide&Save", "Sender", "Sender&Save"],{"default": "Preview"}), + "link_id": ("INT", {"default": 0, "min": 0, "max": sys.maxsize, "step": 1}), + "save_prefix": ("STRING", {"default": "ComfyUI"}), + }, + "optional": { + "model": ("MODEL",), + }, + "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID", } + } + + RETURN_TYPES = ("PIPE_LINE", "IMAGE", "IMAGE", "IMAGE") + RETURN_NAMES = ("pipe", "image", "cropped_refined", "cropped_enhanced_alpha") + OUTPUT_NODE = True + OUTPUT_IS_LIST = (False, False, True, True) + FUNCTION = "doit" + + CATEGORY = "EasyUse/Fix" + + + def doit(self, pipe, image_output, link_id, save_prefix, model=None, prompt=None, extra_pnginfo=None, my_unique_id=None): + + # Clean loaded_objects + easyCache.update_loaded_objects(prompt) + + # my_unique_id can be a composite ID (e.g. `101:134`) when put inside a sub-graph; this fixes it so that it can support sub-graphs + try: + my_unique_id = int(my_unique_id) + except (ValueError, TypeError): + my_unique_id = int(str(my_unique_id).split(':')[-1]) + + model = model or (pipe["model"] if "model" in pipe else None) + if model is None: + raise Exception(f"[ERROR] model or pipe['model'] is missing") + + detail_fix_settings = pipe["detail_fix_settings"] if "detail_fix_settings" in pipe else None + if detail_fix_settings is None: + raise Exception(f"[ERROR] detail_fix_settings or pipe['detail_fix_settings'] is missing") + + mask = pipe["mask"] if "mask" in pipe else None + + image = pipe["images"] + clip = pipe["clip"] + vae = pipe["vae"] + seed = pipe["seed"] + positive = pipe["positive"] + negative = pipe["negative"] + loader_settings = pipe["loader_settings"] if "loader_settings" in pipe else {} + guide_size = pipe["detail_fix_settings"]["guide_size"] if "guide_size" in pipe["detail_fix_settings"] else 256 + guide_size_for = pipe["detail_fix_settings"]["guide_size_for"] if "guide_size_for" in pipe[ + "detail_fix_settings"] else True + max_size = pipe["detail_fix_settings"]["max_size"] if "max_size" in pipe["detail_fix_settings"] else 768 + steps = pipe["detail_fix_settings"]["steps"] if "steps" in pipe["detail_fix_settings"] else 20 + cfg = pipe["detail_fix_settings"]["cfg"] if "cfg" in pipe["detail_fix_settings"] else 1.0 + sampler_name = pipe["detail_fix_settings"]["sampler_name"] if "sampler_name" in pipe[ + "detail_fix_settings"] else None + scheduler = pipe["detail_fix_settings"]["scheduler"] if "scheduler" in pipe["detail_fix_settings"] else None + denoise = pipe["detail_fix_settings"]["denoise"] if "denoise" in pipe["detail_fix_settings"] else 0.5 + feather = pipe["detail_fix_settings"]["feather"] if "feather" in pipe["detail_fix_settings"] else 5 + crop_factor = pipe["detail_fix_settings"]["crop_factor"] if "crop_factor" in pipe["detail_fix_settings"] else 3.0 + drop_size = pipe["detail_fix_settings"]["drop_size"] if "drop_size" in pipe["detail_fix_settings"] else 10 + refiner_ratio = pipe["detail_fix_settings"]["refiner_ratio"] if "refiner_ratio" in pipe else 0.2 + batch_size = pipe["detail_fix_settings"]["batch_size"] if "batch_size" in pipe["detail_fix_settings"] else 1 + noise_mask = pipe["detail_fix_settings"]["noise_mask"] if "noise_mask" in pipe["detail_fix_settings"] else None + force_inpaint = pipe["detail_fix_settings"]["force_inpaint"] if "force_inpaint" in pipe["detail_fix_settings"] else False + wildcard = pipe["detail_fix_settings"]["wildcard"] if "wildcard" in pipe["detail_fix_settings"] else "" + cycle = pipe["detail_fix_settings"]["cycle"] if "cycle" in pipe["detail_fix_settings"] else 1 + + bbox_segm_pipe = pipe["bbox_segm_pipe"] if pipe and "bbox_segm_pipe" in pipe else None + sam_pipe = pipe["sam_pipe"] if "sam_pipe" in pipe else None + + # 细节修复初始时间 + start_time = int(time.time() * 1000) + if "mask_settings" in pipe: + mask_mode = pipe['mask_settings']["mask_mode"] if "inpaint_model" in pipe['mask_settings'] else True + inpaint_model = pipe['mask_settings']["inpaint_model"] if "inpaint_model" in pipe['mask_settings'] else False + noise_mask_feather = pipe['mask_settings']["noise_mask_feather"] if "noise_mask_feather" in pipe['mask_settings'] else 20 + cls = ALL_NODE_CLASS_MAPPINGS["MaskDetailerPipe"] + if "MaskDetailerPipe" not in ALL_NODE_CLASS_MAPPINGS: + raise Exception(f"[ERROR] To use MaskDetailerPipe, you need to install 'Impact Pack'") + basic_pipe = (model, clip, vae, positive, negative) + result_img, result_cropped_enhanced, result_cropped_enhanced_alpha, basic_pipe, refiner_basic_pipe_opt = cls().doit(image, mask, basic_pipe, guide_size, guide_size_for, max_size, mask_mode, + seed, steps, cfg, sampler_name, scheduler, denoise, + feather, crop_factor, drop_size, refiner_ratio, batch_size, cycle=1, + refiner_basic_pipe_opt=None, detailer_hook=None, inpaint_model=inpaint_model, noise_mask_feather=noise_mask_feather) + result_mask = mask + result_cnet_images = () + else: + if bbox_segm_pipe is None: + raise Exception(f"[ERROR] bbox_segm_pipe or pipe['bbox_segm_pipe'] is missing") + if sam_pipe is None: + raise Exception(f"[ERROR] sam_pipe or pipe['sam_pipe'] is missing") + bbox_detector_opt, bbox_threshold, bbox_dilation, bbox_crop_factor, segm_detector_opt = bbox_segm_pipe + sam_model_opt, sam_detection_hint, sam_dilation, sam_threshold, sam_bbox_expansion, sam_mask_hint_threshold, sam_mask_hint_use_negative = sam_pipe + if "FaceDetailer" not in ALL_NODE_CLASS_MAPPINGS: + raise Exception(f"[ERROR] To use FaceDetailer, you need to install 'Impact Pack'") + cls = ALL_NODE_CLASS_MAPPINGS["FaceDetailer"] + + result_img, result_cropped_enhanced, result_cropped_enhanced_alpha, result_mask, pipe, result_cnet_images = cls().doit( + image, model, clip, vae, guide_size, guide_size_for, max_size, seed, steps, cfg, sampler_name, + scheduler, + positive, negative, denoise, feather, noise_mask, force_inpaint, + bbox_threshold, bbox_dilation, bbox_crop_factor, + sam_detection_hint, sam_dilation, sam_threshold, sam_bbox_expansion, sam_mask_hint_threshold, + sam_mask_hint_use_negative, drop_size, bbox_detector_opt, wildcard, cycle, sam_model_opt, + segm_detector_opt, + detailer_hook=None) + + # 细节修复结束时间 + end_time = int(time.time() * 1000) + + spent_time = 'Fix:' + str((end_time - start_time) / 1000) + '"' + + results = easySave(result_img, save_prefix, image_output, prompt, extra_pnginfo) + sampler.update_value_by_id("results", my_unique_id, results) + + # Clean loaded_objects + easyCache.update_loaded_objects(prompt) + + new_pipe = { + "samples": None, + "images": result_img, + "model": model, + "clip": clip, + "vae": vae, + "seed": seed, + "positive": positive, + "negative": negative, + "wildcard": wildcard, + "bbox_segm_pipe": bbox_segm_pipe, + "sam_pipe": sam_pipe, + + "loader_settings": { + **loader_settings, + "spent_time": spent_time + }, + "detail_fix_settings": detail_fix_settings + } + if "mask_settings" in pipe: + new_pipe["mask_settings"] = pipe["mask_settings"] + + sampler.update_value_by_id("pipe_line", my_unique_id, new_pipe) + + del bbox_segm_pipe + del sam_pipe + del pipe + + if image_output in ("Hide", "Hide&Save"): + return (new_pipe, result_img, result_cropped_enhanced, result_cropped_enhanced_alpha, result_mask, result_cnet_images) + + if image_output in ("Sender", "Sender&Save"): + PromptServer.instance.send_sync("img-send", {"link_id": link_id, "images": results}) + + return {"ui": {"images": results}, "result": (new_pipe, result_img, result_cropped_enhanced, result_cropped_enhanced_alpha, result_mask, result_cnet_images )} + +class ultralyticsDetectorForDetailerFix: + @classmethod + def INPUT_TYPES(s): + bboxs = ["bbox/" + x for x in folder_paths.get_filename_list("ultralytics_bbox")] + segms = ["segm/" + x for x in folder_paths.get_filename_list("ultralytics_segm")] + return {"required": + {"model_name": (bboxs + segms,), + "bbox_threshold": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}), + "bbox_dilation": ("INT", {"default": 10, "min": -512, "max": 512, "step": 1}), + "bbox_crop_factor": ("FLOAT", {"default": 3.0, "min": 1.0, "max": 10, "step": 0.1}), + } + } + + RETURN_TYPES = ("PIPE_LINE",) + RETURN_NAMES = ("bbox_segm_pipe",) + FUNCTION = "doit" + + CATEGORY = "EasyUse/Fix" + + def doit(self, model_name, bbox_threshold, bbox_dilation, bbox_crop_factor): + if 'UltralyticsDetectorProvider' not in ALL_NODE_CLASS_MAPPINGS: + raise Exception(f"[ERROR] To use UltralyticsDetectorProvider, you need to install 'Impact Pack'") + cls = ALL_NODE_CLASS_MAPPINGS['UltralyticsDetectorProvider'] + bbox_detector, segm_detector = cls().doit(model_name) + pipe = (bbox_detector, bbox_threshold, bbox_dilation, bbox_crop_factor, segm_detector) + return (pipe,) + +class samLoaderForDetailerFix: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "model_name": (folder_paths.get_filename_list("sams"),), + "device_mode": (["AUTO", "Prefer GPU", "CPU"],{"default": "AUTO"}), + "sam_detection_hint": ( + ["center-1", "horizontal-2", "vertical-2", "rect-4", "diamond-4", "mask-area", "mask-points", + "mask-point-bbox", "none"],), + "sam_dilation": ("INT", {"default": 0, "min": -512, "max": 512, "step": 1}), + "sam_threshold": ("FLOAT", {"default": 0.93, "min": 0.0, "max": 1.0, "step": 0.01}), + "sam_bbox_expansion": ("INT", {"default": 0, "min": 0, "max": 1000, "step": 1}), + "sam_mask_hint_threshold": ("FLOAT", {"default": 0.7, "min": 0.0, "max": 1.0, "step": 0.01}), + "sam_mask_hint_use_negative": (["False", "Small", "Outter"],), + } + } + + RETURN_TYPES = ("PIPE_LINE",) + RETURN_NAMES = ("sam_pipe",) + FUNCTION = "doit" + + CATEGORY = "EasyUse/Fix" + + def doit(self, model_name, device_mode, sam_detection_hint, sam_dilation, sam_threshold, sam_bbox_expansion, sam_mask_hint_threshold, sam_mask_hint_use_negative): + if 'SAMLoader' not in ALL_NODE_CLASS_MAPPINGS: + raise Exception(f"[ERROR] To use SAMLoader, you need to install 'Impact Pack'") + cls = ALL_NODE_CLASS_MAPPINGS['SAMLoader'] + (sam_model,) = cls().load_model(model_name, device_mode) + pipe = (sam_model, sam_detection_hint, sam_dilation, sam_threshold, sam_bbox_expansion, sam_mask_hint_threshold, sam_mask_hint_use_negative) + return (pipe,) + + +NODE_CLASS_MAPPINGS = { + "easy hiresFix": hiresFix, + "easy preDetailerFix": preDetailerFix, + "easy preMaskDetailerFix": preMaskDetailerFix, + "easy ultralyticsDetectorPipe": ultralyticsDetectorForDetailerFix, + "easy samLoaderPipe": samLoaderForDetailerFix, + "easy detailerFix": detailerFix +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "easy hiresFix": "HiresFix", + "easy preDetailerFix": "PreDetailerFix", + "easy preMaskDetailerFix": "preMaskDetailerFix", + "easy ultralyticsDetectorPipe": "UltralyticsDetector (Pipe)", + "easy samLoaderPipe": "SAMLoader (Pipe)", + "easy detailerFix": "DetailerFix", +} diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/image.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/image.py new file mode 100644 index 0000000000000000000000000000000000000000..4b3a14cc4bbadf36db76d7ad4d113906c6155e67 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/image.py @@ -0,0 +1,2207 @@ +import os +import folder_paths +import torch +import numpy as np +import comfy.utils +import comfy.model_management +import shutil +from comfy_extras.nodes_compositing import JoinImageWithAlpha +from server import PromptServer +from nodes import MAX_RESOLUTION, NODE_CLASS_MAPPINGS as ALL_NODE_CLASS_MAPPINGS +from PIL import Image, ImageDraw, ImageFilter, ImageOps +import torch.nn.functional as F +from torchvision.transforms import Resize, CenterCrop, GaussianBlur, ToPILImage +from torchvision.transforms.functional import to_pil_image +from ..libs.log import log_node_info +from ..libs.utils import AlwaysEqualProxy, ByPassTypeTuple +from ..libs.cache import cache, update_cache, remove_cache +from ..libs.image import pil2tensor, tensor2pil, ResizeMode, get_new_bounds, RGB2RGBA, image2mask, empty_image, fit_resize_image +from ..libs.colorfix import adain_color_fix, wavelet_color_fix +from ..config import REMBG_DIR, REMBG_MODELS, HUMANPARSING_MODELS, MEDIAPIPE_MODELS, MEDIAPIPE_DIR + +any_type = AlwaysEqualProxy("*") +# 图像数量 +class imageCount: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "images": ("IMAGE",), + } + } + + CATEGORY = "EasyUse/Image" + + RETURN_TYPES = ("INT",) + RETURN_NAMES = ("count",) + FUNCTION = "get_count" + + def get_count(self, images): + return (images.size(0),) + +class imagesCountInDirectory: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "directory": ("STRING",), + "start_index": ("INT", {"default": 0, "min": 0, "step": 1}), + "limit": ("INT", {"default": -1, "min": -1, "max": 10000}), + } + } + + CATEGORY = "EasyUse/Image" + + RETURN_TYPES = ("INT",) + RETURN_NAMES = ("count",) + FUNCTION = "get_count" + + def get_count(self, directory, start_index, limit, **kwargs): + dir_files = os.listdir(directory) + valid_extensions = ['.jpg', '.jpeg', '.png', '.webp'] + dir_files = [f for f in dir_files if any(f.lower().endswith(ext) for ext in valid_extensions)] + if limit == -1: + files_length = len(dir_files) + total = files_length - start_index if start_index > 0 else files_length + else: + total = limit + return (total,) + +# 图像裁切 +class imageInsetCrop: + + @classmethod + def INPUT_TYPES(cls): # pylint: disable = invalid-name, missing-function-docstring + return { + "required": { + "image": ("IMAGE",), + "measurement": (['Pixels', 'Percentage'],), + "left": ("INT", { + "default": 0, + "min": 0, + "max": MAX_RESOLUTION, + "step": 8 + }), + "right": ("INT", { + "default": 0, + "min": 0, + "max": MAX_RESOLUTION, + "step": 8 + }), + "top": ("INT", { + "default": 0, + "min": 0, + "max": MAX_RESOLUTION, + "step": 8 + }), + "bottom": ("INT", { + "default": 0, + "min": 0, + "max": MAX_RESOLUTION, + "step": 8 + }), + }, + } + + RETURN_TYPES = ("IMAGE",) + FUNCTION = "crop" + + CATEGORY = "EasyUse/Image" + + # pylint: disable = too-many-arguments + def crop(self, measurement, left, right, top, bottom, image=None): + """Does the crop.""" + + _, height, width, _ = image.shape + + if measurement == 'Percentage': + left = int(width - (width * (100 - left) / 100)) + right = int(width - (width * (100 - right) / 100)) + top = int(height - (height * (100 - top) / 100)) + bottom = int(height - (height * (100 - bottom) / 100)) + + # Snap to 8 pixels + left = left // 8 * 8 + right = right // 8 * 8 + top = top // 8 * 8 + bottom = bottom // 8 * 8 + + if left == 0 and right == 0 and bottom == 0 and top == 0: + return (image,) + + inset_left, inset_right, inset_top, inset_bottom = get_new_bounds(width, height, left, right, + top, bottom) + if inset_top > inset_bottom: + raise ValueError( + f"Invalid cropping dimensions top ({inset_top}) exceeds bottom ({inset_bottom})") + if inset_left > inset_right: + raise ValueError( + f"Invalid cropping dimensions left ({inset_left}) exceeds right ({inset_right})") + + log_node_info("Image Inset Crop", f'Cropping image {width}x{height} width inset by {inset_left},{inset_right}, ' + + f'and height inset by {inset_top}, {inset_bottom}') + image = image[:, inset_top:inset_bottom, inset_left:inset_right, :] + + return (image,) + +# 图像尺寸 +class imageSize: + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "image": ("IMAGE",), + } + } + + RETURN_TYPES = ("INT", "INT") + RETURN_NAMES = ("width_int", "height_int") + OUTPUT_NODE = True + FUNCTION = "image_width_height" + + CATEGORY = "EasyUse/Image" + + def image_width_height(self, image): + _, raw_H, raw_W, _ = image.shape + + width = raw_W + height = raw_H + + if width is not None and height is not None: + result = (width, height) + else: + result = (0, 0) + return {"ui": {"text": "Width: "+str(width)+" , Height: "+str(height)}, "result": result} + +# 图像尺寸(最长边) +class imageSizeBySide: + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "image": ("IMAGE",), + "side": (["Longest", "Shortest"],) + } + } + + RETURN_TYPES = ("INT",) + RETURN_NAMES = ("resolution",) + OUTPUT_NODE = True + FUNCTION = "image_side" + + CATEGORY = "EasyUse/Image" + + def image_side(self, image, side): + _, raw_H, raw_W, _ = image.shape + + width = raw_W + height = raw_H + if width is not None and height is not None: + if side == "Longest": + result = (width,) if width > height else (height,) + elif side == 'Shortest': + result = (width,) if width < height else (height,) + else: + result = (0,) + return {"ui": {"text": str(result[0])}, "result": result} + +# 图像尺寸(最长边) +class imageSizeByLongerSide: + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "image": ("IMAGE",), + } + } + + RETURN_TYPES = ("INT",) + RETURN_NAMES = ("resolution",) + OUTPUT_NODE = True + FUNCTION = "image_longer_side" + + CATEGORY = "EasyUse/Image" + + def image_longer_side(self, image): + _, raw_H, raw_W, _ = image.shape + + width = raw_W + height = raw_H + if width is not None and height is not None: + if width > height: + result = (width,) + else: + result = (height,) + else: + result = (0,) + return {"ui": {"text": str(result[0])}, "result": result} + +# 图像缩放 +class imageScaleDown: + crop_methods = ["disabled", "center"] + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "images": ("IMAGE",), + "width": ( + "INT", + {"default": 512, "min": 1, "max": MAX_RESOLUTION, "step": 1}, + ), + "height": ( + "INT", + {"default": 512, "min": 1, "max": MAX_RESOLUTION, "step": 1}, + ), + "crop": (s.crop_methods,), + } + } + + RETURN_TYPES = ("IMAGE",) + CATEGORY = "EasyUse/Image" + FUNCTION = "image_scale_down" + + def image_scale_down(self, images, width, height, crop): + if crop == "center": + old_width = images.shape[2] + old_height = images.shape[1] + old_aspect = old_width / old_height + new_aspect = width / height + x = 0 + y = 0 + if old_aspect > new_aspect: + x = round((old_width - old_width * (new_aspect / old_aspect)) / 2) + elif old_aspect < new_aspect: + y = round((old_height - old_height * (old_aspect / new_aspect)) / 2) + s = images[:, y: old_height - y, x: old_width - x, :] + else: + s = images + + results = [] + for image in s: + img = tensor2pil(image).convert("RGB") + img = img.resize((width, height), Image.LANCZOS) + results.append(pil2tensor(img)) + + return (torch.cat(results, dim=0),) + +# 图像缩放比例 +class imageScaleDownBy(imageScaleDown): + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "images": ("IMAGE",), + "scale_by": ( + "FLOAT", + {"default": 0.5, "min": 0.01, "max": 1.0, "step": 0.01}, + ), + } + } + + RETURN_TYPES = ("IMAGE",) + CATEGORY = "EasyUse/Image" + FUNCTION = "image_scale_down_by" + + def image_scale_down_by(self, images, scale_by): + width = images.shape[2] + height = images.shape[1] + new_width = int(width * scale_by) + new_height = int(height * scale_by) + return self.image_scale_down(images, new_width, new_height, "center") + +# 图像缩放尺寸 +class imageScaleDownToSize(imageScaleDownBy): + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "images": ("IMAGE",), + "size": ("INT", {"default": 512, "min": 1, "max": MAX_RESOLUTION, "step": 1}), + "mode": ("BOOLEAN", {"default": True, "label_on": "max", "label_off": "min"}), + } + } + + RETURN_TYPES = ("IMAGE",) + CATEGORY = "EasyUse/Image" + FUNCTION = "image_scale_down_to_size" + + def image_scale_down_to_size(self, images, size, mode): + width = images.shape[2] + height = images.shape[1] + + if mode: + scale_by = size / max(width, height) + else: + scale_by = size / min(width, height) + + scale_by = min(scale_by, 1.0) + return self.image_scale_down_by(images, scale_by) + +class imageScaleToNormPixels: + upscale_methods = ["nearest-exact", "bilinear", "area", "bicubic", "lanczos"] + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "image": ("IMAGE",), + "upscale_method": (s.upscale_methods,), + "scale_by": ("FLOAT", {"default": 1.0, "min": 0.01, "max": 8.0, "step": 0.01}), + } + } + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("image",) + FUNCTION = "scale" + CATEGORY = "EasyUse/Image" + + def scale(self, image, upscale_method, scale_by): + height, width = image.shape[1:3] + width = int(width * scale_by - width * scale_by % 8) + height = int(height * scale_by - height * scale_by % 8) + upscale_image_cls = ALL_NODE_CLASS_MAPPINGS['ImageScale'] + image, = upscale_image_cls().upscale(image, upscale_method, width, height, "disabled") + return (image,) + +# 图像比率 +class imageRatio: + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "image": ("IMAGE",), + } + } + + RETURN_TYPES = ("INT", "INT", "FLOAT", "FLOAT") + RETURN_NAMES = ("width_ratio_int", "height_ratio_int", "width_ratio_float", "height_ratio_float") + OUTPUT_NODE = True + FUNCTION = "image_ratio" + + CATEGORY = "EasyUse/Image" + + def gcf(self, a, b): + while b: + a, b = b, a % b + return a + + def image_ratio(self, image): + _, raw_H, raw_W, _ = image.shape + + width = raw_W + height = raw_H + + ratio = self.gcf(width, height) + + if width is not None and height is not None: + width_ratio = width // ratio + height_ratio = height // ratio + result = (width_ratio, height_ratio, width_ratio, height_ratio) + else: + width_ratio = 0 + height_ratio = 0 + result = (0, 0, 0.0, 0.0) + text = f"Image Ratio is {str(width_ratio)}:{str(height_ratio)}" + + return {"ui": {"text": text}, "result": result} + + +# 图像完美像素 +class imagePixelPerfect: + @classmethod + def INPUT_TYPES(s): + RESIZE_MODES = [ResizeMode.RESIZE.value, ResizeMode.INNER_FIT.value, ResizeMode.OUTER_FIT.value] + return { + "required": { + "image": ("IMAGE",), + "resize_mode": (RESIZE_MODES, {"default": ResizeMode.RESIZE.value}) + } + } + + RETURN_TYPES = ("INT",) + RETURN_NAMES = ("resolution",) + OUTPUT_NODE = True + FUNCTION = "execute" + + CATEGORY = "EasyUse/Image" + + def execute(self, image, resize_mode): + + _, raw_H, raw_W, _ = image.shape + + width = raw_W + height = raw_H + + k0 = float(height) / float(raw_H) + k1 = float(width) / float(raw_W) + + if resize_mode == ResizeMode.OUTER_FIT.value: + estimation = min(k0, k1) * float(min(raw_H, raw_W)) + else: + estimation = max(k0, k1) * float(min(raw_H, raw_W)) + + result = int(np.round(estimation)) + text = f"Width:{str(width)}\nHeight:{str(height)}\nPixelPerfect:{str(result)}" + + return {"ui": {"text": text}, "result": (result,)} + +# 图像保存 (简易) +from nodes import PreviewImage, SaveImage +class imageSaveSimple: + + def __init__(self): + self.output_dir = folder_paths.get_output_directory() + self.type = "output" + self.prefix_append = "" + self.compress_level = 4 + + @classmethod + def INPUT_TYPES(s): + return {"required": + { + "images": ("IMAGE",), + "filename_prefix": ("STRING", {"default": "ComfyUI"}), + "only_preview": ("BOOLEAN", {"default": False}), + }, + "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"}, + } + + RETURN_TYPES = () + FUNCTION = "save" + OUTPUT_NODE = True + CATEGORY = "EasyUse/Image" + + def save(self, images, filename_prefix="ComfyUI", only_preview=False, prompt=None, extra_pnginfo=None): + if only_preview: + return PreviewImage().save_images(images, filename_prefix, prompt, extra_pnginfo) + else: + return SaveImage().save_images(images, filename_prefix, prompt, extra_pnginfo) + +# 图像批次合并 +class JoinImageBatch: + """Turns an image batch into one big image.""" + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "images": ("IMAGE",), + "mode": (("horizontal", "vertical"), {"default": "horizontal"}), + }, + } + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("image",) + FUNCTION = "join" + CATEGORY = "EasyUse/Image" + + def join(self, images, mode): + n, h, w, c = images.shape + image = None + if mode == "vertical": + # for vertical we can just reshape + image = images.reshape(1, n * h, w, c) + elif mode == "horizontal": + # for horizontal we have to swap axes + image = torch.transpose(torch.transpose(images, 1, 2).reshape(1, n * w, h, c), 1, 2) + return (image,) + +class imageListToImageBatch: + @classmethod + def INPUT_TYPES(s): + return {"required": { + "images": ("IMAGE",), + }} + + INPUT_IS_LIST = True + + RETURN_TYPES = ("IMAGE",) + FUNCTION = "doit" + + CATEGORY = "EasyUse/Image" + + def doit(self, images): + if len(images) <= 1: + return (images[0],) + else: + image_shape = images[0].shape + for i, img in enumerate(images): + if image_shape[1:] == img[1:]: + continue + else: + images[i] = comfy.utils.common_upscale(img.movedim(-1, 1), img.shape[2], image_shape[1], "lanczos", + "center").movedim(1, -1) + images = torch.cat(images, dim=0) + return (images,) + + +class imageBatchToImageList: + @classmethod + def INPUT_TYPES(s): + return {"required": {"image": ("IMAGE",), }} + + RETURN_TYPES = ("IMAGE",) + OUTPUT_IS_LIST = (True,) + FUNCTION = "doit" + + CATEGORY = "EasyUse/Image" + + def doit(self, image): + images = [image[i:i + 1, ...] for i in range(image.shape[0])] + return (images,) + +# 图像拆分 +class imageSplitList: + @classmethod + + def INPUT_TYPES(s): + return { + "required": { + "images": ("IMAGE",), + }, + } + + RETURN_TYPES = ("IMAGE", "IMAGE", "IMAGE",) + RETURN_NAMES = ("images", "images", "images",) + FUNCTION = "doit" + CATEGORY = "EasyUse/Image" + + def doit(self, images): + length = len(images) + new_images = ([], [], []) + if length % 3 == 0: + for index, img in enumerate(images): + if index % 3 == 0: + new_images[0].append(img) + elif (index+1) % 3 == 0: + new_images[2].append(img) + else: + new_images[1].append(img) + elif length % 2 == 0: + for index, img in enumerate(images): + if index % 2 == 0: + new_images[0].append(img) + else: + new_images[1].append(img) + return new_images + +class imageSplitGrid: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "images": ("IMAGE",), + "row": ("INT", {"default": 1,"min": 1,"max": 10,"step": 1,}), + "column": ("INT", {"default": 1,"min": 1,"max": 10,"step": 1,}), + } + } + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("images",) + FUNCTION = "doit" + CATEGORY = "EasyUse/Image" + + def crop(self, image, width, height, x, y): + x = min(x, image.shape[2] - 1) + y = min(y, image.shape[1] - 1) + to_x = width + x + to_y = height + y + img = image[:, y:to_y, x:to_x, :] + return img + + def doit(self, images, row, column): + _, height, width, _ = images.shape + sub_width = width // column + sub_height = height // row + new_images = [] + for i in range(row): + for j in range(column): + new_images.append(self.crop(images, sub_width, sub_height, j * sub_width, i * sub_height)) + + return (torch.cat(new_images, dim=0),) + +class imageSplitTiles: + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "image": ("IMAGE",), + "overlap_ratio": ("FLOAT", {"default": 0, "min": 0, "max": 0.5, "step": 0.01, }), + "overlap_offset": ("INT", {"default": 0, "min": - MAX_RESOLUTION // 2, "max": MAX_RESOLUTION // 2, "step": 1, }), + "tiles_rows": ("INT", {"default": 2, "min": 1, "max": 50, "step": 1}), + "tiles_cols": ("INT", {"default": 2, "min": 1, "max": 50, "step": 1}), + }, + "optional": { + "norm": ("BOOLEAN", {"default": True}), + } + } + + RETURN_TYPES = ("IMAGE", "MASK", "OVERLAP", "INT") + RETURN_NAMES = ("tiles", "masks", "overlap", "total") + FUNCTION = "doit" + CATEGORY = "EasyUse/Image" + + def doit(self, image, overlap_ratio, overlap_offset, tiles_rows, tiles_cols, norm=True): + height, width = image.shape[1:3] + + total = tiles_rows * tiles_cols + tile_w = int(width // tiles_cols) + tile_h = int(height // tiles_rows) + + overlap_w = int(tile_w * overlap_ratio) + overlap_offset + overlap_h = int(tile_h * overlap_ratio) + overlap_offset + + overlap_w = min(tile_w // 2, overlap_w) + overlap_h = min(tile_h // 2, overlap_h) + + if norm: + overlap_w = int(overlap_w - overlap_w % 8) + overlap_h = int(overlap_h - overlap_h % 8) + + if tiles_rows == 1: + overlap_h = 0 + if tiles_cols == 1: + overlap_w = 0 + + solid_mask_cls = ALL_NODE_CLASS_MAPPINGS['SolidMask'] + feather_mask_cls = ALL_NODE_CLASS_MAPPINGS['FeatherMask'] + + tiles, masks = [], [] + + x, y = 0, 0 + for i in range(tiles_rows): + for j in range(tiles_cols): + y1 = i * tile_h + x1 = j * tile_w + + if i > 0: + y1 -= overlap_h + if j > 0: + x1 -= overlap_w + + y2 = y1 + tile_h + overlap_h + x2 = x1 + tile_w + overlap_w + + if y2 > height: + y2 = height + y1 = y2 - tile_h - overlap_h + if x2 > width: + x2 = width + x1 = x2 - tile_w - overlap_w + + tile = image[:, y1:y2, x1:x2, :] + h = tile.shape[1] + w = tile.shape[2] + tiles.append(tile) + + fearing_left = overlap_w if overlap_w * j > 0 else 0 + fearing_top = overlap_h if overlap_h * i > 0 else 0 + fearing_right = 0 + fearing_bottom = 0 + + mask, = solid_mask_cls().solid(1, w, h) + mask, = feather_mask_cls().feather(mask, fearing_left, fearing_top, fearing_right, fearing_bottom) + masks.append(mask) + + tiles = torch.cat(tiles, dim=0) + masks = torch.cat(masks, dim=0) + + return (tiles, masks, (overlap_w, overlap_h, tile_w, tile_h, tiles_rows, tiles_cols), total) + +class imageTilesFromBatch: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "tiles": ("IMAGE",), + "masks": ("MASK",), + "overlap": ("OVERLAP",), + "index":("INT", {"default": 0, "min": 0, "max": 10000, "step": 1}), + }, + } + + RETURN_TYPES = ("IMAGE", "MASK", "INT", "INT") + RETURN_NAMES = ("image", "mask", "x", "y") + FUNCTION = "doit" + CATEGORY = "EasyUse/Image" + + def imageFromBatch(self, image, batch_index, length=1): + s_in = image + batch_index = min(s_in.shape[0] - 1, batch_index) + length = min(s_in.shape[0] - batch_index, length) + s = s_in[batch_index:batch_index + length].clone() + return s + + def maskFromBatch(self, mask, start, length=1): + if length > mask.shape[0]: + length = mask.shape[0] + start = min(start, mask.shape[0]-1) + length = min(mask.shape[0]-start, length) + return mask[start:start + length] + + def doit(self, tiles, masks, overlap, index): + tile = self.imageFromBatch(tiles, index) + mask = self.maskFromBatch(masks, index) + overlap_w, overlap_h, tile_w, tile_h, tiles_rows, tiles_cols = overlap + + x = tile_w * (index % tiles_cols) - overlap_w if (index % tiles_cols) > 0 else 0 + y = tile_h * (index // tiles_cols) - overlap_h if tiles_rows > 1 and index > tiles_cols - 1 else 0 + + return (tile, mask, x, y) + + + +class imagesSplitImage: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "images": ("IMAGE",), + } + } + + RETURN_TYPES = ("IMAGE", "IMAGE", "IMAGE", "IMAGE", "IMAGE") + RETURN_NAMES = ("image1", "image2", "image3", "image4", "image5") + FUNCTION = "split" + CATEGORY = "EasyUse/Image" + + def split(self, images,): + new_images = torch.chunk(images, len(images), dim=0) + return new_images + +class imageConcat: + @classmethod + def INPUT_TYPES(s): + return {"required": { + "image1": ("IMAGE",), + "image2": ("IMAGE",), + "direction": (['right','down','left','up',],{"default": 'right'}), + "match_image_size": ("BOOLEAN", {"default": False}), + }} + + RETURN_TYPES = ("IMAGE",) + FUNCTION = "concat" + CATEGORY = "EasyUse/Image" + + def concat(self, image1, image2, direction, match_image_size): + if image1 is None: + return (image2,) + elif image2 is None: + return (image1,) + if match_image_size: + # Convert tensor to PIL for proper aspect ratio resizing + pil_image2 = tensor2pil(image2) + if direction in ['right', 'left']: + aspect_ratio = pil_image2.width / pil_image2.height + new_height = image1.shape[1] + new_width = int(aspect_ratio * new_height) + pil_image2 = fit_resize_image(pil_image2, new_width, new_height, 'fill', Image.LANCZOS, '#000000') + else: # 'up' or 'down' + aspect_ratio = pil_image2.height / pil_image2.width + new_width = image1.shape[2] + new_height = int(aspect_ratio * new_width) + pil_image2 = fit_resize_image(pil_image2, new_width, new_height, 'fill', Image.LANCZOS, '#000000') + image2 = pil2tensor(pil_image2) + + if direction == 'right': + row = torch.cat((image1, image2), dim=2) + elif direction == 'down': + row = torch.cat((image1, image2), dim=1) + elif direction == 'left': + row = torch.cat((image2, image1), dim=2) + elif direction == 'up': + row = torch.cat((image2, image1), dim=1) + return (row,) + +# 图片背景移除 +from ..libs.utils import get_local_filepath, easySave, install_package +class imageRemBg: + @classmethod + def INPUT_TYPES(self): + return { + "required": { + "images": ("IMAGE",), + "rem_mode": (("RMBG-2.0", "RMBG-1.4", "Inspyrenet", "BEN2"), {"default": "RMBG-1.4"}), + "image_output": (["Hide", "Preview", "Save", "Hide/Save"], {"default": "Preview"}), + "save_prefix": ("STRING", {"default": "ComfyUI"}), + }, + "optional":{ + "torchscript_jit": ("BOOLEAN", {"default": False}), + "add_background": (["none", "white", "black"], {"default": "none"}), + "refine_foreground": ("BOOLEAN", {"default": False}), + }, + "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"}, + } + + RETURN_TYPES = ("IMAGE", "MASK") + RETURN_NAMES = ("image", "mask") + FUNCTION = "remove" + OUTPUT_NODE = True + + CATEGORY = "EasyUse/Image" + + + def remove(self, rem_mode, images, image_output, save_prefix, torchscript_jit=False, add_background='none', refine_foreground=False, prompt=None, extra_pnginfo=None): + new_images = list() + masks = list() + device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + if rem_mode == "RMBG-2.0": + if rem_mode in cache: + _, model = cache[rem_mode][1] + else: + repo_id = REMBG_MODELS[rem_mode]['model_url'] + model_path = os.path.join(REMBG_DIR, 'RMBG-2.0') + if not os.path.exists(model_path): + from huggingface_hub import snapshot_download + snapshot_download(repo_id=repo_id, local_dir=model_path, ignore_patterns=["*.md", "*.txt"]) + from transformers import AutoModelForImageSegmentation + model = AutoModelForImageSegmentation.from_pretrained(model_path, trust_remote_code=True) + torch.set_float32_matmul_precision('high') + model.to(device) + model.eval() + update_cache(rem_mode, 'remove_background', (False, model)) + + from torchvision import transforms + transform_image = transforms.Compose([ + transforms.Resize((1024, 1024)), + transforms.ToTensor(), + transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) + ]) + for image in images: + orig_im = tensor2pil(image) + input_image = transform_image(orig_im).unsqueeze(0).to(device) + + with torch.no_grad(): + preds = model(input_image)[-1].sigmoid().cpu() + pred = preds[0].squeeze() + + mask = transforms.ToPILImage()(pred) + mask = mask.resize(orig_im.size) + + new_im = orig_im.copy() + new_im.putalpha(mask) + + new_im_tensor = pil2tensor(new_im) + mask_tensor = pil2tensor(mask) + + new_images.append(new_im_tensor) + masks.append(mask_tensor) + + torch.cuda.empty_cache() + new_images = torch.cat(new_images, dim=0) + masks = torch.cat(masks, dim=0) + + elif rem_mode == "RMBG-1.4": + from ..modules.briaai.rembg import BriaRMBG, preprocess_image, postprocess_image + if rem_mode in cache: + _, net = cache[rem_mode][1] + else: + model_url = REMBG_MODELS[rem_mode]['model_url'] + suffix = model_url.split(".")[-1] + model_path = get_local_filepath(model_url, REMBG_DIR, rem_mode+'.'+suffix) + net = BriaRMBG() + net.load_state_dict(torch.load(model_path, map_location=device)) + net.to(device) + net.eval() + update_cache(rem_mode, 'remove_background', (False, net)) + + # prepare input + model_input_size = [1024, 1024] + for image in images: + orig_im = tensor2pil(image) + w, h = orig_im.size + image = preprocess_image(orig_im, model_input_size).to(device) + # inference + result = net(image) + result_image = postprocess_image(result[0][0], (h, w)) + mask_im = Image.fromarray(result_image) + new_im = Image.new("RGBA", mask_im.size, (0,0,0,0)) + new_im.paste(orig_im, mask=mask_im) + + new_images.append(pil2tensor(new_im)) + masks.append(pil2tensor(mask_im)) + + new_images = torch.cat(new_images, dim=0) + masks = torch.cat(masks, dim=0) + elif rem_mode == "BEN2": + if rem_mode in cache: + _, model = cache[rem_mode][1] + else: + from ..modules.ben.model import BEN_Base + model_url = REMBG_MODELS[rem_mode]['model_url'] + model_path = get_local_filepath(model_url, REMBG_DIR) + + model = BEN_Base().to(device).eval() + model.loadcheckpoints(model_path) + update_cache(rem_mode, 'remove_background', (False, model)) + + for image in images: + input_image = tensor2pil(image) + + if input_image.mode != 'RGBA': + input_image = input_image.convert("RGBA") + + mask, new_im = model.inference(input_image, refine_foreground) + + new_im_tensor = pil2tensor(new_im) + mask_tensor = pil2tensor(mask) + + new_images.append(new_im_tensor) + masks.append(mask_tensor) + + new_images = torch.cat(new_images, dim=0) + masks = torch.cat(masks, dim=0) + + elif rem_mode == "Inspyrenet": + from tqdm import tqdm + try: + from transparent_background import Remover + except: + install_package("transparent_background") + from transparent_background import Remover + + if rem_mode in cache: + _, remover = cache[rem_mode][1] + else: + remover = Remover(jit=torchscript_jit) + update_cache(rem_mode, 'remove_background', (False, remover)) + + for img in tqdm(images, "Inspyrenet Rembg"): + mid = remover.process(tensor2pil(img), type='rgba') + out = pil2tensor(mid) + new_images.append(out) + mask = out[:, :, :, 3] + masks.append(mask) + new_images = torch.cat(new_images, dim=0) + masks = torch.cat(masks, dim=0) + + if add_background != 'none': + + _layer = tensor2pil(new_images) + _canvas = Image.new('RGB', _layer.size, (255,255,255) if add_background == 'white' else (0, 0, 0)) + _canvas.paste(_layer, mask=_layer) + new_images = pil2tensor(_canvas) + + results = easySave(new_images, save_prefix, image_output, prompt, extra_pnginfo) + + if image_output in ("Hide", "Hide/Save"): + return {"ui": {}, + "result": (new_images, masks)} + + return {"ui": {"images": results}, + "result": (new_images, masks)} + +# 图像选择器 +from ..libs.chooser import wait_for_chooser +class imageChooser(PreviewImage): + @classmethod + def INPUT_TYPES(self): + return { + "required":{ + "mode": (['Always Pause', 'Keep Last Selection'], {"default": "Always Pause"}), + "preview_rescale": ("FLOAT", {"default": 1.0, "min": 0.05, "max": 1.0, "step": 0.05}), + }, + "optional": { + "images": ("IMAGE",), + }, + "hidden": {"prompt": "PROMPT", "my_unique_id": "UNIQUE_ID", "extra_pnginfo": "EXTRA_PNGINFO"}, + } + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("image",) + FUNCTION = "chooser" + OUTPUT_NODE = True + INPUT_IS_LIST = True + CATEGORY = "EasyUse/Image" + + last_ic = {} + @classmethod + def IS_CHANGED(cls, my_unique_id, **kwargs): + return cls.last_ic[my_unique_id[0]] + + def tensor_bundle(self, tensor_in: torch.Tensor, picks): + if tensor_in is not None and len(picks): + batch = tensor_in.shape[0] + return torch.cat(tuple([tensor_in[(x) % batch].unsqueeze_(0) for x in picks])).reshape( + [-1] + list(tensor_in.shape[1:])) + else: + return None + + def chooser(self, prompt=None, my_unique_id=None, extra_pnginfo=None, **kwargs): + id = my_unique_id[0] + id = id.split('.')[len(id.split('.')) - 1] if "." in id else id + + if (kwargs.get('images') is None): + return (torch.zeros(1, 1, 1, 3),) + + images_in = torch.cat(kwargs.pop('images')) + for x in kwargs: kwargs[x] = kwargs[x][0] + + try: + pnginfo = extra_pnginfo[0] + except: + pnginfo = None + + preview_rescale = kwargs.pop('preview_rescale', 1.0) + if preview_rescale < 1.0: + images_preview, = imageScaleDownBy().image_scale_down_by(images_in, preview_rescale) + else: + images_preview = images_in + result = self.save_images(images=images_preview, prompt=prompt, extra_pnginfo=pnginfo) + if "ui" in result and "images" in result['ui']: + images = result["ui"]["images"] + else: + images = [] + try: + PromptServer.instance.send_sync("easyuse-image-choose", {"id": id, "urls": images}) + except Exception as e: + pass + + # 获取上次选择 + mode = kwargs.pop('mode', 'Always Pause') + return wait_for_chooser(id, images_in, mode) + +class imageColorMatch(PreviewImage): + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "image_ref": ("IMAGE",), + "image_target": ("IMAGE",), + "method": (['wavelet', 'adain', 'mkl', 'hm', 'reinhard', 'mvgd', 'hm-mvgd-hm', 'hm-mkl-hm'],), + "image_output": (["Hide", "Preview", "Save", "Hide/Save"], {"default": "Preview"}), + "save_prefix": ("STRING", {"default": "ComfyUI"}), + }, + "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"}, + } + + CATEGORY = "EasyUse/Image" + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("image",) + OUTPUT_NODE = True + FUNCTION = "color_match" + + def color_match(self, image_ref, image_target, method, image_output, save_prefix, prompt=None, extra_pnginfo=None): + if method in ["wavelet", "adain"]: + result_images = wavelet_color_fix(tensor2pil(image_target), tensor2pil(image_ref)) if method == 'wavelet' else adain_color_fix(tensor2pil(image_target), tensor2pil(image_ref)) + new_images = pil2tensor(result_images) + else: + try: + from color_matcher import ColorMatcher + except: + install_package("color-matcher") + from color_matcher import ColorMatcher + image_ref = image_ref.cpu() + image_target = image_target.cpu() + batch_size = image_target.size(0) + out = [] + images_target = image_target.squeeze() + images_ref = image_ref.squeeze() + + image_ref_np = images_ref.numpy() + images_target_np = images_target.numpy() + if image_ref.size(0) > 1 and image_ref.size(0) != batch_size: + raise ValueError("ColorMatch: Use either single reference image or a matching batch of reference images.") + cm = ColorMatcher() + for i in range(batch_size): + image_target_np = images_target_np if batch_size == 1 else images_target[i].numpy() + image_ref_np_i = image_ref_np if image_ref.size(0) == 1 else images_ref[i].numpy() + try: + image_result = cm.transfer(src=image_target_np, ref=image_ref_np_i, method=method) + except BaseException as e: + print(f"Error occurred during transfer: {e}") + break + out.append(torch.from_numpy(image_result)) + + new_images = torch.stack(out, dim=0).to(torch.float32) + + results = easySave(new_images, save_prefix, image_output, prompt, extra_pnginfo) + + if image_output in ("Hide", "Hide/Save"): + return {"ui": {}, + "result": (new_images,)} + + return {"ui": {"images": results}, + "result": (new_images,)} + +class imageDetailTransfer: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "target": ("IMAGE",), + "source": ("IMAGE",), + "mode": (["add", "multiply", "screen", "overlay", "soft_light", "hard_light", "color_dodge", "color_burn", "difference", "exclusion", "divide",],{"default": "add"}), + "blur_sigma": ("FLOAT", {"default": 1.0, "min": 0.1, "max": 100.0, "step": 0.01}), + "blend_factor": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.001, "round": 0.001}), + "image_output": (["Hide", "Preview", "Save", "Hide/Save"], {"default": "Preview"}), + "save_prefix": ("STRING", {"default": "ComfyUI"}), + }, + "optional": { + "mask": ("MASK",), + }, + "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"}, + } + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("image",) + OUTPUT_NODE = True + FUNCTION = "transfer" + CATEGORY = "EasyUse/Image" + + + + def transfer(self, target, source, mode, blur_sigma, blend_factor, image_output, save_prefix, mask=None, prompt=None, extra_pnginfo=None): + batch_size, height, width, _ = source.shape + device = comfy.model_management.get_torch_device() + target_tensor = target.permute(0, 3, 1, 2).clone().to(device) + source_tensor = source.permute(0, 3, 1, 2).clone().to(device) + + if target.shape[1:] != source.shape[1:]: + target_tensor = comfy.utils.common_upscale(target_tensor, width, height, "bilinear", "disabled") + if mask is not None and target.shape[1:] != mask.shape[1:]: + mask = mask.unsqueeze(1) + mask = F.interpolate(mask, size=(height, width), mode="bilinear") + mask = mask.squeeze(1) + + if source.shape[0] < batch_size: + source = source[0].unsqueeze(0).repeat(batch_size, 1, 1, 1) + + kernel_size = int(6 * int(blur_sigma) + 1) + + gaussian_blur = GaussianBlur(kernel_size=(kernel_size, kernel_size), sigma=(blur_sigma, blur_sigma)) + + blurred_target = gaussian_blur(target_tensor) + blurred_source = gaussian_blur(source_tensor) + + if mode == "add": + new_image = (source_tensor - blurred_source) + blurred_target + elif mode == "multiply": + new_image = source_tensor * blurred_target + elif mode == "screen": + new_image = 1 - (1 - source_tensor) * (1 - blurred_target) + elif mode == "overlay": + new_image = torch.where(blurred_target < 0.5, 2 * source_tensor * blurred_target, + 1 - 2 * (1 - source_tensor) * (1 - blurred_target)) + elif mode == "soft_light": + new_image = (1 - 2 * blurred_target) * source_tensor ** 2 + 2 * blurred_target * source_tensor + elif mode == "hard_light": + new_image = torch.where(source_tensor < 0.5, 2 * source_tensor * blurred_target, + 1 - 2 * (1 - source_tensor) * (1 - blurred_target)) + elif mode == "difference": + new_image = torch.abs(blurred_target - source_tensor) + elif mode == "exclusion": + new_image = 0.5 - 2 * (blurred_target - 0.5) * (source_tensor - 0.5) + elif mode == "color_dodge": + new_image = blurred_target / (1 - source_tensor) + elif mode == "color_burn": + new_image = 1 - (1 - blurred_target) / source_tensor + elif mode == "divide": + new_image = (source_tensor / blurred_source) * blurred_target + else: + new_image = source_tensor + + new_image = torch.lerp(target_tensor, new_image, blend_factor) + if mask is not None: + mask = mask.to(device) + if mask.dim() == 3: # (B,H,W) batch/video mask -> (B,1,H,W) so it broadcasts over channels + mask = mask.unsqueeze(1) + new_image = torch.lerp(target_tensor, new_image, mask) + new_image = torch.clamp(new_image, 0, 1) + new_image = new_image.permute(0, 2, 3, 1).cpu().float() + + results = easySave(new_image, save_prefix, image_output, prompt, extra_pnginfo) + + if image_output in ("Hide", "Hide/Save"): + return {"ui": {}, + "result": (new_image,)} + + return {"ui": {"images": results}, + "result": (new_image,)} + +# 图像反推 +from ..libs.image import ci +class imageInterrogator: + @classmethod + def INPUT_TYPES(self): + return { + "required": { + "image": ("IMAGE",), + "mode": (['fast','classic','best','negative'],), + "use_lowvram": ("BOOLEAN", {"default": True}), + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("prompt",) + FUNCTION = "interrogate" + CATEGORY = "EasyUse/Image" + OUTPUT_NODE = False + OUTPUT_IS_LIST = (True,) + + def interrogate(self, image, mode, use_lowvram=False): + prompt = ci.image_to_prompt(image, mode, low_vram=use_lowvram) + return (prompt,) + +# 人类分割器 +class humanSegmentation: + + @classmethod + def INPUT_TYPES(cls): + return { + "required":{ + "image": ("IMAGE",), + "method": (["selfie_multiclass_256x256", "human_parsing_lip", "human_parts (deeplabv3p)", "segformer_b3_clothes", "segformer_b3_fashion", "face_parsing"],), + "confidence": ("FLOAT", {"default": 0.4, "min": 0.05, "max": 0.95, "step": 0.01},), + "crop_multi": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 10.0, "step": 0.001},), + "mask_components":( + "EASY_COMBO",{ + "options": [{'label':'Background','value':0}], + "multi_select": { + "placeholder": "select mask components", + "chip": True, + "max_selected_labels": 4, + } + } + ) + }, + "hidden": { + "prompt": "PROMPT", + "my_unique_id": "UNIQUE_ID", + } + } + + RETURN_TYPES = ("IMAGE", "MASK", "BBOX") + RETURN_NAMES = ("image", "mask", "bbox") + FUNCTION = "parsing" + CATEGORY = "EasyUse/Segmentation" + + def get_mediapipe_image(self, image: Image): + import mediapipe as mp + # Convert image to NumPy array + numpy_image = np.asarray(image) + image_format = mp.ImageFormat.SRGB + # Convert BGR to RGB (if necessary) + if numpy_image.shape[-1] == 4: + image_format = mp.ImageFormat.SRGBA + elif numpy_image.shape[-1] == 3: + image_format = mp.ImageFormat.SRGB + numpy_image = cv2.cvtColor(numpy_image, cv2.COLOR_BGR2RGB) + return mp.Image(image_format=image_format, data=numpy_image) + + def parsing(self, image, confidence, method, crop_multi, mask_components, prompt=None, my_unique_id=None): + if isinstance(mask_components, str): + mask_components = [int(x) for x in mask_components.split(',') if x] + else: + mask_components = mask_components if mask_components else [] + + if method == 'selfie_multiclass_256x256': + try: + import mediapipe as mp + except: + install_package("mediapipe") + import mediapipe as mp + + from functools import reduce + + if method in cache: + _, model_asset_buffer = cache["selfie_multiclass_256x256"][1] + else: + model_path = get_local_filepath(MEDIAPIPE_MODELS['selfie_multiclass_256x256']['model_url'], MEDIAPIPE_DIR) + model_asset_buffer = None + with open(model_path, "rb") as f: + model_asset_buffer = f.read() + update_cache(method, 'human_segmentation', (False, model_asset_buffer)) + image_segmenter_base_options = mp.tasks.BaseOptions(model_asset_buffer=model_asset_buffer) + options = mp.tasks.vision.ImageSegmenterOptions( + base_options=image_segmenter_base_options, + running_mode=mp.tasks.vision.RunningMode.IMAGE, + output_category_mask=True) + # Create the image segmenter + ret_images = [] + ret_masks = [] + + if len(mask_components) == 0: + return (image, torch.zeros_like(image[:, :, :, 0:1]), torch.tensor([0,0,0,0])) + + with mp.tasks.vision.ImageSegmenter.create_from_options(options) as segmenter: + for img in image: + _image = torch.unsqueeze(img, 0) + orig_image = tensor2pil(_image).convert('RGB') + # Convert the Tensor to a PIL image + i = 255. * img.cpu().numpy() + image_pil = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8)) + # create our foreground and background arrays for storing the mask results + mask_background_array = np.zeros((image_pil.size[0], image_pil.size[1], 4), dtype=np.uint8) + mask_background_array[:] = (0, 0, 0, 255) + mask_foreground_array = np.zeros((image_pil.size[0], image_pil.size[1], 4), dtype=np.uint8) + mask_foreground_array[:] = (255, 255, 255, 255) + # Retrieve the masks for the segmented image + media_pipe_image = self.get_mediapipe_image(image=image_pil) + segmented_masks = segmenter.segment(media_pipe_image) + masks = [] + for i, com in enumerate(mask_components): + masks.append(segmented_masks.confidence_masks[com]) + + image_data = media_pipe_image.numpy_view() + image_shape = image_data.shape + # convert the image shape from "rgb" to "rgba" aka add the alpha channel + if image_shape[-1] == 3: + image_shape = (image_shape[0], image_shape[1], 4) + mask_background_array = np.zeros(image_shape, dtype=np.uint8) + mask_background_array[:] = (0, 0, 0, 255) + mask_foreground_array = np.zeros(image_shape, dtype=np.uint8) + mask_foreground_array[:] = (255, 255, 255, 255) + mask_arrays = [] + if len(masks) == 0: + mask_arrays.append(mask_background_array) + else: + for i, mask in enumerate(masks): + mask_2d = mask.numpy_view() + if mask_2d.ndim == 3 and mask_2d.shape[2] == 1: + mask_2d = mask_2d.squeeze(axis=2) + elif mask_2d.ndim != 2: + raise ValueError(f"Unexpected mask shape: {mask_2d.shape}") + condition = np.stack((mask_2d,) * image_shape[-1], axis=-1) > confidence + if condition.ndim == 4 and condition.shape[2] == 1: + condition = condition.squeeze(2) + mask_array = np.where(condition, mask_foreground_array, mask_background_array) + mask_arrays.append(mask_array) + # Merge our masks taking the maximum from each + merged_mask_arrays = reduce(np.maximum, mask_arrays) + # Create the image + mask_image = Image.fromarray(merged_mask_arrays) + # convert PIL image to tensor image + tensor_mask = mask_image.convert("RGB") + tensor_mask = np.array(tensor_mask).astype(np.float32) / 255.0 + tensor_mask = torch.from_numpy(tensor_mask)[None,] + _mask = tensor_mask.squeeze(3)[..., 0] + + _mask = tensor2pil(tensor_mask).convert('L') + + ret_image = RGB2RGBA(orig_image, _mask) + ret_images.append(pil2tensor(ret_image)) + ret_masks.append(image2mask(_mask)) + + output_image = torch.cat(ret_images, dim=0) + mask = torch.cat(ret_masks, dim=0) + + elif method == "human_parsing_lip": + if method in cache: + _, parsing = cache[method][1] + else: + from ..modules.human_parsing.run_parsing import HumanParsing + onnx_path = os.path.join(folder_paths.models_dir, 'onnx') + model_path = get_local_filepath(HUMANPARSING_MODELS['parsing_lip']['model_url'], onnx_path) + parsing = HumanParsing(model_path=model_path) + update_cache(method, 'human_segmentation', (False, parsing)) + + model_image = image.squeeze(0) + model_image = model_image.permute((2, 0, 1)) + model_image = to_pil_image(model_image) + + map_image, mask = parsing(model_image, mask_components) + + mask = mask[:, :, :, 0] + + alpha = 1.0 - mask + + try: + output_image, = JoinImageWithAlpha().execute(image, alpha) + except: + output_image, = JoinImageWithAlpha().join_image_with_alpha(image, alpha) + + + elif method == "human_parts (deeplabv3p)": + if method in cache: + _, parsing = cache[method][1] + else: + from ..modules.human_parsing.run_parsing import HumanParts + onnx_path = os.path.join(folder_paths.models_dir, 'onnx') + human_parts_path = os.path.join(onnx_path, 'human-parts') + model_path = get_local_filepath(HUMANPARSING_MODELS['human-parts']['model_url'], human_parts_path) + parsing = HumanParts(model_path=model_path) + update_cache(method, 'human_segmentation', (False, parsing)) + + ret_images = [] + ret_masks = [] + for img in image: + mask, = parsing(img, mask_components) + _mask = tensor2pil(mask).convert('L') + + ret_image = RGB2RGBA(tensor2pil(img).convert('RGB'), _mask.convert('L')) + ret_images.append(pil2tensor(ret_image)) + ret_masks.append(image2mask(_mask)) + + output_image = torch.cat(ret_images, dim=0) + mask = torch.cat(ret_masks, dim=0) + + elif method in ["segformer_b3_clothes", "segformer_b3_fashion", "face_parsing"]: + from transformers import SegformerImageProcessor, AutoModelForSemanticSegmentation + + # 分割 + def get_segmentation_from_model(tensor_image, model, processor): + cloth = tensor2pil(tensor_image) + inputs = processor(images=cloth, return_tensors="pt") + outputs = model(**inputs) + logits = outputs.logits.cpu() + upsampled_logits = F.interpolate(logits, size=cloth.size[::-1], mode="bilinear", + align_corners=False) + pred_seg = upsampled_logits.argmax(dim=1)[0].numpy() + return pred_seg, cloth + + + if method in cache: + _, (processor, model) = cache[method][1] + else: + model_folder_path = os.path.join(folder_paths.models_dir, method) + if os.path.exists(model_folder_path): + print(f"Start to load existing model...") + else: + from huggingface_hub import snapshot_download + PromptServer.instance.send_sync("easyuse-toast", {"content": f"Model not found locally. Downloading {method}...", "type": 'loading', "duration": 10000}) + print(f"Model not found locally. Downloading {method}...") + model_path_cache = os.path.join(folder_paths.models_dir, "cache-"+method) + snapshot_download( + repo_id=HUMANPARSING_MODELS[method]['model_name'], + local_dir=model_path_cache, + local_dir_use_symlinks=False, + resume_download=True + ) + shutil.move(model_path_cache, model_folder_path) + print(f"Model downloaded to {model_folder_path}...") + try: + model_folder_path = os.path.normpath(folder_paths.folder_names_and_paths[method][0][0]) + except: + pass + + processor = SegformerImageProcessor.from_pretrained(model_folder_path) + model = AutoModelForSemanticSegmentation.from_pretrained(model_folder_path) + update_cache(method, 'human_segmentation', (False, (processor, model))) + + ret_images = [] + ret_masks = [] + + if method == "face_parsing": + import matplotlib + import torchvision.transforms as T + transform = ToPILImage() + colormap = matplotlib.colormaps['viridis'] + device = model.device + results = [] + images = [] + for img in image: + size = img.shape[:2] + inputs = processor(images=transform(img.permute(2, 0, 1)), return_tensors="pt") + inputs = {k: v.to(device) for k, v in inputs.items()} + outputs = model(**inputs) + logits = outputs.logits + upsampled_logits = F.interpolate( + logits, + size=size, + mode="bilinear", + align_corners=False) + + pred_seg = upsampled_logits.argmax(dim=1)[0] + pred_seg_np = pred_seg.cpu().detach().numpy().astype(np.uint8) + results.append(torch.tensor(pred_seg_np)) + + results_out = torch.stack(results, dim=0) + for img, result_item in zip(image, results_out): + mask = torch.zeros(result_item.shape, dtype=torch.uint8) + for i in mask_components: + mask = mask | torch.where(result_item == i, 1, 0) + + # 将mask转换为numpy数组,并确保数据类型正确 + mask_np = (mask * 255).numpy().astype(np.uint8) + _mask = Image.fromarray(mask_np) + + # 处理图像输出 + ret_image = RGB2RGBA(tensor2pil(img).convert('RGB'), _mask.convert('L')) + ret_images.append(pil2tensor(ret_image)) + ret_masks.append(image2mask(_mask)) + + else: + for img in image: + pred_seg, cloth = get_segmentation_from_model(img, model, processor) + i = torch.unsqueeze(img, 0) + i = pil2tensor(tensor2pil(i).convert('RGB')) + + mask = np.isin(pred_seg, mask_components).astype(np.uint8) + _mask = Image.fromarray(mask * 255) + + ret_image = RGB2RGBA(tensor2pil(img).convert('RGB'), _mask.convert('L')) + ret_images.append(pil2tensor(ret_image)) + ret_masks.append(image2mask(_mask)) + + output_image = torch.cat(ret_images, dim=0) + mask = torch.cat(ret_masks, dim=0) + + # use crop + bbox = [[0, 0, 0, 0]] + if crop_multi > 0.0: + output_image, mask, bbox = imageCropFromMask().crop(output_image, mask, crop_multi, crop_multi, 1.0) + + return (output_image, mask, bbox) + +class imageCropFromMask: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "image": ("IMAGE",), + "mask": ("MASK",), + "image_crop_multi": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.001}), + "mask_crop_multi": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.001}), + "bbox_smooth_alpha": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}), + }, + } + + RETURN_TYPES = ("IMAGE", "MASK", "BBOX",) + RETURN_NAMES = ("crop_image", "crop_mask", "bbox",) + FUNCTION = "crop" + CATEGORY = "EasyUse/Image" + + def smooth_bbox_size(self, prev_bbox_size, curr_bbox_size, alpha): + if alpha == 0: + return prev_bbox_size + return round(alpha * curr_bbox_size + (1 - alpha) * prev_bbox_size) + + def smooth_center(self, prev_center, curr_center, alpha=0.5): + if alpha == 0: + return prev_center + return ( + round(alpha * curr_center[0] + (1 - alpha) * prev_center[0]), + round(alpha * curr_center[1] + (1 - alpha) * prev_center[1]) + ) + + def image2mask(self, image): + return image[:, :, :, 0] + + def mask2image(self, mask): + return mask.reshape((-1, 1, mask.shape[-2], mask.shape[-1])).movedim(1, -1).expand(-1, -1, -1, 3) + + def cropimage(self, original_images, masks, crop_size_mult, bbox_smooth_alpha): + + bounding_boxes = [] + cropped_images = [] + + self.max_bbox_width = 0 + self.max_bbox_height = 0 + + # First, calculate the maximum bounding box size across all masks + curr_max_bbox_width = 0 + curr_max_bbox_height = 0 + for mask in masks: + _mask = tensor2pil(mask) + non_zero_indices = np.nonzero(np.array(_mask)) + min_x, max_x = np.min(non_zero_indices[1]), np.max(non_zero_indices[1]) + min_y, max_y = np.min(non_zero_indices[0]), np.max(non_zero_indices[0]) + width = max_x - min_x + height = max_y - min_y + curr_max_bbox_width = max(curr_max_bbox_width, width) + curr_max_bbox_height = max(curr_max_bbox_height, height) + + # Smooth the changes in the bounding box size + self.max_bbox_width = self.smooth_bbox_size(self.max_bbox_width, curr_max_bbox_width, bbox_smooth_alpha) + self.max_bbox_height = self.smooth_bbox_size(self.max_bbox_height, curr_max_bbox_height, bbox_smooth_alpha) + + # Apply the crop size multiplier + self.max_bbox_width = round(self.max_bbox_width * crop_size_mult) + self.max_bbox_height = round(self.max_bbox_height * crop_size_mult) + bbox_aspect_ratio = self.max_bbox_width / self.max_bbox_height + + # Then, for each mask and corresponding image... + for i, (mask, img) in enumerate(zip(masks, original_images)): + _mask = tensor2pil(mask) + non_zero_indices = np.nonzero(np.array(_mask)) + min_x, max_x = np.min(non_zero_indices[1]), np.max(non_zero_indices[1]) + min_y, max_y = np.min(non_zero_indices[0]), np.max(non_zero_indices[0]) + + # Calculate center of bounding box + center_x = np.mean(non_zero_indices[1]) + center_y = np.mean(non_zero_indices[0]) + curr_center = (round(center_x), round(center_y)) + + # If this is the first frame, initialize prev_center with curr_center + if not hasattr(self, 'prev_center'): + self.prev_center = curr_center + + # Smooth the changes in the center coordinates from the second frame onwards + if i > 0: + center = self.smooth_center(self.prev_center, curr_center, bbox_smooth_alpha) + else: + center = curr_center + + # Update prev_center for the next frame + self.prev_center = center + + # Create bounding box using max_bbox_width and max_bbox_height + half_box_width = round(self.max_bbox_width / 2) + half_box_height = round(self.max_bbox_height / 2) + min_x = max(0, center[0] - half_box_width) + max_x = min(img.shape[1], center[0] + half_box_width) + min_y = max(0, center[1] - half_box_height) + max_y = min(img.shape[0], center[1] + half_box_height) + + # Append bounding box coordinates + bounding_boxes.append((min_x, min_y, max_x - min_x, max_y - min_y)) + + # Crop the image from the bounding box + cropped_img = img[min_y:max_y, min_x:max_x, :] + + # Calculate the new dimensions while maintaining the aspect ratio + new_height = min(cropped_img.shape[0], self.max_bbox_height) + new_width = round(new_height * bbox_aspect_ratio) + + # Resize the image + resize_transform = Resize((new_height, new_width)) + resized_img = resize_transform(cropped_img.permute(2, 0, 1)) + + # Perform the center crop to the desired size + crop_transform = CenterCrop((self.max_bbox_height, self.max_bbox_width)) # swap the order here if necessary + cropped_resized_img = crop_transform(resized_img) + + cropped_images.append(cropped_resized_img.permute(1, 2, 0)) + + return cropped_images, bounding_boxes + + def crop(self, image, mask, image_crop_multi, mask_crop_multi, bbox_smooth_alpha): + cropped_images, bounding_boxes = self.cropimage(image, mask, image_crop_multi, bbox_smooth_alpha) + cropped_mask_image, _ = self.cropimage(self.mask2image(mask), mask, mask_crop_multi, bbox_smooth_alpha) + + cropped_image_out = torch.stack(cropped_images, dim=0) + cropped_mask_out = torch.stack(cropped_mask_image, dim=0) + + return (cropped_image_out, cropped_mask_out[:, :, :, 0], bounding_boxes) + + +class imageUncropFromBBOX: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "original_image": ("IMAGE",), + "crop_image": ("IMAGE",), + "bbox": ("BBOX",), + "border_blending": ("FLOAT", {"default": 0.25, "min": 0.0, "max": 1.0, "step": 0.01},), + "use_square_mask": ("BOOLEAN", {"default": True}), + }, + "optional":{ + "optional_mask": ("MASK",) + } + } + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("image",) + FUNCTION = "uncrop" + CATEGORY = "EasyUse/Image" + + def bbox_check(self, bbox, target_size=None): + if not target_size: + return bbox + + new_bbox = ( + bbox[0], + bbox[1], + min(target_size[0] - bbox[0], bbox[2]), + min(target_size[1] - bbox[1], bbox[3]), + ) + return new_bbox + + def bbox_to_region(self, bbox, target_size=None): + bbox = self.bbox_check(bbox, target_size) + return (bbox[0], bbox[1], bbox[0] + bbox[2], bbox[1] + bbox[3]) + + def uncrop(self, original_image, crop_image, bbox, border_blending, use_square_mask, optional_mask=None): + def inset_border(image, border_width=20, border_color=(0)): + width, height = image.size + bordered_image = Image.new(image.mode, (width, height), border_color) + bordered_image.paste(image, (0, 0)) + draw = ImageDraw.Draw(bordered_image) + draw.rectangle((0, 0, width - 1, height - 1), outline=border_color, width=border_width) + return bordered_image + + if len(original_image) != len(crop_image): + raise ValueError( + f"The number of original_images ({len(original_image)}) and cropped_images ({len(crop_image)}) should be the same") + + # Ensure there are enough bboxes, but drop the excess if there are more bboxes than images + if len(bbox) > len(original_image): + print(f"Warning: Dropping excess bounding boxes. Expected {len(original_image)}, but got {len(bbox)}") + bbox = bbox[:len(original_image)] + elif len(bbox) < len(original_image): + raise ValueError("There should be at least as many bboxes as there are original and cropped images") + + + out_images = [] + + for i in range(len(original_image)): + img = tensor2pil(original_image[i]) + crop = tensor2pil(crop_image[i]) + _bbox = bbox[i] + + bb_x, bb_y, bb_width, bb_height = _bbox + paste_region = self.bbox_to_region((bb_x, bb_y, bb_width, bb_height), img.size) + + # rescale the crop image to fit the paste_region + crop = crop.resize((round(paste_region[2] - paste_region[0]), round(paste_region[3] - paste_region[1]))) + crop_img = crop.convert("RGB") + + # border blending + if border_blending > 1.0: + border_blending = 1.0 + elif border_blending < 0.0: + border_blending = 0.0 + + blend_ratio = (max(crop_img.size) / 2) * float(border_blending) + blend = img.convert("RGBA") + + if use_square_mask: + mask = Image.new("L", img.size, 0) + mask_block = Image.new("L", (paste_region[2] - paste_region[0], paste_region[3] - paste_region[1]), 255) + mask_block = inset_border(mask_block, round(blend_ratio / 2), (0)) + mask.paste(mask_block, paste_region) + else: + if optional_mask is None: + raise ValueError("optional_mask is required when use_square_mask is False") + original_mask = tensor2pil(optional_mask) + original_mask = original_mask.resize((paste_region[2] - paste_region[0], paste_region[3] - paste_region[1])) + mask = Image.new("L", img.size, 0) + mask.paste(original_mask, paste_region) + + mask = mask.filter(ImageFilter.BoxBlur(radius=blend_ratio / 4)) + mask = mask.filter(ImageFilter.GaussianBlur(radius=blend_ratio / 4)) + + blend.paste(crop_img, paste_region) + blend.putalpha(mask) + + img = Image.alpha_composite(img.convert("RGBA"), blend) + out_images.append(img.convert("RGB")) + + output_images = torch.cat([pil2tensor(img) for img in out_images], dim=0) + return (output_images,) + + + +import cv2 +import base64 +class loadImageBase64: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "base64_data": ("STRING", {"default": ""}), + "image_output": (["Hide", "Preview", "Save", "Hide/Save"], {"default": "Preview"}), + "save_prefix": ("STRING", {"default": "ComfyUI"}), + }, + "optional": { + + }, + "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"}, + } + + RETURN_TYPES = ("IMAGE", "MASK") + OUTPUT_NODE = True + FUNCTION = "load_image" + CATEGORY = "EasyUse/Image/LoadImage" + + def convert_color(self, image,): + if len(image.shape) > 2 and image.shape[2] >= 4: + return cv2.cvtColor(image, cv2.COLOR_BGRA2RGB) + return cv2.cvtColor(image, cv2.COLOR_BGR2RGB) + + def load_image(self, base64_data, image_output, save_prefix, prompt=None, extra_pnginfo=None): + nparr = np.frombuffer(base64.b64decode(base64_data), np.uint8) + + result = cv2.imdecode(nparr, cv2.IMREAD_UNCHANGED) + channels = cv2.split(result) + if len(channels) > 3: + mask = channels[3].astype(np.float32) / 255.0 + mask = torch.from_numpy(mask) + else: + mask = torch.ones(channels[0].shape, dtype=torch.float32, device="cpu") + + result = self.convert_color(result) + result = result.astype(np.float32) / 255.0 + new_images = torch.from_numpy(result)[None,] + + results = easySave(new_images, save_prefix, image_output, None, None) + mask = mask.unsqueeze(0) + + if image_output in ("Hide", "Hide/Save"): + return {"ui": {}, + "result": (new_images, mask)} + + return {"ui": {"images": results}, + "result": (new_images, mask)} + +class imageToBase64: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "image": ("IMAGE",), + }, + } + + RETURN_TYPES = ("STRING",) + FUNCTION = "to_base64" + CATEGORY = "EasyUse/Image" + OUTPUT_NODE = True + + def to_base64(self, image, ): + import base64 + from io import BytesIO + + # 将张量图像转换为PIL图像 + pil_image = tensor2pil(image) + + buffered = BytesIO() + pil_image.save(buffered, format="PNG") + image_bytes = buffered.getvalue() + + base64_str = base64.b64encode(image_bytes).decode("utf-8") + return {"result": (base64_str,)} + +class removeLocalImage: + + def __init__(self): + self.hasFile = False + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "any": (any_type,), + "file_name": ("STRING",{"default":""}), + }, + } + + RETURN_TYPES = () + OUTPUT_NODE = True + FUNCTION = "remove" + CATEGORY = "EasyUse/Image" + + + + def remove(self, any, file_name): + self.hasFile = False + def listdir(path, dir_name=''): + for file in os.listdir(path): + file_path = os.path.join(path, file) + if os.path.isdir(file_path): + dir_name = os.path.basename(file_path) + listdir(file_path, dir_name) + else: + file = os.path.join(dir_name, file) + name_without_extension, file_extension = os.path.splitext(file) + if name_without_extension == file_name or file == file_name: + os.remove(os.path.join(folder_paths.input_directory, file)) + self.hasFile = True + break + + listdir(folder_paths.input_directory, '') + + if self.hasFile: + PromptServer.instance.send_sync("easyuse-toast", {"content": "Removed SuccessFully", "type":'success'}) + else: + PromptServer.instance.send_sync("easyuse-toast", {"content": "Removed Failed", "type": 'error'}) + return () + +try: + from comfy_execution.graph_utils import GraphBuilder, is_link +except: + GraphBuilder = None +class loadImagesForLoop: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "directory": ("STRING", {"default": ""}), + }, + "optional": { + "start_index": ("INT", {"default": 0, "min": 0, "step": 1}), + "limit": ("INT", {"default":-1, "min":-1, "max": 10000}), + "initial_value1": (any_type,), + "initial_value2": (any_type,), + }, + "hidden": { + "initial_value0": (any_type,), + "prompt": "PROMPT", + "extra_pnginfo": "EXTRA_PNGINFO", + "unique_id": "UNIQUE_ID" + } + } + + RETURN_TYPES = ByPassTypeTuple(tuple(["FLOW_CONTROL", "INT", "IMAGE", "MASK", "STRING", any_type, any_type])) + RETURN_NAMES = ByPassTypeTuple(tuple(["flow", "index", "image", "mask", "name", "value1", "value2"])) + + FUNCTION = "load_images" + + CATEGORY = "image" + + def load_images(self, directory: str, start_index: int = 0, limit: int =-1, prompt=None, extra_pnginfo=None, unique_id=None, **kwargs): + print(directory) + if not os.path.isdir(directory): + raise FileNotFoundError(f"Directory '{directory}' cannot be found.") + + dir_files = os.listdir(directory) + if len(dir_files) == 0: + raise FileNotFoundError(f"No files in directory '{directory}'.") + + # Filter files by extension + valid_extensions = ['.jpg', '.jpeg', '.png', '.webp'] + dir_files = [f for f in dir_files if any(f.lower().endswith(ext) for ext in valid_extensions)] + + dir_files = sorted(dir_files) + dir_files = [os.path.join(directory, x) for x in dir_files] + + graph = GraphBuilder() + index = 0 + # unique_id = unique_id.split('.')[len(unique_id.split('.')) - 1] if "." in unique_id else unique_id + # update_cache('forloop' + str(unique_id), 'forloop', total) + if "initial_value0" in kwargs: + index = kwargs["initial_value0"] + # start at start_index + image_path = dir_files[start_index+index] + + name = os.path.splitext(os.path.basename(image_path))[0] + + i = Image.open(image_path) + i = ImageOps.exif_transpose(i) + image = i.convert("RGB") + image = np.array(image).astype(np.float32) / 255.0 + image = torch.from_numpy(image)[None,] + + if 'A' in i.getbands(): + mask = np.array(i.getchannel('A')).astype(np.float32) / 255.0 + mask = 1. - torch.from_numpy(mask) + else: + mask = torch.zeros((64, 64), dtype=torch.float32, device="cpu") + + while_open = graph.node("easy whileLoopStart", condition=True, initial_value0=index, initial_value1=kwargs.get('initial_value1',None), initial_value2=kwargs.get('initial_value2',None)) + outputs = [kwargs.get('initial_value1',None), kwargs.get('initial_value2',None)] + + return { + "result": tuple(["stub", index, image, mask, name] + outputs), + "expand": graph.finalize(), + } + +class makeImageForICRepaint: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "image_1": ("IMAGE",), + "direction": (["top-bottom", "left-right"], {"default": "left-right"}), + "pixels": ("INT", {"default": 0, "max": MAX_RESOLUTION, "min": 0, "step": 8, "tooltip": "The pixel of the output image is not set when it is 0"}), + "method": (["uniform height", "uniform width", "auto"],{"default": "auto"}), + }, + "optional": { + "image_2": ("IMAGE",), + "mask_1": ("MASK",), + "mask_2": ("MASK",), + }, + } + + DESCRIPTION = "make Image for ICLora to Re-paint" + CATEGORY = "EasyUse/Image" + FUNCTION = "make" + + RETURN_TYPES = ("IMAGE", "MASK", "MASK", "INT", "INT", "INT", "INT") + RETURN_NAMES = ("image", "mask", "context_mask", "width", "height", "x", "y") + + def fillMask(self, width, height, mask, box=(0, 0), color=0): + bg = Image.new("L", (width, height), color) + bg.paste(mask, box, mask) + return bg + + def emptyImage(self, width, height, batch_size=1, color=0): + r = torch.full([batch_size, height, width, 1], ((color >> 16) & 0xFF) / 0xFF) + g = torch.full([batch_size, height, width, 1], ((color >> 8) & 0xFF) / 0xFF) + b = torch.full([batch_size, height, width, 1], ((color) & 0xFF) / 0xFF) + return torch.cat((r, g, b), dim=-1) + + def resize_image_and_mask(self, image, mask, w, h ,fit='fill'): + ret_images = [] + ret_masks = [] + _mask = Image.new('L', size=(w, h), color='black') + _image = Image.new('RGB', size=(w, h), color='black') + if image is not None and len(image) > 0: + for i in image: + _image = tensor2pil(i).convert('RGB') + _image = fit_resize_image(_image, w, h, fit, Image.LANCZOS, '#000000') + ret_images.append(pil2tensor(_image)) + if mask is not None and len(mask) > 0: + for m in mask: + _mask = tensor2pil(m).convert('L') + _mask = fit_resize_image(_mask, w, h, fit, Image.LANCZOS).convert('L') + ret_masks.append(image2mask(_mask)) + + if len(ret_images) > 0 and len(ret_masks) > 0: + return (torch.cat(ret_images, dim=0), torch.cat(ret_masks, dim=0),) + elif len(ret_images) > 0 and len(ret_masks) == 0: + return (torch.cat(ret_images, dim=0), None,) + elif len(ret_images) == 0 and len(ret_masks) > 0: + return (None, torch.cat(ret_masks, dim=0),) + else: + return (None, None) + + def make(self, image_1, direction, pixels, method, image_2=None, mask_1=None, mask_2=None): + if image_2 is None: + image_2 = self.emptyImage(image_1.shape[2], image_1.shape[1]) + mask_2 = torch.full((1, image_1.shape[1], image_1.shape[2]), 1, dtype=torch.float32, device="cpu") + + elif image_2 is not None and mask_2 is None: + mask_2 = torch.full((1, image_2.shape[1], image_2.shape[2]), 1, dtype=torch.float32, device="cpu") + + if pixels > 0: + _, img2_h, img2_w, _ = image_2.shape + if method == "uniform height": + h = pixels + w = int(img2_w * (pixels / img2_h)) + elif method == "uniform width": + w = pixels + h = int(img2_h * (pixels / img2_w)) + else: + h = pixels if direction == 'left-right' else int(img2_h * (pixels / img2_w)) + w = pixels if direction == 'top-bottom' else int(img2_w * (pixels / img2_h)) + + image_2, mask_2 = self.resize_image_and_mask(image_2, mask_2, w, h) + + _, img1_h, img1_w, _ = image_1.shape + _, img2_h, img2_w, _ = image_2.shape + + image, mask, context_mask = None, None, None + + # resize + if img1_h != img2_h and img1_w != img2_w: + width, height = img2_w, img2_h + fit = 'crop' + if method != 'uniform width': + if direction == 'left-right' and img1_h != img2_h: + scale_factor = img2_h / img1_h + width = round(img1_w * scale_factor) + elif direction == 'top-bottom' and img1_w != img2_w: + scale_factor = img2_w / img1_w + height = round(img1_h * scale_factor) + fit = 'fill' + image_1, mask_1 = self.resize_image_and_mask(image_1, mask_1, width, height, fit) + + if mask_1 is None: + mask_1 = torch.full((1, image_1.shape[1], image_1.shape[2]), 0, dtype=torch.float32, device="cpu") + + orig_image_1 = tensor2pil(image_1) + orig_mask_1 = tensor2pil(mask_1).convert('L') + + if orig_mask_1.size != orig_image_1.size: + orig_mask_1 = orig_mask_1.resize(orig_image_1.size) + + img1_w, img1_h = orig_image_1.size + image_1 = pil2tensor(orig_image_1) + image = torch.cat((image_1, image_2), dim=2) if direction == 'left-right' else torch.cat((image_1, image_2), + dim=1) + + context_mask = self.fillMask(image.shape[2], image.shape[1], orig_mask_1) + context_mask = pil2tensor(context_mask) + + orig_mask_2 = tensor2pil(mask_2).convert('L') + x = img1_w if direction == 'left-right' else 0 + y = img1_h if direction == 'top-bottom' else 0 + mask = self.fillMask(image.shape[2], image.shape[1], orig_mask_2, (x, y)) + mask = pil2tensor(mask) + + return (image, mask, context_mask, img2_w, img2_h, x, y) + + +NODE_CLASS_MAPPINGS = { + "easy imageInsetCrop": imageInsetCrop, + "easy imageCount": imageCount, + "easy imagesCountInDirectory": imagesCountInDirectory, + "easy imageSize": imageSize, + "easy imageSizeBySide": imageSizeBySide, + "easy imageSizeByLongerSide": imageSizeByLongerSide, + "easy imagePixelPerfect": imagePixelPerfect, + "easy imageScaleDown": imageScaleDown, + "easy imageScaleDownBy": imageScaleDownBy, + "easy imageScaleDownToSize": imageScaleDownToSize, + "easy imageScaleToNormPixels": imageScaleToNormPixels, + "easy imageRatio": imageRatio, + "easy imageConcat": imageConcat, + "easy imageListToImageBatch": imageListToImageBatch, + "easy imageBatchToImageList": imageBatchToImageList, + "easy imageSplitList": imageSplitList, + "easy imageSplitGrid": imageSplitGrid, + "easy imagesSplitImage": imagesSplitImage, + "easy imageSplitTiles": imageSplitTiles, + "easy imageTilesFromBatch": imageTilesFromBatch, + "easy imageCropFromMask": imageCropFromMask, + "easy imageUncropFromBBOX": imageUncropFromBBOX, + "easy imageSave": imageSaveSimple, + "easy imageRemBg": imageRemBg, + "easy imageChooser": imageChooser, + "easy imageColorMatch": imageColorMatch, + "easy imageDetailTransfer": imageDetailTransfer, + "easy imageInterrogator": imageInterrogator, + "easy loadImagesForLoop": loadImagesForLoop, + "easy loadImageBase64": loadImageBase64, + "easy imageToBase64": imageToBase64, + "easy joinImageBatch": JoinImageBatch, + "easy humanSegmentation": humanSegmentation, + "easy removeLocalImage": removeLocalImage, + "easy makeImageForICLora": makeImageForICRepaint +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "easy imageInsetCrop": "ImageInsetCrop", + "easy imageCount": "ImageCount", + "easy imagesCountInDirectory": "imagesCountInDirectory", + "easy imageSize": "ImageSize", + "easy imageSizeBySide": "ImageSize (Side)", + "easy imageSizeByLongerSide": "ImageSize (LongerSide)", + "easy imagePixelPerfect": "ImagePixelPerfect", + "easy imageScaleDown": "Image Scale Down", + "easy imageScaleDownBy": "Image Scale Down By", + "easy imageScaleDownToSize": "Image Scale Down To Size", + "easy imageScaleToNormPixels": "ImageScaleToNormPixels", + "easy imageRatio": "ImageRatio", + "easy imageHSVMask": "ImageHSVMask", + "easy imageConcat": "imageConcat", + "easy imageListToImageBatch": "Image List To Image Batch", + "easy imageBatchToImageList": "Image Batch To Image List", + "easy imageSplitList": "imageSplitList", + "easy imageSplitGrid": "imageSplitGrid", + "easy imageSplitTiles": "imageSplitTiles", + "easy imageTilesFromBatch": "imageTilesFromBatch", + "easy imagesSplitImage": "imagesSplitImage", + "easy imageCropFromMask": "imageCropFromMask", + "easy imageUncropFromBBOX": "imageUncropFromBBOX", + "easy imageSave": "Save Image (Simple)", + "easy imageRemBg": "Image Remove Bg", + "easy imageChooser": "Image Chooser", + "easy imageColorMatch": "Image Color Match", + "easy imageDetailTransfer": "Image Detail Transfer", + "easy imageInterrogator": "Image To Prompt", + "easy joinImageBatch": "JoinImageBatch", + "easy loadImageBase64": "Load Image (Base64)", + "easy loadImagesForLoop": "Load Images For Loop", + "easy imageToBase64": "Image To Base64", + "easy humanSegmentation": "Human Segmentation", + "easy removeLocalImage": "Remove Local Image", + "easy makeImageForICLora": "Make Image For ICLora" +} \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/inpaint.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/inpaint.py new file mode 100644 index 0000000000000000000000000000000000000000..ab1b1d0127505b4d85258629f786fa30b7743860 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/inpaint.py @@ -0,0 +1,356 @@ +import re +import torch +import comfy +from comfy_extras.nodes_mask import GrowMask +from nodes import VAEEncodeForInpaint, NODE_CLASS_MAPPINGS as ALL_NODE_CLASS_MAPPINGS +from ..libs.utils import get_local_filepath +from ..libs.log import log_node_info +from ..libs import cache as backend_cache +from ..config import * + +# FooocusInpaint +class applyFooocusInpaint: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "model": ("MODEL",), + "latent": ("LATENT",), + "head": (list(FOOOCUS_INPAINT_HEAD.keys()),), + "patch": (list(FOOOCUS_INPAINT_PATCH.keys()),), + }, + } + + RETURN_TYPES = ("MODEL",) + RETURN_NAMES = ("model",) + CATEGORY = "EasyUse/Inpaint" + FUNCTION = "apply" + + def apply(self, model, latent, head, patch): + from ..modules.fooocus import InpaintHead, InpaintWorker + head_file = get_local_filepath(FOOOCUS_INPAINT_HEAD[head]["model_url"], INPAINT_DIR) + inpaint_head_model = InpaintHead() + sd = torch.load(head_file, map_location='cpu') + inpaint_head_model.load_state_dict(sd) + + patch_file = get_local_filepath(FOOOCUS_INPAINT_PATCH[patch]["model_url"], INPAINT_DIR) + inpaint_lora = comfy.utils.load_torch_file(patch_file, safe_load=True) + + patch = (inpaint_head_model, inpaint_lora) + worker = InpaintWorker(node_name="easy kSamplerInpainting") + cloned = model.clone() + + m, = worker.patch(cloned, latent, patch) + return (m,) + +# brushnet +from ..modules.brushnet import BrushNet +class applyBrushNet: + + def get_files_with_extension(folder='inpaint', extensions='.safetensors'): + return [file for file in folder_paths.get_filename_list(folder) if file.endswith(extensions)] + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "pipe": ("PIPE_LINE",), + "image": ("IMAGE",), + "mask": ("MASK",), + "brushnet": (s.get_files_with_extension(),), + "dtype": (['float16', 'bfloat16', 'float32', 'float64'], ), + "scale": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0}), + "start_at": ("INT", {"default": 0, "min": 0, "max": 10000}), + "end_at": ("INT", {"default": 10000, "min": 0, "max": 10000}), + }, + } + + RETURN_TYPES = ("PIPE_LINE",) + RETURN_NAMES = ("pipe",) + CATEGORY = "EasyUse/Inpaint" + FUNCTION = "apply" + + def apply(self, pipe, image, mask, brushnet, dtype, scale, start_at, end_at): + + model = pipe['model'] + vae = pipe['vae'] + positive = pipe['positive'] + negative = pipe['negative'] + cls = BrushNet() + if brushnet in backend_cache.cache: + log_node_info("easy brushnetApply", f"Using {brushnet} Cached") + _, brushnet_model = backend_cache.cache[brushnet][1] + else: + brushnet_file = os.path.join(folder_paths.get_full_path("inpaint", brushnet)) + brushnet_model, = cls.load_brushnet_model(brushnet_file, dtype) + backend_cache.update_cache(brushnet, 'brushnet', (False, brushnet_model)) + m, positive, negative, latent = cls.brushnet_model_update(model=model, vae=vae, image=image, mask=mask, + brushnet=brushnet_model, positive=positive, + negative=negative, scale=scale, start_at=start_at, + end_at=end_at) + new_pipe = { + **pipe, + "model": m, + "positive": positive, + "negative": negative, + "samples": latent, + } + del pipe + return (new_pipe,) + +# #powerpaint +class applyPowerPaint: + def get_files_with_extension(folder='inpaint', extensions='.safetensors'): + return [file for file in folder_paths.get_filename_list(folder) if file.endswith(extensions)] + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "pipe": ("PIPE_LINE",), + "image": ("IMAGE",), + "mask": ("MASK",), + "powerpaint_model": (s.get_files_with_extension(),), + "powerpaint_clip": (s.get_files_with_extension(extensions='.bin'),), + "dtype": (['float16', 'bfloat16', 'float32', 'float64'],), + "fitting": ("FLOAT", {"default": 1.0, "min": 0.3, "max": 1.0}), + "function": (['text guided', 'shape guided', 'object removal', 'context aware', 'image outpainting'],), + "scale": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0}), + "start_at": ("INT", {"default": 0, "min": 0, "max": 10000}), + "end_at": ("INT", {"default": 10000, "min": 0, "max": 10000}), + "save_memory": (['none', 'auto', 'max'],), + }, + } + + RETURN_TYPES = ("PIPE_LINE",) + RETURN_NAMES = ("pipe",) + CATEGORY = "EasyUse/Inpaint" + FUNCTION = "apply" + + def apply(self, pipe, image, mask, powerpaint_model, powerpaint_clip, dtype, fitting, function, scale, start_at, end_at, save_memory='none'): + model = pipe['model'] + vae = pipe['vae'] + positive = pipe['positive'] + negative = pipe['negative'] + + cls = BrushNet() + # load powerpaint clip + if powerpaint_clip in backend_cache.cache: + log_node_info("easy powerpaintApply", f"Using {powerpaint_clip} Cached") + _, ppclip = backend_cache.cache[powerpaint_clip][1] + else: + model_url = POWERPAINT_MODELS['base_fp16']['model_url'] + base_clip = get_local_filepath(model_url, os.path.join(folder_paths.models_dir, 'clip')) + ppclip, = cls.load_powerpaint_clip(base_clip, os.path.join(folder_paths.get_full_path("inpaint", powerpaint_clip))) + backend_cache.update_cache(powerpaint_clip, 'ppclip', (False, ppclip)) + # load powerpaint model + if powerpaint_model in backend_cache.cache: + log_node_info("easy powerpaintApply", f"Using {powerpaint_model} Cached") + _, powerpaint = backend_cache.cache[powerpaint_model][1] + else: + powerpaint_file = os.path.join(folder_paths.get_full_path("inpaint", powerpaint_model)) + powerpaint, = cls.load_brushnet_model(powerpaint_file, dtype) + backend_cache.update_cache(powerpaint_model, 'powerpaint', (False, powerpaint)) + m, positive, negative, latent = cls.powerpaint_model_update(model=model, vae=vae, image=image, mask=mask, powerpaint=powerpaint, + clip=ppclip, positive=positive, + negative=negative, fitting=fitting, function=function, + scale=scale, start_at=start_at, end_at=end_at, save_memory=save_memory) + new_pipe = { + **pipe, + "model": m, + "positive": positive, + "negative": negative, + "samples": latent, + } + del pipe + return (new_pipe,) + +from node_helpers import conditioning_set_values +class applyInpaint: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "pipe": ("PIPE_LINE",), + "image": ("IMAGE",), + "mask": ("MASK",), + "inpaint_mode": (('normal', 'fooocus_inpaint', 'brushnet_random', 'brushnet_segmentation', 'powerpaint'),), + "encode": (('none', 'vae_encode_inpaint', 'inpaint_model_conditioning', 'different_diffusion'), {"default": "none"}), + "grow_mask_by": ("INT", {"default": 6, "min": 0, "max": 64, "step": 1}), + "dtype": (['float16', 'bfloat16', 'float32', 'float64'],), + "fitting": ("FLOAT", {"default": 1.0, "min": 0.3, "max": 1.0}), + "function": (['text guided', 'shape guided', 'object removal', 'context aware', 'image outpainting'],), + "scale": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0}), + "start_at": ("INT", {"default": 0, "min": 0, "max": 10000}), + "end_at": ("INT", {"default": 10000, "min": 0, "max": 10000}), + }, + "optional":{ + "noise_mask": ("BOOLEAN", {"default": True}) + } + } + + RETURN_TYPES = ("PIPE_LINE",) + RETURN_NAMES = ("pipe",) + CATEGORY = "EasyUse/Inpaint" + FUNCTION = "apply" + + def inpaint_model_conditioning(self, pipe, image, vae, mask, grow_mask_by, noise_mask=True): + if grow_mask_by >0: + mask, = GrowMask().expand_mask(mask, grow_mask_by, False) + positive, negative, = pipe['positive'], pipe['negative'] + + pixels = image + x = (pixels.shape[1] // 8) * 8 + y = (pixels.shape[2] // 8) * 8 + mask = torch.nn.functional.interpolate(mask.reshape((-1, 1, mask.shape[-2], mask.shape[-1])), + size=(pixels.shape[1], pixels.shape[2]), mode="bilinear") + + orig_pixels = pixels + pixels = orig_pixels.clone() + if pixels.shape[1] != x or pixels.shape[2] != y: + x_offset = (pixels.shape[1] % 8) // 2 + y_offset = (pixels.shape[2] % 8) // 2 + pixels = pixels[:, x_offset:x + x_offset, y_offset:y + y_offset, :] + mask = mask[:, :, x_offset:x + x_offset, y_offset:y + y_offset] + + m = (1.0 - mask.round()).squeeze(1) + for i in range(3): + pixels[:, :, :, i] -= 0.5 + pixels[:, :, :, i] *= m + pixels[:, :, :, i] += 0.5 + concat_latent = vae.encode(pixels) + orig_latent = vae.encode(orig_pixels) + + out_latent = {} + + out_latent["samples"] = orig_latent + if noise_mask: + out_latent["noise_mask"] = mask + + out = [] + for conditioning in [positive, negative]: + c = conditioning_set_values(conditioning, {"concat_latent_image": concat_latent, + "concat_mask": mask}) + out.append(c) + + pipe['positive'] = out[0] + pipe['negative'] = out[1] + pipe['samples'] = out_latent + + return pipe + + def get_brushnet_model(self, type, model): + model_type = 'sdxl' if isinstance(model.model.model_config, comfy.supported_models.SDXL) else 'sd1' + if type == 'brushnet_random': + brush_model = BRUSHNET_MODELS['random_mask'][model_type]['model_url'] + if model_type == 'sdxl': + pattern = 'brushnet.random.mask.sdxl.*.(safetensors|bin)$' + else: + pattern = 'brushnet.random.mask.*.(safetensors|bin)$' + elif type == 'brushnet_segmentation': + brush_model = BRUSHNET_MODELS['segmentation_mask'][model_type]['model_url'] + if model_type == 'sdxl': + pattern = 'brushnet.segmentation.mask.sdxl.*.(safetensors|bin)$' + else: + pattern = 'brushnet.segmentation.mask.*.(safetensors|bin)$' + + + brushfile = [e for e in folder_paths.get_filename_list('inpaint') if re.search(pattern, e, re.IGNORECASE)] + brushname = brushfile[0] if brushfile else None + if not brushname: + from urllib.parse import urlparse + get_local_filepath(brush_model, INPAINT_DIR) + parsed_url = urlparse(brush_model) + brushname = os.path.basename(parsed_url.path) + return brushname + + def get_powerpaint_model(self, model): + model_type = 'sdxl' if isinstance(model.model.model_config, comfy.supported_models.SDXL) else 'sd1' + if model_type == 'sdxl': + raise Exception("Powerpaint not supported for SDXL models") + + powerpaint_model = POWERPAINT_MODELS['v2.1']['model_url'] + powerpaint_clip = POWERPAINT_MODELS['v2.1']['clip_url'] + + from urllib.parse import urlparse + get_local_filepath(powerpaint_model, os.path.join(INPAINT_DIR, 'powerpaint')) + model_parsed_url = urlparse(powerpaint_model) + clip_parsed_url = urlparse(powerpaint_clip) + model_name = os.path.join("powerpaint",os.path.basename(model_parsed_url.path)) + clip_name = os.path.join("powerpaint",os.path.basename(clip_parsed_url.path)) + return model_name, clip_name + + def apply(self, pipe, image, mask, inpaint_mode, encode, grow_mask_by, dtype, fitting, function, scale, start_at, end_at, noise_mask=True): + new_pipe = { + **pipe, + } + del pipe + if inpaint_mode in ['brushnet_random', 'brushnet_segmentation']: + brushnet = self.get_brushnet_model(inpaint_mode, new_pipe['model']) + new_pipe, = applyBrushNet().apply(new_pipe, image, mask, brushnet, dtype, scale, start_at, end_at) + elif inpaint_mode == 'powerpaint': + powerpaint_model, powerpaint_clip = self.get_powerpaint_model(new_pipe['model']) + new_pipe, = applyPowerPaint().apply(new_pipe, image, mask, powerpaint_model, powerpaint_clip, dtype, fitting, function, scale, start_at, end_at) + + vae = new_pipe['vae'] + if encode == 'none': + if inpaint_mode == 'fooocus_inpaint': + model, = applyFooocusInpaint().apply(new_pipe['model'], new_pipe['samples'], + list(FOOOCUS_INPAINT_HEAD.keys())[0], + list(FOOOCUS_INPAINT_PATCH.keys())[0]) + new_pipe['model'] = model + elif encode == 'vae_encode_inpaint': + latent, = VAEEncodeForInpaint().encode(vae, image, mask, grow_mask_by) + new_pipe['samples'] = latent + if inpaint_mode == 'fooocus_inpaint': + model, = applyFooocusInpaint().apply(new_pipe['model'], new_pipe['samples'], + list(FOOOCUS_INPAINT_HEAD.keys())[0], + list(FOOOCUS_INPAINT_PATCH.keys())[0]) + new_pipe['model'] = model + elif encode == 'inpaint_model_conditioning': + if inpaint_mode == 'fooocus_inpaint': + latent, = VAEEncodeForInpaint().encode(vae, image, mask, grow_mask_by) + new_pipe['samples'] = latent + model, = applyFooocusInpaint().apply(new_pipe['model'], new_pipe['samples'], + list(FOOOCUS_INPAINT_HEAD.keys())[0], + list(FOOOCUS_INPAINT_PATCH.keys())[0]) + new_pipe['model'] = model + new_pipe = self.inpaint_model_conditioning(new_pipe, image, vae, mask, 0, noise_mask=noise_mask) + else: + new_pipe = self.inpaint_model_conditioning(new_pipe, image, vae, mask, grow_mask_by, noise_mask=noise_mask) + elif encode == 'different_diffusion': + if inpaint_mode == 'fooocus_inpaint': + latent, = VAEEncodeForInpaint().encode(vae, image, mask, grow_mask_by) + new_pipe['samples'] = latent + model, = applyFooocusInpaint().apply(new_pipe['model'], new_pipe['samples'], + list(FOOOCUS_INPAINT_HEAD.keys())[0], + list(FOOOCUS_INPAINT_PATCH.keys())[0]) + new_pipe['model'] = model + new_pipe = self.inpaint_model_conditioning(new_pipe, image, vae, mask, 0, noise_mask=noise_mask) + else: + new_pipe = self.inpaint_model_conditioning(new_pipe, image, vae, mask, grow_mask_by, noise_mask=noise_mask) + cls = ALL_NODE_CLASS_MAPPINGS['DifferentialDiffusion'] + if cls is not None: + try: + model, = cls().execute(new_pipe['model']) + except Exception: + model, = cls().apply(new_pipe['model']) + new_pipe['model'] = model + else: + raise Exception("Differential Diffusion not found,please update comfyui") + + return (new_pipe,) + +NODE_CLASS_MAPPINGS = { + "easy applyFooocusInpaint": applyFooocusInpaint, + "easy applyBrushNet": applyBrushNet, + "easy applyPowerPaint": applyPowerPaint, + "easy applyInpaint": applyInpaint +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "easy applyFooocusInpaint": "Easy Apply Fooocus Inpaint", + "easy applyBrushNet": "Easy Apply BrushNet", + "easy applyPowerPaint": "Easy Apply PowerPaint", + "easy applyInpaint": "Easy Apply Inpaint" +} \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/loaders.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/loaders.py new file mode 100644 index 0000000000000000000000000000000000000000..2ad24d03bf1b20a0bdbb3a5884485e85eb826943 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/loaders.py @@ -0,0 +1,1574 @@ +import torch +import comfy.utils, comfy.sample, comfy.samplers, comfy.controlnet, comfy.model_base, comfy.model_management, comfy.sampler_helpers, comfy.supported_models +from comfy.sd import CLIP, VAE +from comfy.model_patcher import ModelPatcher +from PIL import Image + +from nodes import MAX_RESOLUTION, NODE_CLASS_MAPPINGS as ALL_NODE_CLASS_MAPPINGS, ConditioningConcat, CLIPTextEncode, ConditioningZeroOut + +from ..libs.log import log_node_info, log_node_error, log_node_warn +from ..libs.wildcards import process_with_loras +from ..libs.utils import find_wildcards_seed, is_linked_styles_selector, get_sd_version, AlwaysEqualProxy +from ..libs.sampler import easySampler +from ..libs.controlnet import easyControlnet, union_controlnet_types +from ..libs.conditioning import prompt_to_cond +from ..libs.easing import EasingBase +from ..libs.translate import has_chinese, zh_to_en + +from ..config import * + +from .. import easyCache, sampler + +any_type = AlwaysEqualProxy("*") +# 简易加载器完整 +resolution_strings = [f"{width} x {height} (custom)" if width == 'width' and height == 'height' else f"{width} x {height}" for width, height in BASE_RESOLUTIONS] +class fullLoader: + + @classmethod + def INPUT_TYPES(cls): + a1111_prompt_style_default = False + + return {"required": { + "ckpt_name": (folder_paths.get_filename_list("checkpoints") + ['None'],), + "config_name": (["Default", ] + folder_paths.get_filename_list("configs"), {"default": "Default"}), + "vae_name": (["Baked VAE"] + folder_paths.get_filename_list("vae"),), + "clip_skip": ("INT", {"default": -2, "min": -24, "max": 0, "step": 1}), + + "lora_name": (["None"] + folder_paths.get_filename_list("loras"),), + "lora_model_strength": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}), + "lora_clip_strength": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}), + + "resolution": (resolution_strings, {"default": "512 x 512"}), + "empty_latent_width": ("INT", {"default": 512, "min": 64, "max": MAX_RESOLUTION, "step": 8}), + "empty_latent_height": ("INT", {"default": 512, "min": 64, "max": MAX_RESOLUTION, "step": 8}), + + "positive": ("STRING", {"default": "", "placeholder": "Positive", "multiline": True}), + "positive_token_normalization": (["none", "mean", "length", "length+mean"],), + "positive_weight_interpretation": (["comfy", "A1111", "comfy++", "compel", "fixed attention"],), + + "negative": ("STRING", {"default": "", "placeholder": "Negative", "multiline": True}), + "negative_token_normalization": (["none", "mean", "length", "length+mean"],), + "negative_weight_interpretation": (["comfy", "A1111", "comfy++", "compel", "fixed attention"],), + + "batch_size": ( + "INT", {"default": 1, "min": 1, "max": 4096, "tooltip": "The number of latent images in the batch."}) + }, + "optional": {"model_override": ("MODEL",), "clip_override": ("CLIP",), "vae_override": ("VAE",), "optional_lora_stack": ("LORA_STACK",), "optional_controlnet_stack": ("CONTROL_NET_STACK",), "a1111_prompt_style": ("BOOLEAN", {"default": a1111_prompt_style_default})}, + "hidden": {"video_length": "INT", "prompt": "PROMPT", "my_unique_id": "UNIQUE_ID"} + } + + RETURN_TYPES = ("PIPE_LINE", "MODEL", "VAE", "CLIP", "CONDITIONING", "CONDITIONING", "LATENT") + RETURN_NAMES = ("pipe", "model", "vae", "clip", "positive", "negative", "latent") + + FUNCTION = "adv_pipeloader" + CATEGORY = "EasyUse/Loaders" + + def adv_pipeloader(self, ckpt_name, config_name, vae_name, clip_skip, + lora_name, lora_model_strength, lora_clip_strength, + resolution, empty_latent_width, empty_latent_height, + positive, positive_token_normalization, positive_weight_interpretation, + negative, negative_token_normalization, negative_weight_interpretation, + batch_size, model_override=None, clip_override=None, vae_override=None, optional_lora_stack=None, optional_controlnet_stack=None, a1111_prompt_style=False, video_length=25, prompt=None, + my_unique_id=None + ): + + if ckpt_name == 'None' and model_override is None: + raise Exception("Please select a checkpoint or provide a model override.") + + # Clean models from loaded_objects + easyCache.update_loaded_objects(prompt) + + # Load models + log_node_warn("Loading models...") + model, clip, vae, clip_vision, lora_stack = easyCache.load_main(ckpt_name, config_name, vae_name, lora_name, lora_model_strength, lora_clip_strength, optional_lora_stack, model_override, clip_override, vae_override, prompt) + + # Create Empty Latent + model_type = get_sd_version(model) + samples = sampler.emptyLatent(resolution, empty_latent_width, empty_latent_height, batch_size, model_type=model_type, video_length=video_length) + + # Prompt to Conditioning + positive_embeddings_final, positive_wildcard_prompt, model, clip = prompt_to_cond('positive', model, clip, clip_skip, lora_stack, positive, positive_token_normalization, positive_weight_interpretation, a1111_prompt_style, my_unique_id, prompt, easyCache, model_type=model_type) + negative_embeddings_final, negative_wildcard_prompt, model, clip = prompt_to_cond('negative', model, clip, clip_skip, lora_stack, negative, negative_token_normalization, negative_weight_interpretation, a1111_prompt_style, my_unique_id, prompt, easyCache, model_type=model_type) + + if negative_embeddings_final is None: + negative_embeddings_final, = ConditioningZeroOut().zero_out(positive_embeddings_final) + + # Conditioning add controlnet + if optional_controlnet_stack is not None and len(optional_controlnet_stack) > 0: + for controlnet in optional_controlnet_stack: + positive_embeddings_final, negative_embeddings_final = easyControlnet().apply(controlnet[0], controlnet[5], positive_embeddings_final, negative_embeddings_final, controlnet[1], start_percent=controlnet[2], end_percent=controlnet[3], control_net=None, scale_soft_weights=controlnet[4], mask=None, easyCache=easyCache, use_cache=True, model=model, vae=vae) + + pipe = { + "model": model, + "positive": positive_embeddings_final, + "negative": negative_embeddings_final, + "vae": vae, + "clip": clip, + + "samples": samples, + "images": None, + + "loader_settings": { + "ckpt_name": ckpt_name, + "vae_name": vae_name, + "lora_name": lora_name, + "lora_model_strength": lora_model_strength, + "lora_clip_strength": lora_clip_strength, + "lora_stack": lora_stack, + + "clip_skip": clip_skip, + "a1111_prompt_style": a1111_prompt_style, + "positive": positive, + "positive_token_normalization": positive_token_normalization, + "positive_weight_interpretation": positive_weight_interpretation, + "negative": negative, + "negative_token_normalization": negative_token_normalization, + "negative_weight_interpretation": negative_weight_interpretation, + "resolution": resolution, + "empty_latent_width": empty_latent_width, + "empty_latent_height": empty_latent_height, + "batch_size": batch_size, + } + } + + return {"ui": {"positive": positive_wildcard_prompt, "negative": negative_wildcard_prompt}, "result": (pipe, model, vae, clip, positive_embeddings_final, negative_embeddings_final, samples)} + +# A1111简易加载器 +class a1111Loader(fullLoader): + @classmethod + def INPUT_TYPES(cls): + a1111_prompt_style_default = False + checkpoints = folder_paths.get_filename_list("checkpoints") + loras = ["None"] + folder_paths.get_filename_list("loras") + return { + "required": { + "ckpt_name": (checkpoints,), + "vae_name": (["Baked VAE"] + folder_paths.get_filename_list("vae"),), + "clip_skip": ("INT", {"default": -2, "min": -24, "max": 0, "step": 1}), + + "lora_name": (loras,), + "lora_model_strength": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}), + "lora_clip_strength": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}), + + "resolution": (resolution_strings, {"default": "512 x 512"}), + "empty_latent_width": ("INT", {"default": 512, "min": 64, "max": MAX_RESOLUTION, "step": 8}), + "empty_latent_height": ("INT", {"default": 512, "min": 64, "max": MAX_RESOLUTION, "step": 8}), + + "positive": ("STRING", {"default":"", "placeholder": "Positive", "multiline": True}), + "negative": ("STRING", {"default":"", "placeholder": "Negative", "multiline": True}), + "batch_size": ("INT", {"default": 1, "min": 1, "max": 4096, "tooltip": "The number of latent images in the batch."}) + }, + "optional": { + "optional_lora_stack": ("LORA_STACK",), + "optional_controlnet_stack": ("CONTROL_NET_STACK",), + "a1111_prompt_style": ("BOOLEAN", {"default": a1111_prompt_style_default}), + }, + "hidden": {"prompt": "PROMPT", "my_unique_id": "UNIQUE_ID"} + } + + RETURN_TYPES = ("PIPE_LINE", "MODEL", "VAE") + RETURN_NAMES = ("pipe", "model", "vae") + + FUNCTION = "a1111loader" + CATEGORY = "EasyUse/Loaders" + + def a1111loader(self, ckpt_name, vae_name, clip_skip, + lora_name, lora_model_strength, lora_clip_strength, + resolution, empty_latent_width, empty_latent_height, + positive, negative, batch_size, optional_lora_stack=None, optional_controlnet_stack=None, a1111_prompt_style=False, prompt=None, + my_unique_id=None): + + return super().adv_pipeloader(ckpt_name, 'Default', vae_name, clip_skip, + lora_name, lora_model_strength, lora_clip_strength, + resolution, empty_latent_width, empty_latent_height, + positive, 'mean', 'A1111', + negative,'mean','A1111', + batch_size, None, None, None, optional_lora_stack=optional_lora_stack, optional_controlnet_stack=optional_controlnet_stack,a1111_prompt_style=a1111_prompt_style, prompt=prompt, + my_unique_id=my_unique_id + ) + +# Comfy简易加载器 +class comfyLoader(fullLoader): + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "ckpt_name": (folder_paths.get_filename_list("checkpoints"),), + "vae_name": (["Baked VAE"] + folder_paths.get_filename_list("vae"),), + "clip_skip": ("INT", {"default": -2, "min": -24, "max": 0, "step": 1}), + + "lora_name": (["None"] + folder_paths.get_filename_list("loras"),), + "lora_model_strength": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}), + "lora_clip_strength": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}), + + "resolution": (resolution_strings, {"default": "512 x 512"}), + "empty_latent_width": ("INT", {"default": 512, "min": 64, "max": MAX_RESOLUTION, "step": 8}), + "empty_latent_height": ("INT", {"default": 512, "min": 64, "max": MAX_RESOLUTION, "step": 8}), + + "positive": ("STRING", {"default": "", "placeholder": "Positive", "multiline": True}), + "negative": ("STRING", {"default": "", "placeholder": "Negative", "multiline": True}), + + "batch_size": ("INT", {"default": 1, "min": 1, "max": 4096, "tooltip": "The number of latent images in the batch."}) + }, + "optional": {"optional_lora_stack": ("LORA_STACK",), "optional_controlnet_stack": ("CONTROL_NET_STACK",),}, + "hidden": {"prompt": "PROMPT", "my_unique_id": "UNIQUE_ID"} + } + + RETURN_TYPES = ("PIPE_LINE", "MODEL", "VAE") + RETURN_NAMES = ("pipe", "model", "vae") + + FUNCTION = "comfyloader" + CATEGORY = "EasyUse/Loaders" + + def comfyloader(self, ckpt_name, vae_name, clip_skip, + lora_name, lora_model_strength, lora_clip_strength, + resolution, empty_latent_width, empty_latent_height, + positive, negative, batch_size, optional_lora_stack=None, optional_controlnet_stack=None, prompt=None, + my_unique_id=None): + return super().adv_pipeloader(ckpt_name, 'Default', vae_name, clip_skip, + lora_name, lora_model_strength, lora_clip_strength, + resolution, empty_latent_width, empty_latent_height, + positive, 'none', 'comfy', + negative, 'none', 'comfy', + batch_size, None, None, None, optional_lora_stack=optional_lora_stack, optional_controlnet_stack=optional_controlnet_stack, a1111_prompt_style=False, prompt=prompt, + my_unique_id=my_unique_id + ) + +# hydit简易加载器 +class hunyuanDiTLoader(fullLoader): + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "ckpt_name": (folder_paths.get_filename_list("checkpoints"),), + "vae_name": (["Baked VAE"] + folder_paths.get_filename_list("vae"),), + + "lora_name": (["None"] + folder_paths.get_filename_list("loras"),), + "lora_model_strength": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}), + "lora_clip_strength": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}), + + "resolution": (resolution_strings, {"default": "1024 x 1024"}), + "empty_latent_width": ("INT", {"default": 1024, "min": 64, "max": MAX_RESOLUTION, "step": 8}), + "empty_latent_height": ("INT", {"default": 1024, "min": 64, "max": MAX_RESOLUTION, "step": 8}), + + "positive": ("STRING", {"default": "", "placeholder": "Positive", "multiline": True}), + "negative": ("STRING", {"default": "", "placeholder": "Negative", "multiline": True}), + + "batch_size": ("INT", {"default": 1, "min": 1, "max": 64}), + }, + "optional": {"optional_lora_stack": ("LORA_STACK",), "optional_controlnet_stack": ("CONTROL_NET_STACK",),}, + "hidden": {"prompt": "PROMPT", "my_unique_id": "UNIQUE_ID"} + } + + RETURN_TYPES = ("PIPE_LINE", "MODEL", "VAE") + RETURN_NAMES = ("pipe", "model", "vae") + + FUNCTION = "hyditloader" + CATEGORY = "EasyUse/Loaders" + + def hyditloader(self, ckpt_name, vae_name, + lora_name, lora_model_strength, lora_clip_strength, + resolution, empty_latent_width, empty_latent_height, + positive, negative, batch_size, optional_lora_stack=None, optional_controlnet_stack=None, prompt=None, + my_unique_id=None): + + return super().adv_pipeloader(ckpt_name, 'Default', vae_name, 0, + lora_name, lora_model_strength, lora_clip_strength, + resolution, empty_latent_width, empty_latent_height, + positive, 'none', 'comfy', + negative, 'none', 'comfy', + batch_size, None, None, None, optional_lora_stack=optional_lora_stack, optional_controlnet_stack=optional_controlnet_stack, a1111_prompt_style=False, prompt=prompt, + my_unique_id=my_unique_id + ) + +# stable Cascade +class cascadeLoader: + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(s): + + return {"required": { + "stage_c": (folder_paths.get_filename_list("unet") + folder_paths.get_filename_list("checkpoints"),), + "stage_b": (folder_paths.get_filename_list("unet") + folder_paths.get_filename_list("checkpoints"),), + "stage_a": (["Baked VAE"]+folder_paths.get_filename_list("vae"),), + "clip_name": (["None"] + folder_paths.get_filename_list("clip"),), + + "lora_name": (["None"] + folder_paths.get_filename_list("loras"),), + "lora_model_strength": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}), + "lora_clip_strength": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}), + + "resolution": (resolution_strings, {"default": "1024 x 1024"}), + "empty_latent_width": ("INT", {"default": 1024, "min": 16, "max": MAX_RESOLUTION, "step": 8}), + "empty_latent_height": ("INT", {"default": 1024, "min": 16, "max": MAX_RESOLUTION, "step": 8}), + "compression": ("INT", {"default": 42, "min": 32, "max": 64, "step": 1}), + + "positive": ("STRING", {"default":"", "placeholder": "Positive", "multiline": True}), + "negative": ("STRING", {"default":"", "placeholder": "Negative", "multiline": True}), + + "batch_size": ("INT", {"default": 1, "min": 1, "max": 64}), + }, + "optional": {"optional_lora_stack": ("LORA_STACK",), }, + "hidden": {"prompt": "PROMPT", "my_unique_id": "UNIQUE_ID"} + } + + RETURN_TYPES = ("PIPE_LINE", "MODEL", "LATENT", "VAE") + RETURN_NAMES = ("pipe", "model_c", "latent_c", "vae") + + FUNCTION = "adv_pipeloader" + CATEGORY = "EasyUse/Loaders" + + def is_ckpt(self, name): + is_ckpt = False + path = folder_paths.get_full_path("checkpoints", name) + if path is not None: + is_ckpt = True + return is_ckpt + + def adv_pipeloader(self, stage_c, stage_b, stage_a, clip_name, lora_name, lora_model_strength, lora_clip_strength, + resolution, empty_latent_width, empty_latent_height, compression, + positive, negative, batch_size, optional_lora_stack=None,prompt=None, + my_unique_id=None): + + vae: VAE | None = None + model_c: ModelPatcher | None = None + model_b: ModelPatcher | None = None + clip: CLIP | None = None + can_load_lora = True + pipe_lora_stack = [] + + # Clean models from loaded_objects + easyCache.update_loaded_objects(prompt) + + # Create Empty Latent + samples = sampler.emptyLatent(resolution, empty_latent_width, empty_latent_height, batch_size, compression) + + if self.is_ckpt(stage_c): + model_c, clip, vae_c, clip_vision = easyCache.load_checkpoint(stage_c) + else: + model_c = easyCache.load_unet(stage_c) + vae_c = None + if self.is_ckpt(stage_b): + model_b, clip, vae_b, clip_vision = easyCache.load_checkpoint(stage_b) + else: + model_b = easyCache.load_unet(stage_b) + vae_b = None + + if optional_lora_stack is not None and can_load_lora: + for lora in optional_lora_stack: + lora = {"lora_name": lora[0], "model": model_c, "clip": clip, "model_strength": lora[1], "clip_strength": lora[2]} + model_c, clip = easyCache.load_lora(lora) + lora['model'] = model_c + lora['clip'] = clip + pipe_lora_stack.append(lora) + + if lora_name != "None" and can_load_lora: + lora = {"lora_name": lora_name, "model": model_c, "clip": clip, "model_strength": lora_model_strength, + "clip_strength": lora_clip_strength} + model_c, clip = easyCache.load_lora(lora) + pipe_lora_stack.append(lora) + + model = (model_c, model_b) + # Load clip + if clip_name != 'None': + clip = easyCache.load_clip(clip_name, "stable_cascade") + # Load vae + if stage_a not in ["Baked VAE", "Baked-VAE"]: + vae_b = easyCache.load_vae(stage_a) + + vae = (vae_c, vae_b) + # 判断是否连接 styles selector + is_positive_linked_styles_selector = is_linked_styles_selector(prompt, my_unique_id, 'positive') + is_negative_linked_styles_selector = is_linked_styles_selector(prompt, my_unique_id, 'negative') + + positive_seed = find_wildcards_seed(my_unique_id, positive, prompt) + # Translate cn to en + if has_chinese(positive): + positive = zh_to_en([positive])[0] + model_c, clip, positive, positive_decode, show_positive_prompt, pipe_lora_stack = process_with_loras(positive, + model_c, clip, + "positive", + positive_seed, + can_load_lora, + pipe_lora_stack, + easyCache) + positive_wildcard_prompt = positive_decode if show_positive_prompt or is_positive_linked_styles_selector else "" + negative_seed = find_wildcards_seed(my_unique_id, negative, prompt) + # Translate cn to en + if has_chinese(negative): + negative = zh_to_en([negative])[0] + model_c, clip, negative, negative_decode, show_negative_prompt, pipe_lora_stack = process_with_loras(negative, + model_c, clip, + "negative", + negative_seed, + can_load_lora, + pipe_lora_stack, + easyCache) + negative_wildcard_prompt = negative_decode if show_negative_prompt or is_negative_linked_styles_selector else "" + + tokens = clip.tokenize(positive) + cond, pooled = clip.encode_from_tokens(tokens, return_pooled=True) + positive_embeddings_final = [[cond, {"pooled_output": pooled}]] + + tokens = clip.tokenize(negative) + cond, pooled = clip.encode_from_tokens(tokens, return_pooled=True) + negative_embeddings_final = [[cond, {"pooled_output": pooled}]] + + image = easySampler.pil2tensor(Image.new('RGB', (1, 1), (0, 0, 0))) + + pipe = { + "model": model, + "positive": positive_embeddings_final, + "negative": negative_embeddings_final, + "vae": vae, + "clip": clip, + + "samples": samples, + "images": image, + "seed": 0, + + "loader_settings": { + "vae_name": stage_a, + "lora_name": lora_name, + "lora_model_strength": lora_model_strength, + "lora_clip_strength": lora_clip_strength, + "lora_stack": pipe_lora_stack, + + "positive": positive, + "positive_token_normalization": 'none', + "positive_weight_interpretation": 'comfy', + "negative": negative, + "negative_token_normalization": 'none', + "negative_weight_interpretation": 'comfy', + "resolution": resolution, + "empty_latent_width": empty_latent_width, + "empty_latent_height": empty_latent_height, + "batch_size": batch_size, + "compression": compression + } + } + + return {"ui": {"positive": positive_wildcard_prompt, "negative": negative_wildcard_prompt}, + "result": (pipe, model_c, model_b, vae)} + +# Zero123简易加载器 (3D) +try: + from comfy_extras.nodes_stable3d import camera_embeddings +except FileNotFoundError: + log_node_error("EasyUse[zero123Loader]", "请更新ComfyUI到最新版本") + +class zero123Loader: + + @classmethod + def INPUT_TYPES(cls): + def get_file_list(filenames): + return [file for file in filenames if file != "put_models_here.txt" and "zero123" in file.lower()] + + return {"required": { + "ckpt_name": (get_file_list(folder_paths.get_filename_list("checkpoints")),), + "vae_name": (["Baked VAE"] + folder_paths.get_filename_list("vae"),), + + "init_image": ("IMAGE",), + "empty_latent_width": ("INT", {"default": 256, "min": 16, "max": MAX_RESOLUTION, "step": 8}), + "empty_latent_height": ("INT", {"default": 256, "min": 16, "max": MAX_RESOLUTION, "step": 8}), + + "batch_size": ("INT", {"default": 1, "min": 1, "max": 64}), + + "elevation": ("FLOAT", {"default": 0.0, "min": -180.0, "max": 180.0}), + "azimuth": ("FLOAT", {"default": 0.0, "min": -180.0, "max": 180.0}), + }, + "hidden": {"prompt": "PROMPT", "my_unique_id": "UNIQUE_ID"} + } + + RETURN_TYPES = ("PIPE_LINE", "MODEL", "VAE") + RETURN_NAMES = ("pipe", "model", "vae") + + FUNCTION = "adv_pipeloader" + CATEGORY = "EasyUse/Loaders" + + def adv_pipeloader(self, ckpt_name, vae_name, init_image, empty_latent_width, empty_latent_height, batch_size, elevation, azimuth, prompt=None, my_unique_id=None): + model: ModelPatcher | None = None + vae: VAE | None = None + clip: CLIP | None = None + clip_vision = None + + # Clean models from loaded_objects + easyCache.update_loaded_objects(prompt) + + model, clip, vae, clip_vision = easyCache.load_checkpoint(ckpt_name, "Default", True) + + output = clip_vision.encode_image(init_image) + pooled = output.image_embeds.unsqueeze(0) + pixels = comfy.utils.common_upscale(init_image.movedim(-1, 1), empty_latent_width, empty_latent_height, "bilinear", "center").movedim(1, -1) + encode_pixels = pixels[:, :, :, :3] + t = vae.encode(encode_pixels) + cam_embeds = camera_embeddings(elevation, azimuth) + cond = torch.cat([pooled, cam_embeds.repeat((pooled.shape[0], 1, 1))], dim=-1) + + positive = [[cond, {"concat_latent_image": t}]] + negative = [[torch.zeros_like(pooled), {"concat_latent_image": torch.zeros_like(t)}]] + latent = torch.zeros([batch_size, 4, empty_latent_height // 8, empty_latent_width // 8]) + samples = {"samples": latent} + + image = easySampler.pil2tensor(Image.new('RGB', (1, 1), (0, 0, 0))) + + pipe = {"model": model, + "positive": positive, + "negative": negative, + "vae": vae, + "clip": clip, + + "samples": samples, + "images": image, + "seed": 0, + + "loader_settings": {"ckpt_name": ckpt_name, + "vae_name": vae_name, + + "positive": positive, + "negative": negative, + "empty_latent_width": empty_latent_width, + "empty_latent_height": empty_latent_height, + "batch_size": batch_size, + "seed": 0, + } + } + + return (pipe, model, vae) + +# SV3D加载器 +class sv3dLoader(EasingBase): + + def __init__(self): + super().__init__() + + @classmethod + def INPUT_TYPES(cls): + def get_file_list(filenames): + return [file for file in filenames if file != "put_models_here.txt" and "sv3d" in file] + + return {"required": { + "ckpt_name": (get_file_list(folder_paths.get_filename_list("checkpoints")),), + "vae_name": (["Baked VAE"] + folder_paths.get_filename_list("vae"),), + + "init_image": ("IMAGE",), + "empty_latent_width": ("INT", {"default": 576, "min": 16, "max": MAX_RESOLUTION, "step": 8}), + "empty_latent_height": ("INT", {"default": 576, "min": 16, "max": MAX_RESOLUTION, "step": 8}), + + "batch_size": ("INT", {"default": 21, "min": 1, "max": 4096}), + "interp_easing": (["linear", "ease_in", "ease_out", "ease_in_out"], {"default": "linear"}), + "easing_mode": (["azimuth", "elevation", "custom"], {"default": "azimuth"}), + }, + "optional": {"scheduler": ("STRING", {"default": "", "multiline": True})}, + "hidden": {"prompt": "PROMPT", "my_unique_id": "UNIQUE_ID"} + } + + RETURN_TYPES = ("PIPE_LINE", "MODEL", "STRING") + RETURN_NAMES = ("pipe", "model", "interp_log") + + FUNCTION = "adv_pipeloader" + CATEGORY = "EasyUse/Loaders" + + def adv_pipeloader(self, ckpt_name, vae_name, init_image, empty_latent_width, empty_latent_height, batch_size, interp_easing, easing_mode, scheduler='',prompt=None, my_unique_id=None): + model: ModelPatcher | None = None + vae: VAE | None = None + clip: CLIP | None = None + + # Clean models from loaded_objects + easyCache.update_loaded_objects(prompt) + + model, clip, vae, clip_vision = easyCache.load_checkpoint(ckpt_name, "Default", True) + + output = clip_vision.encode_image(init_image) + pooled = output.image_embeds.unsqueeze(0) + pixels = comfy.utils.common_upscale(init_image.movedim(-1, 1), empty_latent_width, empty_latent_height, "bilinear", "center").movedim(1, + -1) + encode_pixels = pixels[:, :, :, :3] + t = vae.encode(encode_pixels) + + azimuth_points = [] + elevation_points = [] + if easing_mode == 'azimuth': + azimuth_points = [(0, 0), (batch_size-1, 360)] + elevation_points = [(0, 0)] * batch_size + elif easing_mode == 'elevation': + azimuth_points = [(0, 0)] * batch_size + elevation_points = [(0, -90), (batch_size-1, 90)] + else: + schedulers = scheduler.rstrip('\n') + for line in schedulers.split('\n'): + frame_str, point_str = line.split(':') + point_str = point_str.strip()[1:-1] + point = point_str.split(',') + azimuth_point = point[0] + elevation_point = point[1] if point[1] else 0.0 + frame = int(frame_str.strip()) + azimuth = float(azimuth_point) + azimuth_points.append((frame, azimuth)) + elevation_val = float(elevation_point) + elevation_points.append((frame, elevation_val)) + azimuth_points.sort(key=lambda x: x[0]) + elevation_points.sort(key=lambda x: x[0]) + + #interpolation + next_point = 1 + next_elevation_point = 1 + elevations = [] + azimuths = [] + # For azimuth interpolation + for i in range(batch_size): + # Find the interpolated azimuth for the current frame + while next_point < len(azimuth_points) and i >= azimuth_points[next_point][0]: + next_point += 1 + if next_point == len(azimuth_points): + next_point -= 1 + prev_point = max(next_point - 1, 0) + + if azimuth_points[next_point][0] != azimuth_points[prev_point][0]: + timing = (i - azimuth_points[prev_point][0]) / ( + azimuth_points[next_point][0] - azimuth_points[prev_point][0]) + interpolated_azimuth = self.ease(azimuth_points[prev_point][1], azimuth_points[next_point][1], self.easing(timing, interp_easing)) + else: + interpolated_azimuth = azimuth_points[prev_point][1] + + # Interpolate the elevation + next_elevation_point = 1 + while next_elevation_point < len(elevation_points) and i >= elevation_points[next_elevation_point][0]: + next_elevation_point += 1 + if next_elevation_point == len(elevation_points): + next_elevation_point -= 1 + prev_elevation_point = max(next_elevation_point - 1, 0) + + if elevation_points[next_elevation_point][0] != elevation_points[prev_elevation_point][0]: + timing = (i - elevation_points[prev_elevation_point][0]) / ( + elevation_points[next_elevation_point][0] - elevation_points[prev_elevation_point][0]) + interpolated_elevation = self.ease(elevation_points[prev_point][1], elevation_points[next_point][1], self.easing(timing, interp_easing)) + else: + interpolated_elevation = elevation_points[prev_elevation_point][1] + + azimuths.append(interpolated_azimuth) + elevations.append(interpolated_elevation) + + log_node_info("easy sv3dLoader", "azimuths:" + str(azimuths)) + log_node_info("easy sv3dLoader", "elevations:" + str(elevations)) + + log = 'azimuths:' + str(azimuths) + '\n\n' + "elevations:" + str(elevations) + # Structure the final output + positive = [[pooled, {"concat_latent_image": t, "elevation": elevations, "azimuth": azimuths}]] + negative = [[torch.zeros_like(pooled), + {"concat_latent_image": torch.zeros_like(t), "elevation": elevations, "azimuth": azimuths}]] + + latent = torch.zeros([batch_size, 4, empty_latent_height // 8, empty_latent_width // 8]) + samples = {"samples": latent} + + image = easySampler.pil2tensor(Image.new('RGB', (1, 1), (0, 0, 0))) + + + pipe = {"model": model, + "positive": positive, + "negative": negative, + "vae": vae, + "clip": clip, + + "samples": samples, + "images": image, + "seed": 0, + + "loader_settings": {"ckpt_name": ckpt_name, + "vae_name": vae_name, + + "positive": positive, + "negative": negative, + "empty_latent_width": empty_latent_width, + "empty_latent_height": empty_latent_height, + "batch_size": batch_size, + "seed": 0, + } + } + + return (pipe, model, log) + +#svd加载器 +class svdLoader: + + @classmethod + def INPUT_TYPES(cls): + def get_file_list(filenames): + return [file for file in filenames if file != "put_models_here.txt" and "svd" in file.lower()] + + return {"required": { + "ckpt_name": (get_file_list(folder_paths.get_filename_list("checkpoints")),), + "vae_name": (["Baked VAE"] + folder_paths.get_filename_list("vae"),), + "clip_name": (["None"] + folder_paths.get_filename_list("clip"),), + + "init_image": ("IMAGE",), + "resolution": (resolution_strings, {"default": "1024 x 576"}), + "empty_latent_width": ("INT", {"default": 256, "min": 16, "max": MAX_RESOLUTION, "step": 8}), + "empty_latent_height": ("INT", {"default": 256, "min": 16, "max": MAX_RESOLUTION, "step": 8}), + + "video_frames": ("INT", {"default": 14, "min": 1, "max": 4096}), + "motion_bucket_id": ("INT", {"default": 127, "min": 1, "max": 1023}), + "fps": ("INT", {"default": 6, "min": 1, "max": 1024}), + "augmentation_level": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 10.0, "step": 0.01}) + }, + "optional": { + "optional_positive": ("STRING", {"default": "", "multiline": True}), + "optional_negative": ("STRING", {"default": "", "multiline": True}), + }, + "hidden": {"prompt": "PROMPT", "my_unique_id": "UNIQUE_ID"} + } + + RETURN_TYPES = ("PIPE_LINE", "MODEL", "VAE") + RETURN_NAMES = ("pipe", "model", "vae") + + FUNCTION = "adv_pipeloader" + CATEGORY = "EasyUse/Loaders" + + def adv_pipeloader(self, ckpt_name, vae_name, clip_name, init_image, resolution, empty_latent_width, empty_latent_height, video_frames, motion_bucket_id, fps, augmentation_level, optional_positive=None, optional_negative=None, prompt=None, my_unique_id=None): + model: ModelPatcher | None = None + vae: VAE | None = None + clip: CLIP | None = None + clip_vision = None + + # resolution + if resolution != "自定义 x 自定义": + try: + width, height = map(int, resolution.split(' x ')) + empty_latent_width = width + empty_latent_height = height + except ValueError: + raise ValueError("Invalid base_resolution format.") + + # Clean models from loaded_objects + easyCache.update_loaded_objects(prompt) + + model, clip, vae, clip_vision = easyCache.load_checkpoint(ckpt_name, "Default", True) + + output = clip_vision.encode_image(init_image) + pooled = output.image_embeds.unsqueeze(0) + pixels = comfy.utils.common_upscale(init_image.movedim(-1, 1), empty_latent_width, empty_latent_height, "bilinear", "center").movedim(1, -1) + encode_pixels = pixels[:, :, :, :3] + if augmentation_level > 0: + encode_pixels += torch.randn_like(pixels) * augmentation_level + t = vae.encode(encode_pixels) + positive = [[pooled, + {"motion_bucket_id": motion_bucket_id, "fps": fps, "augmentation_level": augmentation_level, + "concat_latent_image": t}]] + negative = [[torch.zeros_like(pooled), + {"motion_bucket_id": motion_bucket_id, "fps": fps, "augmentation_level": augmentation_level, + "concat_latent_image": torch.zeros_like(t)}]] + if optional_positive is not None and optional_positive != '': + if clip_name == 'None': + raise Exception("You need choose a open_clip model when positive is not empty") + clip = easyCache.load_clip(clip_name) + if has_chinese(optional_positive): + optional_positive = zh_to_en([optional_positive])[0] + positive_embeddings_final, = CLIPTextEncode().encode(clip, optional_positive) + positive, = ConditioningConcat().concat(positive, positive_embeddings_final) + if optional_negative is not None and optional_negative != '': + if clip_name == 'None': + raise Exception("You need choose a open_clip model when negative is not empty") + if has_chinese(optional_negative): + optional_positive = zh_to_en([optional_negative])[0] + negative_embeddings_final, = CLIPTextEncode().encode(clip, optional_negative) + negative, = ConditioningConcat().concat(negative, negative_embeddings_final) + + latent = torch.zeros([video_frames, 4, empty_latent_height // 8, empty_latent_width // 8]) + samples = {"samples": latent} + + image = easySampler.pil2tensor(Image.new('RGB', (1, 1), (0, 0, 0))) + + pipe = {"model": model, + "positive": positive, + "negative": negative, + "vae": vae, + "clip": clip, + + "samples": samples, + "images": image, + "seed": 0, + + "loader_settings": {"ckpt_name": ckpt_name, + "vae_name": vae_name, + + "positive": positive, + "negative": negative, + "resolution": resolution, + "empty_latent_width": empty_latent_width, + "empty_latent_height": empty_latent_height, + "batch_size": 1, + "seed": 0, + } + } + + return (pipe, model, vae) + + +# kolors Loader +from ..modules.kolors.text_encode import chatglm3_adv_text_encode +class kolorsLoader: + + @classmethod + def INPUT_TYPES(cls): + return { + "required":{ + "unet_name": (folder_paths.get_filename_list("unet"),), + "vae_name": (folder_paths.get_filename_list("vae"),), + "chatglm3_name": (folder_paths.get_filename_list("llm"),), + "lora_name": (["None"] + folder_paths.get_filename_list("loras"),), + "lora_model_strength": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}), + "lora_clip_strength": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}), + "resolution": (resolution_strings, {"default": "1024 x 576"}), + "empty_latent_width": ("INT", {"default": 1024, "min": 64, "max": MAX_RESOLUTION, "step": 8}), + "empty_latent_height": ("INT", {"default": 1024, "min": 64, "max": MAX_RESOLUTION, "step": 8}), + + "positive": ("STRING", {"default": "", "placeholder": "Positive", "multiline": True}), + "negative": ("STRING", {"default": "", "placeholder": "Negative", "multiline": True}), + + "batch_size": ("INT", {"default": 1, "min": 1, "max": 64}), + }, + "optional": { + "model_override": ("MODEL",), + "vae_override": ("VAE",), + "optional_lora_stack": ("LORA_STACK",), + "auto_clean_gpu": ("BOOLEAN", {"default": False}), + }, + "hidden": {"prompt": "PROMPT", "my_unique_id": "UNIQUE_ID"} + } + + RETURN_TYPES = ("PIPE_LINE", "MODEL", "VAE") + RETURN_NAMES = ("pipe", "model", "vae") + + FUNCTION = "adv_pipeloader" + CATEGORY = "EasyUse/Loaders" + + def adv_pipeloader(self, unet_name, vae_name, chatglm3_name, lora_name, lora_model_strength, lora_clip_strength, resolution, empty_latent_width, empty_latent_height, positive, negative, batch_size, model_override=None, optional_lora_stack=None, vae_override=None, auto_clean_gpu=False, prompt=None, my_unique_id=None): + # load unet + if model_override: + model = model_override + else: + model = easyCache.load_kolors_unet(unet_name) + # load vae + if vae_override: + vae = vae_override + else: + vae = easyCache.load_vae(vae_name) + # load chatglm3 + chatglm3_model = easyCache.load_chatglm3(chatglm3_name) + # load lora + lora_stack = [] + if optional_lora_stack is not None: + for lora in optional_lora_stack: + lora = {"lora_name": lora[0], "model": model, "clip": None, "model_strength": lora[1], + "clip_strength": lora[2]} + model, _ = easyCache.load_lora(lora) + lora['model'] = model + lora['clip'] = None + lora_stack.append(lora) + + if lora_name != "None": + lora = {"lora_name": lora_name, "model": model, "clip": None, "model_strength": lora_model_strength, + "clip_strength": lora_clip_strength} + model, _ = easyCache.load_lora(lora) + lora_stack.append(lora) + + + # text encode + log_node_warn("Positive encoding...") + positive_embeddings_final = chatglm3_adv_text_encode(chatglm3_model, positive, auto_clean_gpu) + log_node_warn("Negative encoding...") + negative_embeddings_final = chatglm3_adv_text_encode(chatglm3_model, negative, auto_clean_gpu) + + # empty latent + samples = sampler.emptyLatent(resolution, empty_latent_width, empty_latent_height, batch_size) + + pipe = { + "model": model, + "chatglm3_model": chatglm3_model, + "positive": positive_embeddings_final, + "negative": negative_embeddings_final, + "vae": vae, + "clip": None, + + "samples": samples, + "images": None, + + "loader_settings": { + "unet_name": unet_name, + "vae_name": vae_name, + "chatglm3_name": chatglm3_name, + + "lora_name": lora_name, + "lora_model_strength": lora_model_strength, + "lora_clip_strength": lora_clip_strength, + + "positive": positive, + "negative": negative, + "resolution": resolution, + "empty_latent_width": empty_latent_width, + "empty_latent_height": empty_latent_height, + "batch_size": batch_size, + "auto_clean_gpu": auto_clean_gpu, + } + } + + return {"ui": {}, + "result": (pipe, model, vae, chatglm3_model, positive_embeddings_final, negative_embeddings_final, samples)} + + + return (chatglm3_model, None, None) + +# Flux Loader +class fluxLoader(fullLoader): + @classmethod + def INPUT_TYPES(cls): + checkpoints = folder_paths.get_filename_list("checkpoints") + loras = ["None"] + folder_paths.get_filename_list("loras") + return { + "required": { + "ckpt_name": (checkpoints + ['None'],), + "vae_name": (["Baked VAE"] + folder_paths.get_filename_list("vae"),), + "lora_name": (loras,), + "lora_model_strength": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}), + "lora_clip_strength": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}), + "resolution": (resolution_strings, {"default": "1024 x 1024"}), + "empty_latent_width": ("INT", {"default": 1024, "min": 64, "max": MAX_RESOLUTION, "step": 8}), + "empty_latent_height": ("INT", {"default": 1024, "min": 64, "max": MAX_RESOLUTION, "step": 8}), + + "positive": ("STRING", {"default": "", "placeholder": "Positive", "multiline": True}), + + "batch_size": ("INT", {"default": 1, "min": 1, "max": 64}), + }, + "optional": { + "model_override": ("MODEL",), + "clip_override": ("CLIP",), + "vae_override": ("VAE",), + "optional_lora_stack": ("LORA_STACK",), + "optional_controlnet_stack": ("CONTROL_NET_STACK",), + }, + "hidden": {"prompt": "PROMPT", "my_unique_id": "UNIQUE_ID"} + } + + RETURN_TYPES = ("PIPE_LINE", "MODEL", "VAE") + RETURN_NAMES = ("pipe", "model", "vae") + + FUNCTION = "fluxloader" + CATEGORY = "EasyUse/Loaders" + + def fluxloader(self, ckpt_name, vae_name, + lora_name, lora_model_strength, lora_clip_strength, + resolution, empty_latent_width, empty_latent_height, + positive, batch_size, model_override=None, clip_override=None, vae_override=None, optional_lora_stack=None, optional_controlnet_stack=None, + a1111_prompt_style=False, prompt=None, + my_unique_id=None): + + if positive == '': + positive = None + + return super().adv_pipeloader(ckpt_name, 'Default', vae_name, 0, + lora_name, lora_model_strength, lora_clip_strength, + resolution, empty_latent_width, empty_latent_height, + positive, 'none', 'comfy', + None, 'none', 'comfy', + batch_size, model_override, clip_override, vae_override, optional_lora_stack=optional_lora_stack, + optional_controlnet_stack=optional_controlnet_stack, + a1111_prompt_style=a1111_prompt_style, prompt=prompt, + my_unique_id=my_unique_id) + + +# Dit Loader +from ..modules.dit.pixArt.config import pixart_conf, pixart_res + +class pixArtLoader: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "ckpt_name": (folder_paths.get_filename_list("checkpoints"),), + "model_name":(list(pixart_conf.keys()),), + "vae_name": (folder_paths.get_filename_list("vae"),), + "t5_type": (['sd3'],), + "clip_name": (folder_paths.get_filename_list("clip"),), + "padding": ("INT", {"default": 1, "min": 1, "max": 300}), + "t5_name": (folder_paths.get_filename_list("t5"),), + "device": (["auto", "cpu", "gpu"], {"default": "cpu"}), + "dtype": (["default", "auto (comfy)", "FP32", "FP16", "BF16"],), + + "lora_name": (["None"] + folder_paths.get_filename_list("loras"),), + "lora_model_strength": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}), + + "ratio": (["custom"] + list(pixart_res["PixArtMS_XL_2"].keys()), {"default":"1.00"}), + "empty_latent_width": ("INT", {"default": 1024, "min": 64, "max": MAX_RESOLUTION, "step": 8}), + "empty_latent_height": ("INT", {"default": 1024, "min": 64, "max": MAX_RESOLUTION, "step": 8}), + + "positive": ("STRING", {"default": "", "placeholder": "Positive", "multiline": True}), + "negative": ("STRING", {"default": "", "placeholder": "Negative", "multiline": True}), + + "batch_size": ("INT", {"default": 1, "min": 1, "max": 64}), + }, + "optional":{ + "optional_lora_stack": ("LORA_STACK",), + }, + "hidden": {"prompt": "PROMPT", "my_unique_id": "UNIQUE_ID"} + } + + RETURN_TYPES = ("PIPE_LINE", "MODEL", "VAE") + RETURN_NAMES = ("pipe", "model", "vae") + FUNCTION = "pixart_pipeloader" + CATEGORY = "EasyUse/Loaders" + + def pixart_pipeloader(self, ckpt_name, model_name, vae_name, t5_type, clip_name, padding, t5_name, device, dtype, lora_name, lora_model_strength, ratio, empty_latent_width, empty_latent_height, positive, negative, batch_size, optional_lora_stack=None, prompt=None, my_unique_id=None): + # Clean models from loaded_objects + easyCache.update_loaded_objects(prompt) + + # load checkpoint + model = easyCache.load_dit_ckpt(ckpt_name=ckpt_name, model_name=model_name, pixart_conf=pixart_conf, + model_type='PixArt') + # load vae + vae = easyCache.load_vae(vae_name) + + # load t5 + if t5_type == 'sd3': + clip = easyCache.load_clip(clip_name=clip_name,type='sd3') + clip = easyCache.load_t5_from_sd3_clip(sd3_clip=clip, padding=padding) + lora_stack = None + if optional_lora_stack is not None: + for lora in optional_lora_stack: + lora = {"lora_name": lora[0], "model": model, "clip": clip, "model_strength": lora[1], + "clip_strength": lora[2]} + model, _ = easyCache.load_lora(lora, type='PixArt') + lora['model'] = model + lora['clip'] = clip + lora_stack.append(lora) + + if lora_name != "None": + lora = {"lora_name": lora_name, "model": model, "clip": clip, "model_strength": lora_model_strength, + "clip_strength": 1} + model, _ = easyCache.load_lora(lora, type='PixArt') + lora_stack.append(lora) + + positive_embeddings_final, = CLIPTextEncode().encode(clip, positive) + negative_embeddings_final, = CLIPTextEncode().encode(clip, negative) + else: + # todo t5v11 + positive_embeddings_final, negative_embeddings_final = None, None + clip = None + pass + + # Create Empty Latent + if ratio != 'custom': + if model_name in ['ControlPixArtMSHalf','PixArtMS_Sigma_XL_2_900M']: + res_name = 'PixArtMS_XL_2' + elif model_name in ['ControlPixArtHalf']: + res_name = 'PixArt_XL_2' + else: + res_name = model_name + width, height = pixart_res[res_name][ratio] + empty_latent_width = width + empty_latent_height = height + + latent = torch.zeros([batch_size, 4, empty_latent_height // 8, empty_latent_width // 8], device=sampler.device) + samples = {"samples": latent} + + log_node_warn("加载完毕...") + pipe = { + "model": model, + "positive": positive_embeddings_final, + "negative": negative_embeddings_final, + "vae": vae, + "clip": clip, + + "samples": samples, + "images": None, + + "loader_settings": { + "ckpt_name": ckpt_name, + "clip_name": clip_name, + "vae_name": vae_name, + "t5_name": t5_name, + + "positive": positive, + "negative": negative, + "ratio": ratio, + "empty_latent_width": empty_latent_width, + "empty_latent_height": empty_latent_height, + "batch_size": batch_size, + } + } + + return {"ui": {}, + "result": (pipe, model, vae, clip, positive_embeddings_final, negative_embeddings_final, samples)} + + +# Mochi加载器 +class mochiLoader(fullLoader): + @classmethod + def INPUT_TYPES(cls): + checkpoints = folder_paths.get_filename_list("checkpoints") + return { + "required": { + "ckpt_name": (checkpoints,), + "vae_name": (["Baked VAE"] + folder_paths.get_filename_list("vae"), {"default": "mochi_vae.safetensors"}), + + "positive": ("STRING", {"default":"", "placeholder": "Positive", "multiline": True}), + "negative": ("STRING", {"default":"", "placeholder": "Negative", "multiline": True}), + + "resolution": (resolution_strings, {"default": "width x height (custom)"}), + "empty_latent_width": ("INT", {"default": 848, "min": 64, "max": MAX_RESOLUTION, "step": 8}), + "empty_latent_height": ("INT", {"default": 480, "min": 64, "max": MAX_RESOLUTION, "step": 8}), + "length": ("INT", {"default": 25, "min": 7, "max": MAX_RESOLUTION, "step": 6}), + "batch_size": ("INT", {"default": 1, "min": 1, "max": 4096, "tooltip": "The number of latent images in the batch."}) + }, + "optional": { + "model_override": ("MODEL",), "clip_override": ("CLIP",), "vae_override": ("VAE",), + }, + "hidden": {"prompt": "PROMPT", "my_unique_id": "UNIQUE_ID"} + } + + RETURN_TYPES = ("PIPE_LINE", "MODEL", "VAE") + RETURN_NAMES = ("pipe", "model", "vae") + + FUNCTION = "mochiLoader" + CATEGORY = "EasyUse/Loaders" + + def mochiLoader(self, ckpt_name, vae_name, + positive, negative, + resolution, empty_latent_width, empty_latent_height, + length, batch_size, model_override=None, clip_override=None, vae_override=None, optional_lora_stack=None, optional_controlnet_stack=None, a1111_prompt_style=False, prompt=None, + my_unique_id=None): + + return super().adv_pipeloader(ckpt_name, 'Default', vae_name, 0, + "None", 1.0, 1.0, + resolution, empty_latent_width, empty_latent_height, + positive, 'none', 'comfy', + negative,'none','comfy', + batch_size, model_override, clip_override, vae_override, a1111_prompt_style=False, video_length=length, prompt=prompt, + my_unique_id=my_unique_id + ) +# lora +class loraSwitcher: + @classmethod + def INPUT_TYPES(s): + max_lora_num = 50 + inputs = { + "required": { + "toggle": ("BOOLEAN", {"label_on": "on", "label_off": "off"}), + "select": ("INT", {"default": 1, "min": 1, "max": max_lora_num}), + "num_loras": ("INT", {"default": 1, "min": 1, "max": max_lora_num}), + "lora_strength": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}) + }, + "optional": { + "optional_lora_stack": ("LORA_STACK",), + }, + } + + for i in range(1, max_lora_num + 1): + inputs["optional"][f"lora_{i}_name"] = ( + ["None"] + folder_paths.get_filename_list("loras"), {"default": "None"}) + + return inputs + + RETURN_TYPES = ("LORA_STACK", any_type) + RETURN_NAMES = ("lora_stack", "lora_name") + FUNCTION = "stack" + + CATEGORY = "EasyUse/Loaders" + + def stack(self, toggle, select,num_loras, lora_strength, optional_lora_stack=None, **kwargs): + if toggle in [False, None, "False"]: + return (optional_lora_stack, '') + + if not kwargs and optional_lora_stack is None: + return (None, '') + + loras = [] + + # Import Stack values + if optional_lora_stack is not None: + loras.extend([l for l in optional_lora_stack if l[0] != "None"]) + + # Import Lora values + lora_name = kwargs.get(f"lora_{select}_name") + + if not lora_name or lora_name == "None": + return (None,'') + + loras.append((lora_name, lora_strength, lora_strength)) + + name = os.path.splitext(os.path.basename(str(lora_name)))[0] + return (loras, name) + + +class loraStack: + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(s): + max_lora_num = 10 + inputs = { + "required": { + "toggle": ("BOOLEAN", {"label_on": "on", "label_off": "off"}), + "mode": (["simple", "advanced"],), + "num_loras": ("INT", {"default": 1, "min": 1, "max": max_lora_num}), + }, + "optional": { + "optional_lora_stack": ("LORA_STACK",), + }, + } + + for i in range(1, max_lora_num+1): + inputs["optional"][f"lora_{i}_name"] = ( + ["None"] + folder_paths.get_filename_list("loras"), {"default": "None"}) + inputs["optional"][f"lora_{i}_strength"] = ( + "FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}) + inputs["optional"][f"lora_{i}_model_strength"] = ( + "FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}) + inputs["optional"][f"lora_{i}_clip_strength"] = ( + "FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}) + + return inputs + + RETURN_TYPES = ("LORA_STACK",) + RETURN_NAMES = ("lora_stack",) + FUNCTION = "stack" + + CATEGORY = "EasyUse/Loaders" + + def stack(self, toggle, mode, num_loras, optional_lora_stack=None, **kwargs): + if toggle in [False, None, "False"]: + return (optional_lora_stack,) + + if not kwargs and optional_lora_stack is None: + return (None,) + + loras = [] + + # Import Stack values + if optional_lora_stack is not None: + loras.extend([l for l in optional_lora_stack if l[0] != "None"]) + + # Import Lora values + for i in range(1, num_loras + 1): + lora_name = kwargs.get(f"lora_{i}_name") + + if not lora_name or lora_name == "None": + continue + + if mode == "simple": + lora_strength = float(kwargs.get(f"lora_{i}_strength")) + loras.append((lora_name, lora_strength, lora_strength)) + elif mode == "advanced": + model_strength = float(kwargs.get(f"lora_{i}_model_strength")) + clip_strength = float(kwargs.get(f"lora_{i}_clip_strength")) + loras.append((lora_name, model_strength, clip_strength)) + return (loras,) + +class controlnetStack: + + + @classmethod + def INPUT_TYPES(s): + max_cn_num = 3 + inputs = { + "required": { + "toggle": ("BOOLEAN", {"label_on": "enabled", "label_off": "disabled"}), + "mode": (["simple", "advanced"],), + "num_controlnet": ("INT", {"default": 1, "min": 1, "max": max_cn_num}), + }, + "optional": { + "optional_controlnet_stack": ("CONTROL_NET_STACK",), + } + } + + for i in range(1, max_cn_num+1): + inputs["optional"][f"controlnet_{i}"] = (["None"] + folder_paths.get_filename_list("controlnet"), {"default": "None"}) + inputs["optional"][f"controlnet_{i}_strength"] = ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01},) + inputs["optional"][f"start_percent_{i}"] = ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001},) + inputs["optional"][f"end_percent_{i}"] = ("FLOAT",{"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.001},) + inputs["optional"][f"scale_soft_weight_{i}"] = ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.001},) + inputs["optional"][f"image_{i}"] = ("IMAGE",) + return inputs + + RETURN_TYPES = ("CONTROL_NET_STACK",) + RETURN_NAMES = ("controlnet_stack",) + FUNCTION = "stack" + CATEGORY = "EasyUse/Loaders" + + def stack(self, toggle, mode, num_controlnet, optional_controlnet_stack=None, **kwargs): + if toggle in [False, None, "False"]: + return (optional_controlnet_stack,) + + if not kwargs and optional_controlnet_stack is None: + return (None,) + + controlnets = [] + + # Import Stack values + if optional_controlnet_stack is not None: + controlnets.extend([l for l in optional_controlnet_stack if l[0] != "None"]) + + # Import Controlnet values + for i in range(1, num_controlnet+1): + controlnet_name = kwargs.get(f"controlnet_{i}") + + if not controlnet_name or controlnet_name == "None": + continue + + controlnet_strength = float(kwargs.get(f"controlnet_{i}_strength")) + start_percent = float(kwargs.get(f"start_percent_{i}")) if mode == "advanced" else 0 + end_percent = float(kwargs.get(f"end_percent_{i}")) if mode == "advanced" else 1.0 + scale_soft_weights = float(kwargs.get(f"scale_soft_weight_{i}")) + image = kwargs.get(f"image_{i}") + + controlnets.append((controlnet_name, controlnet_strength, start_percent, end_percent, scale_soft_weights, image, True)) + + return (controlnets,) +# controlnet +class controlnetSimple: + @classmethod + def INPUT_TYPES(s): + + return { + "required": { + "pipe": ("PIPE_LINE",), + "image": ("IMAGE",), + "control_net_name": (folder_paths.get_filename_list("controlnet"),), + }, + "optional": { + "control_net": ("CONTROL_NET",), + "strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}), + "scale_soft_weights": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.001},), + } + } + + RETURN_TYPES = ("PIPE_LINE", "CONDITIONING", "CONDITIONING") + RETURN_NAMES = ("pipe", "positive", "negative") + + FUNCTION = "controlnetApply" + CATEGORY = "EasyUse/Loaders" + + def controlnetApply(self, pipe, image, control_net_name, control_net=None, strength=1, scale_soft_weights=1, union_type=None): + + positive, negative = easyControlnet().apply(control_net_name, image, pipe["positive"], pipe["negative"], strength, 0, 1, control_net, scale_soft_weights, mask=None, easyCache=easyCache, model=pipe['model'], vae=pipe['vae']) + + new_pipe = { + "model": pipe['model'], + "positive": positive, + "negative": negative, + "vae": pipe['vae'], + "clip": pipe['clip'], + + "samples": pipe["samples"], + "images": pipe["images"], + "seed": 0, + + "loader_settings": pipe["loader_settings"] + } + + del pipe + return (new_pipe, positive, negative) + +# controlnetADV +class controlnetAdvanced: + + @classmethod + def INPUT_TYPES(s): + + return { + "required": { + "pipe": ("PIPE_LINE",), + "image": ("IMAGE",), + "control_net_name": (folder_paths.get_filename_list("controlnet"),), + }, + "optional": { + "control_net": ("CONTROL_NET",), + "strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}), + "start_percent": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001}), + "end_percent": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.001}), + "scale_soft_weights": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.001},), + } + } + + RETURN_TYPES = ("PIPE_LINE", "CONDITIONING", "CONDITIONING") + RETURN_NAMES = ("pipe", "positive", "negative") + + FUNCTION = "controlnetApply" + CATEGORY = "EasyUse/Loaders" + + + def controlnetApply(self, pipe, image, control_net_name, control_net=None, strength=1, start_percent=0, end_percent=1, scale_soft_weights=1): + positive, negative = easyControlnet().apply(control_net_name, image, pipe["positive"], pipe["negative"], + strength, start_percent, end_percent, control_net, scale_soft_weights, union_type=None, mask=None, easyCache=easyCache, model=pipe['model'], vae=pipe['vae']) + + new_pipe = { + "model": pipe['model'], + "positive": positive, + "negative": negative, + "vae": pipe['vae'], + "clip": pipe['clip'], + + "samples": pipe["samples"], + "images": image, + "seed": 0, + + "loader_settings": pipe["loader_settings"] + } + + del pipe + + return (new_pipe, positive, negative) + +# controlnetPlusPlus +class controlnetPlusPlus: + + @classmethod + def INPUT_TYPES(s): + + return { + "required": { + "pipe": ("PIPE_LINE",), + "image": ("IMAGE",), + "control_net_name": (folder_paths.get_filename_list("controlnet"),), + }, + "optional": { + "control_net": ("CONTROL_NET",), + "strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}), + "start_percent": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001}), + "end_percent": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.001}), + "scale_soft_weights": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.001},), + "union_type": (list(union_controlnet_types.keys()),) + } + } + + RETURN_TYPES = ("PIPE_LINE", "CONDITIONING", "CONDITIONING") + RETURN_NAMES = ("pipe", "positive", "negative") + + FUNCTION = "controlnetApply" + CATEGORY = "EasyUse/Loaders" + + + def controlnetApply(self, pipe, image, control_net_name, control_net=None, strength=1, start_percent=0, end_percent=1, scale_soft_weights=1, union_type=None): + if scale_soft_weights < 1: + if "ScaledSoftControlNetWeights" in ALL_NODE_CLASS_MAPPINGS: + soft_weight_cls = ALL_NODE_CLASS_MAPPINGS['ScaledSoftControlNetWeights'] + (weights, timestep_keyframe) = soft_weight_cls().load_weights(scale_soft_weights, False) + cn_adv_cls = ALL_NODE_CLASS_MAPPINGS['ACN_ControlNet++LoaderSingle'] + if union_type == 'auto': + union_type = 'none' + elif union_type == 'canny/lineart/anime_lineart/mlsd': + union_type = 'canny/lineart/mlsd' + elif union_type == 'repaint': + union_type = 'inpaint/outpaint' + control_net, = cn_adv_cls().load_controlnet_plusplus(control_net_name, union_type) + apply_adv_cls = ALL_NODE_CLASS_MAPPINGS['ACN_AdvancedControlNetApply'] + positive, negative, _ = apply_adv_cls().apply_controlnet(pipe["positive"], pipe["negative"], control_net, image, strength, start_percent, end_percent, timestep_kf=timestep_keyframe,) + else: + raise Exception( + f"[Advanced-ControlNet Not Found] you need to install 'COMFYUI-Advanced-ControlNet'") + else: + positive, negative = easyControlnet().apply(control_net_name, image, pipe["positive"], pipe["negative"], + strength, start_percent, end_percent, control_net, scale_soft_weights, union_type=union_type, mask=None, easyCache=easyCache, model=pipe['model']) + + new_pipe = { + "model": pipe['model'], + "positive": positive, + "negative": negative, + "vae": pipe['vae'], + "clip": pipe['clip'], + + "samples": pipe["samples"], + "images": pipe["images"], + "seed": 0, + + "loader_settings": pipe["loader_settings"] + } + + del pipe + + return (new_pipe, positive, negative) + +# LLLiteLoader +from ..libs.lllite import load_control_net_lllite_patch +class LLLiteLoader: + def __init__(self): + pass + @classmethod + def INPUT_TYPES(s): + def get_file_list(filenames): + return [file for file in filenames if file != "put_models_here.txt" and "lllite" in file] + + return { + "required": { + "model": ("MODEL",), + "model_name": (get_file_list(folder_paths.get_filename_list("controlnet")),), + "cond_image": ("IMAGE",), + "strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}), + "steps": ("INT", {"default": 0, "min": 0, "max": 200, "step": 1}), + "start_percent": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 100.0, "step": 0.1}), + "end_percent": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 100.0, "step": 0.1}), + } + } + + RETURN_TYPES = ("MODEL",) + FUNCTION = "load_lllite" + CATEGORY = "EasyUse/Loaders" + + def load_lllite(self, model, model_name, cond_image, strength, steps, start_percent, end_percent): + # cond_image is b,h,w,3, 0-1 + + model_path = os.path.join(folder_paths.get_full_path("controlnet", model_name)) + + model_lllite = model.clone() + patch = load_control_net_lllite_patch(model_path, cond_image, strength, steps, start_percent, end_percent) + if patch is not None: + model_lllite.set_model_attn1_patch(patch) + model_lllite.set_model_attn2_patch(patch) + + return (model_lllite,) + + +NODE_CLASS_MAPPINGS = { + "easy fullLoader": fullLoader, + "easy a1111Loader": a1111Loader, + "easy comfyLoader": comfyLoader, + "easy svdLoader": svdLoader, + "easy sv3dLoader": sv3dLoader, + "easy zero123Loader": zero123Loader, + "easy cascadeLoader": cascadeLoader, + "easy kolorsLoader": kolorsLoader, + "easy fluxLoader": fluxLoader, + "easy hunyuanDiTLoader": hunyuanDiTLoader, + "easy pixArtLoader": pixArtLoader, + "easy mochiLoader": mochiLoader, + "easy loraSwitcher": loraSwitcher, + "easy loraStack": loraStack, + "easy controlnetStack": controlnetStack, + "easy controlnetLoader": controlnetSimple, + "easy controlnetLoaderADV": controlnetAdvanced, + "easy controlnetLoader++": controlnetPlusPlus, + "easy LLLiteLoader": LLLiteLoader +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "easy fullLoader": "EasyLoader (Full)", + "easy a1111Loader": "EasyLoader (A1111)", + "easy comfyLoader": "EasyLoader (Comfy)", + "easy svdLoader": "EasyLoader (SVD)", + "easy sv3dLoader": "EasyLoader (SV3D)", + "easy zero123Loader": "EasyLoader (Zero123)", + "easy cascadeLoader": "EasyCascadeLoader", + "easy kolorsLoader": "EasyLoader (Kolors)", + "easy fluxLoader": "EasyLoader (Flux)", + "easy hunyuanDiTLoader": "EasyLoader (HunyuanDiT)", + "easy pixArtLoader": "EasyLoader (PixArt)", + "easy mochiLoader": "EasyLoader (Mochi)", + "easy loraSwitcher": "EasyLoraSwitcher", + "easy loraStack": "EasyLoraStack", + "easy controlnetStack": "EasyControlnetStack", + "easy controlnetLoader": "EasyControlnet", + "easy controlnetLoaderADV": "EasyControlnet (Advanced)", + "easy controlnetLoader++": "EasyControlnet++", + "easy LLLiteLoader": "EasyLLLite" +} \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/logic.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/logic.py new file mode 100644 index 0000000000000000000000000000000000000000..a124e9679b1659e46c3806d885ddd71bd10ea79c --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/logic.py @@ -0,0 +1,1711 @@ +from typing import Iterator, List, Tuple, Dict, Any, Union, Optional +from _decimal import Context, getcontext +from decimal import Decimal +from nodes import PreviewImage, SaveImage, NODE_CLASS_MAPPINGS as ALL_NODE_CLASS_MAPPINGS +from PIL import Image, ImageDraw, ImageFilter, ImageOps +from PIL.PngImagePlugin import PngInfo +from ..libs.utils import AlwaysEqualProxy, ByPassTypeTuple, cleanGPUUsedForce, compare_revision +from ..libs.cache import cache, update_cache, remove_cache +from ..libs.log import log_node_info, log_node_warn +from ..libs.math import evaluate_formula +import numpy as np +import time +import os +import re +import csv +import json +import torch +import comfy.utils +import folder_paths +from comfy_api.latest import io + +DEFAULT_FLOW_NUM = 2 +MAX_FLOW_NUM = 20 + +# kept for legacy compatibility but no longer used in node definitions +any_type = AlwaysEqualProxy("*") +lazy_options = {"lazy": True} + + +def validate_list_args(args: Dict[str, List[Any]]) -> Tuple[bool, None, None]: + if len(args) == 1: + return True, None, None + len_to_match = None + matched_arg_name = None + for arg_name, arg in args.items(): + if arg_name == "self": + continue + if len(arg) != 1: + if len_to_match is None: + len_to_match = len(arg) + matched_arg_name = arg_name + elif len(arg) != len_to_match: + return False, arg_name, matched_arg_name + return True, None, None + + +def error_if_mismatched_list_args(args: Dict[str, List[Any]]) -> None: + is_valid, failed_key1, failed_key2 = validate_list_args(args) + if not is_valid: + assert failed_key1 is not None + assert failed_key2 is not None + raise ValueError( + f"Mismatched list inputs received. {failed_key1}({len(args[failed_key1])}) !== {failed_key2}({len(args[failed_key2])})" + ) + + +def zip_with_fill(*lists: Union[List[Any], None]) -> Iterator[Tuple[Any, ...]]: + max_len = max(len(lst) if lst is not None else 0 for lst in lists) + for i in range(max_len): + yield tuple(None if lst is None else (lst[0] if len(lst) == 1 else lst[i]) for lst in lists) + + +# ---------------------------------------------------------------类型 开始----------------------------------------------------------------------# + +class String(io.ComfyNode): + @classmethod + def define_schema(cls): + return io.Schema( + node_id="easy string", + category="EasyUse/Logic/Type", + inputs=[io.String.Input("value", default="")], + outputs=[io.String.Output("string")], + ) + + @classmethod + def execute(cls, value): + return io.NodeOutput(value) + + +class Int(io.ComfyNode): + @classmethod + def define_schema(cls): + return io.Schema( + node_id="easy int", + category="EasyUse/Logic/Type", + inputs=[io.Int.Input("value", default=0, min=-999999, max=999999)], + outputs=[io.Int.Output("int")], + ) + + @classmethod + def execute(cls, value): + return io.NodeOutput(value) + + +class RangeInt(io.ComfyNode): + @classmethod + def define_schema(cls): + return io.Schema( + node_id="easy rangeInt", + category="EasyUse/Logic/Type", + is_input_list=True, + inputs=[ + io.Combo.Input("range_mode", options=["step", "num_steps"], default="step"), + io.Int.Input("start", default=0, min=-4096, max=4096, step=1), + io.Int.Input("stop", default=1, min=-4096, max=4096, step=1), + io.Int.Input("step", default=1, min=-4096, max=4096, step=1), + io.Int.Input("num_steps", default=0, min=-4096, max=4096, step=1), + io.Combo.Input("end_mode", options=["Inclusive", "Exclusive"], default="Inclusive"), + ], + outputs=[ + io.Int.Output("range", is_output_list=True), + io.Int.Output("range_sizes", is_output_list=True), + ], + ) + + @classmethod + def execute(cls, range_mode, start, stop, step, num_steps, end_mode): + input_args = {k: v for k, v in locals().items() if k not in ("self", "cls")} + error_if_mismatched_list_args(input_args) + ranges = [] + range_sizes = [] + for rm, e_start, e_stop, e_num_steps, e_step, e_end_mode in zip_with_fill( + range_mode, start, stop, num_steps, step, end_mode + ): + if rm == "step": + if e_step == 0: + vals = [e_start] + else: + if e_end_mode == "Inclusive": + e_stop += 1 + vals = list(range(e_start, e_stop, e_step)) + ranges.extend(vals) + range_sizes.append(len(vals)) + elif rm == "num_steps": + direction = 1 if e_stop > e_start else -1 + if e_end_mode == "Exclusive": + e_stop -= direction + vals = (np.rint(np.linspace(e_start, e_stop, e_num_steps)).astype(int).tolist()) + ranges.extend(vals) + range_sizes.append(len(vals)) + return io.NodeOutput(ranges, range_sizes) + + +class Float(io.ComfyNode): + @classmethod + def define_schema(cls): + return io.Schema( + node_id="easy float", + category="EasyUse/Logic/Type", + inputs=[io.Float.Input("value", default=0, step=0.01, min=-0xffffffffffffffff, max=0xffffffffffffffff)], + outputs=[io.Float.Output("float")], + ) + + @classmethod + def execute(cls, value): + return io.NodeOutput(round(value, 3)) + + +class RangeFloat(io.ComfyNode): + @classmethod + def define_schema(cls): + return io.Schema( + node_id="easy rangeFloat", + category="EasyUse/Logic/Type", + is_input_list=True, + inputs=[ + io.Combo.Input("range_mode", options=["step", "num_steps"], default="step"), + io.Float.Input("start", default=0, min=-4096, max=4096, step=0.1), + io.Float.Input("stop", default=1.0, min=-4096, max=4096, step=0.1), + io.Float.Input("step", default=1.0, min=-4096, max=4096, step=0.1), + io.Int.Input("num_steps", default=1, min=-4096, max=4096, step=1), + io.Combo.Input("end_mode", options=["Inclusive", "Exclusive"], default="Inclusive"), + ], + outputs=[ + io.Float.Output("range", is_output_list=True), + io.Int.Output("range_sizes", is_output_list=True), + ], + ) + + @staticmethod + def _decimal_range(range_mode, start, stop, step, num_steps, inclusive): + if range_mode == "step": + ret_val = start + if inclusive: + stop = stop + step + direction = 1 if step > 0 else -1 + while (ret_val - stop) * direction < 0: + yield float(ret_val) + ret_val += step + elif range_mode == "num_steps": + if num_steps is None or num_steps <= 0: + return + if num_steps == 1: + yield float(start) + return + if inclusive: + s = (stop - start) / Decimal(num_steps - 1) + for i in range(num_steps): + yield float(stop if i == num_steps - 1 else (start + s * Decimal(i))) + else: + s = (stop - start) / Decimal(num_steps) + for i in range(num_steps): + yield float(start + s * Decimal(i)) + + @classmethod + def execute(cls, range_mode, start, stop, step, num_steps, end_mode): + input_args = {k: v for k, v in locals().items() if k not in ("self", "cls")} + error_if_mismatched_list_args(input_args) + getcontext().prec = 12 + start = [round(Decimal(s), 2) for s in start] + stop = [round(Decimal(s), 2) for s in stop] + step = [round(Decimal(s), 2) for s in step] + ranges = [] + range_sizes = [] + for rm, e_start, e_stop, e_step, e_num_steps, e_end_mode in zip_with_fill( + range_mode, start, stop, step, num_steps, end_mode + ): + vals = list(cls._decimal_range(rm, e_start, e_stop, e_step, e_num_steps, e_end_mode == "Inclusive")) + ranges.extend(vals) + range_sizes.append(len(vals)) + return io.NodeOutput(ranges, range_sizes) + + +class Boolean(io.ComfyNode): + @classmethod + def define_schema(cls): + return io.Schema( + node_id="easy boolean", + category="EasyUse/Logic/Type", + inputs=[io.Boolean.Input("value", default=False)], + outputs=[io.Boolean.Output("boolean")], + ) + + @classmethod + def execute(cls, value): + return io.NodeOutput(value) + + +# ---------------------------------------------------------------开关 开始----------------------------------------------------------------------# + +class imageSwitch(io.ComfyNode): + @classmethod + def define_schema(cls): + return io.Schema( + node_id="easy imageSwitch", + category="EasyUse/Logic/Switch", + inputs=[ + io.Image.Input("image_a"), + io.Image.Input("image_b"), + io.Boolean.Input("boolean", default=False), + ], + outputs=[io.Image.Output("IMAGE")], + ) + + @classmethod + def execute(cls, image_a, image_b, boolean): + return io.NodeOutput(image_a if boolean else image_b) + + +class textSwitch(io.ComfyNode): + @classmethod + def define_schema(cls): + return io.Schema( + node_id="easy textSwitch", + category="EasyUse/Logic/Switch", + inputs=[ + io.Int.Input("input", default=1, min=1, max=2), + io.String.Input("text1", force_input=True, optional=True), + io.String.Input("text2", force_input=True, optional=True), + ], + outputs=[io.String.Output("STRING")], + ) + + @classmethod + def execute(cls, input, text1=None, text2=None): + return io.NodeOutput(text1 if input == 1 else text2) + + +# ---------------------------------------------------------------Index Switch----------------------------------------------------------------------# + +class ab(io.ComfyNode): + @classmethod + def define_schema(cls): + return io.Schema( + node_id="easy ab", + category="EasyUse/Logic", + inputs=[ + io.Boolean.Input("A or B", default=True, label_on="A", label_off="B"), + io.AnyType.Input("in"), + ], + outputs=[ + io.AnyType.Output("A"), + io.AnyType.Output("B"), + ], + hidden=[io.Hidden.unique_id], + ) + + @classmethod + def blocker(cls, value, block=False): + from comfy_execution.graph import ExecutionBlocker + return ExecutionBlocker(None) if block else value + + @classmethod + def execute(cls, **kwargs): + is_a = kwargs["A or B"] + a = cls.blocker(kwargs["in"], not is_a) + b = cls.blocker(kwargs["in"], is_a) + return io.NodeOutput(a, b) + + +class anythingInversedSwitch(io.ComfyNode): + @classmethod + def define_schema(cls): + return io.Schema( + node_id="easy anythingInversedSwitch", + category="EasyUse/Logic", + inputs=[ + io.Int.Input("index", default=0, min=0, max=9, step=1), + io.AnyType.Input("in"), + ], + outputs=[io.AnyType.Output("out%d" % i) for i in range(MAX_FLOW_NUM)], + hidden=[io.Hidden.unique_id], + ) + + @classmethod + def execute(cls, index, **kwargs): + from comfy_execution.graph import ExecutionBlocker + res = [kwargs["in"] if index == i else ExecutionBlocker(None) for i in range(MAX_FLOW_NUM)] + return io.NodeOutput(*res) + + +class anythingIndexSwitch(io.ComfyNode): + @classmethod + def define_schema(cls): + inputs = [io.Int.Input("index", default=0, min=0, max=9, step=1)] + for i in range(MAX_FLOW_NUM): + inputs.append(io.AnyType.Input("value%d" % i, optional=True, lazy=True)) + return io.Schema( + node_id="easy anythingIndexSwitch", + category="EasyUse/Logic/Index Switch", + inputs=inputs, + outputs=[io.AnyType.Output("value")], + ) + + @classmethod + def check_lazy_status(cls, index, **kwargs): + key = "value%d" % index + if kwargs.get(key, None) is None: + return [key] + + @classmethod + def execute(cls, index, **kwargs): + return io.NodeOutput(kwargs["value%d" % index]) + + +class imageIndexSwitch(io.ComfyNode): + @classmethod + def define_schema(cls): + inputs = [io.Int.Input("index", default=0, min=0, max=9, step=1)] + for i in range(MAX_FLOW_NUM): + inputs.append(io.Image.Input("image%d" % i, optional=True, lazy=True)) + return io.Schema( + node_id="easy imageIndexSwitch", + category="EasyUse/Logic/Index Switch", + inputs=inputs, + outputs=[io.Image.Output("image")], + ) + + @classmethod + def check_lazy_status(cls, index, **kwargs): + key = "image%d" % index + if kwargs.get(key, None) is None: + return [key] + + @classmethod + def execute(cls, index, **kwargs): + return io.NodeOutput(kwargs["image%d" % index]) + + +class textIndexSwitch(io.ComfyNode): + @classmethod + def define_schema(cls): + inputs = [io.Int.Input("index", default=0, min=0, max=9, step=1)] + for i in range(MAX_FLOW_NUM): + inputs.append(io.String.Input("text%d" % i, optional=True, lazy=True, force_input=True)) + return io.Schema( + node_id="easy textIndexSwitch", + category="EasyUse/Logic/Index Switch", + inputs=inputs, + outputs=[io.String.Output("text")], + ) + + @classmethod + def check_lazy_status(cls, index, **kwargs): + key = "text%d" % index + if kwargs.get(key, None) is None: + return [key] + + @classmethod + def execute(cls, index, **kwargs): + return io.NodeOutput(kwargs["text%d" % index]) + + +class conditioningIndexSwitch(io.ComfyNode): + @classmethod + def define_schema(cls): + inputs = [io.Int.Input("index", default=0, min=0, max=9, step=1)] + for i in range(MAX_FLOW_NUM): + inputs.append(io.Conditioning.Input("cond%d" % i, optional=True, lazy=True)) + return io.Schema( + node_id="easy conditioningIndexSwitch", + category="EasyUse/Logic/Index Switch", + inputs=inputs, + outputs=[io.Conditioning.Output("conditioning")], + ) + + @classmethod + def check_lazy_status(cls, index, **kwargs): + key = "cond%d" % index + if kwargs.get(key, None) is None: + return [key] + + @classmethod + def execute(cls, index, **kwargs): + return io.NodeOutput(kwargs["cond%d" % index]) + + +# ---------------------------------------------------------------Math----------------------------------------------------------------------# + +class mathIntOperation(io.ComfyNode): + @classmethod + def define_schema(cls): + return io.Schema( + node_id="easy mathInt", + category="EasyUse/Logic/Math", + inputs=[ + io.Int.Input("a", default=0, min=-0xffffffffffffffff, max=0xffffffffffffffff, step=1), + io.Int.Input("b", default=0, min=-0xffffffffffffffff, max=0xffffffffffffffff, step=1), + io.Combo.Input("operation", options=["add", "subtract", "multiply", "divide", "modulo", "power"]), + ], + outputs=[io.Int.Output("INT")], + ) + + @classmethod + def execute(cls, a, b, operation): + ops = { + "add": lambda: a + b, "subtract": lambda: a - b, "multiply": lambda: a * b, + "divide": lambda: a // b, "modulo": lambda: a % b, "power": lambda: a ** b, + } + return io.NodeOutput(ops[operation]()) + + +class mathFloatOperation(io.ComfyNode): + @classmethod + def define_schema(cls): + return io.Schema( + node_id="easy mathFloat", + category="EasyUse/Logic/Math", + inputs=[ + io.Float.Input("a", default=0, min=-999999999999.0, max=999999999999.0, step=0.01), + io.Float.Input("b", default=0, min=-999999999999.0, max=999999999999.0, step=0.01), + io.Combo.Input("operation", options=["add", "subtract", "multiply", "divide", "modulo", "power"]), + ], + outputs=[io.Float.Output("FLOAT")], + ) + + @classmethod + def execute(cls, a, b, operation): + ops = { + "add": lambda: round(a + b, 3), "subtract": lambda: round(a - b, 3), "multiply": lambda: round(a * b, 3), + "divide": lambda: round(a / b, 3), "modulo": lambda: round(a % b, 3), "power": lambda: round(a ** b, 3), + } + return io.NodeOutput(ops[operation]()) + + +class mathStringOperation(io.ComfyNode): + @classmethod + def define_schema(cls): + return io.Schema( + node_id="easy mathString", + category="EasyUse/Logic/Math", + inputs=[ + io.String.Input("a", multiline=False), + io.String.Input("b", multiline=False), + io.Combo.Input("operation", options=["a == b", "a != b", "a IN b", "a MATCH REGEX(b)", "a BEGINSWITH b", "a ENDSWITH b"]), + io.Boolean.Input("case_sensitive", default=True), + ], + outputs=[io.Boolean.Output("BOOLEAN")], + ) + + @classmethod + def execute(cls, a, b, operation, case_sensitive): + if not case_sensitive: + a = a.lower() + b = b.lower() + if operation == "a == b": + return io.NodeOutput(a == b) + elif operation == "a != b": + return io.NodeOutput(a != b) + elif operation == "a IN b": + return io.NodeOutput(a in b) + elif operation == "a MATCH REGEX(b)": + try: + return io.NodeOutput(re.match(b, a) is not None) + except Exception: + return io.NodeOutput(False) + elif operation == "a BEGINSWITH b": + return io.NodeOutput(a.startswith(b)) + elif operation == "a ENDSWITH b": + return io.NodeOutput(a.endswith(b)) + + +class simpleMath(io.ComfyNode): + """简单计算器节点,支持字符串数学公式计算""" + + @classmethod + def define_schema(cls): + return io.Schema( + node_id="easy simpleMath", + category="EasyUse/Logic/Math", + inputs=[ + io.String.Input("value", default="", placeholder="输入数学公式,如: a + b, pow(a, 2), ceil(a / b), floor(a * b), round(a / b, 2)"), + io.AnyType.Input("a", optional=True), + io.AnyType.Input("b", optional=True), + io.AnyType.Input("c", optional=True), + ], + outputs=[ + io.Int.Output("int"), + io.Float.Output("float"), + io.Boolean.Output("boolean"), + ], + ) + + @classmethod + def execute(cls, value, a=0, b=0, c=0): + try: + result = evaluate_formula(value, a, b, c) + if isinstance(result, list): + result_int = [int(r) for r in result] + return io.NodeOutput(result_int, result, [r != 0 for r in result]) + result_int = int(result) + return io.NodeOutput(result_int, result, result_int != 0) + except Exception as e: + log_node_warn(f"计算错误: {str(e)}") + return io.NodeOutput(0, 0.0, False) + + +class simpleMathDual(io.ComfyNode): + """双公式计算器节点,支持两个独立的数学公式计算""" + + @classmethod + def define_schema(cls): + return io.Schema( + node_id="easy simpleMathDual", + category="EasyUse/Logic/Math", + inputs=[ + io.String.Input("value1", default="", placeholder="输入数学公式1,如: a + b, pow(a, 2), ceil(a / b)"), + io.String.Input("value2", default="", placeholder="输入数学公式2,如: c * d, sqrt(c), floor(d / 2)"), + io.AnyType.Input("a", optional=True), + io.AnyType.Input("b", optional=True), + io.AnyType.Input("c", optional=True), + io.AnyType.Input("d", optional=True), + ], + outputs=[ + io.Int.Output("int1"), + io.Float.Output("float1"), + io.Int.Output("int2"), + io.Float.Output("float2"), + ], + ) + + @classmethod + def execute(cls, value1, value2, a=0, b=0, c=0, d=0): + try: + result1 = evaluate_formula(value1, a, b, c, d) + result1_int = [int(r) for r in result1] if isinstance(result1, list) else int(result1) + except Exception as e: + log_node_warn(f"公式1计算错误: {str(e)}") + result1 = 0.0 + result1_int = 0 + try: + result2 = evaluate_formula(value2, a, b, c, d) + result2_int = [int(r) for r in result2] if isinstance(result2, list) else int(result2) + except Exception as e: + log_node_warn(f"公式2计算错误: {str(e)}") + result2 = 0.0 + result2_int = 0 + return io.NodeOutput(result1_int, result1, result2_int, result2) + + +# ---------------------------------------------------------------Flow----------------------------------------------------------------------# +try: + from comfy_execution.graph_utils import GraphBuilder, is_link + from comfy_execution.graph import ExecutionBlocker +except Exception: + GraphBuilder = None + ExecutionBlocker = None + + +class whileLoopStart: + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + inputs = { + "required": { + "condition": ("BOOLEAN", {"default": True}), + }, + "optional": { + }, + } + for i in range(MAX_FLOW_NUM): + inputs["optional"]["initial_value%d" % i] = (any_type,) + return inputs + + RETURN_TYPES = ByPassTypeTuple(tuple(["FLOW_CONTROL"] + [any_type] * MAX_FLOW_NUM)) + RETURN_NAMES = ByPassTypeTuple(tuple(["flow"] + ["value%d" % i for i in range(MAX_FLOW_NUM)])) + FUNCTION = "while_loop_open" + + CATEGORY = "EasyUse/Logic/While Loop" + + def while_loop_open(self, condition, **kwargs): + values = [] + for i in range(MAX_FLOW_NUM): + values.append(kwargs.get("initial_value%d" % i, None) if condition else ExecutionBlocker(None)) + return tuple(["stub"] + values) + + +class whileLoopEnd: + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + inputs = { + "required": { + "flow": ("FLOW_CONTROL", {"rawLink": True}), + "condition": ("BOOLEAN", {}), + }, + "optional": { + }, + "hidden": { + "dynprompt": "DYNPROMPT", + "unique_id": "UNIQUE_ID", + "extra_pnginfo": "EXTRA_PNGINFO", + } + } + for i in range(MAX_FLOW_NUM): + inputs["optional"]["initial_value%d" % i] = (any_type,) + return inputs + + RETURN_TYPES = ByPassTypeTuple(tuple([any_type] * MAX_FLOW_NUM)) + RETURN_NAMES = ByPassTypeTuple(tuple(["value%d" % i for i in range(MAX_FLOW_NUM)])) + FUNCTION = "while_loop_close" + + CATEGORY = "EasyUse/Logic/While Loop" + + def explore_dependencies(self, node_id, dynprompt, upstream, parent_ids): + node_info = dynprompt.get_node(node_id) + if "inputs" not in node_info: + return + + for k, v in node_info["inputs"].items(): + if is_link(v): + parent_id = v[0] + display_id = dynprompt.get_display_node_id(parent_id) + display_node = dynprompt.get_node(display_id) + class_type = display_node["class_type"] + if class_type not in ['easy forLoopEnd', 'easy whileLoopEnd']: + parent_ids.append(display_id) + if parent_id not in upstream: + upstream[parent_id] = [] + self.explore_dependencies(parent_id, dynprompt, upstream, parent_ids) + + upstream[parent_id].append(node_id) + + def explore_output_nodes(self, dynprompt, upstream, output_nodes, parent_ids): + for parent_id in upstream: + display_id = dynprompt.get_display_node_id(parent_id) + for output_id in output_nodes: + id = output_nodes[output_id][0] + if id in parent_ids and display_id == id and output_id not in upstream[parent_id]: + if '.' in parent_id: + arr = parent_id.split('.') + arr[len(arr)-1] = output_id + upstream[parent_id].append('.'.join(arr)) + else: + upstream[parent_id].append(output_id) + + def collect_contained(self, node_id, upstream, contained): + if node_id not in upstream: + return + for child_id in upstream[node_id]: + if child_id not in contained: + contained[child_id] = True + self.collect_contained(child_id, upstream, contained) + + def while_loop_close(self, flow, condition, dynprompt=None, unique_id=None,**kwargs): + if not condition: + # We're done with the loop + values = [] + for i in range(MAX_FLOW_NUM): + values.append(kwargs.get("initial_value%d" % i, None)) + return tuple(values) + + # We want to loop + this_node = dynprompt.get_node(unique_id) + upstream = {} + # Get the list of all nodes between the open and close nodes + parent_ids = [] + self.explore_dependencies(unique_id, dynprompt, upstream, parent_ids) + parent_ids = list(set(parent_ids)) + # Get the list of all output nodes between the open and close nodes + prompts = dynprompt.get_original_prompt() + output_nodes = {} + for id in prompts: + node = prompts[id] + if "inputs" not in node: + continue + class_type = node["class_type"] + class_def = ALL_NODE_CLASS_MAPPINGS[class_type] + if hasattr(class_def, 'OUTPUT_NODE') and class_def.OUTPUT_NODE == True: + for k, v in node['inputs'].items(): + if is_link(v): + output_nodes[id] = v + + graph = GraphBuilder() + self.explore_output_nodes(dynprompt, upstream, output_nodes, parent_ids) + contained = {} + open_node = flow[0] + self.collect_contained(open_node, upstream, contained) + contained[unique_id] = True + contained[open_node] = True + + for node_id in contained: + original_node = dynprompt.get_node(node_id) + node = graph.node(original_node["class_type"], "Recurse" if node_id == unique_id else node_id) + node.set_override_display_id(node_id) + for node_id in contained: + original_node = dynprompt.get_node(node_id) + node = graph.lookup_node("Recurse" if node_id == unique_id else node_id) + for k, v in original_node["inputs"].items(): + if is_link(v) and v[0] in contained: + parent = graph.lookup_node(v[0]) + node.set_input(k, parent.out(v[1])) + else: + node.set_input(k, v) + + new_open = graph.lookup_node(open_node) + for i in range(MAX_FLOW_NUM): + key = "initial_value%d" % i + new_open.set_input(key, kwargs.get(key, None)) + my_clone = graph.lookup_node("Recurse") + result = map(lambda x: my_clone.out(x), range(MAX_FLOW_NUM)) + return { + "result": tuple(result), + "expand": graph.finalize(), + } + + +class forLoopStart: + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "total": ("INT", {"default": 1, "min": 1, "max": 100000, "step": 1}), + }, + "optional": { + "initial_value%d" % i: (any_type,) for i in range(1, MAX_FLOW_NUM) + }, + "hidden": { + "initial_value0": (any_type,), + "prompt": "PROMPT", + "extra_pnginfo": "EXTRA_PNGINFO", + "unique_id": "UNIQUE_ID" + } + } + + RETURN_TYPES = ByPassTypeTuple(tuple(["FLOW_CONTROL", "INT"] + [any_type] * (MAX_FLOW_NUM - 1))) + RETURN_NAMES = ByPassTypeTuple(tuple(["flow", "index"] + ["value%d" % i for i in range(1, MAX_FLOW_NUM)])) + FUNCTION = "for_loop_start" + + CATEGORY = "EasyUse/Logic/For Loop" + + def for_loop_start(self, total, prompt=None, extra_pnginfo=None, unique_id=None, **kwargs): + graph = GraphBuilder() + i = 0 + if "initial_value0" in kwargs: + i = kwargs["initial_value0"] + + initial_values = {("initial_value%d" % num): kwargs.get("initial_value%d" % num, None) for num in + range(1, MAX_FLOW_NUM)} + while_open = graph.node("easy whileLoopStart", condition=total, initial_value0=i, **initial_values) + outputs = [kwargs.get("initial_value%d" % num, None) for num in range(1, MAX_FLOW_NUM)] + return { + "result": tuple(["stub", i] + outputs), + "expand": graph.finalize(), + } + + +class forLoopEnd: + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "flow": ("FLOW_CONTROL", {"rawLink": True}), + }, + "optional": { + "initial_value%d" % i: (any_type, {"rawLink": True}) for i in range(1, MAX_FLOW_NUM) + }, + "hidden": { + "dynprompt": "DYNPROMPT", + "extra_pnginfo": "EXTRA_PNGINFO", + "unique_id": "UNIQUE_ID" + }, + } + + RETURN_TYPES = ByPassTypeTuple(tuple([any_type] * (MAX_FLOW_NUM - 1))) + RETURN_NAMES = ByPassTypeTuple(tuple(["value%d" % i for i in range(1, MAX_FLOW_NUM)])) + FUNCTION = "for_loop_end" + + CATEGORY = "EasyUse/Logic/For Loop" + + + + def for_loop_end(self, flow, dynprompt=None, extra_pnginfo=None, unique_id=None, **kwargs): + graph = GraphBuilder() + while_open = flow[0] + total = None + + # Using dynprompt to get the original node + forstart_node = dynprompt.get_node(while_open) + if forstart_node['class_type'] == 'easy forLoopStart': + inputs = forstart_node['inputs'] + total = inputs['total'] + elif forstart_node['class_type'] == 'easy loadImagesForLoop': + inputs = forstart_node['inputs'] + limit = inputs['limit'] + start_index = inputs['start_index'] + # Filter files by extension + directory = inputs['directory'] + total = graph.node('easy imagesCountInDirectory', directory=directory, limit=limit, start_index=start_index, extension='*').out(0) + + sub = graph.node("easy mathInt", operation="add", a=[while_open, 1], b=1) + cond = graph.node("easy compare", a=sub.out(0), b=total, comparison='a < b') + input_values = {("initial_value%d" % i): kwargs.get("initial_value%d" % i, None) for i in + range(1, MAX_FLOW_NUM)} + while_close = graph.node("easy whileLoopEnd", + flow=flow, + condition=cond.out(0), + initial_value0=sub.out(0), + **input_values) + return { + "result": tuple([while_close.out(i) for i in range(1, MAX_FLOW_NUM)]), + "expand": graph.finalize(), + } + +COMPARE_FUNCTIONS = { + "a == b": lambda a, b: a == b, + "a != b": lambda a, b: a != b, + "a < b": lambda a, b: a < b, + "a > b": lambda a, b: a > b, + "a <= b": lambda a, b: a <= b, + "a >= b": lambda a, b: a >= b, + "a > 0": lambda a, b: a > 0, + "a <= 0": lambda a, b: a <= 0, + "b > 0": lambda a, b: b > 0, + "b <= 0": lambda a, b: b <= 0, +} + +class Compare(io.ComfyNode): + @classmethod + def define_schema(cls): + return io.Schema( + node_id="easy compare", + category="EasyUse/Logic/Math", + inputs=[ + io.AnyType.Input("a", optional=True), + io.AnyType.Input("b", optional=True), + io.Combo.Input("comparison", options=list(COMPARE_FUNCTIONS.keys()), default="a == b", optional=True), + ], + outputs=[io.Boolean.Output("boolean")], + ) + + @classmethod + def execute(cls, a=0, b=0, comparison="a == b"): + return io.NodeOutput(COMPARE_FUNCTIONS[comparison](a, b)) + + +class IfElse(io.ComfyNode): + @classmethod + def define_schema(cls): + return io.Schema( + node_id="easy ifElse", + category="EasyUse/Logic", + inputs=[ + io.Boolean.Input("boolean"), + io.AnyType.Input("on_true", lazy=True), + io.AnyType.Input("on_false", lazy=True), + ], + outputs=[io.AnyType.Output("*")], + ) + + @classmethod + def check_lazy_status(cls, boolean=True, on_true=None, on_false=None): + if boolean and on_true is None: + return ["on_true"] + if not boolean and on_false is None: + return ["on_false"] + + @classmethod + def execute(cls, **kwargs): + return io.NodeOutput(kwargs["on_true"] if kwargs["boolean"] else kwargs["on_false"]) + + +class Blocker(io.ComfyNode): + @classmethod + def define_schema(cls): + return io.Schema( + node_id="easy blocker", + category="EasyUse/Logic", + inputs=[ + io.Boolean.Input("continue", default=False), + io.AnyType.Input("in",), + ], + outputs=[io.AnyType.Output("out")], + ) + + @classmethod + def execute(cls, **kwargs): + from comfy_execution.graph import ExecutionBlocker + return io.NodeOutput(kwargs["in"] if kwargs["continue"] else ExecutionBlocker(None)) + + +from comfy.sdxl_clip import SDXLClipModel, SDXLRefinerClipModel, SDXLClipG + + +class isMaskEmpty(io.ComfyNode): + @classmethod + def define_schema(cls): + return io.Schema( + node_id="easy isMaskEmpty", + category="EasyUse/Logic", + inputs=[io.Mask.Input("mask")], + outputs=[io.Boolean.Output("boolean")], + ) + + @classmethod + def execute(cls, mask): + if mask is None or torch.all(mask == 0): + return io.NodeOutput(True) + return io.NodeOutput(False) + + +class isNone(io.ComfyNode): + @classmethod + def define_schema(cls): + return io.Schema( + node_id="easy isNone", + category="EasyUse/Logic", + inputs=[io.AnyType.Input("any")], + outputs=[io.Boolean.Output("boolean")], + ) + + @classmethod + def execute(cls, any): + result = (isinstance(any, str) and any == "") or \ + (isinstance(any, (int, float)) and any == 0) or \ + any is None + return io.NodeOutput(result) + + +class isSDXL(io.ComfyNode): + @classmethod + def define_schema(cls): + return io.Schema( + node_id="easy isSDXL", + category="EasyUse/Logic", + inputs=[ + io.Custom(io_type="PIPE_LINE").Input("optional_pipe", optional=True), + io.Clip.Input("optional_clip", optional=True), + ], + outputs=[io.Boolean.Output("boolean")], + ) + + @classmethod + def execute(cls, optional_pipe=None, optional_clip=None): + if optional_pipe is None and optional_clip is None: + raise Exception("[ERROR] optional_pipe or optional_clip is missing") + clip = optional_clip if optional_clip is not None else optional_pipe["clip"] + return io.NodeOutput(isinstance(clip.cond_stage_model, (SDXLClipModel, SDXLRefinerClipModel, SDXLClipG))) + + +class isFileExist(io.ComfyNode): + @classmethod + def define_schema(cls): + return io.Schema( + node_id="easy isFileExist", + category="EasyUse/Logic", + inputs=[ + io.String.Input("file_path", default=""), + io.String.Input("file_name", default=""), + io.String.Input("file_extension", default=""), + ], + outputs=[io.Boolean.Output("boolean")], + ) + + @classmethod + def execute(cls, file_path, file_name, file_extension): + if not file_path: + raise Exception("file_path is missing") + if file_name: + file_path = os.path.join(file_path, file_name) + if file_extension: + file_path = file_path + "." + file_extension + return io.NodeOutput(os.path.exists(file_path) and os.path.isfile(file_path)) + + +from nodes import MAX_RESOLUTION +from ..config import BASE_RESOLUTIONS + + +class pixels(io.ComfyNode): + @classmethod + def define_schema(cls): + resolution_strings = [ + f"{width} x {height} (custom)" if width == "width" and height == "height" else f"{width} x {height}" + for width, height in BASE_RESOLUTIONS + ] + return io.Schema( + node_id="easy pixels", + category="EasyUse/Logic", + inputs=[ + io.Combo.Input("resolution", options=resolution_strings), + io.Int.Input("width", default=512, min=64, max=MAX_RESOLUTION, step=8), + io.Int.Input("height", default=512, min=64, max=MAX_RESOLUTION, step=8), + io.Float.Input("scale", default=2.000, min=0.001, max=10, step=0.001), + io.Boolean.Input("flip_w/h", default=False), + ], + outputs=[ + io.Int.Output("width_norm"), + io.Int.Output("height_norm"), + io.AnyType.Output("width"), + io.AnyType.Output("height"), + io.AnyType.Output("scale_factor"), + ], + ) + + @classmethod + def execute(cls, resolution, width, height, scale, **kwargs): + if resolution not in ["自定义 x 自定义", "width x height (custom)"]: + try: + _width, _height = map(int, resolution.split(" x ")) + width = _width + height = _height + except ValueError: + raise ValueError("Invalid base_resolution format.") + width = width * scale + height = height * scale + width_norm = int(width - width % 8) + height_norm = int(height - height % 8) + if kwargs.get("flip_w/h", False): + width, height = height, width + width_norm, height_norm = height_norm, width_norm + return io.NodeOutput(width_norm, height_norm, width, height, scale) + + +class xyAny(io.ComfyNode): + @classmethod + def define_schema(cls): + return io.Schema( + node_id="easy xyAny", + category="EasyUse/Logic", + is_input_list=True, + inputs=[ + io.AnyType.Input("X"), + io.AnyType.Input("Y"), + io.Combo.Input("direction", options=["horizontal", "vertical"], default="horizontal"), + ], + outputs=[ + io.AnyType.Output("X", is_output_list=True), + io.AnyType.Output("Y", is_output_list=True), + ], + ) + + @classmethod + def execute(cls, X, Y, direction): + new_x, new_y = [], [] + if direction[0] == "horizontal": + for y in Y: + for x in X: + new_x.append(x) + new_y.append(y) + else: + for x in X: + for y in Y: + new_x.append(x) + new_y.append(y) + return io.NodeOutput(new_x, new_y) + + +class lengthAnything(io.ComfyNode): + @classmethod + def define_schema(cls): + return io.Schema( + node_id="easy lengthAnything", + category="EasyUse/Logic", + is_input_list=True, + inputs=[io.AnyType.Input("any")], + outputs=[io.Int.Output("length")], + hidden=[io.Hidden.prompt, io.Hidden.unique_id], + ) + + @classmethod + def execute(cls, any, **kwargs): + prompt = cls.hidden.prompt + my_unique_id = cls.hidden.unique_id + if isinstance(prompt, list): + prompt = prompt[0] + if isinstance(my_unique_id, list): + my_unique_id = my_unique_id[0] + my_unique_id = my_unique_id.split(".")[-1] if "." in my_unique_id else my_unique_id + id, slot = prompt[my_unique_id]["inputs"]["any"] + class_type = prompt[id]["class_type"] + node_class = ALL_NODE_CLASS_MAPPINGS[class_type] + output_is_list = node_class.OUTPUT_IS_LIST[slot] if hasattr(node_class, "OUTPUT_IS_LIST") else False + return io.NodeOutput(len(any) if output_is_list or len(any) > 1 else len(any[0])) + + +class indexAnything(io.ComfyNode): + @classmethod + def define_schema(cls): + return io.Schema( + node_id="easy indexAnything", + category="EasyUse/Logic", + is_input_list=True, + inputs=[ + io.AnyType.Input("any"), + io.Int.Input("index", default=0, min=-1000000, max=1000000, step=1), + ], + outputs=[io.AnyType.Output("out")], + hidden=[io.Hidden.prompt, io.Hidden.unique_id], + ) + + @classmethod + def execute(cls, any, index, **kwargs): + if isinstance(index, list): + index = index[0] + prompt = cls.hidden.prompt + my_unique_id = cls.hidden.unique_id + if isinstance(prompt, list): + prompt = prompt[0] + if isinstance(my_unique_id, list): + my_unique_id = my_unique_id[0] + my_unique_id = my_unique_id.split(".")[-1] if "." in my_unique_id else my_unique_id + id, slot = prompt[my_unique_id]["inputs"]["any"] + class_type = prompt[id]["class_type"] + node_class = ALL_NODE_CLASS_MAPPINGS[class_type] + output_is_list = node_class.OUTPUT_IS_LIST[slot] if hasattr(node_class, "OUTPUT_IS_LIST") else False + + def normalize_index(idx, length): + if idx < 0: + idx = length + idx + return min(max(0, idx), length - 1) + + if output_is_list or len(any) > 1: + return io.NodeOutput(any[normalize_index(index, len(any))]) + elif isinstance(any[0], torch.Tensor): + idx = normalize_index(index, any[0].shape[0]) + return io.NodeOutput(any[0][idx:idx+1].clone()) + else: + if hasattr(any[0], "__len__") and len(any[0]) > 0: + return io.NodeOutput(any[0][normalize_index(index, len(any[0]))]) + return io.NodeOutput(any[0]) + + +class batchAnything(io.ComfyNode): + @classmethod + def define_schema(cls): + return io.Schema( + node_id="easy batchAnything", + category="EasyUse/Logic", + inputs=[ + io.AnyType.Input("any_1"), + io.AnyType.Input("any_2"), + ], + outputs=[io.AnyType.Output("batch")], + ) + + @classmethod + def latentBatch(cls, any_1, any_2): + samples_out = any_1.copy() + s1 = any_1["samples"] + s2 = any_2["samples"] + if s1.shape[1:] != s2.shape[1:]: + s2 = comfy.utils.common_upscale(s2, s1.shape[3], s1.shape[2], "bilinear", "center") + s = torch.cat((s1, s2), dim=0) + samples_out["samples"] = s + samples_out["batch_index"] = any_1.get("batch_index", list(range(s1.shape[0]))) + \ + any_2.get("batch_index", list(range(s2.shape[0]))) + return samples_out + + @classmethod + def execute(cls, any_1, any_2): + if isinstance(any_1, torch.Tensor) or isinstance(any_2, torch.Tensor): + if any_1 is None: + return io.NodeOutput(any_2) + if any_2 is None: + return io.NodeOutput(any_1) + if any_1.shape[1:] != any_2.shape[1:]: + any_2 = comfy.utils.common_upscale(any_2.movedim(-1, 1), any_1.shape[2], any_1.shape[1], "bilinear", "center").movedim(1, -1) + return io.NodeOutput(torch.cat((any_1, any_2), 0)) + elif isinstance(any_1, (str, float, int)): + if any_2 is None: + return io.NodeOutput(any_1) + elif isinstance(any_2, tuple): + return io.NodeOutput(any_2 + (any_1,)) + elif isinstance(any_2, list): + return io.NodeOutput(any_2 + [any_1]) + return io.NodeOutput([any_1, any_2]) + elif isinstance(any_2, (str, float, int)): + if any_1 is None: + return io.NodeOutput(any_2) + elif isinstance(any_1, tuple): + return io.NodeOutput(any_1 + (any_2,)) + elif isinstance(any_1, list): + return io.NodeOutput(any_1 + [any_2]) + return io.NodeOutput([any_2, any_1]) + elif isinstance(any_1, dict) and "samples" in any_1: + if any_2 is None: + return io.NodeOutput(any_1) + if isinstance(any_2, dict) and "samples" in any_2: + return io.NodeOutput(cls.latentBatch(any_1, any_2)) + elif isinstance(any_2, dict) and "samples" in any_2: + if any_1 is None: + return io.NodeOutput(any_2) + if isinstance(any_1, dict) and "samples" in any_1: + return io.NodeOutput(cls.latentBatch(any_2, any_1)) + if any_1 is None: + return io.NodeOutput(any_2) + if any_2 is None: + return io.NodeOutput(any_1) + return io.NodeOutput(any_1 + any_2) + + +class convertAnything(io.ComfyNode): + @classmethod + def define_schema(cls): + return io.Schema( + node_id="easy convertAnything", + category="EasyUse/Logic", + is_output_node=True, + inputs=[ + io.AnyType.Input("*"), + io.Combo.Input("output_type", options=["string", "int", "float", "boolean"], default="string"), + ], + outputs=[io.AnyType.Output("output")], + ) + + @classmethod + def execute(cls, **kwargs): + anything = kwargs["*"] + output_type = kwargs["output_type"] + converters = {"string": str, "int": int, "float": float, "boolean": bool} + return io.NodeOutput(converters[output_type](anything)) + + +class showAnything(io.ComfyNode): + @classmethod + def define_schema(cls): + return io.Schema( + node_id="easy showAnything", + category="EasyUse/Logic", + is_input_list=True, + is_output_node=True, + inputs=[io.AnyType.Input("anything", optional=True)], + outputs=[io.AnyType.Output("output")], + hidden=[io.Hidden.unique_id, io.Hidden.extra_pnginfo], + ) + + @classmethod + def execute(cls, **kwargs): + unique_id = cls.hidden.unique_id + extra_pnginfo = cls.hidden.extra_pnginfo + + values = [] + if "anything" in kwargs: + for val in kwargs["anything"]: + if isinstance(val, str): + values.append(val) + elif isinstance(val, (int, float, bool)): + values.append(str(val)) + elif isinstance(val, list) and len(val) <= 30: + values = val + elif val is not None: + try: + values.append(json.dumps(val, indent=4, ensure_ascii=False)) + except Exception: + try: + values.append(str(val)) + except Exception: + raise Exception("source exists, but could not be serialized.") + + if extra_pnginfo and isinstance(extra_pnginfo, dict) and "workflow" in extra_pnginfo: + _uid = unique_id[0] if isinstance(unique_id, list) else unique_id + node = next((x for x in extra_pnginfo["workflow"]["nodes"] if str(x["id"]) == _uid), None) + if node: + node["widgets_values"] = [values] + + result_val = values[0] if (isinstance(values, list) and len(values) == 1) else values + return io.NodeOutput(result_val, ui={"text": values}) + + +class showTensorShape(io.ComfyNode): + @classmethod + def define_schema(cls): + return io.Schema( + node_id="easy showTensorShape", + category="EasyUse/Logic", + is_output_node=True, + inputs=[io.AnyType.Input("tensor")], + outputs=[], + hidden=[io.Hidden.unique_id, io.Hidden.extra_pnginfo], + ) + + @classmethod + def execute(cls, tensor, **kwargs): + shapes = [] + + def tensorShape(t): + if isinstance(t, dict): + for k in t: + tensorShape(t[k]) + elif isinstance(t, list): + for item in t: + tensorShape(item) + elif hasattr(t, "shape"): + shapes.append(list(t.shape)) + + tensorShape(tensor) + return io.NodeOutput(ui={"text": shapes}) + + +class stringToIntList(io.ComfyNode): + @classmethod + def define_schema(cls): + return io.Schema( + node_id="easy stringToIntList", + category="EasyUse/Logic", + inputs=[io.String.Input("string", default="1, 2, 3", multiline=True)], + outputs=[io.Int.Output("INT")], + ) + + @classmethod + def execute(cls, string): + return io.NodeOutput([int(x.strip()) for x in string.split(",")]) + + +class stringToFloatList(io.ComfyNode): + @classmethod + def define_schema(cls): + return io.Schema( + node_id="easy stringToFloatList", + category="EasyUse/Logic", + inputs=[io.String.Input("string", default="1, 2, 3", multiline=True)], + outputs=[io.Float.Output("FLOAT")], + ) + + @classmethod + def execute(cls, string): + return io.NodeOutput([float(x.strip()) for x in string.split(",")]) + + +class stringJoinLines(io.ComfyNode): + @classmethod + def define_schema(cls): + return io.Schema( + node_id="easy stringJoinLines", + category="EasyUse/Logic", + inputs=[ + io.String.Input("string", default="", multiline=True), + io.String.Input("delimiter", default=" | "), + ], + outputs=[io.String.Output("STRING")], + ) + + @classmethod + def execute(cls, string, delimiter): + lines = [line.strip() for line in string.split("\n") if line.strip()] + return io.NodeOutput(delimiter.join(lines)) + + +class outputToList(io.ComfyNode): + @classmethod + def define_schema(cls): + return io.Schema( + node_id="easy outputToList", + category="EasyUse/Logic", + inputs=[io.AnyType.Input("tuple")], + outputs=[io.AnyType.Output("list", is_output_list=True)], + ) + + @classmethod + def execute(cls, tuple): + return io.NodeOutput(tuple) + + +class cleanGPUUsed(io.ComfyNode): + @classmethod + def define_schema(cls): + return io.Schema( + node_id="easy cleanGpuUsed", + category="EasyUse/Logic", + is_output_node=True, + inputs=[io.AnyType.Input("anything")], + outputs=[io.AnyType.Output("output")], + hidden=[io.Hidden.unique_id, io.Hidden.extra_pnginfo], + ) + + @classmethod + def execute(cls, anything, **kwargs): + cleanGPUUsedForce() + return io.NodeOutput(anything) + + +class clearCacheKey(io.ComfyNode): + @classmethod + def define_schema(cls): + return io.Schema( + node_id="easy clearCacheKey", + category="EasyUse/Logic", + is_output_node=True, + inputs=[ + io.AnyType.Input("anything"), + io.String.Input("cache_key", default="*"), + ], + outputs=[io.AnyType.Output("output")], + hidden=[io.Hidden.unique_id, io.Hidden.extra_pnginfo], + ) + + @classmethod + def execute(cls, anything, cache_key, **kwargs): + remove_cache(cache_key) + return io.NodeOutput(anything) + + +class clearCacheAll(io.ComfyNode): + @classmethod + def define_schema(cls): + return io.Schema( + node_id="easy clearCacheAll", + category="EasyUse/Logic", + is_output_node=True, + inputs=[io.AnyType.Input("anything")], + outputs=[io.AnyType.Output("output")], + hidden=[io.Hidden.unique_id, io.Hidden.extra_pnginfo], + ) + + @classmethod + def execute(cls, anything, **kwargs): + remove_cache("*") + return io.NodeOutput(anything) + + +class saveText(io.ComfyNode): + + @classmethod + def define_schema(cls): + return io.Schema( + node_id="easy saveText", + category="EasyUse/Logic", + is_output_node=True, + inputs=[ + io.String.Input("text", default="", force_input=True), + io.String.Input("output_file_path", multiline=False, default=""), + io.String.Input("file_name", multiline=False, default=""), + io.Combo.Input("file_extension", options=["txt", "csv"]), + io.Boolean.Input("overwrite", default=True), + io.Image.Input("image", optional=True), + ], + outputs=[ + io.String.Output("text"), + io.Image.Output("image"), + ], + ) + + @classmethod + def save_image(cls, images, filename_prefix="", extension="png", quality=100, prompt=None, + extra_pnginfo=None, delimiter="_", filename_number_start="true", number_padding=4, + overwrite_mode="prefix_as_filename", output_path="", show_history="true", + show_previews="true", embed_workflow="true", lossless_webp=False): + for image in images: + i = 255. * image.cpu().numpy() + img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8)) + if extension == "webp": + img_exif = img.getexif() + workflow_metadata = "" + if prompt is not None: + img_exif[0x010f] = "Prompt:" + json.dumps(prompt) + if extra_pnginfo is not None: + for x in extra_pnginfo: + workflow_metadata += json.dumps(extra_pnginfo[x]) + img_exif[0x010e] = "Workflow:" + workflow_metadata + exif_data = img_exif.tobytes() + else: + metadata = PngInfo() + if embed_workflow == "true": + if prompt is not None: + metadata.add_text("prompt", json.dumps(prompt)) + if extra_pnginfo is not None: + for x in extra_pnginfo: + metadata.add_text(x, json.dumps(extra_pnginfo[x])) + exif_data = metadata + file = f"{filename_prefix}.{extension}" + try: + output_file = os.path.abspath(os.path.join(output_path, file)) + if extension in ["jpg", "jpeg"]: + img.save(output_file, quality=quality, optimize=True) + elif extension == "webp": + img.save(output_file, quality=quality, lossless=lossless_webp, exif=exif_data) + elif extension == "png": + img.save(output_file, pnginfo=exif_data, optimize=True) + elif extension == "bmp": + img.save(output_file) + elif extension == "tiff": + img.save(output_file, quality=quality, optimize=True) + else: + img.save(output_file, pnginfo=exif_data, optimize=True) + except Exception as e: + print(e) + + @classmethod + def execute(cls, text, output_file_path, file_name, file_extension, overwrite, image=None, **kwargs): + if isinstance(file_name, list): + file_name = file_name[0] + + if output_file_path == "" or file_name == "": + log_node_warn("Save Text", "No file details found. No file output.") + return io.NodeOutput(text, None) + + filepath = os.path.join(output_file_path, file_name) + "." + file_extension + if not os.path.exists(output_file_path): + os.makedirs(output_file_path) + + file_mode = "w" if overwrite else "a" + log_node_info("Save Text", f"Saving to {filepath}") + + if file_extension == "csv": + with open(filepath, file_mode, newline="", encoding="utf-8") as csv_file: + csv_writer = csv.writer(csv_file) + for line in [i.strip() for i in text.split("\n")]: + csv_writer.writerow([line]) + else: + with open(filepath, file_mode, newline="", encoding="utf-8") as text_file: + for line in text: + text_file.write(line) + + result_image = None + if image is not None: + imagepath = os.path.join(output_file_path, file_name) + index = 1 + if not overwrite: + while os.path.exists(filepath): + imagepath = os.path.join(output_file_path, file_name) + "_" + str(index) + index += 1 + + output_dir = folder_paths.output_directory + output_path_val = "" if output_file_path in [None, "", "none", "."] else output_file_path + if not os.path.isabs(output_file_path): + output_path_val = os.path.join(output_dir, output_path_val) + if output_path_val.strip(): + if not os.path.isabs(output_path_val): + output_path_val = os.path.join(folder_paths.output_directory, output_path_val) + if not os.path.exists(output_path_val.strip()): + print(f"The path `{output_path_val.strip()}` does not exist! Creating directory.") + os.makedirs(output_path_val, exist_ok=True) + + images_tensor = torch.cat([image], dim=0) + cls.save_image(images_tensor, imagepath, "png", 100, None, None, + filename_number_start="true", output_path=output_path_val, + delimiter="_", number_padding=4, lossless_webp=False) + log_node_info("Save Text", f"Saving Image to {imagepath}") + result_image = image + + return io.NodeOutput(text, result_image) + + +class sleep(io.ComfyNode): + @classmethod + def define_schema(cls): + return io.Schema( + node_id="easy sleep", + category="EasyUse/Logic", + inputs=[ + io.AnyType.Input("any"), + io.Float.Input("delay", default=1.0, min=0.0, max=1000000, step=0.1), + ], + outputs=[io.AnyType.Output("out")], + ) + + @classmethod + def execute(cls, any, delay): + time.sleep(delay) + return io.NodeOutput(any) + + +NODE_CLASS_MAPPINGS = { + "easy string": String, + "easy int": Int, + "easy rangeInt": RangeInt, + "easy float": Float, + "easy rangeFloat": RangeFloat, + "easy boolean": Boolean, + "easy mathString": mathStringOperation, + "easy mathInt": mathIntOperation, + "easy mathFloat": mathFloatOperation, + "easy simpleMath": simpleMath, + "easy simpleMathDual": simpleMathDual, + "easy compare": Compare, + "easy imageSwitch": imageSwitch, + "easy textSwitch": textSwitch, + "easy imageIndexSwitch": imageIndexSwitch, + "easy textIndexSwitch": textIndexSwitch, + "easy conditioningIndexSwitch": conditioningIndexSwitch, + "easy anythingIndexSwitch": anythingIndexSwitch, + "easy ab": ab, + "easy anythingInversedSwitch": anythingInversedSwitch, + "easy whileLoopStart": whileLoopStart, + "easy whileLoopEnd": whileLoopEnd, + "easy forLoopStart": forLoopStart, + "easy forLoopEnd": forLoopEnd, + "easy blocker": Blocker, + "easy ifElse": IfElse, + "easy isMaskEmpty": isMaskEmpty, + "easy isNone": isNone, + "easy isSDXL": isSDXL, + "easy isFileExist": isFileExist, + "easy stringToIntList": stringToIntList, + "easy stringToFloatList": stringToFloatList, + "easy stringJoinLines": stringJoinLines, + "easy outputToList": outputToList, + "easy pixels": pixels, + "easy xyAny": xyAny, + "easy lengthAnything": lengthAnything, + "easy indexAnything": indexAnything, + "easy batchAnything": batchAnything, + "easy convertAnything": convertAnything, + "easy showAnything": showAnything, + "easy showTensorShape": showTensorShape, + "easy clearCacheKey": clearCacheKey, + "easy clearCacheAll": clearCacheAll, + "easy cleanGpuUsed": cleanGPUUsed, + "easy saveText": saveText, + "easy sleep": sleep, +} +NODE_DISPLAY_NAME_MAPPINGS = { + "easy string": "String", + "easy int": "Int", + "easy rangeInt": "Range(Int)", + "easy float": "Float", + "easy rangeFloat": "Range(Float)", + "easy boolean": "Boolean", + "easy compare": "Compare", + "easy mathString": "Math String", + "easy mathInt": "Math Int", + "easy mathFloat": "Math Float", + "easy simpleMath": "Simple Math", + "easy simpleMathDual": "Simple Math Dual", + "easy imageSwitch": "Image Switch", + "easy textSwitch": "Text Switch", + "easy imageIndexSwitch": "Image Index Switch", + "easy textIndexSwitch": "Text Index Switch", + "easy conditioningIndexSwitch": "Conditioning Index Switch", + "easy anythingIndexSwitch": "Any Index Switch", + "easy ab": "A or B", + "easy anythingInversedSwitch": "Any Inversed Switch", + "easy whileLoopStart": "While Loop Start", + "easy whileLoopEnd": "While Loop End", + "easy forLoopStart": "For Loop Start", + "easy forLoopEnd": "For Loop End", + "easy ifElse": "If else", + "easy blocker": "Blocker", + "easy isMaskEmpty": "Is Mask Empty", + "easy isNone": "Is None", + "easy isSDXL": "Is SDXL", + "easy isFileExist": "Is File Exist", + "easy stringToIntList": "String to Int List", + "easy stringToFloatList": "String to Float List", + "easy stringJoinLines": "String Join Lines", + "easy outputToList": "Output to List", + "easy pixels": "Pixels W/H Norm", + "easy xyAny": "XY Any", + "easy lengthAnything": "Length Any", + "easy indexAnything": "Index Any", + "easy batchAnything": "Batch Any", + "easy convertAnything": "Convert Any", + "easy showAnything": "Show Any", + "easy showTensorShape": "Show Tensor Shape", + "easy clearCacheKey": "Clear Cache Key", + "easy clearCacheAll": "Clear Cache All", + "easy cleanGpuUsed": "Clean VRAM Used", + "easy saveText": "Save Text", + "easy sleep": "Sleep", +} diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/pipe.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/pipe.py new file mode 100644 index 0000000000000000000000000000000000000000..65225eb8d32d2c4fcf0f36e2cdcacb73b6feb38e --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/pipe.py @@ -0,0 +1,782 @@ +import os +import folder_paths +import comfy.samplers, comfy.supported_models + +from nodes import LatentFromBatch, RepeatLatentBatch +from ..config import MAX_SEED_NUM + +from ..libs.log import log_node_warn +from ..libs.utils import get_sd_version +from ..libs.conditioning import prompt_to_cond, set_cond + +from .. import easyCache + +# 节点束输入 +class pipeIn: + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(s): + return { + "required": {}, + "optional": { + "pipe": ("PIPE_LINE",), + "model": ("MODEL",), + "pos": ("CONDITIONING",), + "neg": ("CONDITIONING",), + "latent": ("LATENT",), + "vae": ("VAE",), + "clip": ("CLIP",), + "image": ("IMAGE",), + "xyPlot": ("XYPLOT",), + }, + "hidden": {"my_unique_id": "UNIQUE_ID"}, + } + + RETURN_TYPES = ("PIPE_LINE",) + RETURN_NAMES = ("pipe",) + FUNCTION = "flush" + + CATEGORY = "EasyUse/Pipe" + + def flush(self, pipe=None, model=None, pos=None, neg=None, latent=None, vae=None, clip=None, image=None, xyplot=None, my_unique_id=None): + + model = model if model is not None else pipe.get("model") + if model is None: + log_node_warn(f'pipeIn[{my_unique_id}]', "Model missing from pipeLine") + pos = pos if pos is not None else pipe.get("positive") + if pos is None: + log_node_warn(f'pipeIn[{my_unique_id}]', "Pos Conditioning missing from pipeLine") + neg = neg if neg is not None else pipe.get("negative") + if neg is None: + log_node_warn(f'pipeIn[{my_unique_id}]', "Neg Conditioning missing from pipeLine") + vae = vae if vae is not None else pipe.get("vae") + if vae is None: + log_node_warn(f'pipeIn[{my_unique_id}]', "VAE missing from pipeLine") + clip = clip if clip is not None else pipe.get("clip") if pipe is not None and "clip" in pipe else None + # if clip is None: + # log_node_warn(f'pipeIn[{my_unique_id}]', "Clip missing from pipeLine") + if latent is not None: + samples = latent + elif image is None: + samples = pipe.get("samples") if pipe is not None else None + image = pipe.get("images") if pipe is not None else None + elif image is not None: + if pipe is None: + batch_size = 1 + else: + batch_size = pipe["loader_settings"]["batch_size"] if "batch_size" in pipe["loader_settings"] else 1 + samples = {"samples": vae.encode(image[:, :, :, :3])} + samples = RepeatLatentBatch().repeat(samples, batch_size)[0] + + if pipe is None: + pipe = {"loader_settings": {"positive": "", "negative": "", "xyplot": None}} + + xyplot = xyplot if xyplot is not None else pipe['loader_settings']['xyplot'] if xyplot in pipe['loader_settings'] else None + + new_pipe = { + **pipe, + "model": model, + "positive": pos, + "negative": neg, + "vae": vae, + "clip": clip, + + "samples": samples, + "images": image, + "seed": pipe.get('seed') if pipe is not None and "seed" in pipe else None, + + "loader_settings": { + **pipe["loader_settings"], + "xyplot": xyplot + } + } + del pipe + + return (new_pipe,) + +# 节点束输出 +class pipeOut: + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "pipe": ("PIPE_LINE",), + }, + "hidden": {"my_unique_id": "UNIQUE_ID"}, + } + + RETURN_TYPES = ("PIPE_LINE", "MODEL", "CONDITIONING", "CONDITIONING", "LATENT", "VAE", "CLIP", "IMAGE", "INT",) + RETURN_NAMES = ("pipe", "model", "pos", "neg", "latent", "vae", "clip", "image", "seed",) + FUNCTION = "flush" + + CATEGORY = "EasyUse/Pipe" + + def flush(self, pipe, my_unique_id=None): + model = pipe.get("model") + pos = pipe.get("positive") + neg = pipe.get("negative") + latent = pipe.get("samples") + vae = pipe.get("vae") + clip = pipe.get("clip") + image = pipe.get("images") + seed = pipe.get("seed") + + return pipe, model, pos, neg, latent, vae, clip, image, seed + +# 编辑节点束 +class pipeEdit: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "clip_skip": ("INT", {"default": -1, "min": -24, "max": 0, "step": 1}), + + "optional_positive": ("STRING", {"default": "", "multiline": True}), + "positive_token_normalization": (["none", "mean", "length", "length+mean"],), + "positive_weight_interpretation": (["comfy", "A1111", "comfy++", "compel", "fixed attention"],), + + "optional_negative": ("STRING", {"default": "", "multiline": True}), + "negative_token_normalization": (["none", "mean", "length", "length+mean"],), + "negative_weight_interpretation": (["comfy", "A1111", "comfy++", "compel", "fixed attention"],), + + "a1111_prompt_style": ("BOOLEAN", {"default": False}), + "conditioning_mode": (['replace', 'concat', 'combine', 'average', 'timestep'], {"default": "replace"}), + "average_strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}), + "old_cond_start": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001}), + "old_cond_end": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.001}), + "new_cond_start": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001}), + "new_cond_end": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.001}), + }, + "optional": { + "pipe": ("PIPE_LINE",), + "model": ("MODEL",), + "pos": ("CONDITIONING",), + "neg": ("CONDITIONING",), + "latent": ("LATENT",), + "vae": ("VAE",), + "clip": ("CLIP",), + "image": ("IMAGE",), + }, + "hidden": {"my_unique_id": "UNIQUE_ID", "prompt":"PROMPT"}, + } + + RETURN_TYPES = ("PIPE_LINE", "MODEL", "CONDITIONING", "CONDITIONING", "LATENT", "VAE", "CLIP", "IMAGE") + RETURN_NAMES = ("pipe", "model", "pos", "neg", "latent", "vae", "clip", "image") + FUNCTION = "edit" + + CATEGORY = "EasyUse/Pipe" + + def edit(self, clip_skip, optional_positive, positive_token_normalization, positive_weight_interpretation, optional_negative, negative_token_normalization, negative_weight_interpretation, a1111_prompt_style, conditioning_mode, average_strength, old_cond_start, old_cond_end, new_cond_start, new_cond_end, pipe=None, model=None, pos=None, neg=None, latent=None, vae=None, clip=None, image=None, my_unique_id=None, prompt=None): + + model = model if model is not None else pipe.get("model") + if model is None: + log_node_warn(f'pipeIn[{my_unique_id}]', "Model missing from pipeLine") + vae = vae if vae is not None else pipe.get("vae") + if vae is None: + log_node_warn(f'pipeIn[{my_unique_id}]', "VAE missing from pipeLine") + clip = clip if clip is not None else pipe.get("clip") + if clip is None: + log_node_warn(f'pipeIn[{my_unique_id}]', "Clip missing from pipeLine") + if image is None: + image = pipe.get("images") if pipe is not None else None + samples = latent if latent is not None else pipe.get("samples") + if samples is None: + log_node_warn(f'pipeIn[{my_unique_id}]', "Latent missing from pipeLine") + else: + batch_size = pipe["loader_settings"]["batch_size"] if "batch_size" in pipe["loader_settings"] else 1 + samples = {"samples": vae.encode(image[:, :, :, :3])} + samples = RepeatLatentBatch().repeat(samples, batch_size)[0] + + pipe_lora_stack = pipe.get("lora_stack") if pipe is not None and "lora_stack" in pipe else [] + + steps = pipe["loader_settings"]["steps"] if "steps" in pipe["loader_settings"] else 1 + if pos is None and optional_positive != '': + pos, positive_wildcard_prompt, model, clip = prompt_to_cond('positive', model, clip, clip_skip, + pipe_lora_stack, optional_positive, positive_token_normalization,positive_weight_interpretation, + a1111_prompt_style, my_unique_id, prompt, easyCache, True, steps) + pos = set_cond(pipe['positive'], pos, conditioning_mode, average_strength, old_cond_start, old_cond_end, new_cond_start, new_cond_end) + pipe['loader_settings']['positive'] = positive_wildcard_prompt + pipe['loader_settings']['positive_token_normalization'] = positive_token_normalization + pipe['loader_settings']['positive_weight_interpretation'] = positive_weight_interpretation + if a1111_prompt_style: + pipe['loader_settings']['a1111_prompt_style'] = True + else: + pos = pipe.get("positive") + if pos is None: + log_node_warn(f'pipeIn[{my_unique_id}]', "Pos Conditioning missing from pipeLine") + + if neg is None and optional_negative != '': + neg, negative_wildcard_prompt, model, clip = prompt_to_cond("negative", model, clip, clip_skip, pipe_lora_stack, optional_negative, + negative_token_normalization, negative_weight_interpretation, + a1111_prompt_style, my_unique_id, prompt, easyCache, True, steps) + neg = set_cond(pipe['negative'], neg, conditioning_mode, average_strength, old_cond_start, old_cond_end, new_cond_start, new_cond_end) + pipe['loader_settings']['negative'] = negative_wildcard_prompt + pipe['loader_settings']['negative_token_normalization'] = negative_token_normalization + pipe['loader_settings']['negative_weight_interpretation'] = negative_weight_interpretation + if a1111_prompt_style: + pipe['loader_settings']['a1111_prompt_style'] = True + else: + neg = pipe.get("negative") + if neg is None: + log_node_warn(f'pipeIn[{my_unique_id}]', "Neg Conditioning missing from pipeLine") + if pipe is None: + pipe = {"loader_settings": {"positive": "", "negative": "", "xyplot": None}} + + new_pipe = { + **pipe, + "model": model, + "positive": pos, + "negative": neg, + "vae": vae, + "clip": clip, + + "samples": samples, + "images": image, + "seed": pipe.get('seed') if pipe is not None and "seed" in pipe else None, + "loader_settings":{ + **pipe["loader_settings"] + } + } + del pipe + + return (new_pipe, model,pos, neg, latent, vae, clip, image) + +# 编辑节点束提示词 +class pipeEditPrompt: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "pipe": ("PIPE_LINE",), + "positive": ("STRING", {"default": "", "multiline": True}), + "negative": ("STRING", {"default": "", "multiline": True}), + }, + "hidden": {"my_unique_id": "UNIQUE_ID", "prompt": "PROMPT"}, + } + + RETURN_TYPES = ("PIPE_LINE",) + RETURN_NAMES = ("pipe",) + FUNCTION = "edit" + + CATEGORY = "EasyUse/Pipe" + + def edit(self, pipe, positive, negative, my_unique_id=None, prompt=None): + model = pipe.get("model") + if model is None: + log_node_warn(f'pipeEdit[{my_unique_id}]', "Model missing from pipeLine") + + from ..modules.kolors.loader import is_kolors_model + model_type = get_sd_version(model) + if model_type == 'sdxl' and is_kolors_model(model): + from ..modules.kolors.text_encode import chatglm3_adv_text_encode + auto_clean_gpu = pipe["loader_settings"]["auto_clean_gpu"] if "auto_clean_gpu" in pipe["loader_settings"] else False + chatglm3_model = pipe["chatglm3_model"] if "chatglm3_model" in pipe else None + # text encode + log_node_warn("Positive encoding...") + positive_embeddings_final = chatglm3_adv_text_encode(chatglm3_model, positive, auto_clean_gpu) + log_node_warn("Negative encoding...") + negative_embeddings_final = chatglm3_adv_text_encode(chatglm3_model, negative, auto_clean_gpu) + else: + clip_skip = pipe["loader_settings"]["clip_skip"] if "clip_skip" in pipe["loader_settings"] else -1 + lora_stack = pipe.get("lora_stack") if pipe is not None and "lora_stack" in pipe else [] + clip = pipe.get("clip") if pipe is not None and "clip" in pipe else None + positive_token_normalization = pipe["loader_settings"]["positive_token_normalization"] if "positive_token_normalization" in pipe["loader_settings"] else "none" + positive_weight_interpretation = pipe["loader_settings"]["positive_weight_interpretation"] if "positive_weight_interpretation" in pipe["loader_settings"] else "comfy" + negative_token_normalization = pipe["loader_settings"]["negative_token_normalization"] if "negative_token_normalization" in pipe["loader_settings"] else "none" + negative_weight_interpretation = pipe["loader_settings"]["negative_weight_interpretation"] if "negative_weight_interpretation" in pipe["loader_settings"] else "comfy" + a1111_prompt_style = pipe["loader_settings"]["a1111_prompt_style"] if "a1111_prompt_style" in pipe["loader_settings"] else False + # Prompt to Conditioning + positive_embeddings_final, positive_wildcard_prompt, model, clip = prompt_to_cond('positive', model, clip, + clip_skip, lora_stack, + positive, + positive_token_normalization, + positive_weight_interpretation, + a1111_prompt_style, + my_unique_id, prompt, + easyCache, + model_type=model_type) + negative_embeddings_final, negative_wildcard_prompt, model, clip = prompt_to_cond('negative', model, clip, + clip_skip, lora_stack, + negative, + negative_token_normalization, + negative_weight_interpretation, + a1111_prompt_style, + my_unique_id, prompt, + easyCache, + model_type=model_type) + new_pipe = { + **pipe, + "model": model, + "positive": positive_embeddings_final, + "negative": negative_embeddings_final, + } + del pipe + + return (new_pipe,) + + +# 节点束到基础节点束(pipe to ComfyUI-Impack-pack's basic_pipe) +class pipeToBasicPipe: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "pipe": ("PIPE_LINE",), + }, + "hidden": {"my_unique_id": "UNIQUE_ID"}, + } + + RETURN_TYPES = ("BASIC_PIPE",) + RETURN_NAMES = ("basic_pipe",) + FUNCTION = "doit" + + CATEGORY = "EasyUse/Pipe" + + def doit(self, pipe, my_unique_id=None): + new_pipe = (pipe.get('model'), pipe.get('clip'), pipe.get('vae'), pipe.get('positive'), pipe.get('negative')) + del pipe + return (new_pipe,) + +# 批次索引 +class pipeBatchIndex: + @classmethod + def INPUT_TYPES(s): + return {"required": {"pipe": ("PIPE_LINE",), + "batch_index": ("INT", {"default": 0, "min": 0, "max": 63}), + "length": ("INT", {"default": 1, "min": 1, "max": 64}), + }, + "hidden": {"my_unique_id": "UNIQUE_ID"},} + + RETURN_TYPES = ("PIPE_LINE",) + RETURN_NAMES = ("pipe",) + FUNCTION = "doit" + + CATEGORY = "EasyUse/Pipe" + + def doit(self, pipe, batch_index, length, my_unique_id=None): + samples = pipe["samples"] + new_samples, = LatentFromBatch().frombatch(samples, batch_index, length) + new_pipe = { + **pipe, + "samples": new_samples + } + del pipe + return (new_pipe,) + +# pipeXYPlot +class pipeXYPlot: + lora_list = ["None"] + folder_paths.get_filename_list("loras") + lora_strengths = {"min": -4.0, "max": 4.0, "step": 0.01} + token_normalization = ["none", "mean", "length", "length+mean"] + weight_interpretation = ["comfy", "A1111", "compel", "comfy++"] + + loader_dict = { + "ckpt_name": folder_paths.get_filename_list("checkpoints"), + "vae_name": ["Baked-VAE"] + folder_paths.get_filename_list("vae"), + "clip_skip": {"min": -24, "max": -1, "step": 1}, + "lora_name": lora_list, + "lora_model_strength": lora_strengths, + "lora_clip_strength": lora_strengths, + "positive": [], + "negative": [], + } + + sampler_dict = { + "steps": {"min": 1, "max": 100, "step": 1}, + "cfg": {"min": 0.0, "max": 100.0, "step": 1.0}, + "sampler_name": comfy.samplers.KSampler.SAMPLERS, + "scheduler": comfy.samplers.KSampler.SCHEDULERS, + "denoise": {"min": 0.0, "max": 1.0, "step": 0.01}, + "seed": {"min": 0, "max": MAX_SEED_NUM}, + } + + plot_dict = {**sampler_dict, **loader_dict} + + plot_values = ["None", ] + plot_values.append("---------------------") + for k in sampler_dict: + plot_values.append(f'preSampling: {k}') + plot_values.append("---------------------") + for k in loader_dict: + plot_values.append(f'loader: {k}') + + def __init__(self): + pass + + rejected = ["None", "---------------------", "Nothing"] + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "grid_spacing": ("INT", {"min": 0, "max": 500, "step": 5, "default": 0, }), + "output_individuals": (["False", "True"], {"default": "False"}), + "flip_xy": (["False", "True"], {"default": "False"}), + "x_axis": (pipeXYPlot.plot_values, {"default": 'None'}), + "x_values": ( + "STRING", {"default": '', "multiline": True, "placeholder": 'insert values seperated by "; "'}), + "y_axis": (pipeXYPlot.plot_values, {"default": 'None'}), + "y_values": ( + "STRING", {"default": '', "multiline": True, "placeholder": 'insert values seperated by "; "'}), + }, + "optional": { + "pipe": ("PIPE_LINE",) + }, + "hidden": { + "plot_dict": (pipeXYPlot.plot_dict,), + }, + } + + RETURN_TYPES = ("PIPE_LINE",) + RETURN_NAMES = ("pipe",) + FUNCTION = "plot" + + CATEGORY = "EasyUse/Pipe" + + def plot(self, grid_spacing, output_individuals, flip_xy, x_axis, x_values, y_axis, y_values, pipe=None, font_path=None): + def clean_values(values): + original_values = values.split("; ") + cleaned_values = [] + + for value in original_values: + # Strip the semi-colon + cleaned_value = value.strip(';').strip() + + if cleaned_value == "": + continue + + # Try to convert the cleaned_value back to int or float if possible + try: + cleaned_value = int(cleaned_value) + except ValueError: + try: + cleaned_value = float(cleaned_value) + except ValueError: + pass + + # Append the cleaned_value to the list + cleaned_values.append(cleaned_value) + + return cleaned_values + + if x_axis in self.rejected: + x_axis = "None" + x_values = [] + else: + x_values = clean_values(x_values) + + if y_axis in self.rejected: + y_axis = "None" + y_values = [] + else: + y_values = clean_values(y_values) + + if flip_xy == "True": + x_axis, y_axis = y_axis, x_axis + x_values, y_values = y_values, x_values + + + xy_plot = {"x_axis": x_axis, + "x_vals": x_values, + "y_axis": y_axis, + "y_vals": y_values, + "custom_font": font_path, + "grid_spacing": grid_spacing, + "output_individuals": output_individuals} + + if pipe is not None: + new_pipe = pipe.copy() + new_pipe['loader_settings'] = { + **pipe['loader_settings'], + "xyplot": xy_plot + } + del pipe + return (new_pipe, xy_plot,) + +# pipeXYPlotAdvanced +import platform +class pipeXYPlotAdvanced: + if platform.system() == "Windows": + system_root = os.environ.get("SystemRoot") + user_root = os.environ.get("USERPROFILE") + font_dir = os.path.join(system_root, "Fonts") if system_root else None + user_font_dir = os.path.join(user_root, "AppData","Local","Microsoft","Windows", "Fonts") if user_root else None + + # Default debian-based Linux & MacOS font dirs + elif platform.system() == "Linux": + font_dir = "/usr/share/fonts/truetype" + user_font_dir = None + elif platform.system() == "Darwin": + font_dir = "/System/Library/Fonts" + user_font_dir = None + else: + font_dir = None + user_font_dir = None + + @classmethod + def INPUT_TYPES(s): + files_list = [] + if s.font_dir and os.path.exists(s.font_dir): + font_dir = s.font_dir + files_list = files_list + [f for f in os.listdir(font_dir) if os.path.isfile(os.path.join(font_dir, f)) and f.lower().endswith(".ttf")] + + if s.user_font_dir and os.path.exists(s.user_font_dir): + files_list = files_list + [f for f in os.listdir(s.user_font_dir) if os.path.isfile(os.path.join(s.user_font_dir, f)) and f.lower().endswith(".ttf")] + + return { + "required": { + "pipe": ("PIPE_LINE",), + "grid_spacing": ("INT", {"min": 0, "max": 500, "step": 5, "default": 0, }), + "output_individuals": (["False", "True"], {"default": "False"}), + "flip_xy": (["False", "True"], {"default": "False"}), + }, + "optional": { + "X": ("X_Y",), + "Y": ("X_Y",), + "font": (["None"] + files_list,) + }, + "hidden": {"my_unique_id": "UNIQUE_ID"} + } + + RETURN_TYPES = ("PIPE_LINE",) + RETURN_NAMES = ("pipe",) + FUNCTION = "plot" + + CATEGORY = "EasyUse/Pipe" + + def plot(self, pipe, grid_spacing, output_individuals, flip_xy, X=None, Y=None, font=None, my_unique_id=None): + font_path = os.path.join(self.font_dir, font) if font != "None" else None + if font_path and not os.path.exists(font_path): + font_path = os.path.join(self.user_font_dir, font) + + if X is not None: + if isinstance(X, tuple): + X = X[0] + x_axis = X.get('axis') + x_values = X.get('values') + else: + x_axis = "Nothing" + x_values = [""] + if Y is not None: + if isinstance(Y, tuple): + Y = Y[0] + y_axis = Y.get('axis') + y_values = Y.get('values') + else: + y_axis = "Nothing" + y_values = [""] + + if pipe is not None: + new_pipe = pipe.copy() + positive = pipe["loader_settings"]["positive"] if "positive" in pipe["loader_settings"] else "" + negative = pipe["loader_settings"]["negative"] if "negative" in pipe["loader_settings"] else "" + + if x_axis == 'advanced: ModelMergeBlocks': + models = X.get('models') + vae_use = X.get('vae_use') + if models is None: + raise Exception("models is not found") + new_pipe['loader_settings'] = { + **pipe['loader_settings'], + "models": models, + "vae_use": vae_use + } + if y_axis == 'advanced: ModelMergeBlocks': + models = Y.get('models') + vae_use = Y.get('vae_use') + if models is None: + raise Exception("models is not found") + new_pipe['loader_settings'] = { + **pipe['loader_settings'], + "models": models, + "vae_use": vae_use + } + + if x_axis in ['advanced: Lora', 'advanced: Checkpoint']: + lora_stack = X.get('lora_stack') + _lora_stack = [] + if lora_stack is not None: + for lora in lora_stack: + _lora_stack.append( + {"lora_name": lora[0], "model": pipe['model'], "clip": pipe['clip'], "model_strength": lora[1], + "clip_strength": lora[2]}) + del lora_stack + x_values = "; ".join(x_values) + lora_stack = pipe['lora_stack'] + _lora_stack if 'lora_stack' in pipe else _lora_stack + new_pipe['loader_settings'] = { + **pipe['loader_settings'], + "lora_stack": lora_stack, + } + + if y_axis in ['advanced: Lora', 'advanced: Checkpoint']: + lora_stack = Y.get('lora_stack') + _lora_stack = [] + if lora_stack is not None: + for lora in lora_stack: + _lora_stack.append( + {"lora_name": lora[0], "model": pipe['model'], "clip": pipe['clip'], "model_strength": lora[1], + "clip_strength": lora[2]}) + del lora_stack + y_values = "; ".join(y_values) + lora_stack = pipe['lora_stack'] + _lora_stack if 'lora_stack' in pipe else _lora_stack + new_pipe['loader_settings'] = { + **pipe['loader_settings'], + "lora_stack": lora_stack, + } + + if x_axis == 'advanced: Seeds++ Batch': + seed = new_pipe.get('seed') or 0 + value = x_values + x_values = [] + for index in range(value): + x_values.append(str(seed + index)) + x_values = "; ".join(x_values) + if y_axis == 'advanced: Seeds++ Batch': + seed = new_pipe.get('seed') or 0 + value = y_values + y_values = [] + for index in range(value): + y_values.append(str(seed + index)) + y_values = "; ".join(y_values) + + if x_axis == 'advanced: Positive Prompt S/R': + if positive: + x_value = x_values + x_values = [] + for index, value in enumerate(x_value): + search_txt, replace_txt, replace_all = value + if replace_all: + txt = replace_txt if replace_txt is not None else positive + x_values.append(txt) + else: + txt = positive.replace(search_txt, replace_txt, 1) if replace_txt is not None else positive + x_values.append(txt) + x_values = "; ".join(x_values) + if y_axis == 'advanced: Positive Prompt S/R': + if positive: + y_value = y_values + y_values = [] + for index, value in enumerate(y_value): + search_txt, replace_txt, replace_all = value + if replace_all: + txt = replace_txt if replace_txt is not None else positive + y_values.append(txt) + else: + txt = positive.replace(search_txt, replace_txt, 1) if replace_txt is not None else positive + y_values.append(txt) + y_values = "; ".join(y_values) + + if x_axis == 'advanced: Negative Prompt S/R': + if negative: + x_value = x_values + x_values = [] + for index, value in enumerate(x_value): + search_txt, replace_txt, replace_all = value + if replace_all: + txt = replace_txt if replace_txt is not None else negative + x_values.append(txt) + else: + txt = negative.replace(search_txt, replace_txt, 1) if replace_txt is not None else negative + x_values.append(txt) + x_values = "; ".join(x_values) + if y_axis == 'advanced: Negative Prompt S/R': + if negative: + y_value = y_values + y_values = [] + for index, value in enumerate(y_value): + search_txt, replace_txt, replace_all = value + if replace_all: + txt = replace_txt if replace_txt is not None else negative + y_values.append(txt) + else: + txt = negative.replace(search_txt, replace_txt, 1) if replace_txt is not None else negative + y_values.append(txt) + y_values = "; ".join(y_values) + + if "advanced: ControlNet" in x_axis: + x_value = x_values + x_values = [] + cnet = [] + for index, value in enumerate(x_value): + cnet.append(value) + x_values.append(str(index)) + x_values = "; ".join(x_values) + new_pipe['loader_settings'] = { + **pipe['loader_settings'], + "cnet_stack": cnet, + } + + if "advanced: ControlNet" in y_axis: + y_value = y_values + y_values = [] + cnet = [] + for index, value in enumerate(y_value): + cnet.append(value) + y_values.append(str(index)) + y_values = "; ".join(y_values) + new_pipe['loader_settings'] = { + **pipe['loader_settings'], + "cnet_stack": cnet, + } + + if "advanced: Pos Condition" in x_axis: + x_values = "; ".join(x_values) + cond = X.get('cond') + new_pipe['loader_settings'] = { + **pipe['loader_settings'], + "positive_cond_stack": cond, + } + if "advanced: Pos Condition" in y_axis: + y_values = "; ".join(y_values) + cond = Y.get('cond') + new_pipe['loader_settings'] = { + **pipe['loader_settings'], + "positive_cond_stack": cond, + } + + if "advanced: Neg Condition" in x_axis: + x_values = "; ".join(x_values) + cond = X.get('cond') + new_pipe['loader_settings'] = { + **pipe['loader_settings'], + "negative_cond_stack": cond, + } + if "advanced: Neg Condition" in y_axis: + y_values = "; ".join(y_values) + cond = Y.get('cond') + new_pipe['loader_settings'] = { + **pipe['loader_settings'], + "negative_cond_stack": cond, + } + + del pipe + + return pipeXYPlot().plot(grid_spacing, output_individuals, flip_xy, x_axis, x_values, y_axis, y_values, new_pipe, font_path) + + +NODE_CLASS_MAPPINGS = { + "easy pipeIn": pipeIn, + "easy pipeOut": pipeOut, + "easy pipeEdit": pipeEdit, + "easy pipeEditPrompt": pipeEditPrompt, + "easy pipeToBasicPipe": pipeToBasicPipe, + "easy pipeBatchIndex": pipeBatchIndex, + "easy XYPlot": pipeXYPlot, + "easy XYPlotAdvanced": pipeXYPlotAdvanced +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "easy pipeIn": "Pipe In", + "easy pipeOut": "Pipe Out", + "easy pipeEdit": "Pipe Edit", + "easy pipeEditPrompt": "Pipe Edit Prompt", + "easy pipeBatchIndex": "Pipe Batch Index", + "easy pipeToBasicPipe": "Pipe -> BasicPipe", + "easy XYPlot": "XY Plot", + "easy XYPlotAdvanced": "XY Plot Advanced" +} \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/preSampling.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/preSampling.py new file mode 100644 index 0000000000000000000000000000000000000000..803410781caedae6f605f68209e2ab7c45eed4e6 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/preSampling.py @@ -0,0 +1,1002 @@ +import torch +import numpy as np +import comfy.utils, comfy.sample, comfy.samplers, comfy.controlnet, comfy.model_base, comfy.model_management, comfy.sampler_helpers, comfy.supported_models +from comfy.model_patcher import ModelPatcher + +from nodes import RepeatLatentBatch, CLIPTextEncode, VAEEncodeForInpaint +from ..modules.layer_diffuse import LayerMethod +from ..config import * + +from .. import easyCache, sampler + + +# 预采样设置(基础) +class samplerSettings: + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + return {"required": + {"pipe": ("PIPE_LINE",), + "steps": ("INT", {"default": 20, "min": 1, "max": 10000}), + "cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0}), + "sampler_name": (comfy.samplers.KSampler.SAMPLERS,), + "scheduler": (comfy.samplers.KSampler.SCHEDULERS + NEW_SCHEDULERS,), + "denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}), + "seed": ("INT", {"default": 0, "min": 0, "max": MAX_SEED_NUM}), + }, + "optional": { + "image_to_latent": ("IMAGE",), + "latent": ("LATENT",), + }, + "hidden": + {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID"}, + } + + RETURN_TYPES = ("PIPE_LINE", ) + RETURN_NAMES = ("pipe",) + + FUNCTION = "settings" + CATEGORY = "EasyUse/PreSampling" + + def settings(self, pipe, steps, cfg, sampler_name, scheduler, denoise, seed, image_to_latent=None, latent=None, prompt=None, extra_pnginfo=None, my_unique_id=None): + # 图生图转换 + vae = pipe["vae"] + batch_size = pipe["loader_settings"]["batch_size"] if "batch_size" in pipe["loader_settings"] else 1 + if image_to_latent is not None: + _, height, width, _ = image_to_latent.shape + if height == 1 and width == 1: + samples = pipe["samples"] + images = pipe["images"] + else: + samples = {"samples": vae.encode(image_to_latent[:, :, :, :3])} + samples = RepeatLatentBatch().repeat(samples, batch_size)[0] + images = image_to_latent + elif latent is not None: + samples = latent + images = pipe["images"] + else: + samples = pipe["samples"] + images = pipe["images"] + + new_pipe = { + "model": pipe['model'], + "positive": pipe['positive'], + "negative": pipe['negative'], + "vae": pipe['vae'], + "clip": pipe['clip'], + + "samples": samples, + "images": images, + "seed": seed, + + "loader_settings": { + **pipe["loader_settings"], + "steps": steps, + "cfg": cfg, + "sampler_name": sampler_name, + "scheduler": scheduler, + "denoise": denoise, + "add_noise": "enabled" + } + } + + del pipe + + return {"ui": {"value": [seed]}, "result": (new_pipe,)} + +# 预采样设置(高级) +class samplerSettingsAdvanced: + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + return {"required": + {"pipe": ("PIPE_LINE",), + "steps": ("INT", {"default": 20, "min": 1, "max": 10000}), + "cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0}), + "sampler_name": (comfy.samplers.KSampler.SAMPLERS,), + "scheduler": (comfy.samplers.KSampler.SCHEDULERS + NEW_SCHEDULERS,), + "start_at_step": ("INT", {"default": 0, "min": 0, "max": 10000}), + "end_at_step": ("INT", {"default": 10000, "min": 0, "max": 10000}), + "add_noise": (["enable (CPU)", "enable (GPU=A1111)", "disable"], {"default": "enable (CPU)"}), + "seed": ("INT", {"default": 0, "min": 0, "max": MAX_SEED_NUM}), + "return_with_leftover_noise": (["disable", "enable"], ), + }, + "optional": { + "image_to_latent": ("IMAGE",), + "latent": ("LATENT",) + }, + "hidden": + {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID"}, + } + + RETURN_TYPES = ("PIPE_LINE", ) + RETURN_NAMES = ("pipe",) + + FUNCTION = "settings" + CATEGORY = "EasyUse/PreSampling" + + def settings(self, pipe, steps, cfg, sampler_name, scheduler, start_at_step, end_at_step, add_noise, seed, return_with_leftover_noise, image_to_latent=None, latent=None, prompt=None, extra_pnginfo=None, my_unique_id=None): + # 图生图转换 + vae = pipe["vae"] + batch_size = pipe["loader_settings"]["batch_size"] if "batch_size" in pipe["loader_settings"] else 1 + if image_to_latent is not None: + _, height, width, _ = image_to_latent.shape + if height == 1 and width == 1: + samples = pipe["samples"] + images = pipe["images"] + else: + samples = {"samples": vae.encode(image_to_latent[:, :, :, :3])} + samples = RepeatLatentBatch().repeat(samples, batch_size)[0] + images = image_to_latent + elif latent is not None: + samples = latent + images = pipe["images"] + else: + samples = pipe["samples"] + images = pipe["images"] + + force_full_denoise = True + if return_with_leftover_noise == "enable": + force_full_denoise = False + + new_pipe = { + "model": pipe['model'], + "positive": pipe['positive'], + "negative": pipe['negative'], + "vae": pipe['vae'], + "clip": pipe['clip'], + + "samples": samples, + "images": images, + "seed": seed, + + "loader_settings": { + **pipe["loader_settings"], + "steps": steps, + "cfg": cfg, + "sampler_name": sampler_name, + "scheduler": scheduler, + "start_step": start_at_step, + "last_step": end_at_step, + "denoise": 1.0, + "add_noise": add_noise, + "force_full_denoise": force_full_denoise + } + } + + del pipe + + return {"ui": {"value": [seed]}, "result": (new_pipe,)} + +# 预采样设置(噪声注入) +class samplerSettingsNoiseIn: + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + return {"required": + {"pipe": ("PIPE_LINE",), + "factor": ("FLOAT", {"default": 0.1, "min": 0.0, "max": 1.0, "step":0.01, "round": 0.01}), + "steps": ("INT", {"default": 20, "min": 1, "max": 10000}), + "cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0}), + "sampler_name": (comfy.samplers.KSampler.SAMPLERS,), + "scheduler": (comfy.samplers.KSampler.SCHEDULERS+NEW_SCHEDULERS,), + "denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}), + "seed": ("INT", {"default": 0, "min": 0, "max": MAX_SEED_NUM}), + }, + "optional": { + "optional_noise_seed": ("INT",{"forceInput": True}), + "optional_latent": ("LATENT",), + }, + "hidden": + {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID"}, + } + + RETURN_TYPES = ("PIPE_LINE", ) + RETURN_NAMES = ("pipe",) + + FUNCTION = "settings" + CATEGORY = "EasyUse/PreSampling" + + def slerp(self, val, low, high): + dims = low.shape + + low = low.reshape(dims[0], -1) + high = high.reshape(dims[0], -1) + + low_norm = low / torch.norm(low, dim=1, keepdim=True) + high_norm = high / torch.norm(high, dim=1, keepdim=True) + + low_norm[low_norm != low_norm] = 0.0 + high_norm[high_norm != high_norm] = 0.0 + + omega = torch.acos((low_norm * high_norm).sum(1)) + so = torch.sin(omega) + res = (torch.sin((1.0 - val) * omega) / so).unsqueeze(1) * low + (torch.sin(val * omega) / so).unsqueeze( + 1) * high + + return res.reshape(dims) + + def prepare_mask(self, mask, shape): + mask = torch.nn.functional.interpolate(mask.reshape((-1, 1, mask.shape[-2], mask.shape[-1])), + size=(shape[2], shape[3]), mode="bilinear") + mask = mask.expand((-1, shape[1], -1, -1)) + if mask.shape[0] < shape[0]: + mask = mask.repeat((shape[0] - 1) // mask.shape[0] + 1, 1, 1, 1)[:shape[0]] + return mask + + def expand_mask(self, mask, expand, tapered_corners): + try: + import scipy + + c = 0 if tapered_corners else 1 + kernel = np.array([[c, 1, c], + [1, 1, 1], + [c, 1, c]]) + mask = mask.reshape((-1, mask.shape[-2], mask.shape[-1])) + out = [] + for m in mask: + output = m.numpy() + for _ in range(abs(expand)): + if expand < 0: + output = scipy.ndimage.grey_erosion(output, footprint=kernel) + else: + output = scipy.ndimage.grey_dilation(output, footprint=kernel) + output = torch.from_numpy(output) + out.append(output) + + return torch.stack(out, dim=0) + except: + return None + + def settings(self, pipe, factor, steps, cfg, sampler_name, scheduler, denoise, seed, optional_noise_seed=None, optional_latent=None, prompt=None, extra_pnginfo=None, my_unique_id=None): + latent = optional_latent if optional_latent is not None else pipe["samples"] + model = pipe["model"] + + # generate base noise + batch_size, _, height, width = latent["samples"].shape + generator = torch.manual_seed(seed) + base_noise = torch.randn((1, 4, height, width), dtype=torch.float32, device="cpu", generator=generator).repeat(batch_size, 1, 1, 1).cpu() + + # generate variation noise + if optional_noise_seed is None or optional_noise_seed == seed: + optional_noise_seed = seed+1 + generator = torch.manual_seed(optional_noise_seed) + variation_noise = torch.randn((batch_size, 4, height, width), dtype=torch.float32, device="cpu", + generator=generator).cpu() + + slerp_noise = self.slerp(factor, base_noise, variation_noise) + + end_at_step = steps # min(steps, end_at_step) + start_at_step = round(end_at_step - end_at_step * denoise) + + device = comfy.model_management.get_torch_device() + comfy.model_management.load_model_gpu(model) + model_patcher = comfy.model_patcher.ModelPatcher(model.model, load_device=device, offload_device=comfy.model_management.unet_offload_device()) + sampler = comfy.samplers.KSampler(model_patcher, steps=steps, device=device, sampler=sampler_name, + scheduler=scheduler, denoise=1.0, model_options=model.model_options) + sigmas = sampler.sigmas + sigma = sigmas[start_at_step] - sigmas[end_at_step] + sigma /= model.model.latent_format.scale_factor + sigma = sigma.cpu().numpy() + + work_latent = latent.copy() + work_latent["samples"] = latent["samples"].clone() + slerp_noise * sigma + + if "noise_mask" in latent: + noise_mask = self.prepare_mask(latent["noise_mask"], latent['samples'].shape) + work_latent["samples"] = noise_mask * work_latent["samples"] + (1-noise_mask) * latent["samples"] + work_latent['noise_mask'] = self.expand_mask(latent["noise_mask"].clone(), 5, True) + + if pipe is None: + pipe = {} + + new_pipe = { + "model": pipe['model'], + "positive": pipe['positive'], + "negative": pipe['negative'], + "vae": pipe['vae'], + "clip": pipe['clip'], + + "samples": work_latent, + "images": pipe['images'], + "seed": seed, + + "loader_settings": { + **pipe["loader_settings"], + "steps": steps, + "cfg": cfg, + "sampler_name": sampler_name, + "scheduler": scheduler, + "denoise": denoise, + "add_noise": "disable" + } + } + + return (new_pipe,) + +# 预采样设置(自定义) +class samplerCustomSettings: + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + return {"required": { + "pipe": ("PIPE_LINE",), + "guider": (['CFG','DualCFG','Basic', 'IP2P+CFG', 'IP2P+DualCFG','IP2P+Basic'],{"default":"Basic"}), + "cfg": ("FLOAT", {"default": 3.5, "min": 0.0, "max": 100.0}), + "cfg_negative": ("FLOAT", {"default": 1.5, "min": 0.0, "max": 100.0}), + "sampler_name": (comfy.samplers.KSampler.SAMPLERS + ['inversed_euler'],), + "scheduler": (comfy.samplers.KSampler.SCHEDULERS + ['karrasADV','exponentialADV','polyExponential', 'sdturbo', 'vp', 'alignYourSteps', 'gits'],), + "coeff": ("FLOAT", {"default": 1.20, "min": 0.80, "max": 1.50, "step": 0.05}), + "steps": ("INT", {"default": 20, "min": 1, "max": 10000}), + "sigma_max": ("FLOAT", {"default": 14.614642, "min": 0.0, "max": 1000.0, "step": 0.01, "round": False}), + "sigma_min": ("FLOAT", {"default": 0.0291675, "min": 0.0, "max": 1000.0, "step": 0.01, "round": False}), + "rho": ("FLOAT", {"default": 7.0, "min": 0.0, "max": 100.0, "step": 0.01, "round": False}), + "beta_d": ("FLOAT", {"default": 19.9, "min": 0.0, "max": 1000.0, "step": 0.01, "round": False}), + "beta_min": ("FLOAT", {"default": 0.1, "min": 0.0, "max": 1000.0, "step": 0.01, "round": False}), + "eps_s": ("FLOAT", {"default": 0.001, "min": 0.0, "max": 1.0, "step": 0.0001, "round": False}), + "flip_sigmas": ("BOOLEAN", {"default": False}), + "denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}), + "add_noise": (["enable (CPU)", "enable (GPU=A1111)", "disable"], {"default": "enable (CPU)"}), + "seed": ("INT", {"default": 0, "min": 0, "max": MAX_SEED_NUM}), + }, + "optional": { + "image_to_latent": ("IMAGE",), + "latent": ("LATENT",), + "optional_sampler":("SAMPLER",), + "optional_sigmas":("SIGMAS",), + }, + "hidden": + {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID"}, + } + + RETURN_TYPES = ("PIPE_LINE", ) + RETURN_NAMES = ("pipe",) + + FUNCTION = "settings" + CATEGORY = "EasyUse/PreSampling" + + def ip2p(self, positive, negative, vae, pixels, latent=None): + if latent is not None: + concat_latent = latent + else: + x = (pixels.shape[1] // 8) * 8 + y = (pixels.shape[2] // 8) * 8 + + if pixels.shape[1] != x or pixels.shape[2] != y: + x_offset = (pixels.shape[1] % 8) // 2 + y_offset = (pixels.shape[2] % 8) // 2 + pixels = pixels[:,x_offset:x + x_offset, y_offset:y + y_offset,:] + + concat_latent = vae.encode(pixels) + + out_latent = {} + out_latent["samples"] = torch.zeros_like(concat_latent) + + out = [] + for conditioning in [positive, negative]: + c = [] + for t in conditioning: + d = t[1].copy() + d["concat_latent_image"] = concat_latent + n = [t[0], d] + c.append(n) + out.append(c) + return (out[0], out[1], out_latent) + + + def settings(self, pipe, guider, cfg, cfg_negative, sampler_name, scheduler, coeff, steps, sigma_max, sigma_min, rho, beta_d, beta_min, eps_s, flip_sigmas, denoise, add_noise, seed, image_to_latent=None, latent=None, optional_sampler=None, optional_sigmas=None, prompt=None, extra_pnginfo=None, my_unique_id=None): + + # 图生图转换 + vae = pipe["vae"] + model = pipe["model"] + positive = pipe['positive'] + negative = pipe['negative'] + batch_size = pipe["loader_settings"]["batch_size"] if "batch_size" in pipe["loader_settings"] else 1 + + if image_to_latent is not None: + _, height, width, _ = image_to_latent.shape + if height == 1 and width == 1: + samples = pipe["samples"] + images = pipe["images"] + else: + if "IP2P" in guider: + positive, negative, latent = self.ip2p(pipe['positive'], pipe['negative'], vae, image_to_latent) + samples = latent + else: + samples = {"samples": vae.encode(image_to_latent[:, :, :, :3])} + samples = RepeatLatentBatch().repeat(samples, batch_size)[0] + images = image_to_latent + elif latent is not None: + if "IP2P" in guider: + positive, negative, latent = self.ip2p(pipe['positive'], pipe['negative'], latent=latent) + samples = latent + else: + samples = latent + images = pipe["images"] + else: + samples = pipe["samples"] + images = pipe["images"] + + + new_pipe = { + "model": model, + "positive": positive, + "negative": negative, + "vae": pipe['vae'], + "clip": pipe['clip'], + + "samples": samples, + "images": images, + "seed": seed, + + "loader_settings": { + **pipe["loader_settings"], + "middle": pipe['negative'], + "steps": steps, + "cfg": cfg, + "cfg_negative": cfg_negative, + "sampler_name": sampler_name, + "scheduler": scheduler, + "denoise": denoise, + "add_noise": add_noise, + "custom": { + "guider": guider, + "coeff": coeff, + "sigma_max": sigma_max, + "sigma_min": sigma_min, + "rho": rho, + "beta_d": beta_d, + "beta_min": beta_min, + "eps_s": beta_min, + "flip_sigmas": flip_sigmas + }, + "optional_sampler": optional_sampler, + "optional_sigmas": optional_sigmas + } + } + + del pipe + + return {"ui": {"value": [seed]}, "result": (new_pipe,)} + +# 预采样设置(SDTurbo) +from ..libs.gradual_latent_hires_fix import sample_dpmpp_2s_ancestral, sample_dpmpp_2m_sde, sample_lcm, sample_euler_ancestral +class sdTurboSettings: + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + return {"required": { + "pipe": ("PIPE_LINE",), + "steps": ("INT", {"default": 1, "min": 1, "max": 10}), + "cfg": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 100.0}), + "sampler_name": (comfy.samplers.SAMPLER_NAMES,), + "eta": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01, "round": False}), + "s_noise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01, "round": False}), + "upscale_ratio": ("FLOAT", {"default": 2.0, "min": 0.0, "max": 16.0, "step": 0.01, "round": False}), + "start_step": ("INT", {"default": 5, "min": 0, "max": 1000, "step": 1}), + "end_step": ("INT", {"default": 15, "min": 0, "max": 1000, "step": 1}), + "upscale_n_step": ("INT", {"default": 3, "min": 0, "max": 1000, "step": 1}), + "unsharp_kernel_size": ("INT", {"default": 3, "min": 1, "max": 21, "step": 1}), + "unsharp_sigma": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 10.0, "step": 0.01, "round": False}), + "unsharp_strength": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 10.0, "step": 0.01, "round": False}), + "seed": ("INT", {"default": 0, "min": 0, "max": MAX_SEED_NUM}), + }, + "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID"}, + } + + RETURN_TYPES = ("PIPE_LINE",) + RETURN_NAMES = ("pipe",) + + FUNCTION = "settings" + CATEGORY = "EasyUse/PreSampling" + + def settings(self, pipe, steps, cfg, sampler_name, eta, s_noise, upscale_ratio, start_step, end_step, upscale_n_step, unsharp_kernel_size, unsharp_sigma, unsharp_strength, seed, prompt=None, extra_pnginfo=None, my_unique_id=None): + model = pipe['model'] + # sigma + timesteps = torch.flip(torch.arange(1, 11) * 100 - 1, (0,))[:steps] + sigmas = model.model.model_sampling.sigma(timesteps) + sigmas = torch.cat([sigmas, sigmas.new_zeros([1])]) + + #sampler + sample_function = None + extra_options = { + "eta": eta, + "s_noise": s_noise, + "upscale_ratio": upscale_ratio, + "start_step": start_step, + "end_step": end_step, + "upscale_n_step": upscale_n_step, + "unsharp_kernel_size": unsharp_kernel_size, + "unsharp_sigma": unsharp_sigma, + "unsharp_strength": unsharp_strength, + } + if sampler_name == "euler_ancestral": + sample_function = sample_euler_ancestral + elif sampler_name == "dpmpp_2s_ancestral": + sample_function = sample_dpmpp_2s_ancestral + elif sampler_name == "dpmpp_2m_sde": + sample_function = sample_dpmpp_2m_sde + elif sampler_name == "lcm": + sample_function = sample_lcm + + if sample_function is not None: + unsharp_kernel_size = unsharp_kernel_size if unsharp_kernel_size % 2 == 1 else unsharp_kernel_size + 1 + extra_options["unsharp_kernel_size"] = unsharp_kernel_size + _sampler = comfy.samplers.KSAMPLER(sample_function, extra_options) + else: + _sampler = comfy.samplers.sampler_object(sampler_name) + extra_options = None + + new_pipe = { + "model": pipe['model'], + "positive": pipe['positive'], + "negative": pipe['negative'], + "vae": pipe['vae'], + "clip": pipe['clip'], + + "samples": pipe["samples"], + "images": pipe["images"], + "seed": seed, + + "loader_settings": { + **pipe["loader_settings"], + "extra_options": extra_options, + "sampler": _sampler, + "sigmas": sigmas, + "steps": steps, + "cfg": cfg, + "add_noise": "enabled" + } + } + + del pipe + + return {"ui": {"value": [seed]}, "result": (new_pipe,)} + + +# cascade预采样参数 +class cascadeSettings: + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + return {"required": + {"pipe": ("PIPE_LINE",), + "encode_vae_name": (["None"] + folder_paths.get_filename_list("vae"),), + "decode_vae_name": (["None"] + folder_paths.get_filename_list("vae"),), + "steps": ("INT", {"default": 20, "min": 1, "max": 10000}), + "cfg": ("FLOAT", {"default": 4.0, "min": 0.0, "max": 100.0}), + "sampler_name": (comfy.samplers.KSampler.SAMPLERS, {"default":"euler_ancestral"}), + "scheduler": (comfy.samplers.KSampler.SCHEDULERS, {"default":"simple"}), + "denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}), + "seed": ("INT", {"default": 0, "min": 0, "max": MAX_SEED_NUM}), + }, + "optional": { + "image_to_latent_c": ("IMAGE",), + "latent_c": ("LATENT",), + }, + "hidden":{"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID"}, + } + + RETURN_TYPES = ("PIPE_LINE",) + RETURN_NAMES = ("pipe",) + + FUNCTION = "settings" + CATEGORY = "EasyUse/PreSampling" + + def settings(self, pipe, encode_vae_name, decode_vae_name, steps, cfg, sampler_name, scheduler, denoise, seed, model=None, image_to_latent_c=None, latent_c=None, prompt=None, extra_pnginfo=None, my_unique_id=None): + images, samples_c = None, None + samples = pipe['samples'] + batch_size = pipe["loader_settings"]["batch_size"] if "batch_size" in pipe["loader_settings"] else 1 + + encode_vae_name = encode_vae_name if encode_vae_name is not None else pipe['loader_settings']['encode_vae_name'] + decode_vae_name = decode_vae_name if decode_vae_name is not None else pipe['loader_settings']['decode_vae_name'] + + if image_to_latent_c is not None: + if encode_vae_name != 'None': + encode_vae = easyCache.load_vae(encode_vae_name) + else: + encode_vae = pipe['vae'][0] + if "compression" not in pipe["loader_settings"]: + raise Exception("compression is not found") + compression = pipe["loader_settings"]['compression'] + width = image_to_latent_c.shape[-2] + height = image_to_latent_c.shape[-3] + out_width = (width // compression) * encode_vae.downscale_ratio + out_height = (height // compression) * encode_vae.downscale_ratio + + s = comfy.utils.common_upscale(image_to_latent_c.movedim(-1, 1), out_width, out_height, "bicubic", + "center").movedim(1, + -1) + c_latent = encode_vae.encode(s[:, :, :, :3]) + b_latent = torch.zeros([c_latent.shape[0], 4, height // 4, width // 4]) + + samples_c = {"samples": c_latent} + samples_c = RepeatLatentBatch().repeat(samples_c, batch_size)[0] + + samples_b = {"samples": b_latent} + samples_b = RepeatLatentBatch().repeat(samples_b, batch_size)[0] + samples = (samples_c, samples_b) + images = image_to_latent_c + elif latent_c is not None: + samples_c = latent_c + samples = (samples_c, samples[1]) + images = pipe["images"] + if samples_c is not None: + samples = (samples_c, samples[1]) + + new_pipe = { + "model": pipe['model'], + "positive": pipe['positive'], + "negative": pipe['negative'], + "vae": pipe['vae'], + "clip": pipe['clip'], + + "samples": samples, + "images": images, + "seed": seed, + + "loader_settings": { + **pipe["loader_settings"], + "encode_vae_name": encode_vae_name, + "decode_vae_name": decode_vae_name, + "steps": steps, + "cfg": cfg, + "sampler_name": sampler_name, + "scheduler": scheduler, + "denoise": denoise, + "add_noise": "enabled" + } + } + + sampler.update_value_by_id("pipe_line", my_unique_id, new_pipe) + + del pipe + + return {"ui": {"value": [seed]}, "result": (new_pipe,)} + +# layerDiffusion预采样参数 +class layerDiffusionSettings: + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + return {"required": + { + "pipe": ("PIPE_LINE",), + "method": ([LayerMethod.FG_ONLY_ATTN.value, LayerMethod.FG_ONLY_CONV.value, LayerMethod.EVERYTHING.value, LayerMethod.FG_TO_BLEND.value, LayerMethod.BG_TO_BLEND.value],), + "weight": ("FLOAT",{"default": 1.0, "min": -1, "max": 3, "step": 0.05},), + "steps": ("INT", {"default": 20, "min": 1, "max": 10000}), + "cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0}), + "sampler_name": (comfy.samplers.KSampler.SAMPLERS, {"default": "euler"}), + "scheduler": (comfy.samplers.KSampler.SCHEDULERS+ NEW_SCHEDULERS, {"default": "normal"}), + "denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}), + "seed": ("INT", {"default": 0, "min": 0, "max": MAX_SEED_NUM}), + }, + "optional": { + "image": ("IMAGE",), + "blended_image": ("IMAGE",), + "mask": ("MASK",), + # "latent": ("LATENT",), + # "blended_latent": ("LATENT",), + }, + "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID"}, + } + + RETURN_TYPES = ("PIPE_LINE",) + RETURN_NAMES = ("pipe",) + + FUNCTION = "settings" + CATEGORY = "EasyUse/PreSampling" + + def get_layer_diffusion_method(self, method, has_blend_latent): + method = LayerMethod(method) + if has_blend_latent: + if method == LayerMethod.BG_TO_BLEND: + method = LayerMethod.BG_BLEND_TO_FG + elif method == LayerMethod.FG_TO_BLEND: + method = LayerMethod.FG_BLEND_TO_BG + return method + + def settings(self, pipe, method, weight, steps, cfg, sampler_name, scheduler, denoise, seed, image=None, blended_image=None, mask=None, prompt=None, extra_pnginfo=None, my_unique_id=None): + blend_samples = pipe['blend_samples'] if "blend_samples" in pipe else None + vae = pipe["vae"] + batch_size = pipe["loader_settings"]["batch_size"] if "batch_size" in pipe["loader_settings"] else 1 + + method = self.get_layer_diffusion_method(method, blend_samples is not None or blended_image is not None) + + if image is not None or "image" in pipe: + image = image if image is not None else pipe['image'] + if mask is not None: + print('inpaint') + samples, = VAEEncodeForInpaint().encode(vae, image, mask) + else: + samples = {"samples": vae.encode(image[:,:,:,:3])} + samples = RepeatLatentBatch().repeat(samples, batch_size)[0] + images = image + elif "samp_images" in pipe: + samples = {"samples": vae.encode(pipe["samp_images"][:,:,:,:3])} + samples = RepeatLatentBatch().repeat(samples, batch_size)[0] + images = pipe["samp_images"] + else: + if method not in [LayerMethod.FG_ONLY_ATTN, LayerMethod.FG_ONLY_CONV, LayerMethod.EVERYTHING]: + raise Exception("image is missing") + + samples = pipe["samples"] + images = pipe["images"] + + if method in [LayerMethod.BG_BLEND_TO_FG, LayerMethod.FG_BLEND_TO_BG]: + if blended_image is None and blend_samples is None: + raise Exception("blended_image is missing") + elif blended_image is not None: + blend_samples = {"samples": vae.encode(blended_image[:,:,:,:3])} + blend_samples = RepeatLatentBatch().repeat(blend_samples, batch_size)[0] + + new_pipe = { + "model": pipe['model'], + "positive": pipe['positive'], + "negative": pipe['negative'], + "vae": pipe['vae'], + "clip": pipe['clip'], + + "samples": samples, + "blend_samples": blend_samples, + "images": images, + "seed": seed, + + "loader_settings": { + **pipe["loader_settings"], + "steps": steps, + "cfg": cfg, + "sampler_name": sampler_name, + "scheduler": scheduler, + "denoise": denoise, + "add_noise": "enabled", + "layer_diffusion_method": method, + "layer_diffusion_weight": weight, + } + } + + del pipe + + return {"ui": {"value": [seed]}, "result": (new_pipe,)} + +# 预采样设置(layerDiffuse附加) +class layerDiffusionSettingsADDTL: + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + return {"required": + { + "pipe": ("PIPE_LINE",), + "foreground_prompt": ("STRING", {"default": "", "placeholder": "Foreground Additional Prompt", "multiline": True}), + "background_prompt": ("STRING", {"default": "", "placeholder": "Background Additional Prompt", "multiline": True}), + "blended_prompt": ("STRING", {"default": "", "placeholder": "Blended Additional Prompt", "multiline": True}), + }, + "optional": { + "optional_fg_cond": ("CONDITIONING",), + "optional_bg_cond": ("CONDITIONING",), + "optional_blended_cond": ("CONDITIONING",), + }, + "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID"}, + } + + RETURN_TYPES = ("PIPE_LINE",) + RETURN_NAMES = ("pipe",) + + FUNCTION = "settings" + CATEGORY = "EasyUse/PreSampling" + + def settings(self, pipe, foreground_prompt, background_prompt, blended_prompt, optional_fg_cond=None, optional_bg_cond=None, optional_blended_cond=None, prompt=None, extra_pnginfo=None, my_unique_id=None): + fg_cond, bg_cond, blended_cond = None, None, None + clip = pipe['clip'] + if optional_fg_cond is not None: + fg_cond = optional_fg_cond + elif foreground_prompt != "": + fg_cond, = CLIPTextEncode().encode(clip, foreground_prompt) + if optional_bg_cond is not None: + bg_cond = optional_bg_cond + elif background_prompt != "": + bg_cond, = CLIPTextEncode().encode(clip, background_prompt) + if optional_blended_cond is not None: + blended_cond = optional_blended_cond + elif blended_prompt != "": + blended_cond, = CLIPTextEncode().encode(clip, blended_prompt) + + new_pipe = { + **pipe, + "loader_settings": { + **pipe["loader_settings"], + "layer_diffusion_cond": (fg_cond, bg_cond, blended_cond) + } + } + + del pipe + + return (new_pipe,) + +# 预采样设置(动态CFG) +from ..libs.dynthres_core import DynThresh +class dynamicCFGSettings: + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + return {"required": + {"pipe": ("PIPE_LINE",), + "steps": ("INT", {"default": 20, "min": 1, "max": 10000}), + "cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0}), + "cfg_mode": (DynThresh.Modes,), + "cfg_scale_min": ("FLOAT", {"default": 3.5, "min": 0.0, "max": 100.0, "step": 0.5}), + "sampler_name": (comfy.samplers.KSampler.SAMPLERS,), + "scheduler": (comfy.samplers.KSampler.SCHEDULERS+NEW_SCHEDULERS,), + "denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}), + "seed": ("INT", {"default": 0, "min": 0, "max": MAX_SEED_NUM}), + }, + "optional":{ + "image_to_latent": ("IMAGE",), + "latent": ("LATENT",) + }, + "hidden": + {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID"}, + } + + RETURN_TYPES = ("PIPE_LINE",) + RETURN_NAMES = ("pipe",) + + FUNCTION = "settings" + CATEGORY = "EasyUse/PreSampling" + + def settings(self, pipe, steps, cfg, cfg_mode, cfg_scale_min,sampler_name, scheduler, denoise, seed, image_to_latent=None, latent=None, prompt=None, extra_pnginfo=None, my_unique_id=None): + + + dynamic_thresh = DynThresh(7.0, 1.0,"CONSTANT", 0, cfg_mode, cfg_scale_min, 0, 0, 999, False, + "MEAN", "AD", 1) + + def sampler_dyn_thresh(args): + input = args["input"] + cond = input - args["cond"] + uncond = input - args["uncond"] + cond_scale = args["cond_scale"] + time_step = args["timestep"] + dynamic_thresh.step = 999 - time_step[0] + + return input - dynamic_thresh.dynthresh(cond, uncond, cond_scale, None) + + model = pipe['model'] + + m = model.clone() + m.set_model_sampler_cfg_function(sampler_dyn_thresh) + + # 图生图转换 + vae = pipe["vae"] + batch_size = pipe["loader_settings"]["batch_size"] if "batch_size" in pipe["loader_settings"] else 1 + if image_to_latent is not None: + samples = {"samples": vae.encode(image_to_latent[:, :, :, :3])} + samples = RepeatLatentBatch().repeat(samples, batch_size)[0] + images = image_to_latent + elif latent is not None: + samples = RepeatLatentBatch().repeat(latent, batch_size)[0] + images = pipe["images"] + else: + samples = pipe["samples"] + images = pipe["images"] + + new_pipe = { + "model": m, + "positive": pipe['positive'], + "negative": pipe['negative'], + "vae": pipe['vae'], + "clip": pipe['clip'], + + "samples": samples, + "images": images, + "seed": seed, + + "loader_settings": { + **pipe["loader_settings"], + "steps": steps, + "cfg": cfg, + "sampler_name": sampler_name, + "scheduler": scheduler, + "denoise": denoise + }, + } + + del pipe + + return {"ui": {"value": [seed]}, "result": (new_pipe,)} + +# 动态CFG +class dynamicThresholdingFull: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "model": ("MODEL",), + "mimic_scale": ("FLOAT", {"default": 7.0, "min": 0.0, "max": 100.0, "step": 0.5}), + "threshold_percentile": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}), + "mimic_mode": (DynThresh.Modes,), + "mimic_scale_min": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 100.0, "step": 0.5}), + "cfg_mode": (DynThresh.Modes,), + "cfg_scale_min": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 100.0, "step": 0.5}), + "sched_val": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 100.0, "step": 0.01}), + "separate_feature_channels": (["enable", "disable"],), + "scaling_startpoint": (DynThresh.Startpoints,), + "variability_measure": (DynThresh.Variabilities,), + "interpolate_phi": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}), + } + } + + RETURN_TYPES = ("MODEL",) + FUNCTION = "patch" + CATEGORY = "EasyUse/PreSampling" + + def patch(self, model, mimic_scale, threshold_percentile, mimic_mode, mimic_scale_min, cfg_mode, cfg_scale_min, + sched_val, separate_feature_channels, scaling_startpoint, variability_measure, interpolate_phi): + dynamic_thresh = DynThresh(mimic_scale, threshold_percentile, mimic_mode, mimic_scale_min, cfg_mode, + cfg_scale_min, sched_val, 0, 999, separate_feature_channels == "enable", + scaling_startpoint, variability_measure, interpolate_phi) + + def sampler_dyn_thresh(args): + input = args["input"] + cond = input - args["cond"] + uncond = input - args["uncond"] + cond_scale = args["cond_scale"] + time_step = args["timestep"] + dynamic_thresh.step = 999 - time_step[0] + + return input - dynamic_thresh.dynthresh(cond, uncond, cond_scale, None) + + m = model.clone() + m.set_model_sampler_cfg_function(sampler_dyn_thresh) + return (m,) + + + +NODE_CLASS_MAPPINGS = { + "easy preSampling": samplerSettings, + "easy preSamplingAdvanced": samplerSettingsAdvanced, + "easy preSamplingNoiseIn": samplerSettingsNoiseIn, + "easy preSamplingCustom": samplerCustomSettings, + "easy preSamplingSdTurbo": sdTurboSettings, + "easy preSamplingDynamicCFG": dynamicCFGSettings, + "easy preSamplingCascade": cascadeSettings, + "easy preSamplingLayerDiffusion": layerDiffusionSettings, + "easy preSamplingLayerDiffusionADDTL": layerDiffusionSettingsADDTL, + "dynamicThresholdingFull": dynamicThresholdingFull, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "easy preSampling": "PreSampling", + "easy preSamplingAdvanced": "PreSampling (Advanced)", + "easy preSamplingNoiseIn": "PreSampling (NoiseIn)", + "easy preSamplingCustom": "PreSampling (Custom)", + "easy preSamplingSdTurbo": "PreSampling (SDTurbo)", + "easy preSamplingDynamicCFG": "PreSampling (DynamicCFG)", + "easy preSamplingCascade": "PreSampling (Cascade)", + "easy preSamplingLayerDiffusion": "PreSampling (LayerDiffuse)", + "easy preSamplingLayerDiffusionADDTL": "PreSampling (LayerDiffuse ADDTL)", + "dynamicThresholdingFull": "DynamicThresholdingFull", +} \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/prompt.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/prompt.py new file mode 100644 index 0000000000000000000000000000000000000000..3720762f27c819d338d3ce49254c4516a5a95f9e --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/prompt.py @@ -0,0 +1,795 @@ +import json +import os +from urllib.request import urlopen +import folder_paths + +from .. import easyCache +from ..config import FOOOCUS_STYLES_DIR, MAX_SEED_NUM, PROMPT_TEMPLATE, RESOURCES_DIR +from ..libs.log import log_node_info +from ..libs.wildcards import WildcardProcessor, get_wildcard_list, process + +from comfy_api.latest import io + + +# 正面提示词 +class positivePrompt(io.ComfyNode): + + @classmethod + def define_schema(cls): + return io.Schema( + node_id="easy positive", + category="EasyUse/Prompt", + inputs=[ + io.String.Input("positive", default="", multiline=True, placeholder="Positive"), + ], + outputs=[ + io.String.Output(id="output_positive", display_name="positive"), + ], + ) + + @classmethod + def execute(cls, positive): + return io.NodeOutput(positive) + +# 通配符提示词 +class wildcardsPrompt(io.ComfyNode): + + @classmethod + def define_schema(cls): + wildcard_list = get_wildcard_list() + return io.Schema( + node_id="easy wildcards", + category="EasyUse/Prompt", + inputs=[ + io.String.Input("text", default="", multiline=True, dynamic_prompts=False, placeholder="(Support wildcard)"), + io.Combo.Input("Select to add LoRA", options=["Select the LoRA to add to the text"] + folder_paths.get_filename_list("loras")), + io.Combo.Input("Select to add Wildcard", options=["Select the Wildcard to add to the text"] + wildcard_list), + io.Int.Input("seed", default=0, min=0, max=MAX_SEED_NUM), + io.Boolean.Input("multiline_mode", default=False), + ], + outputs=[ + io.String.Output(id="output_text", display_name="text", is_output_list=True), + io.String.Output(id="populated_text", display_name="populated_text", is_output_list=True), + ], + hidden=[ + io.Hidden.prompt, + io.Hidden.extra_pnginfo, + io.Hidden.unique_id, + ], + ) + + @classmethod + def execute(cls, text, seed, multiline_mode, **kwargs): + prompt = cls.hidden.prompt + + # Clean loaded_objects + if prompt: + easyCache.update_loaded_objects(prompt) + + if multiline_mode: + populated_text = [] + _text = [] + text_lines = text.split("\n") + for t in text_lines: + _text.append(t) + populated_text.append(process(t, seed)) + text = _text + else: + populated_text = [process(text, seed)] + text = [text] + return io.NodeOutput(text, populated_text, ui={"value": [seed]}) + +# 通配符提示词矩阵,会按顺序返回包含通配符的提示词所生成的所有可能 +class wildcardsPromptMatrix(io.ComfyNode): + + @classmethod + def define_schema(cls): + wildcard_list = get_wildcard_list() + return io.Schema( + node_id="easy wildcardsMatrix", + category="EasyUse/Prompt", + inputs=[ + io.String.Input("text", default="", multiline=True, dynamic_prompts=False, placeholder="(Support Lora Block Weight and wildcard)"), + io.Combo.Input("Select to add LoRA", options=["Select the LoRA to add to the text"] + folder_paths.get_filename_list("loras")), + io.Combo.Input("Select to add Wildcard", options=["Select the Wildcard to add to the text"] + wildcard_list), + io.Int.Input("offset", default=0, min=0, max=MAX_SEED_NUM, step=1, control_after_generate=True), + io.Int.Input("output_limit", default=1, min=-1, step=1, tooltip="Output All Probilities", optional=True), + ], + outputs=[ + io.String.Output("populated_text", is_output_list=True), + io.Int.Output("total"), + io.Int.Output("factors", is_output_list=True), + ], + hidden=[ + io.Hidden.prompt, + io.Hidden.extra_pnginfo, + io.Hidden.unique_id, + ], + ) + + @classmethod + def execute(cls, text, offset, output_limit=1, **kwargs): + prompt = cls.hidden.prompt + # Clean loaded_objects + if prompt: + easyCache.update_loaded_objects(prompt) + + p = WildcardProcessor(text) + total = p.total() + limit = total if output_limit > total or output_limit == -1 else output_limit + offset = 0 if output_limit == -1 else offset + populated_text = p.getmany(limit, offset) if output_limit != 1 else [p.getn(offset)] + return io.NodeOutput(populated_text, p.total(), list(p.placeholder_choices.values()), ui={"value": [offset]}) + +# 负面提示词 +class negativePrompt(io.ComfyNode): + + @classmethod + def define_schema(cls): + return io.Schema( + node_id="easy negative", + category="EasyUse/Prompt", + inputs=[ + io.String.Input("negative", default="", multiline=True, placeholder="Negative"), + ], + outputs=[ + io.String.Output(id="output_negative", display_name="negative"), + ], + ) + + @classmethod + def execute(cls, negative): + return io.NodeOutput(negative) + +# 风格提示词选择器 +class stylesPromptSelector(io.ComfyNode): + + @classmethod + def define_schema(cls): + styles = ["fooocus_styles"] + styles_dir = FOOOCUS_STYLES_DIR + for file_name in os.listdir(styles_dir): + file = os.path.join(styles_dir, file_name) + if os.path.isfile(file) and file_name.endswith(".json"): + if file_name != "fooocus_styles.json": + styles.append(file_name.split(".")[0]) + + return io.Schema( + node_id="easy stylesSelector", + category="EasyUse/Prompt", + inputs=[ + io.Combo.Input("styles", options=styles, default="fooocus_styles"), + io.String.Input("positive", default="", force_input=True, optional=True), + io.String.Input("negative", default="", force_input=True, optional=True), + io.Custom(io_type="EASY_PROMPT_STYLES").Input("select_styles", optional=True), + ], + outputs=[ + io.String.Output(id="output_positive", display_name="positive"), + io.String.Output(id="output_negative", display_name="negative"), + ], + hidden=[ + io.Hidden.prompt, + io.Hidden.extra_pnginfo, + io.Hidden.unique_id, + ], + ) + + @classmethod + def execute(cls, styles, positive='', negative='', select_styles=None, **kwargs): + values = [] + all_styles = {} + positive_prompt, negative_prompt = '', negative + fooocus_custom_dir = os.path.join(FOOOCUS_STYLES_DIR, 'fooocus_styles.json') + if styles == "fooocus_styles" and not os.path.exists(fooocus_custom_dir): + file = os.path.join(RESOURCES_DIR, styles + '.json') + else: + file = os.path.join(FOOOCUS_STYLES_DIR, styles + '.json') + f = open(file, 'r', encoding='utf-8') + data = json.load(f) + f.close() + for d in data: + all_styles[d['name']] = d + # if my_unique_id in prompt: + # if prompt[my_unique_id]["inputs"]['select_styles']: + # values = prompt[my_unique_id]["inputs"]['select_styles'].split(',') + + if isinstance(select_styles, str): + values = select_styles.split(',') + else: + values = select_styles if select_styles else [] + + has_prompt = False + if len(values) == 0: + return io.NodeOutput(positive, negative) + + for index, val in enumerate(values): + if val not in all_styles: + continue + if 'prompt' in all_styles[val]: + if "{prompt}" in all_styles[val]['prompt'] and has_prompt == False: + positive_prompt = all_styles[val]['prompt'].replace('{prompt}', positive) + has_prompt = True + elif "{prompt}" in all_styles[val]['prompt']: + positive_prompt += ', ' + all_styles[val]['prompt'].replace(', {prompt}', '').replace('{prompt}', '') + else: + positive_prompt = all_styles[val]['prompt'] if positive_prompt == '' else positive_prompt + ', ' + all_styles[val]['prompt'] + if 'negative_prompt' in all_styles[val]: + negative_prompt += ', ' + all_styles[val]['negative_prompt'] if negative_prompt else all_styles[val]['negative_prompt'] + + if has_prompt == False and positive: + positive_prompt = positive + positive_prompt + ', ' + + return io.NodeOutput(positive_prompt, negative_prompt) + +#prompt +class prompt(io.ComfyNode): + @classmethod + def define_schema(cls): + return io.Schema( + node_id="easy prompt", + category="EasyUse/Prompt", + inputs=[ + io.String.Input("text", default="", multiline=True, placeholder="Prompt"), + io.Combo.Input("prefix", options=["Select the prefix add to the text"] + PROMPT_TEMPLATE["prefix"], default="Select the prefix add to the text"), + io.Combo.Input("subject", options=["👤Select the subject add to the text"] + PROMPT_TEMPLATE["subject"], default="👤Select the subject add to the text"), + io.Combo.Input("action", options=["🎬Select the action add to the text"] + PROMPT_TEMPLATE["action"], default="🎬Select the action add to the text"), + io.Combo.Input("clothes", options=["👚Select the clothes add to the text"] + PROMPT_TEMPLATE["clothes"], default="👚Select the clothes add to the text"), + io.Combo.Input("environment", options=["☀️Select the illumination environment add to the text"] + PROMPT_TEMPLATE["environment"], default="☀️Select the illumination environment add to the text"), + io.Combo.Input("background", options=["🎞️Select the background add to the text"] + PROMPT_TEMPLATE["background"], default="🎞️Select the background add to the text"), + io.Combo.Input("nsfw", options=["🔞Select the nsfw add to the text"] + PROMPT_TEMPLATE["nsfw"], default="🔞️Select the nsfw add to the text"), + ], + outputs=[ + io.String.Output("prompt"), + ], + hidden=[ + io.Hidden.prompt, + io.Hidden.extra_pnginfo, + io.Hidden.unique_id, + ], + ) + + @classmethod + def execute(cls, text, **kwargs): + return io.NodeOutput(text) + +#promptList +class promptList(io.ComfyNode): + @classmethod + def define_schema(cls): + return io.Schema( + node_id="easy promptList", + category="EasyUse/Prompt", + inputs=[ + io.String.Input("prompt_1", multiline=True, default=""), + io.String.Input("prompt_2", multiline=True, default=""), + io.String.Input("prompt_3", multiline=True, default=""), + io.String.Input("prompt_4", multiline=True, default=""), + io.String.Input("prompt_5", multiline=True, default=""), + io.Custom(io_type="LIST").Input("optional_prompt_list", optional=True), + ], + outputs=[ + io.Custom(io_type="LIST").Output("prompt_list"), + io.String.Output("prompt_strings", is_output_list=True), + ], + ) + + @classmethod + def execute(cls, prompt_1="", prompt_2="", prompt_3="", prompt_4="", prompt_5="", optional_prompt_list=None, **kwargs): + prompts = [] + + if optional_prompt_list: + for l in optional_prompt_list: + prompts.append(l) + + # Add individual prompts + for p in [prompt_1, prompt_2, prompt_3, prompt_4, prompt_5]: + if isinstance(p, str) and p != '': + prompts.append(p) + + return io.NodeOutput(prompts, prompts) + +#promptLine +class promptLine(io.ComfyNode): + + @classmethod + def define_schema(cls): + return io.Schema( + node_id="easy promptLine", + category="EasyUse/Prompt", + inputs=[ + io.String.Input("prompt", multiline=True, default="text"), + io.Int.Input("start_index", default=0, min=0, max=9999), + io.Int.Input("max_rows", default=1000, min=1, max=9999), + io.Boolean.Input("remove_empty_lines", default=True), + ], + outputs=[ + io.String.Output("STRING", is_output_list=True), + io.Combo.Output("COMBO", is_output_list=True), + ], + hidden=[ + io.Hidden.prompt, + io.Hidden.unique_id, + ], + ) + + @classmethod + def execute(cls, prompt, start_index, max_rows, remove_empty_lines=True, **kwargs): + lines = prompt.split('\n') + + if remove_empty_lines: + lines = [line for line in lines if line.strip()] + + start_index = max(0, min(start_index, len(lines) - 1)) + + end_index = min(start_index + max_rows, len(lines)) + + rows = lines[start_index:end_index] + + return io.NodeOutput(rows, rows) + +import comfy.utils +from server import PromptServer +from ..libs.messages import MessageCancelled, Message +class promptAwait(io.ComfyNode): + + @classmethod + def define_schema(cls): + return io.Schema( + node_id="easy promptAwait", + category="EasyUse/Prompt", + inputs=[ + io.AnyType.Input("now"), + io.String.Input("prompt", multiline=True, default="", placeholder="Enter a prompt or use voice to enter to text"), + io.Custom(io_type="EASY_PROMPT_AWAIT_BAR").Input("toolbar"), + io.AnyType.Input("prev", optional=True), + ], + outputs=[ + io.AnyType.Output(id="output", display_name="output"), + io.String.Output(id="output_prompt", display_name="prompt"), + io.Boolean.Output("continue"), + io.Int.Output("seed"), + ], + hidden=[ + io.Hidden.prompt, + io.Hidden.unique_id, + io.Hidden.extra_pnginfo, + ], + ) + + @classmethod + def execute(cls, now, prompt, toolbar, prev=None, **kwargs): + id = cls.hidden.unique_id + id = id.split('.')[len(id.split('.')) - 1] if "." in id else id + if ":" in id: + id = id.split(":")[0] + pbar = comfy.utils.ProgressBar(100) + pbar.update_absolute(30) + PromptServer.instance.send_sync('easyuse_prompt_await', {"id": id}) + try: + res = Message.waitForMessage(id, asList=False) + if res is None or res == "-1": + result = (now, prompt, False, 0) + else: + input = now if res['select'] == 'now' or prev is None else prev + result = (input, res['prompt'], False if res['result'] == -1 else True, res['seed'] if res['unlock'] else res['last_seed']) + pbar.update_absolute(100) + return io.NodeOutput(*result) + except MessageCancelled: + pbar.update_absolute(100) + raise comfy.model_management.InterruptProcessingException() + +class promptConcat(io.ComfyNode): + @classmethod + def define_schema(cls): + return io.Schema( + node_id="easy promptConcat", + category="EasyUse/Prompt", + inputs=[ + io.String.Input("prompt1", multiline=False, default="", force_input=True, optional=True), + io.String.Input("prompt2", multiline=False, default="", force_input=True, optional=True), + io.String.Input("separator", multiline=False, default="", optional=True), + ], + outputs=[ + io.String.Output("prompt"), + ], + ) + + @classmethod + def execute(cls, prompt1="", prompt2="", separator=""): + def to_string(value): + if isinstance(value, (list, tuple)): + return ", ".join(to_string(v) for v in value) + return str(value) + + return io.NodeOutput(to_string(prompt1) + to_string(separator) + to_string(prompt2)) + +class promptReplace(io.ComfyNode): + + @classmethod + def define_schema(cls): + return io.Schema( + node_id="easy promptReplace", + category="EasyUse/Prompt", + inputs=[ + io.String.Input("prompt", multiline=True, default="", force_input=True), + io.String.Input("find1", multiline=False, default="", optional=True), + io.String.Input("replace1", multiline=False, default="", optional=True), + io.String.Input("find2", multiline=False, default="", optional=True), + io.String.Input("replace2", multiline=False, default="", optional=True), + io.String.Input("find3", multiline=False, default="", optional=True), + io.String.Input("replace3", multiline=False, default="", optional=True), + ], + outputs=[ + io.String.Output(id="output_prompt",display_name="prompt"), + ], + ) + + @classmethod + def execute(cls, prompt, find1="", replace1="", find2="", replace2="", find3="", replace3=""): + prompt = prompt.replace(find1, replace1) + prompt = prompt.replace(find2, replace2) + prompt = prompt.replace(find3, replace3) + + return io.NodeOutput(prompt) + + +# 肖像大师 +# Created by AI Wiz Art (Stefano Flore) +# Version: 2.2 +# https://stefanoflore.it +# https://ai-wiz.art +class portraitMaster(io.ComfyNode): + + @classmethod + def define_schema(cls): + max_float_value = 1.95 + prompt_path = os.path.join(RESOURCES_DIR, 'portrait_prompt.json') + if not os.path.exists(prompt_path): + response = urlopen('https://raw.githubusercontent.com/yolain/ComfyUI-Easy-Use/main/resources/portrait_prompt.json') + temp_prompt = json.loads(response.read()) + prompt_serialized = json.dumps(temp_prompt, indent=4) + with open(prompt_path, "w") as f: + f.write(prompt_serialized) + del response, temp_prompt + # Load local + with open(prompt_path, 'r') as f: + data = json.load(f) + + inputs = [] + # Shot + inputs.append(io.Combo.Input("shot", options=['-'] + data['shot_list'])) + inputs.append(io.Float.Input("shot_weight", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider)) + # Gender and age + inputs.append(io.Combo.Input("gender", options=['-'] + data['gender_list'], default="Woman")) + inputs.append(io.Int.Input("age", default=30, min=18, max=90, step=1, display_mode=io.NumberDisplay.slider)) + # Nationality + inputs.append(io.Combo.Input("nationality_1", options=['-'] + data['nationality_list'], default="Chinese")) + inputs.append(io.Combo.Input("nationality_2", options=['-'] + data['nationality_list'])) + inputs.append(io.Float.Input("nationality_mix", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider)) + # Body + inputs.append(io.Combo.Input("body_type", options=['-'] + data['body_type_list'])) + inputs.append(io.Float.Input("body_type_weight", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider)) + inputs.append(io.Combo.Input("model_pose", options=['-'] + data['model_pose_list'])) + inputs.append(io.Combo.Input("eyes_color", options=['-'] + data['eyes_color_list'])) + # Face + inputs.append(io.Combo.Input("facial_expression", options=['-'] + data['face_expression_list'])) + inputs.append(io.Float.Input("facial_expression_weight", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider)) + inputs.append(io.Combo.Input("face_shape", options=['-'] + data['face_shape_list'])) + inputs.append(io.Float.Input("face_shape_weight", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider)) + inputs.append(io.Float.Input("facial_asymmetry", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider)) + # Hair + inputs.append(io.Combo.Input("hair_style", options=['-'] + data['hair_style_list'])) + inputs.append(io.Combo.Input("hair_color", options=['-'] + data['hair_color_list'])) + inputs.append(io.Float.Input("disheveled", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider)) + inputs.append(io.Combo.Input("beard", options=['-'] + data['beard_list'])) + # Skin details + inputs.append(io.Float.Input("skin_details", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider)) + inputs.append(io.Float.Input("skin_pores", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider)) + inputs.append(io.Float.Input("dimples", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider)) + inputs.append(io.Float.Input("freckles", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider)) + inputs.append(io.Float.Input("moles", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider)) + inputs.append(io.Float.Input("skin_imperfections", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider)) + inputs.append(io.Float.Input("skin_acne", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider)) + inputs.append(io.Float.Input("tanned_skin", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider)) + # Eyes + inputs.append(io.Float.Input("eyes_details", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider)) + inputs.append(io.Float.Input("iris_details", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider)) + inputs.append(io.Float.Input("circular_iris", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider)) + inputs.append(io.Float.Input("circular_pupil", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider)) + # Light + inputs.append(io.Combo.Input("light_type", options=['-'] + data['light_type_list'])) + inputs.append(io.Combo.Input("light_direction", options=['-'] + data['light_direction_list'])) + inputs.append(io.Float.Input("light_weight", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider)) + # Additional + inputs.append(io.Combo.Input("photorealism_improvement", options=["enable", "disable"])) + inputs.append(io.String.Input("prompt_start", multiline=True, default="raw photo, (realistic:1.5)")) + inputs.append(io.String.Input("prompt_additional", multiline=True, default="")) + inputs.append(io.String.Input("prompt_end", multiline=True, default="")) + inputs.append(io.String.Input("negative_prompt", multiline=True, default="")) + + return io.Schema( + node_id="easy portraitMaster", + category="EasyUse/Prompt", + inputs=inputs, + outputs=[ + io.String.Output("positive"), + io.String.Output("negative"), + ], + ) + + @classmethod + def execute(cls, shot="-", shot_weight=1, gender="-", body_type="-", body_type_weight=0, eyes_color="-", + facial_expression="-", facial_expression_weight=0, face_shape="-", face_shape_weight=0, + nationality_1="-", nationality_2="-", nationality_mix=0.5, age=30, hair_style="-", hair_color="-", + disheveled=0, dimples=0, freckles=0, skin_pores=0, skin_details=0, moles=0, skin_imperfections=0, + wrinkles=0, tanned_skin=0, eyes_details=1, iris_details=1, circular_iris=1, circular_pupil=1, + facial_asymmetry=0, prompt_additional="", prompt_start="", prompt_end="", light_type="-", + light_direction="-", light_weight=0, negative_prompt="", photorealism_improvement="disable", beard="-", + model_pose="-", skin_acne=0): + + prompt = [] + + if gender == "-": + gender = "" + else: + if age <= 25 and gender == 'Woman': + gender = 'girl' + if age <= 25 and gender == 'Man': + gender = 'boy' + gender = " " + gender + " " + + if nationality_1 != '-' and nationality_2 != '-': + nationality = f"[{nationality_1}:{nationality_2}:{round(nationality_mix, 2)}]" + elif nationality_1 != '-': + nationality = nationality_1 + " " + elif nationality_2 != '-': + nationality = nationality_2 + " " + else: + nationality = "" + + if prompt_start != "": + prompt.append(f"{prompt_start}") + + if shot != "-" and shot_weight > 0: + prompt.append(f"({shot}:{round(shot_weight, 2)})") + + prompt.append(f"({nationality}{gender}{round(age)}-years-old:1.5)") + + if body_type != "-" and body_type_weight > 0: + prompt.append(f"({body_type}, {body_type} body:{round(body_type_weight, 2)})") + + if model_pose != "-": + prompt.append(f"({model_pose}:1.5)") + + if eyes_color != "-": + prompt.append(f"({eyes_color} eyes:1.25)") + + if facial_expression != "-" and facial_expression_weight > 0: + prompt.append( + f"({facial_expression}, {facial_expression} expression:{round(facial_expression_weight, 2)})") + + if face_shape != "-" and face_shape_weight > 0: + prompt.append(f"({face_shape} shape face:{round(face_shape_weight, 2)})") + + if hair_style != "-": + prompt.append(f"({hair_style} hairstyle:1.25)") + + if hair_color != "-": + prompt.append(f"({hair_color} hair:1.25)") + + if beard != "-": + prompt.append(f"({beard}:1.15)") + + if disheveled != "-" and disheveled > 0: + prompt.append(f"(disheveled:{round(disheveled, 2)})") + + if prompt_additional != "": + prompt.append(f"{prompt_additional}") + + if skin_details > 0: + prompt.append(f"(skin details, skin texture:{round(skin_details, 2)})") + + if skin_pores > 0: + prompt.append(f"(skin pores:{round(skin_pores, 2)})") + + if skin_imperfections > 0: + prompt.append(f"(skin imperfections:{round(skin_imperfections, 2)})") + + if skin_acne > 0: + prompt.append(f"(acne, skin with acne:{round(skin_acne, 2)})") + + if wrinkles > 0: + prompt.append(f"(skin imperfections:{round(wrinkles, 2)})") + + if tanned_skin > 0: + prompt.append(f"(tanned skin:{round(tanned_skin, 2)})") + + if dimples > 0: + prompt.append(f"(dimples:{round(dimples, 2)})") + + if freckles > 0: + prompt.append(f"(freckles:{round(freckles, 2)})") + + if moles > 0: + prompt.append(f"(skin pores:{round(moles, 2)})") + + if eyes_details > 0: + prompt.append(f"(eyes details:{round(eyes_details, 2)})") + + if iris_details > 0: + prompt.append(f"(iris details:{round(iris_details, 2)})") + + if circular_iris > 0: + prompt.append(f"(circular iris:{round(circular_iris, 2)})") + + if circular_pupil > 0: + prompt.append(f"(circular pupil:{round(circular_pupil, 2)})") + + if facial_asymmetry > 0: + prompt.append(f"(facial asymmetry, face asymmetry:{round(facial_asymmetry, 2)})") + + if light_type != '-' and light_weight > 0: + if light_direction != '-': + prompt.append(f"({light_type} {light_direction}:{round(light_weight, 2)})") + else: + prompt.append(f"({light_type}:{round(light_weight, 2)})") + + if prompt_end != "": + prompt.append(f"{prompt_end}") + + prompt = ", ".join(prompt) + prompt = prompt.lower() + + if photorealism_improvement == "enable": + prompt = prompt + ", (professional photo, balanced photo, balanced exposure:1.2), (film grain:1.15)" + + if photorealism_improvement == "enable": + negative_prompt = negative_prompt + ", (shinny skin, reflections on the skin, skin reflections:1.25)" + + log_node_info("Portrait Master as generate the prompt:", prompt) + + return io.NodeOutput(prompt, negative_prompt) + +# 多角度 +class multiAngle(io.ComfyNode): + + @classmethod + def define_schema(cls): + return io.Schema( + node_id="easy multiAngle", + category="EasyUse/Prompt", + inputs=[ + io.Custom(io_type="EASY_MULTI_ANGLE").Input("multi_angle", optional=True), + ], + outputs=[ + io.String.Output("prompt", is_output_list=True), + io.Custom(io_type="EASY_MULTI_ANGLE").Output("params"), + ], + ) + + @classmethod + def execute(cls, multi_angle=None, **kwargs): + if multi_angle is None: + return io.NodeOutput([""]) + + if isinstance(multi_angle, str): + try: + multi_angle = json.loads(multi_angle) + except: + raise Exception(f"Invalid multi angle: {multi_angle}") + + prompts = [] + for angle_data in multi_angle: + rotate = angle_data.get("rotate", 0) + vertical = angle_data.get("vertical", 0) + zoom = angle_data.get("zoom", 5) + add_angle_prompt = angle_data.get("add_angle_prompt", True) + + # Validate input ranges + rotate = max(0, min(360, int(rotate))) + vertical = max(-90, min(90, int(vertical))) + zoom = max(0.0, min(10.0, float(zoom))) + + h_angle = rotate % 360 + + # Horizontal direction mapping + h_suffix = "" if add_angle_prompt else " quarter" + if h_angle < 22.5 or h_angle >= 337.5: h_direction = "front view" + elif h_angle < 67.5: h_direction = f"front-right{h_suffix} view" + elif h_angle < 112.5: h_direction = "right side view" + elif h_angle < 157.5: h_direction = f"back-right{h_suffix} view" + elif h_angle < 202.5: h_direction = "back view" + elif h_angle < 247.5: h_direction = f"back-left{h_suffix} view" + elif h_angle < 292.5: h_direction = "left side view" + else: h_direction = f"front-left{h_suffix} view" + + # Vertical direction mapping + if add_angle_prompt: + if vertical == -90: + v_direction = "bottom-looking-up perspective, extreme worm's eye view, focus subject bottom" + elif vertical < -75: + v_direction = "bottom-looking-up perspective, extreme worm's eye view" + elif vertical < -45: + v_direction = "ultra-low angle" + elif vertical < -15: + v_direction = "low angle" + elif vertical < 15: + v_direction = "eye level" + elif vertical < 45: + v_direction = "high angle" + elif vertical < 75: + v_direction = "bird's eye view" + elif vertical < 90: + v_direction = "top-down perspective, looking straight down at the top of the subject" + else: + v_direction = "top-down perspective, looking straight down at the top of the subject, face not visible, focus on subject head" + else: + if vertical < -15: + v_direction = "low-angle shot" + elif vertical < 15: + v_direction = "eye-level shot" + elif vertical < 45: + v_direction = "elevated shot" + elif vertical < 75: + v_direction = "high-angle shot" + elif vertical < 90: + v_direction = "top-down perspective, looking straight down at the top of the subject" + else: + v_direction = "top-down perspective, looking straight down at the top of the subject, face not visible, focus on subject head" + + # Distance/zoom mapping + if add_angle_prompt: + if zoom < 2: distance = "extreme wide shot" + elif zoom < 4: distance = "wide shot" + elif zoom < 6: distance = "medium shot" + elif zoom < 8: distance = "close-up" + else: distance = "extreme close-up" + else: + if zoom < 2: distance = "extreme wide shot" + elif zoom < 4: distance = "wide shot" + elif zoom < 6: distance = "medium shot" + elif zoom < 8: distance = "close-up" + else: distance = "extreme close-up" + + # Build prompt + if add_angle_prompt: + prompt = f"{h_direction}, {v_direction}, {distance} (horizontal: {rotate}, vertical: {vertical}, zoom: {zoom:.1f})" + else: + prompt = f"{h_direction} {v_direction} {distance}" + + prompts.append(prompt) + + return io.NodeOutput(prompts, multi_angle) + + +NODE_CLASS_MAPPINGS = { + "easy positive": positivePrompt, + "easy negative": negativePrompt, + "easy wildcards": wildcardsPrompt, + "easy wildcardsMatrix": wildcardsPromptMatrix, + "easy prompt": prompt, + "easy promptList": promptList, + "easy promptLine": promptLine, + "easy promptAwait": promptAwait, + "easy promptConcat": promptConcat, + "easy promptReplace": promptReplace, + "easy stylesSelector": stylesPromptSelector, + "easy portraitMaster": portraitMaster, + "easy multiAngle": multiAngle, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "easy positive": "Positive", + "easy negative": "Negative", + "easy wildcards": "Wildcards", + "easy wildcardsMatrix": "Wildcards Matrix", + "easy prompt": "Prompt", + "easy promptList": "PromptList", + "easy promptLine": "PromptLine", + "easy promptAwait": "PromptAwait", + "easy promptConcat": "PromptConcat", + "easy promptReplace": "PromptReplace", + "easy stylesSelector": "Styles Selector", + "easy portraitMaster": "Portrait Master", + "easy multiAngle": "Multi Angle", +} diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/samplers.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/samplers.py new file mode 100644 index 0000000000000000000000000000000000000000..c8ac6dcf3995ccc07c3aed10df6a770a1266fb45 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/samplers.py @@ -0,0 +1,1360 @@ +import sys, re, time +import torch +import comfy.utils, comfy.sample, comfy.samplers, comfy.controlnet, comfy.model_base, comfy.model_management, comfy.sampler_helpers, comfy.supported_models +import folder_paths +from comfy.model_patcher import ModelPatcher +from comfy_extras.nodes_mask import GrowMask +import comfy_extras.nodes_custom_sampler as custom_samplers +from tqdm import trange + +from server import PromptServer +from nodes import RepeatLatentBatch, NODE_CLASS_MAPPINGS as ALL_NODE_CLASS_MAPPINGS, VAEEncodeForInpaint, InpaintModelConditioning +from ..modules.layer_diffuse import LayerDiffuse +from ..config import * + +from ..libs.log import log_node_warn +from ..libs.utils import easySave, get_local_filepath, get_sd_version +from ..libs.sampler import alignYourStepsScheduler, gitsScheduler +from ..libs.xyplot import easyXYPlot + +from .. import easyCache, sampler + +class samplerFull: + + @classmethod + def INPUT_TYPES(cls): + return {"required": + {"pipe": ("PIPE_LINE",), + "steps": ("INT", {"default": 20, "min": 1, "max": 10000}), + "cfg": ("FLOAT", {"default": 8, "min": 0.0, "max": 100.0}), + "sampler_name": (comfy.samplers.KSampler.SAMPLERS,), + "scheduler": (comfy.samplers.KSampler.SCHEDULERS+NEW_SCHEDULERS,), + "denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}), + "image_output": (["Hide", "Preview", "Save", "Hide&Save", "Sender", "Sender&Save", "None"],), + "link_id": ("INT", {"default": 0, "min": 0, "max": sys.maxsize, "step": 1}), + "save_prefix": ("STRING", {"default": "ComfyUI"}), + }, + "optional": { + "seed": ("INT", {"default": 0, "min": 0, "max": MAX_SEED_NUM}), + "model": ("MODEL",), + "positive": ("CONDITIONING",), + "negative": ("CONDITIONING",), + "latent": ("LATENT",), + "vae": ("VAE",), + "clip": ("CLIP",), + "xyPlot": ("XYPLOT",), + "image": ("IMAGE",), + }, + "hidden": + {"tile_size": "INT", "prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID", + "embeddingsList": (folder_paths.get_filename_list("embeddings"),) + } + } + + RETURN_TYPES = ("PIPE_LINE", "IMAGE", "MODEL", "CONDITIONING", "CONDITIONING", "LATENT", "VAE", "CLIP", "INT",) + RETURN_NAMES = ("pipe", "image", "model", "positive", "negative", "latent", "vae", "clip", "seed",) + OUTPUT_NODE = True + FUNCTION = "run" + CATEGORY = "EasyUse/Sampler" + + def ip2p(self, positive, negative, vae=None, pixels=None, latent=None): + if latent is not None: + concat_latent = latent + else: + x = (pixels.shape[1] // 8) * 8 + y = (pixels.shape[2] // 8) * 8 + + if pixels.shape[1] != x or pixels.shape[2] != y: + x_offset = (pixels.shape[1] % 8) // 2 + y_offset = (pixels.shape[2] % 8) // 2 + pixels = pixels[:, x_offset:x + x_offset, y_offset:y + y_offset, :] + + concat_latent = vae.encode(pixels) + + out_latent = {} + out_latent["samples"] = torch.zeros_like(concat_latent) + + out = [] + for conditioning in [positive, negative]: + c = [] + for t in conditioning: + d = t[1].copy() + d["concat_latent_image"] = concat_latent + n = [t[0], d] + c.append(n) + out.append(c) + return (out[0], out[1], out_latent) + + def get_inversed_euler_sampler(self): + @torch.no_grad() + def sample_inversed_euler(model, x, sigmas, extra_args=None, callback=None, disable=None, s_churn=0., s_tmin=0.,s_tmax=float('inf'), s_noise=1.): + """Implements Algorithm 2 (Euler steps) from Karras et al. (2022).""" + extra_args = {} if extra_args is None else extra_args + s_in = x.new_ones([x.shape[0]]) + for i in trange(1, len(sigmas), disable=disable): + sigma_in = sigmas[i - 1] + + if i == 1: + sigma_t = sigmas[i] + else: + sigma_t = sigma_in + + denoised = model(x, sigma_t * s_in, **extra_args) + + if i == 1: + d = (x - denoised) / (2 * sigmas[i]) + else: + d = (x - denoised) / sigmas[i - 1] + + dt = sigmas[i] - sigmas[i - 1] + x = x + d * dt + if callback is not None: + callback( + {'x': x, 'i': i, 'sigma': sigmas[i], 'sigma_hat': sigmas[i], 'denoised': denoised}) + return x / sigmas[-1] + + ksampler = comfy.samplers.KSAMPLER(sample_inversed_euler) + return (ksampler,) + + def get_custom_cls(self, sampler_name): + try: + cls = custom_samplers.__dict__[sampler_name] + cls = cls() + if hasattr(cls, "get_sigmas"): + cls.execute = cls.get_sigmas + elif hasattr(cls, "get_guider"): + cls.execute = cls.get_guider + elif hasattr(cls, "get_sampler"): + cls.execute = cls.get_sampler + return cls + except: + raise Exception(f"Custom sampler {sampler_name} not found, Please updated your ComfyUI") + + def add_model_patch_option(self, model): + if 'transformer_options' not in model.model_options: + model.model_options['transformer_options'] = {} + to = model.model_options['transformer_options'] + if "model_patch" not in to: + to["model_patch"] = {} + return to + + def get_sampler_custom(self, model, positive, negative, loader_settings): + _guider = None + middle = loader_settings['middle'] if "middle" in loader_settings else negative + steps = loader_settings['steps'] if "steps" in loader_settings else 20 + cfg = loader_settings['cfg'] if "cfg" in loader_settings else 8.0 + cfg_negative = loader_settings['cfg_negative'] if "cfg_negative" in loader_settings else 8.0 + sampler_name = loader_settings['sampler_name'] if "sampler_name" in loader_settings else "euler" + scheduler = loader_settings['scheduler'] if "scheduler" in loader_settings else "normal" + guider = loader_settings['custom']['guider'] if "guider" in loader_settings['custom'] else "CFG" + beta_d = loader_settings['custom']['beta_d'] if "beta_d" in loader_settings['custom'] else 0.1 + beta_min = loader_settings['custom']['beta_min'] if "beta_min" in loader_settings['custom'] else 0.1 + eps_s = loader_settings['custom']['eps_s'] if "eps_s" in loader_settings['custom'] else 0.1 + sigma_max = loader_settings['custom']['sigma_max'] if "sigma_max" in loader_settings['custom'] else 14.61 + sigma_min = loader_settings['custom']['sigma_min'] if "sigma_min" in loader_settings['custom'] else 0.03 + rho = loader_settings['custom']['rho'] if "rho" in loader_settings['custom'] else 7.0 + coeff = loader_settings['custom']['coeff'] if "coeff" in loader_settings['custom'] else 1.2 + flip_sigmas = loader_settings['custom']['flip_sigmas'] if "flip_sigmas" in loader_settings['custom'] else False + denoise = loader_settings['denoise'] if "denoise" in loader_settings else 1.0 + optional_sigmas = loader_settings['optional_sigmas'] if "optional_sigmas" in loader_settings else None + optional_sampler = loader_settings['optional_sampler'] if "optional_sampler" in loader_settings else None + + # sigmas + if optional_sigmas is not None: + sigmas = optional_sigmas + else: + if scheduler == 'vp': + sigmas, = self.get_custom_cls('VPScheduler').execute(steps, beta_d, beta_min, eps_s) + elif scheduler == 'karrasADV': + sigmas, = self.get_custom_cls('KarrasScheduler').execute(steps, sigma_max, sigma_min, rho) + elif scheduler == 'exponentialADV': + sigmas, = self.get_custom_cls('ExponentialScheduler').execute(steps, sigma_max, sigma_min) + elif scheduler == 'polyExponential': + sigmas, = self.get_custom_cls('PolyexponentialScheduler').execute(steps, sigma_max, sigma_min, rho) + elif scheduler == 'sdturbo': + sigmas, = self.get_custom_cls('SDTurboScheduler').execute(model, steps, denoise) + elif scheduler == 'alignYourSteps': + model_type = get_sd_version(model) + if model_type == 'unknown': + model_type = 'sdxl' + sigmas, = alignYourStepsScheduler().get_sigmas(model_type.upper(), steps, denoise) + elif scheduler == 'gits': + sigmas, = gitsScheduler().get_sigmas(coeff, steps, denoise) + else: + sigmas, = self.get_custom_cls('BasicScheduler').execute(model, scheduler, steps, denoise) + + # filp_sigmas + if flip_sigmas: + sigmas, = self.get_custom_cls('FlipSigmas').execute(sigmas) + + ####################################################################################### + # brushnet + to = None + transformer_options = model.model_options['transformer_options'] if "transformer_options" in model.model_options else {} + if 'model_patch' in transformer_options and 'brushnet' in transformer_options['model_patch']: + to = self.add_model_patch_option(model) + mp = to['model_patch'] + if isinstance(model.model.model_config, comfy.supported_models.SD15): + mp['SDXL'] = False + elif isinstance(model.model.model_config, comfy.supported_models.SDXL): + mp['SDXL'] = True + else: + print('Base model type: ', type(model.model.model_config)) + raise Exception("Unsupported model type: ", type(model.model.model_config)) + + mp['all_sigmas'] = sigmas + mp['unet'] = model.model.diffusion_model + mp['step'] = 0 + mp['total_steps'] = 1 + ####################################################################################### + # guider + if cfg > 0 and get_sd_version(model) == 'flux': + c = [] + for t in positive: + n = [t[0], t[1]] + n[1]['guidance'] = cfg + c.append(n) + positive = c + + if guider in ['CFG', 'IP2P+CFG']: + _guider, = self.get_custom_cls('CFGGuider').execute(model, positive, negative, cfg) + elif guider in ['DualCFG', 'IP2P+DualCFG']: + _guider, = self.get_custom_cls('DualCFGGuider').execute(model, positive, middle, + negative, cfg, cfg_negative) + else: + _guider, = self.get_custom_cls('BasicGuider').execute(model, positive) + + # sampler + if optional_sampler: + _sampler = optional_sampler + else: + if sampler_name == 'inversed_euler': + _sampler, = self.get_inversed_euler_sampler() + else: + _sampler, = self.get_custom_cls('KSamplerSelect').execute(sampler_name) + + + return (_guider, _sampler, sigmas) + + def run(self, pipe, steps, cfg, sampler_name, scheduler, denoise, image_output, link_id, save_prefix, seed=None, model=None, positive=None, negative=None, latent=None, vae=None, clip=None, xyPlot=None, tile_size=None, prompt=None, extra_pnginfo=None, my_unique_id=None, force_full_denoise=False, disable_noise=False, downscale_options=None, image=None): + + samp_model = model if model is not None else pipe["model"] + samp_positive = positive if positive is not None else pipe["positive"] + samp_negative = negative if negative is not None else pipe["negative"] + samp_samples = latent if latent is not None else pipe["samples"] + samp_vae = vae if vae is not None else pipe["vae"] + samp_clip = clip if clip is not None else pipe["clip"] + + samp_seed = seed if seed is not None else pipe['seed'] + + samp_custom = pipe["loader_settings"] if "custom" in pipe["loader_settings"] else None + + steps = steps if steps is not None else pipe['loader_settings']['steps'] + start_step = pipe['loader_settings']['start_step'] if 'start_step' in pipe['loader_settings'] else 0 + last_step = pipe['loader_settings']['last_step'] if 'last_step' in pipe['loader_settings'] else 10000 + cfg = cfg if cfg is not None else pipe['loader_settings']['cfg'] + sampler_name = sampler_name if sampler_name is not None else pipe['loader_settings']['sampler_name'] + scheduler = scheduler if scheduler is not None else pipe['loader_settings']['scheduler'] + denoise = denoise if denoise is not None else pipe['loader_settings']['denoise'] + add_noise = pipe['loader_settings']['add_noise'] if 'add_noise' in pipe['loader_settings'] else 'enabled' + force_full_denoise = pipe['loader_settings']['force_full_denoise'] if 'force_full_denoise' in pipe['loader_settings'] else True + noise_device = 'GPU' if ('a1111_prompt_style' in pipe['loader_settings'] and pipe['loader_settings']['a1111_prompt_style']) or add_noise == 'enable (GPU=A1111)' else 'CPU' + + if image is not None and latent is None: + samp_samples = {"samples": samp_vae.encode(image[:, :, :, :3])} + + disable_noise = False + if add_noise == "disable": + disable_noise = True + + def downscale_model_unet(samp_model): + # 获取Unet参数 + if "PatchModelAddDownscale" in ALL_NODE_CLASS_MAPPINGS: + cls = ALL_NODE_CLASS_MAPPINGS['PatchModelAddDownscale'] + # 自动收缩Unet + if downscale_options['downscale_factor'] is None: + unet_config = samp_model.model.model_config.unet_config + if unet_config is not None and "samples" in samp_samples: + height = samp_samples['samples'].shape[2] * 8 + width = samp_samples['samples'].shape[3] * 8 + context_dim = unet_config.get('context_dim') + longer_side = width if width > height else height + if context_dim is not None and longer_side > context_dim: + width_downscale_factor = float(width / context_dim) + height_downscale_factor = float(height / context_dim) + if width_downscale_factor > 1.75: + log_node_warn("Patch model unet add downscale...") + log_node_warn("Downscale factor:" + str(width_downscale_factor)) + (samp_model,) = cls().execute(samp_model, downscale_options['block_number'], width_downscale_factor, 0, 0.35, True, "bicubic", + "bicubic") + elif height_downscale_factor > 1.25: + log_node_warn("Patch model unet add downscale....") + log_node_warn("Downscale factor:" + str(height_downscale_factor)) + (samp_model,) = cls().execute(samp_model, downscale_options['block_number'], height_downscale_factor, 0, 0.35, True, "bicubic", + "bicubic") + else: + cls = ALL_NODE_CLASS_MAPPINGS['PatchModelAddDownscale'] + log_node_warn("Patch model unet add downscale....") + log_node_warn("Downscale factor:" + str(downscale_options['downscale_factor'])) + (samp_model,) = cls().execute(samp_model, downscale_options['block_number'], downscale_options['downscale_factor'], downscale_options['start_percent'], downscale_options['end_percent'], downscale_options['downscale_after_skip'], downscale_options['downscale_method'], downscale_options['upscale_method']) + return samp_model + + def process_sample_state(pipe, samp_model, samp_clip, samp_samples, samp_vae, samp_seed, samp_positive, + samp_negative, + steps, start_step, last_step, cfg, sampler_name, scheduler, denoise, + image_output, link_id, save_prefix, tile_size, prompt, extra_pnginfo, my_unique_id, + preview_latent, force_full_denoise=force_full_denoise, disable_noise=disable_noise, samp_custom=None, noise_device='cpu'): + + # LayerDiffusion + layerDiffuse = None + samp_blend_samples = None + layer_diffusion_method = pipe['loader_settings']['layer_diffusion_method'] if 'layer_diffusion_method' in pipe['loader_settings'] else None + if layer_diffusion_method is not None: + layerDiffuse = LayerDiffuse() + samp_blend_samples = pipe["blend_samples"] if "blend_samples" in pipe else None + additional_cond = pipe["loader_settings"]['layer_diffusion_cond'] if "layer_diffusion_cond" in pipe[ + 'loader_settings'] else (None, None, None) + method = layerDiffuse.get_layer_diffusion_method(pipe['loader_settings']['layer_diffusion_method'], + samp_blend_samples is not None) + + images = pipe["images"] if "images" in pipe else None + weight = pipe['loader_settings']['layer_diffusion_weight'] if 'layer_diffusion_weight' in pipe[ + 'loader_settings'] else 1.0 + samp_model, samp_positive, samp_negative = layerDiffuse.apply_layer_diffusion(samp_model, method, weight, + samp_samples, samp_blend_samples, + samp_positive, samp_negative, + images, additional_cond) + resolution = pipe['loader_settings']['resolution'] if 'resolution' in pipe['loader_settings'] else "自定义 X 自定义" + empty_latent_width = pipe['loader_settings']['empty_latent_width'] if 'empty_latent_width' in pipe['loader_settings'] else 512 + empty_latent_height = pipe['loader_settings']['empty_latent_height'] if 'empty_latent_height' in pipe['loader_settings'] else 512 + batch_size = pipe["loader_settings"]["batch_size"] if "batch_size" in pipe["loader_settings"] else 1 + samp_samples = sampler.emptyLatent(resolution, empty_latent_width, empty_latent_height, batch_size) + + # Downscale Model Unet + if samp_model is not None and downscale_options is not None: + samp_model = downscale_model_unet(samp_model) + # 推理初始时间 + start_time = int(time.time() * 1000) + # 开始推理 + if samp_custom is not None: + _guider, _sampler, sigmas = self.get_sampler_custom(samp_model, samp_positive, samp_negative, samp_custom) + samp_samples, samp_blend_samples = sampler.custom_advanced_ksampler(_guider, _sampler, sigmas, samp_samples, add_noise, samp_seed, preview_latent=preview_latent) + elif scheduler == 'align_your_steps': + model_type = get_sd_version(samp_model) + if model_type == 'unknown': + model_type = 'sdxl' + sigmas, = alignYourStepsScheduler().get_sigmas(model_type.upper(), steps, denoise) + _sampler = comfy.samplers.sampler_object(sampler_name) + samp_samples = sampler.custom_ksampler(samp_model, samp_seed, steps, cfg, _sampler, sigmas, samp_positive, samp_negative, samp_samples, disable_noise=disable_noise, preview_latent=preview_latent, noise_device=noise_device) + elif scheduler == 'gits': + sigmas, = gitsScheduler().get_sigmas(coeff=1.2, steps=steps, denoise=denoise) + _sampler = comfy.samplers.sampler_object(sampler_name) + samp_samples = sampler.custom_ksampler(samp_model, samp_seed, steps, cfg, _sampler, sigmas, samp_positive, samp_negative, samp_samples, disable_noise=disable_noise, preview_latent=preview_latent, noise_device=noise_device) + else: + samp_samples = sampler.common_ksampler(samp_model, samp_seed, steps, cfg, sampler_name, scheduler, samp_positive, samp_negative, samp_samples, denoise=denoise, preview_latent=preview_latent, start_step=start_step, last_step=last_step, force_full_denoise=force_full_denoise, disable_noise=disable_noise, noise_device=noise_device) + # 推理结束时间 + end_time = int(time.time() * 1000) + latent = samp_samples["samples"] + + # 解码图片 + if image_output == 'None': + samp_images, new_images, alpha, results = None, None, None, None + spent_time = 'Diffusion:' + str((end_time - start_time) / 1000) + '″' + else: + if tile_size is not None: + samp_images = samp_vae.decode_tiled(latent, tile_x=tile_size // 8, tile_y=tile_size // 8, ) + else: + samp_images = samp_vae.decode(latent).cpu() + if len(samp_images.shape) == 5: # Combine batches + samp_images = samp_images.reshape(-1, samp_images.shape[-3], samp_images.shape[-2], samp_images.shape[-1]) + # LayerDiffusion Decode + if layerDiffuse is not None: + new_images, samp_images, alpha = layerDiffuse.layer_diffusion_decode(layer_diffusion_method, latent, samp_blend_samples, samp_images, samp_model) + else: + new_images = samp_images + alpha = None + + # 推理总耗时(包含解码) + end_decode_time = int(time.time() * 1000) + spent_time = 'Diffusion:' + str((end_time-start_time)/1000)+'″, VAEDecode:' + str((end_decode_time-end_time)/1000)+'″ ' + + results = easySave(new_images, save_prefix, image_output, prompt, extra_pnginfo) + + new_pipe = { + **pipe, + "positive": samp_positive, + "negative": samp_negative, + "vae": samp_vae, + "clip": samp_clip, + + "samples": samp_samples, + "blend_samples": samp_blend_samples, + "images": new_images, + "samp_images": samp_images, + "alpha": alpha, + "seed": samp_seed, + + "loader_settings": { + **pipe["loader_settings"], + "steps": steps, + "cfg": cfg, + "sampler_name": sampler_name, + "scheduler": scheduler, + "denoise": denoise, + "add_noise": add_noise, + "spent_time": spent_time + } + } + + del pipe + + if image_output in ("Hide", "Hide&Save", "None"): + return {"ui":{}, "result":sampler.get_output(new_pipe,)} + + if image_output in ("Sender", "Sender&Save"): + PromptServer.instance.send_sync("img-send", {"link_id": link_id, "images": results}) + + return {"ui": {"images": results}, + "result": sampler.get_output(new_pipe,)} + + def process_xyPlot(pipe, samp_model, samp_clip, samp_samples, samp_vae, samp_seed, samp_positive, samp_negative, + steps, cfg, sampler_name, scheduler, denoise, + image_output, link_id, save_prefix, tile_size, prompt, extra_pnginfo, my_unique_id, preview_latent, xyPlot, force_full_denoise, disable_noise, samp_custom, noise_device): + + sampleXYplot = easyXYPlot(xyPlot, save_prefix, image_output, prompt, extra_pnginfo, my_unique_id, sampler, easyCache) + + if not sampleXYplot.validate_xy_plot(): + return process_sample_state(pipe, samp_model, samp_clip, samp_samples, samp_vae, samp_seed, samp_positive, + samp_negative, steps, 0, 10000, cfg, + sampler_name, scheduler, denoise, image_output, link_id, save_prefix, tile_size, prompt, + extra_pnginfo, my_unique_id, preview_latent, samp_custom=samp_custom, noise_device=noise_device) + + # Downscale Model Unet + if samp_model is not None and downscale_options is not None: + samp_model = downscale_model_unet(samp_model) + + blend_samples = pipe['blend_samples'] if "blend_samples" in pipe else None + layer_diffusion_method = pipe['loader_settings']['layer_diffusion_method'] if 'layer_diffusion_method' in pipe['loader_settings'] else None + + plot_image_vars = { + "x_node_type": sampleXYplot.x_node_type, "y_node_type": sampleXYplot.y_node_type, + "lora_name": pipe["loader_settings"]["lora_name"] if "lora_name" in pipe["loader_settings"] else None, + "lora_model_strength": pipe["loader_settings"]["lora_model_strength"] if "lora_model_strength" in pipe["loader_settings"] else 1.0, + "lora_clip_strength": pipe["loader_settings"]["lora_clip_strength"] if "lora_clip_strength" in pipe["loader_settings"] else 1.0, + "lora_stack": pipe["loader_settings"]["lora_stack"] if "lora_stack" in pipe["loader_settings"] else None, + "steps": steps, + "cfg": cfg, + "sampler_name": sampler_name, + "scheduler": scheduler, + "denoise": denoise, + "seed": samp_seed, + "images": pipe['images'], + + "model": samp_model, "vae": samp_vae, "clip": samp_clip, "positive_cond": samp_positive, + "negative_cond": samp_negative, + "noise_device":noise_device, + + "ckpt_name": pipe['loader_settings']['ckpt_name'] if "ckpt_name" in pipe["loader_settings"] else None, + "vae_name": pipe['loader_settings']['vae_name'] if "vae_name" in pipe["loader_settings"] else None, + "clip_skip": pipe['loader_settings']['clip_skip'] if "clip_skip" in pipe["loader_settings"] else None, + "positive": pipe['loader_settings']['positive'] if "positive" in pipe["loader_settings"] else None, + "positive_token_normalization": pipe['loader_settings']['positive_token_normalization'] if "positive_token_normalization" in pipe["loader_settings"] else None, + "positive_weight_interpretation": pipe['loader_settings']['positive_weight_interpretation'] if "positive_weight_interpretation" in pipe["loader_settings"] else None, + "negative": pipe['loader_settings']['negative'] if "negative" in pipe["loader_settings"] else None, + "negative_token_normalization": pipe['loader_settings']['negative_token_normalization'] if "negative_token_normalization" in pipe["loader_settings"] else None, + "negative_weight_interpretation": pipe['loader_settings']['negative_weight_interpretation'] if "negative_weight_interpretation" in pipe["loader_settings"] else None, + } + + if "models" in pipe["loader_settings"]: + plot_image_vars["models"] = pipe["loader_settings"]["models"] + if "vae_use" in pipe["loader_settings"]: + plot_image_vars["vae_use"] = pipe["loader_settings"]["vae_use"] + if "a1111_prompt_style" in pipe["loader_settings"]: + plot_image_vars["a1111_prompt_style"] = pipe["loader_settings"]["a1111_prompt_style"] + if "cnet_stack" in pipe["loader_settings"]: + plot_image_vars["cnet"] = pipe["loader_settings"]["cnet_stack"] + if "positive_cond_stack" in pipe["loader_settings"]: + plot_image_vars["positive_cond_stack"] = pipe["loader_settings"]["positive_cond_stack"] + if "negative_cond_stack" in pipe["loader_settings"]: + plot_image_vars["negative_cond_stack"] = pipe["loader_settings"]["negative_cond_stack"] + if layer_diffusion_method: + plot_image_vars["layer_diffusion_method"] = layer_diffusion_method + if "layer_diffusion_weight" in pipe["loader_settings"]: + plot_image_vars["layer_diffusion_weight"] = pipe['loader_settings']['layer_diffusion_weight'] + if "layer_diffusion_cond" in pipe["loader_settings"]: + plot_image_vars["layer_diffusion_cond"] = pipe['loader_settings']['layer_diffusion_cond'] + if "empty_samples" in pipe["loader_settings"]: + plot_image_vars["empty_samples"] = pipe["loader_settings"]['empty_samples'] + + latent_image = sampleXYplot.get_latent(pipe["samples"]) + latents_plot = sampleXYplot.get_labels_and_sample(plot_image_vars, latent_image, preview_latent, start_step, + last_step, force_full_denoise, disable_noise) + + samp_samples = {"samples": latents_plot} + + images, image_list = sampleXYplot.plot_images_and_labels(plot_image_vars) + + # Generate output_images + output_images = torch.stack([tensor.squeeze() for tensor in image_list]) + + if layer_diffusion_method is not None: + layerDiffuse = LayerDiffuse() + new_images, samp_images, alpha = layerDiffuse.layer_diffusion_decode(layer_diffusion_method, latents_plot, blend_samples, + output_images, samp_model) + else: + new_images = output_images + samp_images = output_images + alpha = None + + results = easySave(images, save_prefix, image_output, prompt, extra_pnginfo) + + new_pipe = { + **pipe, + "positive": samp_positive, + "negative": samp_negative, + "vae": samp_vae, + "clip": samp_clip, + + "samples": samp_samples, + "blend_samples": blend_samples, + "samp_images": samp_images, + "images": new_images, + "seed": samp_seed, + "alpha": alpha, + + "loader_settings": pipe["loader_settings"], + } + + del pipe + + if image_output in ("Hide", "Hide&Save", "None"): + return {"ui": {}, "result": sampler.get_output(new_pipe,)} + + return {"ui": {"images": results}, "result": sampler.get_output(new_pipe)} + + preview_latent = True + if image_output in ("Hide", "Hide&Save", "None"): + preview_latent = False + + xyplot_id = next((x for x in prompt if "XYPlot" in str(prompt[x]["class_type"])), None) + if xyplot_id is None: + xyPlot = None + else: + xyPlot = pipe["loader_settings"]["xyplot"] if "xyplot" in pipe["loader_settings"] else xyPlot + + # Fooocus model patch + model_options = samp_model.model_options if samp_model.model_options else samp_model.model.model_options + transformer_options = model_options["transformer_options"] if "transformer_options" in model_options else {} + if "fooocus" in transformer_options: + from ..modules.fooocus import applyFooocusInpaint + del transformer_options["fooocus"] + with applyFooocusInpaint(): + if xyPlot is not None: + return process_xyPlot(pipe, samp_model, samp_clip, samp_samples, samp_vae, samp_seed, samp_positive, + samp_negative, steps, cfg, sampler_name, scheduler, denoise, image_output, + link_id, save_prefix, tile_size, prompt, extra_pnginfo, my_unique_id, + preview_latent, xyPlot, force_full_denoise, disable_noise, samp_custom, noise_device) + else: + return process_sample_state(pipe, samp_model, samp_clip, samp_samples, samp_vae, samp_seed, + samp_positive, samp_negative, steps, start_step, last_step, cfg, + sampler_name, scheduler, denoise, image_output, link_id, save_prefix, + tile_size, prompt, extra_pnginfo, my_unique_id, preview_latent, + force_full_denoise, disable_noise, samp_custom, noise_device) + else: + if xyPlot is not None: + return process_xyPlot(pipe, samp_model, samp_clip, samp_samples, samp_vae, samp_seed, samp_positive, samp_negative, steps, cfg, sampler_name, scheduler, denoise, image_output, link_id, save_prefix, tile_size, prompt, extra_pnginfo, my_unique_id, preview_latent, xyPlot, force_full_denoise, disable_noise, samp_custom, noise_device) + else: + return process_sample_state(pipe, samp_model, samp_clip, samp_samples, samp_vae, samp_seed, samp_positive, samp_negative, steps, start_step, last_step, cfg, sampler_name, scheduler, denoise, image_output, link_id, save_prefix, tile_size, prompt, extra_pnginfo, my_unique_id, preview_latent, force_full_denoise, disable_noise, samp_custom, noise_device) + +# 简易采样器 +class samplerSimple(samplerFull): + + @classmethod + def INPUT_TYPES(cls): + return {"required": + {"pipe": ("PIPE_LINE",), + "image_output": (["Hide", "Preview", "Save", "Hide&Save", "Sender", "Sender&Save", "None"],{"default": "Preview"}), + "link_id": ("INT", {"default": 0, "min": 0, "max": sys.maxsize, "step": 1}), + "save_prefix": ("STRING", {"default": "ComfyUI"}), + }, + "optional": { + "model": ("MODEL",), + }, + "hidden": + {"tile_size": "INT", "prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID", + "embeddingsList": (folder_paths.get_filename_list("embeddings"),) + } + } + + + RETURN_TYPES = ("PIPE_LINE", "IMAGE",) + RETURN_NAMES = ("pipe", "image",) + OUTPUT_NODE = True + FUNCTION = "simple" + CATEGORY = "EasyUse/Sampler" + + def simple(self, pipe, image_output, link_id, save_prefix, model=None, tile_size=None, prompt=None, extra_pnginfo=None, my_unique_id=None, force_full_denoise=False, disable_noise=False): + + return super().run(pipe, None, None, None, None, None, image_output, link_id, save_prefix, + None, model, None, None, None, None, None, None, + None, prompt, extra_pnginfo, my_unique_id, force_full_denoise, disable_noise) + +class samplerSimpleCustom(samplerFull): + + @classmethod + def INPUT_TYPES(cls): + return {"required": + {"pipe": ("PIPE_LINE",), + "image_output": (["Hide", "Preview", "Save", "Hide&Save", "Sender", "Sender&Save", "None"],{"default": "None"}), + "link_id": ("INT", {"default": 0, "min": 0, "max": sys.maxsize, "step": 1}), + "save_prefix": ("STRING", {"default": "ComfyUI"}), + }, + "optional": { + "model": ("MODEL",), + }, + "hidden": + {"tile_size": "INT", "prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID", + "embeddingsList": (folder_paths.get_filename_list("embeddings"),) + } + } + + + RETURN_TYPES = ("PIPE_LINE", "LATENT", "LATENT", "IMAGE") + RETURN_NAMES = ("pipe", "output", "denoised_output", "image") + OUTPUT_NODE = True + FUNCTION = "simple" + CATEGORY = "EasyUse/Sampler" + + def simple(self, pipe, image_output, link_id, save_prefix, model=None, tile_size=None, prompt=None, extra_pnginfo=None, my_unique_id=None, force_full_denoise=False, disable_noise=False): + + result = super().run(pipe, None, None, None, None, None, image_output, link_id, save_prefix, + None, model, None, None, None, None, None, None, + None, prompt, extra_pnginfo, my_unique_id, force_full_denoise, disable_noise) + + pipe = result["result"][0] if "result" in result else None + + return ({"ui": result['ui'], "result": (pipe, pipe["samples"], pipe["blend_samples"], pipe["images"])}) + +# 简易采样器 (Tiled) +class samplerSimpleTiled(samplerFull): + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + return {"required": + {"pipe": ("PIPE_LINE",), + "tile_size": ("INT", {"default": 512, "min": 320, "max": 4096, "step": 64}), + "image_output": (["Hide", "Preview", "Save", "Hide&Save", "Sender", "Sender&Save", "None"],{"default": "Preview"}), + "link_id": ("INT", {"default": 0, "min": 0, "max": sys.maxsize, "step": 1}), + "save_prefix": ("STRING", {"default": "ComfyUI"}) + }, + "optional": { + "model": ("MODEL",), + }, + "hidden": { + "prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID", + "embeddingsList": (folder_paths.get_filename_list("embeddings"),) + } + } + + RETURN_TYPES = ("PIPE_LINE", "IMAGE",) + RETURN_NAMES = ("pipe", "image",) + OUTPUT_NODE = True + FUNCTION = "tiled" + CATEGORY = "EasyUse/Sampler" + + def tiled(self, pipe, tile_size=512, image_output='preview', link_id=0, save_prefix='ComfyUI', model=None, prompt=None, extra_pnginfo=None, my_unique_id=None, force_full_denoise=False, disable_noise=False): + + return super().run(pipe, None, None,None,None,None, image_output, link_id, save_prefix, + None, model, None, None, None, None, None, None, + tile_size, prompt, extra_pnginfo, my_unique_id, force_full_denoise, disable_noise) + +# 简易采样器 (LayerDiffusion) +class samplerSimpleLayerDiffusion(samplerFull): + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + return {"required": + {"pipe": ("PIPE_LINE",), + "image_output": (["Hide", "Preview", "Save", "Hide&Save", "Sender", "Sender&Save"], {"default": "Preview"}), + "link_id": ("INT", {"default": 0, "min": 0, "max": sys.maxsize, "step": 1}), + "save_prefix": ("STRING", {"default": "ComfyUI"}) + }, + "optional": { + "model": ("MODEL",), + }, + "hidden": { + "prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID", + "embeddingsList": (folder_paths.get_filename_list("embeddings"),) + } + } + + RETURN_TYPES = ("PIPE_LINE", "IMAGE", "IMAGE", "MASK") + RETURN_NAMES = ("pipe", "final_image", "original_image", "alpha") + OUTPUT_NODE = True + OUTPUT_IS_LIST = (False, False, False, True) + FUNCTION = "layerDiffusion" + CATEGORY = "EasyUse/Sampler" + + def layerDiffusion(self, pipe, image_output='preview', link_id=0, save_prefix='ComfyUI', model=None, prompt=None, extra_pnginfo=None, my_unique_id=None, force_full_denoise=False, disable_noise=False): + + result = super().run(pipe, None, None,None,None,None, image_output, link_id, save_prefix, + None, model, None, None, None, None, None, None, + None, prompt, extra_pnginfo, my_unique_id, force_full_denoise, disable_noise) + pipe = result["result"][0] if "result" in result else None + return ({"ui":result['ui'], "result":(pipe, pipe["images"], pipe["samp_images"], pipe["alpha"])}) + +# 简易采样器(收缩Unet) +class samplerSimpleDownscaleUnet(samplerFull): + + upscale_methods = ["bicubic", "nearest-exact", "bilinear", "area", "bislerp"] + + @classmethod + def INPUT_TYPES(s): + return {"required": + {"pipe": ("PIPE_LINE",), + "downscale_mode": (["None", "Auto", "Custom"],{"default": "Auto"}), + "block_number": ("INT", {"default": 3, "min": 1, "max": 32, "step": 1}), + "downscale_factor": ("FLOAT", {"default": 2.0, "min": 0.1, "max": 9.0, "step": 0.001}), + "start_percent": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001}), + "end_percent": ("FLOAT", {"default": 0.35, "min": 0.0, "max": 1.0, "step": 0.001}), + "downscale_after_skip": ("BOOLEAN", {"default": True}), + "downscale_method": (s.upscale_methods,), + "upscale_method": (s.upscale_methods,), + "image_output": (["Hide", "Preview", "Save", "Hide&Save", "Sender", "Sender&Save"],{"default": "Preview"}), + "link_id": ("INT", {"default": 0, "min": 0, "max": sys.maxsize, "step": 1}), + "save_prefix": ("STRING", {"default": "ComfyUI"}), + }, + "optional": { + "model": ("MODEL",), + }, + "hidden": + {"tile_size": "INT", "prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID", + "embeddingsList": (folder_paths.get_filename_list("embeddings"),) + } + } + + + RETURN_TYPES = ("PIPE_LINE", "IMAGE",) + RETURN_NAMES = ("pipe", "image",) + OUTPUT_NODE = True + FUNCTION = "downscale_unet" + CATEGORY = "EasyUse/Sampler" + + def downscale_unet(self, pipe, downscale_mode, block_number, downscale_factor, start_percent, end_percent, downscale_after_skip, downscale_method, upscale_method, image_output, link_id, save_prefix, model=None, tile_size=None, prompt=None, extra_pnginfo=None, my_unique_id=None, force_full_denoise=False, disable_noise=False): + downscale_options = None + if downscale_mode == 'Auto': + downscale_options = { + "block_number": block_number, + "downscale_factor": None, + "start_percent": 0, + "end_percent":0.35, + "downscale_after_skip": True, + "downscale_method": "bicubic", + "upscale_method": "bicubic" + } + elif downscale_mode == 'Custom': + downscale_options = { + "block_number": block_number, + "downscale_factor": downscale_factor, + "start_percent": start_percent, + "end_percent": end_percent, + "downscale_after_skip": downscale_after_skip, + "downscale_method": downscale_method, + "upscale_method": upscale_method + } + + return super().run(pipe, None, None,None,None,None, image_output, link_id, save_prefix, + None, model, None, None, None, None, None, None, + tile_size, prompt, extra_pnginfo, my_unique_id, force_full_denoise, disable_noise, downscale_options) +# 简易采样器 (内补) +class samplerSimpleInpainting(samplerFull): + @classmethod + def INPUT_TYPES(cls): + return {"required": + {"pipe": ("PIPE_LINE",), + "grow_mask_by": ("INT", {"default": 6, "min": 0, "max": 64, "step": 1}), + "image_output": (["Hide", "Preview", "Save", "Hide&Save", "Sender", "Sender&Save"],{"default": "Preview"}), + "link_id": ("INT", {"default": 0, "min": 0, "max": sys.maxsize, "step": 1}), + "save_prefix": ("STRING", {"default": "ComfyUI"}), + "additional": (["None", "InpaintModelCond", "Differential Diffusion", "Fooocus Inpaint", "Fooocus Inpaint + DD", "Brushnet Random", "Brushnet Random + DD", "Brushnet Segmentation", "Brushnet Segmentation + DD"],{"default": "None"}) + }, + "optional": { + "model": ("MODEL",), + "mask": ("MASK",), + }, + "hidden": + {"tile_size": "INT", "prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID", + "embeddingsList": (folder_paths.get_filename_list("embeddings"),) + } + } + + RETURN_TYPES = ("PIPE_LINE", "IMAGE", "VAE") + RETURN_NAMES = ("pipe", "image", "vae") + OUTPUT_NODE = True + FUNCTION = "inpainting" + CATEGORY = "EasyUse/Sampler" + + def dd(self, model, positive, negative, pixels, vae, mask): + positive, negative, latent = InpaintModelConditioning().encode(positive, negative, pixels, vae, mask, noise_mask=True) + cls = ALL_NODE_CLASS_MAPPINGS['DifferentialDiffusion'] + if cls is not None: + model, = cls().apply(model) + else: + raise Exception("Differential Diffusion not found,please update comfyui") + return positive, negative, latent, model + + def get_brushnet_model(self, type, model): + model_type = 'sdxl' if isinstance(model.model.model_config, comfy.supported_models.SDXL) else 'sd1' + if type == 'random': + brush_model = BRUSHNET_MODELS['random_mask'][model_type]['model_url'] + if model_type == 'sdxl': + pattern = 'brushnet.random.mask.sdxl.*.(safetensors|bin)$' + else: + pattern = 'brushnet.random.mask.*.(safetensors|bin)$' + elif type == 'segmentation': + brush_model = BRUSHNET_MODELS['segmentation_mask'][model_type]['model_url'] + if model_type == 'sdxl': + pattern = 'brushnet.segmentation.mask.sdxl.*.(safetensors|bin)$' + else: + pattern = 'brushnet.segmentation.mask.*.(safetensors|bin)$' + + + brushfile = [e for e in folder_paths.get_filename_list('inpaint') if re.search(pattern, e, re.IGNORECASE)] + brushname = brushfile[0] if brushfile else None + if not brushname: + from urllib.parse import urlparse + get_local_filepath(brush_model, INPAINT_DIR) + parsed_url = urlparse(brush_model) + brushname = os.path.basename(parsed_url.path) + return brushname + + def apply_brushnet(self, brushname, model, vae, image, mask, positive, negative, scale=1.0, start_at=0, end_at=10000): + if "BrushNetLoader" not in ALL_NODE_CLASS_MAPPINGS: + raise Exception("BrushNetLoader not found,please install ComfyUI-BrushNet") + cls = ALL_NODE_CLASS_MAPPINGS['BrushNetLoader'] + brushnet, = cls().brushnet_loading(brushname, 'float16') + cls = ALL_NODE_CLASS_MAPPINGS['BrushNet'] + m, positive, negative, latent = cls().model_update(model=model, vae=vae, image=image, mask=mask, brushnet=brushnet, positive=positive, negative=negative, scale=scale, start_at=start_at, end_at=end_at) + return m, positive, negative, latent + + def inpainting(self, pipe, grow_mask_by, image_output, link_id, save_prefix, additional, model=None, mask=None, tile_size=None, prompt=None, extra_pnginfo=None, my_unique_id=None, force_full_denoise=False, disable_noise=False): + _model = model if model is not None else pipe['model'] + latent = pipe['samples'] if 'samples' in pipe else None + positive = pipe['positive'] + negative = pipe['negative'] + images = pipe["images"] if pipe and "images" in pipe else None + vae = pipe["vae"] if pipe and "vae" in pipe else None + if 'noise_mask' in latent and mask is None: + mask = latent['noise_mask'] + elif mask is not None: + if images is None: + raise Exception("No Images found") + if vae is None: + raise Exception("No VAE found") + + if additional == 'Differential Diffusion': + positive, negative, latent, _model = self.dd(_model, positive, negative, images, vae, mask) + elif additional == 'InpaintModelCond': + if mask is not None: + mask, = GrowMask().expand_mask(mask, grow_mask_by, False) + positive, negative, latent = InpaintModelConditioning().encode(positive, negative, images, vae, mask, True) + elif additional == 'Fooocus Inpaint': + head = list(FOOOCUS_INPAINT_HEAD.keys())[0] + patch = list(FOOOCUS_INPAINT_PATCH.keys())[0] + if mask is not None: + latent, = VAEEncodeForInpaint().encode(vae, images, mask, grow_mask_by) + _model, = ALL_NODE_CLASS_MAPPINGS['easy applyFooocusInpaint']().apply(_model, latent, head, patch) + elif additional == 'Fooocus Inpaint + DD': + head = list(FOOOCUS_INPAINT_HEAD.keys())[0] + patch = list(FOOOCUS_INPAINT_PATCH.keys())[0] + if mask is not None: + latent, = VAEEncodeForInpaint().encode(vae, images, mask, grow_mask_by) + _model, = ALL_NODE_CLASS_MAPPINGS['easy applyFooocusInpaint']().apply(_model, latent, head, patch) + positive, negative, latent, _model = self.dd(_model, positive, negative, images, vae, mask) + elif additional == 'Brushnet Random': + mask, = GrowMask().expand_mask(mask, grow_mask_by, False) + brush_name = self.get_brushnet_model('random', _model) + _model, positive, negative, latent = self.apply_brushnet(brush_name, _model, vae, images, mask, positive, + negative) + elif additional == 'Brushnet Random + DD': + mask, = GrowMask().expand_mask(mask, grow_mask_by, False) + brush_name = self.get_brushnet_model('random', _model) + _model, positive, negative, latent = self.apply_brushnet(brush_name, _model, vae, images, mask, positive, + negative) + positive, negative, latent, _model = self.dd(_model, positive, negative, images, vae, mask) + elif additional == 'Brushnet Segmentation': + mask, = GrowMask().expand_mask(mask, grow_mask_by, False) + brush_name = self.get_brushnet_model('segmentation', _model) + _model, positive, negative, latent = self.apply_brushnet(brush_name, _model, vae, images, mask, positive, + negative) + elif additional == 'Brushnet Segmentation + DD': + mask, = GrowMask().expand_mask(mask, grow_mask_by, False) + brush_name = self.get_brushnet_model('segmentation', _model) + _model, positive, negative, latent = self.apply_brushnet(brush_name, _model, vae, images, mask, positive, + negative) + positive, negative, latent, _model = self.dd(_model, positive, negative, images, vae, mask) + else: + latent, = VAEEncodeForInpaint().encode(vae, images, mask, grow_mask_by) + + results = super().run(pipe, None, None,None,None,None, image_output, link_id, save_prefix, + None, _model, positive, negative, latent, vae, None, None, + tile_size, prompt, extra_pnginfo, my_unique_id, force_full_denoise, disable_noise) + + result = results['result'] + + return {"ui":results['ui'],"result":(result[0], result[1], result[0]['vae'],)} + +# SDTurbo采样器 +class samplerSDTurbo: + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + return {"required": + {"pipe": ("PIPE_LINE",), + "image_output": (["Hide", "Preview", "Save", "Hide&Save", "Sender", "Sender&Save"],{"default": "Preview"}), + "link_id": ("INT", {"default": 0, "min": 0, "max": sys.maxsize, "step": 1}), + "save_prefix": ("STRING", {"default": "ComfyUI"}), + }, + "optional": { + "model": ("MODEL",), + }, + "hidden": + {"tile_size": "INT", "prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", + "my_unique_id": "UNIQUE_ID", + "embeddingsList": (folder_paths.get_filename_list("embeddings"),) + } + } + + RETURN_TYPES = ("PIPE_LINE", "IMAGE",) + RETURN_NAMES = ("pipe", "image",) + OUTPUT_NODE = True + FUNCTION = "run" + + CATEGORY = "EasyUse/Sampler" + + def run(self, pipe, image_output, link_id, save_prefix, model=None, tile_size=None, prompt=None, extra_pnginfo=None, my_unique_id=None,): + # Clean loaded_objects + easyCache.update_loaded_objects(prompt) + + my_unique_id = int(my_unique_id) + + samp_model = pipe["model"] if model is None else model + samp_positive = pipe["positive"] + samp_negative = pipe["negative"] + samp_samples = pipe["samples"] + samp_vae = pipe["vae"] + samp_clip = pipe["clip"] + + samp_seed = pipe['seed'] + + samp_sampler = pipe['loader_settings']['sampler'] + + sigmas = pipe['loader_settings']['sigmas'] + cfg = pipe['loader_settings']['cfg'] + steps = pipe['loader_settings']['steps'] + + disable_noise = False + + preview_latent = True + if image_output in ("Hide", "Hide&Save"): + preview_latent = False + + # 推理初始时间 + start_time = int(time.time() * 1000) + # 开始推理 + samp_samples = sampler.custom_ksampler(samp_model, samp_seed, steps, cfg, samp_sampler, sigmas, samp_positive, samp_negative, samp_samples, + disable_noise, preview_latent) + # 推理结束时间 + end_time = int(time.time() * 1000) + + latent = samp_samples['samples'] + + # 解码图片 + if tile_size is not None: + samp_images = samp_vae.decode_tiled(latent, tile_x=tile_size // 8, tile_y=tile_size // 8, ) + else: + samp_images = samp_vae.decode(latent).cpu() + + # 推理总耗时(包含解码) + end_decode_time = int(time.time() * 1000) + spent_time = 'Diffusion:' + str((end_time - start_time) / 1000) + '″, VAEDecode:' + str( + (end_decode_time - end_time) / 1000) + '″ ' + + # Clean loaded_objects + easyCache.update_loaded_objects(prompt) + + results = easySave(samp_images, save_prefix, image_output, prompt, extra_pnginfo) + sampler.update_value_by_id("results", my_unique_id, results) + + new_pipe = { + "model": samp_model, + "positive": samp_positive, + "negative": samp_negative, + "vae": samp_vae, + "clip": samp_clip, + + "samples": samp_samples, + "images": samp_images, + "seed": samp_seed, + + "loader_settings": { + **pipe["loader_settings"], + "spent_time": spent_time + } + } + + sampler.update_value_by_id("pipe_line", my_unique_id, new_pipe) + + del pipe + + if image_output in ("Hide", "Hide&Save"): + return {"ui": {}, + "result": sampler.get_output(new_pipe, )} + + if image_output in ("Sender", "Sender&Save"): + PromptServer.instance.send_sync("img-send", {"link_id": link_id, "images": results}) + + return {"ui": {"images": results}, + "result": sampler.get_output(new_pipe, )} + + +# Cascade完整采样器 +class samplerCascadeFull: + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + return {"required": + {"pipe": ("PIPE_LINE",), + "encode_vae_name": (["None"] + folder_paths.get_filename_list("vae"),), + "decode_vae_name": (["None"] + folder_paths.get_filename_list("vae"),), + "steps": ("INT", {"default": 20, "min": 1, "max": 10000}), + "cfg": ("FLOAT", {"default": 4.0, "min": 0.0, "max": 100.0}), + "sampler_name": (comfy.samplers.KSampler.SAMPLERS, {"default":"euler_ancestral"}), + "scheduler": (comfy.samplers.KSampler.SCHEDULERS, {"default":"simple"}), + "denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}), + "image_output": (["Hide", "Preview", "Save", "Hide&Save", "Sender", "Sender&Save"],), + "link_id": ("INT", {"default": 0, "min": 0, "max": sys.maxsize, "step": 1}), + "save_prefix": ("STRING", {"default": "ComfyUI"}), + "seed": ("INT", {"default": 0, "min": 0, "max": MAX_SEED_NUM}), + }, + + "optional": { + "image_to_latent_c": ("IMAGE",), + "latent_c": ("LATENT",), + "model_c": ("MODEL",), + }, + "hidden":{"tile_size": "INT", "prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID", + "embeddingsList": (folder_paths.get_filename_list("embeddings"),) + } + } + + RETURN_TYPES = ("PIPE_LINE", "MODEL", "LATENT") + RETURN_NAMES = ("pipe", "model_b", "latent_b") + OUTPUT_NODE = True + + FUNCTION = "run" + CATEGORY = "EasyUse/Sampler" + + def run(self, pipe, encode_vae_name, decode_vae_name, steps, cfg, sampler_name, scheduler, denoise, image_output, link_id, save_prefix, seed, image_to_latent_c=None, latent_c=None, model_c=None, tile_size=None, prompt=None, extra_pnginfo=None, my_unique_id=None, force_full_denoise=False, disable_noise=False): + + encode_vae_name = encode_vae_name if encode_vae_name is not None else pipe['loader_settings']['encode_vae_name'] + decode_vae_name = decode_vae_name if decode_vae_name is not None else pipe['loader_settings']['decode_vae_name'] + + batch_size = pipe["loader_settings"]["batch_size"] if "batch_size" in pipe["loader_settings"] else 1 + if image_to_latent_c is not None: + if encode_vae_name != 'None': + encode_vae = easyCache.load_vae(encode_vae_name) + else: + encode_vae = pipe['vae'][0] + if "compression" not in pipe["loader_settings"]: + raise Exception("compression is not found") + + compression = pipe["loader_settings"]['compression'] + width = image_to_latent_c.shape[-2] + height = image_to_latent_c.shape[-3] + out_width = (width // compression) * encode_vae.downscale_ratio + out_height = (height // compression) * encode_vae.downscale_ratio + + s = comfy.utils.common_upscale(image_to_latent_c.movedim(-1, 1), out_width, out_height, "bicubic", + "center").movedim(1, -1) + latent_c = encode_vae.encode(s[:, :, :, :3]) + latent_b = torch.zeros([latent_c.shape[0], 4, height // 4, width // 4]) + + samples_c = {"samples": latent_c} + samples_c = RepeatLatentBatch().repeat(samples_c, batch_size)[0] + + samples_b = {"samples": latent_b} + samples_b = RepeatLatentBatch().repeat(samples_b, batch_size)[0] + images = image_to_latent_c + elif latent_c is not None: + samples_c = latent_c + samples_b = pipe["samples"][1] + images = pipe["images"] + else: + samples_c = pipe["samples"][0] + samples_b = pipe["samples"][1] + images = pipe["images"] + + # Clean loaded_objects + easyCache.update_loaded_objects(prompt) + samp_model = model_c if model_c else pipe["model"][0] + samp_positive = pipe["positive"] + samp_negative = pipe["negative"] + samp_samples = samples_c + + samp_seed = seed if seed is not None else pipe['seed'] + + steps = steps if steps is not None else pipe['loader_settings']['steps'] + start_step = pipe['loader_settings']['start_step'] if 'start_step' in pipe['loader_settings'] else 0 + last_step = pipe['loader_settings']['last_step'] if 'last_step' in pipe['loader_settings'] else 10000 + cfg = cfg if cfg is not None else pipe['loader_settings']['cfg'] + sampler_name = sampler_name if sampler_name is not None else pipe['loader_settings']['sampler_name'] + scheduler = scheduler if scheduler is not None else pipe['loader_settings']['scheduler'] + denoise = denoise if denoise is not None else pipe['loader_settings']['denoise'] + noise_device = 'gpu' if "a1111_prompt_style" in pipe['loader_settings'] and pipe['loader_settings']['a1111_prompt_style'] else 'cpu' + # 推理初始时间 + start_time = int(time.time() * 1000) + # 开始推理 + samp_samples = sampler.common_ksampler(samp_model, samp_seed, steps, cfg, sampler_name, scheduler, + samp_positive, samp_negative, samp_samples, denoise=denoise, + preview_latent=False, start_step=start_step, + last_step=last_step, force_full_denoise=False, + disable_noise=False, noise_device=noise_device) + # 推理结束时间 + end_time = int(time.time() * 1000) + stage_c = samp_samples["samples"] + results = None + + if image_output not in ['Hide', 'Hide&Save']: + if decode_vae_name != 'None': + decode_vae = easyCache.load_vae(decode_vae_name) + else: + decode_vae = pipe['vae'][0] + samp_images = decode_vae.decode(stage_c).cpu() + + results = easySave(samp_images, save_prefix, image_output, prompt, extra_pnginfo) + sampler.update_value_by_id("results", my_unique_id, results) + + # 推理总耗时(包含解码) + end_decode_time = int(time.time() * 1000) + spent_time = 'Diffusion:' + str((end_time - start_time) / 1000) + '″, VAEDecode:' + str( + (end_decode_time - end_time) / 1000) + '″ ' + + # Clean loaded_objects + easyCache.update_loaded_objects(prompt) + # zero_out + c1 = [] + for t in samp_positive: + d = t[1].copy() + if "pooled_output" in d: + d["pooled_output"] = torch.zeros_like(d["pooled_output"]) + n = [torch.zeros_like(t[0]), d] + c1.append(n) + # stage_b_conditioning + c2 = [] + for t in c1: + d = t[1].copy() + d['stable_cascade_prior'] = stage_c + n = [t[0], d] + c2.append(n) + + + new_pipe = { + "model": pipe['model'][1], + "positive": c2, + "negative": c1, + "vae": pipe['vae'][1], + "clip": pipe['clip'], + + "samples": samples_b, + "images": images, + "seed": seed, + + "loader_settings": { + **pipe["loader_settings"], + "spent_time": spent_time + } + } + sampler.update_value_by_id("pipe_line", my_unique_id, new_pipe) + + del pipe + + if image_output in ("Hide", "Hide&Save"): + return {"ui": {}, + "result": sampler.get_output(new_pipe, )} + + if image_output in ("Sender", "Sender&Save") and results is not None: + PromptServer.instance.send_sync("img-send", {"link_id": link_id, "images": results}) + + return {"ui": {"images": results}, "result": (new_pipe, new_pipe['model'], new_pipe['samples'])} + +# 简易采样器Cascade +class samplerCascadeSimple(samplerCascadeFull): + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + return {"required": + {"pipe": ("PIPE_LINE",), + "image_output": (["Hide", "Preview", "Save", "Hide&Save", "Sender", "Sender&Save"], {"default": "Preview"}), + "link_id": ("INT", {"default": 0, "min": 0, "max": sys.maxsize, "step": 1}), + "save_prefix": ("STRING", {"default": "ComfyUI"}), + }, + "optional": { + "model_c": ("MODEL",), + }, + "hidden": + {"tile_size": "INT", "prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID", + "embeddingsList": (folder_paths.get_filename_list("embeddings"),) + } + } + + + RETURN_TYPES = ("PIPE_LINE", "IMAGE",) + RETURN_NAMES = ("pipe", "image",) + OUTPUT_NODE = True + FUNCTION = "simple" + CATEGORY = "EasyUse/Sampler" + + def simple(self, pipe, image_output, link_id, save_prefix, model_c=None, tile_size=None, prompt=None, extra_pnginfo=None, my_unique_id=None, force_full_denoise=False, disable_noise=False): + + return super().run(pipe, None, None,None, None,None,None,None, image_output, link_id, save_prefix, + None, None, None, model_c, tile_size, prompt, extra_pnginfo, my_unique_id, force_full_denoise, disable_noise) + +class unsampler: + @classmethod + def INPUT_TYPES(s): + return {"required":{ + "steps": ("INT", {"default": 20, "min": 1, "max": 10000}), + "end_at_step": ("INT", {"default": 0, "min": 0, "max": 10000}), + "cfg": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 100.0}), + "sampler_name": (comfy.samplers.KSampler.SAMPLERS,), + "scheduler": (comfy.samplers.KSampler.SCHEDULERS,), + "normalize": (["disable", "enable"],), + + }, + "optional": { + "pipe": ("PIPE_LINE",), + "optional_model": ("MODEL",), + "optional_positive": ("CONDITIONING",), + "optional_negative": ("CONDITIONING",), + "optional_latent": ("LATENT",), + } + } + + RETURN_TYPES = ("PIPE_LINE", "LATENT",) + RETURN_NAMES = ("pipe", "latent",) + FUNCTION = "unsampler" + + CATEGORY = "EasyUse/Sampler" + + def unsampler(self, cfg, sampler_name, steps, end_at_step, scheduler, normalize, pipe=None, optional_model=None, optional_positive=None, optional_negative=None, + optional_latent=None): + + model = optional_model if optional_model is not None else pipe["model"] + positive = optional_positive if optional_positive is not None else pipe["positive"] + negative = optional_negative if optional_negative is not None else pipe["negative"] + latent_image = optional_latent if optional_latent is not None else pipe["samples"] + + normalize = normalize == "enable" + device = comfy.model_management.get_torch_device() + latent = latent_image + latent_image = latent["samples"] + + end_at_step = min(end_at_step, steps - 1) + end_at_step = steps - end_at_step + + noise = torch.zeros(latent_image.size(), dtype=latent_image.dtype, layout=latent_image.layout, device="cpu") + noise_mask = None + if "noise_mask" in latent: + noise_mask = comfy.sampler_helpers.prepare_mask(latent["noise_mask"], noise.shape, device) + + noise = noise.to(device) + latent_image = latent_image.to(device) + + _positive = comfy.sampler_helpers.convert_cond(positive) + _negative = comfy.sampler_helpers.convert_cond(negative) + models, inference_memory = comfy.sampler_helpers.get_additional_models({"positive": _positive, "negative": _negative}, model.model_dtype()) + + + comfy.model_management.load_models_gpu([model] + models, model.memory_required(noise.shape) + inference_memory) + + model_patcher = comfy.model_patcher.ModelPatcher(model.model, load_device=device, offload_device=comfy.model_management.unet_offload_device()) + + sampler = comfy.samplers.KSampler(model_patcher, steps=steps, device=device, sampler=sampler_name, + scheduler=scheduler, denoise=1.0, model_options=model.model_options) + + sigmas = sampler.sigmas.flip(0) + 0.0001 + + pbar = comfy.utils.ProgressBar(steps) + + def callback(step, x0, x, total_steps): + pbar.update_absolute(step + 1, total_steps) + + samples = sampler.sample(noise, positive, negative, cfg=cfg, latent_image=latent_image, + force_full_denoise=False, denoise_mask=noise_mask, sigmas=sigmas, start_step=0, + last_step=end_at_step, callback=callback) + if normalize: + # technically doesn't normalize because unsampling is not guaranteed to end at a std given by the schedule + samples -= samples.mean() + samples /= samples.std() + samples = samples.cpu() + + comfy.sample.cleanup_additional_models(models) + + out = latent.copy() + out["samples"] = samples + + if pipe is None: + pipe = {} + + new_pipe = { + **pipe, + "samples": out + } + + return (new_pipe, out,) + + +NODE_CLASS_MAPPINGS = { + # kSampler k采样器 + "easy fullkSampler": samplerFull, + "easy kSampler": samplerSimple, + "easy kSamplerCustom": samplerSimpleCustom, + "easy kSamplerTiled": samplerSimpleTiled, + "easy kSamplerLayerDiffusion": samplerSimpleLayerDiffusion, + "easy kSamplerInpainting": samplerSimpleInpainting, + "easy kSamplerDownscaleUnet": samplerSimpleDownscaleUnet, + "easy kSamplerSDTurbo": samplerSDTurbo, + "easy fullCascadeKSampler": samplerCascadeFull, + "easy cascadeKSampler": samplerCascadeSimple, + "easy unSampler": unsampler, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "easy kSampler": "EasyKSampler", + "easy kSamplerCustom": "EasyKSampler (Custom)", + "easy fullkSampler": "EasyKSampler (Full)", + "easy kSamplerTiled": "EasyKSampler (Tiled Decode)", + "easy kSamplerLayerDiffusion": "EasyKSampler (LayerDiffuse)", + "easy kSamplerInpainting": "EasyKSampler (Inpainting)", + "easy kSamplerDownscaleUnet": "EasyKsampler (Downscale Unet)", + "easy kSamplerSDTurbo": "EasyKSampler (SDTurbo)", + "easy cascadeKSampler": "EasyCascadeKsampler", + "easy fullCascadeKSampler": "EasyCascadeKsampler (Full)", + "easy unSampler": "EasyUnSampler", +} \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/seed.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/seed.py new file mode 100644 index 0000000000000000000000000000000000000000..e81b25f158f8082502f41e5cd4d0c5f3c0073b34 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/seed.py @@ -0,0 +1,106 @@ +from ..config import MAX_SEED_NUM +import hashlib +import random + +class easySeed: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "seed": ("INT", {"default": 0, "min": 0, "max": MAX_SEED_NUM}), + }, + "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID"}, + } + + RETURN_TYPES = ("INT",) + RETURN_NAMES = ("seed",) + FUNCTION = "doit" + + CATEGORY = "EasyUse/Seed" + + def doit(self, seed=0, prompt=None, extra_pnginfo=None, my_unique_id=None): + return seed, + +class seedList: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "min_num": ("INT", {"default": 0, "min": 0, "max": MAX_SEED_NUM}), + "max_num": ("INT", {"default": MAX_SEED_NUM, "max": MAX_SEED_NUM, "min": 0 }), + "method": (["random", "increment", "decrement"], {"default": "random"}), + "total": ("INT", {"default": 1, "min": 1, "max": 100000}), + "seed": ("INT", {"default": 0, "min": 0, "max": MAX_SEED_NUM,}), + }, + "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID"}, + } + + RETURN_TYPES = ("INT", "INT") + RETURN_NAMES = ("seed", "total") + FUNCTION = "doit" + DESCRIPTION = "Random number seed that can be used in a for loop, by connecting index and easy indexAny node to realize different seed values in the loop." + + CATEGORY = "EasyUse/Seed" + + def doit(self, min_num, max_num, method, total, seed=0, prompt=None, extra_pnginfo=None, my_unique_id=None): + random.seed(seed) + + seed_list = [] + if min_num > max_num: + min_num, max_num = max_num, min_num + for i in range(total): + if method == 'random': + s = random.randint(min_num, max_num) + elif method == 'increment': + s = min_num + i + if s > max_num: + s = max_num + elif method == 'decrement': + s = max_num - i + if s < min_num: + s = min_num + seed_list.append(s) + return seed_list, total + + @classmethod + def IS_CHANGED(s, seed, **kwargs): + m = hashlib.sha256() + m.update(seed) + return m.digest().hex() + +# 全局随机种 +class globalSeed: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "value": ("INT", {"default": 0, "min": 0, "max": MAX_SEED_NUM}), + "mode": ("BOOLEAN", {"default": True, "label_on": "control_before_generate", "label_off": "control_after_generate"}), + "action": (["fixed", "increment", "decrement", "randomize", + "increment for each node", "decrement for each node", "randomize for each node"], ), + "last_seed": ("STRING", {"default": ""}), + } + } + + RETURN_TYPES = () + FUNCTION = "doit" + + CATEGORY = "EasyUse/Seed" + + OUTPUT_NODE = True + + def doit(self, **kwargs): + return {} + + +NODE_CLASS_MAPPINGS = { + "easy seed": easySeed, + "easy seedList": seedList, + "easy globalSeed": globalSeed, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "easy seed": "EasySeed", + "easy seedList": "EasySeedList", + "easy globalSeed": "EasyGlobalSeed", +} \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/util.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/util.py new file mode 100644 index 0000000000000000000000000000000000000000..cd7eb25f11876ddfbbf9f06e7062a603d8433b00 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/util.py @@ -0,0 +1,424 @@ +import os +import re +import folder_paths +import json +from ..libs.utils import AlwaysEqualProxy + +class showLoaderSettingsNames: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "pipe": ("PIPE_LINE",), + }, + "hidden": { + "unique_id": "UNIQUE_ID", + "extra_pnginfo": "EXTRA_PNGINFO", + }, + } + + RETURN_TYPES = ("STRING", "STRING", "STRING",) + RETURN_NAMES = ("ckpt_name", "vae_name", "lora_name") + + FUNCTION = "notify" + OUTPUT_NODE = True + + CATEGORY = "EasyUse/Util" + + def notify(self, pipe, names=None, unique_id=None, extra_pnginfo=None): + if unique_id and extra_pnginfo and "workflow" in extra_pnginfo: + workflow = extra_pnginfo["workflow"] + node = next((x for x in workflow["nodes"] if str(x["id"]) == unique_id), None) + if node: + ckpt_name = pipe['loader_settings']['ckpt_name'] if 'ckpt_name' in pipe['loader_settings'] else '' + vae_name = pipe['loader_settings']['vae_name'] if 'vae_name' in pipe['loader_settings'] else '' + lora_name = pipe['loader_settings']['lora_name'] if 'lora_name' in pipe['loader_settings'] else '' + + if ckpt_name: + ckpt_name = os.path.basename(os.path.splitext(ckpt_name)[0]) + if vae_name: + vae_name = os.path.basename(os.path.splitext(vae_name)[0]) + if lora_name: + lora_name = os.path.basename(os.path.splitext(lora_name)[0]) + + names = "ckpt_name: " + ckpt_name + '\n' + "vae_name: " + vae_name + '\n' + "lora_name: " + lora_name + node["widgets_values"] = names + + return {"ui": {"text": [names]}, "result": (ckpt_name, vae_name, lora_name)} + +class sliderControl: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "mode": (['ipadapter layer weights'],), + "model_type": (['sdxl', 'sd1'],), + }, + "hidden": { + "prompt": "PROMPT", + "my_unique_id": "UNIQUE_ID", + "extra_pnginfo": "EXTRA_PNGINFO", + }, + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("layer_weights",) + + FUNCTION = "control" + + CATEGORY = "EasyUse/Util" + + def control(self, mode, model_type, prompt=None, my_unique_id=None, extra_pnginfo=None): + values = '' + if my_unique_id in prompt: + if 'values' in prompt[my_unique_id]["inputs"]: + values = prompt[my_unique_id]["inputs"]['values'] + + return (values,) + +class setCkptName: + @classmethod + def INPUT_TYPES(cls): + return {"required": { + "ckpt_name": (folder_paths.get_filename_list("checkpoints"),), + } + } + + RETURN_TYPES = (AlwaysEqualProxy('*'),) + RETURN_NAMES = ("ckpt_name",) + FUNCTION = "set_name" + CATEGORY = "EasyUse/Util" + + def set_name(self, ckpt_name): + return (ckpt_name,) + +class setControlName: + + @classmethod + def INPUT_TYPES(cls): + return {"required": { + "controlnet_name": (folder_paths.get_filename_list("controlnet"),), + } + } + + RETURN_TYPES = (AlwaysEqualProxy('*'),) + RETURN_NAMES = ("controlnet_name",) + FUNCTION = "set_name" + CATEGORY = "EasyUse/Util" + + def set_name(self, controlnet_name): + return (controlnet_name,) + +class setLoraName: + + @classmethod + def INPUT_TYPES(cls): + return {"required": { + "lora_name": (folder_paths.get_filename_list("loras"),), + } + } + + RETURN_TYPES = (AlwaysEqualProxy('*'),) + RETURN_NAMES = ("lora_name",) + FUNCTION = "set_name" + CATEGORY = "EasyUse/Util" + + def set_name(self, lora_name): + return (lora_name,) + + +def _markdown_table_to_image(markdown: str, font_path: str): + """将 Markdown 表格字符串渲染为 PIL.Image(RGB),支持单元格自动换行。""" + from PIL import Image, ImageDraw, ImageFont + + # 解析行,过滤分隔行(如 |---|---|) + lines = [l for l in (markdown or '').strip().splitlines() if l.strip()] + table_rows = [] + for line in lines: + if re.match(r'^\|[\s\-:|]+\|$', line.strip()): + continue + cells = [re.sub(r'\*\*(.+?)\*\*', lambda m: '\x01' + m.group(1) + '\x02', + re.sub(r'', '\n', c.strip(), flags=re.IGNORECASE)) + for c in line.strip().strip('|').split('|')] + table_rows.append(cells) + + if not table_rows: + return Image.new("RGB", (400, 80), (255, 255, 255)) + + num_cols = max(len(r) for r in table_rows) + table_rows = [r + [''] * (num_cols - len(r)) for r in table_rows] + + # 加载字体 + font_size = 16 + try: + font = ImageFont.truetype(font_path, font_size) + except Exception: + font = ImageFont.load_default() + + pad_x, pad_y = 14, 10 + border = 1 + max_cell_text_width = 200 # 单元格文字区域最大宽度(像素) + + def get_text_width(text): + clean = re.sub('[\x01\x02]', '', text) + try: + bbox = font.getbbox(clean) + return bbox[2] - bbox[0] + except Exception: + return len(clean) * 9 + + def get_line_height(): + try: + bbox = font.getbbox('Ag\u4e2d') + return bbox[3] - bbox[1] + except Exception: + return font_size + 2 + + def wrap_text(text, max_width): + """换行:先按 \\n 切段,每段再按英文单词边界 / CJK 字符换行。""" + if not text: + return [''] + # 先按显式换行符切段,再对每段分别软换行 + hard_lines = text.split('\n') + if len(hard_lines) > 1: + result = [] + for hl in hard_lines: + result.extend(wrap_text(hl, max_width)) + return result if result else [''] + + # 将文本拆分为:CJK 单字符 / 空白序列 / 非CJK非空白序列(英文单词/标点等) + tokens = re.findall( + r'[\u4e00-\u9fff\u3000-\u303f\uff00-\uffef]' + r'|[ \t]+' + r'|[^ \t\u4e00-\u9fff\u3000-\u303f\uff00-\uffef]+', + text + ) + + result, current = [], '' + for token in tokens: + is_space = token.strip() == '' + test = current + token + if get_text_width(test) <= max_width: + if is_space and not current: + continue # 跳过行首空格 + current = test + else: + if is_space: + # 空白处换行,丢弃该空白 + if current: + result.append(current) + current = '' + elif get_text_width(token) <= max_width: + # 整个 token 能放一行,整体移到下一行 + if current: + result.append(current) + current = token + else: + # token 本身超宽(极长单词),逐字符强拆 + for char in token: + if get_text_width(current + char) <= max_width: + current += char + else: + if current: + result.append(current) + current = char + if current: + result.append(current) + return result if result else [''] + + line_h = get_line_height() + + def parse_line_segments(line): + """将含 \\x01..\\x02 粗体标记的行拆分为 (text, is_bold) 片段列表。""" + result, bold = [], False + for part in re.split('([\x01\x02])', line): + if part == '\x01': + bold = True + elif part == '\x02': + bold = False + elif part: + result.append((part, bold)) + return result or [('', False)] + + def balance_bold_markers(lines): + """确保每行粗体标记自成一对:跨行时在行首补开、行尾补关标记。""" + result, in_bold = [], False + for line in lines: + if in_bold: + line = '\x01' + line + for ch in line: + if ch == '\x01': in_bold = True + elif ch == '\x02': in_bold = False + if in_bold: + line = line + '\x02' + result.append(line) + return result + + # 第一遍:计算各列宽度(不超过 max_cell_text_width,按子行分别测量) + col_text_widths = [] + for col_idx in range(num_cols): + max_w = 0 + for row in table_rows: + cell = row[col_idx] if col_idx < len(row) else '' + for seg_line in cell.split('\n'): + max_w = max(max_w, min(get_text_width(seg_line), max_cell_text_width)) + col_text_widths.append(max_w) + col_widths = [w + pad_x * 2 for w in col_text_widths] + + # 第二遍:对每行每格换行,计算各行高度 + wrapped_rows = [] + row_heights = [] + for row in table_rows: + wrapped_cells = [] + max_lines = 1 + for col_idx in range(num_cols): + cell = row[col_idx] if col_idx < len(row) else '' + wrapped = balance_bold_markers(wrap_text(cell, col_text_widths[col_idx])) + wrapped_cells.append(wrapped) + max_lines = max(max_lines, len(wrapped)) + wrapped_rows.append(wrapped_cells) + row_heights.append(max_lines * line_h + pad_y * 2) + + # 每列左边缘 x 坐标(每列前留 1px 边框) + col_x = [border] + for cw in col_widths: + col_x.append(col_x[-1] + cw + border) + + total_width = col_x[-1] + total_height = border + sum(rh + border for rh in row_heights) + + # 配色 + header_bg = (52, 73, 94) + header_fg = (255, 255, 255) + even_bg = (248, 249, 252) + odd_bg = (255, 255, 255) + border_color = (180, 185, 195) + text_color = (50, 54, 62) + + # 以边框色填充整张图,格线自然显现 + img = Image.new("RGB", (total_width, total_height), border_color) + draw = ImageDraw.Draw(img) + + def render_line(x, y, line, fg): + """逐片段渲染一行文字;粗体通过向右偏移 1px 再描一遍来模拟加粗。""" + try: + text_offset = -font.getbbox(re.sub('[\x01\x02]', '', line) or 'A')[1] + except Exception: + text_offset = 0 + sx = x + for seg, is_bold in parse_line_segments(line): + draw.text((sx, y + text_offset), seg, font=font, fill=fg) + if is_bold: + draw.text((sx + 1, y + text_offset), seg, font=font, fill=fg) + try: + w = font.getbbox(seg)[2] - font.getbbox(seg)[0] + except Exception: + w = len(seg) * 9 + sx += w + (1 if is_bold else 0) + + row_y = border + for row_idx, (wrapped_cells, rh) in enumerate(zip(wrapped_rows, row_heights)): + is_header = row_idx == 0 + bg = header_bg if is_header else (odd_bg if row_idx % 2 == 1 else even_bg) + fg = header_fg if is_header else text_color + + for col_idx in range(num_cols): + cx, cw = col_x[col_idx], col_widths[col_idx] + # 填充单元格背景 + draw.rectangle([cx, row_y, cx + cw - 1, row_y + rh - 1], fill=bg) + + cell_lines = wrapped_cells[col_idx] if col_idx < len(wrapped_cells) else [''] + total_text_h = len(cell_lines) * line_h + ty = row_y + (rh - total_text_h) // 2 # 垂直居中起点 + for line_text in cell_lines: + render_line(cx + pad_x, ty, line_text, fg) + ty += line_h + + row_y += rh + border + + # 最长边不小于 1280,等比放大 + min_long_side = 1280 + long_side = max(img.width, img.height) + if long_side < min_long_side: + scale = min_long_side / long_side + new_w = round(img.width * scale) + new_h = round(img.height * scale) + img = img.resize((new_w, new_h), Image.LANCZOS) + + return img + + +class tableEditor: + """表格编辑器节点 —— 通过可视化表格或 Markdown 语法编辑数据,输出 Markdown 字符串。""" + + CATEGORY = "EasyUse/Util" + + RETURN_TYPES = ("STRING", "IMAGE") + RETURN_NAMES = ("markdown", "image") + FUNCTION = "execute" + + DESCRIPTION = "通过可视化表格或 Markdown 语法编辑数据,输出 Markdown 格式的表格字符串。" + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "table_data": ("EASY_TABLE_EDITOR",), + }, + } + + def execute(self, table_data): + # 表格数据可能是纯 Markdown 字符串,也可能是序列化后的 JSON + if isinstance(table_data, str) and table_data.strip().startswith('{'): + try: + obj = json.loads(table_data) + markdown = obj.get('markdown', '') + if not markdown: + # 重新从 headers/rows 生成 + headers = obj.get('headers', []) + rows = obj.get('rows', []) + col_widths = [max(len(str(h)), 3) for h in headers] + for row in rows: + for i, cell in enumerate(row): + if i < len(col_widths): + col_widths[i] = max(col_widths[i], len(str(cell))) + header_line = '| ' + ' | '.join(str(h).ljust(col_widths[i]) for i, h in enumerate(headers)) + ' |' + sep_line = '| ' + ' | '.join('-' * w for w in col_widths) + ' |' + row_lines = [ + '| ' + ' | '.join(str(row[i] if i < len(row) else '').ljust(col_widths[i]) for i in range(len(headers))) + ' |' + for row in rows + ] + markdown = '\n'.join([header_line, sep_line] + row_lines) + except Exception: + markdown = table_data + else: + markdown = table_data + + # 将 Markdown 表格渲染为图像 + font_path = os.path.join( + os.path.dirname(os.path.dirname(os.path.dirname(__file__))), + 'resources', 'wenquan.ttf' + ) + from ..libs.image import pil2tensor + img_tensor = pil2tensor(_markdown_table_to_image(markdown, font_path).convert("RGB")) + + return (markdown, img_tensor) + + +NODE_CLASS_MAPPINGS = { + "easy showLoaderSettingsNames": showLoaderSettingsNames, + "easy sliderControl": sliderControl, + "easy ckptNames": setCkptName, + "easy controlnetNames": setControlName, + "easy loraNames": setLoraName, + "easy tableEditor": tableEditor, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "easy showLoaderSettingsNames": "Show Loader Settings Names", + "easy sliderControl": "Easy Slider Control", + "easy ckptNames": "Ckpt Names", + "easy controlnetNames": "ControlNet Names", + "easy loraNames": "Lora Names", + "easy tableEditor": "Table Editor", +} diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/xyplot.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/xyplot.py new file mode 100644 index 0000000000000000000000000000000000000000..5840a098ffd9cd45a99cd03bcab25fa4055d6332 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/nodes/xyplot.py @@ -0,0 +1,699 @@ +import os +import json +import comfy +import folder_paths +from ..config import RESOURCES_DIR +from ..libs.utils import getMetadata +def load_preset(filename): + path = os.path.join(RESOURCES_DIR, filename) + path = os.path.abspath(path) + preset_list = [] + + if os.path.exists(path): + with open(path, 'r') as file: + for line in file: + preset_list.append(line.strip()) + + return preset_list + else: + return [] +def generate_floats(batch_count, first_float, last_float): + if batch_count > 1: + interval = (last_float - first_float) / (batch_count - 1) + values = [str(round(first_float + i * interval, 3)) for i in range(batch_count)] + else: + values = [str(first_float)] if batch_count == 1 else [] + return "; ".join(values) + +def generate_ints(batch_count, first_int, last_int): + if batch_count > 1: + interval = (last_int - first_int) / (batch_count - 1) + values = [str(int(first_int + i * interval)) for i in range(batch_count)] + else: + values = [str(first_int)] if batch_count == 1 else [] + # values = list(set(values)) # Remove duplicates + # values.sort() # Sort in ascending order + return "; ".join(values) + +# Seed++ Batch +class XYplot_SeedsBatch: + + @classmethod + def INPUT_TYPES(cls): + return {"required": { + "batch_count": ("INT", {"default": 3, "min": 1, "max": 50}), }, + } + + RETURN_TYPES = ("X_Y",) + RETURN_NAMES = ("X or Y",) + FUNCTION = "xy_value" + CATEGORY = "EasyUse/XY Inputs" + + def xy_value(self, batch_count): + + axis = "advanced: Seeds++ Batch" + xy_values = {"axis": axis, "values": batch_count} + return (xy_values,) + +# Step Values +class XYplot_Steps: + parameters = ["steps", "start_at_step", "end_at_step",] + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "target_parameter": (cls.parameters,), + "batch_count": ("INT", {"default": 3, "min": 0, "max": 50}), + "first_step": ("INT", {"default": 10, "min": 1, "max": 10000}), + "last_step": ("INT", {"default": 20, "min": 1, "max": 10000}), + "first_start_step": ("INT", {"default": 0, "min": 0, "max": 10000}), + "last_start_step": ("INT", {"default": 10, "min": 0, "max": 10000}), + "first_end_step": ("INT", {"default": 10, "min": 0, "max": 10000}), + "last_end_step": ("INT", {"default": 20, "min": 0, "max": 10000}), + } + } + + RETURN_TYPES = ("X_Y",) + RETURN_NAMES = ("X or Y",) + FUNCTION = "xy_value" + CATEGORY = "EasyUse/XY Inputs" + + def xy_value(self, target_parameter, batch_count, first_step, last_step, first_start_step, last_start_step, + first_end_step, last_end_step,): + + axis, xy_first, xy_last = None, None, None + + if target_parameter == "steps": + axis = "advanced: Steps" + xy_first = first_step + xy_last = last_step + elif target_parameter == "start_at_step": + axis = "advanced: StartStep" + xy_first = first_start_step + xy_last = last_start_step + elif target_parameter == "end_at_step": + axis = "advanced: EndStep" + xy_first = first_end_step + xy_last = last_end_step + + values = generate_ints(batch_count, xy_first, xy_last) + return ({"axis": axis, "values": values},) if values is not None else (None,) + +class XYplot_CFG: + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "batch_count": ("INT", {"default": 3, "min": 0, "max": 50}), + "first_cfg": ("FLOAT", {"default": 7.0, "min": 0.0, "max": 100.0}), + "last_cfg": ("FLOAT", {"default": 9.0, "min": 0.0, "max": 100.0}), + } + } + + RETURN_TYPES = ("X_Y",) + RETURN_NAMES = ("X or Y",) + FUNCTION = "xy_value" + CATEGORY = "EasyUse/XY Inputs" + + def xy_value(self, batch_count, first_cfg, last_cfg): + axis = "advanced: CFG Scale" + values = generate_floats(batch_count, first_cfg, last_cfg) + return ({"axis": axis, "values": values},) if values else (None,) + +class XYplot_FluxGuidance: + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "batch_count": ("INT", {"default": 3, "min": 0, "max": 50}), + "first_guidance": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 100.0}), + "last_guidance": ("FLOAT", {"default": 3.5, "min": 0.0, "max": 100.0}), + } + } + + RETURN_TYPES = ("X_Y",) + RETURN_NAMES = ("X or Y",) + FUNCTION = "xy_value" + CATEGORY = "EasyUse/XY Inputs" + + def xy_value(self, batch_count, first_guidance, last_guidance): + axis = "advanced: Flux Guidance" + values = generate_floats(batch_count, first_guidance, last_guidance) + return ({"axis": axis, "values": values},) if values else (None,) + +# Step Values +class XYplot_Sampler_Scheduler: + parameters = ["sampler", "scheduler", "sampler & scheduler"] + + @classmethod + def INPUT_TYPES(cls): + samplers = ["None"] + comfy.samplers.KSampler.SAMPLERS + schedulers = ["None"] + comfy.samplers.KSampler.SCHEDULERS + inputs = { + "required": { + "target_parameter": (cls.parameters,), + "input_count": ("INT", {"default": 1, "min": 1, "max": 30, "step": 1}) + } + } + for i in range(1, 30 + 1): + inputs["required"][f"sampler_{i}"] = (samplers,) + inputs["required"][f"scheduler_{i}"] = (schedulers,) + + return inputs + + RETURN_TYPES = ("X_Y",) + RETURN_NAMES = ("X or Y",) + FUNCTION = "xy_value" + CATEGORY = "EasyUse/XY Inputs" + + def xy_value(self, target_parameter, input_count, **kwargs): + axis, values, = None, None, + if target_parameter == "scheduler": + axis = "advanced: Scheduler" + schedulers = [kwargs.get(f"scheduler_{i}") for i in range(1, input_count + 1)] + values = [scheduler for scheduler in schedulers if scheduler != "None"] + elif target_parameter == "sampler": + axis = "advanced: Sampler" + samplers = [kwargs.get(f"sampler_{i}") for i in range(1, input_count + 1)] + values = [sampler for sampler in samplers if sampler != "None"] + else: + axis = "advanced: Sampler&Scheduler" + samplers = [kwargs.get(f"sampler_{i}") for i in range(1, input_count + 1)] + schedulers = [kwargs.get(f"scheduler_{i}") for i in range(1, input_count + 1)] + values = [] + for sampler, scheduler in zip(samplers, schedulers): + sampler = sampler if sampler else 'None' + scheduler = scheduler if scheduler else 'None' + values.append(sampler +','+ scheduler) + values = "; ".join(values) + return ({"axis": axis, "values": values},) if values else (None,) + +class XYplot_Denoise: + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "batch_count": ("INT", {"default": 3, "min": 0, "max": 50}), + "first_denoise": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.1}), + "last_denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.1}), + } + } + + RETURN_TYPES = ("X_Y",) + RETURN_NAMES = ("X or Y",) + FUNCTION = "xy_value" + CATEGORY = "EasyUse/XY Inputs" + + def xy_value(self, batch_count, first_denoise, last_denoise): + axis = "advanced: Denoise" + values = generate_floats(batch_count, first_denoise, last_denoise) + return ({"axis": axis, "values": values},) if values else (None,) + +# PromptSR +class XYplot_PromptSR: + + @classmethod + def INPUT_TYPES(cls): + inputs = { + "required": { + "target_prompt": (["positive", "negative"],), + "search_txt": ("STRING", {"default": "", "multiline": False}), + "replace_all_text": ("BOOLEAN", {"default": False}), + "replace_count": ("INT", {"default": 3, "min": 1, "max": 30 - 1}), + } + } + + # Dynamically add replace_X inputs + for i in range(1, 30): + replace_key = f"replace_{i}" + inputs["required"][replace_key] = ("STRING", {"default": "", "multiline": False, "placeholder": replace_key}) + + return inputs + + RETURN_TYPES = ("X_Y",) + RETURN_NAMES = ("X or Y",) + FUNCTION = "xy_value" + CATEGORY = "EasyUse/XY Inputs" + + def xy_value(self, target_prompt, search_txt, replace_all_text, replace_count, **kwargs): + axis = None + + if target_prompt == "positive": + axis = "advanced: Positive Prompt S/R" + elif target_prompt == "negative": + axis = "advanced: Negative Prompt S/R" + + # Create base entry + values = [(search_txt, None, replace_all_text)] + + if replace_count > 0: + # Append additional entries based on replace_count + values.extend([(search_txt, kwargs.get(f"replace_{i+1}"), replace_all_text) for i in range(replace_count)]) + return ({"axis": axis, "values": values},) if values is not None else (None,) + +# XYPlot Pos Condition +class XYplot_Positive_Cond: + + @classmethod + def INPUT_TYPES(cls): + inputs = { + "optional": { + "positive_1": ("CONDITIONING",), + "positive_2": ("CONDITIONING",), + "positive_3": ("CONDITIONING",), + "positive_4": ("CONDITIONING",), + } + } + + return inputs + + RETURN_TYPES = ("X_Y",) + RETURN_NAMES = ("X or Y",) + FUNCTION = "xy_value" + CATEGORY = "EasyUse/XY Inputs" + + def xy_value(self, positive_1=None, positive_2=None, positive_3=None, positive_4=None): + axis = "advanced: Pos Condition" + values = [] + cond = [] + # Create base entry + if positive_1 is not None: + values.append("0") + cond.append(positive_1) + if positive_2 is not None: + values.append("1") + cond.append(positive_2) + if positive_3 is not None: + values.append("2") + cond.append(positive_3) + if positive_4 is not None: + values.append("3") + cond.append(positive_4) + + return ({"axis": axis, "values": values, "cond": cond},) if values is not None else (None,) + +# XYPlot Neg Condition +class XYplot_Negative_Cond: + + @classmethod + def INPUT_TYPES(cls): + inputs = { + "optional": { + "negative_1": ("CONDITIONING",), + "negative_2": ("CONDITIONING",), + "negative_3": ("CONDITIONING",), + "negative_4": ("CONDITIONING",), + } + } + + return inputs + + RETURN_TYPES = ("X_Y",) + RETURN_NAMES = ("X or Y",) + FUNCTION = "xy_value" + CATEGORY = "EasyUse/XY Inputs" + + def xy_value(self, negative_1=None, negative_2=None, negative_3=None, negative_4=None): + axis = "advanced: Neg Condition" + values = [] + cond = [] + # Create base entry + if negative_1 is not None: + values.append(0) + cond.append(negative_1) + if negative_2 is not None: + values.append(1) + cond.append(negative_2) + if negative_3 is not None: + values.append(2) + cond.append(negative_3) + if negative_4 is not None: + values.append(3) + cond.append(negative_4) + + return ({"axis": axis, "values": values, "cond": cond},) if values is not None else (None,) + +# XYPlot Pos Condition List +class XYplot_Positive_Cond_List: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "positive": ("CONDITIONING",), + } + } + + INPUT_IS_LIST = True + RETURN_TYPES = ("X_Y",) + RETURN_NAMES = ("X or Y",) + FUNCTION = "xy_value" + CATEGORY = "EasyUse/XY Inputs" + + def xy_value(self, positive): + axis = "advanced: Pos Condition" + values = [] + cond = [] + for index, c in enumerate(positive): + values.append(str(index)) + cond.append(c) + + return ({"axis": axis, "values": values, "cond": cond},) if values is not None else (None,) + +# XYPlot Neg Condition List +class XYplot_Negative_Cond_List: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "negative": ("CONDITIONING",), + } + } + + INPUT_IS_LIST = True + RETURN_TYPES = ("X_Y",) + RETURN_NAMES = ("X or Y",) + FUNCTION = "xy_value" + CATEGORY = "EasyUse/XY Inputs" + + def xy_value(self, negative): + axis = "advanced: Neg Condition" + values = [] + cond = [] + for index, c in enumerate(negative): + values.append(index) + cond.append(c) + + return ({"axis": axis, "values": values, "cond": cond},) if values is not None else (None,) + +# XY Plot: ControlNet +class XYplot_Control_Net: + parameters = ["strength", "start_percent", "end_percent"] + @classmethod + def INPUT_TYPES(cls): + def get_file_list(filenames): + return [file for file in filenames if file != "put_models_here.txt" and "lllite" not in file] + + return { + "required": { + "control_net_name": (get_file_list(folder_paths.get_filename_list("controlnet")),), + "image": ("IMAGE",), + "target_parameter": (cls.parameters,), + "batch_count": ("INT", {"default": 3, "min": 1, "max": 30}), + "first_strength": ("FLOAT", {"default": 0.0, "min": 0.00, "max": 10.0, "step": 0.01}), + "last_strength": ("FLOAT", {"default": 1.0, "min": 0.00, "max": 10.0, "step": 0.01}), + "first_start_percent": ("FLOAT", {"default": 0.0, "min": 0.00, "max": 1.0, "step": 0.01}), + "last_start_percent": ("FLOAT", {"default": 1.0, "min": 0.00, "max": 1.0, "step": 0.01}), + "first_end_percent": ("FLOAT", {"default": 0.0, "min": 0.00, "max": 1.0, "step": 0.01}), + "last_end_percent": ("FLOAT", {"default": 1.0, "min": 0.00, "max": 1.0, "step": 0.01}), + "strength": ("FLOAT", {"default": 1.0, "min": 0.00, "max": 10.0, "step": 0.01}), + "start_percent": ("FLOAT", {"default": 0.0, "min": 0.00, "max": 1.0, "step": 0.01}), + "end_percent": ("FLOAT", {"default": 1.0, "min": 0.00, "max": 1.0, "step": 0.01}), + }, + "optional": { + "control_net": ("CONTROL_NET",), + }, + } + + RETURN_TYPES = ("X_Y",) + RETURN_NAMES = ("X or Y",) + FUNCTION = "xy_value" + CATEGORY = "EasyUse/XY Inputs" + + def xy_value(self, control_net_name, image, target_parameter, batch_count, first_strength, last_strength, first_start_percent, + last_start_percent, first_end_percent, last_end_percent, strength, start_percent, end_percent, control_net=None): + + axis, = None, + + values = [] + + if target_parameter == "strength": + axis = "advanced: ControlNetStrength" + + values.append([(control_net_name, image, first_strength, start_percent, end_percent, control_net)]) + strength_increment = (last_strength - first_strength) / (batch_count - 1) if batch_count > 1 else 0 + for i in range(1, batch_count - 1): + values.append([(control_net_name, image, first_strength + i * strength_increment, start_percent, + end_percent, control_net)]) + if batch_count > 1: + values.append([(control_net_name, image, last_strength, start_percent, end_percent, control_net)]) + + elif target_parameter == "start_percent": + axis = "advanced: ControlNetStart%" + + percent_increment = (last_start_percent - first_start_percent) / (batch_count - 1) if batch_count > 1 else 0 + values.append([(control_net_name, image, strength, first_start_percent, end_percent, control_net)]) + for i in range(1, batch_count - 1): + values.append([(control_net_name, image, strength, first_start_percent + i * percent_increment, + end_percent, control_net)]) + + # Always add the last start_percent if batch_count is more than 1. + if batch_count > 1: + values.append([(control_net_name, image, strength, last_start_percent, end_percent, control_net)]) + + elif target_parameter == "end_percent": + axis = "advanced: ControlNetEnd%" + + percent_increment = (last_end_percent - first_end_percent) / (batch_count - 1) if batch_count > 1 else 0 + values.append([(control_net_name, image, strength, start_percent, first_end_percent, control_net)]) + for i in range(1, batch_count - 1): + values.append([(control_net_name, image, strength, start_percent, + first_end_percent + i * percent_increment, control_net)]) + + if batch_count > 1: + values.append([(control_net_name, image, strength, start_percent, last_end_percent, control_net)]) + + + return ({"axis": axis, "values": values},) + + +#Checkpoints +class XYplot_Checkpoint: + + modes = ["Ckpt Names", "Ckpt Names+ClipSkip", "Ckpt Names+ClipSkip+VAE"] + + @classmethod + def INPUT_TYPES(cls): + + checkpoints = ["None"] + folder_paths.get_filename_list("checkpoints") + vaes = ["Baked VAE"] + folder_paths.get_filename_list("vae") + + inputs = { + "required": { + "input_mode": (cls.modes,), + "ckpt_count": ("INT", {"default": 3, "min": 0, "max": 10, "step": 1}), + } + } + + for i in range(1, 10 + 1): + inputs["required"][f"ckpt_name_{i}"] = (checkpoints,) + inputs["required"][f"clip_skip_{i}"] = ("INT", {"default": -1, "min": -24, "max": -1, "step": 1}) + inputs["required"][f"vae_name_{i}"] = (vaes,) + + inputs["optional"] = { + "optional_lora_stack": ("LORA_STACK",) + } + return inputs + + RETURN_TYPES = ("X_Y",) + RETURN_NAMES = ("X or Y",) + FUNCTION = "xy_value" + + CATEGORY = "EasyUse/XY Inputs" + + def xy_value(self, input_mode, ckpt_count, **kwargs): + + axis = "advanced: Checkpoint" + + checkpoints = [kwargs.get(f"ckpt_name_{i}") for i in range(1, ckpt_count + 1)] + clip_skips = [kwargs.get(f"clip_skip_{i}") for i in range(1, ckpt_count + 1)] + vaes = [kwargs.get(f"vae_name_{i}") for i in range(1, ckpt_count + 1)] + + # Set None for Clip Skip and/or VAE if not correct modes + for i in range(ckpt_count): + if "ClipSkip" not in input_mode: + clip_skips[i] = 'None' + if "VAE" not in input_mode: + vaes[i] = 'None' + + # Extend each sub-array with lora_stack if it's not None + values = [checkpoint.replace(',', '*')+','+str(clip_skip)+','+vae.replace(',', '*') for checkpoint, clip_skip, vae in zip(checkpoints, clip_skips, vaes) if + checkpoint != "None"] + + optional_lora_stack = kwargs.get("optional_lora_stack") if "optional_lora_stack" in kwargs else [] + + xy_values = {"axis": axis, "values": values, "lora_stack": optional_lora_stack} + return (xy_values,) + +#Loras +class XYplot_Lora: + + modes = ["Lora Names", "Lora Names+Weights"] + + @classmethod + def INPUT_TYPES(cls): + loras = ["None"] + folder_paths.get_filename_list("loras") + + inputs = { + "required": { + "input_mode": (cls.modes,), + "lora_count": ("INT", {"default": 3, "min": 0, "max": 10, "step": 1}), + "model_strength": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}), + "clip_strength": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}), + } + } + + for i in range(1, 10 + 1): + inputs["required"][f"lora_name_{i}"] = (loras,) + inputs["required"][f"model_str_{i}"] = ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}) + inputs["required"][f"clip_str_{i}"] = ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}) + + inputs["optional"] = { + "optional_lora_stack": ("LORA_STACK",), + "display_trigger_word": ("BOOLEAN", {"display_trigger_word": True, "tooltip": "Trigger words showing lora model pass through the model's metadata, but not necessarily accurately."}), + } + return inputs + + RETURN_TYPES = ("X_Y",) + RETURN_NAMES = ("X or Y",) + FUNCTION = "xy_value" + + CATEGORY = "EasyUse/XY Inputs" + + def sort_tags_by_frequency(self, meta_tags): + if meta_tags is None: + return [] + if "ss_tag_frequency" in meta_tags: + meta_tags = meta_tags["ss_tag_frequency"] + meta_tags = json.loads(meta_tags) + sorted_tags = {} + for _, dataset in meta_tags.items(): + for tag, count in dataset.items(): + tag = str(tag).strip() + if tag in sorted_tags: + sorted_tags[tag] = sorted_tags[tag] + count + else: + sorted_tags[tag] = count + # sort tags by training frequency. Most seen tags firsts + sorted_tags = dict(sorted(sorted_tags.items(), key=lambda item: item[1], reverse=True)) + return list(sorted_tags.keys()) + else: + return [] + + def get_trigger_words(self, lora_name, display=False): + if not display: + return "" + + file_path = folder_paths.get_full_path('loras', lora_name) + if not file_path: + return '' + header = getMetadata(file_path) + header_json = json.loads(header) + meta = header_json["__metadata__"] if "__metadata__" in header_json else None + tags = self.sort_tags_by_frequency(meta) + return ' '+ tags[0] if len(tags) > 0 else '' + def xy_value(self, input_mode, lora_count, model_strength, clip_strength, display_trigger_words=True, **kwargs): + + axis = "advanced: Lora" + # Extract values from kwargs + loras = [kwargs.get(f"lora_name_{i}") for i in range(1, lora_count + 1)] + model_strs = [kwargs.get(f"model_str_{i}", model_strength) for i in range(1, lora_count + 1)] + clip_strs = [kwargs.get(f"clip_str_{i}", clip_strength) for i in range(1, lora_count + 1)] + + # Use model_strength and clip_strength for the loras where values are not provided + if "Weights" not in input_mode: + for i in range(lora_count): + model_strs[i] = model_strength + clip_strs[i] = clip_strength + + # Extend each sub-array with lora_stack if it's not None + values = [lora.replace(',', '*')+','+str(model_str)+','+str(clip_str) +',' + self.get_trigger_words(lora, display_trigger_words) for lora, model_str, clip_str + in zip(loras, model_strs, clip_strs) if lora != "None"] + + optional_lora_stack = kwargs.get("optional_lora_stack") if "optional_lora_stack" in kwargs else [] + + xy_values = {"axis": axis, "values": values, "lora_stack": optional_lora_stack} + return (xy_values,) + +# 模型叠加 +class XYplot_ModelMergeBlocks: + + @classmethod + def INPUT_TYPES(s): + checkpoints = folder_paths.get_filename_list("checkpoints") + vae = ["Use Model 1", "Use Model 2"] + folder_paths.get_filename_list("vae") + + preset = ["Preset"] # 20 + preset += load_preset("mmb-preset.txt") + preset += load_preset("mmb-preset.custom.txt") + + default_vectors = "1,0,0; \n0,1,0; \n0,0,1; \n1,1,0; \n1,0,1; \n0,1,1; " + return { + "required": { + "ckpt_name_1": (checkpoints,), + "ckpt_name_2": (checkpoints,), + "vae_use": (vae, {"default": "Use Model 1"}), + "preset": (preset, {"default": "preset"}), + "values": ("STRING", {"default": default_vectors, "multiline": True, "placeholder": 'Support 2 methods:\n\n1.input, middle, out in same line and insert values seperated by "; "\n\n2.model merge block number seperated by ", " in same line and insert values seperated by "; "'}), + }, + "hidden": {"my_unique_id": "UNIQUE_ID"} + } + + RETURN_TYPES = ("X_Y",) + RETURN_NAMES = ("X or Y",) + FUNCTION = "xy_value" + + CATEGORY = "EasyUse/XY Inputs" + + def xy_value(self, ckpt_name_1, ckpt_name_2, vae_use, preset, values, my_unique_id=None): + + axis = "advanced: ModelMergeBlocks" + if ckpt_name_1 is None: + raise Exception("ckpt_name_1 is not found") + if ckpt_name_2 is None: + raise Exception("ckpt_name_2 is not found") + + models = (ckpt_name_1, ckpt_name_2) + + xy_values = {"axis":axis, "values":values, "models":models, "vae_use": vae_use} + return (xy_values,) + + +NODE_CLASS_MAPPINGS = { + "easy XYInputs: Seeds++ Batch": XYplot_SeedsBatch, + "easy XYInputs: Steps": XYplot_Steps, + "easy XYInputs: CFG Scale": XYplot_CFG, + "easy XYInputs: FluxGuidance": XYplot_FluxGuidance, + "easy XYInputs: Sampler/Scheduler": XYplot_Sampler_Scheduler, + "easy XYInputs: Denoise": XYplot_Denoise, + "easy XYInputs: Checkpoint": XYplot_Checkpoint, + "easy XYInputs: Lora": XYplot_Lora, + "easy XYInputs: ModelMergeBlocks": XYplot_ModelMergeBlocks, + "easy XYInputs: PromptSR": XYplot_PromptSR, + "easy XYInputs: ControlNet": XYplot_Control_Net, + "easy XYInputs: PositiveCond": XYplot_Positive_Cond, + "easy XYInputs: PositiveCondList": XYplot_Positive_Cond_List, + "easy XYInputs: NegativeCond": XYplot_Negative_Cond, + "easy XYInputs: NegativeCondList": XYplot_Negative_Cond_List, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "easy XYInputs: Seeds++ Batch": "XY Inputs: Seeds++ Batch //EasyUse", + "easy XYInputs: Steps": "XY Inputs: Steps //EasyUse", + "easy XYInputs: CFG Scale": "XY Inputs: CFG Scale //EasyUse", + "easy XYInputs: FluxGuidance": "XY Inputs: Flux Guidance //EasyUse", + "easy XYInputs: Sampler/Scheduler": "XY Inputs: Sampler/Scheduler //EasyUse", + "easy XYInputs: Denoise": "XY Inputs: Denoise //EasyUse", + "easy XYInputs: Checkpoint": "XY Inputs: Checkpoint //EasyUse", + "easy XYInputs: Lora": "XY Inputs: Lora //EasyUse", + "easy XYInputs: ModelMergeBlocks": "XY Inputs: ModelMergeBlocks //EasyUse", + "easy XYInputs: PromptSR": "XY Inputs: PromptSR //EasyUse", + "easy XYInputs: ControlNet": "XY Inputs: Controlnet //EasyUse", + "easy XYInputs: PositiveCond": "XY Inputs: PosCond //EasyUse", + "easy XYInputs: PositiveCondList": "XY Inputs: PosCondList //EasyUse", + "easy XYInputs: NegativeCond": "XY Inputs: NegCond //EasyUse", + "easy XYInputs: NegativeCondList": "XY Inputs: NegCondList //EasyUse", +} \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/routes.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/routes.py new file mode 100644 index 0000000000000000000000000000000000000000..9a4a92eb802a8391928071a1ada30f559fc954d1 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/routes.py @@ -0,0 +1,299 @@ +import os +import hashlib +import sys +import json +import shutil +import folder_paths +from aiohttp import web +from server import PromptServer +from .config import RESOURCES_DIR, FOOOCUS_STYLES_DIR, FOOOCUS_STYLES_SAMPLES +from .libs.model import easyModelManager +from .libs.utils import getMetadata, cleanGPUUsedForce, get_local_filepath +from .libs.cache import remove_cache +from .libs.translate import has_chinese, zh_to_en + +@PromptServer.instance.routes.get('/easyuse/version') +def get_version(request): + try: + from .. import __version__ + return web.json_response({"version": __version__}) + except Exception as e: + print(e) + return web.Response(status=500) + +@PromptServer.instance.routes.post("/easyuse/cleangpu") +def cleanGPU(request): + try: + cleanGPUUsedForce() + return web.Response(status=200) + except Exception as e: + return web.Response(status=500) + pass + +@PromptServer.instance.routes.post("/easyuse/removecache") +async def removecache(request): + post = await request.post() + key = post.get("key") + try: + remove_cache(key) + return web.Response(status=200) + except Exception as e: + return web.Response(status=500) + pass + +@PromptServer.instance.routes.post("/easyuse/translate") +async def translate(request): + post = await request.post() + text = post.get("text") + if has_chinese(text): + return web.json_response({"text": zh_to_en([text])[0]}) + else: + return web.json_response({"text": text}) + +@PromptServer.instance.routes.get("/easyuse/reboot") +def reboot(request): + try: + sys.stdout.close_log() + except Exception as e: + pass + + return os.execv(sys.executable, [sys.executable] + sys.argv) + +# parse csv +@PromptServer.instance.routes.post("/easyuse/upload/csv") +async def parse_csv(request): + post = await request.post() + csv = post.get("csv") + if csv and csv.file: + file = csv.file + text = '' + for line in file.readlines(): + line = str(line.strip()) + line = line.replace("'", "").replace("b",'') + text += line + '; \n' + return web.json_response(text) + +#get style list +@PromptServer.instance.routes.get("/easyuse/prompt/styles") +async def getStylesList(request): + if "name" in request.rel_url.query: + style_name = request.rel_url.query["name"] + fooocus_custom_dir = os.path.join(FOOOCUS_STYLES_DIR, 'fooocus_styles.json') + if style_name == 'fooocus_styles' and not os.path.exists(fooocus_custom_dir): + file = os.path.join(RESOURCES_DIR, style_name+'.json') + cn_file = os.path.join(RESOURCES_DIR, style_name + '_cn.json') + else: + file = os.path.join(FOOOCUS_STYLES_DIR, style_name+'.json') + cn_file = os.path.join(FOOOCUS_STYLES_DIR, style_name + '_cn.json') + cn_data = None + if os.path.isfile(cn_file): + f = open(cn_file, 'r', encoding='utf-8') + cn_data = json.load(f) + f.close() + if os.path.isfile(file): + f = open(file, 'r', encoding='utf-8') + data = json.load(f) + f.close() + if data: + ndata = [] + for d in data: + nd = {} + name = d['name'].replace('-', ' ') + words = name.split(' ') + key = ' '.join( + word.upper() if word.lower() in ['mre', 'sai', '3d'] else word.capitalize() for word in + words) + if "name_cn" in d: + nd['name_cn'] = d['name_cn'] + elif cn_data: + nd['name_cn'] = cn_data[key] if key in cn_data else key + nd["name"] = d['name'] + if "thumbnail" in d: + thumbnail = d['thumbnail'] + if isinstance(d['thumbnail'], str): + nd['thumbnail'] = thumbnail if "http" in thumbnail else f'/easyuse/prompt/styles/image?path={thumbnail}' + elif isinstance(d['thumbnail'], list): + nd['thumbnail'] = [thumb if "http" in thumb else f'/easyuse/prompt/styles/image?path={thumb}' for thumb in thumbnail] + else: + nd['thumbnail'] = f'/easyuse/prompt/styles/image?name={name}&styles_name={style_name}' + if "thumbnail_variant" in d: + nd['thumbnailVariant'] = d['thumbnail_variant'] + if "media_type" in d: + nd['mediaType'] = d['media_type'] + if "media_subtype" in d: + nd['mediaSubtype'] = d['media_subtype'] + if "prompt" in d: + nd['prompt'] = d['prompt'] + if "negative_prompt" in d: + nd['negative_prompt'] = d['negative_prompt'] + ndata.append(nd) + return web.json_response(ndata) + return web.Response(status=400) + +# get style preview image +@PromptServer.instance.routes.get("/easyuse/prompt/styles/image") +async def getStylesImage(request): + styles_name = request.rel_url.query["styles_name"] if "styles_name" in request.rel_url.query else None + if "path" in request.rel_url.query: + path = request.rel_url.query["path"] + file = os.path.join(FOOOCUS_STYLES_DIR, 'samples', path) + parent_file = os.path.join(FOOOCUS_STYLES_DIR, path) + if os.path.isfile(file): + return web.FileResponse(file) + elif os.path.isfile(parent_file): + return web.FileResponse(parent_file) + elif "name" in request.rel_url.query: + name = request.rel_url.query["name"] + if os.path.exists(os.path.join(FOOOCUS_STYLES_DIR, 'samples')): + file = os.path.join(FOOOCUS_STYLES_DIR, 'samples', name + '.jpg') + if os.path.isfile(file): + return web.FileResponse(file) + elif styles_name == 'fooocus_styles': + return web.Response(text=FOOOCUS_STYLES_SAMPLES + name + '.jpg') + elif styles_name == 'fooocus_styles': + return web.Response(text=FOOOCUS_STYLES_SAMPLES + name + '.jpg') + return web.Response(status=400) + +# get models lists +@PromptServer.instance.routes.get("/easyuse/models/list") +async def getModelsList(request): + if "type" in request.rel_url.query: + type = request.rel_url.query["type"] + if type not in ['checkpoints', 'loras']: + return web.Response(status=400) + manager = easyModelManager() + return web.json_response(manager.get_model_lists(type)) + else: + return web.Response(status=400) + +@PromptServer.instance.routes.post("/easyuse/metadata/notes/{name}") +async def save_notes(request): + name = request.match_info["name"] + pos = name.index("/") + type = name[0:pos] + name = name[pos+1:] + + file_path = None + if type == "embeddings" or type == "loras": + name = name.lower() + files = folder_paths.get_filename_list(type) + for f in files: + lower_f = f.lower() + if lower_f == name: + file_path = folder_paths.get_full_path(type, f) + else: + n = os.path.splitext(f)[0].lower() + if n == name: + file_path = folder_paths.get_full_path(type, f) + + if file_path is not None: + break + else: + file_path = folder_paths.get_full_path( + type, name) + if not file_path: + return web.Response(status=404) + + file_no_ext = os.path.splitext(file_path)[0] + info_file = file_no_ext + ".txt" + with open(info_file, "w") as f: + f.write(await request.text()) + + return web.Response(status=200) + +@PromptServer.instance.routes.get("/easyuse/metadata/{name}") +async def load_metadata(request): + name = request.match_info["name"] + pos = name.index("/") + type = name[0:pos] + name = name[pos+1:] + + file_path = None + if type == "embeddings": + name = name.lower() + files = folder_paths.get_filename_list(type) + for f in files: + lower_f = f.lower() + if lower_f == name: + file_path = folder_paths.get_full_path(type, f) + else: + n = os.path.splitext(f)[0].lower() + if n == name: + file_path = folder_paths.get_full_path(type, f) + + if file_path is not None: + break + else: + file_path = folder_paths.get_full_path(type, name) + if not file_path: + return web.Response(status=404) + + try: + header = getMetadata(file_path) + header_json = json.loads(header) + meta = header_json["__metadata__"] if "__metadata__" in header_json else None + except: + meta = None + + if meta is None: + meta = {} + + file_no_ext = os.path.splitext(file_path)[0] + + info_file = file_no_ext + ".txt" + if os.path.isfile(info_file): + with open(info_file, "r") as f: + meta["easyuse.notes"] = f.read() + + hash_file = file_no_ext + ".sha256" + if os.path.isfile(hash_file): + with open(hash_file, "rt") as f: + meta["easyuse.sha256"] = f.read() + else: + with open(file_path, "rb") as f: + meta["easyuse.sha256"] = hashlib.sha256(f.read()).hexdigest() + with open(hash_file, "wt") as f: + f.write(meta["easyuse.sha256"]) + + return web.json_response(meta) + +@PromptServer.instance.routes.post("/easyuse/save/{name}") +async def save_preview(request): + name = request.match_info["name"] + pos = name.index("/") + type = name[0:pos] + name = name[pos+1:] + + body = await request.json() + + dir = folder_paths.get_directory_by_type(body.get("type", "output")) + subfolder = body.get("subfolder", "") + full_output_folder = os.path.join(dir, os.path.normpath(subfolder)) + + if os.path.commonpath((dir, os.path.abspath(full_output_folder))) != dir: + return web.Response(status=400) + + filepath = os.path.join(full_output_folder, body.get("filename", "")) + image_path = folder_paths.get_full_path(type, name) + image_path = os.path.splitext( + image_path)[0] + os.path.splitext(filepath)[1] + + shutil.copyfile(filepath, image_path) + + return web.json_response({ + "image": type + "/" + os.path.basename(image_path) + }) + +@PromptServer.instance.routes.post("/easyuse/model/download") +async def download_model(request): + post = await request.post() + url = post.get("url") + local_dir = post.get("local_dir") + if local_dir not in ['checkpoints', 'loras', 'controlnet', 'onnx', 'instantid', 'ipadapter', 'dynamicrafter_models', 'mediapipe', 'rembg', 'layer_model']: + return web.Response(status=400) + local_path = os.path.join(folder_paths.models_dir, local_dir) + try: + get_local_filepath(url, local_path) + return web.Response(status=200) + except: + return web.Response(status=500) diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/server.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/server.py new file mode 100644 index 0000000000000000000000000000000000000000..b47f7061f43757d4cd2326942693fb68bb582f24 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/py/server.py @@ -0,0 +1,170 @@ +import random +import server +from enum import Enum + +# Dedicated RNG for seed generation. Many custom nodes call random.seed() while they run, +# which resets the global RNG and makes the generated seeds repeat. +_seed_rng = random.Random() + +class SGmode(Enum): + FIX = 1 + INCR = 2 + DECR = 3 + RAND = 4 + + +class SeedGenerator: + def __init__(self, base_value, action): + self.base_value = base_value + + if action == "fixed" or action == "increment" or action == "decrement" or action == "randomize": + self.action = SGmode.FIX + elif action == 'increment for each node': + self.action = SGmode.INCR + elif action == 'decrement for each node': + self.action = SGmode.DECR + elif action == 'randomize for each node': + self.action = SGmode.RAND + + def next(self): + seed = self.base_value + + if self.action == SGmode.INCR: + self.base_value += 1 + if self.base_value > 1125899906842624: + self.base_value = 0 + elif self.action == SGmode.DECR: + self.base_value -= 1 + if self.base_value < 0: + self.base_value = 1125899906842624 + elif self.action == SGmode.RAND: + self.base_value = _seed_rng.randint(0, 1125899906842624) + + return seed + + +def control_seed(v, action, seed_is_global): + action = v['inputs']['action'] if seed_is_global else action + value = v['inputs']['value'] if seed_is_global else v['inputs']['seed_num'] + + if action == 'increment' or action == 'increment for each node': + value = value + 1 + if value > 1125899906842624: + value = 0 + elif action == 'decrement' or action == 'decrement for each node': + value = value - 1 + if value < 0: + value = 1125899906842624 + elif action == 'randomize' or action == 'randomize for each node': + value = _seed_rng.randint(0, 1125899906842624) + if seed_is_global: + v['inputs']['value'] = value + + return value + + +def prompt_seed_update(json_data): + try: + seed_widget_map = json_data['extra_data']['extra_pnginfo']['workflow']['seed_widgets'] + except: + return None + + workflow = json_data['extra_data']['extra_pnginfo']['workflow'] + seed_widget_map = workflow['seed_widgets'] + value = None + mode = None + node = None + action = None + seed_is_global = False + + for k, v in json_data['prompt'].items(): + if 'class_type' not in v: + continue + + cls = v['class_type'] + + if cls == 'easy globalSeed': + mode = v['inputs']['mode'] + action = v['inputs']['action'] + value = v['inputs']['value'] + node = k, v + seed_is_global = True + + # control before generated + if mode is not None and mode and seed_is_global: + value = control_seed(node[1], action, seed_is_global) + + if seed_is_global: + if value is not None: + seed_generator = SeedGenerator(value, action) + + for k, v in json_data['prompt'].items(): + for k2, v2 in v['inputs'].items(): + if isinstance(v2, str) and '$GlobalSeed.value$' in v2: + v['inputs'][k2] = v2.replace('$GlobalSeed.value$', str(value)) + + if k not in seed_widget_map: + continue + + if 'seed_num' in v['inputs']: + if isinstance(v['inputs']['seed_num'], int): + v['inputs']['seed_num'] = seed_generator.next() + + if 'seed' in v['inputs']: + if isinstance(v['inputs']['seed'], int): + v['inputs']['seed'] = seed_generator.next() + + if 'noise_seed' in v['inputs']: + if isinstance(v['inputs']['noise_seed'], int): + v['inputs']['noise_seed'] = seed_generator.next() + + for k2, v2 in v['inputs'].items(): + if isinstance(v2, str) and '$GlobalSeed.value$' in v2: + v['inputs'][k2] = v2.replace('$GlobalSeed.value$', str(value)) + # control after generated + if mode is not None and not mode: + control_seed(node[1], action, seed_is_global) + + return value is not None + + +def workflow_seed_update(json_data): + nodes = json_data['extra_data']['extra_pnginfo']['workflow']['nodes'] + seed_widget_map = json_data['extra_data']['extra_pnginfo']['workflow']['seed_widgets'] + prompt = json_data['prompt'] + + updated_seed_map = {} + value = None + + for node in nodes: + node_id = str(node['id']) + if node_id in prompt: + if node['type'] == 'easy globalSeed': + value = prompt[node_id]['inputs']['value'] + length = len(node['widgets_values']) + node['widgets_values'][length-1] = node['widgets_values'][0] + node['widgets_values'][0] = value + elif node_id in seed_widget_map: + widget_idx = seed_widget_map[node_id] + + if 'seed_num' in prompt[node_id]['inputs']: + seed = prompt[node_id]['inputs']['seed_num'] + elif 'noise_seed' in prompt[node_id]['inputs']: + seed = prompt[node_id]['inputs']['noise_seed'] + else: + seed = prompt[node_id]['inputs']['seed'] + + node['widgets_values'][widget_idx] = seed + updated_seed_map[node_id] = seed + + server.PromptServer.instance.send_sync("easyuse-global-seed", {"id": node_id, "value": value, "seed_map": updated_seed_map}) + + +def onprompt(json_data): + is_changed = prompt_seed_update(json_data) + if is_changed: + workflow_seed_update(json_data) + + return json_data + +server.PromptServer.instance.add_on_prompt_handler(onprompt) diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/pyproject.toml b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/pyproject.toml new file mode 100644 index 0000000000000000000000000000000000000000..ae7142d584e660ddc2f0732c84b818622cfe48fb --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/pyproject.toml @@ -0,0 +1,15 @@ +[project] +name = "comfyui-easy-use" +description = "To enhance the usability of ComfyUI, optimizations and integrations have been implemented for several commonly used nodes." +version = "1.3.6" +license = { file = "LICENSE" } +dependencies = ["diffusers", "accelerate", "clip_interrogator", "sentencepiece", "lark", "onnxruntime", "spandrel", "opencv-python-headless", "matplotlib", "peft"] + +[project.urls] +Repository = "https://github.com/yolain/ComfyUI-Easy-Use" +# Used by Comfy Registry https://comfyregistry.org + +[tool.comfy] +PublisherId = "yolain" +DisplayName = "ComfyUI-Easy-Use" +Icon = "https://mintlify.s3.us-west-1.amazonaws.com/yolain/images/logo.svg" diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/repair_dependency_list.txt b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/repair_dependency_list.txt new file mode 100644 index 0000000000000000000000000000000000000000..83d820ea936c72f2f6d9ce2e67d5b76fd113251f --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/repair_dependency_list.txt @@ -0,0 +1,6 @@ +numpy>=1.19.0 +diffusers>=0.32.2 +huggingface_hub>=0.25.0 +transformers>=4.48.0 +peft>=0.14.0 +protobuf>=4.25.3 diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/requirements.txt b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..3f6703356fbc14604fc36bf74f511a2420ab96ff --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/requirements.txt @@ -0,0 +1,10 @@ +diffusers +accelerate +clip_interrogator>=0.6.0 +lark +onnxruntime +opencv-python-headless +sentencepiece +spandrel +matplotlib +peft diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/resources/OpenSans-Medium.ttf b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/resources/OpenSans-Medium.ttf new file mode 100644 index 0000000000000000000000000000000000000000..8faa49e71119eaf344af62587eefb1a65def7ded --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/resources/OpenSans-Medium.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5ed28e4bb6c0fa26885f2d3e9d27e18049defe9594cf54a62acfe730b5f67397 +size 129948 diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/resources/fooocus_styles.json b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/resources/fooocus_styles.json new file mode 100644 index 0000000000000000000000000000000000000000..b1db960a6293f6c8050e8cc23be98e4efea8e6c9 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/resources/fooocus_styles.json @@ -0,0 +1,1925 @@ +[ + { + "name": "Fooocus Enhance", + "negative_prompt": "(worst quality, low quality, normal quality, lowres, low details, oversaturated, undersaturated, overexposed, underexposed, grayscale, bw, bad photo, bad photography, bad art:1.4), (watermark, signature, text font, username, error, logo, words, letters, digits, autograph, trademark, name:1.2), (blur, blurry, grainy), morbid, ugly, asymmetrical, mutated malformed, mutilated, poorly lit, bad shadow, draft, cropped, out of frame, cut off, censored, jpeg artifacts, out of focus, glitch, duplicate, (airbrushed, cartoon, anime, semi-realistic, cgi, render, blender, digital art, manga, amateur:1.3), (3D ,3D Game, 3D Game Scene, 3D Character:1.1), (bad hands, bad anatomy, bad body, bad face, bad teeth, bad arms, bad legs, deformities:1.3)", + "name_cn": "Fooocus-优化增强", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/fooocus_enhance.jpg" + }, + { + "name": "Fooocus Sharp", + "prompt": "cinematic still {prompt} . emotional, harmonious, vignette, 4k epic detailed, shot on kodak, 35mm photo, sharp focus, high budget, cinemascope, moody, epic, gorgeous, film grain, grainy", + "negative_prompt": "anime, cartoon, graphic, (blur, blurry, bokeh), text, painting, crayon, graphite, abstract, glitch, deformed, mutated, ugly, disfigured", + "name_cn": "Fooocus-锐化", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/fooocus_sharp.jpg" + }, + { + "name": "Fooocus Masterpiece", + "prompt": "(masterpiece), (best quality), (ultra-detailed), {prompt}, illustration, disheveled hair, detailed eyes, perfect composition, moist skin, intricate details, earrings, by wlop", + "negative_prompt": "longbody, lowres, bad anatomy, bad hands, missing fingers, pubic hair,extra digit, fewer digits, cropped, worst quality, low quality", + "name_cn": "Fooocus-杰作", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/fooocus_masterpiece.jpg" + }, + { + "name": "Fooocus Photograph", + "prompt": "photograph {prompt}, 50mm . cinematic 4k epic detailed 4k epic detailed photograph shot on kodak detailed cinematic hbo dark moody, 35mm photo, grainy, vignette, vintage, Kodachrome, Lomography, stained, highly detailed, found footage", + "negative_prompt": "Brad Pitt, bokeh, depth of field, blurry, cropped, regular face, saturated, contrast, deformed iris, deformed pupils, semi-realistic, cgi, 3d, render, sketch, cartoon, drawing, anime, text, cropped, out of frame, worst quality, low quality, jpeg artifacts, ugly, duplicate, morbid, mutilated, extra fingers, mutated hands, poorly drawn hands, poorly drawn face, mutation, deformed, dehydrated, bad anatomy, bad proportions, extra limbs, cloned face, disfigured, gross proportions, malformed limbs, missing arms, missing legs, extra arms, extra legs, fused fingers, too many fingers, long neck", + "name_cn": "Fooocus-照片", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/fooocus_photograph.jpg" + }, + { + "name": "Fooocus Negative", + "negative_prompt": "deformed, bad anatomy, disfigured, poorly drawn face, mutated, extra limb, ugly, poorly drawn hands, missing limb, floating limbs, disconnected limbs, disconnected head, malformed hands, long neck, mutated hands and fingers, bad hands, missing fingers, cropped, worst quality, low quality, mutation, poorly drawn, huge calf, bad hands, fused hand, missing hand, disappearing arms, disappearing thigh, disappearing calf, disappearing legs, missing fingers, fused fingers, abnormal eye proportion, Abnormal hands, abnormal legs, abnormal feet, abnormal fingers, drawing, painting, crayon, sketch, graphite, impressionist, noisy, blurry, soft, deformed, ugly, anime, cartoon, graphic, text, painting, crayon, graphite, abstract, glitch", + "name_cn": "Fooocus-反向提示词", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/fooocus_negative.jpg" + }, + { + "name": "Fooocus Cinematic", + "prompt": "cinematic still {prompt} . emotional, harmonious, vignette, highly detailed, high budget, bokeh, cinemascope, moody, epic, gorgeous, film grain, grainy", + "negative_prompt": "anime, cartoon, graphic, text, painting, crayon, graphite, abstract, glitch, deformed, mutated, ugly, disfigured", + "name_cn": "Fooocus-电影感", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/fooocus_cinematic.jpg" + }, + { + "name": "sai-3d-model", + "prompt": "professional 3d model {prompt} . octane render, highly detailed, volumetric, dramatic lighting", + "negative_prompt": "ugly, deformed, noisy, low poly, blurry, painting", + "name_cn": "SAI-3D模型", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/sai_3d_model.jpg" + }, + { + "name": "sai-analog film", + "prompt": "analog film photo {prompt} . faded film, desaturated, 35mm photo, grainy, vignette, vintage, Kodachrome, Lomography, stained, highly detailed, found footage", + "negative_prompt": "painting, drawing, illustration, glitch, deformed, mutated, cross-eyed, ugly, disfigured", + "name_cn": "SAI-模拟电影", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/sai_analog_film.jpg" + }, + { + "name": "sai-anime", + "prompt": "anime artwork {prompt} . anime style, key visual, vibrant, studio anime, highly detailed", + "negative_prompt": "photo, deformed, black and white, realism, disfigured, low contrast", + "name_cn": "SAI-动漫", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/sai_anime.jpg" + }, + { + "name": "sai-cinematic", + "prompt": "cinematic film still {prompt} . shallow depth of field, vignette, highly detailed, high budget, bokeh, cinemascope, moody, epic, gorgeous, film grain, grainy", + "negative_prompt": "anime, cartoon, graphic, text, painting, crayon, graphite, abstract, glitch, deformed, mutated, ugly, disfigured", + "name_cn": "SAI-电影片段", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/sai_cinematic.jpg" + }, + { + "name": "sai-comic book", + "prompt": "comic {prompt} . graphic illustration, comic art, graphic novel art, vibrant, highly detailed", + "negative_prompt": "photograph, deformed, glitch, noisy, realistic, stock photo", + "name_cn": "SAI-漫画", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/sai_comic_book.jpg" + }, + { + "name": "sai-craft clay", + "prompt": "play-doh style {prompt} . sculpture, clay art, centered composition, Claymation", + "negative_prompt": "sloppy, messy, grainy, highly detailed, ultra textured, photo", + "name_cn": "SAI-工艺粘土", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/sai_craft_clay.jpg" + }, + { + "name": "sai-digital art", + "prompt": "concept art {prompt} . digital artwork, illustrative, painterly, matte painting, highly detailed", + "negative_prompt": "photo, photorealistic, realism, ugly", + "name_cn": "SAI-数字艺术", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/sai_digital_art.jpg" + }, + { + "name": "sai-enhance", + "prompt": "breathtaking {prompt} . award-winning, professional, highly detailed", + "negative_prompt": "ugly, deformed, noisy, blurry, distorted, grainy", + "name_cn": "SAI-增强", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/sai_enhance.jpg" + }, + { + "name": "sai-fantasy art", + "prompt": "ethereal fantasy concept art of {prompt} . magnificent, celestial, ethereal, painterly, epic, majestic, magical, fantasy art, cover art, dreamy", + "negative_prompt": "photographic, realistic, realism, 35mm film, dslr, cropped, frame, text, deformed, glitch, noise, noisy, off-center, deformed, cross-eyed, closed eyes, bad anatomy, ugly, disfigured, sloppy, duplicate, mutated, black and white", + "name_cn": "SAI-奇幻艺术", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/sai_fantasy_art.jpg" + }, + { + "name": "sai-isometric", + "prompt": "isometric style {prompt} . vibrant, beautiful, crisp, detailed, ultra detailed, intricate", + "negative_prompt": "deformed, mutated, ugly, disfigured, blur, blurry, noise, noisy, realistic, photographic", + "name_cn": "SAI-等距风格", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/sai_isometric.jpg" + }, + { + "name": "sai-line art", + "prompt": "line art drawing {prompt} . professional, sleek, modern, minimalist, graphic, line art, vector graphics", + "negative_prompt": "anime, photorealistic, 35mm film, deformed, glitch, blurry, noisy, off-center, deformed, cross-eyed, closed eyes, bad anatomy, ugly, disfigured, mutated, realism, realistic, impressionism, expressionism, oil, acrylic", + "name_cn": "SAI-线条艺术", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/sai_line_art.jpg" + }, + { + "name": "sai-lowpoly", + "prompt": "low-poly style {prompt} . low-poly game art, polygon mesh, jagged, blocky, wireframe edges, centered composition", + "negative_prompt": "noisy, sloppy, messy, grainy, highly detailed, ultra textured, photo", + "name_cn": "SAI-低多边形", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/sai_lowpoly.jpg" + }, + { + "name": "sai-neonpunk", + "prompt": "neonpunk style {prompt} . cyberpunk, vaporwave, neon, vibes, vibrant, stunningly beautiful, crisp, detailed, sleek, ultramodern, magenta highlights, dark purple shadows, high contrast, cinematic, ultra detailed, intricate, professional", + "negative_prompt": "painting, drawing, illustration, glitch, deformed, mutated, cross-eyed, ugly, disfigured", + "name_cn": "SAI-霓虹朋克", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/sai_neonpunk.jpg" + }, + { + "name": "sai-origami", + "prompt": "origami style {prompt} . paper art, pleated paper, folded, origami art, pleats, cut and fold, centered composition", + "negative_prompt": "noisy, sloppy, messy, grainy, highly detailed, ultra textured, photo", + "name_cn": "SAI-折纸", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/sai_origami.jpg" + }, + { + "name": "sai-photographic", + "prompt": "cinematic photo {prompt} . 35mm photograph, film, bokeh, professional, 4k, highly detailed", + "negative_prompt": "drawing, painting, crayon, sketch, graphite, impressionist, noisy, blurry, soft, deformed, ugly", + "name_cn": "SAI-摄影", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/sai_photographic.jpg" + }, + { + "name": "sai-pixel art", + "prompt": "pixel-art {prompt} . low-res, blocky, pixel art style, 8-bit graphics", + "negative_prompt": "sloppy, messy, blurry, noisy, highly detailed, ultra textured, photo, realistic", + "name_cn": "SAI-像素艺术", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/sai_pixel_art.jpg" + }, + { + "name": "sai-texture", + "prompt": "texture {prompt} top down close-up", + "negative_prompt": "ugly, deformed, noisy, blurry", + "name_cn": "SAI-纹理", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/sai_texture.jpg" + }, + { + "name": "mre-cinematic-dynamic", + "prompt": "epic cinematic shot of dynamic {prompt} in motion. main subject of high budget action movie. raw photo, motion blur. best quality, high resolution", + "negative_prompt": "static, still, motionless, sluggish. drawing, painting, illustration, rendered. low budget. low quality, low resolution", + "name_cn": "MRE-史诗电影", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mre_cinematic_dynamic.jpg" + }, + { + "name": "mre-spontaneous-picture", + "prompt": "spontaneous picture of {prompt}, taken by talented amateur. best quality, high resolution. magical moment, natural look. simple but good looking", + "negative_prompt": "overthinked. low quality, low resolution", + "name_cn": "MRE-自然的抓拍照片", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mre_spontaneous_picture.jpg" + }, + { + "name": "mre-artistic-vision", + "prompt": "powerful artistic vision of {prompt}. breathtaking masterpiece made by great artist. best quality, high resolution", + "negative_prompt": "insignificant, flawed, made by bad artist. low quality, low resolution", + "name_cn": "MRE-艺术视觉", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mre_artistic_vision.jpg" + }, + { + "name": "mre-dark-dream", + "prompt": "dark and unsettling dream showing {prompt}. best quality, high resolution. created by genius but depressed mad artist. grim beauty", + "negative_prompt": "naive, cheerful. comfortable, casual, boring, cliche. low quality, low resolution", + "name_cn": "MRE-黑暗梦境", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mre_dark_dream.jpg" + }, + { + "name": "mre-gloomy-art", + "prompt": "astonishing gloomy art made mainly of shadows and lighting, forming {prompt}. masterful usage of lighting, shadows and chiaroscuro. made by black-hearted artist, drawing from darkness. best quality, high resolution", + "negative_prompt": "low quality, low resolution", + "name_cn": "MRE-阴郁艺术", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mre_gloomy_art.jpg" + }, + { + "name": "mre-bad-dream", + "prompt": "picture from really bad dream about terrifying {prompt}, true horror. bone-chilling vision. mad world that shouldn't exist. best quality, high resolution", + "negative_prompt": "nice dream, pleasant experience. low quality, low resolution", + "name_cn": "MRE-噩梦", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mre_bad_dream.jpg" + }, + { + "name": "mre-underground", + "prompt": "uncanny caliginous vision of {prompt}, created by remarkable underground artist. best quality, high resolution. raw and brutal art, careless but impressive style. inspired by darkness and chaos", + "negative_prompt": "photography, mainstream, civilized. low quality, low resolution", + "name_cn": "MRE-阴森地下", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mre_underground.jpg" + }, + { + "name": "mre-surreal-painting", + "prompt": "surreal painting representing strange vision of {prompt}. harmonious madness, synergy with chance. unique artstyle, mindbending art, magical surrealism. best quality, high resolution", + "negative_prompt": "photography, illustration, drawing. realistic, possible. logical, sane. low quality, low resolution", + "name_cn": "MRE-超现实主义绘画", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mre_surreal_painting.jpg" + }, + { + "name": "mre-dynamic-illustration", + "prompt": "insanely dynamic illustration of {prompt}. best quality, high resolution. crazy artstyle, careless brushstrokes, emotional and fun", + "negative_prompt": "photography, realistic. static, still, slow, boring. low quality, low resolution", + "name_cn": "MRE-动态插画", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mre_dynamic_illustration.jpg" + }, + { + "name": "mre-undead-art", + "prompt": "long forgotten art created by undead artist illustrating {prompt}, tribute to the death and decay. miserable art of the damned. wretched and decaying world. best quality, high resolution", + "negative_prompt": "alive, playful, living. low quality, low resolution", + "name_cn": "MRE-遗忘艺术家作品", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mre_undead_art.jpg" + }, + { + "name": "mre-elemental-art", + "prompt": "art illustrating insane amounts of raging elemental energy turning into {prompt}, avatar of elements. magical surrealism, wizardry. best quality, high resolution", + "negative_prompt": "photography, realistic, real. low quality, low resolution", + "name_cn": "MRE-元素艺术", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mre_elemental_art.jpg" + }, + { + "name": "mre-space-art", + "prompt": "winner of inter-galactic art contest illustrating {prompt}, symbol of the interstellar singularity. best quality, high resolution. artstyle previously unseen in the whole galaxy", + "negative_prompt": "created by human race, low quality, low resolution", + "name_cn": "MRE-空间艺术", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mre_space_art.jpg" + }, + { + "name": "mre-ancient-illustration", + "prompt": "sublime ancient illustration of {prompt}, predating human civilization. crude and simple, but also surprisingly beautiful artwork, made by genius primeval artist. best quality, high resolution", + "negative_prompt": "low quality, low resolution", + "name_cn": "MRE-古代插图", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mre_ancient_illustration.jpg" + }, + { + "name": "mre-brave-art", + "prompt": "brave, shocking, and brutally true art showing {prompt}. inspired by courage and unlimited creativity. truth found in chaos. best quality, high resolution", + "negative_prompt": "low quality, low resolution", + "name_cn": "MRE-勇敢艺术", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mre_brave_art.jpg" + }, + { + "name": "mre-heroic-fantasy", + "prompt": "heroic fantasy painting of {prompt}, in the dangerous fantasy world. airbrush over oil on canvas. best quality, high resolution", + "negative_prompt": "low quality, low resolution", + "name_cn": "MRE-英雄幻想", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mre_heroic_fantasy.jpg" + }, + { + "name": "mre-dark-cyberpunk", + "prompt": "dark cyberpunk illustration of brutal {prompt} in a world without hope, ruled by ruthless criminal corporations. best quality, high resolution", + "negative_prompt": "low quality, low resolution", + "name_cn": "MRE-黑暗赛博朋克", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mre_dark_cyberpunk.jpg" + }, + { + "name": "mre-lyrical-geometry", + "prompt": "geometric and lyrical abstraction painting presenting {prompt}. oil on metal. best quality, high resolution", + "negative_prompt": "photography, realistic, drawing, rendered. low quality, low resolution", + "name_cn": "MRE-抒情几何抽象画", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mre_lyrical_geometry.jpg" + }, + { + "name": "mre-sumi-e-symbolic", + "prompt": "big long brushstrokes of deep black sumi-e turning into symbolic painting of {prompt}. master level raw art. best quality, high resolution", + "negative_prompt": "photography, rendered. low quality, low resolution", + "name_cn": "MRE-墨绘长笔画", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mre_sumi_e_symbolic.jpg" + }, + { + "name": "mre-sumi-e-detailed", + "prompt": "highly detailed black sumi-e painting of {prompt}. in-depth study of perfection, created by a master. best quality, high resolution", + "negative_prompt": "low quality, low resolution", + "name_cn": "MRE-精细墨绘画", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mre_sumi_e_detailed.jpg" + }, + { + "name": "mre-manga", + "prompt": "manga artwork presenting {prompt}. created by japanese manga artist. highly emotional. best quality, high resolution", + "negative_prompt": "low quality, low resolution", + "name_cn": "MRE-日本漫画", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mre_manga.jpg" + }, + { + "name": "mre-anime", + "prompt": "anime artwork illustrating {prompt}. created by japanese anime studio. highly emotional. best quality, high resolution", + "negative_prompt": "low quality, low resolution", + "name_cn": "MRE-日本动画片", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mre_anime.jpg" + }, + { + "name": "mre-comic", + "prompt": "breathtaking illustration from adult comic book presenting {prompt}. fabulous artwork. best quality, high resolution", + "negative_prompt": "deformed, ugly, low quality, low resolution", + "name_cn": "MRE-成人漫画书插画", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mre_comic.jpg" + }, + { + "name": "ads-advertising", + "prompt": "advertising poster style {prompt} . Professional, modern, product-focused, commercial, eye-catching, highly detailed", + "negative_prompt": "noisy, blurry, amateurish, sloppy, unattractive", + "name_cn": "广告-广告", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/ads_advertising.jpg" + }, + { + "name": "ads-automotive", + "prompt": "automotive advertisement style {prompt} . sleek, dynamic, professional, commercial, vehicle-focused, high-resolution, highly detailed", + "negative_prompt": "noisy, blurry, unattractive, sloppy, unprofessional", + "name_cn": "广告-汽车", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/ads_automotive.jpg" + }, + { + "name": "ads-corporate", + "prompt": "corporate branding style {prompt} . professional, clean, modern, sleek, minimalist, business-oriented, highly detailed", + "negative_prompt": "noisy, blurry, grungy, sloppy, cluttered, disorganized", + "name_cn": "广告-企业品牌", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/ads_corporate.jpg" + }, + { + "name": "ads-fashion editorial", + "prompt": "fashion editorial style {prompt} . high fashion, trendy, stylish, editorial, magazine style, professional, highly detailed", + "negative_prompt": "outdated, blurry, noisy, unattractive, sloppy", + "name_cn": "广告-时尚编辑", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/ads_fashion_editorial.jpg" + }, + { + "name": "ads-food photography", + "prompt": "food photography style {prompt} . appetizing, professional, culinary, high-resolution, commercial, highly detailed", + "negative_prompt": "unappetizing, sloppy, unprofessional, noisy, blurry", + "name_cn": "广告-食品摄影", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/ads_food_photography.jpg" + }, + { + "name": "ads-gourmet food photography", + "prompt": "gourmet food photo of {prompt} . soft natural lighting, macro details, vibrant colors, fresh ingredients, glistening textures, bokeh background, styled plating, wooden tabletop, garnished, tantalizing, editorial quality", + "negative_prompt": "cartoon, anime, sketch, grayscale, dull, overexposed, cluttered, messy plate, deformed", + "name_cn": "广告-顶级美食摄影", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/ads_gourmet_food_photography.jpg" + }, + { + "name": "ads-luxury", + "prompt": "luxury product style {prompt} . elegant, sophisticated, high-end, luxurious, professional, highly detailed", + "negative_prompt": "cheap, noisy, blurry, unattractive, amateurish", + "name_cn": "广告-奢侈品", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/ads_luxury.jpg" + }, + { + "name": "ads-real estate", + "prompt": "real estate photography style {prompt} . professional, inviting, well-lit, high-resolution, property-focused, commercial, highly detailed", + "negative_prompt": "dark, blurry, unappealing, noisy, unprofessional", + "name_cn": "广告-房地产", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/ads_real_estate.jpg" + }, + { + "name": "ads-retail", + "prompt": "retail packaging style {prompt} . vibrant, enticing, commercial, product-focused, eye-catching, professional, highly detailed", + "negative_prompt": "noisy, blurry, amateurish, sloppy, unattractive", + "name_cn": "广告-零售", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/ads_retail.jpg" + }, + { + "name": "artstyle-abstract", + "prompt": "abstract style {prompt} . non-representational, colors and shapes, expression of feelings, imaginative, highly detailed", + "negative_prompt": "realistic, photographic, figurative, concrete", + "name_cn": "艺术风格-抽象", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/artstyle_abstract.jpg" + }, + { + "name": "artstyle-abstract expressionism", + "prompt": "abstract expressionist painting {prompt} . energetic brushwork, bold colors, abstract forms, expressive, emotional", + "negative_prompt": "realistic, photorealistic, low contrast, plain, simple, monochrome", + "name_cn": "艺术风格-抽象表现主义", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/artstyle_abstract_expressionism.jpg" + }, + { + "name": "artstyle-art deco", + "prompt": "art deco style {prompt} . geometric shapes, bold colors, luxurious, elegant, decorative, symmetrical, ornate, detailed", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast, realism, photorealistic, modernist, minimalist", + "name_cn": "艺术风格-装饰艺术", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/artstyle_art_deco.jpg" + }, + { + "name": "artstyle-art nouveau", + "prompt": "art nouveau style {prompt} . elegant, decorative, curvilinear forms, nature-inspired, ornate, detailed", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast, realism, photorealistic, modernist, minimalist", + "name_cn": "艺术风格-新艺术", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/artstyle_art_nouveau.jpg" + }, + { + "name": "artstyle-constructivist", + "prompt": "constructivist style {prompt} . geometric shapes, bold colors, dynamic composition, propaganda art style", + "negative_prompt": "realistic, photorealistic, low contrast, plain, simple, abstract expressionism", + "name_cn": "艺术风格-构造主义", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/artstyle_constructivist.jpg" + }, + { + "name": "artstyle-cubist", + "prompt": "cubist artwork {prompt} . geometric shapes, abstract, innovative, revolutionary", + "negative_prompt": "anime, photorealistic, 35mm film, deformed, glitch, low contrast, noisy", + "name_cn": "艺术风格-立体主义", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/artstyle_cubist.jpg" + }, + { + "name": "artstyle-expressionist", + "prompt": "expressionist {prompt} . raw, emotional, dynamic, distortion for emotional effect, vibrant, use of unusual colors, detailed", + "negative_prompt": "realism, symmetry, quiet, calm, photo", + "name_cn": "艺术风格-表现主义", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/artstyle_expressionist.jpg" + }, + { + "name": "artstyle-graffiti", + "prompt": "graffiti style {prompt} . street art, vibrant, urban, detailed, tag, mural", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast, realism, photorealistic", + "name_cn": "艺术风格-涂鸦", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/artstyle_graffiti.jpg" + }, + { + "name": "artstyle-hyperrealism", + "prompt": "hyperrealistic art {prompt} . extremely high-resolution details, photographic, realism pushed to extreme, fine texture, incredibly lifelike", + "negative_prompt": "simplified, abstract, unrealistic, impressionistic, low resolution", + "name_cn": "艺术风格-超写实主义", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/artstyle_hyperrealism.jpg" + }, + { + "name": "artstyle-impressionist", + "prompt": "impressionist painting {prompt} . loose brushwork, vibrant color, light and shadow play, captures feeling over form", + "negative_prompt": "anime, photorealistic, 35mm film, deformed, glitch, low contrast, noisy", + "name_cn": "艺术风格-印象派", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/artstyle_impressionist.jpg" + }, + { + "name": "artstyle-pointillism", + "prompt": "pointillism style {prompt} . composed entirely of small, distinct dots of color, vibrant, highly detailed", + "negative_prompt": "line drawing, smooth shading, large color fields, simplistic", + "name_cn": "艺术风格-点彩派", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/artstyle_pointillism.jpg" + }, + { + "name": "artstyle-pop art", + "prompt": "pop Art style {prompt} . bright colors, bold outlines, popular culture themes, ironic or kitsch", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast, realism, photorealistic, minimalist", + "name_cn": "艺术风格-波普艺术", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/artstyle_pop_art.jpg" + }, + { + "name": "artstyle-psychedelic", + "prompt": "psychedelic style {prompt} . vibrant colors, swirling patterns, abstract forms, surreal, trippy", + "negative_prompt": "monochrome, black and white, low contrast, realistic, photorealistic, plain, simple", + "name_cn": "艺术风格-迷幻", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/artstyle_psychedelic.jpg" + }, + { + "name": "artstyle-renaissance", + "prompt": "renaissance style {prompt} . realistic, perspective, light and shadow, religious or mythological themes, highly detailed", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast, modernist, minimalist, abstract", + "name_cn": "艺术风格-文艺复兴", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/artstyle_renaissance.jpg" + }, + { + "name": "artstyle-steampunk", + "prompt": "steampunk style {prompt} . antique, mechanical, brass and copper tones, gears, intricate, detailed", + "negative_prompt": "deformed, glitch, noisy, low contrast, anime, photorealistic", + "name_cn": "艺术风格-蒸汽朋克", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/artstyle_steampunk.jpg" + }, + { + "name": "artstyle-surrealist", + "prompt": "surrealist art {prompt} . dreamlike, mysterious, provocative, symbolic, intricate, detailed", + "negative_prompt": "anime, photorealistic, realistic, deformed, glitch, noisy, low contrast", + "name_cn": "艺术风格-超现实主义", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/artstyle_surrealist.jpg" + }, + { + "name": "artstyle-typography", + "prompt": "typographic art {prompt} . stylized, intricate, detailed, artistic, text-based", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast, realism, photorealistic", + "name_cn": "艺术风格-字体设计", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/artstyle_typography.jpg" + }, + { + "name": "artstyle-watercolor", + "prompt": "watercolor painting {prompt} . vibrant, beautiful, painterly, detailed, textural, artistic", + "negative_prompt": "anime, photorealistic, 35mm film, deformed, glitch, low contrast, noisy", + "name_cn": "艺术风格-水彩", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/artstyle_watercolor.jpg" + }, + { + "name": "futuristic-biomechanical", + "prompt": "biomechanical style {prompt} . blend of organic and mechanical elements, futuristic, cybernetic, detailed, intricate", + "negative_prompt": "natural, rustic, primitive, organic, simplistic", + "name_cn": "未来主义-生物机械", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/futuristic_biomechanical.jpg" + }, + { + "name": "futuristic-biomechanical cyberpunk", + "prompt": "biomechanical cyberpunk {prompt} . cybernetics, human-machine fusion, dystopian, organic meets artificial, dark, intricate, highly detailed", + "negative_prompt": "natural, colorful, deformed, sketch, low contrast, watercolor", + "name_cn": "未来主义-生物机械-赛博朋克", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/futuristic_biomechanical_cyberpunk.jpg" + }, + { + "name": "futuristic-cybernetic", + "prompt": "cybernetic style {prompt} . futuristic, technological, cybernetic enhancements, robotics, artificial intelligence themes", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast, realism, photorealistic, historical, medieval", + "name_cn": "未来主义-人机融合", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/futuristic_cybernetic.jpg" + }, + { + "name": "futuristic-cybernetic robot", + "prompt": "cybernetic robot {prompt} . android, AI, machine, metal, wires, tech, futuristic, highly detailed", + "negative_prompt": "organic, natural, human, sketch, watercolor, low contrast", + "name_cn": "未来主义-人机融合-机器人", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/futuristic_cybernetic_robot.jpg" + }, + { + "name": "futuristic-cyberpunk cityscape", + "prompt": "cyberpunk cityscape {prompt} . neon lights, dark alleys, skyscrapers, futuristic, vibrant colors, high contrast, highly detailed", + "negative_prompt": "natural, rural, deformed, low contrast, black and white, sketch, watercolor", + "name_cn": "未来主义-赛博朋克城市", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/futuristic_cyberpunk_cityscape.jpg" + }, + { + "name": "futuristic-futuristic", + "prompt": "futuristic style {prompt} . sleek, modern, ultramodern, high tech, detailed", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast, realism, photorealistic, vintage, antique", + "name_cn": "未来主义-未来主义", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/futuristic_futuristic.jpg" + }, + { + "name": "futuristic-retro cyberpunk", + "prompt": "retro cyberpunk {prompt} . 80's inspired, synthwave, neon, vibrant, detailed, retro futurism", + "negative_prompt": "modern, desaturated, black and white, realism, low contrast", + "name_cn": "未来主义-复古赛博朋克", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/futuristic_retro_cyberpunk.jpg" + }, + { + "name": "futuristic-retro futurism", + "prompt": "retro-futuristic {prompt} . vintage sci-fi, 50s and 60s style, atomic age, vibrant, highly detailed", + "negative_prompt": "contemporary, realistic, rustic, primitive", + "name_cn": "未来主义-复古未来主义", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/futuristic_retro_futurism.jpg" + }, + { + "name": "futuristic-sci-fi", + "prompt": "sci-fi style {prompt} . futuristic, technological, alien worlds, space themes, advanced civilizations", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast, realism, photorealistic, historical, medieval", + "name_cn": "未来主义-科幻", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/futuristic_sci_fi.jpg" + }, + { + "name": "futuristic-vaporwave", + "prompt": "vaporwave style {prompt} . retro aesthetic, cyberpunk, vibrant, neon colors, vintage 80s and 90s style, highly detailed", + "negative_prompt": "monochrome, muted colors, realism, rustic, minimalist, dark", + "name_cn": "未来主义-蒸汽波", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/futuristic_vaporwave.jpg" + }, + { + "name": "game-bubble bobble", + "prompt": "Bubble Bobble style {prompt} . 8-bit, cute, pixelated, fantasy, vibrant, reminiscent of Bubble Bobble game", + "negative_prompt": "realistic, modern, photorealistic, violent, horror", + "name_cn": "游戏-泡泡龙", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/game_bubble_bobble.jpg" + }, + { + "name": "game-cyberpunk game", + "prompt": "cyberpunk game style {prompt} . neon, dystopian, futuristic, digital, vibrant, detailed, high contrast, reminiscent of cyberpunk genre video games", + "negative_prompt": "historical, natural, rustic, low detailed", + "name_cn": "游戏-赛博朋克游戏", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/game_cyberpunk_game.jpg" + }, + { + "name": "game-fighting game", + "prompt": "fighting game style {prompt} . dynamic, vibrant, action-packed, detailed character design, reminiscent of fighting video games", + "negative_prompt": "peaceful, calm, minimalist, photorealistic", + "name_cn": "游戏-格斗游戏", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/game_fighting_game.jpg" + }, + { + "name": "game-gta", + "prompt": "GTA-style artwork {prompt} . satirical, exaggerated, pop art style, vibrant colors, iconic characters, action-packed", + "negative_prompt": "realistic, black and white, low contrast, impressionist, cubist, noisy, blurry, deformed", + "name_cn": "游戏-侠盗猎车手", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/game_gta.jpg" + }, + { + "name": "game-mario", + "prompt": "Super Mario style {prompt} . vibrant, cute, cartoony, fantasy, playful, reminiscent of Super Mario series", + "negative_prompt": "realistic, modern, horror, dystopian, violent", + "name_cn": "游戏-马里奥", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/game_mario.jpg" + }, + { + "name": "game-minecraft", + "prompt": "Minecraft style {prompt} . blocky, pixelated, vibrant colors, recognizable characters and objects, game assets", + "negative_prompt": "smooth, realistic, detailed, photorealistic, noise, blurry, deformed", + "name_cn": "游戏-我的世界", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/game_minecraft.jpg" + }, + { + "name": "game-pokemon", + "prompt": "Pokémon style {prompt} . vibrant, cute, anime, fantasy, reminiscent of Pokémon series", + "negative_prompt": "realistic, modern, horror, dystopian, violent", + "name_cn": "游戏-宝可梦", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/game_pokemon.jpg" + }, + { + "name": "game-retro arcade", + "prompt": "retro arcade style {prompt} . 8-bit, pixelated, vibrant, classic video game, old school gaming, reminiscent of 80s and 90s arcade games", + "negative_prompt": "modern, ultra-high resolution, photorealistic, 3D", + "name_cn": "游戏-复古街机", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/game_retro_arcade.jpg" + }, + { + "name": "game-retro game", + "prompt": "retro game art {prompt} . 16-bit, vibrant colors, pixelated, nostalgic, charming, fun", + "negative_prompt": "realistic, photorealistic, 35mm film, deformed, glitch, low contrast, noisy", + "name_cn": "游戏-复古游戏", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/game_retro_game.jpg" + }, + { + "name": "game-rpg fantasy game", + "prompt": "role-playing game (RPG) style fantasy {prompt} . detailed, vibrant, immersive, reminiscent of high fantasy RPG games", + "negative_prompt": "sci-fi, modern, urban, futuristic, low detailed", + "name_cn": "游戏-角色扮演幻想游戏", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/game_rpg_fantasy_game.jpg" + }, + { + "name": "game-strategy game", + "prompt": "strategy game style {prompt} . overhead view, detailed map, units, reminiscent of real-time strategy video games", + "negative_prompt": "first-person view, modern, photorealistic", + "name_cn": "游戏-策略游戏", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/game_strategy_game.jpg" + }, + { + "name": "game-streetfighter", + "prompt": "Street Fighter style {prompt} . vibrant, dynamic, arcade, 2D fighting game, highly detailed, reminiscent of Street Fighter series", + "negative_prompt": "3D, realistic, modern, photorealistic, turn-based strategy", + "name_cn": "游戏-街头霸王", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/game_streetfighter.jpg" + }, + { + "name": "game-zelda", + "prompt": "Legend of Zelda style {prompt} . vibrant, fantasy, detailed, epic, heroic, reminiscent of The Legend of Zelda series", + "negative_prompt": "sci-fi, modern, realistic, horror", + "name_cn": "游戏-塞尔达传说", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/game_zelda.jpg" + }, + { + "name": "misc-architectural", + "prompt": "architectural style {prompt} . clean lines, geometric shapes, minimalist, modern, architectural drawing, highly detailed", + "negative_prompt": "curved lines, ornate, baroque, abstract, grunge", + "name_cn": "其他-建筑", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/misc_architectural.jpg" + }, + { + "name": "misc-disco", + "prompt": "disco-themed {prompt} . vibrant, groovy, retro 70s style, shiny disco balls, neon lights, dance floor, highly detailed", + "negative_prompt": "minimalist, rustic, monochrome, contemporary, simplistic", + "name_cn": "其他-迪斯科", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/misc_disco.jpg" + }, + { + "name": "misc-dreamscape", + "prompt": "dreamscape {prompt} . surreal, ethereal, dreamy, mysterious, fantasy, highly detailed", + "negative_prompt": "realistic, concrete, ordinary, mundane", + "name_cn": "其他-梦境", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/misc_dreamscape.jpg" + }, + { + "name": "misc-dystopian", + "prompt": "dystopian style {prompt} . bleak, post-apocalyptic, somber, dramatic, highly detailed", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast, cheerful, optimistic, vibrant, colorful", + "name_cn": "其他-反乌托邦", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/misc_dystopian.jpg" + }, + { + "name": "misc-fairy tale", + "prompt": "fairy tale {prompt} . magical, fantastical, enchanting, storybook style, highly detailed", + "negative_prompt": "realistic, modern, ordinary, mundane", + "name_cn": "其他-童话故事", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/misc_fairy_tale.jpg" + }, + { + "name": "misc-gothic", + "prompt": "gothic style {prompt} . dark, mysterious, haunting, dramatic, ornate, detailed", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast, realism, photorealistic, cheerful, optimistic", + "name_cn": "其他-哥特风", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/misc_gothic.jpg" + }, + { + "name": "misc-grunge", + "prompt": "grunge style {prompt} . textured, distressed, vintage, edgy, punk rock vibe, dirty, noisy", + "negative_prompt": "smooth, clean, minimalist, sleek, modern, photorealistic", + "name_cn": "其他-垮掉的", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/misc_grunge.jpg" + }, + { + "name": "misc-horror", + "prompt": "horror-themed {prompt} . eerie, unsettling, dark, spooky, suspenseful, grim, highly detailed", + "negative_prompt": "cheerful, bright, vibrant, light-hearted, cute", + "name_cn": "其他-恐怖", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/misc_horror.jpg" + }, + { + "name": "misc-kawaii", + "prompt": "kawaii style {prompt} . cute, adorable, brightly colored, cheerful, anime influence, highly detailed", + "negative_prompt": "dark, scary, realistic, monochrome, abstract", + "name_cn": "其他-可爱", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/misc_kawaii.jpg" + }, + { + "name": "misc-lovecraftian", + "prompt": "lovecraftian horror {prompt} . eldritch, cosmic horror, unknown, mysterious, surreal, highly detailed", + "negative_prompt": "light-hearted, mundane, familiar, simplistic, realistic", + "name_cn": "其他-洛夫克拉夫特", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/misc_lovecraftian.jpg" + }, + { + "name": "misc-macabre", + "prompt": "macabre style {prompt} . dark, gothic, grim, haunting, highly detailed", + "negative_prompt": "bright, cheerful, light-hearted, cartoonish, cute", + "name_cn": "其他-恐怖", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/misc_macabre.jpg" + }, + { + "name": "misc-manga", + "prompt": "manga style {prompt} . vibrant, high-energy, detailed, iconic, Japanese comic style", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast, realism, photorealistic, Western comic style", + "name_cn": "其他-漫画", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/misc_manga.jpg" + }, + { + "name": "misc-metropolis", + "prompt": "metropolis-themed {prompt} . urban, cityscape, skyscrapers, modern, futuristic, highly detailed", + "negative_prompt": "rural, natural, rustic, historical, simple", + "name_cn": "其他-大都市", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/misc_metropolis.jpg" + }, + { + "name": "misc-minimalist", + "prompt": "minimalist style {prompt} . simple, clean, uncluttered, modern, elegant", + "negative_prompt": "ornate, complicated, highly detailed, cluttered, disordered, messy, noisy", + "name_cn": "其他-极简主义", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/misc_minimalist.jpg" + }, + { + "name": "misc-monochrome", + "prompt": "monochrome {prompt} . black and white, contrast, tone, texture, detailed", + "negative_prompt": "colorful, vibrant, noisy, blurry, deformed", + "name_cn": "其他-单色", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/misc_monochrome.jpg" + }, + { + "name": "misc-nautical", + "prompt": "nautical-themed {prompt} . sea, ocean, ships, maritime, beach, marine life, highly detailed", + "negative_prompt": "landlocked, desert, mountains, urban, rustic", + "name_cn": "其他-航海", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/misc_nautical.jpg" + }, + { + "name": "misc-space", + "prompt": "space-themed {prompt} . cosmic, celestial, stars, galaxies, nebulas, planets, science fiction, highly detailed", + "negative_prompt": "earthly, mundane, ground-based, realism", + "name_cn": "其他-太空", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/misc_space.jpg" + }, + { + "name": "misc-stained glass", + "prompt": "stained glass style {prompt} . vibrant, beautiful, translucent, intricate, detailed", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast, realism, photorealistic", + "name_cn": "其他-彩色玻璃", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/misc_stained_glass.jpg" + }, + { + "name": "misc-techwear fashion", + "prompt": "techwear fashion {prompt} . futuristic, cyberpunk, urban, tactical, sleek, dark, highly detailed", + "negative_prompt": "vintage, rural, colorful, low contrast, realism, sketch, watercolor", + "name_cn": "其他-科技时尚", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/misc_techwear_fashion.jpg" + }, + { + "name": "misc-tribal", + "prompt": "tribal style {prompt} . indigenous, ethnic, traditional patterns, bold, natural colors, highly detailed", + "negative_prompt": "modern, futuristic, minimalist, pastel", + "name_cn": "其他-部落", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/misc_tribal.jpg" + }, + { + "name": "misc-zentangle", + "prompt": "zentangle {prompt} . intricate, abstract, monochrome, patterns, meditative, highly detailed", + "negative_prompt": "colorful, representative, simplistic, large fields of color", + "name_cn": "其他-禅绕画", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/misc_zentangle.jpg" + }, + { + "name": "papercraft-collage", + "prompt": "collage style {prompt} . mixed media, layered, textural, detailed, artistic", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast, realism, photorealistic", + "name_cn": "手工艺-拼贴", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/papercraft_collage.jpg" + }, + { + "name": "papercraft-flat papercut", + "prompt": "flat papercut style {prompt} . silhouette, clean cuts, paper, sharp edges, minimalist, color block", + "negative_prompt": "3D, high detail, noise, grainy, blurry, painting, drawing, photo, disfigured", + "name_cn": "手工艺-平面剪纸", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/papercraft_flat_papercut.jpg" + }, + { + "name": "papercraft-kirigami", + "prompt": "kirigami representation of {prompt} . 3D, paper folding, paper cutting, Japanese, intricate, symmetrical, precision, clean lines", + "negative_prompt": "painting, drawing, 2D, noisy, blurry, deformed", + "name_cn": "手工艺-切纸", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/papercraft_kirigami.jpg" + }, + { + "name": "papercraft-paper mache", + "prompt": "paper mache representation of {prompt} . 3D, sculptural, textured, handmade, vibrant, fun", + "negative_prompt": "2D, flat, photo, sketch, digital art, deformed, noisy, blurry", + "name_cn": "手工艺-纸浆塑造", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/papercraft_paper_mache.jpg" + }, + { + "name": "papercraft-paper quilling", + "prompt": "paper quilling art of {prompt} . intricate, delicate, curling, rolling, shaping, coiling, loops, 3D, dimensional, ornamental", + "negative_prompt": "photo, painting, drawing, 2D, flat, deformed, noisy, blurry", + "name_cn": "手工艺-纸艺卷轴", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/papercraft_paper_quilling.jpg" + }, + { + "name": "papercraft-papercut collage", + "prompt": "papercut collage of {prompt} . mixed media, textured paper, overlapping, asymmetrical, abstract, vibrant", + "negative_prompt": "photo, 3D, realistic, drawing, painting, high detail, disfigured", + "name_cn": "手工艺-剪纸拼贴", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/papercraft_papercut_collage.jpg" + }, + { + "name": "papercraft-papercut shadow box", + "prompt": "3D papercut shadow box of {prompt} . layered, dimensional, depth, silhouette, shadow, papercut, handmade, high contrast", + "negative_prompt": "painting, drawing, photo, 2D, flat, high detail, blurry, noisy, disfigured", + "name_cn": "手工艺-剪纸影箱", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/papercraft_papercut_shadow_box.jpg" + }, + { + "name": "papercraft-stacked papercut", + "prompt": "stacked papercut art of {prompt} . 3D, layered, dimensional, depth, precision cut, stacked layers, papercut, high contrast", + "negative_prompt": "2D, flat, noisy, blurry, painting, drawing, photo, deformed", + "name_cn": "手工艺-层叠剪纸", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/papercraft_stacked_papercut.jpg" + }, + { + "name": "papercraft-thick layered papercut", + "prompt": "thick layered papercut art of {prompt} . deep 3D, volumetric, dimensional, depth, thick paper, high stack, heavy texture, tangible layers", + "negative_prompt": "2D, flat, thin paper, low stack, smooth texture, painting, drawing, photo, deformed", + "name_cn": "手工艺-厚层剪纸", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/papercraft_thick_layered_papercut.jpg" + }, + { + "name": "photo-alien", + "prompt": "alien-themed {prompt} . extraterrestrial, cosmic, otherworldly, mysterious, sci-fi, highly detailed", + "negative_prompt": "earthly, mundane, common, realistic, simple", + "name_cn": "摄影-外星人", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/photo_alien.jpg" + }, + { + "name": "photo-film noir", + "prompt": "film noir style {prompt} . monochrome, high contrast, dramatic shadows, 1940s style, mysterious, cinematic", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast, realism, photorealistic, vibrant, colorful", + "name_cn": "摄影-黑色电影", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/photo_film_noir.jpg" + }, + { + "name": "photo-glamour", + "prompt": "glamorous photo {prompt} . high fashion, luxurious, extravagant, stylish, sensual, opulent, elegance, stunning beauty, professional, high contrast, detailed", + "negative_prompt": "ugly, deformed, noisy, blurry, distorted, grainy, sketch, low contrast, dull, plain, modest", + "name_cn": "摄影-魅力", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/photo_glamour.jpg" + }, + { + "name": "photo-hdr", + "prompt": "HDR photo of {prompt} . High dynamic range, vivid, rich details, clear shadows and highlights, realistic, intense, enhanced contrast, highly detailed", + "negative_prompt": "flat, low contrast, oversaturated, underexposed, overexposed, blurred, noisy", + "name_cn": "摄影-高动态范围", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/photo_hdr.jpg" + }, + { + "name": "photo-iphone photographic", + "prompt": "iphone photo {prompt} . large depth of field, deep depth of field, highly detailed", + "negative_prompt": "drawing, painting, crayon, sketch, graphite, impressionist, noisy, blurry, soft, deformed, ugly, shallow depth of field, bokeh", + "name_cn": "摄影-苹果手机摄影", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/photo_iphone_photographic.jpg" + }, + { + "name": "photo-long exposure", + "prompt": "long exposure photo of {prompt} . Blurred motion, streaks of light, surreal, dreamy, ghosting effect, highly detailed", + "negative_prompt": "static, noisy, deformed, shaky, abrupt, flat, low contrast", + "name_cn": "摄影-长曝光", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/photo_long_exposure.jpg" + }, + { + "name": "photo-neon noir", + "prompt": "neon noir {prompt} . cyberpunk, dark, rainy streets, neon signs, high contrast, low light, vibrant, highly detailed", + "negative_prompt": "bright, sunny, daytime, low contrast, black and white, sketch, watercolor", + "name_cn": "摄影-霓虹黑色", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/photo_neon_noir.jpg" + }, + { + "name": "photo-silhouette", + "prompt": "silhouette style {prompt} . high contrast, minimalistic, black and white, stark, dramatic", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast, color, realism, photorealistic", + "name_cn": "摄影-轮廓", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/photo_silhouette.jpg" + }, + { + "name": "photo-tilt-shift", + "prompt": "tilt-shift photo of {prompt} . selective focus, miniature effect, blurred background, highly detailed, vibrant, perspective control", + "negative_prompt": "blurry, noisy, deformed, flat, low contrast, unrealistic, oversaturated, underexposed", + "name_cn": "摄影-移轴", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/photo_tilt_shift.jpg" + }, + { + "name": "cinematic-diva", + "prompt": "UHD, 8K, ultra detailed, a cinematic photograph of {prompt}, beautiful lighting, great composition", + "negative_prompt": "ugly, deformed, noisy, blurry, NSFW", + "name_cn": "电影女主角", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/cinematic_diva.jpg" + }, + { + "name": "Abstract Expressionism", + "prompt": "Abstract Expressionism Art, {prompt}, High contrast, minimalistic, colorful, stark, dramatic, expressionism", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast, realism, photorealistic", + "name_cn": "抽象表现主义", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/abstract_expressionism.jpg" + }, + { + "name": "Academia", + "prompt": "Academia, {prompt}, preppy Ivy League style, stark, dramatic, chic boarding school, academia", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast, grunge, sloppy, unkempt", + "name_cn": "学术", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/academia.jpg" + }, + { + "name": "Action Figure", + "prompt": "Action Figure, {prompt}, plastic collectable action figure, collectable toy action figure", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast", + "name_cn": "动作人偶", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/action_figure.jpg" + }, + { + "name": "Adorable 3D Character", + "prompt": "Adorable 3D Character, {prompt}, 3D render, adorable character, 3D art", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast, grunge, sloppy, unkempt, photograph, photo, realistic", + "name_cn": "可爱的3D角色", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/adorable_3d_character.jpg" + }, + { + "name": "Adorable Kawaii", + "prompt": "Adorable Kawaii, {prompt}, pretty, cute, adorable, kawaii", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast, gothic, dark, moody, monochromatic", + "name_cn": "可爱的卡哇伊", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/adorable_kawaii.jpg" + }, + { + "name": "Art Deco", + "prompt": "Art Deco, {prompt}, sleek, geometric forms, art deco style", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast", + "name_cn": "装饰艺术", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/art_deco.jpg" + }, + { + "name": "Art Nouveau", + "prompt": "Art Nouveau, beautiful art, {prompt}, sleek, organic forms, long, sinuous, art nouveau style", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast, industrial, mechanical", + "name_cn": "新艺术,美丽艺术", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/art_nouveau.jpg" + }, + { + "name": "Astral Aura", + "prompt": "Astral Aura, {prompt}, astral, colorful aura, vibrant energy", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast", + "name_cn": "星体光环", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/astral_aura.jpg" + }, + { + "name": "Avant-garde", + "prompt": "Avant-garde, {prompt}, unusual, experimental, avant-garde art", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast", + "name_cn": "前卫", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/avant_garde.jpg" + }, + { + "name": "Baroque", + "prompt": "Baroque, {prompt}, dramatic, exuberant, grandeur, baroque art", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast", + "name_cn": "巴洛克", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/baroque.jpg" + }, + { + "name": "Bauhaus-Style Poster", + "prompt": "Bauhaus-Style Poster, {prompt}, simple geometric shapes, clean lines, primary colors, Bauhaus-Style Poster", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast", + "name_cn": "包豪斯风格海报", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/bauhaus_style_poster.jpg" + }, + { + "name": "Blueprint Schematic Drawing", + "prompt": "Blueprint Schematic Drawing, {prompt}, technical drawing, blueprint, schematic", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast", + "name_cn": "蓝图示意图", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/blueprint_schematic_drawing.jpg" + }, + { + "name": "Caricature", + "prompt": "Caricature, {prompt}, exaggerated, comical, caricature", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast, realistic", + "name_cn": "漫画", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/caricature.jpg" + }, + { + "name": "Cel Shaded Art", + "prompt": "Cel Shaded Art, {prompt}, 2D, flat color, toon shading, cel shaded style", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast", + "name_cn": "卡通渲染", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/cel_shaded_art.jpg" + }, + { + "name": "Character Design Sheet", + "prompt": "Character Design Sheet, {prompt}, character reference sheet, character turn around", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast", + "name_cn": "角色设计表", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/character_design_sheet.jpg" + }, + { + "name": "Classicism Art", + "prompt": "Classicism Art, {prompt}, inspired by Roman and Greek culture, clarity, harmonious, classicism art", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast", + "name_cn": "古典主义艺术", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/classicism_art.jpg" + }, + { + "name": "Color Field Painting", + "prompt": "Color Field Painting, {prompt}, abstract, simple, geometic, color field painting style", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast", + "name_cn": "色彩领域绘画", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/color_field_painting.jpg" + }, + { + "name": "Colored Pencil Art", + "prompt": "Colored Pencil Art, {prompt}, colored pencil strokes, light color, visible paper texture, colored pencil art", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast", + "name_cn": "彩色铅笔艺术", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/colored_pencil_art.jpg" + }, + { + "name": "Conceptual Art", + "prompt": "Conceptual Art, {prompt}, concept art", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast", + "name_cn": "概念艺术", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/conceptual_art.jpg" + }, + { + "name": "Constructivism", + "prompt": "Constructivism Art, {prompt}, minimalistic, geometric forms, constructivism art", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast", + "name_cn": "建构主义", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/constructivism.jpg" + }, + { + "name": "Cubism", + "prompt": "Cubism Art, {prompt}, flat geometric forms, cubism art", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast", + "name_cn": "立体主义", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/cubism.jpg" + }, + { + "name": "Dadaism", + "prompt": "Dadaism Art, {prompt}, satirical, nonsensical, dadaism art", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast", + "name_cn": "达达主义", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/dadaism.jpg" + }, + { + "name": "Dark Fantasy", + "prompt": "Dark Fantasy Art, {prompt}, dark, moody, dark fantasy style", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast, bright, sunny", + "name_cn": "黑暗奇幻", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/dark_fantasy.jpg" + }, + { + "name": "Dark Moody Atmosphere", + "prompt": "Dark Moody Atmosphere, {prompt}, dramatic, mysterious, dark moody atmosphere", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast, vibrant, colorful, bright", + "name_cn": "黑暗忧郁气氛", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/dark_moody_atmosphere.jpg" + }, + { + "name": "DMT Art Style", + "prompt": "DMT Art Style, {prompt}, bright colors, surreal visuals, swirling patterns, DMT art style", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast", + "name_cn": "迷幻艺术风格", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/dmt_art_style.jpg" + }, + { + "name": "Doodle Art", + "prompt": "Doodle Art Style, {prompt}, drawing, freeform, swirling patterns, doodle art style", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast", + "name_cn": "涂鸦艺术", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/doodle_art.jpg" + }, + { + "name": "Double Exposure", + "prompt": "Double Exposure Style, {prompt}, double image ghost effect, image combination, double exposure style", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast", + "name_cn": "双重曝光", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/double_exposure.jpg" + }, + { + "name": "Dripping Paint Splatter Art", + "prompt": "Dripping Paint Splatter Art, {prompt}, dramatic, paint drips, splatters, dripping paint", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast", + "name_cn": "滴漆飞溅艺术", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/dripping_paint_splatter_art.jpg" + }, + { + "name": "Expressionism", + "prompt": "Expressionism Art Style, {prompt}, movement, contrast, emotional, exaggerated forms, expressionism art style", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast", + "name_cn": "表现主义", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/expressionism.jpg" + }, + { + "name": "Faded Polaroid Photo", + "prompt": "Faded Polaroid Photo, {prompt}, analog, old faded photo, old polaroid", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast, vibrant, colorful", + "name_cn": "褪色的宝丽来照片", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/faded_polaroid_photo.jpg" + }, + { + "name": "Fauvism", + "prompt": "Fauvism Art, {prompt}, painterly, bold colors, textured brushwork, fauvism art", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast", + "name_cn": "野兽派", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/fauvism.jpg" + }, + { + "name": "Flat 2D Art", + "prompt": "Flat 2D Art, {prompt}, simple flat color, 2-dimensional, Flat 2D Art Style", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast, 3D, photo, realistic", + "name_cn": "平面 2D 艺术", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/flat_2d_art.jpg" + }, + { + "name": "Fortnite Art Style", + "prompt": "Fortnite Art Style, {prompt}, 3D cartoon, colorful, Fortnite Art Style", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast, photo, realistic", + "name_cn": "堡垒之夜艺术风格", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/fortnite_art_style.jpg" + }, + { + "name": "Futurism", + "prompt": "Futurism Art Style, {prompt}, dynamic, dramatic, Futurism Art Style", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast", + "name_cn": "未来派", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/futurism.jpg" + }, + { + "name": "Glitchcore", + "prompt": "Glitchcore Art Style, {prompt}, dynamic, dramatic, distorted, vibrant colors, glitchcore art style", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast", + "name_cn": "故障核心", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/glitchcore.jpg" + }, + { + "name": "Glo-fi", + "prompt": "Glo-fi Art Style, {prompt}, dynamic, dramatic, vibrant colors, glo-fi art style", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast", + "name_cn": "光明高保真", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/glo_fi.jpg" + }, + { + "name": "Googie Art Style", + "prompt": "Googie Art Style, {prompt}, dynamic, dramatic, 1950's futurism, bold boomerang angles, Googie art style", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast", + "name_cn": "古吉艺术风格", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/googie_art_style.jpg" + }, + { + "name": "Graffiti Art", + "prompt": "Graffiti Art Style, {prompt}, dynamic, dramatic, vibrant colors, graffiti art style", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast", + "name_cn": "涂鸦艺术", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/graffiti_art.jpg" + }, + { + "name": "Harlem Renaissance Art", + "prompt": "Harlem Renaissance Art Style, {prompt}, dynamic, dramatic, 1920s African American culture, Harlem Renaissance art style", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast", + "name_cn": "哈莱姆文艺复兴艺术", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/harlem_renaissance_art.jpg" + }, + { + "name": "High Fashion", + "prompt": "High Fashion, {prompt}, dynamic, dramatic, haute couture, elegant, ornate clothing, High Fashion", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast", + "name_cn": "高级时装", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/high_fashion.jpg" + }, + { + "name": "Idyllic", + "prompt": "Idyllic, {prompt}, peaceful, happy, pleasant, happy, harmonious, picturesque, charming", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast", + "name_cn": "田园诗般", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/idyllic.jpg" + }, + { + "name": "Impressionism", + "prompt": "Impressionism, {prompt}, painterly, small brushstrokes, visible brushstrokes, impressionistic style", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast", + "name_cn": "印象派", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/impressionism.jpg" + }, + { + "name": "Infographic Drawing", + "prompt": "Infographic Drawing, {prompt}, diagram, infographic", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast", + "name_cn": "信息图表绘图", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/infographic_drawing.jpg" + }, + { + "name": "Ink Dripping Drawing", + "prompt": "Ink Dripping Drawing, {prompt}, ink drawing, dripping ink", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast, colorful, vibrant", + "name_cn": "滴墨绘画", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/ink_dripping_drawing.jpg" + }, + { + "name": "Japanese Ink Drawing", + "prompt": "Japanese Ink Drawing, {prompt}, ink drawing, inkwash, Japanese Ink Drawing", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast, colorful, vibrant", + "name_cn": "日式水墨画", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/japanese_ink_drawing.jpg" + }, + { + "name": "Knolling Photography", + "prompt": "Knolling Photography, {prompt}, flat lay photography, object arrangment, knolling photography", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast", + "name_cn": "规律摆放摄影", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/knolling_photography.jpg" + }, + { + "name": "Light Cheery Atmosphere", + "prompt": "Light Cheery Atmosphere, {prompt}, happy, joyful, cheerful, carefree, gleeful, lighthearted, pleasant atmosphere", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast, monochromatic, dark, moody", + "name_cn": "轻松愉快的气氛", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/light_cheery_atmosphere.jpg" + }, + { + "name": "Logo Design", + "prompt": "Logo Design, {prompt}, dynamic graphic art, vector art, minimalist, professional logo design", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast", + "name_cn": "标志设计", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/logo_design.jpg" + }, + { + "name": "Luxurious Elegance", + "prompt": "Luxurious Elegance, {prompt}, extravagant, ornate, designer, opulent, picturesque, lavish", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast", + "name_cn": "奢华优雅", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/luxurious_elegance.jpg" + }, + { + "name": "Macro Photography", + "prompt": "Macro Photography, {prompt}, close-up, macro 100mm, macro photography", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast", + "name_cn": "微距摄影", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/macro_photography.jpg" + }, + { + "name": "Mandola Art", + "prompt": "Mandola art style, {prompt}, complex, circular design, mandola", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast", + "name_cn": "曼陀罗艺术", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mandola_art.jpg" + }, + { + "name": "Marker Drawing", + "prompt": "Marker Drawing, {prompt}, bold marker lines, visibile paper texture, marker drawing", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast, photograph, realistic", + "name_cn": "马克笔绘图", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/marker_drawing.jpg" + }, + { + "name": "Medievalism", + "prompt": "Medievalism, {prompt}, inspired by The Middle Ages, medieval art, elaborate patterns and decoration, Medievalism", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast", + "name_cn": "中世纪主义", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/medievalism.jpg" + }, + { + "name": "Minimalism", + "prompt": "Minimalism, {prompt}, abstract, simple geometic shapes, hard edges, sleek contours, Minimalism", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast", + "name_cn": "极简主义", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/minimalism.jpg" + }, + { + "name": "Neo-Baroque", + "prompt": "Neo-Baroque, {prompt}, ornate and elaborate, dynaimc, Neo-Baroque", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast", + "name_cn": "新巴洛克", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/neo_baroque.jpg" + }, + { + "name": "Neo-Byzantine", + "prompt": "Neo-Byzantine, {prompt}, grand decorative religious style, Orthodox Christian inspired, Neo-Byzantine", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast", + "name_cn": "新拜占庭", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/neo_byzantine.jpg" + }, + { + "name": "Neo-Futurism", + "prompt": "Neo-Futurism, {prompt}, high-tech, curves, spirals, flowing lines, idealistic future, Neo-Futurism", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast", + "name_cn": "新未来派", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/neo_futurism.jpg" + }, + { + "name": "Neo-Impressionism", + "prompt": "Neo-Impressionism, {prompt}, tiny dabs of color, Pointillism, painterly, Neo-Impressionism", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast, photograph, realistic", + "name_cn": "新印象派", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/neo_impressionism.jpg" + }, + { + "name": "Neo-Rococo", + "prompt": "Neo-Rococo, {prompt}, curved forms, naturalistic ornamentation, elaborate, decorative, gaudy, Neo-Rococo", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast", + "name_cn": "新洛可可", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/neo_rococo.jpg" + }, + { + "name": "Neoclassicism", + "prompt": "Neoclassicism, {prompt}, ancient Rome and Greece inspired, idealic, sober colors, Neoclassicism", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast", + "name_cn": "新古典主义", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/neoclassicism.jpg" + }, + { + "name": "Op Art", + "prompt": "Op Art, {prompt}, optical illusion, abstract, geometric pattern, impression of movement, Op Art", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast", + "name_cn": "欧普艺术", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/op_art.jpg" + }, + { + "name": "Ornate and Intricate", + "prompt": "Ornate and Intricate, {prompt}, decorative, highly detailed, elaborate, ornate, intricate", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast", + "name_cn": "华丽而复杂", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/ornate_and_intricate.jpg" + }, + { + "name": "Pencil Sketch Drawing", + "prompt": "Pencil Sketch Drawing, {prompt}, black and white drawing, graphite drawing", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast", + "name_cn": "铅笔素描", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/pencil_sketch_drawing.jpg" + }, + { + "name": "Pop Art 2", + "prompt": "Pop Art, {prompt}, vivid colors, flat color, 2D, strong lines, Pop Art", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast, photo, realistic", + "name_cn": "流行艺术2", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/pop_art_2.jpg" + }, + { + "name": "Rococo", + "prompt": "Rococo, {prompt}, flamboyant, pastel colors, curved lines, elaborate detail, Rococo", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast", + "name_cn": "洛可可", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/rococo.jpg" + }, + { + "name": "Silhouette Art", + "prompt": "Silhouette Art, {prompt}, high contrast, well defined, Silhouette Art", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast", + "name_cn": "剪影艺术", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/silhouette_art.jpg" + }, + { + "name": "Simple Vector Art", + "prompt": "Simple Vector Art, {prompt}, 2D flat, simple shapes, minimalistic, professional graphic, flat color, high contrast, Simple Vector Art", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast, 3D, photo, realistic", + "name_cn": "简单矢量艺术", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/simple_vector_art.jpg" + }, + { + "name": "Sketchup", + "prompt": "Sketchup, {prompt}, CAD, professional design, Sketchup", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast, photo, photograph", + "name_cn": "草图", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/sketchup.jpg" + }, + { + "name": "Steampunk 2", + "prompt": "Steampunk, {prompt}, retrofuturistic science fantasy, steam-powered tech, vintage industry, gears, neo-victorian, steampunk", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast", + "name_cn": "赛博朋克2", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/steampunk_2.jpg" + }, + { + "name": "Surrealism", + "prompt": "Surrealism, {prompt}, expressive, dramatic, organic lines and forms, dreamlike and mysterious, Surrealism", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast, realistic", + "name_cn": "超现实主义", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/surrealism.jpg" + }, + { + "name": "Suprematism", + "prompt": "Suprematism, {prompt}, abstract, limited color palette, geometric forms, Suprematism", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast, realistic", + "name_cn": "至上主义", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/suprematism.jpg" + }, + { + "name": "Terragen", + "prompt": "Terragen, {prompt}, beautiful massive landscape, epic scenery, Terragen", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast", + "name_cn": "地表风景", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/terragen.jpg" + }, + { + "name": "Tranquil Relaxing Atmosphere", + "prompt": "Tranquil Relaxing Atmosphere, {prompt}, calming style, soothing colors, peaceful, idealic, Tranquil Relaxing Atmosphere", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast, oversaturated", + "name_cn": "宁静轻松的氛围", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/tranquil_relaxing_atmosphere.jpg" + }, + { + "name": "Sticker Designs", + "prompt": "Vector Art Stickers, {prompt}, professional vector design, sticker designs, Sticker Sheet", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast", + "name_cn": "贴纸设计", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/sticker_designs.jpg" + }, + { + "name": "Vibrant Rim Light", + "prompt": "Vibrant Rim Light, {prompt}, bright rim light, high contrast, bold edge light", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast", + "name_cn": "生动的边缘光", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/vibrant_rim_light.jpg" + }, + { + "name": "Volumetric Lighting", + "prompt": "Volumetric Lighting, {prompt}, light depth, dramatic atmospheric lighting, Volumetric Lighting", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast", + "name_cn": "体积照明", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/volumetric_lighting.jpg" + }, + { + "name": "Watercolor 2", + "prompt": "Watercolor style painting, {prompt}, visible paper texture, colorwash, watercolor", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast, photo, realistic", + "name_cn": "水彩2", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/watercolor_2.jpg" + }, + { + "name": "Whimsical and Playful", + "prompt": "Whimsical and Playful, {prompt}, imaginative, fantastical, bight colors, stylized, happy, Whimsical and Playful", + "negative_prompt": "ugly, deformed, noisy, blurry, low contrast, drab, boring, moody", + "name_cn": "异想天开、俏皮", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/whimsical_and_playful.jpg" + }, + { + "name": "MK Chromolithography", + "prompt": "Chromolithograph {prompt}. Vibrant colors, intricate details, rich color saturation, meticulous registration, multi-layered printing, decorative elements, historical charm, artistic reproductions, commercial posters, nostalgic, ornate compositions.", + "negative_prompt": "monochromatic, simple designs, limited color palette, imprecise registration, minimalistic, modern aesthetic, digital appearance.", + "name_cn": "MK 色彩版画", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mk_chromolithography.jpg" + }, + { + "name": "MK Cross Processing Print", + "prompt": "Cross processing print {prompt}. Experimental color shifts, unconventional tonalities, vibrant and surreal hues, heightened contrasts, unpredictable results, artistic unpredictability, retro and vintage feel, dynamic color interplay, abstract and dreamlike.", + "negative_prompt": "predictable color tones, traditional processing, realistic color representation, subdued contrasts, standard photographic aesthetics.", + "name_cn": "MK 交叉过程打印", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mk_cross_processing_print.jpg" + }, + { + "name": "MK Dufaycolor Photograph", + "prompt": "Dufaycolor photograph {prompt}. Vintage color palette, distinctive color rendering, soft and dreamy atmosphere, historical charm, unique color process, grainy texture, evocative mood, nostalgic aesthetic, hand-tinted appearance, artistic patina.", + "negative_prompt": "modern color reproduction, hyperrealistic tones, sharp and clear details, digital precision, contemporary aesthetic.", + "name_cn": "MK 杜法色彩照片", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mk_dufaycolor_photograph.jpg" + }, + { + "name": "MK Herbarium", + "prompt": "Herbarium drawing{prompt}. Botanical accuracy, old botanical book illustration, detailed illustrations, pressed plants, delicate and precise linework, scientific documentation, meticulous presentation, educational purpose, organic compositions, timeless aesthetic, naturalistic beauty.", + "negative_prompt": "abstract representation, vibrant colors, artistic interpretation, chaotic compositions, fantastical elements, digital appearance.", + "name_cn": "MK 植物标本馆", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mk_herbarium.jpg" + }, + { + "name": "MK Punk Collage", + "prompt": "punk collage style {prompt} . mixed media, papercut,textured paper, overlapping, ripped posters, safety pins, chaotic layers, graffiti-style elements, anarchy symbols, vintage photos, cut-and-paste aesthetic, bold typography, distorted images, political messages, urban decay, distressed textures, newspaper clippings, spray paint, rebellious icons, DIY spirit, vivid colors, punk band logos, edgy and raw compositions, ", + "negative_prompt": "conventional,blurry, noisy, low contrast", + "name_cn": "MK 朋克拼贴画", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mk_punk_collage.jpg" + }, + { + "name": "MK mosaic", + "prompt": "mosaic style {prompt} . fragmented, assembled, colorful, highly detailed", + "negative_prompt": "whole, unbroken, monochrome", + "name_cn": "MK 镶嵌图", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mk_mosaic.jpg" + }, + { + "name": "MK Van Gogh", + "prompt": "Oil painting by Van Gogh {prompt} . Expressive, impasto, swirling brushwork, vibrant, brush strokes, Brushstroke-heavy, Textured, Impasto, Colorful, Dynamic, Bold, Distinctive, Vibrant, Whirling, Expressive, Dramatic, Swirling, Layered, Intense, Contrastive, Atmospheric, Luminous, Textural, Evocative, SpiraledVan Gogh style", + "negative_prompt": "realistic, photorealistic, calm, straight lines, signature, frame, text, watermark", + "name_cn": "MK 梵高", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mk_van_gogh.jpg" + }, + { + "name": "MK Coloring Book", + "prompt": "centered black and white high contrast line drawing, coloring book style,{prompt} . monochrome, blank white background", + "negative_prompt": "greyscale, gradients,shadows,shadow, colored, Red, Blue, Yellow, Green, Orange, Purple, Pink, Brown, Gray, Beige, Turquoise, Lavender, Cyan, Magenta, Olive, Indigo, black background", + "name_cn": "MK 色彩书", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mk_coloring_book.jpg" + }, + { + "name": "MK Singer Sargent", + "prompt": "Oil painting by John Singer Sargent {prompt}. Elegant, refined, masterful technique,realistic portrayal, subtle play of light, captivating expression, rich details, harmonious colors, skillful composition, brush strokes, chiaroscuro.", + "negative_prompt": "realistic, photorealistic, abstract, overly stylized, excessive contrasts, distorted,bright colors,disorder.", + "name_cn": "MK 辛格 · 萨尔生特", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mk_singer_sargent.jpg" + }, + { + "name": "MK Pollock", + "prompt": "Oil painting by Jackson Pollock {prompt}. Abstract expressionism, drip painting, chaotic composition, energetic, spontaneous, unconventional technique, dynamic, bold, distinctive, vibrant, intense, expressive, energetic, layered, non-representational, gestural.", + "negative_prompt": "(realistic:1.5), (photorealistic:1.5), representational, calm, ordered composition, precise lines, detailed forms, subdued colors, quiet, static, traditional, figurative.", + "name_cn": "MK 波洛克", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mk_pollock.jpg" + }, + { + "name": "MK Basquiat", + "prompt": "Artwork by Jean-Michel Basquiat {prompt}. Neo-expressionism, street art influence, graffiti-inspired, raw, energetic, bold colors, dynamic composition, chaotic, layered, textural, expressive, spontaneous, distinctive, symbolic,energetic brushstrokes.", + "negative_prompt": "(realistic:1.5), (photorealistic:1.5), calm, precise lines, conventional composition, subdued", + "name_cn": "MK 巴斯奎特", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mk_basquiat.jpg" + }, + { + "name": "MK Andy Warhol", + "prompt": "Artwork in the style of Andy Warhol {prompt}. Pop art, vibrant colors, bold compositions, repetition of iconic imagery, celebrity culture, commercial aesthetics, mass production influence, stylized simplicity, cultural commentary, graphical elements, distinctive portraits.", + "negative_prompt": "subdued colors, realistic, lack of repetition, minimalistic.", + "name_cn": "MK 安迪 · 沃霍尔", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mk_andy_warhol.jpg" + }, + { + "name": "MK Halftone print", + "prompt": "Halftone print of {prompt}. Dot matrix pattern, grayscale tones, vintage aesthetic, newspaper print vibe, stylized dots, visual texture, black and white contrasts, retro appearance, artistic pointillism,pop culture, (Roy Lichtenstein style:1.5).", + "negative_prompt": "smooth gradients, continuous tones, vibrant colors.", + "name_cn": "MK 半色版画", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mk_halftone_print.jpg" + }, + { + "name": "MK Gond Painting", + "prompt": "Gond painting {prompt}. Intricate patterns, vibrant colors, detailed motifs, nature-inspired themes, tribal folklore, fine lines, intricate detailing, storytelling compositions, mystical and folkloric, cultural richness.", + "negative_prompt": "monochromatic, abstract shapes, minimalistic.", + "name_cn": "MK 贡德绘画", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mk_gond_painting.jpg" + }, + { + "name": "MK Albumen Print", + "prompt": "Albumen print {prompt}. Sepia tones, fine details, subtle tonal gradations, delicate highlights, vintage aesthetic, soft and muted atmosphere, historical charm, rich textures, meticulous craftsmanship, classic photographic technique, vignetting.", + "negative_prompt": "vibrant colors, high contrast, modern, digital appearance, sharp details, contemporary style.", + "name_cn": "MK 白蛋清印刷", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mk_albumen_print.jpg" + }, + { + "name": "MK Aquatint Print", + "prompt": "Aquatint print {prompt}. Soft tonal gradations, atmospheric effects, velvety textures, rich contrasts, fine details, etching process, delicate lines, nuanced shading, expressive and moody atmosphere, artistic depth.", + "negative_prompt": "sharp contrasts, bold lines, minimalistic.", + "name_cn": "MK 水蚀刻印刷", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mk_aquatint_print.jpg" + }, + { + "name": "MK Anthotype Print", + "prompt": "Anthotype print {prompt}. Monochrome dye, soft and muted colors, organic textures, ephemeral and delicate appearance, low details, watercolor canvas, low contrast, overexposed, silhouette, textured paper.", + "negative_prompt": "vibrant synthetic dyes, bold and saturated colors.", + "name_cn": "MK 花纹版画", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mk_anthotype_print.jpg" + }, + { + "name": "MK Inuit Carving", + "prompt": "A sculpture made of ivory, {prompt} made of . Sculptures, Inuit art style, intricate carvings, natural materials, storytelling motifs, arctic wildlife themes, symbolic representations, cultural traditions, earthy tones, harmonious compositions, spiritual and mythological elements.", + "negative_prompt": "abstract, vibrant colors.", + "name_cn": "MK 因纽特雕塑", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mk_inuit_carving.jpg" + }, + { + "name": "MK Bromoil Print", + "prompt": "Bromoil print {prompt}. Painterly effects, sepia tones, textured surfaces, rich contrasts, expressive brushwork, tonal variations, vintage aesthetic, atmospheric mood, handmade quality, artistic experimentation, darkroom craftsmanship, vignetting.", + "negative_prompt": "smooth surfaces, minimal brushwork, contemporary digital appearance.", + "name_cn": "MK 溴油印刷", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mk_bromoil_print.jpg" + }, + { + "name": "MK Calotype Print", + "prompt": "Calotype print {prompt}. Soft focus, subtle tonal range, paper negative process, fine details, vintage aesthetic, artistic experimentation, atmospheric mood, early photographic charm, handmade quality, vignetting.", + "negative_prompt": "sharp focus, bold contrasts, modern aesthetic, digital photography.", + "name_cn": "MK 卡洛雅图印刷", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mk_calotype_print.jpg" + }, + { + "name": "MK Color Sketchnote", + "prompt": "Color sketchnote {prompt}. Hand-drawn elements, vibrant colors, visual hierarchy, playful illustrations, varied typography, graphic icons, organic and dynamic layout, personalized touches, creative expression, engaging storytelling.", + "negative_prompt": "monochromatic, geometric layout.", + "name_cn": "MK色彩素描笔记", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mk_color_sketchnote.jpg" + }, + { + "name": "MK Cibulak Porcelain", + "prompt": "A sculpture made of blue pattern porcelain of {prompt}. Classic design, blue and white color scheme, intricate detailing, floral motifs, onion-shaped elements, historical charm, rococo, white ware, cobalt blue, underglaze pattern, fine craftsmanship, traditional elegance, delicate patterns, vintage aesthetic, Meissen, Blue Onion pattern, Cibulak.", + "negative_prompt": "tea, teapot, cup, teacup,bright colors, bold and modern design, absence of intricate detailing, lack of floral motifs, non-traditional shapes.", + "name_cn": "MK 西布拉瓷器", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mk_cibulak_porcelain.jpg" + }, + { + "name": "MK Alcohol Ink Art", + "prompt": "Alcohol ink art {prompt}. Fluid and vibrant colors, unpredictable patterns, organic textures, translucent layers, abstract compositions, ethereal and dreamy effects, free-flowing movement, expressive brushstrokes, contemporary aesthetic, wet textured paper.", + "negative_prompt": "monochromatic, controlled patterns.", + "name_cn": "MK 酒精水彩艺术", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mk_alcohol_ink_art.jpg" + }, + { + "name": "MK One Line Art", + "prompt": "One line art {prompt}. Continuous and unbroken black line, minimalistic, simplicity, economical use of space, flowing and dynamic, symbolic representations, contemporary aesthetic, evocative and abstract, white background.", + "negative_prompt": "disjointed lines, complexity, complex detailing.", + "name_cn": "MK 一线画", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mk_one_line_art.jpg" + }, + { + "name": "MK Blacklight Paint", + "prompt": "Blacklight paint {prompt}. Fluorescent pigments, vibrant and surreal colors, ethereal glow, otherworldly effects, dynamic and psychedelic compositions, neon aesthetics, transformative in ultraviolet light, contemporary and experimental.", + "negative_prompt": "muted colors, traditional and realistic compositions.", + "name_cn": "MK 黑光油漆", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mk_blacklight_paint.jpg" + }, + { + "name": "MK Carnival Glass", + "prompt": "A sculpture made of Carnival glass, {prompt}. Iridescent surfaces, vibrant colors, intricate patterns, opalescent hues, reflective and prismatic effects, Art Nouveau and Art Deco influences, vintage charm, intricate detailing, lustrous and luminous appearance, Carnival Glass style.", + "negative_prompt": "non-iridescent surfaces, muted colors, absence of intricate patterns, lack of opalescent hues, modern and minimalist aesthetic.", + "name_cn": "MK 嘉年华玻璃", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mk_carnival_glass.jpg" + }, + { + "name": "MK Cyanotype Print", + "prompt": "Cyanotype print {prompt}. Prussian blue tones, distinctive coloration, high contrast, blueprint aesthetics, atmospheric mood, sun-exposed paper, silhouette effects, delicate details, historical charm, handmade and experimental quality.", + "negative_prompt": "vibrant colors, low contrast, modern and polished appearance.", + "name_cn": "MK 青色版画", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mk_cyanotype_print.jpg" + }, + { + "name": "MK Cross-Stitching", + "prompt": "Cross-stitching {prompt}. Intricate patterns, embroidery thread, sewing, fine details, precise stitches, textile artistry, symmetrical designs, varied color palette, traditional and contemporary motifs, handmade and crafted,canvas, nostalgic charm.", + "negative_prompt": "paper, paint, ink, photography.", + "name_cn": "MK 交叉针织", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mk_cross_stitching.jpg" + }, + { + "name": "MK Encaustic Paint", + "prompt": "Encaustic paint {prompt}. Textured surfaces, translucent layers, luminous quality, wax medium, rich color saturation, fluid and organic shapes, contemporary and historical influences, mixed media elements, atmospheric depth.", + "negative_prompt": "flat surfaces, opaque layers, lack of wax medium, muted color palette, absence of textured surfaces, non-mixed media.", + "name_cn": "MK 蜡漆", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mk_encaustic_paint.jpg" + }, + { + "name": "MK Embroidery", + "prompt": "Embroidery {prompt}. Intricate stitching, embroidery thread, fine details, varied thread textures, textile artistry, embellished surfaces, diverse color palette, traditional and contemporary motifs, handmade and crafted, tactile and ornate.", + "negative_prompt": "minimalist, monochromatic.", + "name_cn": "MK 刺绣", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mk_embroidery.jpg" + }, + { + "name": "MK Gyotaku", + "prompt": "Gyotaku {prompt}. Fish impressions, realistic details, ink rubbings, textured surfaces, traditional Japanese art form, nature-inspired compositions, artistic representation of marine life, black and white contrasts, cultural significance.", + "negative_prompt": "photography.", + "name_cn": "MK 鱼拓版画", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mk_gyotaku.jpg" + }, + { + "name": "MK Luminogram", + "prompt": "Luminogram {prompt}. Photogram technique, ethereal and abstract effects, light and shadow interplay, luminous quality, experimental process, direct light exposure, unique and unpredictable results, artistic experimentation.", + "negative_prompt": "", + "name_cn": "MK 光感影像", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mk_luminogram.jpg" + }, + { + "name": "MK Lite Brite Art", + "prompt": "Lite Brite art {prompt}. Luminous and colorful designs, pixelated compositions, retro aesthetic, glowing effects, creative patterns, interactive and playful, nostalgic charm, vibrant and dynamic arrangements.", + "negative_prompt": "monochromatic.", + "name_cn": "MK 彩色灯泡艺术", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mk_lite_brite_art.jpg" + }, + { + "name": "MK Mokume-gane", + "prompt": "Mokume-gane {prompt}. Wood-grain patterns, mixed metal layers, intricate and organic designs, traditional Japanese metalwork, harmonious color combinations, artisanal craftsmanship, unique and layered textures, cultural and historical significance.", + "negative_prompt": "uniform metal surfaces.", + "name_cn": "MK 木金工艺", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mk_mokume_gane.jpg" + }, + { + "name": "Pebble Art", + "prompt": "a sculpture made of peebles, {prompt}. Pebble art style,natural materials, textured surfaces, balanced compositions, organic forms, harmonious arrangements, tactile and 3D effects, beach-inspired aesthetic, creative storytelling, artisanal craftsmanship.", + "negative_prompt": "non-natural materials, lack of textured surfaces, imbalanced compositions, absence of organic forms, non-tactile appearance.", + "name_cn": "MK 鹅卵石艺术", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/pebble_art.jpg" + }, + { + "name": "MK Palekh", + "prompt": "Palekh art {prompt}. Miniature paintings, intricate details, vivid colors, folkloric themes, lacquer finish, storytelling compositions, symbolic elements, Russian folklore influence, cultural and historical significance.", + "negative_prompt": "large-scale paintings.", + "name_cn": "MK 帕列赫", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mk_palekh.jpg" + }, + { + "name": "MK Suminagashi", + "prompt": "Suminagashi {prompt}. Floating ink patterns, marbled effects, delicate and ethereal designs, water-based ink, fluid and unpredictable compositions, meditative process, monochromatic or subtle color palette, Japanese artistic tradition.", + "negative_prompt": "vibrant and bold color palette.", + "name_cn": "MK 澄洗画", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mk_suminagashi.jpg" + }, + { + "name": "MK Scrimshaw", + "prompt": "A Scrimshaw engraving of {prompt}. Intricate engravings on a spermwhale's teeth, marine motifs, detailed scenes, nautical themes, black and white contrasts, historical craftsmanship, artisanal carving, storytelling compositions, maritime heritage.", + "negative_prompt": "colorful, modern.", + "name_cn": "MK 丝线绣", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mk_scrimshaw.jpg" + }, + { + "name": "MK Shibori", + "prompt": "Shibori {prompt}. Textured fabric, intricate patterns, resist-dyeing technique, indigo or vibrant colors, organic and flowing designs, Japanese textile art, cultural tradition, tactile and visual interest.", + "negative_prompt": "monochromatic.", + "name_cn": "MK 湿布雕版印刷", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mk_shibori.jpg" + }, + { + "name": "MK Vitreous Enamel", + "prompt": "A sculpture made of Vitreous enamel {prompt}. Smooth and glossy surfaces, vibrant colors, glass-like finish, durable and resilient, intricate detailing, traditional and contemporary applications, artistic craftsmanship, jewelry and decorative objects, , Vitreous enamel, colored glass.", + "negative_prompt": "rough surfaces, muted colors.", + "name_cn": "MK 玻璃珐琅", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mk_vitreous_enamel.jpg" + }, + { + "name": "MK Ukiyo-e", + "prompt": "Ukiyo-e {prompt}. Woodblock prints, vibrant colors, intricate details, depictions of landscapes, kabuki actors, beautiful women, cultural scenes, traditional Japanese art, artistic craftsmanship, historical significance.", + "negative_prompt": "absence of woodblock prints, muted colors, lack of intricate details, non-traditional Japanese themes, absence of cultural scenes.", + "name_cn": "MK 浮世绘", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mk_ukiyo_e.jpg" + }, + { + "name": "MK vintage-airline-poster", + "prompt": "vintage airline poster {prompt} . classic aviation fonts, pastel colors, elegant aircraft illustrations, scenic destinations, distressed textures, retro travel allure", + "negative_prompt": "modern fonts, bold colors, hyper-realistic, sleek design", + "name_cn": "MK 古董航空公司海报", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mk_vintage_airline_poster.jpg" + }, + { + "name": "MK vintage-travel-poster", + "prompt": "vintage travel poster {prompt} . retro fonts, muted colors, scenic illustrations, iconic landmarks, distressed textures, nostalgic vibes", + "negative_prompt": "modern fonts, vibrant colors, hyper-realistic, sleek design", + "name_cn": "MK 古董旅行海报", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mk_vintage_travel_poster.jpg" + }, + { + "name": "MK bauhaus-style", + "prompt": "Bauhaus-inspired {prompt} . minimalism, geometric precision, primary colors, sans-serif typography, asymmetry, functional design", + "negative_prompt": "ornate, intricate, excessive detail, complex patterns, serif typography", + "name_cn": "Mk 包豪斯风格", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mk_bauhaus_style.jpg" + }, + { + "name": "MK afrofuturism", + "prompt": "Afrofuturism illustration {prompt} . vibrant colors, futuristic elements, cultural symbolism, cosmic imagery, dynamic patterns, empowering narratives", + "negative_prompt": "monochromatic", + "name_cn": "Mk 非洲未来主义", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mk_afrofuturism.jpg" + }, + { + "name": "MK atompunk", + "prompt": "Atompunk illustation, {prompt} . retro-futuristic, atomic age aesthetics, sleek lines, metallic textures, futuristic technology, optimism, energy", + "negative_prompt": "organic, natural textures, rustic, dystopian", + "name_cn": "Mk 原子朋克", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mk_atompunk.jpg" + }, + { + "name": "MK constructivism", + "prompt": "Constructivism {prompt} . geometric abstraction, bold colors, industrial aesthetics, dynamic compositions, utilitarian design, revolutionary spirit", + "negative_prompt": "organic shapes, muted colors, ornate elements, traditional", + "name_cn": "Mk 构成派", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mk_constructivism.jpg" + }, + { + "name": "MK chicano-art", + "prompt": "Chicano art {prompt} . bold colors, cultural symbolism, muralism, lowrider aesthetics, barrio life, political messages, social activism, Mexico", + "negative_prompt": "monochromatic, minimalist, mainstream aesthetics", + "name_cn": "Mk 西班牙裔美国艺术", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mk_chicano_art.jpg" + }, + { + "name": "MK de-stijl", + "prompt": "De Stijl Art {prompt} . neoplasticism, primary colors, geometric abstraction, horizontal and vertical lines, simplicity, harmony, utopian ideals", + "negative_prompt": "complex patterns, muted colors, ornate elements, asymmetry", + "name_cn": "Mk 去风格派", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mk_de_stijl.jpg" + }, + { + "name": "MK dayak-art", + "prompt": "Dayak art sculpture of {prompt} . intricate patterns, nature-inspired motifs, vibrant colors, traditional craftsmanship, cultural symbolism, storytelling", + "negative_prompt": "minimalist, monochromatic, modern", + "name_cn": "Mk 达雅克艺术", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mk_dayak_art.jpg" + }, + { + "name": "MK fayum-portrait", + "prompt": "Fayum portrait {prompt} . encaustic painting, realistic facial features, warm earth tones, serene expressions, ancient Egyptian influences", + "negative_prompt": "abstract, vibrant colors, exaggerated features, modern", + "name_cn": "Mk 法尤姆肖像画", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mk_fayum_portrait.jpg" + }, + { + "name": "MK illuminated-manuscript", + "prompt": "Illuminated manuscript {prompt} . intricate calligraphy, rich colors, detailed illustrations, gold leaf accents, ornate borders, religious, historical, medieval", + "negative_prompt": "modern typography, minimalist design, monochromatic, abstract themes", + "name_cn": "Mk 彩绘手稿", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mk_illuminated_manuscript.jpg" + }, + { + "name": "MK kalighat-painting", + "prompt": "Kalighat painting {prompt} . bold lines, vibrant colors, narrative storytelling, cultural motifs, flat compositions, expressive characters", + "negative_prompt": "subdued colors, intricate details, realistic portrayal, modern aesthetics", + "name_cn": "Mk 卡利加特绘画", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mk_kalighat_painting.jpg" + }, + { + "name": "MK madhubani-painting", + "prompt": "Madhubani painting {prompt} . intricate patterns, vibrant colors, nature-inspired motifs, cultural storytelling, symmetry, folk art aesthetics", + "negative_prompt": "abstract, muted colors, minimalistic design, modern aesthetics", + "name_cn": "Mk 马杜班尼绘画", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mk_madhubani_painting.jpg" + }, + { + "name": "MK pictorialism", + "prompt": "Pictorialism illustration{prompt} . soft focus, atmospheric effects, artistic interpretation, tonality, muted colors, evocative storytelling", + "negative_prompt": "sharp focus, high contrast, realistic depiction, vivid colors", + "name_cn": "Mk 描绘主义", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mk_pictorialism.jpg" + }, + { + "name": "MK pichwai-painting", + "prompt": "Pichwai painting {prompt} . intricate detailing, vibrant colors, religious themes, nature motifs, devotional storytelling, gold leaf accents", + "negative_prompt": "minimalist, subdued colors, abstract design", + "name_cn": "Mk 皮奇瓦伊绘画", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mk_pichwai_painting.jpg" + }, + { + "name": "MK patachitra-painting", + "prompt": "Patachitra painting {prompt} . bold outlines, vibrant colors, intricate detailing, mythological themes, storytelling, traditional craftsmanship", + "negative_prompt": "subdued colors, minimalistic, abstract, modern aesthetics", + "name_cn": "Mk 帕塔基特拉绘画", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mk_patachitra_painting.jpg" + }, + { + "name": "MK samoan-art-inspired", + "prompt": "Samoan art-inspired wooden sculpture {prompt} . traditional motifs, natural elements, bold colors, cultural symbolism, storytelling, craftsmanship", + "negative_prompt": "modern aesthetics, minimalist, abstract", + "name_cn": "Mk 萨莫亚艺术启发的", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mk_samoan_art_inspired.jpg" + }, + { + "name": "MK tlingit-art", + "prompt": "Tlingit art {prompt} . formline design, natural elements, animal motifs, bold colors, cultural storytelling, traditional craftsmanship, Alaska traditional art, (totem:1.5)", + "negative_prompt": "", + "name_cn": "Mk 特林吉特艺术", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mk_tlingit_art.jpg" + }, + { + "name": "MK adnate-style", + "prompt": "Painting by Adnate {prompt} . realistic portraits, street art, large-scale murals, subdued color palette, social narratives", + "negative_prompt": "abstract, vibrant colors, small-scale art", + "name_cn": "Mk 阿达内特风格", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mk_adnate_style.jpg" + }, + { + "name": "MK ron-english-style", + "prompt": "Painting by Ron English {prompt} . pop-surrealism, cultural subversion, iconic mash-ups, vibrant and bold colors, satirical commentary", + "negative_prompt": "traditional, monochromatic", + "name_cn": "Mk 罗恩英国风格", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mk_ron_english_style.jpg" + }, + { + "name": "MK shepard-fairey-style", + "prompt": "Painting by Shepard Fairey {prompt} . street art, political activism, iconic stencils, bold typography, high contrast, red, black, and white color palette", + "negative_prompt": "traditional, muted colors", + "name_cn": "Mk 舒帕德 · 费尔利风格", + "thumbnail": "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/mk_shepard_fairey_style.jpg" + } +] \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/resources/mmb-preset.custom.txt.example b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/resources/mmb-preset.custom.txt.example new file mode 100644 index 0000000000000000000000000000000000000000..eb113a50dd7a1cac37be92e6af5dc1b9e78fceee --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/resources/mmb-preset.custom.txt.example @@ -0,0 +1,8 @@ +MMB-ALL:1,1,1 +MMB-IN:1,0,0 +MMB-MID:0,1,0 +MMB-OUT:0,0,1 +MMB-INMID:1,1,0 +MMB-INOUT:1,0,1 +MMB-MIDOUT:0,1,1 +MMB-NONE:0,0,0 \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/resources/mmb-preset.txt b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/resources/mmb-preset.txt new file mode 100644 index 0000000000000000000000000000000000000000..7403808c1102962a01c9cfe7231c22fe57eafe74 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/resources/mmb-preset.txt @@ -0,0 +1,9 @@ +MMB-ALL:1,1,1 +MMB-IN:1,0,0 +MMB-MID:0,1,0 +MMB-OUT:0,0,1 +MMB-INMID:1,1,0 +MMB-INOUT:1,0,1 +MMB-MIDOUT:0,1,1 +MMB-NONE:0,0,0 +@MMBN-FULL-TEST:27 diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/resources/portrait_prompt.json b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/resources/portrait_prompt.json new file mode 100644 index 0000000000000000000000000000000000000000..7c77a392bd7169c85f712ac1b9fd9479bcf4591d --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/resources/portrait_prompt.json @@ -0,0 +1,407 @@ +{ + "beard_list": [ + "Stubble Beard", + "Goatee", + "Full Beard", + "Van Dyke Beard", + "Soul Patch", + "Garibaldi Beard", + "Mutton Chops", + "Circle Beard", + "Corporate Beard", + "Balbo Beard", + "Ducktail Beard", + "Chinstrap Beard", + "Anchor Beard", + "Chevron Mustache", + "Horseshoe Mustache", + "Handlebar Mustache", + "Imperial Mustache", + "Pencil Mustache", + "Friendly Mutton Chops", + "Zappa Mustache" + ], + "body_type_list": [ + "Underweight", + "Normal weight", + "Overweight", + "Obese" + ], + "eyes_color_list": [ + "Brown", + "Blue", + "Green", + "Hazel", + "Gray", + "Amber", + "Red", + "Violet" + ], + "face_expression_list": [ + "Happy", + "Sad", + "Angry", + "Surprised", + "Fearful", + "Disgusted", + "Contemptuous", + "Excited", + "Nervous", + "Confused", + "Amused", + "Content", + "Disappointed", + "Bored", + "Relieved", + "In love", + "Shy", + "Envious", + "Proud", + "Cautious", + "Serious", + "Serene", + "Peaceful", + "Calm" + ], + "face_shape_list": [ + "Oval", + "Round", + "Square", + "Heart-shaped", + "Long", + "Rectangle", + "Triangle", + "Inverted Triangle", + "Pear-shaped", + "Oblong", + "Square Round", + "Square Oval" + ], + "gender_list": [ + "Man", + "Woman" + ], + "hair_color_list": [ + "Black", + "Brown", + "Blonde", + "Red", + "Auburn", + "Chestnut", + "Gray", + "White", + "Salt and pepper" + ], + "hair_style_list": [ + "Asymmetrical cut", + "Blunt cut", + "Bob cut", + "Braided bob", + "Buzz cut", + "Choppy cut", + "Curly bob", + "Curtain bangs", + "Faux hawk", + "Feathered cut", + "French bob", + "Layered cut", + "Long bob", + "Mohawk", + "Pixie cut", + "Shag cut", + "Side-swept bangs", + "Textured cut", + "Undercut", + "Wavy bob", + "Faux hawk short pixie", + "Brave short haircut with shaved sides", + "Tapered haricut wuth shaved side", + "Stacked bob", + "Lemonade braids", + "Middle part ponytails", + "Stitch braids", + "Deep side part", + "French braids", + "Box braids", + "Two dutch braids", + "Wavy cut with curtains bangs", + "Right side shaved", + "Sweeping pixie", + "Smooth lob", + "Long pixie", + "Sideswept pixie", + "Italian bob", + "Shullet" + ], + "light_direction_list": [ + "top", + "bottom", + "right", + "left", + "front", + "rear", + "top-right", + "top-left", + "bottom-right", + "bottom-left" + ], + "light_type_list": [ + "Natural sunlight", + "Soft ambient light", + "Harsh sunlight", + "Overcast sky", + "Sunset glow", + "Sunrise warmth", + "Twilight hues", + "Candlelight", + "Incandescent lighting", + "Fluorescent lighting", + "Moonlight", + "Dappled sunlight", + "Backlit silhouette", + "Spotlight", + "Rim lighting", + "Firelight", + "City streetlights", + "Studio lighting", + "Lantern light", + "Tungsten lighting", + "Cloudy day diffused light", + "Skylight", + "Golden hour light", + "Blue hour light", + "Flash photography", + "Stage lighting", + "Neon lights", + "Torchlight", + "Softbox lighting", + "Rim light", + "Lightning", + "Abstract light patterns" + ], + "model_pose_list": [ + "Power Pose", + "Walking Pose", + "The Over-the-Shoulder Look", + "S-curve Pose", + "Sitting Pose", + "Close-Up Beauty Shot Pose", + "Leaning Pose", + "Arms Up Pose", + "Casual Stroll Pose", + "Headshot Pose", + "Sitting Cross-Legged Pose", + "Back Arch Pose", + "Hand-on-Hip Pose", + "Gazing into the Distance Pose", + "Candid Laugh Pose", + "Dynamic Action Pose", + "Contrapposto Pose", + "High Fashion Pose" + ], + "nationality_list": [ + "Afghan", + "Albanian", + "Algerian", + "Andorran", + "Angolan", + "Antiguans Barbudans", + "Argentine", + "Armenian", + "Australian", + "Austrian", + "Azerbaijani", + "Bahamian", + "Bahraini", + "Bangladeshi", + "Barbadian", + "Belarusian", + "Belgian", + "Belizean", + "Beninese", + "Bhutanese", + "Bolivian", + "Bosnian Herzegovinian", + "Brazilian", + "British", + "Bruneian", + "Bulgarian", + "Burkinabe", + "Burundian", + "Cambodian", + "Cameroonian", + "Canadian", + "Cape Verdian", + "Central African", + "Chadian", + "Chilean", + "Chinese", + "Colombian", + "Comoran", + "Congolese", + "Costa Rican", + "Croatian", + "Cuban", + "Cypriot", + "Czech", + "Danish", + "Djibouti", + "Dominican", + "Dutch", + "East Timorese", + "Ecuadorean", + "Egyptian", + "Emirian", + "Equatorial Guinean", + "Eritrean", + "Estonian", + "Ethiopian", + "Fijian", + "Filipino", + "Finnish", + "French", + "Gabonese", + "Gambian", + "Georgian", + "German", + "Ghanaian", + "Greek", + "Grenadian", + "Guatemalan", + "Guinean", + "Guyanese", + "Haitian", + "Herzegovinian", + "Honduran", + "Hungarian", + "Icelander", + "Indian", + "Indonesian", + "Iranian", + "Iraqi", + "Irish", + "Israeli", + "Italian", + "Ivorian", + "Jamaican", + "Japanese", + "Jordanian", + "Kazakhstani", + "Kenyan", + "Kiribati", + "North Korean", + "South Korean", + "Kuwaiti", + "Kyrgyz", + "Laotian", + "Latvian", + "Lebanese", + "Liberian", + "Libyan", + "Liechtensteiner", + "Lithuanian", + "Luxembourgish", + "Macedonian", + "Malagasy", + "Malawian", + "Malaysian", + "Maldivan", + "Malian", + "Maltese", + "Marshallese", + "Mauritanian", + "Mauritian", + "Mexican", + "Micronesian", + "Moldovan", + "Monegasque", + "Mongolian", + "Montenegrin", + "Moroccan", + "Mosotho", + "Motswana", + "Mozambican", + "Namibian", + "Nauruan", + "Nepalese", + "New Zealander", + "Ni-Vanuatu", + "Nicaraguan", + "Nigerian", + "Nigerien", + "North Korean", + "Northern Irish", + "Norwegian", + "Omani", + "Pakistani", + "Palauan", + "Palestinian", + "Panamanian", + "Papua New Guinean", + "Paraguayan", + "Peruvian", + "Polish", + "Portuguese", + "Qatari", + "Romanian", + "Russian", + "Rwandan", + "Saint Lucian", + "Salvadoran", + "Samoan", + "San Marinese", + "Sao Tomean", + "Saudi", + "Scottish", + "Senegalese", + "Serbian", + "Seychellois", + "Sierra Leonean", + "Singaporean", + "Slovakian", + "Slovenian", + "Solomon Islander", + "Somali", + "South African", + "South Korean", + "South Sudanese", + "Spanish", + "Sri Lankan", + "Sudanese", + "Surinamer", + "Swazi", + "Swedish", + "Swiss", + "Syrian", + "Tajikistani", + "Tanzanian", + "Thai", + "Togolese", + "Tongan", + "Trinidadian Tobagonian", + "Tunisian", + "Turkish", + "Turkmen", + "Tuvaluan", + "Ugandan", + "Ukrainian", + "Uruguayan", + "Uzbekistani", + "Venezuelan", + "Vietnamese", + "Welsh", + "Yemeni", + "Zambian", + "Zimbabwean" + ], + "shot_list": [ + "Head portrait", + "Head and shoulders portrait", + "Half-length portrait", + "Full-length portrait", + "Face", + "Portrait", + "Full body", + "Close-up" + ] +} \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/resources/wenquan.ttf b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/resources/wenquan.ttf new file mode 100644 index 0000000000000000000000000000000000000000..1055090beecf37cccb14188c39b7838f63f92b1c --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/resources/wenquan.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3481c900f2094fe674a59f1e70a7b8eab723cd9b16ab32c0d1b0e9c1025f4d5d +size 4626136 diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/styles/your_styles.json.example b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/styles/your_styles.json.example new file mode 100644 index 0000000000000000000000000000000000000000..169ebfa42b737b73e96efbc9f8fd8631a4f1b84d --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/styles/your_styles.json.example @@ -0,0 +1,8 @@ +[ + { + "name": "Example Style", + "name_cn": "示例样式", + "prompt": "(masterpiece), (best quality), (ultra-detailed), {prompt} ", + "negative_prompt": "text, watermark, logo" + } +] \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/tools/combine_autocomplete.py b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/tools/combine_autocomplete.py new file mode 100644 index 0000000000000000000000000000000000000000..d0245371ce4fdbba63af467ac0e8696bb64e5962 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/tools/combine_autocomplete.py @@ -0,0 +1,50 @@ +# 自定义提示词自动补全工具 + +import os, sys +import glob +import shutil + +output_file = None +cwd_path = os.path.dirname(os.path.realpath(__file__)) +pyssss_path = os.path.join(cwd_path, "..", "ComfyUI-Custom-Scripts", "user") +combine_folder = os.path.join(cwd_path, "autocomplete") + +def backup_autocomplete(): + bak_file = os.path.join(pyssss_path, "autocomplete.txt.bak") + if os.path.exists(bak_file): + pass + elif os.path.exists(output_file): + shutil.copy(output_file, bak_file) + +def combine_autocomplete(): + if os.path.exists(combine_folder): + pass + else: + os.mkdir(combine_folder) + if os.path.exists(pyssss_path): + output_file = os.path.join(pyssss_path, "autocomplete.txt") + # 遍历 combine 目录下的所有 txt 文件,读取内容并合并 + merged_content = '' + for file_path in glob.glob(os.path.join(combine_folder, '*.txt')): + with open(file_path, 'r', encoding='utf-8', errors='ignore') as file: + try: + file_content = file.read() + merged_content += file_content + '\n' + except UnicodeDecodeError: + pass + if merged_content != '': + # 将合并的内容写入目标文件 autocomplete.txt,并指定编码为 utf-8 + with open(output_file, 'w', encoding='utf-8') as target_file: + target_file.write(merged_content) + +if __name__ == "__main__": + arg = sys.argv[0] + if 'combine_autocomplete' in arg: + arg = sys.argv[1] + if arg == 'backup': + backup_autocomplete() + elif arg == 'combine': + combine_autocomplete() + else: + print("Usage: python combine_autocomplete.py [backup|combine]") + sys.exit(1) \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/css/account.css b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/css/account.css new file mode 100644 index 0000000000000000000000000000000000000000..626aa07c42783ce5377421f3207c4db105957b4e --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/css/account.css @@ -0,0 +1,93 @@ +.easyuse-account{ + +} +.easyuse-account-user{ + font-size: 10px; + color:var(--descrip-text); + text-align: center; +} +.easyuse-account-user-info{ + display: flex; + justify-content: space-between; + align-items: center; + padding-bottom:10px; + cursor: pointer; +} +.easyuse-account-user-info .user{ + display: flex; + align-items: center; +} +.easyuse-account-user-info .edit{ + padding:5px 10px; + background: var(--comfy-menu-bg); + border-radius:4px; +} +.easyuse-account-user-info:hover{ + filter:brightness(110%); +} +.easyuse-account-user-info h5{ + margin:0; + font-size: 10px; + text-align: left; +} +.easyuse-account-user-info h6{ + margin:0; + font-size: 8px; + text-align: left; + font-weight: 300; +} +.easyuse-account-user-info .remark{ + margin-top: 4px; +} +.easyuse-account-user-info .avatar{ + width: 36px; + height: 36px; + background: var(--comfy-input-bg); + border-radius: 50%; + margin-right: 5px; + display: flex; + justify-content: center; + align-items: center; + font-size: 16px; + overflow: hidden; +} +.easyuse-account-user-info .avatar img{ + width: 100%; + height: 100%; +} +.easyuse-account-dialog{ + width: 600px; +} +.easyuse-account-dialog-main a, .easyuse-account-dialog-main a:visited{ + font-weight: 400; + color: var(--theme-color-light); +} +.easyuse-account-dialog-item{ + display: flex; + justify-content: flex-start; + align-items: center; + padding: 10px 0; + border-bottom: 1px solid var(--border-color); +} +.easyuse-account-dialog-item input{ + padding:5px; + margin-right:5px; +} +.easyuse-account-dialog-item input.key{ + flex:1; +} +.easyuse-account-dialog-item button{ + cursor: pointer; + margin-left:5px!important; + padding:5px!important; + font-size: 16px!important; +} +.easyuse-account-dialog-item button:hover{ + filter:brightness(120%); +} +.easyuse-account-dialog-item button.choose { + background: var(--theme-color); +} +.easyuse-account-dialog-item button.delete{ + background: var(--error-color); +} \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/css/chooser.css b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/css/chooser.css new file mode 100644 index 0000000000000000000000000000000000000000..0c1696bf36fcbe2a5b1b0e83ea00897be97bf391 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/css/chooser.css @@ -0,0 +1,35 @@ +.easyuse-chooser-dialog{ + max-width: 600px; +} +.easyuse-chooser-dialog-title{ + font-size: 18px; + font-weight: 700; + text-align: center; + color:var(--input-text); + margin:0; +} +.easyuse-chooser-dialog-images{ + margin-top:10px; + display: flex; + flex-wrap: wrap; + width: 100%; + box-sizing: border-box; +} +.easyuse-chooser-dialog-images img{ + width: 50%; + height: auto; + cursor: pointer; + box-sizing: border-box; + filter:brightness(80%); +} +.easyuse-chooser-dialog-images img:hover{ + filter:brightness(100%); +} +.easyuse-chooser-dialog-images img.selected{ + border: 4px solid var(--success-color); +} + +.easyuse-chooser-hidden{ + display: none; + height:0; +} \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/css/contextmenu.css b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/css/contextmenu.css new file mode 100644 index 0000000000000000000000000000000000000000..9fc0e9583b19394cf73259ead2cba816eac3e351 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/css/contextmenu.css @@ -0,0 +1,20 @@ +.easyuse-model{ + position:relative; +} +.easyuse-model:hover img{ + display: block; + opacity: 1; +} +.easyuse-model img{ + position: absolute; + z-index:1; + right:-155px; + top:0; + width:150px; + height:auto; + display: none; + filter:brightness(70%); + -webkit-filter: brightness(70%); + opacity: 0; + transition:all 0.5s ease-in-out; +} \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/css/dropdown.css b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/css/dropdown.css new file mode 100644 index 0000000000000000000000000000000000000000..176ff618ff050358844309bb344511758dcae6a5 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/css/dropdown.css @@ -0,0 +1,68 @@ +.easy-dropdown, .easy-nested-dropdown { + position: relative; + box-sizing: border-box; + background-color: #171717; + box-shadow: 0 4px 4px rgba(255, 255, 255, .25); + padding: 0; + margin: 0; + list-style: none; + z-index: 1000; + overflow: visible; + max-height: fit-content; + max-width: fit-content; +} + +.easy-dropdown { + position: absolute; + border-radius: 0; +} + +/* Style for final items */ +.easy-dropdown li.item, .easy-nested-dropdown li.item { + font-weight: normal; + min-width: max-content; +} + +/* Style for folders (parent items) */ +.easy-dropdown li.folder, .easy-nested-dropdown li.folder { + cursor: default; + position: relative; + border-right: 3px solid cyan; +} + +.easy-dropdown li.folder::after, .easy-nested-dropdown li.folder::after { + content: ">"; + position: absolute; + right: 2px; + font-weight: normal; +} + +.easy-dropdown li, .easy-nested-dropdown li { + padding: 4px 10px; + cursor: pointer; + font-family: system-ui; + font-size: 0.7rem; + position: relative; +} + +/* Style for nested dropdowns */ +.easy-nested-dropdown { + position: absolute; + top: 0; + left: 100%; + margin: 0; + border: none; + display: none; +} + +.easy-dropdown li.selected > .easy-nested-dropdown, +.easy-nested-dropdown li.selected > .easy-nested-dropdown { + display: block; + border: none; +} + +.easy-dropdown li.selected, +.easy-nested-dropdown li.selected { + background-color: #e5e5e5; + border: none; +} \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/css/easy.css b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/css/easy.css new file mode 100644 index 0000000000000000000000000000000000000000..b2f835372f8daba1de442a008b4cf196cd9e2a83 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/css/easy.css @@ -0,0 +1,130 @@ + +.pysssss-workflow-popup{ + min-width:220px!important; + /*right:0px!important;*/ + /*left:auto!important;*/ +} +body{ + font-family: var(--font-family)!important; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} +textarea{ + font-family: var(--font-family)!important; +} + +.comfy-multiline-input{ + background-color: transparent; + border:1px solid var(--border-color); + border-radius:8px; + padding: 8px; + font-size: 12px; +} +.comfy-modal { + border:1px solid var(--border-color); + box-shadow:none; + backdrop-filter: blur(8px) brightness(120%); +} +.comfy-menu{ + border-radius:16px; + box-shadow:0 0 1px var(--descrip-text); + backdrop-filter: blur(8px) brightness(120%); +} +.comfy-menu button,.comfy-modal button { + font-size: 14px; + padding:4px 0; + margin-bottom:4px; +} +.comfy-menu button.comfy-settings-btn{ + font-size: 12px; +} +.comfy-menu-btns { + margin-bottom: 4px; +} +.comfy-menu-btns button,.comfy-list-actions button{ + font-size: 10px; +} +.comfy-menu > button, +.comfy-menu-btns button, +.comfy-menu .comfy-list button, +.comfy-modal button { + border-width:1px; +} +.comfy-modal-content{ + width: 100%; +} + + +dialog{ + border:1px solid var(--border-color); + background:transparent; + backdrop-filter: blur(8px) brightness(120%); + box-shadow:none; +} +.cm-title{ + background-color:transparent!important; +} +.cm-notice-board{ + border-radius:10px!important; + border:1px solid var(--border-color)!important; +} +.cm-menu-container{ + margin-bottom:50px!important; +} +hr{ + border:1px solid var(--border-color); +} +#comfy-dev-save-api-button{ + justify-content: center; +} +#shareButton{ + background:linear-gradient(to left,var(--theme-color),var(--theme-color-light))!important; + color:white!important; +} +#queue-button{ + position:relative; + overflow:hidden; + min-height:30px; + z-index:1; +} + +#queue-button:after{ + clear: both; + content:attr(data-attr); + background:green; + color:#FFF; + width:var(--process-bar-width); + height:100%; + position:absolute; + top:0; + left:0; + z-index:0; + text-align:center; + display:flex; + justify-content:center; + align-items:center; +} + +.litegraph .litemenu-entry.has_submenu { + border-right: 2px solid var(--theme-color); +} +::-webkit-scrollbar { + width: 0em; +} +::-webkit-scrollbar-track { + background-color: transparent; +} +::-webkit-scrollbar-thumb { + background-color: transparent; + border-radius: 2px; +} +::-webkit-scrollbar-thumb:hover { + background-color: transparent; +} + +[data-theme="dark"] .workspace_manager .chakra-card{ + background-color:var(--comfy-menu-bg)!important; +} +.workspace_manager .chakra-card{ + width: 400px; +} \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/css/groupmap.css b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/css/groupmap.css new file mode 100644 index 0000000000000000000000000000000000000000..75aa8679c80bbc80e06ee174d4d570fb9f1b6844 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/css/groupmap.css @@ -0,0 +1,34 @@ +#easyuse_groups_map{ + flex-direction: column; + align-items: end; + display:flex;position: absolute; + top: 50px; left: 10px; width: 180px; + border-radius:12px; + min-height:100px; + max-height:400px; + color: var(--descrip-text); + background-color: var(--comfy-menu-bg); + padding: 10px 4px; + border: 1px solid var(--border-color); + z-index: 399; + padding-top: 0; +} +#easyuse_groups_map .icon{ + width: 12px; + height:12px; +} +#easyuse_groups_map .closeBtn{ + float: right; + color: var(--input-text); + border-radius:30px; + background-color: var(--comfy-input-bg); + border: 1px solid var(--border-color); + cursor: pointer; + aspect-ratio: 1 / 1; + display: flex; + justify-content: center; + align-items: center; +} +#easyuse_groups_map .closeBtn:hover{ + filter:brightness(120%); +} \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/css/index.css b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/css/index.css new file mode 100644 index 0000000000000000000000000000000000000000..5c148a2153dc91441e6b9581b1d6794ea2159161 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/css/index.css @@ -0,0 +1,11 @@ +@import "theme.css"; +@import "dropdown.css"; +@import "selector.css"; +@import "groupmap.css"; +@import "contextmenu.css"; +@import "modelinfo.css"; +@import "toast.css"; +@import "account.css"; +@import "chooser.css"; +@import "toolbar.css"; +@import "sliderControl.css"; \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/css/modelinfo.css b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/css/modelinfo.css new file mode 100644 index 0000000000000000000000000000000000000000..370c21f9a1c0625ce4d88780ceb1abc9e9c1e25f --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/css/modelinfo.css @@ -0,0 +1,265 @@ +.easyuse-model-info { + color: white; + max-width: 90vw; + font-family: var(--font-family); +} +.easyuse-model-content { + display: flex; + flex-direction: column; + overflow: hidden; +} +.easyuse-model-header{ + margin:0 0 15px 0; +} +.easyuse-model-header-remark{ + display: flex; + align-items: center; + margin-top:5px; +} +.easyuse-model-info h2 { + text-align: left; + margin:0; +} +.easyuse-model-info h5 { + text-align: left; + margin:0 15px 0 0px; + font-weight: 400; + color:var(--descrip-text); +} +.easyuse-model-info p { + margin: 5px 0; +} +.easyuse-model-info a { + color: var(--theme-color-light); +} +.easyuse-model-info a:hover { + text-decoration: underline; +} +.easyuse-model-tags-list { + display: flex; + flex-wrap: wrap; + list-style: none; + gap: 10px; + max-height: 200px; + overflow: auto; + margin: 10px 0; + padding: 0; +} +.easyuse-model-tag { + background-color: var(--comfy-input-bg); + border: 2px solid var(--border-color); + color: var(--input-text); + display: flex; + align-items: center; + gap: 5px; + border-radius: 5px; + padding: 2px 5px; + cursor: pointer; +} +.easyuse-model-tag--selected span::before { + content: "✅"; + position: absolute; + background-color: var(--theme-color-light); + left: 0; + top: 0; + right: 0; + bottom: 0; + text-align: center; +} +.easyuse-model-tag:hover { + border: 2px solid var(--theme-color-light); +} +.easyuse-model-tag p { + margin: 0; +} +.easyuse-model-tag span { + text-align: center; + border-radius: 5px; + background-color: var(--theme-color-light); + padding: 2px; + position: relative; + min-width: 20px; + overflow: hidden; + color: #fff; +} + +.easyuse-model-metadata .comfy-modal-content { + max-width: 100%; +} +.easyuse-model-metadata label { + margin-right: 1ch; + color: #ccc; +} + +.easyuse-model-metadata span { + color: var(--theme-color-light); +} + +.easyuse-preview { + max-width:660px; + margin-right: 15px; + position: relative; +} +.easyuse-preview-group{ + position: relative; + overflow: hidden; + border-radius:.5rem; + width: 660px; +} +.easyuse-preview-list{ + display: flex; + flex-wrap: nowrap; + width: 100%; + transition: all .5s ease-in-out; +} +.easyuse-preview-list.no-transition{ + transition: none; +} +.easyuse-preview-slide{ + display: flex; + flex-basis: calc(50% - 5px); + flex-grow: 0; + flex-shrink: 0; + position: relative; + justify-content: center; + align-items: center; + padding-right:5px; + padding-left:0; +} +.easyuse-preview-slide:nth-child(even){ + padding-left:5px; + padding-right:0; +} +.easyuse-preview-slide-content{ + position: relative; + min-height:150px; + width: 100%; +} +.easyuse-preview-slide-content .save{ + position: absolute; + right: 6px; + z-index: 12; + bottom: 6px; + display: flex; + align-items: center; + height: 26px; + padding: 0 9px; + color: var(--input-text); + font-size: 12px; + line-height: 26px; + background: rgba(0, 0, 0, .5); + border-radius: 13px; + cursor: pointer; + min-width:80px; + text-align: center; +} +.easyuse-preview-slide-content .save:hover{ + filter: brightness(120%); + will-change: auto; +} + +.easyuse-preview-slide-content img { + border-radius: 14px; + object-position: center center; + max-width: 100%; + max-height:700px; + border-style: none; + vertical-align: middle; +} +.easyuse-preview button { + position: absolute; + z-index:10; + top: 50%; + display: flex; + align-items: center; + justify-content: center; + width:30px; + height:30px; + border-radius:15px; + border:1px solid rgba(66, 63, 78, .15); + background-color: rgba(66, 63, 78, .5); + color:hsla(0, 0%, 100%, .8); + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke; + transition-timing-function: cubic-bezier(.4,0,.2,1); + transition-duration: .15s; + transform: translateY(-50%); +} +.easyuse-preview button.left{ + left:10px; +} +.easyuse-preview button.right{ + right:10px; +} + +.easyuse-model-detail{ + margin-top: 16px; + overflow: hidden; + border: 1px solid var(--border-color); + border-radius: 8px; + width:300px; +} +.easyuse-model-detail-head{ + height: 40px; + padding: 0 10px; + font-weight: 500; + font-size: 14px; + font-style: normal; + line-height: 40px; +} +.easyuse-model-detail-body{ + box-sizing: border-box; + font-size: 12px; +} +.easyuse-model-detail-item{ + display: flex; + justify-content: flex-start; + border-top: 1px solid var(--border-color); +} +.easyuse-model-detail-item-label{ + flex-shrink: 0; + width: 88px; + padding-top: 5px; + padding-bottom: 5px; + padding-left: 10px; + border-right: 1px solid var(--border-color); + color: var(--input-text); + font-weight: 400; +} +.easyuse-model-detail-item-value{ + display: flex; + flex-wrap: wrap; + padding: 5px 10px 5px 10px; + color: var(--input-text); +} +.easyuse-model-detail-textarea{ + border-top:1px solid var(--border-color); + padding:10px; + height:100px; + overflow-y: auto; + font-size: 12px; +} +.easyuse-model-detail-textarea textarea{ + width:100%; + height:100%; + border:0; + background-color:transparent; + color: var(--input-text); +} +.easyuse-model-detail-textarea textarea::placeholder{ + color:var(--descrip-text); +} +.easyuse-model-detail-textarea.empty{ + display: flex; + justify-content: center; + align-items: center; + color: var(--descrip-text); +} + +.easyuse-model-notes { + background-color: rgba(0, 0, 0, 0.25); + padding: 5px; + margin-top: 5px; +} +.easyuse-model-notes:empty { + display: none; +} diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/css/selector.css b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/css/selector.css new file mode 100644 index 0000000000000000000000000000000000000000..4ecce1e4f7ebca4c207bd55b87c150d2061bd9bd --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/css/selector.css @@ -0,0 +1,108 @@ +.easyuse-prompt-styles{ + overflow: auto; +} +.easyuse-prompt-styles .tools{ + display:flex; + justify-content:space-between; + height:30px; + padding-bottom:10px; + border-bottom:2px solid var(--border-color); +} +.easyuse-prompt-styles .tools button.delete{ + height:30px; + border-radius: 8px; + border: 2px solid var(--border-color); + font-size:11px; + background:var(--comfy-input-bg); + color:var(--error-text); + box-shadow:none; + cursor:pointer; +} +.easyuse-prompt-styles .tools button.delete:hover{ + filter: brightness(1.2); +} +.easyuse-prompt-styles .tools textarea.search{ + flex:1; + margin-left:10px; + height:20px; + line-height:20px; + border-radius: 8px; + border: 2px solid var(--border-color); + font-size:11px; + background:var(--comfy-input-bg); + color:var(--input-text); + box-shadow:none; + padding:4px 10px; + outline: none; + resize: none; + appearance:none; +} +.easyuse-prompt-styles-list{ + list-style: none; + padding: 0; + margin: 0; + min-height: 150px; + height: calc(100% - 40px); + overflow: auto; + /*display: flex;*/ + /*flex-wrap: wrap;*/ +} +.easyuse-prompt-styles-list.no-top{ + height: auto; +} + +.easyuse-prompt-styles-tag{ + display: inline-block; + vertical-align: middle; + margin-top: 8px; + margin-right: 8px; + padding:4px; + color: var(--input-text); + background-color: var(--comfy-input-bg); + border-radius: 8px; + border: 2px solid var(--border-color); + font-size:11px; + cursor:pointer; +} +.easyuse-prompt-styles-tag.hide{ + display:none; +} +.easyuse-prompt-styles-tag:hover{ + filter: brightness(1.2); +} +.easyuse-prompt-styles-tag input{ + --ring-color: transparent; + position: relative; + box-shadow: none; + border: 2px solid var(--border-color); + border-radius: 2px; + background: linear-gradient(135deg, var(--comfy-menu-bg) 0%, var(--comfy-input-bg) 60%); +} +.easyuse-prompt-styles-tag input[type=checkbox]:checked{ + border: 1px solid var(--theme-color-light); + background-color: var(--theme-color-light); + background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e"); +} +.easyuse-prompt-styles-tag input[type=checkbox]{ + color-adjust: exact; + display: inline-block; + flex-shrink: 0; + vertical-align: middle; + appearance: none; + border: 2px solid var(--border-color); + background-origin: border-box; + padding: 0; + width: 1rem; + height: 1rem; + border-radius:4px; + color:var(--theme-color-light); + user-select: none; +} +.easyuse-prompt-styles-tag span{ + margin:0 4px; + vertical-align: middle; +} +#show_image_id{ + width:128px; + height:128px; +} \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/css/sliderControl.css b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/css/sliderControl.css new file mode 100644 index 0000000000000000000000000000000000000000..42e749b45422289c02be96ab3f17ceb33ca9a9cd --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/css/sliderControl.css @@ -0,0 +1,66 @@ +.easyuse-slider{ + width:100%; + height:100%; + display: flex; + flex-direction: row; + justify-content: space-between; + position: relative; +} +.easyuse-slider-item{ + height: inherit; + min-width: 25px; + justify-content: center; + display: flex; + flex-direction: column; + align-items: center; +} +.easyuse-slider-item.positive .easyuse-slider-item-label{ + color: var(--success-color); +} +.easyuse-slider-item.negative .easyuse-slider-item-label{ + color: var(--error-color); +} +.easyuse-slider-item-input{ + height:15px; + font-size: 10px; + color: var(--input-text); +} +.easyuse-slider-item-label{ + height:15px; + border: none; + color: var(--descrip-text); + font-size: 8px; +} +.easyuse-slider-item-scroll { + width: 5px; + height: calc(100% - 30px); + background: var(--comfy-input-bg); + border-radius: 10px; + position: relative; +} +.easyuse-slider-item-bar{ + width: 10px; + height: 10px; + background: linear-gradient(to bottom, var(--input-text), var(--descrip-text)); + border-radius:100%; + box-shadow: 0 2px 10px var(--bg-color); + position: absolute; + top: 0; + left:-2.5px; + cursor: pointer; + z-index:1; +} +.easyuse-slider-item-area{ + width: 100%; + border-radius:20px; + position: absolute; + bottom: 0; + background: var(--input-text); + z-index:0; +} +.easyuse-slider-item.positive .easyuse-slider-item-area{ + background: var(--success-color); +} +.easyuse-slider-item.negative .easyuse-slider-item-area{ + background: var(--error-color); +} diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/css/theme.css b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/css/theme.css new file mode 100644 index 0000000000000000000000000000000000000000..d2754c8f1ba54e020d971ffb46cbcab3ec537f36 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/css/theme.css @@ -0,0 +1,10 @@ +:root { + /*--theme-color:#3f3eed;*/ + /*--theme-color-light: #008ecb;*/ + --theme-color:#236692; + --theme-color-light: #3485bb; + --success-color: #52c41a; + --error-color: #ff4d4f; + --warning-color: #faad14; + --font-family: Inter, -apple-system, BlinkMacSystemFont, Helvetica Neue, sans-serif; +} diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/css/toast.css b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/css/toast.css new file mode 100644 index 0000000000000000000000000000000000000000..b0166fb4d2c7e5c54773128be7d200260b1cb89a --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/css/toast.css @@ -0,0 +1,110 @@ +.easyuse-toast-container{ + position: fixed; + z-index: 99999; + top: 0; + left: 0; + width: 100%; + height: 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: start; + padding:10px 0; +} +.easyuse-toast-container > div { + position: relative; + height: fit-content; + padding: 4px; + margin-top: -100px; /* re-set by JS */ + opacity: 0; + transition: all 0.33s ease-in-out; + z-index: 3; +} + +.easyuse-toast-container > div:last-child { + z-index: 2; +} + +.easyuse-toast-container > div:not(.-show) { + z-index: 1; +} + +.easyuse-toast-container > div.-show { + opacity: 1; + margin-top: 0px !important; +} + +.easyuse-toast-container > div.-show { + opacity: 1; + transform: translateY(0%); +} + +.easyuse-toast-container > div > div { + position: relative; + background: var(--comfy-menu-bg); + color: var(--input-text); + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + height: fit-content; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.88); + padding: 9px 12px; + border-radius: 8px; + font-family: Arial, sans-serif; + font-size: 14px; + pointer-events: all; +} + +.easyuse-toast-container > div > div > span { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; +} + +.easyuse-toast-container > div > div > span svg { + width: 16px; + height: auto; + margin-right: 8px; +} + +.easyuse-toast-container > div > div > span svg[data-icon=info-circle]{ + fill: var(--theme-color-light); +} +.easyuse-toast-container > div > div > span svg[data-icon=check-circle]{ + fill: var(--success-color); +} +.easyuse-toast-container > div > div > span svg[data-icon=close-circle]{ + fill: var(--error-color); +} +.easyuse-toast-container > div > div > span svg[data-icon=exclamation-circle]{ + fill: var(--warning-color); +} +/*rotate animation*/ +@keyframes rotate { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} +.easyuse-toast-container > div > div > span svg[data-icon=loading]{ + fill: var(--theme-color); + animation: rotate 1s linear infinite; +} + +.easyuse-toast-container a { + cursor: pointer; + text-decoration: underline; + color: var(--theme-color-light); + margin-left: 4px; + display: inline-block; + line-height: 1; +} + +.easyuse-toast-container a:hover { + color: var(--theme-color-light); + text-decoration: none; +} \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/css/toolbar.css b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/css/toolbar.css new file mode 100644 index 0000000000000000000000000000000000000000..89a72439701e8a01149ab87895a0605858ef851d --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/css/toolbar.css @@ -0,0 +1,230 @@ +.easyuse-toolbar{ + background: rgba(15,15,15,.5); + backdrop-filter: blur(4px) brightness(120%); + border-radius:0 12px 12px 0; + min-width:50px; + height:24px; + position: fixed; + bottom:85px; + left:0px; + display: flex; + align-items: center; + z-index:10000; +} +.easyuse-toolbar.disable-render-info{ + bottom: 55px; +} +.easyuse-toolbar-item{ + border-radius:20px; + height: 20px; + width:20px; + cursor: pointer; + display: flex; + justify-content: center; + align-items: center; + transition: all 0.3s ease-in-out; + margin-left:2.5px; +} +.easyuse-toolbar-icon{ + width: 14px; + height: 14px; + display: flex; + justify-content: center; + align-items: center; + font-size: 12px; + color:white; + transition: all 0.3s ease-in-out; +} +.easyuse-toolbar-icon svg{ + width: 14px; + height: 14px; +} +.easyuse-toolbar-tips{ + visibility: hidden; + opacity: 0; + position: absolute; + top: -25px; + left: 0; + color: var(--descrip-text); + padding: 2px 5px; + border-radius: 5px; + font-size: 11px; + min-width:100px; + transition: all 0.3s ease-in-out; +} +.easyuse-toolbar-item:hover{ + background:rgba(12,12,12,1); +} +.easyuse-toolbar-item:hover .easyuse-toolbar-tips{ + opacity: 1; + visibility: visible; +} +.easyuse-toolbar-item:hover .easyuse-toolbar-icon.group{ + color:var(--warning-color); +} +.easyuse-toolbar-item:hover .easyuse-toolbar-icon.rocket{ + color:var(--theme-color-light); +} +.easyuse-toolbar-item:hover .easyuse-toolbar-icon.question{ + color:var(--success-color); +} + + +.easyuse-guide-dialog{ + max-width: 300px; + font-family: var(--font-family); + position: absolute; + z-index:100; + left:0; + bottom:140px; + background: rgba(25,25,25,.85); + backdrop-filter: blur(8px) brightness(120%); + border-radius:0 12px 12px 0; + padding:10px; + transition: .5s all ease-in-out; + visibility: visible; + opacity: 1; + transform: translateX(0%); +} +.easyuse-guide-dialog.disable-render-info{ + bottom:110px; +} +.easyuse-guide-dialog-top{ + display: flex; + justify-content: space-between; + align-items: center; +} +.easyuse-guide-dialog-top .icon{ + width: 12px; + height:12px; +} +.easyuse-guide-dialog.hidden{ + opacity: 0; + transform: translateX(-50%); + visibility: hidden; +} +.easyuse-guide-dialog .closeBtn{ + float: right; + color: var(--input-text); + border-radius:30px; + background-color: var(--comfy-input-bg); + border: 1px solid var(--border-color); + cursor: pointer; + aspect-ratio: 1 / 1; + display: flex; + justify-content: center; + align-items: center; +} +.easyuse-guide-dialog .closeBtn:hover{ + filter:brightness(120%); +} +.easyuse-guide-dialog-title{ + color:var(--input-text); + font-size: 16px; + font-weight: bold; + margin-bottom: 5px; +} +.easyuse-guide-dialog-remark{ + color: var(--input-text); + font-size: 12px; + margin-top: 5px; +} +.easyuse-guide-dialog-content{ + max-height: 600px; + overflow: auto; +} +.easyuse-guide-dialog a, .easyuse-guide-dialog a:visited{ + color: var(--theme-color-light); + cursor: pointer; +} +.easyuse-guide-dialog-note{ + margin-top: 20px; + color:white; +} +.easyuse-guide-dialog p{ + margin:4px 0; + font-size: 12px; + font-weight: 300; +} +.markdown-body h1, .markdown-body h2, .markdown-body h3, .markdown-body h4, .markdown-body h5, .markdown-body h6 { + margin-top: 12px; + margin-bottom: 8px; + font-weight: 600; + line-height: 1.25; + padding-bottom: 5px; + border-bottom: 1px solid var(--border-color); + color: var(--input-text); +} +.markdown-body h1{ + font-size: 18px; +} +.markdown-body h2{ + font-size: 16px; +} +.markdown-body h3{ + font-size: 14px; +} +.markdown-body h4{ + font-size: 13px; +} +.markdown-body table { + display: block; + /*width: 100%;*/ + /*width: max-content;*/ + max-width: 300px; + overflow: auto; + color:var(--input-text); + box-sizing: border-box; + border: 1px solid var(--border-color); + text-align: left; + width: 100%; +} +.markdown-body table th, .markdown-body table td { + padding: 6px 13px; + font-size: 12px; + margin:0; + border-right: 1px solid var(--border-color); + border-bottom: 1px solid var(--border-color); +} +.markdown-body table td { + font-size: 12px; +} +.markdown-body table th:last-child, .markdown-body table td:last-child{ + border-right: none; +} +.markdown-body table tr:last-child td{ + border-bottom: none; +} +.markdown-body table th{ + font-weight: bold; + width: auto; + min-width: 70px; +} +.markdown-body table th:last-child{ + width:100%; +} +.markdown-body .warning{ + color:var(--warning-color) +} +.markdown-body .error{ + color:var(--error-color) +} +.markdown-body .success{ + color:var(--success-color) +} +.markdown-body .link{ + color:var(--theme-color-light) +} + +#comfyui-menu-monitor{ + width:120px; +} +#comfyui-menu-monitor #crystools-monitor-container{ + margin:0 auto!important; +} +#comfyui-menu-monitor #crystools-monitor-container > div{ + margin:2px 0!important; +} +#comfyui-menu-monitor #crystools-monitor-container > div > div > div{ + padding:0 4px!important; +} \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/bookmark.js b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/bookmark.js new file mode 100644 index 0000000000000000000000000000000000000000..554b484fe5d98712e3a7135ea2db52c2e0fd71be --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/bookmark.js @@ -0,0 +1,101 @@ +import { app } from "../../../scripts/app.js"; + + +app.registerExtension({ + name: "easy bookmark", + registerCustomNodes() { + class Bookmark { + type = 'easy bookmark' + title = "🔖"; + + slot_start_y = -20; + + ___collapsed_width = 0; + + get _collapsed_width() { + return this.___collapsed_width; + } + + set _collapsed_width(width){ + const canvas = app.canvas ; + const ctx = canvas.canvas.getContext('2d'); + if(ctx){ + const oldFont = ctx.font; + ctx.font = canvas.title_text_font; + this.___collapsed_width = 40 + ctx.measureText(this.title).width; + ctx.font = oldFont; + } + } + + isVirtualNode = true; + serialize_widgets = true; + keypressBound = null; + + constructor() { + + this.addWidget('text', 'shortcut_key', '1', (value) => { + value = value.trim()[0] || '1'; + if(value !== ''){ + this.title = "🔖 " + value; + } + },{ + y: 8, + }); + this.addWidget('number', 'zoom', 1, (value) => {}, { + y: 8 + LiteGraph.NODE_WIDGET_HEIGHT + 4, + max: 2, + min: 0.5, + precision: 2, + }); + this.keypressBound = this.onKeypress.bind(this); + } + + onAdded(){ + setTimeout(_=>{ + const value = this.widgets[0].value + if(value){ + this.title = "🔖 " + value; + } + },1) + window.addEventListener("keydown", this.keypressBound); + } + + onRemoved() { + window.removeEventListener("keydown", this.keypressBound); + } + + onKeypress(event){ + const target = event.target; + if (['input','textarea'].includes(target.localName)) { + return; + } + if (this.widgets[0] && event.key.toLocaleLowerCase() === this.widgets[0].value.toLocaleLowerCase()) { + this.canvasToBookmark(); + } + } + + canvasToBookmark() { + const canvas = app.canvas; + // ComfyUI seemed to break us again, but couldn't repro. No reason to not check, I guess. + // https://github.com/rgthree/rgthree-comfy/issues/71 + if (canvas?.ds?.offset) { + canvas.ds.offset[0] = -this.pos[0] + 16; + canvas.ds.offset[1] = -this.pos[1] + 40; + } + if (canvas?.ds?.scale != null) { + canvas.ds.scale = Number(this.widgets[1].value || 1); + } + canvas.setDirty(true, true); + } + } + + LiteGraph.registerNodeType( + "easy bookmark", + Object.assign(Bookmark,{ + title: "Bookmark 🔖", + }) + ); + + Bookmark.category = "EasyUse/Util" + } +}) \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/common/dropdown.js b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/common/dropdown.js new file mode 100644 index 0000000000000000000000000000000000000000..f1fd485f842be4c9beb6dbad823f0741cfb2725d --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/common/dropdown.js @@ -0,0 +1,218 @@ +let activeDropdown = null; + +export function removeDropdown() { + if (activeDropdown) { + activeDropdown.removeEventListeners(); + activeDropdown.dropdown.remove(); + activeDropdown = null; + } +} +export function createDropdown(inputEl, suggestions, onSelect, isDict = false) { + removeDropdown(); + new Dropdown(inputEl, suggestions, onSelect, isDict); +} + +class Dropdown { + constructor(inputEl, suggestions, onSelect, isDict = false) { + this.dropdown = document.createElement('ul'); + this.dropdown.setAttribute('role', 'listbox'); + this.dropdown.classList.add('easy-dropdown'); + this.selectedIndex = -1; + this.inputEl = inputEl; + this.suggestions = suggestions; + this.onSelect = onSelect; + this.isDict = isDict; + + this.focusedDropdown = this.dropdown; + + this.buildDropdown(); + + this.onKeyDownBound = this.onKeyDown.bind(this); + this.onWheelBound = this.onWheel.bind(this); + this.onClickBound = this.onClick.bind(this); + + this.addEventListeners(); + } + + buildDropdown() { + if (this.isDict) { + this.buildNestedDropdown(this.suggestions, this.dropdown); + } else { + this.suggestions.forEach((suggestion, index) => { + this.addListItem(suggestion, index, this.dropdown); + }); + } + + const inputRect = this.inputEl.getBoundingClientRect(); + this.dropdown.style.top = (inputRect.top + inputRect.height - 10) + 'px'; + this.dropdown.style.left = inputRect.left + 'px'; + + document.body.appendChild(this.dropdown); + activeDropdown = this; + } + + buildNestedDropdown(dictionary, parentElement) { + let index = 0; + Object.keys(dictionary).forEach((key) => { + const item = dictionary[key]; + if (typeof item === "object" && item !== null) { + const nestedDropdown = document.createElement('ul'); + nestedDropdown.setAttribute('role', 'listbox'); + nestedDropdown.classList.add('easy-nested-dropdown'); + const parentListItem = document.createElement('li'); + parentListItem.classList.add('folder'); + parentListItem.textContent = key; + parentListItem.appendChild(nestedDropdown); + parentListItem.addEventListener('mouseover', this.onMouseOver.bind(this, index, parentElement)); + parentElement.appendChild(parentListItem); + this.buildNestedDropdown(item, nestedDropdown); + index = index + 1; + } else { + const listItem = document.createElement('li'); + listItem.classList.add('item'); + listItem.setAttribute('role', 'option'); + listItem.textContent = key; + listItem.addEventListener('mouseover', this.onMouseOver.bind(this, index, parentElement)); + listItem.addEventListener('mousedown', this.onMouseDown.bind(this, key)); + parentElement.appendChild(listItem); + index = index + 1; + } + }); + } + + addListItem(item, index, parentElement) { + const listItem = document.createElement('li'); + listItem.setAttribute('role', 'option'); + listItem.textContent = item; + listItem.addEventListener('mouseover', this.onMouseOver.bind(this, index)); + listItem.addEventListener('mousedown', this.onMouseDown.bind(this, item)); + parentElement.appendChild(listItem); + } + + addEventListeners() { + document.addEventListener('keydown', this.onKeyDownBound); + this.dropdown.addEventListener('wheel', this.onWheelBound); + document.addEventListener('click', this.onClickBound); + } + + removeEventListeners() { + document.removeEventListener('keydown', this.onKeyDownBound); + this.dropdown.removeEventListener('wheel', this.onWheelBound); + document.removeEventListener('click', this.onClickBound); + } + + onMouseOver(index, parentElement) { + if (parentElement) { + this.focusedDropdown = parentElement; + } + this.selectedIndex = index; + this.updateSelection(); + } + + onMouseOut() { + this.selectedIndex = -1; + this.updateSelection(); + } + + onMouseDown(suggestion, event) { + event.preventDefault(); + this.onSelect(suggestion); + this.dropdown.remove(); + this.removeEventListeners(); + } + + onKeyDown(event) { + const enterKeyCode = 13; + const escKeyCode = 27; + const arrowUpKeyCode = 38; + const arrowDownKeyCode = 40; + const arrowRightKeyCode = 39; + const arrowLeftKeyCode = 37; + const tabKeyCode = 9; + + const items = Array.from(this.focusedDropdown.children); + const selectedItem = items[this.selectedIndex]; + + if (activeDropdown) { + if (event.keyCode === arrowUpKeyCode) { + event.preventDefault(); + this.selectedIndex = Math.max(0, this.selectedIndex - 1); + this.updateSelection(); + } + + else if (event.keyCode === arrowDownKeyCode) { + event.preventDefault(); + this.selectedIndex = Math.min(items.length - 1, this.selectedIndex + 1); + this.updateSelection(); + } + + else if (event.keyCode === arrowRightKeyCode) { + event.preventDefault(); + if (selectedItem && selectedItem.classList.contains('folder')) { + const nestedDropdown = selectedItem.querySelector('.easy-nested-dropdown'); + if (nestedDropdown) { + this.focusedDropdown = nestedDropdown; + this.selectedIndex = 0; + this.updateSelection(); + } + } + } + + else if (event.keyCode === arrowLeftKeyCode && this.focusedDropdown !== this.dropdown) { + const parentDropdown = this.focusedDropdown.closest('.easy-dropdown, .easy-nested-dropdown').parentNode.closest('.easy-dropdown, .easy-nested-dropdown'); + if (parentDropdown) { + this.focusedDropdown = parentDropdown; + this.selectedIndex = Array.from(parentDropdown.children).indexOf(this.focusedDropdown.parentNode); + this.updateSelection(); + } + } + + else if ((event.keyCode === enterKeyCode || event.keyCode === tabKeyCode) && this.selectedIndex >= 0) { + event.preventDefault(); + if (selectedItem.classList.contains('item')) { + this.onSelect(items[this.selectedIndex].textContent); + this.dropdown.remove(); + this.removeEventListeners(); + } + + const nestedDropdown = selectedItem.querySelector('.easy-nested-dropdown'); + if (nestedDropdown) { + this.focusedDropdown = nestedDropdown; + this.selectedIndex = 0; + this.updateSelection(); + } + } + + else if (event.keyCode === escKeyCode) { + this.dropdown.remove(); + this.removeEventListeners(); + } + } + } + + onWheel(event) { + const top = parseInt(this.dropdown.style.top); + if (localStorage.getItem("Comfy.Settings.Comfy.InvertMenuScrolling")) { + this.dropdown.style.top = (top + (event.deltaY < 0 ? 10 : -10)) + "px"; + } else { + this.dropdown.style.top = (top + (event.deltaY < 0 ? -10 : 10)) + "px"; + } + } + + onClick(event) { + if (!this.dropdown.contains(event.target) && event.target !== this.inputEl) { + this.dropdown.remove(); + this.removeEventListeners(); + } + } + + updateSelection() { + Array.from(this.focusedDropdown.children).forEach((li, index) => { + if (index === this.selectedIndex) { + li.classList.add('selected'); + } else { + li.classList.remove('selected'); + } + }); + } +} \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/common/i18n.js b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/common/i18n.js new file mode 100644 index 0000000000000000000000000000000000000000..643a11016b6e884ee5961c94a727445f1170cef0 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/common/i18n.js @@ -0,0 +1,101 @@ +import {getLocale} from './utils.js' +const locale = getLocale() + +const zhCN = { + "Workflow created by": "工作流创建者", + "Watch more video content": "观看更多视频内容", + "Workflow Guide":"工作流指南", + // ExtraMenu + "💎 View Checkpoint Info...": "💎 查看 Checkpoint 信息...", + "💎 View Lora Info...": "💎 查看 Lora 信息...", + "🔃 Reload Node": "🔃 刷新节点", + // ModelInfo + "Updated At:": "最近更新:", + "Created At:": "首次发布:", + "✏️ Edit": "✏️ 编辑", + "💾 Save": "💾 保存", + "No notes": "当前还没有备注内容", + "Saving Notes...": "正在保存备注...", + "Type your notes here":"在这里输入备注内容", + "ModelName":"模型名称", + "Models Required":"所需模型", + "Download Model": "下载模型", + "Source Url": "模型源地址", + "Notes": "备注", + "Type": "类型", + "Trained Words": "训练词", + "BaseModel": "基础算法", + "Details": "详情", + "Description": "描述", + "Download": "下载量", + "Source": "来源", + "Saving Preview...": "正在保存预览图...", + "Saving Succeed":"保存成功", + "Clean SuccessFully":"清理成功", + "Clean Failed": "清理失败", + "Saving Failed":"保存失败", + "No COMBO link": "沒有找到COMBO连接", + "Reboot ComfyUI":"重启ComfyUI", + "Are you sure you'd like to reboot the server?": "是否要重启ComfyUI?", + // GroupMap + "Groups Map": "管理组", + "Cleanup Of GPU Usage": "清理GPU占用", + "Please stop all running tasks before cleaning GPU": "请在清理GPU之前停止所有运行中的任务", + "Always": "启用中", + "Bypass": "已忽略", + "Never": "已停用", + "Auto Sorting": "自动排序", + "Toggle `Show/Hide` can set mode of group, LongPress can set group nodes to never": "点击`启用中/已忽略`可设置组模式, 长按可停用该组节点", + // Quick + "Enable ALT+1~9 to paste nodes from nodes template (ComfyUI-Easy-Use)": "启用ALT1~9从节点模板粘贴到工作流 (ComfyUI-Easy-Use)", + "Enable process bar in queue button (ComfyUI-Easy-Use)": "启用提示词队列进度显示条 (ComfyUI-Easy-Use)", + "Enable ContextMenu Auto Nest Subdirectories (ComfyUI-Easy-Use)": "启用上下文菜单自动嵌套子目录 (ComfyUI-Easy-Use)", + "Enable tool bar fixed on the left-bottom (ComfyUI-Easy-Use)": "启用工具栏固定在左下角 (ComfyUI-Easy-Use)", + "Too many thumbnails, have closed the display": "模型缩略图太多啦,为您关闭了显示", + // selector + "Empty All": "清空所有", + "🔎 Type here to search styles ...": "🔎 在此处输入以搜索样式 ...", + // account + "Loading UserInfo...": "正在获取用户信息...", + "Please set the APIKEY first": "请先设置APIKEY", + "Setting APIKEY": "设置APIKEY", + "Save Account Info": "保存账号信息", + "Choose": "选择", + "Delete": "删除", + "Edit": "编辑", + "At least one account is required": "删除失败: 至少需要一个账户", + "APIKEY is not Empty": "APIKEY 不能为空", + "Add Account": "添加账号", + "Getting Your APIKEY": "获取您的APIKEY", + // choosers + "Choose Selected Images": "选择选中的图片", + "Choose images to continue": "选择图片以继续", + // seg + "Background": "背景", + "Hat": "帽子", + "Hair": "头发", + "Body": "身体", + "Face": "脸部", + "Clothes": "衣服", + "Others": "其他", + "Glove": "手套", + "Sunglasses": "太阳镜", + "Upper-clothes": "上衣", + "Dress": "连衣裙", + "Coat": "外套", + "Socks": "袜子", + "Pants": "裤子", + "Jumpsuits": "连体衣", + "Scarf": "围巾", + "Skirt": "裙子", + "Left-arm": "左臂", + "Right-arm": "右臂", + "Left-leg": "左腿", + "Right-leg": "右腿", + "Left-shoe": "左鞋", + "Right-shoe": "右鞋", +} +export const $t = (key) => { + const cn = zhCN[key] + return locale === 'zh-CN' && cn ? cn : key +} \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/common/icon.js b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/common/icon.js new file mode 100644 index 0000000000000000000000000000000000000000..e71a0a34ff16dcb668d164aeff2ede0abe87b9a0 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/common/icon.js @@ -0,0 +1,25 @@ +export const logoIcon = ` + + + + + + + + + + + + + + + + + +` + +export const quesitonIcon = `` +export const rocketIcon = `` +export const groupIcon = `` +export const rebootIcon = `` +export const closeIcon = `` \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/common/model.js b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/common/model.js new file mode 100644 index 0000000000000000000000000000000000000000..b94928835526d17cf076f1612d4a8148ecec02af --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/common/model.js @@ -0,0 +1,683 @@ +import { $el, ComfyDialog } from "../../../../scripts/ui.js"; +import { api } from "../../../../scripts/api.js"; +import {formatTime} from './utils.js'; +import {$t} from "./i18n.js"; +import {toast} from "./toast.js"; + +class MetadataDialog extends ComfyDialog { + constructor() { + super(); + this.element.classList.add("easyuse-model-metadata"); + } + show(metadata) { + super.show( + $el( + "div", + Object.keys(metadata).map((k) => + $el("div", [$el("label", { textContent: k }), $el("span", { textContent: metadata[k] })]) + ) + ) + ); + } +} + +export class ModelInfoDialog extends ComfyDialog { + constructor(name) { + super(); + this.name = name; + this.element.classList.add("easyuse-model-info"); + } + + get customNotes() { + return this.metadata["easyuse.notes"]; + } + + set customNotes(v) { + this.metadata["easyuse.notes"] = v; + } + + get hash() { + return this.metadata["easyuse.sha256"]; + } + + async show(type, value) { + this.type = type; + + const req = api.fetchApi("/easyuse/metadata/" + encodeURIComponent(`${type}/${value}`)); + this.info = $el("div", { style: { flex: "auto" } }); + // this.img = $el("img", { style: { display: "none" } }); + this.imgCurrent = 0 + this.imgList = $el("div.easyuse-preview-list",{ + style: { display: "none" } + }) + this.imgWrapper = $el("div.easyuse-preview", [ + $el("div.easyuse-preview-group",[ + this.imgList + ]), + ]); + this.main = $el("main", { style: { display: "flex" } }, [this.imgWrapper, this.info]); + this.content = $el("div.easyuse-model-content", [ + $el("div.easyuse-model-header",[$el("h2", { textContent: this.name })]) + , this.main]); + + const loading = $el("div", { textContent: "ℹ️ Loading...", parent: this.content }); + + super.show(this.content); + + this.metadata = await (await req).json(); + this.viewMetadata.style.cursor = this.viewMetadata.style.opacity = ""; + this.viewMetadata.removeAttribute("disabled"); + + loading.remove(); + this.addInfo(); + } + + createButtons() { + const btns = super.createButtons(); + this.viewMetadata = $el("button", { + type: "button", + textContent: "View raw metadata", + disabled: "disabled", + style: { + opacity: 0.5, + cursor: "not-allowed", + }, + onclick: (e) => { + if (this.metadata) { + new MetadataDialog().show(this.metadata); + } + }, + }); + + btns.unshift(this.viewMetadata); + return btns; + } + + parseNote() { + if (!this.customNotes) return []; + + let notes = []; + // Extract links from notes + const r = new RegExp("(\\bhttps?:\\/\\/[^\\s]+)", "g"); + let end = 0; + let m; + do { + m = r.exec(this.customNotes); + let pos; + let fin = 0; + if (m) { + pos = m.index; + fin = m.index + m[0].length; + } else { + pos = this.customNotes.length; + } + + let pre = this.customNotes.substring(end, pos); + if (pre) { + pre = pre.replaceAll("\n", "
"); + notes.push( + $el("span", { + innerHTML: pre, + }) + ); + } + if (m) { + notes.push( + $el("a", { + href: m[0], + textContent: m[0], + target: "_blank", + }) + ); + } + + end = fin; + } while (m); + return notes; + } + + addInfoEntry(name, value) { + return $el( + "p", + { + parent: this.info, + }, + [ + typeof name === "string" ? $el("label", { textContent: name + ": " }) : name, + typeof value === "string" ? $el("span", { textContent: value }) : value, + ] + ); + } + + async getCivitaiDetails() { + const req = await fetch("https://civitai.com/api/v1/model-versions/by-hash/" + this.hash); + if (req.status === 200) { + return await req.json(); + } else if (req.status === 404) { + throw new Error("Model not found"); + } else { + throw new Error(`Error loading info (${req.status}) ${req.statusText}`); + } + } + + addCivitaiInfo() { + const promise = this.getCivitaiDetails(); + const content = $el("span", { textContent: "ℹ️ Loading..." }); + + this.addInfoEntry( + $el("label", [ + $el("img", { + style: { + width: "18px", + position: "relative", + top: "3px", + margin: "0 5px 0 0", + }, + src: "https://civitai.com/favicon.ico", + }), + $el("span", { textContent: "Civitai: " }), + ]), + content + ); + + return promise + .then((info) => { + this.imgWrapper.style.display = 'block' + // 变更标题信息 + let header = this.element.querySelector('.easyuse-model-header') + if(header){ + header.replaceChildren( + $el("h2", { textContent: this.name }), + $el("div.easyuse-model-header-remark",[ + $el("h5", { textContent: $t("Updated At:") + formatTime(new Date(info.updatedAt),'yyyy/MM/dd')}), + $el("h5", { textContent: $t("Created At:") + formatTime(new Date(info.updatedAt),'yyyy/MM/dd')}), + ]) + ) + } + // 替换内容 + let textarea = null + let notes = this.parseNote.call(this) + let editText = $t("✏️ Edit") + console.log(notes) + let textarea_div = $el("div.easyuse-model-detail-textarea",[ + $el("p",notes?.length>0 ? notes : {textContent:$t('No notes')}), + ]) + if(!notes || notes.length == 0) textarea_div.classList.add('empty') + else textarea_div.classList.remove('empty') + this.info.replaceChildren( + $el("div.easyuse-model-detail",[ + $el("div.easyuse-model-detail-head.flex-b",[ + $el('span',$t("Notes")), + $el("a", { + textContent: editText, + href: "#", + style: { + fontSize: "12px", + float: "right", + color: "var(--warning-color)", + textDecoration: "none", + }, + onclick: async (e) => { + e.preventDefault(); + + if (textarea) { + if(textarea.value != this.customNotes){ + toast.showLoading($t('Saving Notes...')) + this.customNotes = textarea.value; + const resp = await api.fetchApi( + "/easyuse/metadata/notes/" + encodeURIComponent(`${this.type}/${this.name}`), + { + method: "POST", + body: this.customNotes, + } + ); + toast.hideLoading() + if (resp.status !== 200) { + toast.error($t('Saving Failed')) + console.error(resp); + alert(`Error saving notes (${resp.status}) ${resp.statusText}`); + return; + } + toast.success($t('Saving Succeed')) + notes = this.parseNote.call(this) + console.log(notes) + textarea_div.replaceChildren($el("p",notes?.length>0 ? notes : {textContent:$t('No notes')})); + if(textarea.value) textarea_div.classList.remove('empty') + else textarea_div.classList.add('empty') + }else { + textarea_div.replaceChildren($el("p",{textContent:$t('No notes')})); + textarea_div.classList.add('empty') + } + e.target.textContent = editText; + textarea.remove(); + textarea = null; + + } else { + e.target.textContent = "💾 Save"; + textarea = $el("textarea", { + placeholder: $t("Type your notes here"), + style: { + width: "100%", + minWidth: "200px", + minHeight: "50px", + height:"100px" + }, + textContent: this.customNotes, + }); + textarea_div.replaceChildren(textarea); + textarea.focus() + } + } + }) + ]), + textarea_div + ]), + $el("div.easyuse-model-detail",[ + $el("div.easyuse-model-detail-head",{textContent:$t("Details")}), + $el("div.easyuse-model-detail-body",[ + $el("div.easyuse-model-detail-item",[ + $el("div.easyuse-model-detail-item-label",{textContent:$t("Type")}), + $el("div.easyuse-model-detail-item-value",{textContent:info.model.type}), + ]), + $el("div.easyuse-model-detail-item",[ + $el("div.easyuse-model-detail-item-label",{textContent:$t("BaseModel")}), + $el("div.easyuse-model-detail-item-value",{textContent:info.baseModel}), + ]), + $el("div.easyuse-model-detail-item",[ + $el("div.easyuse-model-detail-item-label",{textContent:$t("Download")}), + $el("div.easyuse-model-detail-item-value",{textContent:info.stats?.downloadCount || 0}), + ]), + $el("div.easyuse-model-detail-item",[ + $el("div.easyuse-model-detail-item-label",{textContent:$t("Trained Words")}), + $el("div.easyuse-model-detail-item-value",{textContent:info?.trainedWords.join(',') || '-'}), + ]), + $el("div.easyuse-model-detail-item",[ + $el("div.easyuse-model-detail-item-label",{textContent:$t("Source")}), + $el("div.easyuse-model-detail-item-value",[ + $el("label", [ + $el("img", { + style: { + width: "14px", + position: "relative", + top: "3px", + margin: "0 5px 0 0", + }, + src: "https://civitai.com/favicon.ico", + }), + $el("a", { + href: "https://civitai.com/models/" + info.modelId, + textContent: "View " + info.model.name, + target: "_blank", + }) + ]) + ]), + ]) + ]), + ]) + ); + + if (info.images?.length) { + this.imgCurrent = 0 + this.isSaving = false + info.images.map(cate=> + cate.url && + this.imgList.appendChild( + $el('div.easyuse-preview-slide',[ + $el('div.easyuse-preview-slide-content',[ + $el('img',{src:(cate.url)}), + $el("div.save", { + textContent: "Save as preview", + onclick: async () => { + if(this.isSaving) return + this.isSaving = true + toast.showLoading($t('Saving Preview...')) + // Convert the preview to a blob + const blob = await (await fetch(cate.url)).blob(); + + // Store it in temp + const name = "temp_preview." + new URL(cate.url).pathname.split(".")[1]; + const body = new FormData(); + body.append("image", new File([blob], name)); + body.append("overwrite", "true"); + body.append("type", "temp"); + + const resp = await api.fetchApi("/upload/image", { + method: "POST", + body, + }); + + if (resp.status !== 200) { + this.isSaving = false + toast.error($t('Saving Failed')) + toast.hideLoading() + console.error(resp); + alert(`Error saving preview (${req.status}) ${req.statusText}`); + return; + } + + // Use as preview + await api.fetchApi("/easyuse/save/" + encodeURIComponent(`${this.type}/${this.name}`), { + method: "POST", + body: JSON.stringify({ + filename: name, + type: "temp", + }), + headers: { + "content-type": "application/json", + }, + }).then(_=>{ + toast.success($t('Saving Succeed')) + toast.hideLoading() + }); + this.isSaving = false + app.refreshComboInNodes(); + }, + }) + ]) + ]) + ) + ) + let _this = this + this.imgDistance = (-660 * this.imgCurrent).toString() + this.imgList.style.display = '' + this.imgList.style.transform = 'translate3d(' + this.imgDistance +'px, 0px, 0px)' + this.slides = this.imgList.querySelectorAll('.easyuse-preview-slide') + // 添加按钮 + this.slideLeftButton = $el("button.left",{ + parent: this.imgWrapper, + style:{ + display:info.images.length <= 2 ? 'none' : 'block' + }, + innerHTML:``, + onclick: ()=>{ + if(info.images.length <= 2) return + _this.imgList.classList.remove("no-transition") + if(_this.imgCurrent == 0){ + _this.imgCurrent = (info.images.length/2)-1 + this.slides[this.slides.length-1].style.transform = 'translate3d(' + (-660 * (this.imgCurrent+1)).toString()+'px, 0px, 0px)' + this.slides[this.slides.length-2].style.transform = 'translate3d(' + (-660 * (this.imgCurrent+1)).toString()+'px, 0px, 0px)' + _this.imgList.style.transform = 'translate3d(660px, 0px, 0px)' + setTimeout(_=>{ + this.slides[this.slides.length-1].style.transform = 'translate3d(0px, 0px, 0px)' + this.slides[this.slides.length-2].style.transform = 'translate3d(0px, 0px, 0px)' + _this.imgDistance = (-660 * this.imgCurrent).toString() + _this.imgList.style.transform = 'translate3d(' + _this.imgDistance +'px, 0px, 0px)' + _this.imgList.classList.add("no-transition") + },500) + } + else { + _this.imgCurrent = _this.imgCurrent-1 + _this.imgDistance = (-660 * this.imgCurrent).toString() + _this.imgList.style.transform = 'translate3d(' + _this.imgDistance +'px, 0px, 0px)' + } + } + }) + this.slideRightButton = $el("button.right",{ + parent: this.imgWrapper, + style:{ + display:info.images.length <= 2 ? 'none' : 'block' + }, + innerHTML:``, + onclick: ()=>{ + if(info.images.length <= 2) return + _this.imgList.classList.remove("no-transition") + + if( _this.imgCurrent >= (info.images.length/2)-1){ + _this.imgCurrent = 0 + const max = info.images.length/2 + this.slides[0].style.transform = 'translate3d(' + (660 * max).toString()+'px, 0px, 0px)' + this.slides[1].style.transform = 'translate3d(' + (660 * max).toString()+'px, 0px, 0px)' + _this.imgList.style.transform = 'translate3d(' + (-660 * max).toString()+'px, 0px, 0px)' + setTimeout(_=>{ + this.slides[0].style.transform = 'translate3d(0px, 0px, 0px)' + this.slides[1].style.transform = 'translate3d(0px, 0px, 0px)' + _this.imgDistance = (-660 * this.imgCurrent).toString() + _this.imgList.style.transform = 'translate3d(' + _this.imgDistance +'px, 0px, 0px)' + _this.imgList.classList.add("no-transition") + },500) + } + else { + _this.imgCurrent = _this.imgCurrent+1 + _this.imgDistance = (-660 * this.imgCurrent).toString() + _this.imgList.style.transform = 'translate3d(' + _this.imgDistance +'px, 0px, 0px)' + } + + } + }) + + } + + if(info.description){ + $el("div", { + parent: this.content, + innerHTML: info.description, + style: { + marginTop: "10px", + }, + }); + } + + return info; + }) + .catch((err) => { + this.imgWrapper.style.display = 'none' + content.textContent = "⚠️ " + err.message; + }) + .finally(_=>{ + }) + } +} + + +export class CheckpointInfoDialog extends ModelInfoDialog { + async addInfo() { + // super.addInfo(); + await this.addCivitaiInfo(); + } +} + +const MAX_TAGS = 500 +export class LoraInfoDialog extends ModelInfoDialog { + getTagFrequency() { + if (!this.metadata.ss_tag_frequency) return []; + + const datasets = JSON.parse(this.metadata.ss_tag_frequency); + const tags = {}; + for (const setName in datasets) { + const set = datasets[setName]; + for (const t in set) { + if (t in tags) { + tags[t] += set[t]; + } else { + tags[t] = set[t]; + } + } + } + + return Object.entries(tags).sort((a, b) => b[1] - a[1]); + } + + getResolutions() { + let res = []; + if (this.metadata.ss_bucket_info) { + const parsed = JSON.parse(this.metadata.ss_bucket_info); + if (parsed?.buckets) { + for (const { resolution, count } of Object.values(parsed.buckets)) { + res.push([count, `${resolution.join("x")} * ${count}`]); + } + } + } + res = res.sort((a, b) => b[0] - a[0]).map((a) => a[1]); + let r = this.metadata.ss_resolution; + if (r) { + const s = r.split(","); + const w = s[0].replace("(", ""); + const h = s[1].replace(")", ""); + res.push(`${w.trim()}x${h.trim()} (Base res)`); + } else if ((r = this.metadata["modelspec.resolution"])) { + res.push(r + " (Base res"); + } + if (!res.length) { + res.push("⚠️ Unknown"); + } + return res; + } + + getTagList(tags) { + return tags.map((t) => + $el( + "li.easyuse-model-tag", + { + dataset: { + tag: t[0], + }, + $: (el) => { + el.onclick = () => { + el.classList.toggle("easyuse-model-tag--selected"); + }; + }, + }, + [ + $el("p", { + textContent: t[0], + }), + $el("span", { + textContent: t[1], + }), + ] + ) + ); + } + + addTags() { + let tags = this.getTagFrequency(); + let hasMore; + if (tags?.length) { + const c = tags.length; + let list; + if (c > MAX_TAGS) { + tags = tags.slice(0, MAX_TAGS); + hasMore = $el("p", [ + $el("span", { textContent: `⚠️ Only showing first ${MAX_TAGS} tags ` }), + $el("a", { + href: "#", + textContent: `Show all ${c}`, + onclick: () => { + list.replaceChildren(...this.getTagList(this.getTagFrequency())); + hasMore.remove(); + }, + }), + ]); + } + list = $el("ol.easyuse-model-tags-list", this.getTagList(tags)); + this.tags = $el("div", [list]); + } else { + this.tags = $el("p", { textContent: "⚠️ No tag frequency metadata found" }); + } + + this.content.append(this.tags); + + if (hasMore) { + this.content.append(hasMore); + } + } + + async addInfo() { + // this.addInfoEntry("Name", this.metadata.ss_output_name || "⚠️ Unknown"); + // this.addInfoEntry("Base Model", this.metadata.ss_sd_model_name || "⚠️ Unknown"); + // this.addInfoEntry("Clip Skip", this.metadata.ss_clip_skip || "⚠️ Unknown"); + // + // this.addInfoEntry( + // "Resolution", + // $el( + // "select", + // this.getResolutions().map((r) => $el("option", { textContent: r })) + // ) + // ); + + // super.addInfo(); + const p = this.addCivitaiInfo(); + this.addTags(); + + const info = await p; + if (info) { + // $el( + // "p", + // { + // parent: this.content, + // textContent: "Trained Words: ", + // }, + // [ + // $el("pre", { + // textContent: info.trainedWords.join(", "), + // style: { + // whiteSpace: "pre-wrap", + // margin: "10px 0", + // background: "#222", + // padding: "5px", + // borderRadius: "5px", + // maxHeight: "250px", + // overflow: "auto", + // }, + // }), + // ] + // ); + $el("div", { + parent: this.content, + innerHTML: info.description, + style: { + maxHeight: "250px", + overflow: "auto", + }, + }); + } + } + + createButtons() { + const btns = super.createButtons(); + + function copyTags(e, tags) { + const textarea = $el("textarea", { + parent: document.body, + style: { + position: "fixed", + }, + textContent: tags.map((el) => el.dataset.tag).join(", "), + }); + textarea.select(); + try { + document.execCommand("copy"); + if (!e.target.dataset.text) { + e.target.dataset.text = e.target.textContent; + } + e.target.textContent = "Copied " + tags.length + " tags"; + setTimeout(() => { + e.target.textContent = e.target.dataset.text; + }, 1000); + } catch (ex) { + prompt("Copy to clipboard: Ctrl+C, Enter", text); + } finally { + document.body.removeChild(textarea); + } + } + + btns.unshift( + $el("button", { + type: "button", + textContent: "Copy Selected", + onclick: (e) => { + copyTags(e, [...this.tags.querySelectorAll(".easyuse-model-tag--selected")]); + }, + }), + $el("button", { + type: "button", + textContent: "Copy All", + onclick: (e) => { + copyTags(e, [...this.tags.querySelectorAll(".easyuse-model-tag")]); + }, + }) + ); + + return btns; + } +} \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/common/toast.js b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/common/toast.js new file mode 100644 index 0000000000000000000000000000000000000000..f32ab5e41a4a4212ca25894f94e234514249c6c5 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/common/toast.js @@ -0,0 +1,127 @@ +import {sleep} from "./utils.js"; +import {$t} from "./i18n.js"; + +class Toast{ + + constructor() { + this.info_icon = `` + this.success_icon = `` + this.error_icon = `` + this.warn_icon = `` + this.loading_icon = `` + } + + async showToast(data){ + let container = document.querySelector(".easyuse-toast-container"); + if (!container) { + container = document.createElement("div"); + container.classList.add("easyuse-toast-container"); + document.body.appendChild(container); + } + await this.hideToast(data.id); + const toastContainer = document.createElement("div"); + const content = document.createElement("span"); + content.innerHTML = data.content; + toastContainer.appendChild(content); + for (let a = 0; a < (data.actions || []).length; a++) { + const action = data.actions[a]; + if (a > 0) { + const sep = document.createElement("span"); + sep.innerHTML = " | "; + toastContainer.appendChild(sep); + } + const actionEl = document.createElement("a"); + actionEl.innerText = action.label; + if (action.href) { + actionEl.target = "_blank"; + actionEl.href = action.href; + } + if (action.callback) { + actionEl.onclick = (e) => { + return action.callback(e); + }; + } + toastContainer.appendChild(actionEl); + } + const animContainer = document.createElement("div"); + animContainer.setAttribute("toast-id", data.id); + animContainer.appendChild(toastContainer); + container.appendChild(animContainer); + await sleep(64); + animContainer.style.marginTop = `-${animContainer.offsetHeight}px`; + await sleep(64); + animContainer.classList.add("-show"); + if (data.duration) { + await sleep(data.duration); + this.hideToast(data.id); + } + } + async hideToast(id) { + const msg = document.querySelector(`.easyuse-toast-container > [toast-id="${id}"]`); + if (msg === null || msg === void 0 ? void 0 : msg.classList.contains("-show")) { + msg.classList.remove("-show"); + await sleep(750); + } + msg && msg.remove(); + } + async clearAllMessages() { + let container = document.querySelector(".easyuse-toast-container"); + container && (container.innerHTML = ""); + } + + async copyright(duration = 5000, actions = []) { + this.showToast({ + id: `toast-info`, + content: `${this.info_icon} ${$t('Workflow created by')} Yolain , ${$t('Watch more video content')} B站乱乱呀`, + duration, + actions + }); + } + async info(content, duration = 3000, actions = []) { + this.showToast({ + id: `toast-info`, + content: `${this.info_icon} ${content}`, + duration, + actions + }); + } + async success(content, duration = 3000, actions = []) { + this.showToast({ + id: `toast-success`, + content: `${this.success_icon} ${content}`, + duration, + actions + }); + } + async error(content, duration = 3000, actions = []) { + this.showToast({ + id: `toast-error`, + content: `${this.error_icon} ${content}`, + duration, + actions + }); + } + async warn(content, duration = 3000, actions = []) { + this.showToast({ + id: `toast-warn`, + content: `${this.warn_icon} ${content}`, + duration, + actions + }); + } + async showLoading(content, duration = 0, actions = []) { + this.showToast({ + id: `toast-loading`, + content: `${this.loading_icon} ${content}`, + duration, + actions + }); + } + + async hideLoading() { + this.hideToast("toast-loading"); + } + +} + +export const toast = new Toast(); diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/common/utils.js b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/common/utils.js new file mode 100644 index 0000000000000000000000000000000000000000..9a988a7a85da6fc5fe35e7bd43bcca4495c62adc --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/common/utils.js @@ -0,0 +1,187 @@ +export function sleep(ms = 100, value) { + return new Promise((resolve) => { + setTimeout(() => { + resolve(value); + }, ms); + }); +} +export function addPreconnect(href, crossorigin=false){ + const preconnect = document.createElement("link"); + preconnect.rel = 'preconnect' + preconnect.href = href + if(crossorigin) preconnect.crossorigin = '' + document.head.appendChild(preconnect); +} +export function addCss(href, base=true) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.type = "text/css"; + link.href = base ? "extensions/ComfyUI-Easy-Use/"+href : href; + document.head.appendChild(link); +} + +export function addMeta(name, content) { + const meta = document.createElement("meta"); + meta.setAttribute("name", name); + meta.setAttribute('content', content); + document.head.appendChild(meta); +} + +export function deepEqual(obj1, obj2) { + if (typeof obj1 !== typeof obj2) { + return false + } + if (typeof obj1 !== 'object' || obj1 === null || obj2 === null) { + return obj1 === obj2 + } + const keys1 = Object.keys(obj1) + const keys2 = Object.keys(obj2) + if (keys1.length !== keys2.length) { + return false + } + for (let key of keys1) { + if (!deepEqual(obj1[key], obj2[key])) { + return false + } + } + return true +} + + +export function getLocale(){ + const locale = localStorage['AGL.Locale'] || localStorage['Comfy.Settings.AGL.Locale'] || 'en-US' + return locale +} + +export function spliceExtension(fileName){ + return fileName.substring(0,fileName.lastIndexOf('.')) +} +export function getExtension(fileName){ + return fileName.substring(fileName.lastIndexOf('.') + 1) +} + +export function formatTime(time, format) { + time = typeof (time) === "number" ? time : (time instanceof Date ? time.getTime() : parseInt(time)); + if (isNaN(time)) return null; + if (typeof (format) !== 'string' || !format) format = 'yyyy-MM-dd hh:mm:ss'; + let _time = new Date(time); + time = _time.toString().split(/[\s\:]/g).slice(0, -2); + time[1] = ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12'][_time.getMonth()]; + let _mapping = { + MM: 1, + dd: 2, + yyyy: 3, + hh: 4, + mm: 5, + ss: 6 + }; + return format.replace(/([Mmdhs]|y{2})\1/g, (key) => time[_mapping[key]]); +} + + +let origProps = {}; +export const findWidgetByName = (node, name) => node.widgets.find((w) => w.name === name); + +export const doesInputWithNameExist = (node, name) => node.inputs ? node.inputs.some((input) => input.name === name) : false; + +export function updateNodeHeight(node) {node.setSize([node.size[0], node.computeSize()[1]]);} + +export function toggleWidget(node, widget, show = false, suffix = "") { + if (!widget || doesInputWithNameExist(node, widget.name)) return; + if (!origProps[widget.name]) { + origProps[widget.name] = { origType: widget.type, origComputeSize: widget.computeSize }; + } + const origSize = node.size; + + widget.type = show ? origProps[widget.name].origType : "easyHidden" + suffix; + widget.computeSize = show ? origProps[widget.name].origComputeSize : () => [0, -4]; + + widget.linkedWidgets?.forEach(w => toggleWidget(node, w, ":" + widget.name, show)); + + const height = show ? Math.max(node.computeSize()[1], origSize[1]) : node.size[1]; + node.setSize([node.size[0], height]); +} + +export function isLocalNetwork(ip) { + const localNetworkRanges = [ + '192.168.', + '10.', + '127.', + /^172\.((1[6-9]|2[0-9]|3[0-1])\.)/ + ]; + + return localNetworkRanges.some(range => { + if (typeof range === 'string') { + return ip.startsWith(range); + } else { + return range.test(ip); + } + }); +} + + +/** +* accAdd 高精度加法 +* @since 1.0.10 +* @param {Number} arg1 +* @param {Number} arg2 +* @return {Number} +*/ +export function accAdd(arg1, arg2) { + let r1, r2, s1, s2,max; + s1 = typeof arg1 == 'string' ? arg1 : arg1.toString() + s2 = typeof arg2 == 'string' ? arg2 : arg2.toString() + try { r1 = s1.split(".")[1].length } catch (e) { r1 = 0 } + try { r2 = s2.split(".")[1].length } catch (e) { r2 = 0 } + max = Math.pow(10, Math.max(r1, r2)) + return (arg1 * max + arg2 * max) / max +} +/** + * accSub 高精度减法 + * @since 1.0.10 + * @param {Number} arg1 + * @param {Number} arg2 + * @return {Number} + */ +export function accSub(arg1, arg2) { + let r1, r2, max, min,s1,s2; + s1 = typeof arg1 == 'string' ? arg1 : arg1.toString() + s2 = typeof arg2 == 'string' ? arg2 : arg2.toString() + try { r1 = s1.split(".")[1].length } catch (e) { r1 = 0 } + try { r2 = s2.split(".")[1].length } catch (e) { r2 = 0 } + max = Math.pow(10, Math.max(r1, r2)); + //动态控制精度长度 + min = (r1 >= r2) ? r1 : r2; + return ((arg1 * max - arg2 * max) / max).toFixed(min) +} +/** + * accMul 高精度乘法 + * @since 1.0.10 + * @param {Number} arg1 + * @param {Number} arg2 + * @return {Number} + */ +export function accMul(arg1, arg2) { + let max = 0, s1 = typeof arg1 == 'string' ? arg1 : arg1.toString(), s2 = typeof arg2 == 'string' ? arg2 : arg2.toString(); + try { max += s1.split(".")[1].length } catch (e) { } + try { max += s2.split(".")[1].length } catch (e) { } + return Number(s1.replace(".", "")) * Number(s2.replace(".", "")) / Math.pow(10, max) +} +/** + * accDiv 高精度除法 + * @since 1.0.10 + * @param {Number} arg1 + * @param {Number} arg2 + * @return {Number} + */ +export function accDiv(arg1, arg2) { + let t1 = 0, t2 = 0, r1, r2,s1 = typeof arg1 == 'string' ? arg1 : arg1.toString(), s2 = typeof arg2 == 'string' ? arg2 : arg2.toString(); + try { t1 = s1.toString().split(".")[1].length } catch (e) { } + try { t2 = s2.toString().split(".")[1].length } catch (e) { } + r1 = Number(s1.toString().replace(".", "")) + r2 = Number(s2.toString().replace(".", "")) + return (r1 / r2) * Math.pow(10, t2 - t1) +} +Number.prototype.div = function (arg) { + return accDiv(this, arg); +} \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/easy/easy.js b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/easy/easy.js new file mode 100644 index 0000000000000000000000000000000000000000..b22d2fb2c4830ac0a8be8b29a0b0b64b9e70969e --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/easy/easy.js @@ -0,0 +1,610 @@ +import { api } from "../../../../scripts/api.js"; +import { app } from "../../../../scripts/app.js"; +import {deepEqual, addCss, addMeta, isLocalNetwork} from "../common/utils.js"; +import {logoIcon, quesitonIcon, rocketIcon, groupIcon, rebootIcon, closeIcon} from "../common/icon.js"; +import {$t} from '../common/i18n.js'; +import {toast} from "../common/toast.js"; +import {$el, ComfyDialog} from "../../../../scripts/ui.js"; + + +addCss('css/index.css') + +api.addEventListener("easyuse-toast",event=>{ + const content = event.detail.content + const type = event.detail.type + const duration = event.detail.duration + if(!type){ + toast.info(content, duration) + } + else{ + toast.showToast({ + id: `toast-${type}`, + content: `${toast[type+"_icon"]} ${content}`, + duration: duration || 3000, + }) + } +}) + + +let draggerEl = null +let isGroupMapcanMove = true +function createGroupMap(){ + let div = document.querySelector('#easyuse_groups_map') + if(div){ + div.style.display = div.style.display == 'none' ? 'flex' : 'none' + return + } + let groups = app.canvas.graph._groups + let nodes = app.canvas.graph._nodes + let old_nodes = groups.length + div = document.createElement('div') + div.id = 'easyuse_groups_map' + div.innerHTML = '' + let btn = document.createElement('div') + btn.style = `display: flex; + width: calc(100% - 8px); + justify-content: space-between; + align-items: center; + padding: 0 6px; + height: 44px;` + let hideBtn = $el('button.closeBtn',{ + innerHTML:closeIcon, + onclick:_=>div.style.display = 'none' + }) + let textB = document.createElement('p') + btn.appendChild(textB) + btn.appendChild(hideBtn) + textB.style.fontSize = '11px' + textB.innerHTML = `${$t('Groups Map')} (EasyUse)` + div.appendChild(btn) + + div.addEventListener('mousedown', function (e) { + var startX = e.clientX + var startY = e.clientY + var offsetX = div.offsetLeft + var offsetY = div.offsetTop + + function moveBox (e) { + var newX = e.clientX + var newY = e.clientY + var deltaX = newX - startX + var deltaY = newY - startY + div.style.left = offsetX + deltaX + 'px' + div.style.top = offsetY + deltaY + 'px' + } + + function stopMoving () { + document.removeEventListener('mousemove', moveBox) + document.removeEventListener('mouseup', stopMoving) + } + + if(isGroupMapcanMove){ + document.addEventListener('mousemove', moveBox) + document.addEventListener('mouseup', stopMoving) + } + }) + + function updateGroups(groups, groupsDiv, autoSortDiv){ + if(groups.length>0){ + autoSortDiv.style.display = 'block' + }else autoSortDiv.style.display = 'none' + for (let index in groups) { + const group = groups[index] + const title = group.title + const show_text = $t('Always') + const hide_text = $t('Bypass') + const mute_text = $t('Never') + let group_item = document.createElement('div') + let group_item_style = `justify-content: space-between;display:flex;background-color: var(--comfy-input-bg);border-radius: 5px;border:1px solid var(--border-color);margin-top:5px;` + group_item.addEventListener("mouseover",event=>{ + event.preventDefault() + group_item.style = group_item_style + "filter:brightness(1.2);" + }) + group_item.addEventListener("mouseleave",event=>{ + event.preventDefault() + group_item.style = group_item_style + "filter:brightness(1);" + }) + group_item.addEventListener("dragstart",e=>{ + draggerEl = e.currentTarget; + e.currentTarget.style.opacity = "0.6"; + e.currentTarget.style.border = "1px dashed yellow"; + e.dataTransfer.effectAllowed = 'move'; + e.dataTransfer.setDragImage(emptyImg, 0, 0); + }) + group_item.addEventListener("dragend",e=>{ + e.target.style.opacity = "1"; + e.currentTarget.style.border = "1px dashed transparent"; + e.currentTarget.removeAttribute("draggable"); + document.querySelectorAll('.easyuse-group-item').forEach((el,i) => { + var prev_i = el.dataset.id; + if (el == draggerEl && prev_i != i ) { + groups.splice(i, 0, groups.splice(prev_i, 1)[0]); + } + el.dataset.id = i; + }); + isGroupMapcanMove = true + }) + group_item.addEventListener("dragover",e=>{ + e.preventDefault(); + if (e.currentTarget == draggerEl) return; + let rect = e.currentTarget.getBoundingClientRect(); + if (e.clientY > rect.top + rect.height / 2) { + e.currentTarget.parentNode.insertBefore(draggerEl, e.currentTarget.nextSibling); + } else { + e.currentTarget.parentNode.insertBefore(draggerEl, e.currentTarget); + } + isGroupMapcanMove = true + }) + + + group_item.setAttribute('data-id',index) + group_item.className = 'easyuse-group-item' + group_item.style = group_item_style + // 标题 + let text_group_title = document.createElement('div') + text_group_title.style = `flex:1;font-size:12px;color:var(--input-text);padding:4px;white-space: nowrap;overflow: hidden;text-overflow: ellipsis;cursor:pointer` + text_group_title.innerHTML = `${title}` + text_group_title.addEventListener('mousedown',e=>{ + isGroupMapcanMove = false + e.currentTarget.parentNode.draggable = 'true'; + }) + text_group_title.addEventListener('mouseleave',e=>{ + setTimeout(_=>{ + isGroupMapcanMove = true + },150) + }) + group_item.append(text_group_title) + // 按钮组 + let buttons = document.createElement('div') + group.recomputeInsideNodes(); + const nodesInGroup = group._nodes; + let isGroupShow = nodesInGroup && nodesInGroup.length>0 && nodesInGroup[0].mode == 0 + let isGroupMute = nodesInGroup && nodesInGroup.length>0 && nodesInGroup[0].mode == 2 + let go_btn = document.createElement('button') + go_btn.style = "margin-right:6px;cursor:pointer;font-size:10px;padding:2px 4px;color:var(--input-text);background-color: var(--comfy-input-bg);border: 1px solid var(--border-color);border-radius:4px;" + go_btn.innerText = "Go" + go_btn.addEventListener('click', () => { + app.canvas.ds.offset[0] = -group.pos[0] - group.size[0] * 0.5 + (app.canvas.canvas.width * 0.5) / app.canvas.ds.scale; + app.canvas.ds.offset[1] = -group.pos[1] - group.size[1] * 0.5 + (app.canvas.canvas.height * 0.5) / app.canvas.ds.scale; + app.canvas.setDirty(true, true); + app.canvas.setZoom(1) + }) + buttons.append(go_btn) + let see_btn = document.createElement('button') + let defaultStyle = `cursor:pointer;font-size:10px;;padding:2px;border: 1px solid var(--border-color);border-radius:4px;width:36px;` + see_btn.style = isGroupMute ? `background-color:var(--error-text);color:var(--input-text);` + defaultStyle : (isGroupShow ? `background-color:var(--theme-color);color:var(--input-text);` + defaultStyle : `background-color: var(--comfy-input-bg);color:var(--descrip-text);` + defaultStyle) + see_btn.innerText = isGroupMute ? mute_text : (isGroupShow ? show_text : hide_text) + let pressTimer + let firstTime =0, lastTime =0 + let isHolding = false + see_btn.addEventListener('click', () => { + if(isHolding){ + isHolding = false + return + } + for (const node of nodesInGroup) { + node.mode = isGroupShow ? 4 : 0; + node.graph.change(); + } + isGroupShow = nodesInGroup[0].mode == 0 ? true : false + isGroupMute = nodesInGroup[0].mode == 2 ? true : false + see_btn.style = isGroupMute ? `background-color:var(--error-text);color:var(--input-text);` + defaultStyle : (isGroupShow ? `background-color:#006691;color:var(--input-text);` + defaultStyle : `background-color: var(--comfy-input-bg);color:var(--descrip-text);` + defaultStyle) + see_btn.innerText = isGroupMute ? mute_text : (isGroupShow ? show_text : hide_text) + }) + see_btn.addEventListener('mousedown', () => { + firstTime = new Date().getTime(); + clearTimeout(pressTimer); + pressTimer = setTimeout(_=>{ + for (const node of nodesInGroup) { + node.mode = isGroupMute ? 0 : 2; + node.graph.change(); + } + isGroupShow = nodesInGroup[0].mode == 0 ? true : false + isGroupMute = nodesInGroup[0].mode == 2 ? true : false + see_btn.style = isGroupMute ? `background-color:var(--error-text);color:var(--input-text);` + defaultStyle : (isGroupShow ? `background-color:#006691;color:var(--input-text);` + defaultStyle : `background-color: var(--comfy-input-bg);color:var(--descrip-text);` + defaultStyle) + see_btn.innerText = isGroupMute ? mute_text : (isGroupShow ? show_text : hide_text) + },500) + }) + see_btn.addEventListener('mouseup', () => { + lastTime = new Date().getTime(); + if(lastTime - firstTime > 500) isHolding = true + clearTimeout(pressTimer); + }) + buttons.append(see_btn) + group_item.append(buttons) + + groupsDiv.append(group_item) + } + + } + + let groupsDiv = document.createElement('div') + groupsDiv.id = 'easyuse-groups-items' + groupsDiv.style = `overflow-y: auto;max-height: 400px;height:100%;width: 100%;` + + let autoSortDiv = document.createElement('button') + autoSortDiv.style = `cursor:pointer;font-size:10px;padding:2px 4px;color:var(--input-text);background-color: var(--comfy-input-bg);border: 1px solid var(--border-color);border-radius:4px;` + autoSortDiv.innerText = $t('Auto Sorting') + autoSortDiv.addEventListener('click',e=>{ + e.preventDefault() + groupsDiv.innerHTML = `` + let new_groups = groups.sort((a,b)=> a['pos'][0] - b['pos'][0]).sort((a,b)=> a['pos'][1] - b['pos'][1]) + updateGroups(new_groups, groupsDiv, autoSortDiv) + }) + + updateGroups(groups, groupsDiv, autoSortDiv) + + div.appendChild(groupsDiv) + + let remarkDiv = document.createElement('p') + remarkDiv.style = `text-align:center; font-size:10px; padding:0 10px;color:var(--descrip-text)` + remarkDiv.innerText = $t('Toggle `Show/Hide` can set mode of group, LongPress can set group nodes to never') + div.appendChild(groupsDiv) + div.appendChild(remarkDiv) + div.appendChild(autoSortDiv) + + let graphDiv = document.getElementById("graph-canvas") + graphDiv.addEventListener('mouseover', async () => { + groupsDiv.innerHTML = `` + let new_groups = app.canvas.graph._groups + updateGroups(new_groups, groupsDiv, autoSortDiv) + old_nodes = nodes + }) + + if (!document.querySelector('#easyuse_groups_map')){ + document.body.appendChild(div) + }else{ + div.style.display = 'flex' + } + +} + +async function cleanup(){ + try { + const {Running, Pending} = await api.getQueue() + if(Running.length>0 || Pending.length>0){ + toast.error($t("Clean Failed")+ ":"+ $t("Please stop all running tasks before cleaning GPU")) + return + } + api.fetchApi("/easyuse/cleangpu",{ + method:"POST" + }).then(res=>{ + if(res.status == 200){ + toast.success($t("Clean SuccessFully")) + }else{ + toast.error($t("Clean Failed")) + } + }) + + } catch (exception) {} +} + + +let guideDialog = null +let isDownloading = false +function download_model(url,local_dir){ + if(isDownloading || !url || !local_dir) return + isDownloading = true + let body = new FormData(); + body.append('url', url); + body.append('local_dir', local_dir); + api.fetchApi("/easyuse/model/download",{ + method:"POST", + body + }).then(res=>{ + if(res.status == 200){ + toast.success($t("Download SuccessFully")) + }else{ + toast.error($t("Download Failed")) + } + isDownloading = false + }) + +} +class GuideDialog { + + constructor(note, need_models){ + this.dialogDiv = null + this.modelsDiv = null + + if(need_models?.length>0){ + let tbody = [] + + for(let i=0;idownload_model(need_models[i]['download_url'],need_models[i]['local_dir']), target:"_blank", textContent:$t('Download Model')}) : '', + need_models[i]['source_url'] ? $el('a',{href:need_models[i]['source_url'], target:"_blank", textContent:$t('Source Url')}) : '', + need_models[i]['desciption'] ? $el('span',{textContent:need_models[i]['desciption']}) : '', + ]), + ])) + } + this.modelsDiv = $el('div.easyuse-guide-dialog-models.markdown-body',[ + $el('h3',{textContent:$t('Models Required')}), + $el('table',{cellpadding:0,cellspacing:0},[ + $el('thead',[ + $el('tr',[ + $el('th',{innerHTML:$t('ModelName')}), + $el('th',{innerHTML:$t('Description')}), + ]) + ]), + $el('tbody',tbody) + ]) + ]) + } + + this.dialogDiv = $el('div.easyuse-guide-dialog.hidden',[ + $el('div.easyuse-guide-dialog-header',[ + $el('div.easyuse-guide-dialog-top',[ + $el('div.easyuse-guide-dialog-title',{ + innerHTML:$t('Workflow Guide') + }), + $el('button.closeBtn',{innerHTML:closeIcon,onclick:_=>this.close()}) + ]), + + $el('div.easyuse-guide-dialog-remark',{ + innerHTML:`${$t('Workflow created by')} Yolain , ${$t('Watch more video content')} B站乱乱呀` + }) + ]), + $el('div.easyuse-guide-dialog-content.markdown-body',[ + $el('div.easyuse-guide-dialog-note',{ + innerHTML:note + }), + ...this.modelsDiv ? [this.modelsDiv] : [] + ]) + ]) + + if(disableRenderInfo){ + this.dialogDiv.classList.add('disable-render-info') + } + document.body.appendChild(this.dialogDiv) + } + show(){ + if(this.dialogDiv) this.dialogDiv.classList.remove('hidden') + } + + close(){ + if(this.dialogDiv){ + this.dialogDiv.classList.add('hidden') + } + } + toggle(){ + if(this.dialogDiv){ + if(this.dialogDiv.classList.contains('hidden')){ + this.show() + }else{ + this.close() + } + } + } + + remove(){ + if(this.dialogDiv) document.body.removeChild(this.dialogDiv) + } +} + +// toolbar +const toolBarId = "Comfy.EasyUse.toolBar" +const getEnableToolBar = _ => app.ui.settings.getSettingValue(toolBarId, true) +const getNewMenuPosition = _ => { + try{ + return app.ui.settings.getSettingValue('Comfy.UseNewMenu', 'Disabled') + }catch (e){ + return 'Disabled' + } +} + +let note = null +let toolbar = null +let enableToolBar = getEnableToolBar() && getNewMenuPosition() == 'Disabled' +let disableRenderInfo = localStorage['Comfy.Settings.Comfy.EasyUse.disableRenderInfo'] ? true : false +export function addToolBar(app) { + app.ui.settings.addSetting({ + id: toolBarId, + name: $t("Enable tool bar fixed on the left-bottom (ComfyUI-Easy-Use)"), + type: "boolean", + defaultValue: enableToolBar, + onChange(value) { + enableToolBar = !!value; + if(enableToolBar){ + showToolBar() + }else hideToolBar() + }, + }); +} +function showToolBar(){ + if(toolbar) toolbar.style.display = 'flex' +} +function hideToolBar(){ + if(toolbar) toolbar.style.display = 'none' +} +let monitor = null +function setCrystoolsUI(position){ + const crystools = document.getElementById('crystools-root')?.children || null + if(crystools?.length>0){ + if(!monitor){ + for (let i = 0; i < crystools.length; i++) { + if (crystools[i].id === 'crystools-monitor-container') { + monitor = crystools[i]; + break; + } + } + } + if(monitor){ + if(position == 'Disabled'){ + let replace = true + for (let i = 0; i < crystools.length; i++) { + if (crystools[i].id === 'crystools-monitor-container') { + replace = false + break; + } + } + document.getElementById('crystools-root').appendChild(monitor) + } + else { + let monitor_div = document.getElementById('comfyui-menu-monitor') + if(!monitor_div) app.menu.settingsGroup.element.before($el('div',{id:'comfyui-menu-monitor'},monitor)) + else monitor_div.appendChild(monitor) + } + } + } +} +const changeNewMenuPosition = app.ui.settings.settingsLookup?.['Comfy.UseNewMenu'] +if(changeNewMenuPosition) changeNewMenuPosition.onChange = v => { + v == 'Disabled' ? showToolBar() : hideToolBar() + setCrystoolsUI(v) +} + + + +app.registerExtension({ + name: "comfy.easyUse", + init() { + // Canvas Menu + const getCanvasMenuOptions = LGraphCanvas.prototype.getCanvasMenuOptions; + LGraphCanvas.prototype.getCanvasMenuOptions = function () { + const options = getCanvasMenuOptions.apply(this, arguments); + let emptyImg = new Image() + emptyImg.src = "data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs="; + + options.push(null, + // Groups Map + { + content: groupIcon.replace('currentColor','var(--warning-color)') + ' '+ $t('Groups Map') + ' (EasyUse)', + callback: async() => { + createGroupMap() + } + }, + // Force clean ComfyUI GPU Used 强制卸载模型GPU占用 + { + content: rocketIcon.replace('currentColor','var(--theme-color-light)') + ' '+ $t('Cleanup Of GPU Usage') + ' (EasyUse)', + callback: async() =>{ + await cleanup() + } + }, + // Only show the reboot option if the server is running on a local network 仅在本地或局域网环境可重启服务 + isLocalNetwork(window.location.host) ? { + content: rebootIcon.replace('currentColor','var(--error-color)') + ' '+ $t('Reboot ComfyUI') + ' (EasyUse)', + callback: _ =>{ + if (confirm($t("Are you sure you'd like to reboot the server?"))){ + try { + api.fetchApi("/easyuse/reboot"); + } catch (exception) {} + } + } + } : null, + ); + return options; + }; + + let renderInfoEvent = LGraphCanvas.prototype.renderInfo + if(disableRenderInfo){ + LGraphCanvas.prototype.renderInfo = function (ctx, x, y) {} + } + + if(!toolbar){ + toolbar = $el('div.easyuse-toolbar',[ + $el('div.easyuse-toolbar-item',{ + onclick:_=>{ + createGroupMap() + } + },[ + $el('div.easyuse-toolbar-icon.group', {innerHTML:groupIcon}), + $el('div.easyuse-toolbar-tips',$t('Groups Map')) + ]), + $el('div.easyuse-toolbar-item',{ + onclick:async()=>{ + await cleanup() + } + },[ + $el('div.easyuse-toolbar-icon.rocket',{innerHTML:rocketIcon}), + $el('div.easyuse-toolbar-tips',$t('Cleanup Of GPU Usage')) + ]), + ]) + if(disableRenderInfo){ + toolbar.classList.add('disable-render-info') + }else{ + toolbar.classList.remove('disable-render-info') + } + document.body.appendChild(toolbar) + } + + // rewrite handleFile + let loadGraphDataEvent = app.loadGraphData + app.loadGraphData = async function (data, clean=true) { + // if(data?.extra?.cpr){ + // toast.copyright() + // } + if(data?.extra?.note){ + if(guideDialog) { + guideDialog.remove() + guideDialog = null + } + if(note && toolbar) toolbar.removeChild(note) + const need_models = data.extra?.need_models || null + guideDialog = new GuideDialog(data.extra.note, need_models) + note = $el('div.easyuse-toolbar-item',{ + onclick:async()=>{ + guideDialog.toggle() + } + },[ + $el('div.easyuse-toolbar-icon.question',{innerHTML:quesitonIcon}), + $el('div.easyuse-toolbar-tips',$t('Workflow Guide')) + ]) + if(toolbar) toolbar.insertBefore(note, toolbar.firstChild) + } + else{ + if(note) { + toolbar.removeChild(note) + note = null + } + } + return await loadGraphDataEvent.apply(this, [...arguments]) + } + + addToolBar(app) + }, + async setup() { + // New style menu button + if(app.menu?.actionsGroup){ + const groupMap = new (await import('../../../../scripts/ui/components/button.js')).ComfyButton({ + icon:'list-box', + action:()=> createGroupMap(), + tooltip: "EasyUse Group Map", + // content: "EasyUse Group Map", + classList: "comfyui-button comfyui-menu-mobile-collapse" + }); + app.menu.actionsGroup.element.after(groupMap.element); + const position = getNewMenuPosition() + setCrystoolsUI(position) + if(position == 'Disabled') showToolBar() + else hideToolBar() + // const easyNewMenu = $el('div.easyuse-new-menu',[ + // $el('div.easyuse-new-menu-intro',[ + // $el('div.easyuse-new-menu-logo',{innerHTML:logoIcon}), + // $el('div.easyuse-new-menu-title',[ + // $el('div.title',{textContent:'ComfyUI-Easy-Use'}), + // $el('div.desc',{textContent:'Version:'}) + // ]) + // ]) + // ]) + // app.menu?.actionsGroup.element.after(new (await import('../../../../scripts/ui/components/splitButton.js')).ComfySplitButton({ + // primary: groupMap, + // mode:'click', + // position:'absolute', + // horizontal: 'right' + // },easyNewMenu).element); + } + + }, + beforeRegisterNodeDef(nodeType, nodeData, app) { + if (nodeData.name.startsWith("easy")) { + const origOnConfigure = nodeType.prototype.onConfigure; + nodeType.prototype.onConfigure = function () { + const r = origOnConfigure ? origOnConfigure.apply(this, arguments) : undefined; + return r; + }; + } + }, +}); \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/easy/easyAccount.js b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/easy/easyAccount.js new file mode 100644 index 0000000000000000000000000000000000000000..421432fa190616a84dff5a5b8cc482c5c14e4580 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/easy/easyAccount.js @@ -0,0 +1,283 @@ +import { app } from "../../../../scripts/app.js"; +import { api } from "../../../../scripts/api.js"; +import { $el, ComfyDialog } from "../../../../scripts/ui.js"; +import { $t } from '../common/i18n.js' +import { toast } from "../common/toast.js"; +import {sleep, accSub} from "../common/utils.js"; + +let api_keys = [] +let api_current = 0 +let user_info = {} + +const api_cost = { + 'sd3': 6.5, + 'sd3-turbo': 4, +} + +class AccountDialog extends ComfyDialog { + constructor() { + super(); + this.lists = [] + this.dialog_div = null + this.user_div = null + } + + addItem(index, user_div){ + return $el('div.easyuse-account-dialog-item',[ + $el('input',{type:'text',placeholder:'Enter name',oninput: e=>{ + const dataIndex = Array.prototype.indexOf.call(this.dialog_div.querySelectorAll('.easyuse-account-dialog-item'), e.target.parentNode) + api_keys[dataIndex]['name'] = e.target.value + },value:api_keys[index]['name']}), + $el('input.key',{type:'text',oninput: e=>{ + const dataIndex = Array.prototype.indexOf.call(this.dialog_div.querySelectorAll('.easyuse-account-dialog-item'), e.target.parentNode) + api_keys[dataIndex]['key'] = e.target.value + },placeholder:'Enter APIKEY', value:api_keys[index]['key']}), + $el('button.choose',{textContent:$t('Choose'),onclick:async(e)=>{ + const dataIndex = Array.prototype.indexOf.call(this.dialog_div.querySelectorAll('.easyuse-account-dialog-item'), e.target.parentNode) + let name = api_keys[dataIndex]['name'] + let key = api_keys[dataIndex]['key'] + if(!name){ + toast.error($t('Please enter the account name')) + return + } + else if(!key){ + toast.error($t('Please enter the APIKEY')) + return + } + let missing = true + for(let i=0;i{ + new AccountDialog().show(user_div); + } + },[ + $el('div.user',[ + $el('div.avatar', avatar ? [$el('img',{src:avatar})] : '😀'), + $el('div.info', [ + $el('h5.name', email), + $el('h6.remark','Credits: '+ credits) + ]) + ]), + $el('div.edit', {textContent:$t('Edit')}) + ]) + ) + toast.success($t('Save Succeed')) + } + else toast.success($t('Save Succeed')) + this.close() + } else { + toast.error($t('Save Failed')) + } + }}), + $el('button.delete',{textContent:$t('Delete'),onclick:e=>{ + const dataIndex = Array.prototype.indexOf.call(this.dialog_div.querySelectorAll('.easyuse-account-dialog-item'), e.target.parentNode) + if(api_keys.length<=1){ + toast.error($t('At least one account is required')) + return + } + api_keys.splice(dataIndex,1) + this.dialog_div.removeChild(e.target.parentNode) + }}), + ]) + } + + show(userdiv) { + api_keys.forEach((item,index)=>{ + this.lists.push(this.addItem(index,userdiv)) + }) + this.dialog_div = $el("div.easyuse-account-dialog", this.lists) + super.show( + $el('div.easyuse-account-dialog-main',[ + $el('div',[ + $el('a',{href:'https://platform.stability.ai/account/keys',target:'_blank',textContent:$t('Getting Your APIKEY')}), + ]), + this.dialog_div, + ]) + ); + } + + createButtons() { + const btns = super.createButtons(); + btns.unshift($el('button',{ + type:'button', + textContent:$t('Save Account Info'), + onclick:_=>{ + let missing = true + for(let i=0;i { + if (res.status == 200) { + toast.success($t('Save Succeed')) + + } else { + toast.error($t('Save Failed')) + } + }) + } + } + })) + btns.unshift($el('button',{ + type:'button', + textContent:$t('Add Account'), + onclick:_=>{ + const name = 'Account '+(api_keys.length).toString() + api_keys.push({name,key:''}) + const item = this.addItem(api_keys.length - 1) + this.lists.push(item) + this.dialog_div.appendChild(item) + } + })) + return btns + } +} + + +app.registerExtension({ + name: 'comfy.easyUse.account', + async beforeRegisterNodeDef(nodeType, nodeData, app) { + if(nodeData.name == 'easy stableDiffusion3API'){ + + const onNodeCreated = nodeType.prototype.onNodeCreated; + nodeType.prototype.onNodeCreated = async function() { + onNodeCreated ? onNodeCreated?.apply(this, arguments) : undefined; + const seed_widget = this.widgets.find(w => ['seed_num','seed'].includes(w.name)) + const seed_control = this.widgets.find(w=> ['control_before_generate','control_after_generate'].includes(w.name)) + let model_widget = this.widgets.find(w => w.name == 'model') + model_widget.callback = value =>{ + cost_widget.value = '-'+api_cost[value] + } + const cost_widget = this.addWidget('text', 'cost_credit', '0', _=>{ + },{ + serialize:false, + }) + cost_widget.disabled = true + setTimeout(_=>{ + if(seed_control.name == 'control_before_generate' && seed_widget.value === 0){ + seed_widget.value = Math.floor(Math.random() * 4294967294) + } + cost_widget.value = '-'+api_cost[model_widget.value] + },100) + let user_div = $el('div.easyuse-account-user', [$t('Loading UserInfo...')]) + let account = this.addDOMWidget('account',"btn",$el('div.easyuse-account',user_div)); + // 更新balance信息 + api.addEventListener('stable-diffusion-api-generate-succeed', async ({detail}) => { + let remarkDiv = user_div.querySelectorAll('.remark') + if(remarkDiv && remarkDiv[0]){ + const credits = detail?.model ? api_cost[detail.model] : 0 + if(credits) { + let balance = accSub(parseFloat(remarkDiv[0].innerText.replace(/Credits: /g,'')),credits) + if(balance>0){ + remarkDiv[0].innerText = 'Credits: '+ balance.toString() + } + } + } + await sleep(10000) + const res = await api.fetchApi('/easyuse/stability/balance') + if(res.status == 200){ + const data = await res.json() + if(data?.balance){ + const credits = data.balance?.credits || 0 + if(remarkDiv && remarkDiv[0]){ + remarkDiv[0].innerText = 'Credits: ' + credits + } + } + } + }) + // 获取api_keys + const res = await api.fetchApi('/easyuse/stability/api_keys') + if (res.status == 200){ + let data = await res.json() + api_keys = data.keys + api_current = data.current + if (api_keys.length > 0 && api_current!==undefined){ + const api_key = api_keys[api_current]['key'] + const api_name = api_keys[api_current]['name'] + if(!api_key){ + user_div.replaceChildren( + $el('div.easyuse-account-user-info', { + onclick:_=>{ + new AccountDialog().show(user_div); + } + },[ + $el('div.user',[ + $el('div.avatar', '😀'), + $el('div.info', [ + $el('h5.name', api_name), + $el('h6.remark',$t('Click to set the APIKEY first')) + ]) + ]), + $el('div.edit', {textContent:$t('Edit')}) + ]) + ) + }else{ + // 获取账号信息 + const res = await api.fetchApi('/easyuse/stability/user_info') + if(res.status == 200){ + const data = await res.json() + if(data?.account && data?.balance){ + const avatar = data.account?.profile_picture || null + const email = data.account?.email || null + const credits = data.balance?.credits || 0 + user_div.replaceChildren( + $el('div.easyuse-account-user-info', { + onclick:_=>{ + new AccountDialog().show(user_div); + } + },[ + $el('div.user',[ + $el('div.avatar', avatar ? [$el('img',{src:avatar})] : '😀'), + $el('div.info', [ + $el('h5.name', email), + $el('h6.remark','Credits: '+ credits) + ]) + ]), + $el('div.edit', {textContent:$t('Edit')}) + ]) + ) + } + } + } + } + } + } + } + } +}) \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/easy/easyContextMenu.js b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/easy/easyContextMenu.js new file mode 100644 index 0000000000000000000000000000000000000000..0abbdd684b3d0352d06161a4e4b98b3f68a0ea00 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/easy/easyContextMenu.js @@ -0,0 +1,174 @@ +import {app} from "../../../../scripts/app.js"; +import {api} from "../../../../scripts/api.js"; +import {$el} from "../../../../scripts/ui.js"; +import {$t} from "../common/i18n.js"; +import {getExtension, spliceExtension} from '../common/utils.js' +import {toast} from "../common/toast.js"; + +const setting_id = "Comfy.EasyUse.MenuNestSub" +let enableMenuNestSub = false +let thumbnails = [] + +export function addMenuNestSubSetting(app) { + app.ui.settings.addSetting({ + id: setting_id, + name: $t("Enable ContextMenu Auto Nest Subdirectories (ComfyUI-Easy-Use)"), + type: "boolean", + defaultValue: enableMenuNestSub, + onChange(value) { + enableMenuNestSub = !!value; + }, + }); +} + +const getEnableMenuNestSub = _ => app.ui.settings.getSettingValue(setting_id, enableMenuNestSub) + + +const Loaders = ['easy fullLoader','easy a1111Loader','easy comfyLoader'] +app.registerExtension({ + name:"comfy.easyUse.contextMenu", + async setup(app){ + addMenuNestSubSetting(app) + // 获取所有模型图像 + const imgRes = await api.fetchApi(`/easyuse/models/thumbnail`) + if (imgRes.status === 200) { + let data = await imgRes.json(); + thumbnails = data + } + else if(getEnableMenuNestSub()){ + toast.error($t("Too many thumbnails, have closed the display")) + } + const existingContextMenu = LiteGraph.ContextMenu; + LiteGraph.ContextMenu = function(values,options){ + const threshold = 10; + const enabled = getEnableMenuNestSub(); + if(!enabled || (values?.length || 0) <= threshold || !(options?.callback) || values.some(i => typeof i !== 'string')){ + if(enabled){ + // console.log('Skipping context menu auto nesting for incompatible menu.'); + } + return existingContextMenu.apply(this,[...arguments]); + } + const compatValues = values; + const originalValues = [...compatValues]; + const folders = {}; + const specialOps = []; + const folderless = []; + for(const value of compatValues){ + const splitBy = value.indexOf('/') > -1 ? '/' : '\\'; + const valueSplit = value.split(splitBy); + if(valueSplit.length > 1){ + const key = valueSplit.shift(); + folders[key] = folders[key] || []; + folders[key].push(valueSplit.join(splitBy)); + }else if(value === 'CHOOSE' || value.startsWith('DISABLE ')){ + specialOps.push(value); + }else{ + folderless.push(value); + } + } + const foldersCount = Object.values(folders).length; + if(foldersCount > 0){ + const oldcallback = options.callback; + options.callback = null; + const newCallback = (item,options) => { + if(['None','无','無','なし'].includes(item.content)) oldcallback('None',options) + else oldcallback(originalValues.find(i => i.endsWith(item.content),options)); + }; + const addContent = (content, folderName='') => { + const name = folderName ? folderName + '\\' + spliceExtension(content) : spliceExtension(content); + const ext = getExtension(content) + const time = new Date().getTime() + let thumbnail = '' + if(['ckpt', 'pt', 'bin', 'pth', 'safetensors'].includes(ext)){ + for(let i=0;i { + let subs = [] + let less = [] + const b = folder.map(name=> { + const _folders = {}; + const splitBy = name.indexOf('/') > -1 ? '/' : '\\'; + const valueSplit = name.split(splitBy); + if(valueSplit.length > 1){ + const key = valueSplit.shift(); + _folders[key] = _folders[key] || []; + _folders[key].push(valueSplit.join(splitBy)); + } + const foldersCount = Object.values(folders).length; + if(foldersCount > 0){ + let key = Object.keys(_folders)[0] + if(key && _folders[key]) subs.push({key, value:_folders[key][0]}) + else{ + less.push(addContent(name,key)) + } + } + return addContent(name,folderName) + }) + if(subs.length>0){ + let subs_obj = {} + subs.forEach(item => { + subs_obj[item.key] = subs_obj[item.key] || [] + subs_obj[item.key].push(item.value) + }) + return [...Object.entries(subs_obj).map(f => { + return { + content: f[0], + has_submenu: true, + callback: () => {}, + submenu: { + options: add_sub_folder(f[1], f[0]), + } + } + }),...less] + } + else return b + } + + for(const [folderName,folder] of Object.entries(folders)){ + newValues.push({ + content:folderName, + has_submenu:true, + callback:() => {}, + submenu:{ + options:add_sub_folder(folder,folderName), + } + }); + } + newValues.push(...folderless.map(f => addContent(f, ''))); + if(specialOps.length > 0) newValues.push(...specialOps.map(f => addContent(f, ''))); + return existingContextMenu.call(this,newValues,options); + } + return existingContextMenu.apply(this,[...arguments]); + } + LiteGraph.ContextMenu.prototype = existingContextMenu.prototype; + }, + +}) + diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/easy/easyDynamicWidgets.js b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/easy/easyDynamicWidgets.js new file mode 100644 index 0000000000000000000000000000000000000000..a325c531d9e3e262df11629c4c00442458f2c9bb --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/easy/easyDynamicWidgets.js @@ -0,0 +1,1274 @@ +import { app } from "../../../../scripts/app.js"; +import { api } from "../../../../scripts/api.js"; +import { ComfyWidgets } from "../../../../scripts/widgets.js"; +import { toast} from "../common/toast.js"; +import { $t } from '../common/i18n.js'; + +import { findWidgetByName, toggleWidget, updateNodeHeight} from "../common/utils.js"; + +const seedNodes = ["easy seed", "easy latentNoisy", "easy wildcards", "easy preSampling", "easy preSamplingAdvanced", "easy preSamplingNoiseIn", "easy preSamplingSdTurbo", "easy preSamplingCascade", "easy preSamplingDynamicCFG", "easy preSamplingLayerDiffusion", "easy fullkSampler", "easy fullCascadeKSampler"] +const loaderNodes = ["easy fullLoader", "easy a1111Loader", "easy comfyLoader", "easy fluxLoader", "easy hunyuanDiTLoader", "easy pixArtLoader"] + +function widgetLogic(node, widget) { + if (widget.name === 'lora_name') { + if (widget.value === "None") { + toggleWidget(node, findWidgetByName(node, 'lora_model_strength')) + toggleWidget(node, findWidgetByName(node, 'lora_clip_strength')) + } else { + toggleWidget(node, findWidgetByName(node, 'lora_model_strength'), true) + toggleWidget(node, findWidgetByName(node, 'lora_clip_strength'), true) + } + } + if (widget.name === 'rescale') { + let rescale_after_model = findWidgetByName(node, 'rescale_after_model').value + if (widget.value === 'by percentage' && rescale_after_model) { + toggleWidget(node, findWidgetByName(node, 'width')) + toggleWidget(node, findWidgetByName(node, 'height')) + toggleWidget(node, findWidgetByName(node, 'longer_side')) + toggleWidget(node, findWidgetByName(node, 'percent'), true) + } else if (widget.value === 'to Width/Height' && rescale_after_model) { + toggleWidget(node, findWidgetByName(node, 'width'), true) + toggleWidget(node, findWidgetByName(node, 'height'), true) + toggleWidget(node, findWidgetByName(node, 'percent')) + toggleWidget(node, findWidgetByName(node, 'longer_side')) + } else if (rescale_after_model) { + toggleWidget(node, findWidgetByName(node, 'longer_side'), true) + toggleWidget(node, findWidgetByName(node, 'width')) + toggleWidget(node, findWidgetByName(node, 'height')) + toggleWidget(node, findWidgetByName(node, 'percent')) + } + updateNodeHeight(node) + } + if (widget.name === 'upscale_method') { + if (widget.value === "None") { + toggleWidget(node, findWidgetByName(node, 'factor')) + toggleWidget(node, findWidgetByName(node, 'crop')) + } else { + toggleWidget(node, findWidgetByName(node, 'factor'), true) + toggleWidget(node, findWidgetByName(node, 'crop'), true) + } + updateNodeHeight(node) + } + if (widget.name === 'image_output') { + if (widget.value === 'Sender' || widget.value === 'Sender&Save'){ + toggleWidget(node, findWidgetByName(node, 'link_id'), true) + }else { + toggleWidget(node, findWidgetByName(node, 'link_id')) + } + if (widget.value === 'Hide' || widget.value === 'Preview' || widget.value == 'Preview&Choose' || widget.value === 'Sender') { + toggleWidget(node, findWidgetByName(node, 'save_prefix')) + toggleWidget(node, findWidgetByName(node, 'output_path')) + toggleWidget(node, findWidgetByName(node, 'embed_workflow')) + toggleWidget(node, findWidgetByName(node, 'number_padding')) + toggleWidget(node, findWidgetByName(node, 'overwrite_existing')) + } else if (widget.value === 'Save' || widget.value === 'Hide&Save' || widget.value === 'Sender&Save') { + toggleWidget(node, findWidgetByName(node, 'save_prefix'), true) + toggleWidget(node, findWidgetByName(node, 'output_path'), true) + toggleWidget(node, findWidgetByName(node, 'embed_workflow'), true) + toggleWidget(node, findWidgetByName(node, 'number_padding'), true) + toggleWidget(node, findWidgetByName(node, 'overwrite_existing'), true) + } + + if(widget.value === 'Hide' || widget.value === 'Hide&Save'){ + toggleWidget(node, findWidgetByName(node, 'decode_vae_name')) + }else{ + toggleWidget(node, findWidgetByName(node, 'decode_vae_name'), true) + } + } + if (widget.name === 'add_noise') { + let control_before_widget = findWidgetByName(node, 'control_before_generate') + let control_after_widget = findWidgetByName(node, 'control_after_generate') + if (widget.value === "disable") { + toggleWidget(node, findWidgetByName(node, 'seed')) + if(control_before_widget){ + control_before_widget.last_value = control_before_widget.value + control_before_widget.value = 'fixed' + toggleWidget(node, control_before_widget) + } + if(control_after_widget){ + control_after_widget.last_value = control_after_widget.value + control_after_widget.value = 'fixed' + toggleWidget(node, control_after_widget) + } + } else { + toggleWidget(node, findWidgetByName(node, 'seed'), true) + if(control_before_widget){ + if(control_before_widget?.last_value) control_before_widget.value = control_before_widget.last_value + toggleWidget(node, control_before_widget, true) + } + if(control_after_widget) { + if(control_after_widget?.last_value) control_after_widget.value = control_after_widget.last_value + toggleWidget(node, findWidgetByName(node, control_after_widget, true)) + } + } + updateNodeHeight(node) + } + if (widget.name === 'num_loras') { + let number_to_show = widget.value + 1 + for (let i = 0; i < number_to_show; i++) { + toggleWidget(node, findWidgetByName(node, 'lora_'+i+'_name'), true) + if (findWidgetByName(node, 'mode').value === "simple") { + toggleWidget(node, findWidgetByName(node, 'lora_'+i+'_strength'), true) + toggleWidget(node, findWidgetByName(node, 'lora_'+i+'_model_strength')) + toggleWidget(node, findWidgetByName(node, 'lora_'+i+'_clip_strength')) + } else { + toggleWidget(node, findWidgetByName(node, 'lora_'+i+'_strength')) + toggleWidget(node, findWidgetByName(node, 'lora_'+i+'_model_strength'), true) + toggleWidget(node, findWidgetByName(node, 'lora_'+i+'_clip_strength'), true) + } + } + for (let i = number_to_show; i < 21; i++) { + toggleWidget(node, findWidgetByName(node, 'lora_'+i+'_name')) + toggleWidget(node, findWidgetByName(node, 'lora_'+i+'_strength')) + toggleWidget(node, findWidgetByName(node, 'lora_'+i+'_model_strength')) + toggleWidget(node, findWidgetByName(node, 'lora_'+i+'_clip_strength')) + } + updateNodeHeight(node) + } + if (widget.name === 'num_controlnet') { + let number_to_show = widget.value + 1 + for (let i = 0; i < number_to_show; i++) { + toggleWidget(node, findWidgetByName(node, 'controlnet_'+i), true) + toggleWidget(node, findWidgetByName(node, 'controlnet_'+i+'_strength'), true) + toggleWidget(node, findWidgetByName(node, 'scale_soft_weight_'+i),true) + if (findWidgetByName(node, 'mode').value === "simple") { + toggleWidget(node, findWidgetByName(node, 'start_percent_'+i)) + toggleWidget(node, findWidgetByName(node, 'end_percent_'+i)) + } else { + toggleWidget(node, findWidgetByName(node, 'start_percent_'+i),true) + toggleWidget(node, findWidgetByName(node, 'end_percent_'+i), true) + } + } + for (let i = number_to_show; i < 10; i++) { + toggleWidget(node, findWidgetByName(node, 'controlnet_'+i)) + toggleWidget(node, findWidgetByName(node, 'controlnet_'+i+'_strength')) + toggleWidget(node, findWidgetByName(node, 'start_percent_'+i)) + toggleWidget(node, findWidgetByName(node, 'end_percent_'+i)) + toggleWidget(node, findWidgetByName(node, 'scale_soft_weight_'+i)) + } + updateNodeHeight(node) + } + + if (widget.name === 'mode') { + switch (node.comfyClass) { + case 'easy loraStack': + for (let i = 0; i < (findWidgetByName(node, 'num_loras').value + 1); i++) { + if (widget.value === "simple") { + toggleWidget(node, findWidgetByName(node, 'lora_'+i+'_strength'), true) + toggleWidget(node, findWidgetByName(node, 'lora_'+i+'_model_strength')) + toggleWidget(node, findWidgetByName(node, 'lora_'+i+'_clip_strength')) + } else { + toggleWidget(node, findWidgetByName(node, 'lora_'+i+'_strength')) + toggleWidget(node, findWidgetByName(node, 'lora_'+i+'_model_strength'), true) + toggleWidget(node, findWidgetByName(node, 'lora_'+i+'_clip_strength'), true)} + } + updateNodeHeight(node) + break + case 'easy controlnetStack': + for (let i = 0; i < (findWidgetByName(node, 'num_controlnet').value + 1); i++) { + if (widget.value === "simple") { + toggleWidget(node, findWidgetByName(node, 'start_percent_'+i)) + toggleWidget(node, findWidgetByName(node, 'end_percent_'+i)) + } else { + toggleWidget(node, findWidgetByName(node, 'start_percent_' + i), true) + toggleWidget(node, findWidgetByName(node, 'end_percent_' + i), true) + } + } + updateNodeHeight(node) + break + case 'easy icLightApply': + if (widget.value === "Foreground") { + toggleWidget(node, findWidgetByName(node, 'lighting'), true) + toggleWidget(node, findWidgetByName(node, 'remove_bg'), true) + toggleWidget(node, findWidgetByName(node, 'source')) + } else { + toggleWidget(node, findWidgetByName(node, 'lighting')) + toggleWidget(node, findWidgetByName(node, 'source'), true) + toggleWidget(node, findWidgetByName(node, 'remove_bg')) + } + updateNodeHeight(node) + break + } + } + + if (widget.name === 'resolution') { + if(widget.value === "自定义 x 自定义"){ + widget.value = 'width x height (custom)' + } + if (widget.value === "自定义 x 自定义" || widget.value === 'width x height (custom)') { + toggleWidget(node, findWidgetByName(node, 'empty_latent_width'), true) + toggleWidget(node, findWidgetByName(node, 'empty_latent_height'), true) + } else { + toggleWidget(node, findWidgetByName(node, 'empty_latent_width'), false) + toggleWidget(node, findWidgetByName(node, 'empty_latent_height'), false) + } + } + if (widget.name === 'ratio') { + if (widget.value === "custom") { + toggleWidget(node, findWidgetByName(node, 'empty_latent_width'), true) + toggleWidget(node, findWidgetByName(node, 'empty_latent_height'), true) + } else { + toggleWidget(node, findWidgetByName(node, 'empty_latent_width'), false) + toggleWidget(node, findWidgetByName(node, 'empty_latent_height'), false) + } + } + if (widget.name === 'downscale_mode') { + const widget_names = ['block_number', 'downscale_factor', 'start_percent', 'end_percent', 'downscale_after_skip', 'downscale_method', 'upscale_method'] + if (widget.value === "None") widget_names.map(name=> toggleWidget(node, findWidgetByName(node, name))) + else if(widget.value == 'Auto') widget_names.map(name =>toggleWidget(node, findWidgetByName(node, name),name == 'block_number' ? true : false)) + else widget_names.map(name=> toggleWidget(node, findWidgetByName(node, name), true)) + updateNodeHeight(node) + } + + if (widget.name == 'range_mode'){ + if(widget.value == 'step'){ + toggleWidget(node, findWidgetByName(node, 'step'), true) + toggleWidget(node, findWidgetByName(node, 'num_steps')) + }else if(widget.value == 'num_steps'){ + toggleWidget(node, findWidgetByName(node, 'step')) + toggleWidget(node, findWidgetByName(node, 'num_steps'), true) + } + updateNodeHeight(node) + } + + if (widget.name === 'toggle') { + widget.type = 'toggle' + widget.options = {on: 'Enabled', off: 'Disabled'} + } + + if(widget.name == 'text_combine_mode'){ + if(widget.value == 'replace'){ + toggleWidget(node, findWidgetByName(node, 'replace_text'), true) + }else{ + toggleWidget(node, findWidgetByName(node, 'replace_text')) + } + updateNodeHeight(node) + } + + if (widget.name === 'conditioning_mode') { + if (["replace", "concat", "combine"].includes(widget.value)) { + toggleWidget(node, findWidgetByName(node, 'average_strength')) + toggleWidget(node, findWidgetByName(node, 'old_cond_start')) + toggleWidget(node, findWidgetByName(node, 'old_cond_end')) + toggleWidget(node, findWidgetByName(node, 'new_cond_start')) + toggleWidget(node, findWidgetByName(node, 'new_cond_end')) + } else if(widget.value == 'average'){ + toggleWidget(node, findWidgetByName(node, 'average_strength'), true) + toggleWidget(node, findWidgetByName(node, 'old_cond_start')) + toggleWidget(node, findWidgetByName(node, 'old_cond_end')) + toggleWidget(node, findWidgetByName(node, 'new_cond_start')) + toggleWidget(node, findWidgetByName(node, 'new_cond_end')) + }else if(widget.value == 'timestep'){ + toggleWidget(node, findWidgetByName(node, 'average_strength')) + toggleWidget(node, findWidgetByName(node, 'old_cond_start'), true) + toggleWidget(node, findWidgetByName(node, 'old_cond_end'), true) + toggleWidget(node, findWidgetByName(node, 'new_cond_start'), true) + toggleWidget(node, findWidgetByName(node, 'new_cond_end'), true) + } + } + + if (widget.name === 'preset') { + const normol_presets = [ + 'LIGHT - SD1.5 only (low strength)', + 'STANDARD (medium strength)', + 'VIT-G (medium strength)', + 'PLUS (high strength)', 'PLUS FACE (portraits)', + 'FULL FACE - SD1.5 only (portraits stronger)', + ] + const faceid_presets = [ + 'FACEID', + 'FACEID PLUS - SD1.5 only', + 'FACEID PLUS KOLORS', + 'FACEID PLUS V2', + 'FACEID PORTRAIT (style transfer)', + 'FACEID PORTRAIT UNNORM - SDXL only (strong)' + ] + if(normol_presets.includes(widget.value)){ + toggleWidget(node, findWidgetByName(node, 'lora_strength')) + toggleWidget(node, findWidgetByName(node, 'provider')) + toggleWidget(node, findWidgetByName(node, 'weight_faceidv2')) + toggleWidget(node, findWidgetByName(node, 'weight_kolors')) + toggleWidget(node, findWidgetByName(node, 'use_tiled'), true) + let use_tiled = findWidgetByName(node, 'use_tiled') + if(use_tiled && use_tiled.value){ + toggleWidget(node, findWidgetByName(node, 'sharpening'), true) + }else { + toggleWidget(node, findWidgetByName(node, 'sharpening')) + } + + } + else if(faceid_presets.includes(widget.value)){ + toggleWidget(node, findWidgetByName(node, 'weight_faceidv2'), ['FACEID PLUS V2','FACEID PLUS KOLORS'].includes(widget.value) ? true : false); + toggleWidget(node, findWidgetByName(node, 'weight_kolors'), ['FACEID PLUS KOLORS'].includes(widget.value) ? true : false); + if(['FACEID PLUS KOLORS','FACEID PORTRAIT (style transfer)','FACEID PORTRAIT UNNORM - SDXL only (strong)'].includes(widget.value)){ + toggleWidget(node, findWidgetByName(node, 'lora_strength'), false) + } + else{ + toggleWidget(node, findWidgetByName(node, 'lora_strength'), true) + } + toggleWidget(node, findWidgetByName(node, 'provider'), true) + toggleWidget(node, findWidgetByName(node, 'use_tiled')) + toggleWidget(node, findWidgetByName(node, 'sharpening')) + } + updateNodeHeight(node) + } + + if (widget.name === 'use_tiled') { + if(widget.value) + toggleWidget(node, findWidgetByName(node, 'sharpening'), true) + else + toggleWidget(node, findWidgetByName(node, 'sharpening')) + updateNodeHeight(node) + } + + if (widget.name === 'num_embeds') { + let number_to_show = widget.value + 1 + for (let i = 0; i < number_to_show; i++) { + toggleWidget(node, findWidgetByName(node, 'weight'+i), true) + } + for (let i = number_to_show; i < 6; i++) { + toggleWidget(node, findWidgetByName(node, 'weight'+i)) + } + updateNodeHeight(node) + } + + if (widget.name === 'guider'){ + switch (widget.value){ + case 'Basic': + toggleWidget(node, findWidgetByName(node, 'cfg')) + toggleWidget(node, findWidgetByName(node, 'cfg_negative')) + break + case 'CFG': + toggleWidget(node, findWidgetByName(node, 'cfg'),true) + toggleWidget(node, findWidgetByName(node, 'cfg_negative')) + break + case 'IP2P+DualCFG': + case 'DualCFG': + toggleWidget(node, findWidgetByName(node, 'cfg'),true) + toggleWidget(node, findWidgetByName(node, 'cfg_negative'), true) + break + + } + updateNodeHeight(node) + } + + if (widget.name === 'scheduler'){ + if (['karrasADV','exponentialADV','polyExponential'].includes(widget.value)){ + toggleWidget(node, findWidgetByName(node, 'sigma_max'), true) + toggleWidget(node, findWidgetByName(node, 'sigma_min'), true) + toggleWidget(node, findWidgetByName(node, 'denoise')) + toggleWidget(node, findWidgetByName(node, 'beta_d')) + toggleWidget(node, findWidgetByName(node, 'beta_min')) + toggleWidget(node, findWidgetByName(node, 'eps_s')) + toggleWidget(node, findWidgetByName(node, 'coeff')) + if(widget.value != 'exponentialADV'){ + toggleWidget(node, findWidgetByName(node, 'rho'), true) + }else{ + toggleWidget(node, findWidgetByName(node, 'rho')) + } + }else if(widget.value == 'vp'){ + toggleWidget(node, findWidgetByName(node, 'sigma_max')) + toggleWidget(node, findWidgetByName(node, 'sigma_min')) + toggleWidget(node, findWidgetByName(node, 'denoise')) + toggleWidget(node, findWidgetByName(node, 'rho')) + toggleWidget(node, findWidgetByName(node, 'beta_d'),true) + toggleWidget(node, findWidgetByName(node, 'beta_min'),true) + toggleWidget(node, findWidgetByName(node, 'eps_s'),true) + toggleWidget(node, findWidgetByName(node, 'coeff')) + } + else{ + toggleWidget(node, findWidgetByName(node, 'denoise'),true) + toggleWidget(node, findWidgetByName(node, 'sigma_max')) + toggleWidget(node, findWidgetByName(node, 'sigma_min')) + toggleWidget(node, findWidgetByName(node, 'beta_d')) + toggleWidget(node, findWidgetByName(node, 'beta_min')) + toggleWidget(node, findWidgetByName(node, 'eps_s')) + toggleWidget(node, findWidgetByName(node, 'rho')) + if(widget.value == 'gits') toggleWidget(node, findWidgetByName(node, 'coeff'), true) + else toggleWidget(node, findWidgetByName(node, 'coeff')) + } + updateNodeHeight(node) + } + + if(widget.name === 'inpaint_mode'){ + switch (widget.value){ + case 'normal': + case 'fooocus_inpaint': + toggleWidget(node, findWidgetByName(node, 'dtype')) + toggleWidget(node, findWidgetByName(node, 'fitting')) + toggleWidget(node, findWidgetByName(node, 'function')) + toggleWidget(node, findWidgetByName(node, 'scale')) + toggleWidget(node, findWidgetByName(node, 'start_at')) + toggleWidget(node, findWidgetByName(node, 'end_at')) + break + case 'brushnet_random': + case 'brushnet_segmentation': + toggleWidget(node, findWidgetByName(node, 'dtype'), true) + toggleWidget(node, findWidgetByName(node, 'fitting')) + toggleWidget(node, findWidgetByName(node, 'function')) + toggleWidget(node, findWidgetByName(node, 'scale'), true) + toggleWidget(node, findWidgetByName(node, 'start_at'), true) + toggleWidget(node, findWidgetByName(node, 'end_at'), true) + break + case 'powerpaint': + toggleWidget(node, findWidgetByName(node, 'dtype'), true) + toggleWidget(node, findWidgetByName(node, 'fitting'),true) + toggleWidget(node, findWidgetByName(node, 'function'),true) + toggleWidget(node, findWidgetByName(node, 'scale'), true) + toggleWidget(node, findWidgetByName(node, 'start_at'), true) + toggleWidget(node, findWidgetByName(node, 'end_at'), true) + break + } + updateNodeHeight(node) + } + + if(widget.name == 't5_type'){ + switch (widget.value){ + case 'sd3': + toggleWidget(node, findWidgetByName(node, 'clip_name'), true) + toggleWidget(node, findWidgetByName(node, 'padding'), true) + toggleWidget(node, findWidgetByName(node, 't5_name')) + toggleWidget(node, findWidgetByName(node, 'device')) + toggleWidget(node, findWidgetByName(node, 'dtype')) + break + case 't5v11': + toggleWidget(node, findWidgetByName(node, 'clip_name')) + toggleWidget(node, findWidgetByName(node, 'padding')) + toggleWidget(node, findWidgetByName(node, 't5_name'),true) + toggleWidget(node, findWidgetByName(node, 'device'),true) + toggleWidget(node, findWidgetByName(node, 'dtype'),true) + } + updateNodeHeight(node) + } + + if(widget.name == 'rem_mode'){ + switch (widget.value){ + case 'Inspyrenet': + toggleWidget(node, findWidgetByName(node, 'torchscript_jit'), true) + break + default: + toggleWidget(node, findWidgetByName(node, 'torchscript_jit'), false) + break + } + } +} + +function widgetLogic2(node, widget) { + if (widget.name === 'sampler_name') { + const widget_names = ['eta','s_noise','upscale_ratio','start_step','end_step','upscale_n_step','unsharp_kernel_size','unsharp_sigma','unsharp_strength'] + if (["euler_ancestral", "dpmpp_2s_ancestral", "dpmpp_2m_sde", "lcm"].includes(widget.value)) { + widget_names.map(name=> toggleWidget(node, findWidgetByName(node, name)), true) + } else { + widget_names.map(name=> toggleWidget(node, findWidgetByName(node, name))) + } + updateNodeHeight(node) + } +} + +function widgetLogic3(node, widget){ + if (widget.name === 'target_parameter') { + if (node.comfyClass == 'easy XYInputs: Steps'){ + switch (widget.value){ + case "steps": + toggleWidget(node, findWidgetByName(node, 'first_step'), true) + toggleWidget(node, findWidgetByName(node, 'last_step'), true) + toggleWidget(node, findWidgetByName(node, 'first_start_step')) + toggleWidget(node, findWidgetByName(node, 'last_start_step')) + toggleWidget(node, findWidgetByName(node, 'first_end_step')) + toggleWidget(node, findWidgetByName(node, 'last_end_step')) + break + case "start_at_step": + toggleWidget(node, findWidgetByName(node, 'first_step')) + toggleWidget(node, findWidgetByName(node, 'last_step')) + toggleWidget(node, findWidgetByName(node, 'first_start_step'), true) + toggleWidget(node, findWidgetByName(node, 'last_start_step'), true) + toggleWidget(node, findWidgetByName(node, 'first_end_step')) + toggleWidget(node, findWidgetByName(node, 'last_end_step')) + break + case "end_at_step": + toggleWidget(node, findWidgetByName(node, 'first_step')) + toggleWidget(node, findWidgetByName(node, 'last_step')) + toggleWidget(node, findWidgetByName(node, 'first_start_step')) + toggleWidget(node, findWidgetByName(node, 'last_start_step')) + toggleWidget(node, findWidgetByName(node, 'first_end_step'),true) + toggleWidget(node, findWidgetByName(node, 'last_end_step'),true) + break + } + } + if (node.comfyClass == 'easy XYInputs: Sampler/Scheduler'){ + let number_to_show = findWidgetByName(node, 'input_count').value + 1 + for (let i = 0; i < number_to_show; i++) { + switch (widget.value) { + case "sampler": + toggleWidget(node, findWidgetByName(node, 'sampler_'+i), true) + toggleWidget(node, findWidgetByName(node, 'scheduler_'+i)) + break + case "scheduler": + toggleWidget(node, findWidgetByName(node, 'scheduler_'+i), true) + toggleWidget(node, findWidgetByName(node, 'sampler_'+i)) + break + default: + toggleWidget(node, findWidgetByName(node, 'sampler_'+i), true) + toggleWidget(node, findWidgetByName(node, 'scheduler_'+i), true) + break + } + } + updateNodeHeight(node) + } + if (node.comfyClass == 'easy XYInputs: ControlNet'){ + switch (widget.value){ + case "strength": + toggleWidget(node, findWidgetByName(node, 'first_strength'), true) + toggleWidget(node, findWidgetByName(node, 'last_strength'), true) + toggleWidget(node, findWidgetByName(node, 'strength')) + toggleWidget(node, findWidgetByName(node, 'start_percent'), true) + toggleWidget(node, findWidgetByName(node, 'end_percent'), true) + toggleWidget(node, findWidgetByName(node, 'first_start_percent')) + toggleWidget(node, findWidgetByName(node, 'last_start_percent')) + toggleWidget(node, findWidgetByName(node, 'first_end_percent')) + toggleWidget(node, findWidgetByName(node, 'last_end_percent')) + break + case "start_percent": + toggleWidget(node, findWidgetByName(node, 'first_strength')) + toggleWidget(node, findWidgetByName(node, 'last_strength')) + toggleWidget(node, findWidgetByName(node, 'strength'), true) + toggleWidget(node, findWidgetByName(node, 'start_percent')) + toggleWidget(node, findWidgetByName(node, 'end_percent'), true) + toggleWidget(node, findWidgetByName(node, 'first_start_percent'), true) + toggleWidget(node, findWidgetByName(node, 'last_start_percent'), true) + toggleWidget(node, findWidgetByName(node, 'first_end_percent')) + toggleWidget(node, findWidgetByName(node, 'last_end_percent')) + break + case "end_percent": + toggleWidget(node, findWidgetByName(node, 'first_strength')) + toggleWidget(node, findWidgetByName(node, 'last_strength')) + toggleWidget(node, findWidgetByName(node, 'strength'), true) + toggleWidget(node, findWidgetByName(node, 'start_percent'), true) + toggleWidget(node, findWidgetByName(node, 'end_percent')) + toggleWidget(node, findWidgetByName(node, 'first_start_percent')) + toggleWidget(node, findWidgetByName(node, 'last_start_percent')) + toggleWidget(node, findWidgetByName(node, 'first_end_percent'), true) + toggleWidget(node, findWidgetByName(node, 'last_end_percent'), true) + break + } + updateNodeHeight(node) + } + + } + if (node.comfyClass == 'easy XYInputs: PromptSR'){ + let number_to_show = findWidgetByName(node, 'replace_count').value + 1 + for (let i = 0; i < number_to_show; i++) { + toggleWidget(node, findWidgetByName(node, 'replace_'+i), true) + } + for (let i = number_to_show; i < 31; i++) { + toggleWidget(node, findWidgetByName(node, 'replace_'+i)) + } + updateNodeHeight(node) + } + + if(widget.name == 'input_count'){ + let number_to_show = widget.value + 1 + for (let i = 0; i < number_to_show; i++) { + if (findWidgetByName(node, 'target_parameter').value === "sampler") { + toggleWidget(node, findWidgetByName(node, 'sampler_'+i), true) + toggleWidget(node, findWidgetByName(node, 'scheduler_'+i)) + } + else if (findWidgetByName(node, 'target_parameter').value === "scheduler") { + toggleWidget(node, findWidgetByName(node, 'scheduler_'+i), true) + toggleWidget(node, findWidgetByName(node, 'sampler_'+i)) + } else { + toggleWidget(node, findWidgetByName(node, 'sampler_'+i), true) + toggleWidget(node, findWidgetByName(node, 'scheduler_'+i), true) + } + } + for (let i = number_to_show; i < 31; i++) { + toggleWidget(node, findWidgetByName(node, 'sampler_'+i)) + toggleWidget(node, findWidgetByName(node, 'scheduler_'+i)) + } + updateNodeHeight(node) + } + if (widget.name === 'lora_count') { + let number_to_show = widget.value + 1 + const isWeight = findWidgetByName(node, 'input_mode').value.indexOf("Weights") == -1 + for (let i = 0; i < number_to_show; i++) { + toggleWidget(node, findWidgetByName(node, 'lora_name_'+i), true) + if (isWeight) { + toggleWidget(node, findWidgetByName(node, 'lora_name_'+i), true) + toggleWidget(node, findWidgetByName(node, 'model_str_'+i)) + toggleWidget(node, findWidgetByName(node, 'clip_str_'+i)) + } else { + toggleWidget(node, findWidgetByName(node, 'lora_name_'+i), true) + toggleWidget(node, findWidgetByName(node, 'model_str_'+i),true) + toggleWidget(node, findWidgetByName(node, 'clip_str_'+i), true) + } + } + for (let i = number_to_show; i < 11; i++) { + toggleWidget(node, findWidgetByName(node, 'lora_name_'+i)) + toggleWidget(node, findWidgetByName(node, 'model_str_'+i)) + toggleWidget(node, findWidgetByName(node, 'clip_str_'+i)) + } + updateNodeHeight(node) + } + if (widget.name === 'ckpt_count') { + let number_to_show = widget.value + 1 + const hasClipSkip = findWidgetByName(node, 'input_mode').value.indexOf("ClipSkip") != -1 + const hasVae = findWidgetByName(node, 'input_mode').value.indexOf("VAE") != -1 + for (let i = 0; i < number_to_show; i++) { + toggleWidget(node, findWidgetByName(node, 'ckpt_name_'+i), true) + if (hasClipSkip && hasVae) { + toggleWidget(node, findWidgetByName(node, 'clip_skip_'+i), true) + toggleWidget(node, findWidgetByName(node, 'vae_name_'+i), true) + } else if (hasVae){ + toggleWidget(node, findWidgetByName(node, 'clip_skip_' + i)) + toggleWidget(node, findWidgetByName(node, 'vae_name_' + i), true) + }else{ + toggleWidget(node, findWidgetByName(node, 'clip_skip_' + i)) + toggleWidget(node, findWidgetByName(node, 'vae_name_' + i)) + } + } + for (let i = number_to_show; i < 11; i++) { + toggleWidget(node, findWidgetByName(node, 'ckpt_name_'+i)) + toggleWidget(node, findWidgetByName(node, 'clip_skip_'+i)) + toggleWidget(node, findWidgetByName(node, 'vae_name_'+i)) + } + updateNodeHeight(node) + } + + if (widget.name === 'input_mode') { + if(node.comfyClass == 'easy XYInputs: Lora'){ + let number_to_show = findWidgetByName(node, 'lora_count').value + 1 + const hasWeight = widget.value.indexOf("Weights") != -1 + for (let i = 0; i < number_to_show; i++) { + toggleWidget(node, findWidgetByName(node, 'lora_name_'+i), true) + if (hasWeight) { + toggleWidget(node, findWidgetByName(node, 'model_str_'+i), true) + toggleWidget(node, findWidgetByName(node, 'clip_str_'+i), true) + } else { + toggleWidget(node, findWidgetByName(node, 'model_str_' + i)) + toggleWidget(node, findWidgetByName(node, 'clip_str_' + i)) + } + } + if(hasWeight){ + toggleWidget(node, findWidgetByName(node, 'model_strength')) + toggleWidget(node, findWidgetByName(node, 'clip_strength')) + }else{ + toggleWidget(node, findWidgetByName(node, 'model_strength'), true) + toggleWidget(node, findWidgetByName(node, 'clip_strength'),true) + } + } + else if(node.comfyClass == 'easy XYInputs: Checkpoint'){ + let number_to_show = findWidgetByName(node, 'ckpt_count').value + 1 + const hasClipSkip = widget.value.indexOf("ClipSkip") != -1 + const hasVae = widget.value.indexOf("VAE") != -1 + for (let i = 0; i < number_to_show; i++) { + toggleWidget(node, findWidgetByName(node, 'ckpt_name_'+i), true) + if (hasClipSkip && hasVae) { + toggleWidget(node, findWidgetByName(node, 'clip_skip_'+i), true) + toggleWidget(node, findWidgetByName(node, 'vae_name_'+i), true) + } else if (hasClipSkip){ + toggleWidget(node, findWidgetByName(node, 'clip_skip_' + i), true) + toggleWidget(node, findWidgetByName(node, 'vae_name_' + i)) + }else{ + toggleWidget(node, findWidgetByName(node, 'clip_skip_' + i)) + toggleWidget(node, findWidgetByName(node, 'vae_name_' + i)) + } + } + } + + updateNodeHeight(node) + } + + // if(widget.name == 'replace_count'){ + // let number_to_show = widget.value + 1 + // for (let i = 0; i < number_to_show; i++) { + // toggleWidget(node, findWidgetByName(node, 'replace_'+i), true) + // } + // for (let i = number_to_show; i < 31; i++) { + // toggleWidget(node, findWidgetByName(node, 'replace_'+i)) + // } + // updateNodeHeight(node) + // } +} + +app.registerExtension({ + name: "comfy.easyUse.dynamicWidgets", + + nodeCreated(node) { + switch (node.comfyClass){ + case "easy fullLoader": + case "easy a1111Loader": + case "easy fluxLoader": + case "easy comfyLoader": + case "easy cascadeLoader": + case "easy svdLoader": + case "easy dynamiCrafterLoader": + case "easy hunyuanDiTLoader": + case "easy pixArtLoader": + case "easy kolorsLoader": + case "easy loraStack": + case "easy controlnetStack": + case "easy latentNoisy": + case "easy preSampling": + case "easy preSamplingAdvanced": + case "easy preSamplingNoiseIn": + case "easy preSamplingCustom": + case "easy preSamplingSdTurbo": + case "easy preSamplingCascade": + case "easy preSamplingLayerDiffusion": + case "easy fullkSampler": + case "easy kSampler": + case "easy kSamplerSDTurbo": + case "easy kSamplerTiled": + case "easy kSamplerLayerDiffusion": + case "easy kSamplerInpainting": + case "easy kSamplerDownscaleUnet": + case "easy fullCascadeKSampler": + case "easy cascadeKSampler": + case "easy hiresFix": + case "easy detailerFix": + case "easy imageRemBg": + case "easy imageColorMatch": + case "easy imageDetailTransfer": + case "easy loadImageBase64": + case "easy XYInputs: Steps": + case "easy XYInputs: Sampler/Scheduler": + case 'easy XYInputs: Checkpoint': + case "easy XYInputs: Lora": + case "easy XYInputs: PromptSR": + case "easy XYInputs: ControlNet": + case "easy rangeInt": + case "easy rangeFloat": + case 'easy latentCompositeMaskedWithCond': + case 'easy pipeEdit': + case 'easy icLightApply': + case 'easy ipadapterApply': + case 'easy ipadapterApplyADV': + case 'easy ipadapterApplyFaceIDKolors': + case 'easy ipadapterApplyEncoder': + case 'easy applyInpaint': + getSetters(node) + break + case "easy wildcards": + const wildcard_text_widget_index = node.widgets.findIndex((w) => w.name == 'text'); + const wildcard_text_widget = node.widgets[wildcard_text_widget_index]; + + // lora selector, wildcard selector + let combo_id = 1; + + Object.defineProperty(node.widgets[combo_id], "value", { + set: (value) => { + const stackTrace = new Error().stack; + if(stackTrace.includes('inner_value_change')) { + if(value != "Select the LoRA to add to the text") { + let lora_name = value; + if (lora_name.endsWith('.safetensors')) { + lora_name = lora_name.slice(0, -12); + } + + wildcard_text_widget.value += ``; + } + } + }, + get: () => { return "Select the LoRA to add to the text"; } + }); + + Object.defineProperty(node.widgets[combo_id+1], "value", { + set: (value) => { + const stackTrace = new Error().stack; + if(stackTrace.includes('inner_value_change')) { + if(value != "Select the Wildcard to add to the text") { + if(wildcard_text_widget.value != '') + wildcard_text_widget.value += ', ' + + wildcard_text_widget.value += value; + } + } + }, + get: () => { return "Select the Wildcard to add to the text"; } + }); + + // Preventing validation errors from occurring in any situation. + node.widgets[combo_id].serializeValue = () => { return "Select the LoRA to add to the text"; } + node.widgets[combo_id+1].serializeValue = () => { return "Select the Wildcard to add to the text"; } + break + case "easy detailerFix": + const textarea_widget_index = node.widgets.findIndex((w) => w.type === "customtext"); + if(textarea_widget_index == -1) return + node.widgets[textarea_widget_index].dynamicPrompts = false + node.widgets[textarea_widget_index].inputEl.placeholder = "wildcard spec: if kept empty, this option will be ignored"; + node.widgets[textarea_widget_index].serializeValue = () => {return node.widgets[textarea_widget_index].value}; + break + case "easy XYInputs: ModelMergeBlocks": + let preset_i = 3; + let vector_i = 4; + let file_i = 5; + node._value = "Preset"; + + let valuesWidget = node.widgets[vector_i] + Object.defineProperty(node.widgets[preset_i], "value", { + set: (value) => { + const stackTrace = new Error().stack; + if(stackTrace.includes('inner_value_change')) { + if(value != "Preset") { + if(!value.startsWith('@') && valuesWidget.value) + valuesWidget.value += "\n"; + if(value.startsWith('@')) { + let spec = value.split(':')[1]; + var n; + var sub_n = null; + var block = null; + + if(isNaN(spec)) { + let sub_spec = spec.split(','); + + if(sub_spec.length != 3) { + valuesWidget = '!! SPEC ERROR !!'; + node._value = ''; + return; + } + + n = parseInt(sub_spec[0].trim()); + sub_n = parseInt(sub_spec[1].trim()); + block = parseInt(sub_spec[2].trim()); + } + else { + n = parseInt(spec.trim()); + } + + valuesWidget.value = ""; + if(sub_n == null) { + for(let i=1; i<=n; i++) { + var temp = "1,1"; + for(let j=1; j<=n; j++) { + if(temp!='') + temp += ','; + if(j==i) + temp += '1'; + else + temp += '0'; + } + temp += ',1; '; + + valuesWidget.value += `B${i}:${temp}\n`; + } + } + else { + for(let i=1; i<=sub_n; i++) { + var temp = ""; + for(let j=1; j<=n; j++) { + if(temp!='') + temp += ','; + + if(block!=j) + temp += '0'; + else { + temp += ' '; + for(let k=1; k<=sub_n; k++) { + if(k==i) + temp += '1 '; + else + temp += '0 '; + } + } + } + + valuesWidget.value += `B${block}.SUB${i}:${temp}\n`; + } + } + } + else { + valuesWidget.value += `${value}; `; + } + // if(node.widgets_values) { + // valuesWidget.value = node.widgets[preset_i].value+ `; `; + // } + } + } + + node._value = value; + }, + get: () => { + return node._value; + } + }); + + const cb = node.callback; + valuesWidget.callback = function () { + if (cb) { + return cb.apply(this, arguments); + } + }; + + // upload .csv + async function uploadFile(file) { + try { + const body = new FormData(); + body.append("csv", file); + const resp = await api.fetchApi("/easyuse/upload/csv", { + method: "POST", + body, + }); + + if (resp.status === 200) { + const data = await resp.json(); + node.widgets[vector_i].value = data + } else { + alert(resp.status + " - " + resp.statusText); + } + } catch (error) { + alert(error); + } + } + + const fileInput = document.createElement("input"); + Object.assign(fileInput, { + type: "file", + accept: "text/csv", + style: "show: none", + onchange: async (event) => { + if (fileInput.files.length) { + await uploadFile(fileInput.files[0], true); + event.target.value = '' + } + }, + }); + document.body.append(fileInput); + + const name = "choose .csv file into values" + let uploadWidget = node.addWidget("button", name, "csv", () => { + fileInput.click(); + }); + uploadWidget.label = name; + uploadWidget.serialize = false; + + break + } + + }, + async beforeRegisterNodeDef(nodeType, nodeData, app) { + function addText(arr_text) { + var text = ''; + for (let i = 0; i < arr_text.length; i++) { + text += arr_text[i]; + } + return text + } + + if (["easy showSpentTime"].includes(nodeData.name)) { + function populate(text) { + if (this.widgets) { + const pos = this.widgets.findIndex((w) => w.name === "spent_time"); + if (pos !== -1 && this.widgets[pos]) { + const w = this.widgets[pos] + console.log(text) + w.value = text; + } + } + } + + // When the node is executed we will be sent the input text, show this in the widget + const onExecuted = nodeType.prototype.onExecuted; + nodeType.prototype.onExecuted = function (message) { + onExecuted?.apply(this, arguments); + const text = addText(message.text) + populate.call(this, text); + }; + } + + if (["easy showLoaderSettingsNames"].includes(nodeData.name)) { + function populate(text) { + if (this.widgets) { + const pos = this.widgets.findIndex((w) => w.name === "names"); + if (pos !== -1 && this.widgets[pos]) { + const w = this.widgets[pos] + w.value = text; + } + } + } + + // When the node is executed we will be sent the input text, show this in the widget + const onExecuted = nodeType.prototype.onExecuted; + nodeType.prototype.onExecuted = function (message) { + onExecuted?.apply(this, arguments); + const text = addText(message.text) + populate.call(this, text); + }; + } + + if (loaderNodes.includes(nodeData.name)) { + function populate(text, type = 'positive') { + if (this.widgets) { + const pos = this.widgets.findIndex((w) => w.name === type + "_prompt"); + const className = "comfy-multiline-input wildcard_" + type + '_' + this.id.toString() + if (pos == -1 && text) { + const inputEl = document.createElement("textarea"); + inputEl.className = className; + inputEl.placeholder = "Wildcard Prompt (" + type + ")" + const widget = this.addDOMWidget(type + "_prompt", "customtext", inputEl, { + getValue() { + return inputEl.value; + }, + setValue(v) { + inputEl.value = v; + }, + serialize: false, + }); + widget.inputEl = inputEl; + widget.inputEl.readOnly = true + inputEl.addEventListener("input", () => { + widget.callback?.(widget.value); + }); + widget.value = text; + } else if (this.widgets[pos]) { + if (text) { + const w = this.widgets[pos] + w.value = text; + } else { + this.widgets.splice(pos, 1); + const element = document.getElementsByClassName(className) + if (element && element[0]) element[0].remove() + } + } + } + } + + const onExecuted = nodeType.prototype.onExecuted; + nodeType.prototype.onExecuted = function (message) { + onExecuted?.apply(this, arguments); + const positive = addText(message.positive) + const negative = addText(message.negative) + populate.call(this, positive, "positive"); + populate.call(this, negative, "negative"); + }; + } + + if(["easy sv3dLoader"].includes(nodeData.name)){ + function changeSchedulerText(mode, batch_size, inputEl) { + console.log(mode) + switch (mode){ + case 'azimuth': + inputEl.readOnly = true + inputEl.style.opacity = 0.6 + return `0:(0.0,0.0)` + (batch_size > 1 ? `\n${batch_size-1}:(360.0,0.0)` : '') + case 'elevation': + inputEl.readOnly = true + inputEl.style.opacity = 0.6 + return `0:(-90.0,0.0)` + (batch_size > 1 ? `\n${batch_size-1}:(90.0,0.0)` : '') + case 'custom': + inputEl.readOnly = false + inputEl.style.opacity = 1 + return `0:(0.0,0.0)\n9:(180.0,0.0)\n20:(360.0,0.0)` + } + } + + + const onNodeCreated = nodeType.prototype.onNodeCreated; + nodeType.prototype.onNodeCreated = async function () { + onNodeCreated ? onNodeCreated.apply(this, []) : undefined; + const easing_mode_widget = this.widgets.find(w => w.name == 'easing_mode') + const batch_size = this.widgets.find(w => w.name == 'batch_size') + const scheduler = this.widgets.find(w => w.name == 'scheduler') + setTimeout(_=>{ + if(!scheduler.value) scheduler.value = changeSchedulerText(easing_mode_widget.value, batch_size.value, scheduler.inputEl) + },1) + easing_mode_widget.callback = value=>{ + scheduler.value = changeSchedulerText(value, batch_size.value, scheduler.inputEl) + } + batch_size.callback = value =>{ + scheduler.value = changeSchedulerText(easing_mode_widget.value, value, scheduler.inputEl) + } + } + } + + if (seedNodes.includes(nodeData.name)) { + const onNodeCreated = nodeType.prototype.onNodeCreated; + nodeType.prototype.onNodeCreated = async function () { + onNodeCreated ? onNodeCreated.apply(this, []) : undefined; + const seed_widget = this.widgets.find(w => ['seed_num','seed'].includes(w.name)) + const seed_control = this.widgets.find(w=> ['control_before_generate','control_after_generate'].includes(w.name)) + if(nodeData.name == 'easy seed'){ + const randomSeedButton = this.addWidget("button", "🎲 Manual Random Seed", null, _=>{ + if(seed_control.value != 'fixed') seed_control.value = 'fixed' + seed_widget.value = Math.floor(Math.random() * 1125899906842624) + app.queuePrompt(0, 1) + },{ serialize:false}) + seed_widget.linkedWidgets = [randomSeedButton, seed_control]; + } + } + const onAdded = nodeType.prototype.onAdded; + nodeType.prototype.onAdded = async function () { + onAdded ? onAdded.apply(this, []) : undefined; + const seed_widget = this.widgets.find(w => ['seed_num','seed'].includes(w.name)) + const seed_control = this.widgets.find(w=> ['control_before_generate','control_after_generate'].includes(w.name)) + setTimeout(_=>{ + if(seed_control.name == 'control_before_generate' && seed_widget.value === 0) { + seed_widget.value = Math.floor(Math.random() * 1125899906842624) + } + },1) + } + } + + if (nodeData.name == 'easy imageInsetCrop') { + function setWidgetStep(a) { + const measurementWidget = a.widgets[0] + for (let i = 1; i <= 4; i++) { + if (measurementWidget.value === 'Pixels') { + a.widgets[i].options.step = 80; + a.widgets[i].options.max = 8192; + } else { + a.widgets[i].options.step = 10; + a.widgets[i].options.max = 99; + } + } + } + + nodeType.prototype.onAdded = async function (graph) { + const measurementWidget = this.widgets[0]; + let callback = measurementWidget.callback; + measurementWidget.callback = (...args) => { + setWidgetStep(this); + callback && callback.apply(measurementWidget, [...args]); + }; + setTimeout(_=>{ + setWidgetStep(this); + },1) + } + } + + if(['easy showAnything', 'easy showTensorShape', 'easy imageInterrogator'].includes(nodeData.name)){ + function populate(text) { + if (this.widgets) { + const pos = this.widgets.findIndex((w) => w.name === "text"); + if (pos !== -1) { + for (let i = pos; i < this.widgets.length; i++) { + this.widgets[i].onRemove?.(); + } + this.widgets.length = pos; + } + } + + for (const list of text) { + const w = ComfyWidgets["STRING"](this, "text", ["STRING", { multiline: true }], app).widget; + // w.inputEl.readOnly = true; + w.inputEl.style.opacity = 0.6; + w.value = list; + } + + requestAnimationFrame(() => { + const sz = this.computeSize(); + if (sz[0] < this.size[0]) { + sz[0] = this.size[0]; + } + if (sz[1] < this.size[1]) { + sz[1] = this.size[1]; + } + this.onResize?.(sz); + app.graph.setDirtyCanvas(true, false); + }); + } + + // When the node is executed we will be sent the input text, display this in the widget + const onExecuted = nodeType.prototype.onExecuted; + nodeType.prototype.onExecuted = function (message) { + onExecuted?.apply(this, arguments); + populate.call(this, message.text); + }; + + if(!['easy imageInterrogator'].includes(nodeData.name)) { + const onConfigure = nodeType.prototype.onConfigure; + nodeType.prototype.onConfigure = function () { + onConfigure?.apply(this, arguments); + if (this.widgets_values?.length) { + populate.call(this, this.widgets_values); + } + }; + } + } + + if(nodeData.name == 'easy convertAnything'){ + const onNodeCreated = nodeType.prototype.onNodeCreated; + nodeType.prototype.onNodeCreated = async function () { + onNodeCreated ? onNodeCreated.apply(this, []) : undefined; + setTimeout(_=>{ + const type_control = this.widgets[this.widgets.findIndex((w) => w.name === "output_type")] + let _this = this + type_control.callback = async() => { + _this.outputs[0].type = (type_control.value).toUpperCase() + _this.outputs[0].name = type_control.value + _this.outputs[0].label = type_control.value + } + },300) + + } + } + + if (nodeData.name == 'easy promptLine') { + const onAdded = nodeType.prototype.onAdded; + nodeType.prototype.onAdded = async function () { + onAdded ? onAdded.apply(this, []) : undefined; + let prompt_widget = this.widgets.find(w => w.name == "prompt") + const button = this.addWidget("button", "get values from COMBO link", '', () => { + const output_link = this.outputs[1]?.links?.length>0 ? this.outputs[1]['links'][0] : null + const all_nodes = app.graph._nodes + const node = all_nodes.find(cate=> cate.inputs?.find(input=> input.link == output_link)) + if(!output_link || !node){ + toast.error($t('No COMBO link'), 3000) + return + } + else{ + const input = node.inputs.find(input=> input.link == output_link) + const widget_name = input.widget.name + const widgets = node.widgets + const widget = widgets.find(cate=> cate.name == widget_name) + let values = widget?.options.values || null + if(values){ + values = values.join('\n') + prompt_widget.value = values + } + } + }, { + serialize: false + }) + } + } + } +}); + + +const getSetWidgets = ['rescale_after_model', 'rescale', + 'lora_name', 'lora1_name', 'lora2_name', 'lora3_name', + 'refiner_lora1_name', 'refiner_lora2_name', 'upscale_method', + 'image_output', 'add_noise', 'info', 'sampler_name', + 'ckpt_B_name', 'ckpt_C_name', 'save_model', 'refiner_ckpt_name', + 'num_loras', 'num_controlnet', 'mode', 'toggle', 'resolution', 'ratio', 'target_parameter', + 'input_count', 'replace_count', 'downscale_mode', 'range_mode','text_combine_mode', 'input_mode', + 'lora_count','ckpt_count', 'conditioning_mode', 'preset', 'use_tiled', 'use_batch', 'num_embeds', + "easing_mode", "guider", "scheduler", "inpaint_mode", 't5_type', 'rem_mode' +] + +function getSetters(node) { + if (node.widgets) + for (const w of node.widgets) { + if (getSetWidgets.includes(w.name)) { + if(node.comfyClass.indexOf("easy XYInputs:") != -1) widgetLogic3(node, w) + else if(w.name == 'sampler_name' && node.comfyClass == 'easy preSamplingSdTurbo') widgetLogic2(node, w); + else widgetLogic(node, w); + let widgetValue = w.value; + + // Define getters and setters for widget values + Object.defineProperty(w, 'value', { + get() { + return widgetValue; + }, + set(newVal) { + if (newVal !== widgetValue) { + widgetValue = newVal; + if(node.comfyClass.indexOf("easy XYInputs:") != -1) widgetLogic3(node, w) + else if(w.name == 'sampler_name' && node.comfyClass == 'easy preSamplingSdTurbo') widgetLogic2(node, w); + else widgetLogic(node, w); + } + } + }); + } + } +} \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/easy/easyExtraMenu.js b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/easy/easyExtraMenu.js new file mode 100644 index 0000000000000000000000000000000000000000..b4ea61fc9276ab291e9d4a9747f92d2831cbd8ef --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/easy/easyExtraMenu.js @@ -0,0 +1,593 @@ +import {app} from "../../../../scripts/app.js"; +import {$t} from '../common/i18n.js' +import {CheckpointInfoDialog, LoraInfoDialog} from "../common/model.js"; + +const loaders = ['easy fullLoader', 'easy a1111Loader', 'easy comfyLoader', 'easy kolorsLoader', 'easy hunyuanDiTLoader', 'easy pixArtLoader'] +const preSampling = ['easy preSampling', 'easy preSamplingAdvanced', 'easy preSamplingDynamicCFG', 'easy preSamplingNoiseIn', 'easy preSamplingCustom', 'easy preSamplingLayerDiffusion', 'easy fullkSampler'] +const kSampler = ['easy kSampler', 'easy kSamplerTiled', 'easy kSamplerInpainting', 'easy kSamplerDownscaleUnet', 'easy kSamplerLayerDiffusion'] +const controlnet = ['easy controlnetLoader', 'easy controlnetLoaderADV', 'easy controlnetLoader++', 'easy instantIDApply', 'easy instantIDApplyADV'] +const ipadapter = ['easy ipadapterApply', 'easy ipadapterApplyADV', 'easy ipadapterApplyFaceIDKolors', 'easy ipadapterStyleComposition', 'easy ipadapterApplyFromParams', 'easy pulIDApply', 'easy pulIDApplyADV'] +const positive_prompt = ['easy positive', 'easy wildcards'] +const imageNode = ['easy loadImageBase64', 'LoadImage', 'LoadImageMask'] +const inpaint = ['easy applyBrushNet', 'easy applyPowerPaint', 'easy applyInpaint'] +const widgetMapping = { + "positive_prompt":{ + "text": "positive", + "positive": "text" + }, + "loaders":{ + "ckpt_name": "ckpt_name", + "vae_name": "vae_name", + "clip_skip": "clip_skip", + "lora_name": "lora_name", + "resolution": "resolution", + "empty_latent_width": "empty_latent_width", + "empty_latent_height": "empty_latent_height", + "positive": "positive", + "negative": "negative", + "batch_size": "batch_size", + "a1111_prompt_style": "a1111_prompt_style" + }, + "preSampling":{ + "steps": "steps", + "cfg": "cfg", + "cfg_scale_min": "cfg", + "sampler_name": "sampler_name", + "scheduler": "scheduler", + "denoise": "denoise", + "seed_num": "seed_num", + "seed": "seed" + }, + "kSampler":{ + "image_output": "image_output", + "save_prefix": "save_prefix", + "link_id": "link_id" + }, + "controlnet":{ + "control_net_name":"control_net_name", + "strength": ["strength", "cn_strength"], + "scale_soft_weights": ["scale_soft_weights","cn_soft_weights"], + "cn_strength": ["strength", "cn_strength"], + "cn_soft_weights": ["scale_soft_weights","cn_soft_weights"], + }, + "ipadapter":{ + "preset":"preset", + "lora_strength": "lora_strength", + "provider": "provider", + "weight":"weight", + "weight_faceidv2": "weight_faceidv2", + "start_at": "start_at", + "end_at": "end_at", + "cache_mode": "cache_mode", + "use_tiled": "use_tiled", + "insightface": "insightface", + "pulid_file": "pulid_file" + }, + "load_image":{ + "image":"image", + "base64_data":"base64_data", + "channel": "channel" + }, + "inpaint":{ + "dtype": "dtype", + "fitting": "fitting", + "function": "function", + "scale": "scale", + "start_at": "start_at", + "end_at": "end_at" + } +} +const inputMapping = { + "loaders":{ + "optional_lora_stack": "optional_lora_stack", + "positive": "positive", + "negative": "negative" + }, + "preSampling":{ + "pipe": "pipe", + "image_to_latent": "image_to_latent", + "latent": "latent" + }, + "kSampler":{ + "pipe": "pipe", + "model": "model" + }, + "controlnet":{ + "pipe": "pipe", + "image": "image", + "image_kps": "image_kps", + "control_net": "control_net", + "positive": "positive", + "negative": "negative", + "mask": "mask" + }, + "positive_prompt":{ + + }, + "ipadapter":{ + "model":"model", + "image":"image", + "image_style": "image", + "attn_mask":"attn_mask", + "optional_ipadapter":"optional_ipadapter" + }, + "inpaint":{ + "pipe": "pipe", + "image": "image", + "mask": "mask" + } +}; + +const outputMapping = { + "loaders":{ + "pipe": "pipe", + "model": "model", + "vae": "vae", + "clip": null, + "positive": null, + "negative": null, + "latent": null, + }, + "preSampling":{ + "pipe":"pipe" + }, + "kSampler":{ + "pipe": "pipe", + "image": "image" + }, + "controlnet":{ + "pipe": "pipe", + "positive": "positive", + "negative": "negative" + }, + "positive_prompt":{ + "text": "positive", + "positive": "text" + }, + "load_image":{ + "IMAGE":"IMAGE", + "MASK": "MASK" + }, + "ipadapter":{ + "model":"model", + "tiles":"tiles", + "masks":"masks", + "ipadapter":"ipadapter" + }, + "inpaint":{ + "pipe": "pipe", + } +}; + +// 替换节点 +function replaceNode(oldNode, newNodeName, type) { + const newNode = LiteGraph.createNode(newNodeName); + if (!newNode) { + return; + } + app.graph.add(newNode); + + newNode.pos = oldNode.pos.slice(); + newNode.size = oldNode.size.slice(); + + oldNode.widgets.forEach(widget => { + if(widgetMapping[type][widget.name]){ + const newName = widgetMapping[type][widget.name]; + if (newName) { + const newWidget = findWidgetByName(newNode, newName); + if (newWidget) { + newWidget.value = widget.value; + if(widget.name == 'seed_num'){ + newWidget.linkedWidgets[0].value = widget.linkedWidgets[0].value + } + if(widget.type == 'converted-widget'){ + convertToInput(newNode, newWidget, widget); + } + } + } + } + + }); + + if(oldNode.inputs){ + oldNode.inputs.forEach((input, index) => { + if (input && input.link && inputMapping[type][input.name]) { + const newInputName = inputMapping[type][input.name]; + // If the new node does not have this output, skip + if (newInputName === null) { + return; + } + const newInputIndex = newNode.findInputSlot(newInputName); + if (newInputIndex !== -1) { + const originLinkInfo = oldNode.graph.links[input.link]; + if (originLinkInfo) { + const originNode = oldNode.graph.getNodeById(originLinkInfo.origin_id); + if (originNode) { + originNode.connect(originLinkInfo.origin_slot, newNode, newInputIndex); + } + } + } + } + }); + } + + if(oldNode.outputs){ + oldNode.outputs.forEach((output, index) => { + if (output && output.links && outputMapping[type] && outputMapping[type][output.name]) { + const newOutputName = outputMapping[type][output.name]; + // If the new node does not have this output, skip + if (newOutputName === null) { + return; + } + const newOutputIndex = newNode.findOutputSlot(newOutputName); + if (newOutputIndex !== -1) { + output.links.forEach(link => { + const targetLinkInfo = oldNode.graph.links[link]; + if (targetLinkInfo) { + const targetNode = oldNode.graph.getNodeById(targetLinkInfo.target_id); + if (targetNode) { + newNode.connect(newOutputIndex, targetNode, targetLinkInfo.target_slot); + } + } + }); + } + } + }); + } + + + // Remove old node + app.graph.remove(oldNode); + + // Remove others + if(newNode.type == 'easy fullkSampler'){ + const link_output_id = newNode.outputs[0].links + if(link_output_id && link_output_id[0]){ + const nodes = app.graph._nodes + const node = nodes.find(cate=> cate.inputs && cate.inputs[0] && cate.inputs[0]['link'] == link_output_id[0]) + if(node){ + app.graph.remove(node); + } + } + }else if(preSampling.includes(newNode.type)){ + const link_output_id = newNode.outputs[0].links + if(!link_output_id || !link_output_id[0]){ + const ksampler = LiteGraph.createNode('easy kSampler'); + app.graph.add(ksampler); + ksampler.pos = newNode.pos.slice(); + ksampler.pos[0] = ksampler.pos[0] + newNode.size[0] + 20; + const newInputIndex = newNode.findInputSlot('pipe'); + if (newInputIndex !== -1) { + if (newNode) { + newNode.connect(0, ksampler, newInputIndex); + } + } + } + } + + // autoHeight + newNode.setSize([newNode.size[0], newNode.computeSize()[1]]); +} + +export function findWidgetByName(node, widgetName) { + return node.widgets.find(widget => typeof widgetName == 'object' ? widgetName.includes(widget.name) : widget.name === widgetName); +} +function replaceNodeMenuCallback(currentNode, targetNodeName, type) { + return function() { + replaceNode(currentNode, targetNodeName, type); + }; +} +const addMenuHandler = (nodeType, cb)=> { + const getOpts = nodeType.prototype.getExtraMenuOptions; + nodeType.prototype.getExtraMenuOptions = function () { + const r = getOpts.apply(this, arguments); + cb.apply(this, arguments); + return r; + }; +} +const addMenu = (content, type, nodes_include, nodeType, has_submenu=true) => { + addMenuHandler(nodeType, function (_, options) { + options.unshift({ + content: content, + has_submenu: has_submenu, + callback: (value, options, e, menu, node) => showSwapMenu(value, options, e, menu, node, type, nodes_include) + }) + if(type == 'loaders') { + options.unshift({ + content: $t("💎 View Lora Info..."), + callback: (value, options, e, menu, node) => { + const widget = node.widgets.find(cate => cate.name == 'lora_name') + let name = widget.value; + if (!name || name == 'None') return + new LoraInfoDialog(name).show('loras', name); + } + }) + options.unshift({ + content: $t("💎 View Checkpoint Info..."), + callback: (value, options, e, menu, node) => { + let name = node.widgets[0].value; + if (!name || name == 'None') return + new CheckpointInfoDialog(name).show('checkpoints', name); + } + }) + } + }) +} +const showSwapMenu = (value, options, e, menu, node, type, nodes_include) => { + const swapOptions = []; + nodes_include.map(cate=>{ + if (node.type !== cate) { + swapOptions.push({ + content: `${cate}`, + callback: replaceNodeMenuCallback(node, cate, type) + }); + } + }) + new LiteGraph.ContextMenu(swapOptions, { + event: e, + callback: null, + parentMenu: menu, + node: node + }); + return false; +} + +// 重载节点 +const CONVERTED_TYPE = "converted-widget"; +const GET_CONFIG = Symbol(); + +function hideWidget(node, widget, suffix = "") { + widget.origType = widget.type; + widget.origComputeSize = widget.computeSize; + widget.origSerializeValue = widget.serializeValue; + widget.computeSize = () => [0, -4]; // -4 is due to the gap litegraph adds between widgets automatically + widget.type = CONVERTED_TYPE + suffix; + widget.serializeValue = () => { + // Prevent serializing the widget if we have no input linked + if (!node.inputs) { + return undefined; + } + let node_input = node.inputs.find((i) => i.widget?.name === widget.name); + + if (!node_input || !node_input.link) { + return undefined; + } + return widget.origSerializeValue ? widget.origSerializeValue() : widget.value; + }; + + // Hide any linked widgets, e.g. seed+seedControl + if (widget.linkedWidgets) { + for (const w of widget.linkedWidgets) { + hideWidget(node, w, ":" + widget.name); + } + } +} +function convertToInput(node, widget, config) { + console.log('config:', config) + hideWidget(node, widget); + + const { type } = getWidgetType(config); + + // Add input and store widget config for creating on primitive node + const sz = node.size; + if(!widget.options || !widget.options.forceInput){ + node.addInput(widget.name, type, { + widget: { name: widget.name, [GET_CONFIG]: () => config }, + }); + } + + for (const widget of node.widgets) { + widget.last_y += LiteGraph.NODE_SLOT_HEIGHT; + } + + // Restore original size but grow if needed + node.setSize([Math.max(sz[0], node.size[0]), Math.max(sz[1], node.size[1])]); +} + +function getWidgetType(config) { + // Special handling for COMBO so we restrict links based on the entries + let type = config[0]; + if (type instanceof Array) { + type = "COMBO"; + } + return { type }; +} + +const reloadNode = function (node) { + const nodeType = node.constructor.type; + const origVals = node.properties.origVals || {}; + + const nodeTitle = origVals.title || node.title; + const nodeColor = origVals.color || node.color; + const bgColor = origVals.bgcolor || node.bgcolor; + const oldNode = node + const options = { + 'size': [...node.size], + 'color': nodeColor, + 'bgcolor': bgColor, + 'pos': [...node.pos] + } + + let inputLinks = [] + let outputLinks = [] + if(node.inputs){ + for (const input of node.inputs) { + if (input.link) { + const input_name = input.name + const input_slot = node.findInputSlot(input_name) + const input_node = node.getInputNode(input_slot) + const input_link = node.getInputLink(input_slot) + inputLinks.push([input_link.origin_slot, input_node, input_name]) + } + } + } + if(node.outputs) { + for (const output of node.outputs) { + if (output.links) { + const output_name = output.name + + for (const linkID of output.links) { + const output_link = graph.links[linkID] + const output_node = graph._nodes_by_id[output_link.target_id] + outputLinks.push([output_name, output_node, output_link.target_slot]) + } + } + } + } + + app.graph.remove(node) + const newNode = app.graph.add(LiteGraph.createNode(nodeType, nodeTitle, options)); + + function handleLinks() { + // re-convert inputs + if(oldNode.widgets) { + for (let w of oldNode.widgets) { + if (w.type === 'converted-widget') { + const WidgetToConvert = newNode.widgets.find((nw) => nw.name === w.name); + for (let i of oldNode.inputs) { + if (i.name === w.name) { + convertToInput(newNode, WidgetToConvert, i.widget); + } + } + } + } + } + // replace input and output links + for (let input of inputLinks) { + const [output_slot, output_node, input_name] = input; + output_node.connect(output_slot, newNode.id, input_name) + } + for (let output of outputLinks) { + const [output_name, input_node, input_slot] = output; + newNode.connect(output_name, input_node, input_slot) + } + } + + // fix widget values + let values = oldNode.widgets_values; + if (!values && newNode.widgets?.length>0) { + newNode.widgets.forEach((newWidget, index) => { + const oldWidget = oldNode.widgets[index]; + if (newWidget.name === oldWidget.name && newWidget.type === oldWidget.type) { + newWidget.value = oldWidget.value; + } + }); + handleLinks(); + return; + } + let pass = false + const isIterateForwards = values?.length <= newNode.widgets?.length; + let vi = isIterateForwards ? 0 : values.length - 1; + function evalWidgetValues(testValue, newWidg) { + if (testValue === true || testValue === false) { + if (newWidg.options?.on && newWidg.options?.off) { + return { value: testValue, pass: true }; + } + } else if (typeof testValue === "number") { + if (newWidg.options?.min <= testValue && testValue <= newWidg.options?.max) { + return { value: testValue, pass: true }; + } + } else if (newWidg.options?.values?.includes(testValue)) { + return { value: testValue, pass: true }; + } else if (newWidg.inputEl && typeof testValue === "string") { + return { value: testValue, pass: true }; + } + return { value: newWidg.value, pass: false }; + } + const updateValue = (wi) => { + const oldWidget = oldNode.widgets[wi]; + let newWidget = newNode.widgets[wi]; + if (newWidget.name === oldWidget.name && newWidget.type === oldWidget.type) { + while ((isIterateForwards ? vi < values.length : vi >= 0) && !pass) { + let { value, pass } = evalWidgetValues(values[vi], newWidget); + if (pass && value !== null) { + newWidget.value = value; + break; + } + vi += isIterateForwards ? 1 : -1; + } + vi++ + if (!isIterateForwards) { + vi = values.length - (newNode.widgets?.length - 1 - wi); + } + } + }; + if (isIterateForwards && newNode.widgets?.length>0) { + for (let wi = 0; wi < newNode.widgets.length; wi++) { + updateValue(wi); + } + } else if(newNode.widgets?.length>0){ + for (let wi = newNode.widgets.length - 1; wi >= 0; wi--) { + updateValue(wi); + } + } + handleLinks(); +}; + + +app.registerExtension({ + name: "comfy.easyUse.extraMenu", + async beforeRegisterNodeDef(nodeType, nodeData, app) { + // 刷新节点 + addMenuHandler(nodeType, function (_, options) { + options.unshift({ + content: $t("🔃 Reload Node"), + callback: (value, options, e, menu, node) => { + let graphcanvas = LGraphCanvas.active_canvas; + if (!graphcanvas.selected_nodes || Object.keys(graphcanvas.selected_nodes).length <= 1) { + reloadNode(node); + } else { + for (let i in graphcanvas.selected_nodes) { + reloadNode(graphcanvas.selected_nodes[i]); + } + } + } + }) + // ckptNames + if(nodeData.name == 'easy ckptNames'){ + options.unshift({ + content: $t("💎 View Checkpoint Info..."), + callback: (value, options, e, menu, node) => { + let name = node.widgets[0].value; + if (!name || name == 'None') return + new CheckpointInfoDialog(name).show('checkpoints', name); + } + }) + } + }) + + // Swap提示词 + if (positive_prompt.includes(nodeData.name)) { + addMenu("↪️ Swap EasyPrompt", 'positive_prompt', positive_prompt, nodeType) + } + // Swap加载器 + if (loaders.includes(nodeData.name)) { + addMenu("↪️ Swap EasyLoader", 'loaders', loaders, nodeType) + } + // Swap预采样器 + if (preSampling.includes(nodeData.name)) { + addMenu("↪️ Swap EasyPreSampling", 'preSampling', preSampling, nodeType) + } + // Swap kSampler + if (kSampler.includes(nodeData.name)) { + addMenu("↪️ Swap EasyKSampler", 'preSampling', kSampler, nodeType) + } + // Swap ControlNet + if (controlnet.includes(nodeData.name)) { + addMenu("↪️ Swap EasyControlnet", 'controlnet', controlnet, nodeType) + } + // Swap IPAdapater + if (ipadapter.includes(nodeData.name)) { + addMenu("↪️ Swap EasyAdapater", 'ipadapter', ipadapter, nodeType) + } + // Swap Image + if (imageNode.includes(nodeData.name)) { + addMenu("↪️ Swap LoadImage", 'load_image', imageNode, nodeType) + } + // Swap inpaint + if (inpaint.includes(nodeData.name)) { + addMenu("↪️ Swap InpaintNode", 'inpaint', inpaint, nodeType) + } + } +}); + diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/easy/easyInterface.js b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/easy/easyInterface.js new file mode 100644 index 0000000000000000000000000000000000000000..5f73e3390ddb6a9ef30ae75d8336d41502f44f06 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/easy/easyInterface.js @@ -0,0 +1,788 @@ +import { app } from "../../../../scripts/app.js"; +import { api } from "../../../../scripts/api.js"; +import { $el } from "../../../../scripts/ui.js"; +import {addPreconnect, addCss} from "../common/utils.js"; + +const locale = localStorage['AGL.Locale'] || localStorage['Comfy.Settings.AGL.Locale'] || 'en-US' + +const customThemeColor = "#236692" +const customThemeColorLight = "#3485bb" +// 增加Slot颜色 +const customPipeLineLink = "#7737AA" +const customPipeLineSDXLLink = "#7737AA" +const customIntLink = "#29699C" +const customXYPlotLink = "#74DA5D" +const customLoraStackLink = "#94dccd" +const customXYLink = "#38291f" + +var customLinkColors = JSON.parse(localStorage.getItem('Comfy.Settings.ttN.customLinkColors')) || {}; +if (!customLinkColors["PIPE_LINE"] || !LGraphCanvas.link_type_colors["PIPE_LINE"]) {customLinkColors["PIPE_LINE"] = customPipeLineLink;} +if (!customLinkColors["PIPE_LINE_SDXL"] || !LGraphCanvas.link_type_colors["PIPE_LINE_SDXL"]) {customLinkColors["PIPE_LINE_SDXL"] = customPipeLineSDXLLink;} +if (!customLinkColors["INT"] || !LGraphCanvas.link_type_colors["INT"]) {customLinkColors["INT"] = customIntLink;} +if (!customLinkColors["XYPLOT"] || !LGraphCanvas.link_type_colors["XYPLOT"]) {customLinkColors["XYPLOT"] = customXYPlotLink;} +if (!customLinkColors["X_Y"] || !LGraphCanvas.link_type_colors["X_Y"]) {customLinkColors["X_Y"] = customXYLink;} +if (!customLinkColors["LORA_STACK"] || !LGraphCanvas.link_type_colors["LORA_STACK"]) {customLinkColors["LORA_STACK"] = customLoraStackLink;} +if (!customLinkColors["CONTROL_NET_STACK"] || !LGraphCanvas.link_type_colors["CONTROL_NET_STACK"]) {customLinkColors["CONTROL_NET_STACK"] = customLoraStackLink;} + +localStorage.setItem('Comfy.Settings.easyUse.customLinkColors', JSON.stringify(customLinkColors)); + +// 增加自定义主题 +const ui = { + "version": 102, + "id": "obsidian", + "name": "Obsidian", + "colors": { + "node_slot": { + "CLIP": "#FFD500", + "CLIP_VISION": "#A8DADC", + "CLIP_VISION_OUTPUT": "#ad7452", + "CONDITIONING": "#FFA931", + "CONTROL_NET": "#6EE7B7", + "IMAGE": "#64B5F6", + "LATENT": "#FF9CF9", + "MASK": "#81C784", + "MODEL": "#B39DDB", + "STYLE_MODEL": "#C2FFAE", + "VAE": "#FF6E6E", + "TAESD": "#DCC274", + "PIPE_LINE": customPipeLineLink, + "PIPE_LINE_SDXL": customPipeLineSDXLLink, + "INT": customIntLink, + "XYPLOT": customXYPlotLink, + "X_Y": customXYLink + }, + "litegraph_base": { + "BACKGROUND_IMAGE": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAQBJREFUeNrs1rEKwjAUhlETUkj3vP9rdmr1Ysammk2w5wdxuLgcMHyptfawuZX4pJSWZTnfnu/lnIe/jNNxHHGNn//HNbbv+4dr6V+11uF527arU7+u63qfa/bnmh8sWLBgwYJlqRf8MEptXPBXJXa37BSl3ixYsGDBMliwFLyCV/DeLIMFCxYsWLBMwSt4Be/NggXLYMGCBUvBK3iNruC9WbBgwYJlsGApeAWv4L1ZBgsWLFiwYJmCV/AK3psFC5bBggULloJX8BpdwXuzYMGCBctgwVLwCl7Be7MMFixYsGDBsu8FH1FaSmExVfAxBa/gvVmwYMGCZbBg/W4vAQYA5tRF9QYlv/QAAAAASUVORK5CYII=", + "CLEAR_BACKGROUND_COLOR": "#222222", + "NODE_TITLE_COLOR": "rgba(255,255,255,.75)", + "NODE_SELECTED_TITLE_COLOR": "#FFF", + "NODE_TEXT_SIZE": 14, + "NODE_TEXT_COLOR": "#b8b8b8", + "NODE_SUBTEXT_SIZE": 12, + "NODE_DEFAULT_COLOR": "rgba(0,0,0,.8)", + "NODE_DEFAULT_BGCOLOR": "rgba(22,22,22,.8)", + "NODE_DEFAULT_BOXCOLOR": "rgba(255,255,255,.75)", + "NODE_DEFAULT_SHAPE": "box", + "NODE_BOX_OUTLINE_COLOR": customThemeColor, + "DEFAULT_SHADOW_COLOR": "rgba(0,0,0,0)", + "DEFAULT_GROUP_FONT": 24, + + "WIDGET_BGCOLOR": "#242424", + "WIDGET_OUTLINE_COLOR": "#333", + "WIDGET_TEXT_COLOR": "#a3a3a8", + "WIDGET_SECONDARY_TEXT_COLOR": "#97979c", + + "LINK_COLOR": "#9A9", + "EVENT_LINK_COLOR": "#A86", + "CONNECTING_LINK_COLOR": "#AFA" + }, + "comfy_base": { + "fg-color": "#fff", + "bg-color": "#242424", + "comfy-menu-bg": "rgba(24,24,24,.9)", + "comfy-input-bg": "#262626", + "input-text": "#ddd", + "descrip-text": "#999", + "drag-text": "#ccc", + "error-text": "#ff4444", + "border-color": "#29292c", + "tr-even-bg-color": "rgba(28,28,28,.9)", + "tr-odd-bg-color": "rgba(19,19,19,.9)" + } + } +} + +let custom_theme = null +let control_mode = null +try{ + custom_theme = localStorage.getItem('Comfy.Settings.Comfy.CustomColorPalettes') ? JSON.parse(localStorage.getItem('Comfy.Settings.Comfy.CustomColorPalettes')) : {}; +} +catch (e) {custom_theme = {}} +try{ + const dark_bg = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAACXBIWXMAAAsTAAALEwEAmpwYAAAGlmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgOS4xLWMwMDEgNzkuMTQ2Mjg5OSwgMjAyMy8wNi8yNS0yMDowMTo1NSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0RXZ0PSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VFdmVudCMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIDI1LjEgKFdpbmRvd3MpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAyMy0xMS0xM1QwMDoxODowMiswMTowMCIgeG1wOk1vZGlmeURhdGU9IjIwMjMtMTEtMTVUMDI6MDQ6NTkrMDE6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMjMtMTEtMTVUMDI6MDQ6NTkrMDE6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOmIyYzRhNjA5LWJmYTctYTg0MC1iOGFlLTk3MzE2ZjM1ZGIyNyIgeG1wTU06RG9jdW1lbnRJRD0iYWRvYmU6ZG9jaWQ6cGhvdG9zaG9wOjk0ZmNlZGU4LTE1MTctZmQ0MC04ZGU3LWYzOTgxM2E3ODk5ZiIgeG1wTU06T3JpZ2luYWxEb2N1bWVudElEPSJ4bXAuZGlkOjIzMWIxMGIwLWI0ZmItMDI0ZS1iMTJlLTMwNTMwM2NkMDdjOCI+IDx4bXBNTTpIaXN0b3J5PiA8cmRmOlNlcT4gPHJkZjpsaSBzdEV2dDphY3Rpb249ImNyZWF0ZWQiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6MjMxYjEwYjAtYjRmYi0wMjRlLWIxMmUtMzA1MzAzY2QwN2M4IiBzdEV2dDp3aGVuPSIyMDIzLTExLTEzVDAwOjE4OjAyKzAxOjAwIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgMjUuMSAoV2luZG93cykiLz4gPHJkZjpsaSBzdEV2dDphY3Rpb249InNhdmVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOjQ4OWY1NzlmLTJkNjUtZWQ0Zi04OTg0LTA4NGE2MGE1ZTMzNSIgc3RFdnQ6d2hlbj0iMjAyMy0xMS0xNVQwMjowNDo1OSswMTowMCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWRvYmUgUGhvdG9zaG9wIDI1LjEgKFdpbmRvd3MpIiBzdEV2dDpjaGFuZ2VkPSIvIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDpiMmM0YTYwOS1iZmE3LWE4NDAtYjhhZS05NzMxNmYzNWRiMjciIHN0RXZ0OndoZW49IjIwMjMtMTEtMTVUMDI6MDQ6NTkrMDE6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCAyNS4xIChXaW5kb3dzKSIgc3RFdnQ6Y2hhbmdlZD0iLyIvPiA8L3JkZjpTZXE+IDwveG1wTU06SGlzdG9yeT4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz4OTe6GAAAAx0lEQVR42u3WMQoAIQxFwRzJys77X8vSLiRgITif7bYbgrwYc/mKXyBoY4VVBgsWLFiwYFmOlTv+9jfDOjHmr8u6eVkGCxYsWLBgmc5S8ApewXvgYRksWLBgKXidpeBdloL3wMOCBctgwVLwCl7BuyyDBQsWLFiwTGcpeAWv4D3wsAwWLFiwFLzOUvAuS8F74GHBgmWwYCl4Ba/gXZbBggULFixYprMUvIJX8B54WAYLFixYCl5nKXiXpeA98LBgwTJYsGC9tg1o8f4TTtqzNQAAAABJRU5ErkJggg==" + // 修改自定义主题 + if(!custom_theme || !custom_theme.obsidian || !custom_theme.obsidian.version || custom_theme.obsidian.version0) node.execute_triggered--; + if (node.action_triggered>0) node.action_triggered--; + }; + LGraphCanvas.prototype.drawNodeWidgets = function( + node, + posY, + ctx, + active_widget + ) { + if (!node.widgets || !node.widgets.length) { + return 0; + } + var width = node.size[0]; + var widgets = node.widgets; + posY += 2; + var H = LiteGraph.NODE_WIDGET_HEIGHT; + var show_text = this.ds.scale > 0.5; + ctx.save(); + ctx.globalAlpha = this.editor_alpha; + var outline_color = LiteGraph.WIDGET_OUTLINE_COLOR; + var background_color = LiteGraph.WIDGET_BGCOLOR; + var text_color = LiteGraph.WIDGET_TEXT_COLOR; + var secondary_text_color = LiteGraph.WIDGET_SECONDARY_TEXT_COLOR; + var margin = 12; + + for (var i = 0; i < widgets.length; ++i) { + var w = widgets[i]; + var y = posY; + if (w.y) { + y = w.y; + } + w.last_y = y; + ctx.strokeStyle = outline_color; + ctx.fillStyle = "#222"; + ctx.textAlign = "left"; + ctx.lineWidth = 1; + if(w.disabled) + ctx.globalAlpha *= 0.5; + var widget_width = w.width || width; + + switch (w.type) { + case "button": + ctx.font = "10px Inter" + ctx.fillStyle = background_color; + if (w.clicked) { + ctx.fillStyle = "#AAA"; + w.clicked = false; + this.dirty_canvas = true; + } + ctx.beginPath(); + ctx.roundRect(margin, y, widget_width - margin * 2, H, [H * 0.25]); + ctx.fill(); + if(show_text && !w.disabled) + ctx.stroke(); + if (show_text) { + ctx.textAlign = "center"; + ctx.fillStyle = text_color; + ctx.fillText(w.label || w.name, widget_width * 0.5, y + H * 0.7); + } + break; + case "toggle": + ctx.font = "10px Inter" + ctx.textAlign = "left"; + ctx.strokeStyle = outline_color; + ctx.fillStyle = background_color; + ctx.beginPath(); + if (show_text) + ctx.roundRect(margin, y, widget_width - margin * 2, H, [H * 0.25]); + else + ctx.rect(margin, y, widget_width - margin * 2, H ); + ctx.fill(); + if(show_text && !w.disabled) + ctx.stroke(); + ctx.fillStyle = w.value ? customThemeColor : "#333"; + ctx.beginPath(); + ctx.arc( widget_width - margin * 2, y + H * 0.5, H * 0.25, 0, Math.PI * 2 ); + ctx.fill(); + if (show_text) { + ctx.fillStyle = secondary_text_color; + const label = w.label || w.name; + if (label != null) { + ctx.fillText(label, margin * 1.6, y + H * 0.7); + } + ctx.font = "10px Inter" + ctx.fillStyle = w.value ? text_color : secondary_text_color; + ctx.textAlign = "right"; + ctx.fillText( + w.value + ? w.options.on || "true" + : w.options.off || "false", + widget_width - 35, + y + H * 0.7 + ); + } + break; + case "slider": + ctx.font = "10px Inter" + ctx.fillStyle = background_color; + ctx.strokeStyle = outline_color; + ctx.beginPath(); + ctx.roundRect(margin, y, widget_width - margin * 2, H, [H*0.25]); + ctx.fill(); + ctx.stroke() + var range = w.options.max - w.options.min; + var nvalue = (w.value - w.options.min) / range; + if(nvalue < 0.0) nvalue = 0.0; + if(nvalue > 1.0) nvalue = 1.0; + ctx.fillStyle = w.options.hasOwnProperty("slider_color") ? w.options.slider_color : (active_widget == w ? "#333" : customThemeColor); + ctx.beginPath(); + ctx.roundRect(margin, y, nvalue * (widget_width - margin * 2), H, [H*0.25]); + ctx.fill(); + if (w.marker) { + var marker_nvalue = (w.marker - w.options.min) / range; + if(marker_nvalue < 0.0) marker_nvalue = 0.0; + if(marker_nvalue > 1.0) marker_nvalue = 1.0; + ctx.fillStyle = w.options.hasOwnProperty("marker_color") ? w.options.marker_color : "#AA9"; + ctx.roundRect( margin + marker_nvalue * (widget_width - margin * 2), y, 2, H , [H * 0.25] ); + } + if (show_text) { + ctx.textAlign = "center"; + ctx.fillStyle = text_color; + var text = (w.label || w.name) + ": " + (Number(w.value).toFixed(w.options.precision != null ? w.options.precision : 3)).toString() + ctx.fillText( + text, + widget_width * 0.5, + y + H * 0.7 + ); + + } + break; + case "number": + case "combo": + ctx.textAlign = "left"; + ctx.strokeStyle = outline_color; + ctx.fillStyle = background_color; + ctx.beginPath(); + if(show_text) + ctx.roundRect(margin, y, widget_width - margin * 2, H, [H * 0.25] ); + else + ctx.rect(margin, y, widget_width - margin * 2, H ); + ctx.fill(); + if (show_text) { + if(!w.disabled) + ctx.stroke(); + ctx.fillStyle = text_color; + if(!w.disabled) + { + ctx.beginPath(); + ctx.moveTo(margin + 12, y + 6.5); + ctx.lineTo(margin + 6, y + H * 0.5); + ctx.lineTo(margin + 12, y + H - 6.5); + ctx.fill(); + ctx.beginPath(); + ctx.moveTo(widget_width - margin - 12, y + 6.5); + ctx.lineTo(widget_width - margin - 6, y + H * 0.5); + ctx.lineTo(widget_width - margin - 12, y + H - 6.5); + ctx.fill(); + } + ctx.fillStyle = secondary_text_color; + ctx.font = "10px Inter" + ctx.fillText(w.label || w.name, margin * 2 + 5, y + H * 0.7); + ctx.fillStyle = text_color; + ctx.textAlign = "right"; + var rightDistance = 6 + if (w.type == "number") { + ctx.font = "10px Inter,JetBrains Mono,monospace" + ctx.fillText( + Number(w.value).toFixed( + w.options.precision !== undefined + ? w.options.precision + : 3 + ), + widget_width - margin * 2 - rightDistance, + y + H * 0.7 + ); + } else { + var v = w.value; + if( w.options.values ) + { + var values = w.options.values; + if( values.constructor === Function ) + values = values(); + if(values && values.constructor !== Array) + v = values[ w.value ]; + } + ctx.fillText( + v, + widget_width - margin * 2 - rightDistance, + y + H * 0.7 + ); + } + } + break; + case "string": + case "text": + ctx.textAlign = "left"; + ctx.strokeStyle = outline_color; + ctx.fillStyle = background_color; + ctx.beginPath(); + if (show_text) + ctx.roundRect(margin, y, widget_width - margin * 2, H, [H * 0.25]); + else + ctx.rect( margin, y, widget_width - margin * 2, H ); + ctx.fill(); + if (show_text) { + if(!w.disabled) + ctx.stroke(); + ctx.save(); + ctx.beginPath(); + ctx.rect(margin, y, widget_width - margin * 2, H); + ctx.clip(); + + //ctx.stroke(); + ctx.fillStyle = secondary_text_color; + const label = w.label || w.name; + ctx.font = "10px Inter" + if (label != null) { + ctx.fillText(label, margin * 2, y + H * 0.7); + } + ctx.fillStyle = text_color; + ctx.textAlign = "right"; + ctx.fillText(String(w.value).substr(0,30), widget_width - margin * 2, y + H * 0.7); //30 chars max + ctx.restore(); + } + break; + default: + if (w.draw) { + w.draw(ctx, node, widget_width, y, H); + } + break; + } + posY += (w.computeSize ? w.computeSize(widget_width)[1] : H) + 4; + ctx.globalAlpha = this.editor_alpha; + + } + ctx.restore(); + ctx.textAlign = "left"; + }; + } +}catch(e){ + console.error(e) +} + +function updateControlWidgetLabel(widget, controlValueRunBefore=false) { + let replacement = "after"; + let find = "before"; + if (controlValueRunBefore) { + [find, replacement] = [replacement, find] + } + widget.label = (widget.label ?? widget.name).replace(find, replacement); + widget.name = widget.label; +} + +// 节点颜色 +const COLOR_THEMES = LGraphCanvas.node_colors +const NODE_COLORS = { + "easy positive":"green", + "easy negative":"red", + "easy promptList":"cyan", + "easy promptLine":"cyan", + "easy promptConcat":"cyan", + "easy promptReplace":"cyan", + "easy XYInputs: Seeds++ Batch": customXYLink, + "easy XYInputs: ModelMergeBlocks": customXYLink, + 'easy textSwitch': "pale_blue" +} + +function setNodeColors(node, theme) { + if (!theme) {return;} + if(theme.color) node.color = theme.color; + if(theme.bgcolor) node.bgcolor = theme.bgcolor; +} + + +app.registerExtension({ + name: "comfy.easyUse.interface", + setup() { + Object.assign(app.canvas.default_connection_color_byType, customLinkColors); + Object.assign(LGraphCanvas.link_type_colors, customLinkColors); + }, + + async nodeCreated(node) { + if (NODE_COLORS.hasOwnProperty(node.comfyClass)) { + const colorKey = NODE_COLORS[node.comfyClass] + const theme = COLOR_THEMES[colorKey]; + setNodeColors(node, theme); + } + // 修复官方bug: 应该初始化修改节点的control_mode name + if(control_mode && control_mode == 'before'){ + const controlValueRunBefore = control_mode == 'before' + if(node.widgets && node.widgets.length>0) { + for (const w of node.widgets) { + if (['control_before_generate', 'control_after_generate'].includes(w.name)) { + await updateControlWidgetLabel(w, controlValueRunBefore); + if (w.linkedWidgets) { + for (const l of w.linkedWidgets) { + await updateControlWidgetLabel(l, controlValueRunBefore); + } + } + } + } + } + } + }, +}) diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/easy/easyQuick.js b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/easy/easyQuick.js new file mode 100644 index 0000000000000000000000000000000000000000..394bd8a31508a9c1f2f669f2030ea5847d36699e --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/easy/easyQuick.js @@ -0,0 +1,250 @@ +// 1.0.2 +import { app } from "../../../../scripts/app.js"; +import { GroupNodeConfig } from "../../../../extensions/core/groupNode.js"; +import { api } from "../../../../scripts/api.js"; +import { $t } from "../common/i18n.js" + +const nodeTemplateShortcutId = "Comfy.EasyUse.NodeTemplateShortcut" +const processBarId = "Comfy.EasyUse.queueProcessBar" + +let enableNodeTemplateShortcut = true +let enableQueueProcess = false + +export function addNodeTemplateShortcutSetting(app) { + app.ui.settings.addSetting({ + id: nodeTemplateShortcutId, + name: $t("Enable ALT+1~9 to paste nodes from nodes template (ComfyUI-Easy-Use)"), + type: "boolean", + defaultValue: enableNodeTemplateShortcut, + onChange(value) { + enableNodeTemplateShortcut = !!value; + }, + }); +} +export function addQueueProcessSetting(app) { + app.ui.settings.addSetting({ + id: processBarId, + name: $t("Enable process bar in queue button (ComfyUI-Easy-Use)"), + type: "boolean", + defaultValue: enableQueueProcess, + onChange(value) { + enableQueueProcess = !!value; + }, + }); +} +const getEnableNodeTemplateShortcut = _ => app.ui.settings.getSettingValue(nodeTemplateShortcutId, true) +const getQueueProcessSetting = _ => app.ui.settings.getSettingValue(processBarId, false) + +function loadTemplate(){ + return localStorage['Comfy.NodeTemplates'] ? JSON.parse(localStorage['Comfy.NodeTemplates']) : null +} +const clipboardAction = async (cb) => { + const old = localStorage.getItem("litegrapheditor_clipboard"); + await cb(); + localStorage.setItem("litegrapheditor_clipboard", old); +}; +async function addTemplateToCanvas(t){ + const data = JSON.parse(t.data); + await GroupNodeConfig.registerFromWorkflow(data.groupNodes, {}); + localStorage.setItem("litegrapheditor_clipboard", t.data); + app.canvas.pasteFromClipboard(); +} + +app.registerExtension({ + name: 'comfy.easyUse.quick', + init() { + const keybindListener = async function (event) { + let modifierPressed = event.altKey; + const isEnabled = getEnableNodeTemplateShortcut() + if(isEnabled){ + const mac_alt_nums = ['¡','™','£','¢','∞','§','¶','•','ª'] + const nums = ['1','2','3','4','5','6','7','8','9'] + let key = event.key + if(mac_alt_nums.includes(key)){ + const idx = mac_alt_nums.findIndex(cate=> cate == key) + key = nums[idx] + modifierPressed = true + } + if(['1','2','3','4','5','6','7','8','9'].includes(key) && modifierPressed) { + const template = loadTemplate() + const idx = parseInt(key) - 1 + if (template && template[idx]) { + let t = template[idx] + try{ + let data = JSON.parse(t.data) + data.title = t.name + t.data = JSON.stringify(data) + clipboardAction(_ => { + addTemplateToCanvas(t) + }) + }catch (e){ + console.error(e) + } + + } + if (event.ctrlKey || event.altKey || event.metaKey) { + return; + } + } + } + + } + window.addEventListener("keydown", keybindListener, true); + }, + + setup(app) { + addNodeTemplateShortcutSetting(app) + addQueueProcessSetting(app) + registerListeners() + } +}); + +const registerListeners = () => { + const queue_button = document.getElementById("queue-button") + const old_queue_button_text = queue_button.innerText + api.addEventListener('progress', ({ + detail, + }) => { + const isEnabled = getQueueProcessSetting() + if(isEnabled){ + const { + value, max, node, + } = detail; + const progress = Math.floor((value / max) * 100); + // console.log(progress) + if (!isNaN(progress) && progress >= 0 && progress <= 100) { + queue_button.innerText = progress ==0 || progress == 100 ? old_queue_button_text : "ㅤ " + const width = progress ==0 || progress == 100 ? '0%' : progress.toString() + '%' + let bar = document.createElement("div") + queue_button.setAttribute('data-attr', progress ==0 || progress == 100 ? "" : progress.toString() + '%') + document.documentElement.style.setProperty('--process-bar-width', width) + } + } + + }, false); + + api.addEventListener('status', ({ + detail, + }) => { + const queueRemaining = detail?.exec_info.queue_remaining; + if(queueRemaining === 0){ + let queue_button = document.getElementById("queue-button") + queue_button.innerText = old_queue_button_text + queue_button.setAttribute('data-attr', "") + document.documentElement.style.setProperty('--process-bar-width', '0%') + } + }, false); +}; + + +// 修改粘贴指令 +LGraphCanvas.prototype.pasteFromClipboard = function(isConnectUnselected = false) { + // if ctrl + shift + v is off, return when isConnectUnselected is true (shift is pressed) to maintain old behavior + if (!LiteGraph.ctrl_shift_v_paste_connect_unselected_outputs && isConnectUnselected) { + return; + } + var data = localStorage.getItem("litegrapheditor_clipboard"); + if (!data) { + return; + } + + this.graph.beforeChange(); + + //create nodes + var clipboard_info = JSON.parse(data); + // calculate top-left node, could work without this processing but using diff with last node pos :: clipboard_info.nodes[clipboard_info.nodes.length-1].pos + var posMin = false; + var posMinIndexes = false; + for (var i = 0; i < clipboard_info.nodes.length; ++i) { + if (posMin){ + if(posMin[0]>clipboard_info.nodes[i].pos[0]){ + posMin[0] = clipboard_info.nodes[i].pos[0]; + posMinIndexes[0] = i; + } + if(posMin[1]>clipboard_info.nodes[i].pos[1]){ + posMin[1] = clipboard_info.nodes[i].pos[1]; + posMinIndexes[1] = i; + } + } + else{ + posMin = [clipboard_info.nodes[i].pos[0], clipboard_info.nodes[i].pos[1]]; + posMinIndexes = [i, i]; + } + } + var nodes = []; + var left_arr = [], right_arr = [], top_arr =[], bottom_arr =[]; + + for (var i = 0; i < clipboard_info.nodes.length; ++i) { + var node_data = clipboard_info.nodes[i]; + var node = LiteGraph.createNode(node_data.type); + if (node) { + + node.configure(node_data); + //paste in last known mouse position + node.pos[0] += this.graph_mouse[0] - posMin[0]; //+= 5; + node.pos[1] += this.graph_mouse[1] - posMin[1]; //+= 5; + + left_arr.push(node.pos[0]) + right_arr.push(node.pos[0] + node.size[0]) + top_arr.push(node.pos[1]) + bottom_arr.push(node.pos[1] + node.size[1]) + + this.graph.add(node,{doProcessChange:false}); + + nodes.push(node); + + } + } + + if(clipboard_info.title){ + var l = Math.min(...left_arr) - 15; + var r = Math.max(...right_arr) - this.graph_mouse[0] + 30; + var t = Math.min(...top_arr) - 100; + var b = Math.max(...bottom_arr) - this.graph_mouse[1] + 130; + + // create group + const groups = [ + { + "title": clipboard_info.title, + "bounding": [ + l, + t, + r, + b + ], + "color": "#3f789e", + "font_size": 24, + "locked": false + } + ] + + for (var i = 0; i < groups.length; ++i) { + var group = new LiteGraph.LGraphGroup(); + group.configure(groups[i]); + this.graph.add(group); + } + } + + //create links + for (var i = 0; i < clipboard_info.links.length; ++i) { + var link_info = clipboard_info.links[i]; + var origin_node; + var origin_node_relative_id = link_info[0]; + if (origin_node_relative_id != null) { + origin_node = nodes[origin_node_relative_id]; + } else if (LiteGraph.ctrl_shift_v_paste_connect_unselected_outputs && isConnectUnselected) { + var origin_node_id = link_info[4]; + if (origin_node_id) { + origin_node = this.graph.getNodeById(origin_node_id); + } + } + var target_node = nodes[link_info[2]]; + if( origin_node && target_node ) + origin_node.connect(link_info[1], target_node, link_info[3]); + else + console.warn("Warning, nodes missing on pasting"); + } + + this.selectNodes(nodes); + this.graph.afterChange(); +}; \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/easy/easySaveImage.js b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/easy/easySaveImage.js new file mode 100644 index 0000000000000000000000000000000000000000..35adb752ed1e167fa47083fafa149310dffedd0f --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/easy/easySaveImage.js @@ -0,0 +1,36 @@ +import { app } from "../../../../scripts/app.js"; +import { applyTextReplacements } from "../../../../scripts/utils.js"; + +const extraNodes = ["easy imageSave", "easy fullkSampler", "easy kSampler", "easy kSamplerTiled","easy kSamplerInpainting", "easy kSamplerDownscaleUnet", "easy kSamplerSDTurbo","easy detailerFix"] + +app.registerExtension({ + name: "Comfy.Easy.SaveImageExtraOutput", + async beforeRegisterNodeDef(nodeType, nodeData, app) { + if (extraNodes.includes(nodeData.name)) { + const onNodeCreated = nodeType.prototype.onNodeCreated; + // When the SaveImage node is created we want to override the serialization of the output name widget to run our S&R + nodeType.prototype.onNodeCreated = function () { + const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined; + + const widget = this.widgets.find((w) => w.name === "filename_prefix" || w.name === 'save_prefix'); + widget.serializeValue = () => { + return applyTextReplacements(app, widget.value); + }; + + return r; + }; + } else { + // When any other node is created add a property to alias the node + const onNodeCreated = nodeType.prototype.onNodeCreated; + nodeType.prototype.onNodeCreated = function () { + const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined; + + if (!this.properties || !("Node name for S&R" in this.properties)) { + this.addProperty("Node name for S&R", this.constructor.type, "string"); + } + + return r; + }; + } + }, +}); diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/easy/easySeg.js b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/easy/easySeg.js new file mode 100644 index 0000000000000000000000000000000000000000..11e8feb61f3e0b3f8358c5a38c9a6df9730c965e --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/easy/easySeg.js @@ -0,0 +1,136 @@ +import {app} from "../../../../scripts/app.js"; +import {$el} from "../../../../scripts/ui.js"; +import {$t} from "../common/i18n.js"; +import {findWidgetByName, toggleWidget} from "../common/utils.js"; + + +const tags = { + "selfie_multiclass_256x256": ["Background", "Hair", "Body", "Face", "Clothes", "Others",], + "human_parsing_lip":["Background","Hat","Hair","Glove","Sunglasses","Upper-clothes","Dress","Coat","Socks","Pants","Jumpsuits","Scarf","Skirt","Face","Left-arm","Right-arm","Left-leg","Right-leg","Left-shoe","Right-shoe"], +} +function getTagList(tags) { + let rlist=[] + tags.forEach((k,i) => { + rlist.push($el( + "label.easyuse-prompt-styles-tag", + { + dataset: { + tag: i, + name: $t(k), + index: i + }, + $: (el) => { + el.children[0].onclick = () => { + el.classList.toggle("easyuse-prompt-styles-tag-selected"); + }; + }, + }, + [ + $el("input",{ + type: 'checkbox', + name: i + }), + $el("span",{ + textContent: $t(k), + }) + ] + )) + }); + return rlist +} + + +app.registerExtension({ + name: 'comfy.easyUse.seg', + async beforeRegisterNodeDef(nodeType, nodeData, app) { + + if (nodeData.name == 'easy humanSegmentation') { + // 创建时 + const onNodeCreated = nodeType.prototype.onNodeCreated; + nodeType.prototype.onNodeCreated = function () { + onNodeCreated ? onNodeCreated?.apply(this, arguments) : undefined; + const method = this.widgets.findIndex((w) => w.name == 'method'); + const list = $el("ul.easyuse-prompt-styles-list.no-top", []); + let method_values = '' + this.setProperty("values", []) + + let selector = this.addDOMWidget('mask_components',"btn",$el('div.easyuse-prompt-styles',[list])) + + Object.defineProperty(this.widgets[method],'value',{ + set:(value)=>{ + method_values = value + if(method_values){ + selector.element.children[0].innerHTML = '' + if(method_values == 'selfie_multiclass_256x256'){ + toggleWidget(this, findWidgetByName(this, 'confidence'), true) + this.setSize([300, 260]); + }else{ + toggleWidget(this, findWidgetByName(this, 'confidence')) + this.setSize([300, 500]); + } + let list = getTagList(tags[method_values]); + selector.element.children[0].append(...list) + } + }, + get: () => { + return method_values + } + }) + + let mask_select_values = '' + + Object.defineProperty(selector, "value", { + set: (value) => { + setTimeout(_=>{ + selector.element.children[0].querySelectorAll(".easyuse-prompt-styles-tag").forEach(el => { + let arr = value.split(',') + if (arr.includes(el.dataset.tag)) { + el.classList.add("easyuse-prompt-styles-tag-selected"); + el.children[0].checked = true + } + }) + },100) + }, + get: () => { + selector.element.children[0].querySelectorAll(".easyuse-prompt-styles-tag").forEach(el => { + if(el.classList.value.indexOf("easyuse-prompt-styles-tag-selected")>=0){ + if(!this.properties["values"].includes(el.dataset.tag)){ + this.properties["values"].push(el.dataset.tag); + } + }else{ + if(this.properties["values"].includes(el.dataset.tag)){ + this.properties["values"]= this.properties["values"].filter(v=>v!=el.dataset.tag); + } + } + }); + mask_select_values = this.properties["values"].join(','); + return mask_select_values; + } + }); + + let old_values = '' + let mask_lists_dom = selector.element.children[0] + + // 初始化 + setTimeout(_=>{ + if(!method_values) { + method_values = 'selfie_multiclass_256x256' + selector.element.children[0].innerHTML = '' + // 重新排序 + let list = getTagList(tags[method_values]); + selector.element.children[0].append(...list) + } + if(method_values == 'selfie_multiclass_256x256'){ + toggleWidget(this, findWidgetByName(this, 'confidence'), true) + this.setSize([300, 260]); + }else{ + toggleWidget(this, findWidgetByName(this, 'confidence')) + this.setSize([300, 500]); + } + },1) + + return onNodeCreated; + } + } + } +}) \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/easy/easySelector.js b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/easy/easySelector.js new file mode 100644 index 0000000000000000000000000000000000000000..c9dde34da828d7bed2d0361c9aaf489e4ed2d88a --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/easy/easySelector.js @@ -0,0 +1,296 @@ +// 1.0.3 +import { app } from "../../../../scripts/app.js"; +import { api } from "../../../../scripts/api.js"; +import { $el } from "../../../../scripts/ui.js"; +import { $t } from "../common/i18n.js"; + +// 获取风格列表 +let styles_list_cache = {} +let styles_image_cache = {} +async function getStylesList(name){ + if(styles_list_cache[name]) return styles_list_cache[name] + else{ + const resp = await api.fetchApi(`/easyuse/prompt/styles?name=${name}`); + if (resp.status === 200) { + let data = await resp.json(); + styles_list_cache[name] = data; + return data; + } + return undefined; + } +} +async function getStylesImage(name, styles_name){ + if(!styles_image_cache[styles_name]) styles_image_cache[styles_name] = {} + if(styles_image_cache[styles_name][name]) return styles_image_cache[styles_name][name] + else{ + const resp = await api.fetchApi(`/easyuse/prompt/styles/image?name=${name}&styles_name=${styles_name}`); + if (resp.status === 200) { + const text = await resp.text() + if(text.startsWith('http')){ + styles_image_cache[styles_name][name] = text + return text + } + const url = `/easyuse/prompt/styles/image?name=${name}&styles_name=${styles_name}` + styles_image_cache[styles_name][name] = url + return url + } + return undefined; + } +} + +function getTagList(tags, styleName, language='en-US') { + let rlist=[] + tags.forEach((k,i) => { + rlist.push($el( + "label.easyuse-prompt-styles-tag", + { + dataset: { + tag: k['name'], + name: language == 'zh-CN' && k['name_cn'] ? k['name_cn'] : k['name'], + imgName: k['imgName'], + index: i + }, + $: (el) => { + el.children[0].onclick = () => { + el.classList.toggle("easyuse-prompt-styles-tag-selected"); + }; + el.onmousemove = (e) => { + displayImage(el.dataset.imgName, styleName, e) + }; + el.onmouseout = () => { + hiddenImage() + }; + el.onmouseover = (e) => { + displayImage(el.dataset.imgName, styleName) + }; + }, + }, + [ + $el("input",{ + type: 'checkbox', + name: k['name'] + }), + $el("span",{ + textContent: language == 'zh-CN' && k['name_cn'] ? k['name_cn'] : k['name'], + }) + ] + )) + }); + return rlist +} + +const foocus_base_path = "https://raw.githubusercontent.com/lllyasviel/Fooocus/main/sdxl_styles/samples/" +const empty_img = "data:image/jpeg;base64,/9j/4QAYRXhpZgAASUkqAAgAAAAAAAAAAAAAAP/sABFEdWNreQABAAQAAAA8AAD/4QNLaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLwA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA5LjEtYzAwMSA3OS4xNDYyODk5Nzc3LCAyMDIzLzA2LzI1LTIzOjU3OjE0ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgMjUuMSAoMjAyMzA5MDUubS4yMzE2IDk3OWM4NmQpICAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6RjA3NEU1QzNCNUJBMTFFRUExMUVDNkZDRjI0NzlBN0QiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6RjA3NEU1QzRCNUJBMTFFRUExMUVDNkZDRjI0NzlBN0QiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpGMDc0RTVDMUI1QkExMUVFQTExRUM2RkNGMjQ3OUE3RCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpGMDc0RTVDMkI1QkExMUVFQTExRUM2RkNGMjQ3OUE3RCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pv/uAA5BZG9iZQBkwAAAAAH/2wCEAAYEBAQFBAYFBQYJBgUGCQsIBgYICwwKCgsKCgwQDAwMDAwMEAwODxAPDgwTExQUExMcGxsbHB8fHx8fHx8fHx8BBwcHDQwNGBAQGBoVERUaHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fH//AABEIAIAAgAMBEQACEQEDEQH/xACLAAEAAgMBAQEAAAAAAAAAAAAABAUCAwYBBwgBAQADAQEBAAAAAAAAAAAAAAABAgMEBQYQAAEEAgECAwUHAwUAAAAAAAEAAgMEEQUhEgYxEwdBYSIyFFFxgVJyIxWRoTOxwdFiJBEBAAICAQQBBAIDAAAAAAAAAAECEQMxIUESBBOB0SIyUXGCIwX/2gAMAwEAAhEDEQA/AP1SgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICDXJYgj+d4afsVopM8KWvEcy8it1pXdMcjXO/Lnn+im2u0cwV2VniW1UXEBAQEBAQEBAQEBAQRNlc+mgyDh7zhv+5WunX5Sw37fCHM2dh48r06ank7N6rn2Ja7qa4hw5BBwQV010uK+/DsO29v/J68SOI86Jxjl95HIP4gryPc0fHfHaXu+j7Py68zzHSVquV2iAgICAgICAgICDyTr6HdHz4PTnwypjnqic46OauNbY6mGX99p+L8w9xaeV6OufHt0eXtr59M9VFb194E9LmuH3kf6rv17avO2ets7YVcuuuk/uOa3PgBlxP4BdMbq9nLPqbJ5xDbSM9azFXpyujuSO+Bo5kcf0NPyj25We2YtEzaPxdfr6519Kz+UvqEIlELBKQZQ0eYRwC7HOPxXzVsZ6cPpK5x15ZKEiAgICAgICAgICCNc1tG40CzA2XHg4j4h9zhyFpr22p+s4Z7NNL/ALRlTX+1dVFBJOJrcTI2lxZHYcBx+sldWv3bzOMVn6fZy39OkRnNo+v3aoOx9JOxks8tqwHDPS+1IW8+IzGWZVrf9DZHSMR/j9yvo656zMz9V1rdLqdYwsoVIqwd87mNAc79Tvmd+JXJt332ftMy6temlP1jCasmggICAgICAgICAgwlmiib1SPDB7zhWrWZ4VtaI5QXb2l5ojYHvLjjIGB/dbR61sZlhPtVziFb3PYdd0luCvAZbXludVZ1huZQPgyTx4/atvWj4rxaZ6d/6Ye1/t1zSI6zx/bzti5YqaOpBeg8u41n/oa14cA4ccH7lPs1jZebVn8eyPUtOrXFbR+XdYx9xa90pjeXROaSCXDj+oysZ9S+Mx1bR7uvOJ6LGOWKVgfG8PafAtOQueazHLqraJjMMlCRAQEBAQEBAQRLNp4HTFx/2/4WtKR3Y32T2Udl8j3knk/aeSu6kREPPvaZlpY3DmyY8DyrzPZWv8tkvmFv7bg12RyR1DGeeMj2KnjE9JaeUx1hi1sgaet/U7JIOMcE8Dj7FMREcK2zPKMasr5XO6fmOVt5xEOadVplYU45IAOhxa72kLm2TFuXXqrNeF1WtlwDZeHfmHguO+vHDupszylLJsICAgICAg8cMjCQiYR5IVpFmc1Q5qLXHPgfbhbV2MLaYlqNQAYA4V/kV+PDA1fcp81fjYurtYMu4CmLZRNYhtZWBAI8CqzdaKN8df3LObtIokxwe5ZzZrFUloIGFnLWHqhIgICAgICAgxMbSpyjDAwAq3kr4MTWCnzR4MX02PGHDISNmETqieWba7QABwB4KJumKNgjaFXK0VZYChYQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEHzvuv1G7k1W9s6/Xamtaq15oaonmnsCR008HntaI4K8/s4HOeEGXZXqTud7uqtG7r6kNa5HdMU9aaw9zZde+FkrHsnr1+M2MZBPIKDRe9cO2K2mjs/V0m7X61lWzq32W+ZFEbfkSSO4B+GL9zw4QWm99TqFVmjsaSu7fUtxeNM2aTmSMBbHI9zWHqHVJlnDTxjPKCJL6sea502t1D7Ouhr0rNqxNM2CSNuwnkgjAi6ZOotdEc/Egibf1j/j+7JNL9DWdWg84TWn2ywtdFKyMZb5Tg0nLyG55x48IJ3bXqe/ea/a26dFtyTXtldDUqyOdNL5VqaDHS5gwXRxMe3xz1Y9iDKP1Sa7uefUnR7TyYqUVoEU5jY6pJZIz1RY4ZiMYd7TkexBA749Wr2gtCKlrIpGs17NjK29LLWmPmMsyiFkbIZsPEdKQu6y0eAQWdD1E2L93W1tzRyCDY3paev2NaxVlhIjidMfMb5vmse1kbi9pZ7MeKDt0BAQEBAQfEPU+lFY2++q2K1uSSezTnrReVsTTmiZVYHOd9LVuQyubIwANkbxz4FA7FsQ0NrrLNXX7N0eo1+3darGDYPjb5j6prxVRajjDetsRAjj4yM4CDre2uxO7q2hqtm7nua6w9rp5tfXgoSxwyTOMr42PlrPe4Nc8jJJQRDb3Oz1fYFrcV7As0mu3u7nbWkBZ9LSfG5nlxs/yySWRiNozwcBBx9EXadGTXz62+LG41+jZS6adhzS6vfnlkEjgzEZax7T8ePFBu3nbPdUXqJZsw6S5cqbCW1YdIY2lxhhfEGMjfHtoG9HxucwPEZy4/A7kMC87aq2Kmv7mdvxuqGmklFjUU4G2Yp21rdyW00t+kJkFl88pY9vDgwNDvEoK9np73FBcHdkrt2+rZd5FjQx7O0b8WvbzDKZhN1SSse573QdeAHkN+Ichj3p2rBvZq9vUnY2tcNQPqpZYZpJ44GxXqzHdVlzZZpib73mLHViI85c1BZ6OpsIe/6/XSuntevdsz6+8+pI0/yM1dtWVr2Z644P8rmyuj6S53jxkh9aQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBB/9k=" +async function displayImage(imgName, styleName) { + var e = event || window.event; + var img = document.getElementById("show_image_id"); + var pxy= img.parentElement.getBoundingClientRect(); + if(imgName) { + const url = await getStylesImage(imgName, styleName) + img.src = url + img.onerror = _ =>{ + img.src = empty_img + } + } + var scale = app?.canvas?.ds?.scale || 1; + var x = (e.pageX-pxy.x-100)/scale; + var y = (e.pageY-pxy.y+25)/scale; + img.style.left = x+"px"; + img.style.top = y+"px"; + img.style.display = "block"; + img.style.borderRadius = "10px"; + img.style.borderColor = "var(--fg-color)" + img.style.borderWidth = "1px"; + img.style.borderStyle = "solid"; +} +function hiddenImage(){ //theEvent用来传入事件,Firefox的方式 + var img = document.getElementById('show_image_id'); + img.style.display = "none"; +} + +// StylePromptSelector +app.registerExtension({ + name: 'comfy.easyUse.styleSelector', + async beforeRegisterNodeDef(nodeType, nodeData, app) { + + if(nodeData.name == 'easy stylesSelector'){ + // 创建时 + const onNodeCreated = nodeType.prototype.onNodeCreated; + nodeType.prototype.onNodeCreated = function() { + onNodeCreated ? onNodeCreated?.apply(this, arguments) : undefined; + const styles_id = this.widgets.findIndex((w) => w.name == 'styles'); + const language = localStorage['AGL.Locale'] || localStorage['Comfy.Settings.AGL.Locale'] || 'en-US' + const list = $el("ul.easyuse-prompt-styles-list",[]); + let styles_values = '' + this.setProperty("values", []) + + let selector = this.addDOMWidget('select_styles',"btn",$el('div.easyuse-prompt-styles',[$el('div.tools', [ + $el('button.delete',{ + textContent: $t('Empty All'), + style:{}, + onclick:()=>{ + selector.element.children[0].querySelectorAll(".search").forEach(el=>{ + el.value = '' + }) + selector.element.children[1].querySelectorAll(".easyuse-prompt-styles-tag-selected").forEach(el => { + el.classList.remove("easyuse-prompt-styles-tag-selected"); + el.children[0].checked = false + }) + selector.element.children[1].querySelectorAll(".easyuse-prompt-styles-tag").forEach(el => { + el.classList.remove('hide') + }) + this.setProperty("values", []) + }} + ), + $el('textarea.search',{ + dir:"ltr", + style:{"overflow-y": "scroll"}, + rows:1, + placeholder:$t("🔎 Type here to search styles ..."), + oninput:(e)=>{ + let value = e.target.value + selector.element.children[1].querySelectorAll(".easyuse-prompt-styles-tag").forEach(el => { + const name = el.dataset.name.toLowerCase() + const tag = el.dataset.tag.toLowerCase() + const lower_value = value.toLowerCase() + if(name.indexOf(lower_value) != -1 || tag.indexOf(lower_value) != -1 || el.classList.value.indexOf("easyuse-prompt-styles-tag-selected")!=-1){ + el.classList.remove('hide') + } + else{ + el.classList.add('hide') + } + }) + } + }) + ]),list, + $el('img',{id:'show_image_id', + style:{display:'none',position:'absolute'}, + src:``, + onerror:()=>{ + this.src = empty_img + } + }) + ])); + + Object.defineProperty(this.widgets[styles_id],'value',{ + set:(value)=>{ + styles_values = value + if(styles_values){ + getStylesList(styles_values).then(_=>{ + selector.element.children[1].innerHTML='' + if(styles_list_cache[styles_values]){ + let tags = styles_list_cache[styles_values] + // 重新排序 + if(selector.value) tags = tags.sort((a,b)=> selector.value.includes(b.name) - selector.value.includes(a.name)) + this.properties["values"] = [] + let list = getTagList(tags, value, language); + selector.element.children[1].append(...list) + selector.element.children[1].querySelectorAll(".easyuse-prompt-styles-tag").forEach(el => { + if (this.properties["values"].includes(el.dataset.tag)) { + el.classList.add("easyuse-prompt-styles-tag-selected"); + } + if(this.size?.[0]<150 || this.size?.[1]<150) this.setSize([425, 500]); + }) + } + }) + } + }, + get: () => { + return styles_values + } + }) + + + let style_select_values = '' + Object.defineProperty(selector, "value", { + set: (value) => { + setTimeout(_=>{ + selector.element.children[1].querySelectorAll(".easyuse-prompt-styles-tag").forEach(el => { + let arr = value.split(',') + if (arr.includes(el.dataset.tag)) { + el.classList.add("easyuse-prompt-styles-tag-selected"); + el.children[0].checked = true + } + }) + },300) + }, + get: () => { + selector.element.children[1].querySelectorAll(".easyuse-prompt-styles-tag").forEach(el => { + if(el.classList.value.indexOf("easyuse-prompt-styles-tag-selected")>=0){ + if(!this.properties["values"].includes(el.dataset.tag)){ + this.properties["values"].push(el.dataset.tag); + } + }else{ + if(this.properties["values"].includes(el.dataset.tag)){ + this.properties["values"]= this.properties["values"].filter(v=>v!=el.dataset.tag); + } + } + }); + style_select_values = this.properties["values"].join(','); + return style_select_values; + } + }); + + let old_values = '' + let style_lists_dom = selector.element.children[1] + style_lists_dom.addEventListener('mouseenter', function (e) { + let value = '' + style_lists_dom.querySelectorAll(".easyuse-prompt-styles-tag-selected").forEach(el=> value+=el.dataset.tag) + old_values = value + }) + style_lists_dom.addEventListener('mouseleave', function (e) { + let value = '' + style_lists_dom.querySelectorAll(".easyuse-prompt-styles-tag-selected").forEach(el=> value+=el.dataset.tag) + let new_values = value + if(old_values != new_values){ + // console.log("选项发生了变化") + // 获取搜索值 + const search_value = document.getElementsByClassName('search')[0]['value'] + // 重新排序 + const tags = styles_list_cache[styles_values].sort((a,b)=> new_values.includes(b.name) - new_values.includes(a.name)) + style_lists_dom.innerHTML = '' + let list = getTagList(tags, styles_values, language); + style_lists_dom.append(...list) + style_lists_dom.querySelectorAll(".easyuse-prompt-styles-tag").forEach(el => { + if (new_values.includes(el.dataset.tag)) { + el.classList.add("easyuse-prompt-styles-tag-selected"); + el.children[0].checked = true; + } + if(search_value){ + if(el.dataset.name.indexOf(search_value) != -1 || el.dataset.tag.indexOf(search_value) != -1 || el.classList.value.indexOf("easyuse-prompt-styles-tag-selected")!=-1){ + el.classList.remove('hide') + } + else{ + el.classList.add('hide') + } + } + + }) + } + }) + + + // 初始化 + setTimeout(_=>{ + if(!styles_values) { + styles_values = 'fooocus_styles' + getStylesList(styles_values).then(_=>{ + selector.element.children[1].innerHTML='' + if(styles_list_cache[styles_values]){ + let tags = styles_list_cache[styles_values] + // 重新排序 + if(selector.value) tags = tags.sort((a,b)=> selector.value.includes(b.name) - selector.value.includes(a.name)) + let list = getTagList(tags, styles_values, language); + selector.element.children[1].append(...list) + } + }) + } + if(this.size?.[0]<150 || this.size?.[1]<150) this.setSize([425, 500]); + // + },100) + + return onNodeCreated; + } + } + } +}) \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/easy/easySliderControl.js b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/easy/easySliderControl.js new file mode 100644 index 0000000000000000000000000000000000000000..17fc5094c5e773dc88698c9e6beb4758f52819fb --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/easy/easySliderControl.js @@ -0,0 +1,173 @@ +import { app } from "../../../../scripts/app.js"; +import { api } from "../../../../scripts/api.js"; +import { $el } from "../../../../scripts/ui.js"; +import { $t } from "../common/i18n.js"; +import { sleep } from "../common/utils.js"; + + +const calculatePercent = (value, min, max) => ((value-min)/(max-min)*100) + +const getLayerDefaultValue = (index) => { + switch (index){ + case 3: + return 2.5 + case 6: + return 1 + default: + return 0 + } +} + +const addLayer = (_this, layer_total, arrays, sliders, i) => { + let scroll = $el('div.easyuse-slider-item-scroll') + let value = $el('div.easyuse-slider-item-input', {textContent: arrays[i]['value']}) + let label = $el('div.easyuse-slider-item-label', {textContent: 'L'+i}) + let girdTotal = (arrays[i]['max'] - arrays[i]['min']) / arrays[i]['step'] + let area = $el('div.easyuse-slider-item-area', {style:{ height: calculatePercent(arrays[i]['default'],arrays[i]['min'],arrays[i]['max']) + '%'}}) + let bar = $el('div.easyuse-slider-item-bar', { + style:{ top: (100-calculatePercent(arrays[i]['default'],arrays[i]['min'],arrays[i]['max'])) + '%'}, + onmousedown: (e) => { + let event = e || window.event; + var y = event.clientY - bar.offsetTop; + document.onmousemove = (e) => { + let event = e || window.event; + let top = event.clientY - y; + if(top < 0){ + top = 0; + } + else if(top > scroll.offsetHeight - bar.offsetHeight){ + top = scroll.offsetHeight - bar.offsetHeight; + } + // top到最近的girdHeight值 + let girlHeight = (scroll.offsetHeight - bar.offsetHeight)/ girdTotal + top = Math.round(top / girlHeight) * girlHeight; + bar.style.top = Math.floor(top/(scroll.offsetHeight - bar.offsetHeight)* 100) + '%'; + area.style.height = Math.floor((scroll.offsetHeight - bar.offsetHeight - top)/(scroll.offsetHeight - bar.offsetHeight)* 100) + '%'; + value.innerText = parseFloat(parseFloat(arrays[i]['max'] - (arrays[i]['max']-arrays[i]['min']) * (top/(scroll.offsetHeight - bar.offsetHeight))).toFixed(2)) + arrays[i]['value'] = value.innerText + _this.properties['values'][i] = i+':'+value.innerText + window.getSelection ? window.getSelection().removeAllRanges() : document.selection.empty(); + } + }, + ondblclick:_=>{ + bar.style.top = (100-calculatePercent(arrays[i]['default'],arrays[i]['min'],arrays[i]['max'])) + '%' + area.style.height = calculatePercent(arrays[i]['default'],arrays[i]['min'],arrays[i]['max']) + '%' + value.innerText = arrays[i]['default'] + arrays[i]['value'] = arrays[i]['default'] + _this.properties['values'][i] = i+':'+value.innerText + } + }) + document.onmouseup = _=> document.onmousemove = null; + + scroll.replaceChildren(bar,area) + let item_div = $el('div.easyuse-slider-item',[ + value, + scroll, + label + ]) + if(i == 3 ) layer_total == 12 ? item_div.classList.add('negative') : item_div.classList.remove('negative') + else if(i == 6) layer_total == 12 ? item_div.classList.add('positive') : item_div.classList.remove('positive') + sliders.push(item_div) + return item_div +} + +const setSliderValue = (_this, type, refresh=false, values_div, sliders_value) => { + let layer_total = type == 'sdxl' ? 12 : 16 + let sliders = [] + let arrays = Array.from({length: layer_total}, (v, i) => ({default: layer_total == 12 ? getLayerDefaultValue(i) : 0, min: -1, max: 3, step: 0.05, value:layer_total == 12 ? getLayerDefaultValue(i) : 0})) + _this.setProperty("values", Array.from({length: layer_total}, (v, i) => i+':'+arrays[i]['value'])) + for (let i = 0; i < layer_total; i++) { + addLayer(_this, layer_total, arrays, sliders, i) + } + if(refresh) values_div.replaceChildren(...sliders) + else{ + values_div = $el('div.easyuse-slider', sliders) + sliders_value = _this.addDOMWidget('values',"btn",values_div) + } + + Object.defineProperty(sliders_value, 'value', { + set: function() {}, + get: function() { + return _this.properties.values.join(','); + } + }); + return {sliders, arrays, values_div, sliders_value} +} + + +app.registerExtension({ + name: 'comfy.easyUse.sliderControl', + async beforeRegisterNodeDef(nodeType, nodeData, app) { + if(nodeData.name == 'easy sliderControl'){ + // 创建时 + const onNodeCreated = nodeType.prototype.onNodeCreated; + nodeType.prototype.onNodeCreated = function() { + onNodeCreated && onNodeCreated.call(this); + const mode = this.widgets[0]; + const model_type = this.widgets[1]; + let layer_total = model_type.value == 'sdxl' ? 12 : 16 + let _this = this + let values_div = null + let sliders_value = null + mode.callback = async()=>{ + switch (mode.value) { + case 'ipadapter layer weights': + nodeData.output_name = ['layer_weights'] + _this.outputs[0]['name'] = 'layer_weights' + _this.outputs[0]['label'] = 'layer_weights' + break + } + } + + model_type.callback = async()=>{ + if(values_div) { + let r2 = setSliderValue(_this, model_type.value, true, values_div, sliders_value) + values_div = r2.values_div + sliders_value = r2.sliders_value + } + _this.setSize(model_type.value == 'sdxl' ? [375,320] : [455,320]) + } + + let r1 = setSliderValue(_this, model_type.value, false, values_div, sliders_value) + let sliders = r1.sliders + let arrays = r1.arrays + values_div = r1.values_div + sliders_value = r1.sliders_value + setTimeout(_=>{ + let values_widgets_index = this.widgets.findIndex((w) => w.name == 'values'); + if(values_widgets_index != -1){ + let old_values_widget = this.widgets[values_widgets_index]; + let old_value = old_values_widget.value.split(',') + let layer_total = _this.widgets[1].value == 'sdxl' ? 12 : 16 + for (let i = 0; i < layer_total; i++) { + let value = parseFloat(parseFloat(old_value[i].split(':')[1]).toFixed(2)) + let item_div = sliders[i] || null + // 存在层即修改 + if(arrays[i]){ + arrays[i]['value'] = value + _this.properties['values'][i] = old_value[i] + }else{ + arrays.push({default: layer_total == 12 ? getLayerDefaultValue(i) : 0, min: -1, max: 3, step: 0.05, value:layer_total == 12 ? getLayerDefaultValue(i) : 0}) + _this.properties['values'].push(i+':'+arrays[i]['value']) + // 添加缺失层 + item_div = addLayer(_this, layer_total, arrays, sliders, i) + values_div.appendChild(item_div) + } + // todo: 修改bar位置等 + let input = item_div.getElementsByClassName('easyuse-slider-item-input')[0] + let bar = item_div.getElementsByClassName('easyuse-slider-item-bar')[0] + let area = item_div.getElementsByClassName('easyuse-slider-item-area')[0] + if(i == 3 ) layer_total == 12 ? item_div.classList.add('negative') : item_div.classList.remove('negative') + else if(i == 6) layer_total == 12 ? item_div.classList.add('positive') : item_div.classList.remove('positive') + input.textContent = value + bar.style.top = (100-calculatePercent(value,arrays[i]['min'],arrays[i]['max'])) + '%' + area.style.height = calculatePercent(value,arrays[i]['min'],arrays[i]['max']) + '%' + } + } + _this.setSize(model_type.value == 'sdxl' ? [375,320] : [455,320]) + },1) + return onNodeCreated; + } + } + } +}) \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/easy/easySuggestion.js b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/easy/easySuggestion.js new file mode 100644 index 0000000000000000000000000000000000000000..49891b0108a7f3d9d44dfdcba28991d69428aa44 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/easy/easySuggestion.js @@ -0,0 +1,554 @@ +import {app} from "../../../../scripts/app.js"; +import {api} from "../../../../scripts/api.js"; +import {$el} from "../../../../scripts/ui.js"; + +const propmts = ["easy wildcards", "easy positive", "easy negative", "easy stylesSelector", "easy promptConcat", "easy promptReplace"] +const loaders = ["easy a1111Loader", "easy comfyLoader", "easy fullLoader", "easy svdLoader", "easy cascadeLoader", "easy sv3dLoader"] +const preSamplingNodes = ["easy preSampling", "easy preSamplingAdvanced", "easy preSamplingNoiseIn", "easy preSamplingCustom", "easy preSamplingDynamicCFG","easy preSamplingSdTurbo", "easy preSamplingLayerDiffusion"] +const kSampler = ["easy kSampler", "easy kSamplerTiled","easy kSamplerInpainting", "easy kSamplerDownscaleUnet", "easy kSamplerSDTurbo"] +const controlNetNodes = ["easy controlnetLoader", "easy controlnetLoaderADV"] +const instantIDNodes = ["easy instantIDApply", "easy instantIDApplyADV"] +const ipadapterNodes = ["easy ipadapterApply", "easy ipadapterApplyADV" ,"easy ipadapterApplyFaceIDKolors", "easy ipadapterStyleComposition"] +const pipeNodes = ['easy pipeIn','easy pipeOut', 'easy pipeEdit'] +const xyNodes = ['easy XYPlot', 'easy XYPlotAdvanced'] +const extraNodes = ['easy setNode'] +const modelNormalNodes = [...['RescaleCFG','LoraLoaderModelOnly','LoraLoader','FreeU','FreeU_v2'],...ipadapterNodes,...extraNodes] +const suggestions = { + // prompt + "easy seed":{ + "from":{ + "INT": [...preSamplingNodes,...['easy fullkSampler']] + } + }, + "easy positive":{ + "from":{ + "STRING": [...propmts] + } + }, + "easy negative":{ + "from":{ + "STRING": [...propmts] + } + }, + "easy wildcards":{ + "from":{ + "STRING": [...["Reroute","easy showAnything"],...propmts,] + } + }, + "easy stylesSelector":{ + "from":{ + "STRING": [...["Reroute","easy showAnything"],...propmts,] + } + }, + "easy promptConcat":{ + "from":{ + "STRING": [...["Reroute","easy showAnything"],...propmts,] + } + }, + "easy promptReplace":{ + "from":{ + "STRING": [...["Reroute","easy showAnything"],...propmts,] + } + }, + // sd相关 + "easy fullLoader": { + "from":{ + "PIPE_LINE": [...preSamplingNodes,...['easy fullkSampler'],...pipeNodes,...extraNodes], + "MODEL":modelNormalNodes + }, + "to":{ + "STRING": [...propmts] + } + }, + "easy a1111Loader": { + "from": { + "PIPE_LINE": [ ...preSamplingNodes, ...controlNetNodes, ...instantIDNodes, ...pipeNodes, ...extraNodes], + "MODEL": modelNormalNodes + }, + "to":{ + "STRING": [...propmts] + } + }, + "easy comfyLoader": { + "from": { + "PIPE_LINE": [ ...preSamplingNodes, ...controlNetNodes, ...instantIDNodes, ...pipeNodes, ...extraNodes], + "MODEL": modelNormalNodes + }, + "to":{ + "STRING": [...propmts] + } + }, + "easy svdLoader":{ + "from": { + "PIPE_LINE": [ ...["easy preSampling", "easy preSamplingAdvanced", "easy preSamplingDynamicCFG"], ...pipeNodes, ...extraNodes], + "MODEL": modelNormalNodes + }, + "to":{ + "STRING": [...propmts] + } + }, + "easy zero123Loader":{ + "from": { + "PIPE_LINE": [ ...["easy preSampling", "easy preSamplingAdvanced", "easy preSamplingDynamicCFG"], ...pipeNodes, ...extraNodes], + "MODEL": modelNormalNodes + }, + "to":{ + "STRING": [...propmts] + } + }, + "easy sv3dLoader":{ + "from": { + "PIPE_LINE": [ ...["easy preSampling", "easy preSamplingAdvanced", "easy preSamplingDynamicCFG"], ...pipeNodes, ...extraNodes], + "MODEL": modelNormalNodes + }, + "to":{ + "STRING": [...propmts] + } + }, + "easy preSampling": { + "from": { + "PIPE_LINE": [ ...kSampler, ...pipeNodes, ...controlNetNodes, ...xyNodes, ...extraNodes] + }, + }, + "easy preSamplingAdvanced": { + "from": { + "PIPE_LINE": [ ...kSampler, ...pipeNodes, ...controlNetNodes, ...xyNodes, ...extraNodes] + } + }, + "easy preSamplingDynamicCFG": { + "from": { + "PIPE_LINE": [ ...kSampler, ...pipeNodes, ...controlNetNodes, ...xyNodes, ...extraNodes] + } + }, + "easy preSamplingCustom": { + "from": { + "PIPE_LINE": [ ...kSampler, ...pipeNodes, ...controlNetNodes, ...xyNodes, ...extraNodes] + } + }, + "easy preSamplingLayerDiffusion": { + "from": { + "PIPE_LINE": [...["easy kSamplerLayerDiffusion"], ...kSampler, ...pipeNodes, ...controlNetNodes, ...xyNodes, ...extraNodes] + } + }, + "easy preSamplingNoiseIn": { + "from": { + "PIPE_LINE": [ ...kSampler, ...pipeNodes, ...controlNetNodes, ...xyNodes, ...extraNodes] + } + }, + // ksampler + "easy fullkSampler": { + "from": { + "PIPE_LINE": [ ...pipeNodes.reverse(), ...['easy preDetailerFix', 'easy preMaskDetailerFix'], ...preSamplingNodes, ...extraNodes] + } + }, + "easy kSampler": { + "from": { + "PIPE_LINE": [ ...pipeNodes.reverse(), ...['easy preDetailerFix', 'easy preMaskDetailerFix', 'easy hiresFix'], ...preSamplingNodes, ...extraNodes], + } + }, + // cn + "easy controlnetLoader": { + "from": { + "PIPE_LINE": [ ...preSamplingNodes, ...controlNetNodes, ...instantIDNodes, ...pipeNodes, ...extraNodes] + } + }, + "easy controlnetLoaderADV":{ + "from": { + "PIPE_LINE": [ ...preSamplingNodes, ...controlNetNodes, ...instantIDNodes, ...pipeNodes, ...extraNodes] + } + }, + // instant + "easy instantIDApply": { + "from": { + "PIPE_LINE": [ ...preSamplingNodes, ...controlNetNodes, ...instantIDNodes, ...pipeNodes, ...extraNodes], + "MODEL": modelNormalNodes + }, + "to":{ + "COMBO": [...["easy promptLine"]] + } + }, + "easy instantIDApplyADV":{ + "from": { + "PIPE_LINE": [ ...preSamplingNodes, ...controlNetNodes, ...instantIDNodes, ...pipeNodes, ...extraNodes], + "MODEL": modelNormalNodes + }, + "to":{ + "COMBO": [...["easy promptLine"]] + } + }, + "easy ipadapterApply":{ + "to":{ + "COMBO": [...["easy promptLine"]] + } + }, + "easy ipadapterApplyADV":{ + "to":{ + "STRING": [...["easy sliderControl"], ...propmts], + "COMBO": [...["easy promptLine"]] + } + }, + "easy ipadapterStyleComposition":{ + "to":{ + "COMBO": [...["easy promptLine"]] + } + }, + // fix + "easy preDetailerFix":{ + "from": { + "PIPE_LINE": [...["easy detailerFix"], ...pipeNodes, ...extraNodes] + }, + "to":{ + "PIPE_LINE": [...["easy ultralyticsDetectorPipe", "easy samLoaderPipe", "easy kSampler", "easy fullkSampler"]] + } + }, + "easy preMaskDetailerFix":{ + "from": { + "PIPE_LINE": [...["easy detailerFix"], ...pipeNodes, ...extraNodes] + } + }, + "easy samLoaderPipe": { + "from":{ + "PIPE_LINE": [...["easy preDetailerFix"], ...pipeNodes, ...extraNodes] + } + }, + "easy ultralyticsDetectorPipe": { + "from":{ + "PIPE_LINE": [...["easy preDetailerFix"], ...pipeNodes, ...extraNodes] + } + }, + // cascade相关 + "easy cascadeLoader":{ + "from": { + "PIPE_LINE": [ ...["easy fullCascadeKSampler", 'easy preSamplingCascade'], ...controlNetNodes, ...pipeNodes, ...extraNodes], + "MODEL": modelNormalNodes.filter(cate => !ipadapterNodes.includes(cate)) + } + }, + "easy fullCascadeKSampler":{ + "from": { + "PIPE_LINE": [ ...["easy preSampling", "easy preSamplingAdvanced"], ...pipeNodes, ...extraNodes] + } + }, + "easy preSamplingCascade":{ + "from": { + "PIPE_LINE": [ ...["easy cascadeKSampler",], ...pipeNodes, ...extraNodes] + } + }, + "easy cascadeKSampler": { + "from": { + "PIPE_LINE": [ ...["easy preSampling", "easy preSamplingAdvanced"], ...pipeNodes, ...extraNodes] + } + }, +} + +class NullGraphError extends Error { + constructor(message="Attempted to access LGraph reference that was null or undefined.", cause) { + super(message, {cause}) + this.name = "NullGraphError" + } +} +app.registerExtension({ + name: "comfy.easyuse.suggestions", + async setup() { + const createDefaultNodeForSlot = LGraphCanvas.prototype.createDefaultNodeForSlot; + LGraphCanvas.prototype.createDefaultNodeForSlot = function(optPass) { // addNodeMenu for connection + const opts = Object.assign({ nodeFrom: null // input + ,slotFrom: null // input + ,nodeTo: null // output + ,slotTo: null // output + ,position: [] // pass the event coords + ,nodeType: null // choose a nodetype to add, AUTO to set at first good + ,posAdd:[0,0] // adjust x,y + ,posSizeFix:[0,0] // alpha, adjust the position x,y based on the new node size w,h + } + , optPass || {} + ); + const { afterRerouteId } = opts + const that = this; + + const isFrom = opts.nodeFrom && opts.slotFrom!==null; + const isTo = !isFrom && opts.nodeTo && opts.slotTo!==null; + const node = isFrom ? opts.nodeFrom : opts.nodeTo + // Not an Easy Use node, skip showConnectionMenu hijack + if(!node || !Object.keys(suggestions).includes(node.type)){ + return createDefaultNodeForSlot.call(this, optPass) + } + + if (!isFrom && !isTo){ + console.warn("No data passed to createDefaultNodeForSlot "+opts.nodeFrom+" "+opts.slotFrom+" "+opts.nodeTo+" "+opts.slotTo); + return false; + } + if (!opts.nodeType){ + console.warn("No type to createDefaultNodeForSlot"); + return false; + } + + const nodeX = isFrom ? opts.nodeFrom : opts.nodeTo; + const nodeType = nodeX.type + let slotX = isFrom ? opts.slotFrom : opts.slotTo; + + let iSlotConn = false; + switch (typeof slotX){ + case "string": + iSlotConn = isFrom ? nodeX.findOutputSlot(slotX,false) : nodeX.findInputSlot(slotX,false); + slotX = isFrom ? nodeX.outputs[slotX] : nodeX.inputs[slotX]; + break; + case "object": + // ok slotX + iSlotConn = isFrom ? nodeX.findOutputSlot(slotX.name) : nodeX.findInputSlot(slotX.name); + break; + case "number": + iSlotConn = slotX; + slotX = isFrom ? nodeX.outputs[slotX] : nodeX.inputs[slotX]; + break; + case "undefined": + default: + // bad ? + //iSlotConn = 0; + console.warn("Cant get slot information "+slotX); + return false; + } + + if (slotX===false || iSlotConn===false){ + console.warn("createDefaultNodeForSlot bad slotX "+slotX+" "+iSlotConn); + } + + // check for defaults nodes for this slottype + var fromSlotType = slotX.type==LiteGraph.EVENT?"_event_":slotX.type; + var slotTypesDefault = isFrom ? LiteGraph.slot_types_default_out : LiteGraph.slot_types_default_in; + if(slotTypesDefault && slotTypesDefault[fromSlotType]){ + if (slotX.link !== null) { + // is connected + }else{ + // is not not connected + } + let nodeNewType = false; + const fromOrTo = isFrom ? 'from' : 'to' + if(suggestions[nodeType] && suggestions[nodeType][fromOrTo] && suggestions[nodeType][fromOrTo][fromSlotType]?.length>0){ + for(var typeX in suggestions[nodeType][fromOrTo][fromSlotType]){ + if (opts.nodeType == suggestions[nodeType][fromOrTo][fromSlotType][typeX] || opts.nodeType == "AUTO") { + nodeNewType = suggestions[nodeType][fromOrTo][fromSlotType][typeX]; + break + } + } + } + else if(typeof slotTypesDefault[fromSlotType] == "object" || typeof slotTypesDefault[fromSlotType] == "array"){ + for(var typeX in slotTypesDefault[fromSlotType]){ + if (opts.nodeType == slotTypesDefault[fromSlotType][typeX] || opts.nodeType == "AUTO"){ + nodeNewType = slotTypesDefault[fromSlotType][typeX]; + break; + } + } + }else{ + if (opts.nodeType == slotTypesDefault[fromSlotType] || opts.nodeType == "AUTO") nodeNewType = slotTypesDefault[fromSlotType]; + } + if (nodeNewType) { + var nodeNewOpts = false; + if (typeof nodeNewType == "object" && nodeNewType.node){ + nodeNewOpts = nodeNewType; + nodeNewType = nodeNewType.node; + } + + //that.graph.beforeChange(); + + var newNode = LiteGraph.createNode(nodeNewType); + if(newNode){ + // if is object pass options + if (nodeNewOpts){ + if (nodeNewOpts.properties) { + for (var i in nodeNewOpts.properties) { + newNode.addProperty( i, nodeNewOpts.properties[i] ); + } + } + if (nodeNewOpts.inputs) { + newNode.inputs = []; + for (var i in nodeNewOpts.inputs) { + newNode.addOutput( + nodeNewOpts.inputs[i][0], + nodeNewOpts.inputs[i][1] + ); + } + } + if (nodeNewOpts.outputs) { + newNode.outputs = []; + for (var i in nodeNewOpts.outputs) { + newNode.addOutput( + nodeNewOpts.outputs[i][0], + nodeNewOpts.outputs[i][1] + ); + } + } + if (nodeNewOpts.title) { + newNode.title = nodeNewOpts.title; + } + if (nodeNewOpts.json) { + newNode.configure(nodeNewOpts.json); + } + + } + + // add the node + that.graph.add(newNode); + newNode.pos = [ opts.position[0]+opts.posAdd[0]+(opts.posSizeFix[0]?opts.posSizeFix[0]*newNode.size[0]:0) + ,opts.position[1]+opts.posAdd[1]+(opts.posSizeFix[1]?opts.posSizeFix[1]*newNode.size[1]:0)]; //that.last_click_position; //[e.canvasX+30, e.canvasX+5];*/ + + // connect the two! + if (isFrom){ + opts.nodeFrom.connectByType( iSlotConn, newNode, fromSlotType ); + }else{ + opts.nodeTo.connectByTypeOutput( iSlotConn, newNode, fromSlotType ); + } + + return true; + + }else{ + console.log("failed creating "+nodeNewType); + } + } + } + return false; + } + + let showConnectionMenu = LGraphCanvas.prototype.showConnectionMenu + LGraphCanvas.prototype.showConnectionMenu = function(optPass) { // addNodeMenu for connection + const opts = Object.assign({ + nodeFrom: null, // input + slotFrom: null, // input + nodeTo: null, // output + slotTo: null, // output + e: undefined, + allow_searchbox: this.allow_searchbox, + showSearchBox: this.showSearchBox, + } + ,optPass || {} + ); + const that = this; + const { graph } = this + const { afterRerouteId } = opts + const isFrom = opts.nodeFrom && opts.slotFrom; + const isTo = !isFrom && opts.nodeTo && opts.slotTo; + const node = isFrom ? opts.nodeFrom : opts.nodeTo + + // Not an Easy Use node, skip showConnectionMenu hijack + if(!node || !Object.keys(suggestions).includes(node.type)){ + return showConnectionMenu.call(this, optPass) + } + + if (!isFrom && !isTo){ + console.warn("No data passed to showConnectionMenu"); + return false; + } + + const nodeX = isFrom ? opts.nodeFrom : opts.nodeTo; + if (!nodeX) throw new TypeError("nodeX was null when creating default node for slot.") + let slotX = isFrom ? opts.slotFrom : opts.slotTo; + + let iSlotConn = false; + switch (typeof slotX){ + case "string": + iSlotConn = isFrom ? nodeX.findOutputSlot(slotX,false) : nodeX.findInputSlot(slotX,false); + slotX = isFrom ? nodeX.outputs[slotX] : nodeX.inputs[slotX]; + break; + case "object": + // ok slotX + iSlotConn = isFrom ? nodeX.findOutputSlot(slotX.name) : nodeX.findInputSlot(slotX.name); + break; + case "number": + iSlotConn = slotX; + slotX = isFrom ? nodeX.outputs[slotX] : nodeX.inputs[slotX]; + break; + default: + // bad ? + //iSlotConn = 0; + console.warn("Cant get slot information "+slotX); + return false; + } + + const options = ["Add Node", "Add Reroute", null] + if (opts.allow_searchbox){ + options.push("Search"); + options.push(null); + } + + // get defaults nodes for this slottype + var fromSlotType = slotX.type==LiteGraph.EVENT?"_event_":slotX.type; + var slotTypesDefault = isFrom ? LiteGraph.slot_types_default_out : LiteGraph.slot_types_default_in; + var nodeType = nodeX.type + if(slotTypesDefault && slotTypesDefault[fromSlotType]){ + const fromOrTo = isFrom ? 'from' : 'to' + if(suggestions[nodeType] && suggestions[nodeType][fromOrTo] && suggestions[nodeType][fromOrTo][fromSlotType]?.length>0){ + for(var typeX in suggestions[nodeType][fromOrTo][fromSlotType]){ + options.push(suggestions[nodeType][fromOrTo][fromSlotType][typeX]); + } + } + else if(typeof slotTypesDefault[fromSlotType] == "object" || typeof slotTypesDefault[fromSlotType] == "array"){ + for(var typeX in slotTypesDefault[fromSlotType]){ + options.push(slotTypesDefault[fromSlotType][typeX]); + } + }else{ + options.push(slotTypesDefault[fromSlotType]); + } + } + + // build menu + var menu = new LiteGraph.ContextMenu(options, { + event: opts.e, + extra: slotX, + title: + (slotX && slotX.name != "" + ? slotX.name + (fromSlotType ? " | " : "") + : "") + (slotX && fromSlotType ? fromSlotType : ""), + callback: inner_clicked, + }); + + // callback + function inner_clicked(v,options,e) { + //console.log("Process showConnectionMenu selection"); + switch (v) { + case "Add Node": + LGraphCanvas.onMenuAdd(null, null, e, menu, function (node) { + if (!node) return + + if (isFrom) { + opts.nodeFrom?.connectByType(iSlotConn, node, fromSlotType, { afterRerouteId }) + } else { + opts.nodeTo?.connectByTypeOutput(iSlotConn, node, fromSlotType, { afterRerouteId }) + } + }) + break; + case "Add Reroute": + const node = isFrom ? opts.nodeFrom : opts.nodeTo + const slot = options.extra + if (!graph) throw new NullGraphError() + if (!node) throw new TypeError("Cannot add reroute: node was null") + if (!slot) throw new TypeError("Cannot add reroute: slot was null") + if (!opts.e) throw new TypeError("Cannot add reroute: CanvasPointerEvent was null") + + const reroute = node.connectFloatingReroute([opts.e.canvasX, opts.e.canvasY], slot, afterRerouteId) + if (!reroute) throw new Error("Failed to create reroute") + + that.dirty_canvas = true + that.dirty_bgcanvas = true + break + case "Search": + if(isFrom){ + opts.showSearchBox(e,{node_from: opts.nodeFrom, slot_from: slotX, type_filter_in: fromSlotType}); + }else{ + opts.showSearchBox(e,{node_to: opts.nodeTo, slot_from: slotX, type_filter_out: fromSlotType}); + } + break; + default: + const customProps = { + position: [opts.e?.canvasX ?? 0, opts.e?.canvasY ?? 0], + nodeType: v, + afterRerouteId, + } + // check for defaults nodes for this slottype + that.createDefaultNodeForSlot(Object.assign(opts, customProps)) + break; + } + } + + return false; + }; + } +}) \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/easy/easyWidgets.js b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/easy/easyWidgets.js new file mode 100644 index 0000000000000000000000000000000000000000..899befed6a35139d51e23ed99b427bc0e05224ea --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/easy/easyWidgets.js @@ -0,0 +1,400 @@ +import { app } from "../../../../scripts/app.js"; +import { ComfyWidgets } from "../../../../scripts/widgets.js"; + +const KEY_CODES = { ENTER: 13, ESC: 27, ARROW_DOWN: 40, ARROW_UP: 38 }; +const WIDGET_GAP = -4; + +function hideInfoWidget(e, node, widget) { + let dropdownShouldBeRemoved = false; + let selectionIndex = -1; + + if (e) { + e.preventDefault(); + e.stopPropagation(); + displayDropdown(widget); + } else { + hideWidget(widget, node); + } + + function createDropdownElement() { + const dropdown = document.createElement('ul'); + dropdown.id = 'hideinfo-dropdown'; + dropdown.setAttribute('role', 'listbox'); + dropdown.classList.add('hideInfo-dropdown'); + return dropdown; + } + + function createDropdownItem(textContent, action) { + const listItem = document.createElement('li'); + listItem.id = `hideInfo-item-${textContent.replace(/ /g, '')}`; + listItem.classList.add('hideInfo-item'); + listItem.setAttribute('role', 'option'); + listItem.textContent = textContent; + listItem.addEventListener('mousedown', (event) => { + event.preventDefault(); + action(widget, node); // perform the action when dropdown item is clicked + removeDropdown(); + dropdownShouldBeRemoved = false; + }); + listItem.dataset.action = textContent.replace(/ /g, ''); // store the action in a data attribute + return listItem; + } + + function displayDropdown(widget) { + removeDropdown(); + + const dropdown = createDropdownElement(); + const listItemHide = createDropdownItem('Hide info Widget', hideWidget); + const listItemHideAll = createDropdownItem('Hide for all of this node-type', hideWidgetForNodetype); + + dropdown.appendChild(listItemHide); + dropdown.appendChild(listItemHideAll); + + const inputRect = widget.inputEl.getBoundingClientRect(); + dropdown.style.top = `${inputRect.top + inputRect.height}px`; + dropdown.style.left = `${inputRect.left}px`; + dropdown.style.width = `${inputRect.width}px`; + + document.body.appendChild(dropdown); + dropdownShouldBeRemoved = true; + + widget.inputEl.removeEventListener('keydown', handleKeyDown); + widget.inputEl.addEventListener('keydown', handleKeyDown); + document.addEventListener('click', handleDocumentClick); + } + + function removeDropdown() { + const dropdown = document.getElementById('hideinfo-dropdown'); + if (dropdown) { + dropdown.remove(); + widget.inputEl.removeEventListener('keydown', handleKeyDown); + } + document.removeEventListener('click', handleDocumentClick); + + } + + function handleKeyDown(event) { + const dropdownItems = document.querySelectorAll('.hideInfo-item'); + + if (event.keyCode === KEY_CODES.ENTER && dropdownShouldBeRemoved) { + event.preventDefault(); + if (selectionIndex !== -1) { + const selectedAction = dropdownItems[selectionIndex].dataset.action; + if (selectedAction === 'HideinfoWidget') { + hideWidget(widget, node); + } else if (selectedAction === 'Hideforall') { + hideWidgetForNodetype(widget, node); + } + removeDropdown(); + dropdownShouldBeRemoved = false; + } + } else if (event.keyCode === KEY_CODES.ARROW_DOWN && dropdownShouldBeRemoved) { + event.preventDefault(); + if (selectionIndex !== -1) { + dropdownItems[selectionIndex].classList.remove('selected'); + } + selectionIndex = (selectionIndex + 1) % dropdownItems.length; + dropdownItems[selectionIndex].classList.add('selected'); + } else if (event.keyCode === KEY_CODES.ARROW_UP && dropdownShouldBeRemoved) { + event.preventDefault(); + if (selectionIndex !== -1) { + dropdownItems[selectionIndex].classList.remove('selected'); + } + selectionIndex = (selectionIndex - 1 + dropdownItems.length) % dropdownItems.length; + dropdownItems[selectionIndex].classList.add('selected'); + } else if (event.keyCode === KEY_CODES.ESC && dropdownShouldBeRemoved) { + event.preventDefault(); + removeDropdown(); + } + } + + function hideWidget(widget, node) { + node.properties['infoWidgetHidden'] = true; + widget.type = "esayHidden"; + widget.computeSize = () => [0, WIDGET_GAP]; + node.setSize([node.size[0], node.size[1]]); + } + + function hideWidgetForNodetype(widget, node) { + hideWidget(widget, node) + const hiddenNodeTypes = JSON.parse(localStorage.getItem('hiddenWidgetNodeTypes') || "[]"); + if (!hiddenNodeTypes.includes(node.constructor.type)) { + hiddenNodeTypes.push(node.constructor.type); + } + localStorage.setItem('hiddenWidgetNodeTypes', JSON.stringify(hiddenNodeTypes)); + } + + function handleDocumentClick(event) { + const dropdown = document.getElementById('hideinfo-dropdown'); + + // If the click was outside the dropdown and the dropdown should be removed, remove it + if (dropdown && !dropdown.contains(event.target) && dropdownShouldBeRemoved) { + removeDropdown(); + dropdownShouldBeRemoved = false; + } + } +} + + +var styleElement = document.createElement("style"); +const cssCode = ` +.easy-info_widget { + background-color: var(--comfy-input-bg); + color: var(--input-text); + overflow: hidden; + padding: 2px; + resize: none; + border: none; + box-sizing: border-box; + font-size: 10px; + border-radius: 7px; + text-align: center; + text-wrap: balance; +} +.hideInfo-dropdown { + position: absolute; + box-sizing: border-box; + background-color: #121212; + border-radius: 7px; + box-shadow: 0 2px 4px rgba(255, 255, 255, .25); + padding: 0; + margin: 0; + list-style: none; + z-index: 1000; + overflow: auto; + max-height: 200px; +} + +.hideInfo-dropdown li { + padding: 4px 10px; + cursor: pointer; + font-family: system-ui; + font-size: 0.7rem; +} + +.hideInfo-dropdown li:hover, +.hideInfo-dropdown li.selected { + background-color: #e5e5e5; + border-radius: 7px; +} +` +styleElement.innerHTML = cssCode +document.head.appendChild(styleElement); + +const InfoSymbol = Symbol(); +const InfoResizeSymbol = Symbol(); + + + + +// WIDGET FUNCTIONS +function addInfoWidget(node, name, opts, app) { + const INFO_W_SIZE = 50; + + node.addProperty('infoWidgetHidden', false) + + function computeSize(size) { + if (node.widgets[0].last_y == null) return; + + let y = node.widgets[0].last_y; + + // Compute the height of all non easyInfo widgets + let widgetHeight = 0; + const infoWidges = []; + for (let i = 0; i < node.widgets.length; i++) { + const w = node.widgets[i]; + if (w.type === "easyInfo") { + infoWidges.push(w); + } else { + if (w.computeSize) { + widgetHeight += w.computeSize()[1] + 4; + } else { + widgetHeight += LiteGraph.NODE_WIDGET_HEIGHT + 4; + } + } + } + + let infoWidgetSpace = infoWidges.length * INFO_W_SIZE; // Height for all info widgets + + // Check if there's enough space for all widgets + if (size[1] < y + widgetHeight + infoWidgetSpace) { + // There isn't enough space for all the widgets, increase the size of the node + node.size[1] = y + widgetHeight + infoWidgetSpace; + node.graph.setDirtyCanvas(true); + } + + // Position each of the widgets + for (const w of node.widgets) { + w.y = y; + if (w.type === "easyInfo") { + y += INFO_W_SIZE; + } else if (w.computeSize) { + y += w.computeSize()[1] + 4; + } else { + y += LiteGraph.NODE_WIDGET_HEIGHT + 4; + } + } + } + + const widget = { + type: "easyInfo", + name, + get value() { + return this.inputEl.value; + }, + set value(x) { + this.inputEl.value = x; + }, + draw: function (ctx, _, widgetWidth, y, widgetHeight) { + if (!this.parent.inputHeight) { + // If we are initially offscreen when created we wont have received a resize event + // Calculate it here instead + computeSize(node.size); + } + const visible = app.canvas.ds.scale > 0.5 && this.type === "easyInfo"; + const margin = 10; + const elRect = ctx.canvas.getBoundingClientRect(); + const transform = new DOMMatrix() + .scaleSelf(elRect.width / ctx.canvas.width, elRect.height / ctx.canvas.height) + .multiplySelf(ctx.getTransform()) + .translateSelf(margin, margin + y); + + Object.assign(this.inputEl.style, { + transformOrigin: "0 0", + transform: transform, + left: "0px", + top: "0px", + width: `${widgetWidth - (margin * 2)}px`, + height: `${this.parent.inputHeight - (margin * 2)}px`, + position: "absolute", + background: (!node.color)?'':node.color, + color: (!node.color)?'':'white', + zIndex: app.graph._nodes.indexOf(node), + }); + this.inputEl.hidden = !visible; + }, + }; + widget.inputEl = document.createElement("textarea"); + widget.inputEl.className = "easy-info_widget"; + widget.inputEl.value = opts.defaultVal; + widget.inputEl.placeholder = opts.placeholder || ""; + widget.inputEl.readOnly = true; + widget.parent = node; + + document.body.appendChild(widget.inputEl); + + node.addCustomWidget(widget); + + app.canvas.onDrawBackground = function () { + // Draw node isnt fired once the node is off the screen + // if it goes off screen quickly, the input may not be removed + // this shifts it off screen so it can be moved back if the node is visible. + for (let n in app.graph._nodes) { + n = app.graph._nodes[n]; + for (let w in n.widgets) { + let wid = n.widgets[w]; + if (Object.hasOwn(wid, "inputEl")) { + wid.inputEl.style.left = -8000 + "px"; + wid.inputEl.style.position = "absolute"; + } + } + } + }; + + node.onRemoved = function () { + // When removing this node we need to remove the input from the DOM + for (let y in this.widgets) { + if (this.widgets[y].inputEl) { + this.widgets[y].inputEl.remove(); + } + } + }; + + widget.onRemove = () => { + widget.inputEl?.remove(); + + // Restore original size handler if we are the last + if (!--node[InfoSymbol]) { + node.onResize = node[InfoResizeSymbol]; + delete node[InfoSymbol]; + delete node[InfoResizeSymbol]; + } + }; + + if (node[InfoSymbol]) { + node[InfoSymbol]++; + } else { + node[InfoSymbol] = 1; + const onResize = (node[InfoResizeSymbol] = node.onResize); + + node.onResize = function (size) { + computeSize(size); + + // Call original resizer handler + if (onResize) { + console.log(this, arguments) + onResize.apply(this, arguments); + } + }; + } + + return { widget }; +} + +// WIDGETS +const easyCustomWidgets = { + INFO(node, inputName, inputData, app) { + const defaultVal = inputData[1].default || ""; + return addInfoWidget(node, inputName, { defaultVal, ...inputData[1] }, app); + }, +} + + + +app.registerExtension({ + name: "comfy.easy.widgets", + getCustomWidgets(app) { + return easyCustomWidgets; + }, + nodeCreated(node) { + if (node.widgets) { + // Locate info widgets + const widgets = node.widgets.filter((n) => (n.type === "easyInfo")); + for (const widget of widgets) { + widget.inputEl.addEventListener('contextmenu', function(e) { + hideInfoWidget(e, node, widget); + }); + widget.inputEl.addEventListener('click', function(e) { + hideInfoWidget(e, node, widget); + }); + } + } + }, + async beforeRegisterNodeDef(nodeType, nodeData, app) { + const hiddenNodeTypes = JSON.parse(localStorage.getItem('hiddenWidgetNodeTypes') || "[]"); + const origOnConfigure = nodeType.prototype.onConfigure; + nodeType.prototype.onConfigure = function () { + const r = origOnConfigure ? origOnConfigure.apply(this, arguments) : undefined; + if (this.properties['infoWidgetHidden']) { + for (let i in this.widgets) { + if (this.widgets[i].type == "easyInfo") { + hideInfoWidget(null, this, this.widgets[i]); + } + } + } + return r; + }; + const origOnAdded = nodeType.prototype.onAdded; + nodeType.prototype.onAdded = function () { + const r = origOnAdded ? origOnAdded.apply(this, arguments) : undefined; + if (hiddenNodeTypes.includes(this.type)) { + for (let i in this.widgets) { + if (this.widgets[i].type == "easyInfo") { + this.properties['infoWidgetHidden'] = true; + } + } + } + return r; + } + } +}); \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/easy/easyXYPlot.js b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/easy/easyXYPlot.js new file mode 100644 index 0000000000000000000000000000000000000000..6f27007b926511d99cdbbc97fc3747f557c4f3f7 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/easy/easyXYPlot.js @@ -0,0 +1,212 @@ +import { app } from "../../../../scripts/app.js"; +import {removeDropdown, createDropdown} from "../common/dropdown.js"; + +function generateNumList(dictionary) { + const minimum = dictionary["min"] || 0; + const maximum = dictionary["max"] || 0; + const step = dictionary["step"] || 1; + + if (step === 0) { + return []; + } + + const result = []; + let currentValue = minimum; + + while (currentValue <= maximum) { + if (Number.isInteger(step)) { + result.push(Math.round(currentValue) + '; '); + } else { + let formattedValue = currentValue.toFixed(3); + if(formattedValue == -0.000){ + formattedValue = '0.000'; + } + if (!/\.\d{3}$/.test(formattedValue)) { + formattedValue += "0"; + } + result.push(formattedValue + "; "); + } + currentValue += step; + } + + if (maximum >= 0 && minimum >= 0) { + //low to high + return result; + } + else { + //high to low + return result.reverse(); + } +} + +let plotDict = {}; +let currentOptionsDict = {}; + +function getCurrentOptionLists(node, widget) { + const nodeId = String(node.id); + const widgetName = widget.name; + const widgetValue = widget.value.replace(/^(loader|preSampling):\s/, ''); + + if (!currentOptionsDict[widgetName]) { + currentOptionsDict = {...currentOptionsDict, [widgetName]: plotDict[widgetValue]}; + } else if (currentOptionsDict[widgetName] != plotDict[widgetValue]) { + currentOptionsDict[widgetName] = plotDict[widgetValue]; + } +} + +function addGetSetters(node) { + if (node.widgets) + for (const w of node.widgets) { + if (w.name === "x_axis" || + w.name === "y_axis") { + let widgetValue = w.value; + + // Define getters and setters for widget values + Object.defineProperty(w, 'value', { + + get() { + return widgetValue; + }, + set(newVal) { + if (newVal !== widgetValue) { + widgetValue = newVal; + getCurrentOptionLists(node, w); + } + } + }); + } + } +} + +function dropdownCreator(node) { + if (node.widgets) { + const widgets = node.widgets.filter( + (n) => (n.type === "customtext" && n.dynamicPrompts !== false) || n.dynamicPrompts + ); + + for (const w of widgets) { + function replaceOptionSegments(selectedOption, inputSegments, cursorSegmentIndex, optionsList) { + if (selectedOption) { + inputSegments[cursorSegmentIndex] = selectedOption; + } + + return inputSegments.map(segment => verifySegment(segment, optionsList)) + .filter(item => item !== '') + .join(''); + } + + function verifySegment(segment, optionsList) { + segment = cleanSegment(segment); + + if (isInOptionsList(segment, optionsList)) { + return segment + '; '; + } + + let matchedOptions = findMatchedOptions(segment, optionsList); + + if (matchedOptions.length === 1 || matchedOptions.length === 2) { + return matchedOptions[0]; + } + + if (isInOptionsList(formatNumberSegment(segment), optionsList)) { + return formatNumberSegment(segment) + '; '; + } + + return ''; + } + + function cleanSegment(segment) { + return segment.replace(/(\n|;| )/g, ''); + } + + function isInOptionsList(segment, optionsList) { + return optionsList.includes(segment + '; '); + } + + function findMatchedOptions(segment, optionsList) { + return optionsList.filter(option => option.toLowerCase().includes(segment.toLowerCase())); + } + + function formatNumberSegment(segment) { + if (Number(segment)) { + return Number(segment).toFixed(3); + } + + if (['0', '0.', '0.0', '0.00', '00'].includes(segment)) { + return '0.000'; + } + return segment; + } + + + const onInput = function () { + const axisWidgetName = w.name[0] + '_axis'; + let optionsList = currentOptionsDict?.[axisWidgetName] || []; + if (optionsList.length === 0) {return} + + const inputText = w.inputEl.value; + const cursorPosition = w.inputEl.selectionStart; + let inputSegments = inputText.split('; '); + + const cursorSegmentIndex = inputText.substring(0, cursorPosition).split('; ').length - 1; + const currentSegment = inputSegments[cursorSegmentIndex]; + const currentSegmentLower = currentSegment.replace(/\n/g, '').toLowerCase(); + const filteredOptionsList = optionsList.filter(option => option.toLowerCase().includes(currentSegmentLower)).map(option => option.replace(/; /g, '')); + + if (filteredOptionsList.length > 0) { + createDropdown(w.inputEl, filteredOptionsList, (selectedOption) => { + const verifiedText = replaceOptionSegments(selectedOption, inputSegments, cursorSegmentIndex, optionsList); + w.inputEl.value = verifiedText; + }); + } + else { + removeDropdown(); + const verifiedText = replaceOptionSegments(null, inputSegments, cursorSegmentIndex, optionsList); + w.inputEl.value = verifiedText; + } + }; + + w.inputEl.removeEventListener('input', onInput); + w.inputEl.addEventListener('input', onInput); + w.inputEl.removeEventListener('mouseup', onInput); + w.inputEl.addEventListener('mouseup', onInput); + } + } +} + +app.registerExtension({ + name: "comfy.easy.xyPlot", + async beforeRegisterNodeDef(nodeType, nodeData, app) { + if (nodeData.name === "easy XYPlot") { + plotDict = nodeData.input.hidden.plot_dict[0]; + + for (const key in plotDict) { + const value = plotDict[key]; + if (Array.isArray(value)) { + let updatedValues = []; + for (const v of value) { + updatedValues.push(v + '; '); + } + plotDict[key] = updatedValues; + } else if (typeof(value) === 'object') { + if(key == 'seed'){ + plotDict[key] = value + '; '; + } + else { + plotDict[key] = generateNumList(value); + } + } else { + plotDict[key] = value + '; '; + } + } + plotDict["None"] = []; + plotDict["---------------------"] = []; + } + }, + nodeCreated(node) { + if (node.comfyClass === "easy XYPlot") { + addGetSetters(node); + dropdownCreator(node); + } + } +}); \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/getset.js b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/getset.js new file mode 100644 index 0000000000000000000000000000000000000000..78957b283f57fd3a0eaefb89b3c99e00c1b7f202 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/getset.js @@ -0,0 +1,311 @@ +import { app } from "../../../scripts/app.js"; + +// Node that allows you to tunnel connections for cleaner graphs + +app.registerExtension({ + name: "easy setNode", + registerCustomNodes() { + class SetNode { + defaultVisibility = true; + serialize_widgets = true; + constructor() { + if (!this.properties) { + this.properties = { + "previousName": "" + }; + } + this.properties.showOutputText = SetNode.defaultVisibility; + + const node = this; + node.color = LGraphCanvas.node_colors.blue.color; + + this.addWidget( + "text", + "Constant", + '', + (s, t, u, v, x) => { + node.validateName(node.graph); + if(this.widgets[0].value !== ''){ + this.title = "Set_" + this.widgets[0].value; + } + this.update(); + this.properties.previousName = this.widgets[0].value; + }, + {} + ) + + this.addInput("*", "*"); + + + this.onConnectionsChange = function( + slotType, //1 = input, 2 = output + slot, + isChangeConnect, + link_info, + output + ) { + // console.log("onConnectionsChange"); + //On Disconnect + if (slotType == 1 && !isChangeConnect) { + this.inputs[slot].type = '*'; + this.inputs[slot].name = '*'; + } + + //On Connect + if (link_info && node.graph && slotType == 1 && isChangeConnect) { + const fromNode = node.graph._nodes.find((otherNode) => otherNode.id == link_info.origin_id); + const type = fromNode.outputs[link_info.origin_slot].type; + + if (this.title === "Set"){ + this.title = "Set_" + type; + } + if (this.widgets[0].value === '*'){ + this.widgets[0].value = type + } + + this.validateName(node.graph); + this.inputs[0].type = type; + this.inputs[0].name = type; + + setTimeout(_=>{ + if(type != this.widgets[0].value){ + this.title = "Set_" + this.widgets[0].value; + } + },1) + } + + //Update either way + this.update(); + } + + this.validateName = function(graph) { + let widgetValue = node.widgets[0].value; + if (widgetValue != '') { + let tries = 0; + let collisions = []; + + do { + collisions = graph._nodes.filter((otherNode) => { + if (otherNode == this) { + return false; + } + if (otherNode.type == 'easy setNode' && otherNode.widgets[0].value === widgetValue) { + return true; + } + return false; + }) + if (collisions.length > 0) { + widgetValue = node.widgets[0].value + "_" + tries; + } + tries++; + } while (collisions.length > 0) + node.widgets[0].value = widgetValue; + this.update(); + } + } + + this.clone = function () { + const cloned = SetNode.prototype.clone.apply(this); + cloned.inputs[0].name = '*'; + cloned.inputs[0].type = '*'; + cloned.properties.previousName = ''; + cloned.size = cloned.computeSize(); + return cloned; + }; + + this.onAdded = function(graph) { + this.validateName(graph); + } + + + this.update = function() { + if (node.graph) { + this.findGetters(node.graph).forEach((getter) => { + getter.setType(this.inputs[0].type); + }); + if (this.widgets[0].value) { + this.findGetters(node.graph, true).forEach((getter) => { + getter.setName(this.widgets[0].value) + }); + } + + const allGetters = node.graph._nodes.filter((otherNode) => otherNode.type == "easy getNode"); + allGetters.forEach((otherNode) => { + if (otherNode.setComboValues) { + otherNode.setComboValues(); + } + }) + } + } + + + this.findGetters = function(graph, checkForPreviousName) { + const name = checkForPreviousName ? this.properties.previousName : this.widgets[0].value; + return graph._nodes.filter((otherNode) => { + if (otherNode.type == 'easy getNode' && otherNode.widgets[0].value === name && name != '') { + return true; + } + return false; + }) + } + // This node is purely frontend and does not impact the resulting prompt so should not be serialized + this.isVirtualNode = true; + } + + onRemoved() { + const allGetters = this.graph._nodes.filter((otherNode) => otherNode.type == "easy getNode"); + allGetters.forEach((otherNode) => { + if (otherNode.setComboValues) { + otherNode.setComboValues([this]); + } + }) + } + } + + + LiteGraph.registerNodeType( + "easy setNode", + Object.assign(SetNode, { + title: "Set", + }) + ); + + SetNode.category = "EasyUse/Util"; + }, +}); + + +app.registerExtension({ + name: "easy getNode", + registerCustomNodes() { + class GetNode { + + defaultVisibility = true; + serialize_widgets = true; + + constructor() { + if (!this.properties) { + this.properties = {}; + } + this.properties.showOutputText = GetNode.defaultVisibility; + + const node = this; + node.color = LGraphCanvas.node_colors.blue.color; + this.addWidget( + "combo", + "Constant", + "", + (e) => { + this.onRename(); + }, + { + values: () => { + const setterNodes = node.graph._nodes.filter((otherNode) => otherNode.type == 'easy setNode'); + return setterNodes.map((otherNode) => otherNode.widgets[0].value).sort(); + } + } + ) + + + this.addOutput("*", '*'); + + + this.onConnectionsChange = function( + slotType, //0 = output, 1 = input + slot, //self-explanatory + isChangeConnect, + link_info, + output + ) { + this.validateLinks(); + setTimeout(_=>{ + this.title = 'Get_' + this.widgets[0].value + },1) + } + + + this.setName = function(name) { + node.widgets[0].value = name; + node.onRename(); + node.serialize(); + } + + + this.onRename = function() { + const setter = this.findSetter(node.graph); + if (setter) { + this.setType(setter.inputs[0].type); + this.title = "Get_" + setter.widgets[0].value; + } else { + this.setType('*'); + } + } + + this.clone = function () { + const cloned = GetNode.prototype.clone.apply(this); + cloned.size = cloned.computeSize(); + return cloned; + }; + + this.validateLinks = function() { + if (this.outputs[0].type != '*' && this.outputs[0].links) { + this.outputs[0].links.forEach((linkId) => { + const link = node.graph.links[linkId]; + if (link && link.type != this.outputs[0].type && link.type != '*') { + node.graph.removeLink(linkId) + } + }) + } + } + + this.setType = function(type) { + this.outputs[0].name = type; + this.outputs[0].type = type; + this.validateLinks(); + } + + this.findSetter = function(graph) { + const name = this.widgets[0].value; + return graph._nodes.find((otherNode) => { + if (otherNode.type == 'easy setNode' && otherNode.widgets[0].value === name && name != '') { + return true; + } + return false; + }) + } + + // This node is purely frontend and does not impact the resulting prompt so should not be serialized + this.isVirtualNode = true; + } + + + getInputLink(slot) { + const setter = this.findSetter(this.graph); + + if (setter) { + const slot_info = setter.inputs[slot]; + const link = this.graph.links[ slot_info.link ]; + return link; + } else { + throw new Error("No setter found for " + this.widgets[0].value + "(" + this.type + ")"); + } + + } + onAdded(graph) { + //this.setComboValues(); + //this.validateName(graph); + } + + } + + + LiteGraph.registerNodeType( + "easy getNode", + Object.assign(GetNode, { + title: "Get", + }) + ); + + GetNode.category = "EasyUse/Util"; + }, +}); \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/image.js b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/image.js new file mode 100644 index 0000000000000000000000000000000000000000..7d8b45ca893a4a6dcc9ecd8aa8763fe60970ce18 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/image.js @@ -0,0 +1,66 @@ +import { app } from "../../../scripts/app.js"; + + +app.registerExtension({ + name: "comfy.easyUse.imageWidgets", + + nodeCreated(node) { + if (["easy imageSize","easy imageSizeBySide","easy imageSizeByLongerSide","easy imageSizeShow", "easy imageRatio", "easy imagePixelPerfect"].includes(node.comfyClass)) { + + const inputEl = document.createElement("textarea"); + inputEl.className = "comfy-multiline-input"; + inputEl.readOnly = true + + const widget = node.addDOMWidget("info", "customtext", inputEl, { + getValue() { + return inputEl.value; + }, + setValue(v) { + inputEl.value = v; + }, + serialize: false + }); + widget.inputEl = inputEl; + + inputEl.addEventListener("input", () => { + widget.callback?.(widget.value); + }); + } + }, + + beforeRegisterNodeDef(nodeType, nodeData, app) { + if (["easy imageSize","easy imageSizeBySide","easy imageSizeByLongerSide", "easy imageSizeShow", "easy imageRatio", "easy imagePixelPerfect"].includes(nodeData.name)) { + function populate(arr_text) { + var text = ''; + for (let i = 0; i < arr_text.length; i++){ + text += arr_text[i]; + } + if (this.widgets) { + const pos = this.widgets.findIndex((w) => w.name === "info"); + if (pos !== -1 && this.widgets[pos]) { + const w = this.widgets[pos] + w.value = text; + } + } + requestAnimationFrame(() => { + const sz = this.computeSize(); + if (sz[0] < this.size[0]) { + sz[0] = this.size[0]; + } + if (sz[1] < this.size[1]) { + sz[1] = this.size[1]; + } + this.onResize?.(sz); + app.graph.setDirtyCanvas(true, false); + }); + } + + // When the node is executed we will be sent the input text, display this in the widget + const onExecuted = nodeType.prototype.onExecuted; + nodeType.prototype.onExecuted = function (message) { + onExecuted?.apply(this, arguments); + populate.call(this, message.text); + }; + } + } +}) \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/image_chooser/chooser.js b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/image_chooser/chooser.js new file mode 100644 index 0000000000000000000000000000000000000000..e1c153dc6adc0432af90c653c05c6fde0ec1de59 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/image_chooser/chooser.js @@ -0,0 +1,237 @@ +import { app } from "../../../../scripts/app.js"; +import { api } from "../../../../scripts/api.js"; +import { ComfyDialog, $el } from "../../../../scripts/ui.js"; + +import { restart_from_here } from "./prompt.js"; +import { hud, FlowState } from "./state.js"; +import { send_cancel, send_message, send_onstart, skip_next_restart_message } from "./messaging.js"; +import { display_preview_images, additionalDrawBackground, click_is_in_image } from "./preview.js"; +import {$t} from "../common/i18n.js"; + + +class chooserImageDialog extends ComfyDialog { + + constructor() { + super(); + this.node = null + this.select_index = [] + this.dialog_div = null + } + + show(image,node){ + this.select_index = [] + this.node = node + + const images_div = image.map((img, index) => { + const imgEl = $el('img', { + src: img.src, + onclick: _ => { + if(this.select_index.includes(index)){ + this.select_index = this.select_index.filter(i => i !== index) + imgEl.classList.remove('selected') + } else { + this.select_index.push(index) + imgEl.classList.add('selected') + } + if (node.selected.has(index)) node.selected.delete(index); + else node.selected.add(index); + } + }) + return imgEl + }) + super.show($el('div.easyuse-chooser-dialog',[ + $el('h5.easyuse-chooser-dialog-title', $t('Choose images to continue')), + $el('div.easyuse-chooser-dialog-images',images_div) + ])) + } + createButtons() { + const btns = super.createButtons(); + btns[0].onclick = _ => { + if (FlowState.running()) { send_cancel();} + super.close() + } + btns.unshift($el('button', { + type: 'button', + textContent: $t('Choose Selected Images'), + onclick: _ => { + if (FlowState.paused()) { + send_message(this.node.id, [...this.node.selected, -1, ...this.node.anti_selected]); + } + if (FlowState.idle()) { + skip_next_restart_message(); + restart_from_here(this.node.id).then(() => { send_message(this.node.id, [...this.node.selected, -1, ...this.node.anti_selected]); }); + } + super.close() + } + })) + return btns + } + +} + +function progressButtonPressed() { + const node = app.graph._nodes_by_id[this.node_id]; + if (node) { + const selected = [...node.selected] + if(selected?.length>0){ + node.setProperty('values',selected) + } + if (FlowState.paused()) { + send_message(node.id, [...node.selected, -1, ...node.anti_selected]); + } + if (FlowState.idle()) { + skip_next_restart_message(); + restart_from_here(node.id).then(() => { send_message(node.id, [...node.selected, -1, ...node.anti_selected]); }); + } + } +} + +function cancelButtonPressed() { + + if (FlowState.running()) { send_cancel();} +} + +function enable_disabling(button) { + Object.defineProperty(button, 'clicked', { + get : function() { return this._clicked; }, + set : function(v) { this._clicked = (v && this.name!=''); } + }) +} + +function disable_serialize(widget) { + if (!widget.options) widget.options = { }; + widget.options.serialize = false; +} + +app.registerExtension({ + name:'comfy.easyuse.imageChooser', + init() { + window.addEventListener("beforeunload", send_cancel, true); + }, + setup(app) { + + const draw = LGraphCanvas.prototype.draw; + LGraphCanvas.prototype.draw = function() { + if (hud.update()) { + app.graph._nodes.forEach((node)=> { if (node.update) { node.update(); } }) + } + draw.apply(this,arguments); + } + + + function easyuseImageChooser(event) { + const {node,image,isKSampler} = display_preview_images(event); + if(isKSampler) { + const dialog = new chooserImageDialog(); + dialog.show(image,node) + } + } + api.addEventListener("easyuse-image-choose", easyuseImageChooser); + + /* + If a run is interrupted, send a cancel message (unless we're doing the cancelling, to avoid infinite loop) + */ + const original_api_interrupt = api.interrupt; + api.interrupt = function () { + if (FlowState.paused() && !FlowState.cancelling) send_cancel(); + original_api_interrupt.apply(this, arguments); + } + + /* + At the start of execution + */ + function on_execution_start() { + if (send_onstart()) { + app.graph._nodes.forEach((node)=> { + if (node.selected || node.anti_selected) { + node.selected.clear(); + node.anti_selected.clear(); + node.update(); + } + }) + } + } + api.addEventListener("execution_start", on_execution_start); + }, + + async nodeCreated(node, app) { + + if(node.comfyClass == 'easy imageChooser'){ + node.setProperty('values',[]) + + /* A property defining the top of the image when there is just one */ + if(node?.imageIndex === undefined){ + Object.defineProperty(node, 'imageIndex', { + get : function() { return null; }, + set: function (v) {node.overIndex= v}, + }) + } + if(node?.imagey === undefined){ + Object.defineProperty(node, 'imagey', { + get : function() { return null; }, + set: function (v) {return node.widgets[node.widgets.length-1].last_y+LiteGraph.NODE_WIDGET_HEIGHT;}, + }) + } + + /* Capture clicks */ + const org_onMouseDown = node.onMouseDown; + node.onMouseDown = function( e, pos, canvas ) { + if (e.isPrimary) { + const i = click_is_in_image(node, pos); + if (i>=0) { this.imageClicked(i); } + } + return (org_onMouseDown && org_onMouseDown.apply(this, arguments)); + } + + node.send_button_widget = node.addWidget("button", "", "", progressButtonPressed); + node.cancel_button_widget = node.addWidget("button", "", "", cancelButtonPressed); + enable_disabling(node.cancel_button_widget); + enable_disabling(node.send_button_widget); + disable_serialize(node.cancel_button_widget); + disable_serialize(node.send_button_widget); + + } + }, + + beforeRegisterNodeDef(nodeType, nodeData, app) { + if(nodeData?.name == 'easy imageChooser'){ + + const onDrawBackground = nodeType.prototype.onDrawBackground; + nodeType.prototype.onDrawBackground = function(ctx) { + onDrawBackground.apply(this, arguments); + additionalDrawBackground(this, ctx); + } + + nodeType.prototype.imageClicked = function (imageIndex) { + if (nodeType?.comfyClass==="easy imageChooser") { + if (this.selected.has(imageIndex)) this.selected.delete(imageIndex); + else this.selected.add(imageIndex); + this.update(); + } + } + + const update = nodeType.prototype.update; + nodeType.prototype.update = function() { + if (update) update.apply(this,arguments); + if (this.send_button_widget) { + this.send_button_widget.node_id = this.id; + const selection = ( this.selected ? this.selected.size : 0 ) + ( this.anti_selected ? this.anti_selected.size : 0 ) + const maxlength = this.imgs?.length || 0; + if (FlowState.paused_here(this.id) && selection>0) { + this.send_button_widget.name = (selection>1) ? "Progress selected (" + selection + '/' + maxlength +")" : "Progress selected image"; + } else if (selection>0) { + this.send_button_widget.name = (selection>1) ? "Progress selected (" + selection + '/' + maxlength +")" : "Progress selected image as restart"; + } + else { + this.send_button_widget.name = ""; + } + } + if (this.cancel_button_widget) { + const isRunning = FlowState.running() + this.cancel_button_widget.name = isRunning ? "Cancel current run" : ""; + } + this.setDirtyCanvas(true,true); + } + } + } +}) \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/image_chooser/messaging.js b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/image_chooser/messaging.js new file mode 100644 index 0000000000000000000000000000000000000000..b5864462dc1ab3daaa34afa885bb7bfe2d71b74e --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/image_chooser/messaging.js @@ -0,0 +1,34 @@ +import { api } from "../../../../scripts/api.js"; +import { FlowState } from "./state.js"; + +function send_message_from_pausing_node(message) { + const id = app.runningNodeId; + send_message(id, message); +} + +function send_message(id, message) { + const body = new FormData(); + body.append('message',message); + body.append('id', id); + api.fetchApi("/easyuse/image_chooser_message", { method: "POST", body, }); +} + +function send_cancel() { + send_message(-1,'__cancel__'); + FlowState.cancelling = true; + api.interrupt(); + FlowState.cancelling = false; +} + +var skip_next = 0; +function skip_next_restart_message() { skip_next += 1; } +function send_onstart() { + if (skip_next>0) { + skip_next -= 1; + return false; + } + send_message(-1,'__start__'); + return true; +} + +export { send_message_from_pausing_node, send_cancel, send_message, send_onstart, skip_next_restart_message } \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/image_chooser/preview.js b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/image_chooser/preview.js new file mode 100644 index 0000000000000000000000000000000000000000..6f8fe4407812fa809f2b4e6a118baba5ccdb084c --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/image_chooser/preview.js @@ -0,0 +1,90 @@ +import { app } from "../../../../scripts/app.js"; + +const kSampler = ['easy kSampler', 'easy kSamplerTiled', 'easy fullkSampler'] + +function display_preview_images(event) { + const node = app.graph._nodes_by_id[event.detail.id]; + if (node) { + node.selected = new Set(); + node.anti_selected = new Set(); + const image = showImages(node, event.detail.urls); + return {node,image,isKSampler:kSampler.includes(node.type)} + } else { + console.log(`Image Chooser Preview - failed to find ${event.detail.id}`) + } +} + +function showImages(node, urls) { + node.imgs = []; + urls.forEach((u)=> { + const img = new Image(); + node.imgs.push(img); + img.onload = () => { app.graph.setDirtyCanvas(true); }; + img.src = `/view?filename=${encodeURIComponent(u.filename)}&type=temp&subfolder=${app.getPreviewFormatParam()}` + }) + node.setSizeForImage?.(); + return node.imgs +} + +function drawRect(node, s, ctx) { + const padding = 1; + var rect; + if (node.imageRects) { + rect = node.imageRects[s]; + } else { + const y = node.imagey; + rect = [padding,y+padding,node.size[0]-2*padding,node.size[1]-y-2*padding]; + } + ctx.strokeRect(rect[0]+padding, rect[1]+padding, rect[2]-padding*2, rect[3]-padding*2); +} + +function additionalDrawBackground(node, ctx) { + if (!node.imgs) return; + if (node.imageRects) { + for (let i = 0; i < node.imgs.length; i++) { + // delete underlying image + ctx.fillStyle = "#000"; + ctx.fillRect(...node.imageRects[i]) + // draw the new one + const img = node.imgs[i]; + const cellWidth = node.imageRects[i][2]; + const cellHeight = node.imageRects[i][3]; + + let wratio = cellWidth/img.width; + let hratio = cellHeight/img.height; + var ratio = Math.min(wratio, hratio); + + let imgHeight = ratio * img.height; + let imgWidth = ratio * img.width; + + const imgX = node.imageRects[i][0] + (cellWidth - imgWidth)/2; + const imgY = node.imageRects[i][1] + (cellHeight - imgHeight)/2; + const cell_padding = 2; + ctx.drawImage(img, imgX+cell_padding, imgY+cell_padding, imgWidth-cell_padding*2, imgHeight-cell_padding*2); + + } + } + ctx.lineWidth = 2; + ctx.strokeStyle = "green"; + node?.selected?.forEach((s) => { drawRect(node,s, ctx) }) + ctx.strokeStyle = "#F88"; + node?.anti_selected?.forEach((s) => { drawRect(node,s, ctx) }) +} + +function click_is_in_image(node, pos) { + if (node.imgs?.length>1) { + for (var i = 0; i 0 && dx < node.imageRects[i][2] && + dy > 0 && dy < node.imageRects[i][3] ) { + return i; + } + } + } else if (node.imgs?.length==1) { + if (pos[1]>node.imagey) return 0; + } + return -1; +} + +export { display_preview_images, additionalDrawBackground, click_is_in_image } \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/image_chooser/prompt.js b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/image_chooser/prompt.js new file mode 100644 index 0000000000000000000000000000000000000000..c9c89b6289ce05d522a68823c60aa3bce35f60ad --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/image_chooser/prompt.js @@ -0,0 +1,114 @@ +import { app } from "../../../../scripts/app.js"; + +function links_with(p, node_id, down, up) { + const links_with = []; + p.workflow.links.forEach((l) => { + if (down && l[1]===node_id && !links_with.includes(l[3])) links_with.push(l[3]) + if (up && l[3]===node_id && !links_with.includes(l[1])) links_with.push(l[1]) + }); + return links_with; +} + +function _all_v_nodes(p, here_id) { + /* + Make a list of all downstream nodes. + */ + const downstream = []; + const to_process = [here_id] + while(to_process.length>0) { + const id = to_process.pop(); + downstream.push(id); + to_process.push( + ...links_with(p,id,true,false).filter((nid)=>{ + return !(downstream.includes(nid) || to_process.includes(nid)) + }) + ) + } + + /* + Now all upstream nodes from any of the downstream nodes (except us). + Put us on the result list so we don't flow up through us + */ + to_process.push(...downstream.filter((n)=>{ return n!=here_id})); + const back_upstream = [here_id]; + while(to_process.length>0) { + const id = to_process.pop(); + back_upstream.push(id); + to_process.push( + ...links_with(p,id,false,true).filter((nid)=>{ + return !(back_upstream.includes(nid) || to_process.includes(nid)) + }) + ) + } + + const keep = []; + keep.push(...downstream); + keep.push(...back_upstream.filter((n)=>{return !keep.includes(n)})); + + console.log(`Nodes to keep: ${keep}`); + return keep; +} + +async function all_v_nodes(here_id) { + const p = structuredClone(await app.graphToPrompt()); + const all_nodes = []; + p.workflow.nodes.forEach((node)=>{all_nodes.push(node.id)}) + p.workflow.links = p.workflow.links.filter((l)=>{ return (all_nodes.includes(l[1]) && all_nodes.includes(l[3]))} ) + return _all_v_nodes(p,here_id); +} + +async function restart_from_here(here_id, go_down_to_chooser=false) { + const p = structuredClone(await app.graphToPrompt()); + /* + Make a list of all nodes, and filter out links that are no longer valid + */ + const all_nodes = []; + p.workflow.nodes.forEach((node)=>{all_nodes.push(node.id)}) + p.workflow.links = p.workflow.links.filter((l)=>{ return (all_nodes.includes(l[1]) && all_nodes.includes(l[3]))} ) + + /* Move downstream to a chooser */ + if (go_down_to_chooser) { + while (!app.graph._nodes_by_id[here_id].isChooser) { + here_id = links_with(p, here_id, true, false)[0]; + } + } + + const keep = _all_v_nodes(p, here_id); + + /* + Filter p.workflow.nodes and p.workflow.links + */ + p.workflow.nodes = p.workflow.nodes.filter((node) => { + if (node.id===here_id) node.inputs.forEach((i)=>{i.link=null}) // remove our upstream links + return (keep.includes(node.id)) // only keep keepers + }) + p.workflow.links = p.workflow.links.filter((l) => {return (keep.includes(l[1]) && keep.includes(l[3]))}) + + /* + Filter the p.output object to only include nodes we're keeping + */ + const new_output = {} + for (const [key, value] of Object.entries(p.output)) { + if (keep.includes(parseInt(key))) new_output[key] = value; + } + /* + Filter the p.output entry for the start node to remove any list (ie link) inputs + */ + const new_inputs = {}; + for (const [key, value] of Object.entries(new_output[here_id.toString()].inputs)) { + if (!Array.isArray(value)) new_inputs[key] = value; + } + new_output[here_id.toString()].inputs = new_inputs; + + p.output = new_output; + + // temporarily hijack graph_to_prompt with a version that restores the old one but returns this prompt + const gtp_was = app.graphToPrompt; + app.graphToPrompt = () => { + app.graphToPrompt = gtp_was; + return p; + } + app.queuePrompt(0); +} + +export { restart_from_here, all_v_nodes } \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/image_chooser/state.js b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/image_chooser/state.js new file mode 100644 index 0000000000000000000000000000000000000000..69437d72a217a76afd86c5489130e521bcbd41e9 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/image_chooser/state.js @@ -0,0 +1,55 @@ +import { app } from "../../../../scripts/app.js"; + + +class HUD { + constructor() { + this.current_node_id = undefined; + this.class_of_current_node = null; + this.current_node_is_chooser = false; + } + + update() { + if (app.runningNodeId==this.current_node_id) return false; + + this.current_node_id = app.runningNodeId; + + if (this.current_node_id) { + this.class_of_current_node = app.graph?._nodes_by_id[app.runningNodeId.toString()]?.comfyClass; + this.current_node_is_chooser = this.class_of_current_node === "easy imageChooser" + } else { + this.class_of_current_node = undefined; + this.current_node_is_chooser = false; + } + return true; + } +} + +const hud = new HUD(); + + +class FlowState { + constructor(){} + static idle() { + return (!app.runningNodeId); + } + static paused() { + return true; + } + static paused_here(node_id) { + return (FlowState.paused() && FlowState.here(node_id)) + } + static running() { + return (!FlowState.idle()); + } + static here(node_id) { + return (app.runningNodeId==node_id); + } + static state() { + if (FlowState.paused()) return "Paused"; + if (FlowState.running()) return "Running"; + return "Idle"; + } + static cancelling = false; +} + +export { hud, FlowState} \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/poseEditor.js b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/poseEditor.js new file mode 100644 index 0000000000000000000000000000000000000000..bfd6813efd86ca7483db7f00b8677837ac93043e --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/poseEditor.js @@ -0,0 +1,666 @@ +import { app } from "../../../scripts/app.js"; +import { fabric } from "../lib/fabric.js"; + +fabric.Object.prototype.transparentCorners = false; +fabric.Object.prototype.cornerColor = "#108ce6"; +fabric.Object.prototype.borderColor = "#108ce6"; +fabric.Object.prototype.cornerSize = 10; + +let connect_keypoints = [ + [0, 1], + [1, 2], + [2, 3], + [3, 4], + [1, 5], + [5, 6], + [6, 7], + [1, 8], + [8, 9], + [9, 10], + [1, 11], + [11, 12], + [12, 13], + [0, 14], + [14, 16], + [0, 15], + [15, 17], +]; + +let connect_color = [ + [0, 0, 255], + [255, 0, 0], + [255, 170, 0], + [255, 255, 0], + [255, 85, 0], + [170, 255, 0], + [85, 255, 0], + [0, 255, 0], + [0, 255, 85], + [0, 255, 170], + [0, 255, 255], + [0, 170, 255], + [0, 85, 255], + [85, 0, 255], + [170, 0, 255], + [255, 0, 255], + [255, 0, 170], + [255, 0, 85], +]; + +const default_keypoints = [ + [241, 77], + [241, 120], + [191, 118], + [177, 183], + [163, 252], + [298, 118], + [317, 182], + [332, 245], + [225, 241], + [213, 359], + [215, 454], + [270, 240], + [282, 360], + [286, 456], + [232, 59], + [253, 60], + [225, 70], + [260, 72], +]; + +class OpenPose { + constructor(node, canvasElement) { + this.lockMode = false; + this.visibleEyes = true; + this.flipped = false; + this.node = node; + this.undo_history = LS_Poses[node.name].undo_history || []; + this.redo_history = LS_Poses[node.name].redo_history || []; + this.history_change = false; + this.canvas = this.initCanvas(canvasElement); + this.image = node.widgets.find((w) => w.name === "image"); + } + + setPose(keypoints) { + this.canvas.clear(); + + this.canvas.backgroundColor = "#000"; + + const res = []; + for (let i = 0; i < keypoints.length; i += 18) { + const chunk = keypoints.slice(i, i + 18); + res.push(chunk); + } + + for (let item of res) { + this.addPose(item); + this.canvas.discardActiveObject(); + } + } + + addPose(keypoints = undefined) { + if (keypoints === undefined) { + keypoints = default_keypoints; + } + + const group = new fabric.Group(); + + const makeCircle = ( + color, + left, + top, + line1, + line2, + line3, + line4, + line5 + ) => { + let c = new fabric.Circle({ + left: left, + top: top, + strokeWidth: 1, + radius: 5, + fill: color, + stroke: color, + }); + + c.hasControls = c.hasBorders = false; + c.line1 = line1; + c.line2 = line2; + c.line3 = line3; + c.line4 = line4; + c.line5 = line5; + + return c; + }; + + const makeLine = (coords, color) => { + return new fabric.Line(coords, { + fill: color, + stroke: color, + strokeWidth: 10, + selectable: false, + evented: false, + }); + }; + + const lines = []; + const circles = []; + + for (let i = 0; i < connect_keypoints.length; i++) { + // 接続されるidxを指定 [0, 1]なら0と1つなぐ + const item = connect_keypoints[i]; + const line = makeLine( + keypoints[item[0]].concat(keypoints[item[1]]), + `rgba(${connect_color[i].join(", ")}, 0.7)` + ); + lines.push(line); + this.canvas.add(line); + } + + for (let i = 0; i < keypoints.length; i++) { + let list = []; + + connect_keypoints.filter((item, idx) => { + if (item.includes(i)) { + list.push(lines[idx]); + return idx; + } + }); + const circle = makeCircle( + `rgb(${connect_color[i].join(", ")})`, + keypoints[i][0], + keypoints[i][1], + ...list + ); + circle["id"] = i; + circles.push(circle); + group.addWithUpdate(circle); + } + + this.canvas.discardActiveObject(); + this.canvas.setActiveObject(group); + this.canvas.add(group); + group.toActiveSelection(); + this.canvas.requestRenderAll(); + } + + initCanvas() { + this.canvas = new fabric.Canvas(this.canvas, { + backgroundColor: "#000", + preserveObjectStacking: true, + }); + + const updateLines = (target) => { + if ("_objects" in target) { + const flipX = target.flipX ? -1 : 1; + const flipY = target.flipY ? -1 : 1; + this.flipped = flipX * flipY === -1; + const showEyes = this.flipped ? !this.visibleEyes : this.visibleEyes; + + if (target.angle === 0) { + const rtop = target.top; + const rleft = target.left; + for (const item of target._objects) { + let p = item; + p.scaleX = 1; + p.scaleY = 1; + const top = + rtop + + p.top * target.scaleY * flipY + + (target.height * target.scaleY) / 2; + const left = + rleft + + p.left * target.scaleX * flipX + + (target.width * target.scaleX) / 2; + p["_top"] = top; + p["_left"] = left; + if (p["id"] === 0) { + p.line1 && p.line1.set({ x1: left, y1: top }); + } else { + p.line1 && p.line1.set({ x2: left, y2: top }); + } + if (p["id"] === 14 || p["id"] === 15) { + p.radius = showEyes ? 5 : 0; + if (p.line1) p.line1.strokeWidth = showEyes ? 10 : 0; + if (p.line2) p.line2.strokeWidth = showEyes ? 10 : 0; + } + p.line2 && p.line2.set({ x1: left, y1: top }); + p.line3 && p.line3.set({ x1: left, y1: top }); + p.line4 && p.line4.set({ x1: left, y1: top }); + p.line5 && p.line5.set({ x1: left, y1: top }); + } + } else { + const aCoords = target.aCoords; + const center = { + x: (aCoords.tl.x + aCoords.br.x) / 2, + y: (aCoords.tl.y + aCoords.br.y) / 2, + }; + const rad = (target.angle * Math.PI) / 180; + const sin = Math.sin(rad); + const cos = Math.cos(rad); + + for (const item of target._objects) { + let p = item; + const p_top = p.top * target.scaleY * flipY; + const p_left = p.left * target.scaleX * flipX; + const left = center.x + p_left * cos - p_top * sin; + const top = center.y + p_left * sin + p_top * cos; + p["_top"] = top; + p["_left"] = left; + if (p["id"] === 0) { + p.line1 && p.line1.set({ x1: left, y1: top }); + } else { + p.line1 && p.line1.set({ x2: left, y2: top }); + } + if (p["id"] === 14 || p["id"] === 15) { + p.radius = showEyes ? 5 : 0.3; + if (p.line1) p.line1.strokeWidth = showEyes ? 10 : 0; + if (p.line2) p.line2.strokeWidth = showEyes ? 10 : 0; + } + p.line2 && p.line2.set({ x1: left, y1: top }); + p.line3 && p.line3.set({ x1: left, y1: top }); + p.line4 && p.line4.set({ x1: left, y1: top }); + p.line5 && p.line5.set({ x1: left, y1: top }); + } + } + } else { + var p = target; + if (p["id"] === 0) { + p.line1 && p.line1.set({ x1: p.left, y1: p.top }); + } else { + p.line1 && p.line1.set({ x2: p.left, y2: p.top }); + } + p.line2 && p.line2.set({ x1: p.left, y1: p.top }); + p.line3 && p.line3.set({ x1: p.left, y1: p.top }); + p.line4 && p.line4.set({ x1: p.left, y1: p.top }); + p.line5 && p.line5.set({ x1: p.left, y1: p.top }); + } + this.canvas.renderAll(); + }; + + this.canvas.on("object:moving", (e) => { + updateLines(e.target); + }); + + this.canvas.on("object:scaling", (e) => { + updateLines(e.target); + this.canvas.renderAll(); + }); + + this.canvas.on("object:rotating", (e) => { + updateLines(e.target); + this.canvas.renderAll(); + }); + + this.canvas.on("object:modified", () => { + if ( + this.lockMode || + this.canvas.getActiveObject().type == "activeSelection" + ) + return; + this.undo_history.push(this.getJSON()); + this.redo_history.length = 0; + this.history_change = true; + this.uploadPoseFile(this.node.name); + }); + + if (!LS_Poses[this.node.name].undo_history.length) { + this.setPose(default_keypoints); + this.undo_history.push(this.getJSON()); + } + return this.canvas; + } + + undo() { + if (this.undo_history.length > 0) { + this.lockMode = true; + if (this.undo_history.length > 1) + this.redo_history.push(this.undo_history.pop()); + + const content = this.undo_history[this.undo_history.length - 1]; + this.loadPreset(content); + this.canvas.renderAll(); + this.lockMode = false; + this.history_change = true; + this.uploadPoseFile(this.node.name); + } + } + + redo() { + if (this.redo_history.length > 0) { + this.lockMode = true; + const content = this.redo_history.pop(); + this.undo_history.push(content); + this.loadPreset(content); + this.canvas.renderAll(); + this.lockMode = false; + this.history_change = true; + this.uploadPoseFile(this.node.name); + } + } + + resetCanvas() { + this.canvas.clear(); + this.canvas.backgroundColor = "#000"; + this.addPose(); + } + + updateHistoryData() { + if (this.history_change) { + LS_Poses[this.node.name].undo_history = this.undo_history; + LS_Poses[this.node.name].redo_history = this.redo_history; + LS_Save(); + this.history_change = false; + } + } + + uploadPoseFile(fileName) { + // Upload pose to temp folder ComfyUI + + const uploadFile = async (blobFile) => { + try { + const resp = await fetch("/upload/image", { + method: "POST", + body: blobFile, + }); + + if (resp.status === 200) { + const data = await resp.json(); + + if (!this.image.options.values.includes(data.name)) { + this.image.options.values.push(data.name); + } + + this.image.value = data.name; + this.updateHistoryData(); + } else { + alert(resp.status + " - " + resp.statusText); + } + } catch (error) { + console.error(error); + } + }; + + this.canvas.lowerCanvasEl.toBlob(function (blob) { + let formData = new FormData(); + formData.append("image", blob, fileName); + formData.append("overwrite", "true"); + formData.append("type", "temp"); + uploadFile(formData); + }, "image/png"); + // - end + + const callb = this.node.callback, + self = this; + this.image.callback = function () { + this.image.value = self.node.name; + if (callb) { + return callb.apply(this, arguments); + } + }; + } + + getJSON() { + const json = { + keypoints: this.canvas + .getObjects() + .filter((item) => { + if (item.type === "circle") return item; + }) + .map((item) => { + return [Math.round(item.left), Math.round(item.top)]; + }), + }; + + return json; + } + + loadPreset(json) { + try { + if (json["keypoints"].length % 18 === 0) { + this.setPose(json["keypoints"]); + } else { + throw new Error("keypoints is invalid"); + } + } catch (e) { + console.error(e); + } + } +} + +// Create OpenPose widget +function createOpenPose(node, inputName, inputData, app) { + node.name = inputName; + const widget = { + type: "openpose", + name: `w${inputName}`, + + draw: function (ctx, _, widgetWidth, y, widgetHeight) { + const margin = 10, + visible = app.canvas.ds.scale > 0.5 && this.type === "openpose", + clientRectBound = ctx.canvas.getBoundingClientRect(), + transform = new DOMMatrix() + .scaleSelf( + clientRectBound.width / ctx.canvas.width, + clientRectBound.height / ctx.canvas.height + ) + .multiplySelf(ctx.getTransform()) + .translateSelf(margin, margin + y), + w = (widgetWidth - margin * 2 - 3) * transform.a; + + Object.assign(this.openpose.style, { + left: `${transform.a * margin + transform.e}px`, + top: `${transform.d + transform.f}px`, + width: w + "px", + height: w + "px", + position: "absolute", + zIndex: app.graph._nodes.indexOf(node), + }); + + Object.assign(this.openpose.children[0].style, { + width: w + "px", + height: w + "px", + }); + + Object.assign(this.openpose.children[1].style, { + width: w + "px", + height: w + "px", + }); + + Array.from(this.openpose.children[2].children).forEach((element) => { + Object.assign(element.style, { + width: `${28.0 * transform.a}px`, + height: `${22.0 * transform.d}px`, + fontSize: `${transform.d * 10.0}px`, + }); + element.hidden = !visible; + }); + }, + }; + + // Fabric canvas + let canvasOpenPose = document.createElement("canvas"); + node.openPose = new OpenPose(node, canvasOpenPose); + + node.openPose.canvas.setWidth(512); + node.openPose.canvas.setHeight(512); + + let widgetCombo = node.widgets.filter((w) => w.type === "combo"); + widgetCombo[0].value = node.name; + + widget.openpose = node.openPose.canvas.wrapperEl; + widget.parent = node; + + // Create elements undo, redo, clear history + let panelButtons = document.createElement("div"), + undoButton = document.createElement("button"), + redoButton = document.createElement("button"), + historyClearButton = document.createElement("button"); + + panelButtons.className = "panelButtons comfy-menu-btns"; + undoButton.textContent = "⟲"; + redoButton.textContent = "⟳"; + historyClearButton.textContent = "✖"; + undoButton.title = "Undo"; + redoButton.title = "Redo"; + historyClearButton.title = "Clear History"; + + undoButton.addEventListener("click", () => node.openPose.undo()); + redoButton.addEventListener("click", () => node.openPose.redo()); + historyClearButton.addEventListener("click", () => { + if (confirm(`Delete all pose history of a node "${node.name}"?`)) { + node.openPose.undo_history = []; + node.openPose.redo_history = []; + node.openPose.setPose(default_keypoints); + node.openPose.undo_history.push(node.openPose.getJSON()); + node.openPose.history_change = true; + node.openPose.updateHistoryData(); + } + }); + + panelButtons.appendChild(undoButton); + panelButtons.appendChild(redoButton); + panelButtons.appendChild(historyClearButton); + node.openPose.canvas.wrapperEl.appendChild(panelButtons); + + document.body.appendChild(widget.openpose); + + // Add buttons add, reset, undo, redo poses + node.addWidget("button", "Add pose", "add_pose", () => { + node.openPose.addPose(); + }); + + node.addWidget("button", "Reset pose", "reset_pose", () => { + node.openPose.resetCanvas(); + }); + + // Add customWidget to node + node.addCustomWidget(widget); + + node.onRemoved = () => { + if (Object.hasOwn(LS_Poses, node.name)) { + delete LS_Poses[node.name]; + LS_Save(); + } + + // When removing this node we need to remove the input from the DOM + for (let y in node.widgets) { + if (node.widgets[y].openpose) { + node.widgets[y].openpose.remove(); + } + } + }; + + widget.onRemove = () => { + widget.openpose?.remove(); + }; + + app.canvas.onDrawBackground = function () { + // Draw node isnt fired once the node is off the screen + // if it goes off screen quickly, the input may not be removed + // this shifts it off screen so it can be moved back if the node is visible. + for (let n in app.graph._nodes) { + n = graph._nodes[n]; + for (let w in n.widgets) { + let wid = n.widgets[w]; + if (Object.hasOwn(wid, "openpose")) { + wid.openpose.style.left = -8000 + "px"; + wid.openpose.style.position = "absolute"; + } + } + } + }; + return { widget: widget }; +} + +window.LS_Poses = {}; +function LS_Save() { + ///console.log("Save:", LS_Poses); + localStorage.setItem("ComfyUI_Poses", JSON.stringify(LS_Poses)); +} + +app.registerExtension({ + name: "comfy.easyuse.poseEditor", + async init(app) { + // Any initial setup to run as soon as the page loads + let style = document.createElement("style"); + style.innerText = `.panelButtons{ + position: absolute; + padding: 4px; + display: flex; + gap: 4px; + flex-direction: column; + width: fit-content; + } + .panelButtons button:last-child{ + border-color: var(--error-text); + color: var(--error-text) !important; + } + + `; + document.head.appendChild(style); + }, + async setup(app) { + let openPoseNode = app.graph._nodes.filter((wi) => wi.type == "easy poseEditor"); + + if (openPoseNode.length) { + openPoseNode.map((n) => { + console.log(`Setup PoseNode: ${n.name}`); + let widgetImage = n.widgets.find((w) => w.name == "image"); + if (widgetImage && Object.hasOwn(LS_Poses, n.name)) { + let pose_ls = LS_Poses[n.name].undo_history; + n.openPose.loadPreset( + pose_ls.length > 0 + ? pose_ls[pose_ls.length - 1] + : { keypoints: default_keypoints } + ); + } + }); + } + }, + async beforeRegisterNodeDef(nodeType, nodeData, app) { + if (nodeData.name === "easy poseEditor") { + const onNodeCreated = nodeType.prototype.onNodeCreated; + + nodeType.prototype.onNodeCreated = function () { + const r = onNodeCreated + ? onNodeCreated.apply(this, arguments) + : undefined; + + let openPoseNode = app.graph._nodes.filter( + (wi) => {wi.type == "easy poseEditor"} + ), + nodeName = `Pose_${openPoseNode.length}`, + nodeNamePNG = `${nodeName}.png`; + + console.log(`Create PoseNode: ${nodeName}`); + + LS_Poses = + localStorage.getItem("ComfyUI_Poses") && + JSON.parse(localStorage.getItem("ComfyUI_Poses")); + if (!LS_Poses) { + localStorage.setItem("ComfyUI_Poses", JSON.stringify({})); + LS_Poses = JSON.parse(localStorage.getItem("ComfyUI_Poses")); + } + + if (!Object.hasOwn(LS_Poses, nodeNamePNG)) { + LS_Poses[nodeNamePNG] = { + undo_history: [], + redo_history: [], + }; + LS_Save(); + } + + createOpenPose.apply(this, [this, nodeNamePNG, {}, app]); + setTimeout(() => { + this.openPose.uploadPoseFile(nodeNamePNG); + }, 1); + + this.setSize([530, 620]); + + return r; + }; + } + }, +}); diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/seed.js b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/seed.js new file mode 100644 index 0000000000000000000000000000000000000000..dc6c17e050c15c2cc04c0c47af16b02ad4e97b8a --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/js/seed.js @@ -0,0 +1,47 @@ +import { api } from "../../../scripts/api.js"; + +// 全局Seed +function globalSeedHandler(event) { + let nodes = app.graph._nodes_by_id; + for(let i in nodes) { + let node = nodes[i]; + if(node.type == 'easy globalSeed') { + if(node.widgets) { + const w = node.widgets.find((w) => w.name == 'value'); + const last_w = node.widgets.find((w) => w.name == 'last_seed'); + last_w.value = w.value; + w.value = event.detail.value; + } + } + else{ + if(node.widgets) { + const w = node.widgets.find((w) => w.name == 'seed_num' || w.name == 'seed' || w.name == 'noise_seed'); + if(w && event.detail.seed_map[node.id] != undefined) { + w.value = event.detail.seed_map[node.id]; + } + } + } + + } +} + +api.addEventListener("easyuse-global-seed", globalSeedHandler); + +const original_queuePrompt = api.queuePrompt; +async function queuePrompt_with_seed(number, { output, workflow }) { + workflow.seed_widgets = {}; + + for(let i in app.graph._nodes_by_id) { + let widgets = app.graph._nodes_by_id[i].widgets; + if(widgets) { + for(let j in widgets) { + if((widgets[j].name == 'seed_num' || widgets[j].name == 'seed' || widgets[j].name == 'noise_seed') && widgets[j].type != 'converted-widget') + workflow.seed_widgets[i] = parseInt(j); + } + } + } + + return await original_queuePrompt.call(api, number, { output, workflow }); +} + +api.queuePrompt = queuePrompt_with_seed; diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/lib/fabric.js b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/lib/fabric.js new file mode 100644 index 0000000000000000000000000000000000000000..fa7bd3ce09e2b1c4168a380f625cb8a96c05ab0a --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v1/lib/fabric.js @@ -0,0 +1,10 @@ +/** + * Bundled by jsDelivr using Rollup v2.79.1 and Terser v5.17.1. + * Original file: /npm/fabric-with-all-modules@4.6.1/dist/fabric.js + * + * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files + */ +function t(t,e){return e.forEach((function(e){e&&"string"!=typeof e&&!Array.isArray(e)&&Object.keys(e).forEach((function(i){if("default"!==i&&!(i in t)){var r=Object.getOwnPropertyDescriptor(e,i);Object.defineProperty(t,i,r.get?r:{enumerable:!0,get:function(){return e[i]}})}}))})),Object.freeze(t)}var e="undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{},i=[],r=[],n="undefined"!=typeof Uint8Array?Uint8Array:Array,o=!1;function s(){o=!0;for(var t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",e=0;e<64;++e)i[e]=t[e],r[t.charCodeAt(e)]=e;r["-".charCodeAt(0)]=62,r["_".charCodeAt(0)]=63}function a(t,e,r){for(var n,o,s=[],a=e;a>18&63]+i[o>>12&63]+i[o>>6&63]+i[63&o]);return s.join("")}function h(t){var e;o||s();for(var r=t.length,n=r%3,h="",c=[],l=16383,u=0,f=r-n;uf?f:u+l));return 1===n?(e=t[r-1],h+=i[e>>2],h+=i[e<<4&63],h+="=="):2===n&&(e=(t[r-2]<<8)+t[r-1],h+=i[e>>10],h+=i[e>>4&63],h+=i[e<<2&63],h+="="),c.push(h),c.join("")}function c(t,e,i,r,n){var o,s,a=8*n-r-1,h=(1<>1,l=-7,u=i?n-1:0,f=i?-1:1,d=t[e+u];for(u+=f,o=d&(1<<-l)-1,d>>=-l,l+=a;l>0;o=256*o+t[e+u],u+=f,l-=8);for(s=o&(1<<-l)-1,o>>=-l,l+=r;l>0;s=256*s+t[e+u],u+=f,l-=8);if(0===o)o=1-c;else{if(o===h)return s?NaN:1/0*(d?-1:1);s+=Math.pow(2,r),o-=c}return(d?-1:1)*s*Math.pow(2,o-r)}function l(t,e,i,r,n,o){var s,a,h,c=8*o-n-1,l=(1<>1,f=23===n?Math.pow(2,-24)-Math.pow(2,-77):0,d=r?0:o-1,g=r?1:-1,p=e<0||0===e&&1/e<0?1:0;for(e=Math.abs(e),isNaN(e)||e===1/0?(a=isNaN(e)?1:0,s=l):(s=Math.floor(Math.log(e)/Math.LN2),e*(h=Math.pow(2,-s))<1&&(s--,h*=2),(e+=s+u>=1?f/h:f*Math.pow(2,1-u))*h>=2&&(s++,h/=2),s+u>=l?(a=0,s=l):s+u>=1?(a=(e*h-1)*Math.pow(2,n),s+=u):(a=e*Math.pow(2,u-1)*Math.pow(2,n),s=0));n>=8;t[i+d]=255&a,d+=g,a/=256,n-=8);for(s=s<0;t[i+d]=255&s,d+=g,s/=256,c-=8);t[i+d-g]|=128*p}var u={}.toString,f=Array.isArray||function(t){return"[object Array]"==u.call(t)};function d(){return p.TYPED_ARRAY_SUPPORT?2147483647:1073741823}function g(t,e){if(d()=d())throw new RangeError("Attempt to allocate Buffer larger than maximum size: 0x"+d().toString(16)+" bytes");return 0|t}function x(t){return!(null==t||!t._isBuffer)}function C(t,e){if(x(t))return t.length;if("undefined"!=typeof ArrayBuffer&&"function"==typeof ArrayBuffer.isView&&(ArrayBuffer.isView(t)||t instanceof ArrayBuffer))return t.byteLength;"string"!=typeof t&&(t=""+t);var i=t.length;if(0===i)return 0;for(var r=!1;;)switch(e){case"ascii":case"latin1":case"binary":return i;case"utf8":case"utf-8":case void 0:return K(t).length;case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return 2*i;case"hex":return i>>>1;case"base64":return J(t).length;default:if(r)return K(t).length;e=(""+e).toLowerCase(),r=!0}}function w(t,e,i){var r=!1;if((void 0===e||e<0)&&(e=0),e>this.length)return"";if((void 0===i||i>this.length)&&(i=this.length),i<=0)return"";if((i>>>=0)<=(e>>>=0))return"";for(t||(t="utf8");;)switch(t){case"hex":return B(this,e,i);case"utf8":case"utf-8":return F(this,e,i);case"ascii":return I(this,e,i);case"latin1":case"binary":return R(this,e,i);case"base64":return j(this,e,i);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return Y(this,e,i);default:if(r)throw new TypeError("Unknown encoding: "+t);t=(t+"").toLowerCase(),r=!0}}function S(t,e,i){var r=t[e];t[e]=t[i],t[i]=r}function T(t,e,i,r,n){if(0===t.length)return-1;if("string"==typeof i?(r=i,i=0):i>2147483647?i=2147483647:i<-2147483648&&(i=-2147483648),i=+i,isNaN(i)&&(i=n?0:t.length-1),i<0&&(i=t.length+i),i>=t.length){if(n)return-1;i=t.length-1}else if(i<0){if(!n)return-1;i=0}if("string"==typeof e&&(e=p.from(e,r)),x(e))return 0===e.length?-1:O(t,e,i,r,n);if("number"==typeof e)return e&=255,p.TYPED_ARRAY_SUPPORT&&"function"==typeof Uint8Array.prototype.indexOf?n?Uint8Array.prototype.indexOf.call(t,e,i):Uint8Array.prototype.lastIndexOf.call(t,e,i):O(t,[e],i,r,n);throw new TypeError("val must be string, number or Buffer")}function O(t,e,i,r,n){var o,s=1,a=t.length,h=e.length;if(void 0!==r&&("ucs2"===(r=String(r).toLowerCase())||"ucs-2"===r||"utf16le"===r||"utf-16le"===r)){if(t.length<2||e.length<2)return-1;s=2,a/=2,h/=2,i/=2}function c(t,e){return 1===s?t[e]:t.readUInt16BE(e*s)}if(n){var l=-1;for(o=i;oa&&(i=a-h),o=i;o>=0;o--){for(var u=!0,f=0;fn&&(r=n):r=n;var o=e.length;if(o%2!=0)throw new TypeError("Invalid hex string");r>o/2&&(r=o/2);for(var s=0;s>8,n=i%256,o.push(n),o.push(r);return o}(e,t.length-i),t,i,r)}function j(t,e,i){return 0===e&&i===t.length?h(t):h(t.slice(e,i))}function F(t,e,i){i=Math.min(t.length,i);for(var r=[],n=e;n239?4:c>223?3:c>191?2:1;if(n+u<=i)switch(u){case 1:c<128&&(l=c);break;case 2:128==(192&(o=t[n+1]))&&(h=(31&c)<<6|63&o)>127&&(l=h);break;case 3:o=t[n+1],s=t[n+2],128==(192&o)&&128==(192&s)&&(h=(15&c)<<12|(63&o)<<6|63&s)>2047&&(h<55296||h>57343)&&(l=h);break;case 4:o=t[n+1],s=t[n+2],a=t[n+3],128==(192&o)&&128==(192&s)&&128==(192&a)&&(h=(15&c)<<18|(63&o)<<12|(63&s)<<6|63&a)>65535&&h<1114112&&(l=h)}null===l?(l=65533,u=1):l>65535&&(l-=65536,r.push(l>>>10&1023|55296),l=56320|1023&l),r.push(l),n+=u}return function(t){var e=t.length;if(e<=L)return String.fromCharCode.apply(String,t);var i="",r=0;for(;r0&&(t=this.toString("hex",0,50).match(/.{2}/g).join(" "),this.length>50&&(t+=" ... ")),""},p.prototype.compare=function(t,e,i,r,n){if(!x(t))throw new TypeError("Argument must be a Buffer");if(void 0===e&&(e=0),void 0===i&&(i=t?t.length:0),void 0===r&&(r=0),void 0===n&&(n=this.length),e<0||i>t.length||r<0||n>this.length)throw new RangeError("out of range index");if(r>=n&&e>=i)return 0;if(r>=n)return-1;if(e>=i)return 1;if(this===t)return 0;for(var o=(n>>>=0)-(r>>>=0),s=(i>>>=0)-(e>>>=0),a=Math.min(o,s),h=this.slice(r,n),c=t.slice(e,i),l=0;ln)&&(i=n),t.length>0&&(i<0||e<0)||e>this.length)throw new RangeError("Attempt to write outside buffer bounds");r||(r="utf8");for(var o=!1;;)switch(r){case"hex":return P(this,t,e,i);case"utf8":case"utf-8":return E(this,t,e,i);case"ascii":return k(this,t,e,i);case"latin1":case"binary":return A(this,t,e,i);case"base64":return D(this,t,e,i);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return M(this,t,e,i);default:if(o)throw new TypeError("Unknown encoding: "+r);r=(""+r).toLowerCase(),o=!0}},p.prototype.toJSON=function(){return{type:"Buffer",data:Array.prototype.slice.call(this._arr||this,0)}};var L=4096;function I(t,e,i){var r="";i=Math.min(t.length,i);for(var n=e;nr)&&(i=r);for(var n="",o=e;oi)throw new RangeError("Trying to access beyond buffer length")}function U(t,e,i,r,n,o){if(!x(t))throw new TypeError('"buffer" argument must be a Buffer instance');if(e>n||et.length)throw new RangeError("Index out of range")}function G(t,e,i,r){e<0&&(e=65535+e+1);for(var n=0,o=Math.min(t.length-i,2);n>>8*(r?n:1-n)}function H(t,e,i,r){e<0&&(e=4294967295+e+1);for(var n=0,o=Math.min(t.length-i,4);n>>8*(r?n:3-n)&255}function W(t,e,i,r,n,o){if(i+r>t.length)throw new RangeError("Index out of range");if(i<0)throw new RangeError("Index out of range")}function z(t,e,i,r,n){return n||W(t,0,i,4),l(t,e,i,r,23,4),i+4}function N(t,e,i,r,n){return n||W(t,0,i,8),l(t,e,i,r,52,8),i+8}p.prototype.slice=function(t,e){var i,r=this.length;if((t=~~t)<0?(t+=r)<0&&(t=0):t>r&&(t=r),(e=void 0===e?r:~~e)<0?(e+=r)<0&&(e=0):e>r&&(e=r),e0&&(n*=256);)r+=this[t+--e]*n;return r},p.prototype.readUInt8=function(t,e){return e||X(t,1,this.length),this[t]},p.prototype.readUInt16LE=function(t,e){return e||X(t,2,this.length),this[t]|this[t+1]<<8},p.prototype.readUInt16BE=function(t,e){return e||X(t,2,this.length),this[t]<<8|this[t+1]},p.prototype.readUInt32LE=function(t,e){return e||X(t,4,this.length),(this[t]|this[t+1]<<8|this[t+2]<<16)+16777216*this[t+3]},p.prototype.readUInt32BE=function(t,e){return e||X(t,4,this.length),16777216*this[t]+(this[t+1]<<16|this[t+2]<<8|this[t+3])},p.prototype.readIntLE=function(t,e,i){t|=0,e|=0,i||X(t,e,this.length);for(var r=this[t],n=1,o=0;++o=(n*=128)&&(r-=Math.pow(2,8*e)),r},p.prototype.readIntBE=function(t,e,i){t|=0,e|=0,i||X(t,e,this.length);for(var r=e,n=1,o=this[t+--r];r>0&&(n*=256);)o+=this[t+--r]*n;return o>=(n*=128)&&(o-=Math.pow(2,8*e)),o},p.prototype.readInt8=function(t,e){return e||X(t,1,this.length),128&this[t]?-1*(255-this[t]+1):this[t]},p.prototype.readInt16LE=function(t,e){e||X(t,2,this.length);var i=this[t]|this[t+1]<<8;return 32768&i?4294901760|i:i},p.prototype.readInt16BE=function(t,e){e||X(t,2,this.length);var i=this[t+1]|this[t]<<8;return 32768&i?4294901760|i:i},p.prototype.readInt32LE=function(t,e){return e||X(t,4,this.length),this[t]|this[t+1]<<8|this[t+2]<<16|this[t+3]<<24},p.prototype.readInt32BE=function(t,e){return e||X(t,4,this.length),this[t]<<24|this[t+1]<<16|this[t+2]<<8|this[t+3]},p.prototype.readFloatLE=function(t,e){return e||X(t,4,this.length),c(this,t,!0,23,4)},p.prototype.readFloatBE=function(t,e){return e||X(t,4,this.length),c(this,t,!1,23,4)},p.prototype.readDoubleLE=function(t,e){return e||X(t,8,this.length),c(this,t,!0,52,8)},p.prototype.readDoubleBE=function(t,e){return e||X(t,8,this.length),c(this,t,!1,52,8)},p.prototype.writeUIntLE=function(t,e,i,r){(t=+t,e|=0,i|=0,r)||U(this,t,e,i,Math.pow(2,8*i)-1,0);var n=1,o=0;for(this[e]=255&t;++o=0&&(o*=256);)this[e+n]=t/o&255;return e+i},p.prototype.writeUInt8=function(t,e,i){return t=+t,e|=0,i||U(this,t,e,1,255,0),p.TYPED_ARRAY_SUPPORT||(t=Math.floor(t)),this[e]=255&t,e+1},p.prototype.writeUInt16LE=function(t,e,i){return t=+t,e|=0,i||U(this,t,e,2,65535,0),p.TYPED_ARRAY_SUPPORT?(this[e]=255&t,this[e+1]=t>>>8):G(this,t,e,!0),e+2},p.prototype.writeUInt16BE=function(t,e,i){return t=+t,e|=0,i||U(this,t,e,2,65535,0),p.TYPED_ARRAY_SUPPORT?(this[e]=t>>>8,this[e+1]=255&t):G(this,t,e,!1),e+2},p.prototype.writeUInt32LE=function(t,e,i){return t=+t,e|=0,i||U(this,t,e,4,4294967295,0),p.TYPED_ARRAY_SUPPORT?(this[e+3]=t>>>24,this[e+2]=t>>>16,this[e+1]=t>>>8,this[e]=255&t):H(this,t,e,!0),e+4},p.prototype.writeUInt32BE=function(t,e,i){return t=+t,e|=0,i||U(this,t,e,4,4294967295,0),p.TYPED_ARRAY_SUPPORT?(this[e]=t>>>24,this[e+1]=t>>>16,this[e+2]=t>>>8,this[e+3]=255&t):H(this,t,e,!1),e+4},p.prototype.writeIntLE=function(t,e,i,r){if(t=+t,e|=0,!r){var n=Math.pow(2,8*i-1);U(this,t,e,i,n-1,-n)}var o=0,s=1,a=0;for(this[e]=255&t;++o>0)-a&255;return e+i},p.prototype.writeIntBE=function(t,e,i,r){if(t=+t,e|=0,!r){var n=Math.pow(2,8*i-1);U(this,t,e,i,n-1,-n)}var o=i-1,s=1,a=0;for(this[e+o]=255&t;--o>=0&&(s*=256);)t<0&&0===a&&0!==this[e+o+1]&&(a=1),this[e+o]=(t/s>>0)-a&255;return e+i},p.prototype.writeInt8=function(t,e,i){return t=+t,e|=0,i||U(this,t,e,1,127,-128),p.TYPED_ARRAY_SUPPORT||(t=Math.floor(t)),t<0&&(t=255+t+1),this[e]=255&t,e+1},p.prototype.writeInt16LE=function(t,e,i){return t=+t,e|=0,i||U(this,t,e,2,32767,-32768),p.TYPED_ARRAY_SUPPORT?(this[e]=255&t,this[e+1]=t>>>8):G(this,t,e,!0),e+2},p.prototype.writeInt16BE=function(t,e,i){return t=+t,e|=0,i||U(this,t,e,2,32767,-32768),p.TYPED_ARRAY_SUPPORT?(this[e]=t>>>8,this[e+1]=255&t):G(this,t,e,!1),e+2},p.prototype.writeInt32LE=function(t,e,i){return t=+t,e|=0,i||U(this,t,e,4,2147483647,-2147483648),p.TYPED_ARRAY_SUPPORT?(this[e]=255&t,this[e+1]=t>>>8,this[e+2]=t>>>16,this[e+3]=t>>>24):H(this,t,e,!0),e+4},p.prototype.writeInt32BE=function(t,e,i){return t=+t,e|=0,i||U(this,t,e,4,2147483647,-2147483648),t<0&&(t=4294967295+t+1),p.TYPED_ARRAY_SUPPORT?(this[e]=t>>>24,this[e+1]=t>>>16,this[e+2]=t>>>8,this[e+3]=255&t):H(this,t,e,!1),e+4},p.prototype.writeFloatLE=function(t,e,i){return z(this,t,e,!0,i)},p.prototype.writeFloatBE=function(t,e,i){return z(this,t,e,!1,i)},p.prototype.writeDoubleLE=function(t,e,i){return N(this,t,e,!0,i)},p.prototype.writeDoubleBE=function(t,e,i){return N(this,t,e,!1,i)},p.prototype.copy=function(t,e,i,r){if(i||(i=0),r||0===r||(r=this.length),e>=t.length&&(e=t.length),e||(e=0),r>0&&r=this.length)throw new RangeError("sourceStart out of bounds");if(r<0)throw new RangeError("sourceEnd out of bounds");r>this.length&&(r=this.length),t.length-e=0;--n)t[n+e]=this[n+i];else if(o<1e3||!p.TYPED_ARRAY_SUPPORT)for(n=0;n>>=0,i=void 0===i?this.length:i>>>0,t||(t=0),"number"==typeof t)for(o=e;o55295&&i<57344){if(!n){if(i>56319){(e-=3)>-1&&o.push(239,191,189);continue}if(s+1===r){(e-=3)>-1&&o.push(239,191,189);continue}n=i;continue}if(i<56320){(e-=3)>-1&&o.push(239,191,189),n=i;continue}i=65536+(n-55296<<10|i-56320)}else n&&(e-=3)>-1&&o.push(239,191,189);if(n=null,i<128){if((e-=1)<0)break;o.push(i)}else if(i<2048){if((e-=2)<0)break;o.push(i>>6|192,63&i|128)}else if(i<65536){if((e-=3)<0)break;o.push(i>>12|224,i>>6&63|128,63&i|128)}else{if(!(i<1114112))throw new Error("Invalid code point");if((e-=4)<0)break;o.push(i>>18|240,i>>12&63|128,i>>6&63|128,63&i|128)}}return o}function J(t){return function(t){var e,i,a,h,c,l;o||s();var u=t.length;if(u%4>0)throw new Error("Invalid string. Length must be a multiple of 4");c="="===t[u-2]?2:"="===t[u-1]?1:0,l=new n(3*u/4-c),a=c>0?u-4:u;var f=0;for(e=0,i=0;e>16&255,l[f++]=h>>8&255,l[f++]=255&h;return 2===c?(h=r[t.charCodeAt(e)]<<2|r[t.charCodeAt(e+1)]>>4,l[f++]=255&h):1===c&&(h=r[t.charCodeAt(e)]<<10|r[t.charCodeAt(e+1)]<<4|r[t.charCodeAt(e+2)]>>2,l[f++]=h>>8&255,l[f++]=255&h),l}(function(t){if((t=function(t){return t.trim?t.trim():t.replace(/^\s+|\s+$/g,"")}(t).replace(V,"")).length<2)return"";for(;t.length%4!=0;)t+="=";return t}(t))}function Z(t,e,i,r){for(var n=0;n=e.length||n>=t.length);++n)e[n+i]=t[n];return n}function $(t){return!!t.constructor&&"function"==typeof t.constructor.isBuffer&&t.constructor.isBuffer(t)}function Q(t){if(t.__esModule)return t;var e=Object.defineProperty({},"__esModule",{value:!0});return Object.keys(t).forEach((function(i){var r=Object.getOwnPropertyDescriptor(t,i);Object.defineProperty(e,i,r.get?r:{enumerable:!0,get:function(){return t[i]}})})),e}var tt={},et={},it=Q(t({__proto__:null,default:et},[et]));!function(t){ +/*! Fabric.js Copyright 2008-2015, Printio (Juriy Zaytsev, Maxim Chernyak) */ +var e,i,r,n,o,s,a,h,c,l,u,f,d,g,v,m,y,_,b,x,C,w,S,T=T||{version:"4.6.0"};if(t.fabric=T,"undefined"!=typeof document&&"undefined"!=typeof window)document instanceof("undefined"!=typeof HTMLDocument?HTMLDocument:Document)?T.document=document:T.document=document.implementation.createHTMLDocument(""),T.window=window;else{var O=new it.JSDOM(decodeURIComponent("%3C!DOCTYPE%20html%3E%3Chtml%3E%3Chead%3E%3C%2Fhead%3E%3Cbody%3E%3C%2Fbody%3E%3C%2Fhtml%3E"),{features:{FetchExternalResources:["img"]},resources:"usable"}).window;T.document=O.document,T.jsdomImplForWrapper=it.implForWrapper,T.nodeCanvas=it.Canvas,T.window=O,DOMParser=T.window.DOMParser}if(T.isTouchSupported="ontouchstart"in T.window||"ontouchstart"in T.document||T.window&&T.window.navigator&&T.window.navigator.maxTouchPoints>0,T.isLikelyNode=void 0!==p&&"undefined"==typeof window,T.SHARED_ATTRIBUTES=["display","transform","fill","fill-opacity","fill-rule","opacity","stroke","stroke-dasharray","stroke-linecap","stroke-dashoffset","stroke-linejoin","stroke-miterlimit","stroke-opacity","stroke-width","id","paint-order","vector-effect","instantiated_by_use","clip-path"],T.DPI=96,T.reNum="(?:[-+]?(?:\\d+|\\d*\\.\\d+)(?:[eE][-+]?\\d+)?)",T.commaWsp="(?:\\s+,?\\s*|,\\s*)",T.rePathCommand=/([-+]?((\d+\.\d+)|((\d+)|(\.\d+)))(?:[eE][-+]?\d+)?)/gi,T.reNonWord=/[ \n\.,;!\?\-]/,T.fontPaths={},T.iMatrix=[1,0,0,1,0,0],T.svgNS="http://www.w3.org/2000/svg",T.perfLimitSizeTotal=2097152,T.maxCacheSideLimit=4096,T.minCacheSideLimit=256,T.charWidthsCache={},T.textureSize=2048,T.disableStyleCopyPaste=!1,T.enableGLFiltering=!0,T.devicePixelRatio=T.window.devicePixelRatio||T.window.webkitDevicePixelRatio||T.window.mozDevicePixelRatio||1,T.browserShadowBlurConstant=1,T.arcToSegmentsCache={},T.boundsOfCurveCache={},T.cachesBoundsOfCurve=!0,T.forceGLPutImageData=!1,T.initFilterBackend=function(){return T.enableGLFiltering&&T.isWebglSupported&&T.isWebglSupported(T.textureSize)?(console.log("max texture size: "+T.maxTextureSize),new T.WebglFilterBackend({tileSize:T.textureSize})):T.Canvas2dFilterBackend?new T.Canvas2dFilterBackend:void 0},"undefined"!=typeof document&&"undefined"!=typeof window&&(window.fabric=T),void 0===P)var P={};if(function(t){t.modifyEventListener=!1,t.modifySelectors=!1,t.configure=function(e){isFinite(e.modifyEventListener)&&(t.modifyEventListener=e.modifyEventListener),isFinite(e.modifySelectors)&&(t.modifySelectors=e.modifySelectors),!1===d&&t.modifyEventListener&&g(),!1===p&&t.modifySelectors&&v()},t.add=function(t,e,r,n){return i(t,e,r,n,"add")},t.remove=function(t,e,r,n){return i(t,e,r,n,"remove")},t.returnFalse=function(t){return!1},t.stop=function(t){t&&(t.stopPropagation&&t.stopPropagation(),t.cancelBubble=!0,t.cancelBubbleCount=0)},t.prevent=function(t){t&&(t.preventDefault?t.preventDefault():t.preventManipulation?t.preventManipulation():t.returnValue=!1)},t.cancel=function(e){t.stop(e),t.prevent(e)},t.blur=function(){var t=document.activeElement;if(t){var e=document.activeElement.nodeName;"INPUT"!==e&&"TEXTAREA"!==e&&"true"!==t.contentEditable||t.blur&&t.blur()}},t.getEventSupport=function(t,e){if("string"==typeof t&&(e=t,t=window),(e="on"+e)in t)return!0;if(t.setAttribute||(t=document.createElement("div")),t.setAttribute&&t.removeAttribute){t.setAttribute(e,"");var i="function"==typeof t[e];return void 0!==t[e]&&(t[e]=null),t.removeAttribute(e),i}};var e=function(t){if(!t||"object"!=typeof t)return t;var i=new t.constructor;for(var r in t)t[r]&&"object"==typeof t[r]?i[r]=e(t[r]):i[r]=t[r];return i},i=function(o,s,c,d,g,p){if(d=d||{},"[object Object]"===String(o)){var v=o;if(o=v.target,delete v.target,!v.type||!v.listener){for(var m in v){var y=v[m];"function"!=typeof y&&(d[m]=y)}var _={};for(var b in v){m=b.split(",");var x=v[b],C={};for(var w in d)C[w]=d[w];if("function"==typeof x)c=x;else{if("function"!=typeof x.listener)continue;c=x.listener;for(var w in x)"function"!=typeof x[w]&&(C[w]=x[w])}for(var S=0;SO&&window.clearInterval(k),document.querySelector(o)&&(window.clearInterval(k),setTimeout(c,1))}),E);return}s="load",o=window}if("string"==typeof o){if(0===(o=document.querySelectorAll(o)).length)return n("Missing target on listener!",arguments);1===o.length&&(o=o[0])}var A,D={};if(o.length>0&&o!==window){for(var M=0,j=o.length;M=r.maxFingers){var d=[];for(var f in r.tracker)d.push(f);return i.identifier=d.join(","),s}for(var g in a)if(a[g].up){delete a[g],o(u,f),r.cancel=!0;break}if(a[f])continue;o(u,f)}else a=r.tracker={},i.bbox=r.bbox=t.getBoundingBox(r.target),r.fingers=0,r.cancel=!1,o(u,f)}d=[];for(var f in r.tracker)d.push(f);return i.identifier=d.join(","),s},t.pointerEnd=function(t,e,i,r){for(var n=t.touches||[],o=n.length,s={},a=0;ao.x1&&a>o.y1&&h0&&l0&&u0?x:-x,void 0!==v.DEG2&&(x>0?v.rotation+=v.DEG1-v.DEG2:v.rotation-=v.DEG1-v.DEG2,g+=v.rotation),f.push(v.move)}}n.touches=f,n.fingers=r.fingers,n.scale=d/r.fingers,n.rotation=g/r.fingers,n.state="change",r.listener(o,n)}},r.onPointerUp=function(e){var i=r.fingers;t.pointerEnd(e,n,r)&&(P.remove(r.doc,"mousemove",r.onPointerMove),P.remove(r.doc,"mouseup",r.onPointerUp)),i===r.minFingers&&r.fingers4){var g=h*u/d,p=Math.abs(g+f.value);f.value&&p<200?(f.value=g,f.count++,3===f.count&&(t.listener(o,e),i=h,f.value=0,f.count=0)):(f.value=g,f.count=1)}}else t.listener(o,e)};if(window.addEventListener)return window.addEventListener("devicemotion",o,!1),e},P.Gesture=P.Gesture||{},P.Gesture._gestureHandlers=P.Gesture._gestureHandlers||{},P.Gesture._gestureHandlers.shake=t.shake,t}(P.proxy),void 0===P)P={};if(void 0===P.proxy&&(P.proxy={}),P.proxy=function(t){var e=Math.PI/180;return t.swipe=function(i){i.snap=i.snap||90,i.threshold=i.threshold||1,i.gesture=i.gesture||"swipe",i.onPointerDown=function(e){t.pointerStart(e,r,i)&&(P.add(i.doc,"mousemove",i.onPointerMove).listener(e),P.add(i.doc,"mouseup",i.onPointerUp))},i.onPointerMove=function(e){for(var r=e.changedTouches||t.getCoords(e),n=r.length,o=0;o=_&&o>i.threshold&&(a.x/=l,a.y/=l,r.start=a,r.x=h/l,r.y=c/l,r.angle=-(((s/i.snap+.5>>0)*i.snap||360)-360),r.velocity=o,r.fingers=_,r.state="swipe",i.listener(n,r))}};var r=t.pointerSetup(i);return P.add(i.target,"mousedown",i.onPointerDown),r},P.Gesture=P.Gesture||{},P.Gesture._gestureHandlers=P.Gesture._gestureHandlers||{},P.Gesture._gestureHandlers.swipe=t.swipe,t}(P.proxy),void 0===P)P={};if(void 0===P.proxy&&(P.proxy={}),P.proxy=function(t){return t.longpress=function(e){return e.gesture="longpress",t.tap(e)},t.tap=function(e){var i,r;e.delay=e.delay||500,e.timeout=e.timeout||250,e.driftDeviance=e.driftDeviance||10,e.gesture=e.gesture||"tap",e.onPointerDown=function(o){if(t.pointerStart(o,n,e)){if(i=(new Date).getTime(),P.add(e.doc,"mousemove",e.onPointerMove).listener(o),P.add(e.doc,"mouseup",e.onPointerUp),"longpress"!==e.gesture)return;r=setTimeout((function(){if(!(o.cancelBubble&&++o.cancelBubbleCount>1)){var t=0;for(var i in e.tracker){var r=e.tracker[i];if(!0===r.end)return;if(e.cancel)return;t++}e.minFingers<=t&&e.maxFingers>=t&&(n.state="start",n.fingers=t,n.x=r.start.x,n.y=r.start.y,e.listener(o,n))}}),e.delay)}},e.onPointerMove=function(i){for(var r=e.bbox,n=i.changedTouches||t.getCoords(i),o=n.length,s=0;s0&&l0&&u1)return;if("longpress"===e.gesture)return void("start"===n.state&&(n.state="end",e.listener(o,n)));if(e.cancel)return;if((new Date).getTime()-i>e.timeout)return;var s=e.gestureFingers;e.minFingers<=s&&e.maxFingers>=s&&(n.state="tap",n.fingers=e.gestureFingers,e.listener(o,n))}};var n=t.pointerSetup(e);return P.add(e.target,"mousedown",e.onPointerDown),n},P.Gesture=P.Gesture||{},P.Gesture._gestureHandlers=P.Gesture._gestureHandlers||{},P.Gesture._gestureHandlers.tap=t.tap,P.Gesture._gestureHandlers.longpress=t.longpress,t}(P.proxy),void 0===P)P={};if(void 0===P.proxy&&(P.proxy={}),P.proxy=function(t){return t.wheelPreventElasticBounce=function(t){t&&("string"==typeof t&&(t=document.querySelector(t)),P.add(t,"wheel",(function(t,e){e.preventElasticBounce(),P.stop(t)})))},t.wheel=function(t){var e,i=t.timeout||150,r=0,n={gesture:"wheel",state:"start",wheelDelta:0,target:t.target,listener:t.listener,preventElasticBounce:function(t){var e=this.target,i=e.scrollTop;(i+e.offsetHeight===e.scrollHeight&&this.wheelDelta<=0||0===i&&this.wheelDelta>=0)&&P.cancel(t),P.stop(t)},add:function(){t.target[s](h,o,!1)},remove:function(){t.target[a](h,o,!1)}},o=function(o){o=o||window.event,n.state=r++?"change":"start",n.wheelDelta=o.detail?-20*o.detail:o.wheelDelta,t.listener(o,n),clearTimeout(e),e=setTimeout((function(){r=0,n.state="end",n.wheelDelta=0,t.listener(o,n)}),i)},s=document.addEventListener?"addEventListener":"attachEvent",a=document.removeEventListener?"removeEventListener":"detachEvent",h=P.getEventSupport("mousewheel")?"mousewheel":"DOMMouseScroll";return t.target[s](h,o,!1),n},P.Gesture=P.Gesture||{},P.Gesture._gestureHandlers=P.Gesture._gestureHandlers||{},P.Gesture._gestureHandlers.wheel=t.wheel,t}(P.proxy),void 0===E)var E={};function k(t,e){var i=t.canvas,r=e.targetCanvas,n=r.getContext("2d");n.translate(0,r.height),n.scale(1,-1);var o=i.height-r.height;n.drawImage(i,0,o,r.width,r.height,0,0,r.width,r.height)}function A(t,e){var i=e.targetCanvas.getContext("2d"),r=e.destinationWidth,n=e.destinationHeight,o=r*n*4,s=new Uint8Array(this.imageBuffer,0,o),a=new Uint8ClampedArray(this.imageBuffer,0,o);t.readPixels(0,0,r,n,t.RGBA,t.UNSIGNED_BYTE,s);var h=new ImageData(a,r,n);i.putImageData(h,0,0)}void 0===E.proxy&&(E.proxy={}),E.proxy=function(t){return t.orientation=function(t){var e={gesture:"orientationchange",previous:null,current:window.orientation,target:t.target,listener:t.listener,remove:function(){window.removeEventListener("orientationchange",i,!1)}},i=function(i){e.previous=e.current,e.current=window.orientation,null===e.previous||e.previous==e.current||t.listener(i,e)};return window.DeviceOrientationEvent&&window.addEventListener("orientationchange",i,!1),e},E.Gesture=E.Gesture||{},E.Gesture._gestureHandlers=E.Gesture._gestureHandlers||{},E.Gesture._gestureHandlers.orientation=t.orientation,t}(E.proxy),function(){function t(t,e){if(this.__eventListeners[t]){var i=this.__eventListeners[t];e?i[i.indexOf(e)]=!1:T.util.array.fill(i,!1)}}function e(t,e){var i=function(){e.apply(this,arguments),this.off(t,i)}.bind(this);this.on(t,i)}T.Observable={fire:function(t,e){if(!this.__eventListeners)return this;var i=this.__eventListeners[t];if(!i)return this;for(var r=0,n=i.length;r-1||!!e&&this._objects.some((function(e){return"function"==typeof e.contains&&e.contains(t,!0)}))},complexity:function(){return this._objects.reduce((function(t,e){return t+=e.complexity?e.complexity():0}),0)}},T.CommonMethods={_setOptions:function(t){for(var e in t)this.set(e,t[e])},_initGradient:function(t,e){!t||!t.colorStops||t instanceof T.Gradient||this.set(e,new T.Gradient(t))},_initPattern:function(t,e,i){!t||!t.source||t instanceof T.Pattern?i&&i():this.set(e,new T.Pattern(t,i))},_setObject:function(t){for(var e in t)this._set(e,t[e])},set:function(t,e){return"object"==typeof t?this._setObject(t):this._set(t,e),this},_set:function(t,e){this[t]=e},toggle:function(t){var e=this.get(t);return"boolean"==typeof e&&this.set(t,!e),this},get:function(t){return this[t]}},n=t,o=Math.sqrt,s=Math.atan2,a=Math.pow,h=Math.PI/180,c=Math.PI/2,T.util={cos:function(t){if(0===t)return 1;switch(t<0&&(t=-t),t/c){case 1:case 3:return 0;case 2:return-1}return Math.cos(t)},sin:function(t){if(0===t)return 0;var e=1;switch(t<0&&(e=-1),t/c){case 1:return e;case 2:return 0;case 3:return-e}return Math.sin(t)},removeFromArray:function(t,e){var i=t.indexOf(e);return-1!==i&&t.splice(i,1),t},getRandomInt:function(t,e){return Math.floor(Math.random()*(e-t+1))+t},degreesToRadians:function(t){return t*h},radiansToDegrees:function(t){return t/h},rotatePoint:function(t,e,i){var r=new T.Point(t.x-e.x,t.y-e.y),n=T.util.rotateVector(r,i);return new T.Point(n.x,n.y).addEquals(e)},rotateVector:function(t,e){var i=T.util.sin(e),r=T.util.cos(e);return{x:t.x*r-t.y*i,y:t.x*i+t.y*r}},transformPoint:function(t,e,i){return i?new T.Point(e[0]*t.x+e[2]*t.y,e[1]*t.x+e[3]*t.y):new T.Point(e[0]*t.x+e[2]*t.y+e[4],e[1]*t.x+e[3]*t.y+e[5])},makeBoundingBoxFromPoints:function(t,e){if(e)for(var i=0;ie;)(e+=a[d++%f])>l&&(e=l),t[g?"lineTo":"moveTo"](e,0),g=!g;t.restore()},createCanvasElement:function(){return T.document.createElement("canvas")},copyCanvasElement:function(t){var e=T.util.createCanvasElement();return e.width=t.width,e.height=t.height,e.getContext("2d").drawImage(t,0,0),e},toDataURL:function(t,e,i){return t.toDataURL("image/"+e,i)},createImage:function(){return T.document.createElement("img")},multiplyTransformMatrices:function(t,e,i){return[t[0]*e[0]+t[2]*e[1],t[1]*e[0]+t[3]*e[1],t[0]*e[2]+t[2]*e[3],t[1]*e[2]+t[3]*e[3],i?0:t[0]*e[4]+t[2]*e[5]+t[4],i?0:t[1]*e[4]+t[3]*e[5]+t[5]]},qrDecompose:function(t){var e=s(t[1],t[0]),i=a(t[0],2)+a(t[1],2),r=o(i),n=(t[0]*t[3]-t[2]*t[1])/r,c=s(t[0]*t[2]+t[1]*t[3],i);return{angle:e/h,scaleX:r,scaleY:n,skewX:c/h,skewY:0,translateX:t[4],translateY:t[5]}},calcRotateMatrix:function(t){if(!t.angle)return T.iMatrix.concat();var e=T.util.degreesToRadians(t.angle),i=T.util.cos(e),r=T.util.sin(e);return[i,r,-r,i,0,0]},calcDimensionsMatrix:function(t){var e=void 0===t.scaleX?1:t.scaleX,i=void 0===t.scaleY?1:t.scaleY,r=[t.flipX?-e:e,0,0,t.flipY?-i:i,0,0],n=T.util.multiplyTransformMatrices,o=T.util.degreesToRadians;return t.skewX&&(r=n(r,[1,0,Math.tan(o(t.skewX)),1],!0)),t.skewY&&(r=n(r,[1,Math.tan(o(t.skewY)),0,1],!0)),r},composeMatrix:function(t){var e=[1,0,0,1,t.translateX||0,t.translateY||0],i=T.util.multiplyTransformMatrices;return t.angle&&(e=i(e,T.util.calcRotateMatrix(t))),(1!==t.scaleX||1!==t.scaleY||t.skewX||t.skewY||t.flipX||t.flipY)&&(e=i(e,T.util.calcDimensionsMatrix(t))),e},resetObjectTransform:function(t){t.scaleX=1,t.scaleY=1,t.skewX=0,t.skewY=0,t.flipX=!1,t.flipY=!1,t.rotate(0)},saveObjectTransform:function(t){return{scaleX:t.scaleX,scaleY:t.scaleY,skewX:t.skewX,skewY:t.skewY,angle:t.angle,left:t.left,flipX:t.flipX,flipY:t.flipY,top:t.top}},isTransparent:function(t,e,i,r){r>0&&(e>r?e-=r:e=0,i>r?i-=r:i=0);var n,o=!0,s=t.getImageData(e,i,2*r||1,2*r||1),a=s.data.length;for(n=3;n0?A-=2*c:1===a&&A<0&&(A+=2*c);for(var D=Math.ceil(Math.abs(A/c*2)),M=[],j=A/D,F=8/3*Math.sin(j/4)*Math.sin(j/4)/Math.sin(j/2),L=k+j,I=0;I=n?o-n:2*Math.PI-(n-o)}function s(e,i,r,n,o,s,a,h){var c;if(T.cachesBoundsOfCurve&&(c=t.call(arguments),T.boundsOfCurveCache[c]))return T.boundsOfCurveCache[c];var l,u,f,d,g,p,v,m,y=Math.sqrt,_=Math.min,b=Math.max,x=Math.abs,C=[],w=[[],[]];u=6*e-12*r+6*o,l=-3*e+9*r-9*o+3*a,f=3*r-3*e;for(var S=0;S<2;++S)if(S>0&&(u=6*i-12*n+6*s,l=-3*i+9*n-9*s+3*h,f=3*n-3*i),x(l)<1e-12){if(x(u)<1e-12)continue;0<(d=-f/u)&&d<1&&C.push(d)}else(v=u*u-4*f*l)<0||(0<(g=(-u+(m=y(v)))/(2*l))&&g<1&&C.push(g),0<(p=(-u-m)/(2*l))&&p<1&&C.push(p));for(var O,P,E,k=C.length,A=k;k--;)O=(E=1-(d=C[k]))*E*E*e+3*E*E*d*r+3*E*d*d*o+d*d*d*a,w[0][k]=O,P=E*E*E*i+3*E*E*d*n+3*E*d*d*s+d*d*d*h,w[1][k]=P;w[0][A]=e,w[1][A]=i,w[0][A+1]=a,w[1][A+1]=h;var D=[{x:_.apply(null,w[0]),y:_.apply(null,w[1])},{x:b.apply(null,w[0]),y:b.apply(null,w[1])}];return T.cachesBoundsOfCurve&&(T.boundsOfCurveCache[c]=D),D}function a(t,e,i){for(var r=i[1],o=i[2],s=i[3],a=i[4],h=i[5],c=n(i[6]-t,i[7]-e,r,o,a,h,s),l=0,u=c.length;l1e-4;)i=a(o),n=o,(r=h(c.x,c.y,i.x,i.y))+s>e?o-=l/=2:(c=i,o+=l,s+=r);return i.angle=u(n),i}function p(t){for(var e,i,r,n,o=0,s=t.length,a=0,g=0,p=0,v=0,m=[],y=0;yC)for(var S=1,O=p.length;S2;for(e=e||0,c&&(a=t[2].xt[i-2].x?1:n.x===t[i-2].x?0:-1,h=n.y>t[i-2].y?1:n.y===t[i-2].y?0:-1),r.push(["L",n.x+a*e,n.y+h*e]),r},T.util.getPathSegmentsInfo=p,T.util.getBoundsOfCurve=s,T.util.getPointOnPath=function(t,e,i){i||(i=p(t));for(var r=0;e-i[r].length>0&&r=e}))}}}(),function(){function t(e,i,r){if(r)if(!T.isLikelyNode&&i instanceof Element)e=i;else if(i instanceof Array){e=[];for(var n=0,o=i.length;n57343)return t.charAt(e);if(55296<=i&&i<=56319){if(t.length<=e+1)throw"High surrogate without following low surrogate";var r=t.charCodeAt(e+1);if(56320>r||r>57343)throw"High surrogate without following low surrogate";return t.charAt(e)+t.charAt(e+1)}if(0===e)throw"Low surrogate without preceding high surrogate";var n=t.charCodeAt(e-1);if(55296>n||n>56319)throw"Low surrogate without preceding high surrogate";return!1}T.util.string={camelize:function(t){return t.replace(/-+(.)?/g,(function(t,e){return e?e.toUpperCase():""}))},capitalize:function(t,e){return t.charAt(0).toUpperCase()+(e?t.slice(1):t.slice(1).toLowerCase())},escapeXml:function(t){return t.replace(/&/g,"&").replace(/"/g,""").replace(/'/g,"'").replace(//g,">")},graphemeSplit:function(e){var i,r=0,n=[];for(r=0;r-1?t.prototype[n]=function(t){return function(){var i=this.constructor.superclass;this.constructor.superclass=r;var n=e[t].apply(this,arguments);if(this.constructor.superclass=i,"initialize"!==t)return n}}(n):t.prototype[n]=e[n],i&&(e.toString!==Object.prototype.toString&&(t.prototype.toString=e.toString),e.valueOf!==Object.prototype.valueOf&&(t.prototype.valueOf=e.valueOf))};function n(){}function o(e){for(var i=null,r=this;r.constructor.superclass;){var n=r.constructor.superclass.prototype[e];if(r[e]!==n){i=n;break}r=r.constructor.superclass.prototype}return i?arguments.length>1?i.apply(this,t.call(arguments,1)):i.call(this):console.log("tried to callSuper "+e+", method not found in prototype chain",this)}T.util.createClass=function(){var i=null,s=t.call(arguments,0);function a(){this.initialize.apply(this,arguments)}"function"==typeof s[0]&&(i=s.shift()),a.superclass=i,a.subclasses=[],i&&(n.prototype=i.prototype,a.prototype=new n,i.subclasses.push(a));for(var h=0,c=s.length;h-1||"touch"===t.pointerType},f=T.document.createElement("div"),d="string"==typeof f.style.opacity,g="string"==typeof f.style.filter,v=/alpha\s*\(\s*opacity\s*=\s*([^\)]+)\)/,m=function(t){return t},d?m=function(t,e){return t.style.opacity=e,t}:g&&(m=function(t,e){var i=t.style;return t.currentStyle&&!t.currentStyle.hasLayout&&(i.zoom=1),v.test(i.filter)?(e=e>=.9999?"":"alpha(opacity="+100*e+")",i.filter=i.filter.replace(v,e)):i.filter+=" alpha(opacity="+100*e+")",t}),T.util.setStyle=function(t,e){var i=t.style;if(!i)return t;if("string"==typeof e)return t.style.cssText+=";"+e,e.indexOf("opacity")>-1?m(t,e.match(/opacity:\s*(\d?\.?\d*)/)[1]):t;for(var r in e)"opacity"===r?m(t,e[r]):i["float"===r||"cssFloat"===r?void 0===i.styleFloat?"cssFloat":"styleFloat":r]=e[r];return t},function(){var t=Array.prototype.slice;var e,i,r,n,o=function(e){return t.call(e,0)};try{e=o(T.document.childNodes)instanceof Array}catch(t){}function s(t,e){var i=T.document.createElement(t);for(var r in e)"class"===r?i.className=e[r]:"for"===r?i.htmlFor=e[r]:i.setAttribute(r,e[r]);return i}function a(t){for(var e=0,i=0,r=T.document.documentElement,n=T.document.body||{scrollLeft:0,scrollTop:0};t&&(t.parentNode||t.host)&&((t=t.parentNode||t.host)===T.document?(e=n.scrollLeft||r.scrollLeft||0,i=n.scrollTop||r.scrollTop||0):(e+=t.scrollLeft||0,i+=t.scrollTop||0),1!==t.nodeType||"fixed"!==t.style.position););return{left:e,top:i}}e||(o=function(t){for(var e=new Array(t.length),i=t.length;i--;)e[i]=t[i];return e}),i=T.document.defaultView&&T.document.defaultView.getComputedStyle?function(t,e){var i=T.document.defaultView.getComputedStyle(t,null);return i?i[e]:void 0}:function(t,e){var i=t.style[e];return!i&&t.currentStyle&&(i=t.currentStyle[e]),i},r=T.document.documentElement.style,n="userSelect"in r?"userSelect":"MozUserSelect"in r?"MozUserSelect":"WebkitUserSelect"in r?"WebkitUserSelect":"KhtmlUserSelect"in r?"KhtmlUserSelect":"",T.util.makeElementUnselectable=function(t){return void 0!==t.onselectstart&&(t.onselectstart=T.util.falseFunction),n?t.style[n]="none":"string"==typeof t.unselectable&&(t.unselectable="on"),t},T.util.makeElementSelectable=function(t){return void 0!==t.onselectstart&&(t.onselectstart=null),n?t.style[n]="":"string"==typeof t.unselectable&&(t.unselectable=""),t},T.util.setImageSmoothing=function(t,e){t.imageSmoothingEnabled=t.imageSmoothingEnabled||t.webkitImageSmoothingEnabled||t.mozImageSmoothingEnabled||t.msImageSmoothingEnabled||t.oImageSmoothingEnabled,t.imageSmoothingEnabled=e},T.util.getById=function(t){return"string"==typeof t?T.document.getElementById(t):t},T.util.toArray=o,T.util.addClass=function(t,e){t&&-1===(" "+t.className+" ").indexOf(" "+e+" ")&&(t.className+=(t.className?" ":"")+e)},T.util.makeElement=s,T.util.wrapElement=function(t,e,i){return"string"==typeof e&&(e=s(e,i)),t.parentNode&&t.parentNode.replaceChild(e,t),e.appendChild(t),e},T.util.getScrollLeftTop=a,T.util.getElementOffset=function(t){var e,r,n=t&&t.ownerDocument,o={left:0,top:0},s={left:0,top:0},h={borderLeftWidth:"left",borderTopWidth:"top",paddingLeft:"left",paddingTop:"top"};if(!n)return s;for(var c in h)s[h[c]]+=parseInt(i(t,c),10)||0;return e=n.documentElement,void 0!==t.getBoundingClientRect&&(o=t.getBoundingClientRect()),r=a(t),{left:o.left+r.left-(e.clientLeft||0)+s.left,top:o.top+r.top-(e.clientTop||0)+s.top}},T.util.getNodeCanvas=function(t){var e=T.jsdomImplForWrapper(t);return e._canvas||e._image},T.util.cleanUpJsdomNode=function(t){if(T.isLikelyNode){var e=T.jsdomImplForWrapper(t);e&&(e._image=null,e._canvas=null,e._currentSrc=null,e._attributes=null,e._classList=null)}}}(),function(){function t(){}T.util.request=function(e,i){i||(i={});var r=i.method?i.method.toUpperCase():"GET",n=i.onComplete||function(){},o=new T.window.XMLHttpRequest,s=i.body||i.parameters;return o.onreadystatechange=function(){4===o.readyState&&(n(o),o.onreadystatechange=t)},"GET"===r&&(s=null,"string"==typeof i.parameters&&(e=function(t,e){return t+(/\?/.test(t)?"&":"?")+e}(e,i.parameters))),o.open(r,e,!0),"POST"!==r&&"PUT"!==r||o.setRequestHeader("Content-Type","application/x-www-form-urlencoded"),o.send(s),o}}(),T.log=console.log,T.warn=console.warn,function(){function t(){return!1}function e(t,e,i,r){return-i*Math.cos(t/r*(Math.PI/2))+i+e}var i=T.window.requestAnimationFrame||T.window.webkitRequestAnimationFrame||T.window.mozRequestAnimationFrame||T.window.oRequestAnimationFrame||T.window.msRequestAnimationFrame||function(t){return T.window.setTimeout(t,1e3/60)},r=T.window.cancelAnimationFrame||T.window.clearTimeout;function n(){return i.apply(T.window,arguments)}T.util.animate=function(i){var r=!1;return n((function(o){i||(i={});var s,a=o||+new Date,h=i.duration||500,c=a+h,l=i.onChange||t,u=i.abort||t,f=i.onComplete||t,d=i.easing||e,g="startValue"in i?i.startValue:0,p="endValue"in i?i.endValue:100,v=i.byValue||p-g;i.onStart&&i.onStart(),function t(e){var i=(s=e||+new Date)>c?h:s-a,o=i/h,m=d(i,g,v,h),y=Math.abs((m-g)/v);if(!r){if(!u(m,y,o))return s>c?(l(p,1,1),void f(p,1,1)):(l(m,y,o),void n(t));f(p,1,1)}}(a)})),function(){r=!0}},T.util.requestAnimFrame=n,T.util.cancelAnimFrame=function(){return r.apply(T.window,arguments)}}(),function(){function t(t,e,i){var r="rgba("+parseInt(t[0]+i*(e[0]-t[0]),10)+","+parseInt(t[1]+i*(e[1]-t[1]),10)+","+parseInt(t[2]+i*(e[2]-t[2]),10);return r+=","+(t&&e?parseFloat(t[3]+i*(e[3]-t[3])):1),r+=")"}T.util.animateColor=function(e,i,r,n){var o=new T.Color(e).getSource(),s=new T.Color(i).getSource(),a=n.onComplete,h=n.onChange;return n=n||{},T.util.animate(T.util.object.extend(n,{duration:r||500,startValue:o,endValue:s,byValue:s,easing:function(e,i,r,o){return t(i,r,n.colorEasing?n.colorEasing(e,o):1-Math.cos(e/o*(Math.PI/2)))},onComplete:function(e,i,r){if(a)return a(t(s,s,0),i,r)},onChange:function(e,i,r){if(h){if(Array.isArray(e))return h(t(e,e,0),i,r);h(e,i,r)}}}))}}(),function(){function t(t,e,i,r){return t-1&&l>-1&&l-1)&&(i="stroke")}else{if("href"===t||"xlink:href"===t||"font"===t)return i;if("imageSmoothing"===t)return"optimizeQuality"===i;a=h?i.map(o):o(i,n)}}else i="";return!h&&isNaN(a)?i:a}function d(t){return new RegExp("^("+t.join("|")+")\\b","i")}function g(t,e){var i,r,n,o,s=[];for(n=0,o=e.length;n1;)h.shift(),c=e.util.multiplyTransformMatrices(c,h[0]);return c}}();var y=new RegExp("^\\s*("+e.reNum+"+)\\s*,?\\s*("+e.reNum+"+)\\s*,?\\s*("+e.reNum+"+)\\s*,?\\s*("+e.reNum+"+)\\s*$");function _(t){if(!e.svgViewBoxElementsRegEx.test(t.nodeName))return{};var i,r,n,s,a,h,c=t.getAttribute("viewBox"),l=1,u=1,f=t.getAttribute("width"),d=t.getAttribute("height"),g=t.getAttribute("x")||0,p=t.getAttribute("y")||0,v=t.getAttribute("preserveAspectRatio")||"",m=!c||!(c=c.match(y)),_=!f||!d||"100%"===f||"100%"===d,b=m&&_,x={},C="",w=0,S=0;if(x.width=0,x.height=0,x.toBeParsed=b,m&&(g||p)&&t.parentNode&&"#document"!==t.parentNode.nodeName&&(C=" translate("+o(g)+" "+o(p)+") ",a=(t.getAttribute("transform")||"")+C,t.setAttribute("transform",a),t.removeAttribute("x"),t.removeAttribute("y")),b)return x;if(m)return x.width=o(f),x.height=o(d),x;if(i=-parseFloat(c[1]),r=-parseFloat(c[2]),n=parseFloat(c[3]),s=parseFloat(c[4]),x.minX=i,x.minY=r,x.viewBoxWidth=n,x.viewBoxHeight=s,_?(x.width=n,x.height=s):(x.width=o(f),x.height=o(d),l=x.width/n,u=x.height/s),"none"!==(v=e.util.parsePreserveAspectRatioAttribute(v)).alignX&&("meet"===v.meetOrSlice&&(u=l=l>u?u:l),"slice"===v.meetOrSlice&&(u=l=l>u?l:u),w=x.width-n*l,S=x.height-s*l,"Mid"===v.alignX&&(w/=2),"Mid"===v.alignY&&(S/=2),"Min"===v.alignX&&(w=0),"Min"===v.alignY&&(S=0)),1===l&&1===u&&0===i&&0===r&&0===g&&0===p)return x;if((g||p)&&"#document"!==t.parentNode.nodeName&&(C=" translate("+o(g)+" "+o(p)+") "),a=C+" matrix("+l+" 0 0 "+u+" "+(i*l+w)+" "+(r*u+S)+") ","svg"===t.nodeName){for(h=t.ownerDocument.createElementNS(e.svgNS,"g");t.firstChild;)h.appendChild(t.firstChild);t.appendChild(h)}else(h=t).removeAttribute("x"),h.removeAttribute("y"),a=h.getAttribute("transform")+a;return h.setAttribute("transform",a),x}function b(t,e){var i="xlink:href",r=m(t,e.getAttribute(i).substr(1));if(r&&r.getAttribute(i)&&b(t,r),["gradientTransform","x1","x2","y1","y2","gradientUnits","cx","cy","r","fx","fy"].forEach((function(t){r&&!e.hasAttribute(t)&&r.hasAttribute(t)&&e.setAttribute(t,r.getAttribute(t))})),!e.children.length)for(var n=r.cloneNode(!0);n.firstChild;)e.appendChild(n.firstChild);e.removeAttribute(i)}e.parseSVGDocument=function(t,i,n,o){if(t){!function(t){for(var i=g(t,["use","svg:use"]),r=0;i.length&&rt.x&&this.y>t.y},gte:function(t){return this.x>=t.x&&this.y>=t.y},lerp:function(t,e){return void 0===e&&(e=.5),e=Math.max(Math.min(1,e),0),new i(this.x+(t.x-this.x)*e,this.y+(t.y-this.y)*e)},distanceFrom:function(t){var e=this.x-t.x,i=this.y-t.y;return Math.sqrt(e*e+i*i)},midPointFrom:function(t){return this.lerp(t)},min:function(t){return new i(Math.min(this.x,t.x),Math.min(this.y,t.y))},max:function(t){return new i(Math.max(this.x,t.x),Math.max(this.y,t.y))},toString:function(){return this.x+","+this.y},setXY:function(t,e){return this.x=t,this.y=e,this},setX:function(t){return this.x=t,this},setY:function(t){return this.y=t,this},setFromPoint:function(t){return this.x=t.x,this.y=t.y,this},swap:function(t){var e=this.x,i=this.y;this.x=t.x,this.y=t.y,t.x=e,t.y=i},clone:function(){return new i(this.x,this.y)}})}(t),function(t){var e=t.fabric||(t.fabric={});function i(t){this.status=t,this.points=[]}e.Intersection?e.warn("fabric.Intersection is already defined"):(e.Intersection=i,e.Intersection.prototype={constructor:i,appendPoint:function(t){return this.points.push(t),this},appendPoints:function(t){return this.points=this.points.concat(t),this}},e.Intersection.intersectLineLine=function(t,r,n,o){var s,a=(o.x-n.x)*(t.y-n.y)-(o.y-n.y)*(t.x-n.x),h=(r.x-t.x)*(t.y-n.y)-(r.y-t.y)*(t.x-n.x),c=(o.y-n.y)*(r.x-t.x)-(o.x-n.x)*(r.y-t.y);if(0!==c){var l=a/c,u=h/c;0<=l&&l<=1&&0<=u&&u<=1?(s=new i("Intersection")).appendPoint(new e.Point(t.x+l*(r.x-t.x),t.y+l*(r.y-t.y))):s=new i}else s=new i(0===a||0===h?"Coincident":"Parallel");return s},e.Intersection.intersectLinePolygon=function(t,e,r){var n,o,s,a,h=new i,c=r.length;for(a=0;a0&&(h.status="Intersection"),h},e.Intersection.intersectPolygonPolygon=function(t,e){var r,n=new i,o=t.length;for(r=0;r0&&(n.status="Intersection"),n},e.Intersection.intersectPolygonRectangle=function(t,r,n){var o=r.min(n),s=r.max(n),a=new e.Point(s.x,o.y),h=new e.Point(o.x,s.y),c=i.intersectLinePolygon(o,a,t),l=i.intersectLinePolygon(a,s,t),u=i.intersectLinePolygon(s,h,t),f=i.intersectLinePolygon(h,o,t),d=new i;return d.appendPoints(c.points),d.appendPoints(l.points),d.appendPoints(u.points),d.appendPoints(f.points),d.points.length>0&&(d.status="Intersection"),d})}(t),function(t){var e=t.fabric||(t.fabric={});function i(t){t?this._tryParsingColor(t):this.setSource([0,0,0,1])}function r(t,e,i){return i<0&&(i+=1),i>1&&(i-=1),i<1/6?t+6*(e-t)*i:i<.5?e:i<2/3?t+(e-t)*(2/3-i)*6:t}e.Color?e.warn("fabric.Color is already defined."):(e.Color=i,e.Color.prototype={_tryParsingColor:function(t){var e;t in i.colorNameMap&&(t=i.colorNameMap[t]),"transparent"===t&&(e=[255,255,255,0]),e||(e=i.sourceFromHex(t)),e||(e=i.sourceFromRgb(t)),e||(e=i.sourceFromHsl(t)),e||(e=[0,0,0,1]),e&&this.setSource(e)},_rgbToHsl:function(t,i,r){t/=255,i/=255,r/=255;var n,o,s,a=e.util.array.max([t,i,r]),h=e.util.array.min([t,i,r]);if(s=(a+h)/2,a===h)n=o=0;else{var c=a-h;switch(o=s>.5?c/(2-a-h):c/(a+h),a){case t:n=(i-r)/c+(i0)-(t<0)||+t};function d(t,e){var i=t.angle+u(Math.atan2(e.y,e.x))+360;return Math.round(i%360/45)}function g(t,i){var r=i.transform.target,n=r.canvas,o=e.util.object.clone(i);o.target=r,n&&n.fire("object:"+t,o),r.fire(t,i)}function p(t,e){var i=e.canvas,r=t[i.uniScaleKey];return i.uniformScaling&&!r||!i.uniformScaling&&r}function v(t){return t.originX===c&&t.originY===c}function m(t,e,i){var r=t.lockScalingX,n=t.lockScalingY;return!(!r||!n)||(!(e||!r&&!n||!i)||(!(!r||"x"!==e)||!(!n||"y"!==e)))}function y(t,e,i,r){return{e:t,transform:e,pointer:{x:i,y:r}}}function _(t){return function(e,i,r,n){var o=i.target,s=o.getCenterPoint(),a=o.translateToOriginPoint(s,i.originX,i.originY),h=t(e,i,r,n);return o.setPositionByOrigin(a,i.originX,i.originY),h}}function b(t,e){return function(i,r,n,o){var s=e(i,r,n,o);return s&&g(t,y(i,r,n,o)),s}}function x(t,i,r,n,o){var s=t.target,a=s.controls[t.corner],h=s.canvas.getZoom(),c=s.padding/h,l=s.toLocalPoint(new e.Point(n,o),i,r);return l.x>=c&&(l.x-=c),l.x<=-c&&(l.x+=c),l.y>=c&&(l.y-=c),l.y<=c&&(l.y+=c),l.x-=a.offsetX,l.y-=a.offsetY,l}function C(t){return t.flipX!==t.flipY}function w(t,e,i,r,n){if(0!==t[e]){var o=n/t._getTransformedDimensions()[r]*t[i];t.set(i,o)}}function S(t,e,i,r){var n,c=e.target,l=c._getTransformedDimensions(0,c.skewY),f=x(e,e.originX,e.originY,i,r),d=Math.abs(2*f.x)-l.x,g=c.skewX;d<2?n=0:(n=u(Math.atan2(d/c.scaleX,l.y/c.scaleY)),e.originX===o&&e.originY===h&&(n=-n),e.originX===a&&e.originY===s&&(n=-n),C(c)&&(n=-n));var p=g!==n;if(p){var v=c._getTransformedDimensions().y;c.set("skewX",n),w(c,"skewY","scaleY","y",v)}return p}function T(t,e,i,r){var n,c=e.target,l=c._getTransformedDimensions(c.skewX,0),f=x(e,e.originX,e.originY,i,r),d=Math.abs(2*f.y)-l.y,g=c.skewY;d<2?n=0:(n=u(Math.atan2(d/c.scaleY,l.x/c.scaleX)),e.originX===o&&e.originY===h&&(n=-n),e.originX===a&&e.originY===s&&(n=-n),C(c)&&(n=-n));var p=g!==n;if(p){var v=c._getTransformedDimensions().x;c.set("skewY",n),w(c,"skewX","scaleX","x",v)}return p}function O(t,e,i,r,n){n=n||{};var o,s,a,h,c,u,d=e.target,g=d.lockScalingX,y=d.lockScalingY,_=n.by,b=p(t,d),C=m(d,_,b),w=e.gestureScale;if(C)return!1;if(w)s=e.scaleX*w,a=e.scaleY*w;else{if(o=x(e,e.originX,e.originY,i,r),c="y"!==_?f(o.x):1,u="x"!==_?f(o.y):1,e.signX||(e.signX=c),e.signY||(e.signY=u),d.lockScalingFlip&&(e.signX!==c||e.signY!==u))return!1;if(h=d._getTransformedDimensions(),b&&!_){var S=Math.abs(o.x)+Math.abs(o.y),T=e.original,O=S/(Math.abs(h.x*T.scaleX/d.scaleX)+Math.abs(h.y*T.scaleY/d.scaleY));s=T.scaleX*O,a=T.scaleY*O}else s=Math.abs(o.x*d.scaleX/h.x),a=Math.abs(o.y*d.scaleY/h.y);v(e)&&(s*=2,a*=2),e.signX!==c&&"y"!==_&&(e.originX=l[e.originX],s*=-1,e.signX=c),e.signY!==u&&"x"!==_&&(e.originY=l[e.originY],a*=-1,e.signY=u)}var P=d.scaleX,E=d.scaleY;return _?("x"===_&&d.set("scaleX",s),"y"===_&&d.set("scaleY",a)):(!g&&d.set("scaleX",s),!y&&d.set("scaleY",a)),P!==d.scaleX||E!==d.scaleY}n.scaleCursorStyleHandler=function(t,e,r){var n=p(t,r),o="";if(0!==e.x&&0===e.y?o="x":0===e.x&&0!==e.y&&(o="y"),m(r,o,n))return"not-allowed";var s=d(r,e);return i[s]+"-resize"},n.skewCursorStyleHandler=function(t,e,i){var n="not-allowed";if(0!==e.x&&i.lockSkewingY)return n;if(0!==e.y&&i.lockSkewingX)return n;var o=d(i,e)%4;return r[o]+"-resize"},n.scaleSkewCursorStyleHandler=function(t,e,i){return t[i.canvas.altActionKey]?n.skewCursorStyleHandler(t,e,i):n.scaleCursorStyleHandler(t,e,i)},n.rotationWithSnapping=b("rotating",_((function(t,e,i,r){var n=e,o=n.target,s=o.translateToOriginPoint(o.getCenterPoint(),n.originX,n.originY);if(o.lockRotation)return!1;var a,h=Math.atan2(n.ey-s.y,n.ex-s.x),c=Math.atan2(r-s.y,i-s.x),l=u(c-h+n.theta);if(o.snapAngle>0){var f=o.snapAngle,d=o.snapThreshold||f,g=Math.ceil(l/f)*f,p=Math.floor(l/f)*f;Math.abs(l-p)0?o:a:(l>0&&(n=u===s?o:a),l<0&&(n=u===s?a:o),C(h)&&(n=n===o?a:o)),e.originX=n,b("skewing",_(S))(t,e,i,r))},n.skewHandlerY=function(t,e,i,r){var n,a=e.target,l=a.skewY,u=e.originX;return!a.lockSkewingY&&(0===l?n=x(e,c,c,i,r).y>0?s:h:(l>0&&(n=u===o?s:h),l<0&&(n=u===o?h:s),C(a)&&(n=n===s?h:s)),e.originY=n,b("skewing",_(T))(t,e,i,r))},n.dragHandler=function(t,e,i,r){var n=e.target,o=i-e.offsetX,s=r-e.offsetY,a=!n.get("lockMovementX")&&n.left!==o,h=!n.get("lockMovementY")&&n.top!==s;return a&&n.set("left",o),h&&n.set("top",s),(a||h)&&g("moving",y(t,e,i,r)),a||h},n.scaleOrSkewActionName=function(t,e,i){var r=t[i.canvas.altActionKey];return 0===e.x?r?"skewX":"scaleY":0===e.y?r?"skewY":"scaleX":void 0},n.rotationStyleHandler=function(t,e,i){return i.lockRotation?"not-allowed":e.cursorStyle},n.fireEvent=g,n.wrapWithFixedAnchor=_,n.wrapWithFireEvent=b,n.getLocalPoint=x,e.controlsUtils=n}(t),function(t){var e=t.fabric||(t.fabric={}),i=e.util.degreesToRadians,r=e.controlsUtils;r.renderCircleControl=function(t,e,i,r,n){r=r||{};var o,s=this.sizeX||r.cornerSize||n.cornerSize,a=this.sizeY||r.cornerSize||n.cornerSize,h=void 0!==r.transparentCorners?r.transparentCorners:n.transparentCorners,c=h?"stroke":"fill",l=!h&&(r.cornerStrokeColor||n.cornerStrokeColor),u=e,f=i;t.save(),t.fillStyle=r.cornerColor||n.cornerColor,t.strokeStyle=r.cornerStrokeColor||n.cornerStrokeColor,s>a?(o=s,t.scale(1,a/s),f=i*s/a):a>s?(o=a,t.scale(s/a,1),u=e*a/s):o=s,t.lineWidth=1,t.beginPath(),t.arc(u,f,o/2,0,2*Math.PI,!1),t[c](),l&&t.stroke(),t.restore()},r.renderSquareControl=function(t,e,r,n,o){n=n||{};var s=this.sizeX||n.cornerSize||o.cornerSize,a=this.sizeY||n.cornerSize||o.cornerSize,h=void 0!==n.transparentCorners?n.transparentCorners:o.transparentCorners,c=h?"stroke":"fill",l=!h&&(n.cornerStrokeColor||o.cornerStrokeColor),u=s/2,f=a/2;t.save(),t.fillStyle=n.cornerColor||o.cornerColor,t.strokeStyle=n.cornerStrokeColor||o.cornerStrokeColor,t.lineWidth=1,t.translate(e,r),t.rotate(i(o.angle)),t[c+"Rect"](-u,-f,s,a),l&&t.strokeRect(-u,-f,s,a),t.restore()}}(t),function(t){var e=t.fabric||(t.fabric={});e.Control=function(t){for(var e in t)this[e]=t[e]},e.Control.prototype={visible:!0,actionName:"scale",angle:0,x:0,y:0,offsetX:0,offsetY:0,sizeX:null,sizeY:null,touchSizeX:null,touchSizeY:null,cursorStyle:"crosshair",withConnection:!1,actionHandler:function(){},mouseDownHandler:function(){},mouseUpHandler:function(){},getActionHandler:function(){return this.actionHandler},getMouseDownHandler:function(){return this.mouseDownHandler},getMouseUpHandler:function(){return this.mouseUpHandler},cursorStyleHandler:function(t,e){return e.cursorStyle},getActionName:function(t,e){return e.actionName},getVisibility:function(t,e){var i=t._controlsVisibility;return i&&void 0!==i[e]?i[e]:this.visible},setVisibility:function(t){this.visible=t},positionHandler:function(t,i){return e.util.transformPoint({x:this.x*t.x+this.offsetX,y:this.y*t.y+this.offsetY},i)},calcCornerCoords:function(t,i,r,n,o){var s,a,h,c,l=o?this.touchSizeX:this.sizeX,u=o?this.touchSizeY:this.sizeY;if(l&&u&&l!==u){var f=Math.atan2(u,l),d=Math.sqrt(l*l+u*u)/2,g=f-e.util.degreesToRadians(t),p=Math.PI/2-f-e.util.degreesToRadians(t);s=d*e.util.cos(g),a=d*e.util.sin(g),h=d*e.util.cos(p),c=d*e.util.sin(p)}else{d=.7071067812*(l&&u?l:i);g=e.util.degreesToRadians(45-t);s=h=d*e.util.cos(g),a=c=d*e.util.sin(g)}return{tl:{x:r-c,y:n-h},tr:{x:r+s,y:n-a},bl:{x:r-s,y:n+a},br:{x:r+c,y:n+h}}},render:function(t,i,r,n,o){if("circle"===((n=n||{}).cornerStyle||o.cornerStyle))e.controlsUtils.renderCircleControl.call(this,t,i,r,n,o);else e.controlsUtils.renderSquareControl.call(this,t,i,r,n,o)}}}(t),function(){function t(t,e){var i,r,n,o,s=t.getAttribute("style"),a=t.getAttribute("offset")||0;if(a=(a=parseFloat(a)/(/%$/.test(a)?100:1))<0?0:a>1?1:a,s){var h=s.split(/\s*;\s*/);for(""===h[h.length-1]&&h.pop(),o=h.length;o--;){var c=h[o].split(/\s*:\s*/),l=c[0].trim(),u=c[1].trim();"stop-color"===l?i=u:"stop-opacity"===l&&(n=u)}}return i||(i=t.getAttribute("stop-color")||"rgb(0,0,0)"),n||(n=t.getAttribute("stop-opacity")),r=(i=new T.Color(i)).getAlpha(),n=isNaN(parseFloat(n))?1:parseFloat(n),n*=r*e,{offset:a,color:i.toRgb(),opacity:n}}var e=T.util.object.clone;T.Gradient=T.util.createClass({offsetX:0,offsetY:0,gradientTransform:null,gradientUnits:"pixels",type:"linear",initialize:function(t){t||(t={}),t.coords||(t.coords={});var e,i=this;Object.keys(t).forEach((function(e){i[e]=t[e]})),this.id?this.id+="_"+T.Object.__uid++:this.id=T.Object.__uid++,e={x1:t.coords.x1||0,y1:t.coords.y1||0,x2:t.coords.x2||0,y2:t.coords.y2||0},"radial"===this.type&&(e.r1=t.coords.r1||0,e.r2=t.coords.r2||0),this.coords=e,this.colorStops=t.colorStops.slice()},addColorStop:function(t){for(var e in t){var i=new T.Color(t[e]);this.colorStops.push({offset:parseFloat(e),color:i.toRgb(),opacity:i.getAlpha()})}return this},toObject:function(t){var e={type:this.type,coords:this.coords,colorStops:this.colorStops,offsetX:this.offsetX,offsetY:this.offsetY,gradientUnits:this.gradientUnits,gradientTransform:this.gradientTransform?this.gradientTransform.concat():this.gradientTransform};return T.util.populateWithProperties(this,e,t),e},toSVG:function(t,i){var r,n,o,s,a=e(this.coords,!0),h=(i=i||{},e(this.colorStops,!0)),c=a.r1>a.r2,l=this.gradientTransform?this.gradientTransform.concat():T.iMatrix.concat(),u=-this.offsetX,f=-this.offsetY,d=!!i.additionalTransform,g="pixels"===this.gradientUnits?"userSpaceOnUse":"objectBoundingBox";if(h.sort((function(t,e){return t.offset-e.offset})),"objectBoundingBox"===g?(u/=t.width,f/=t.height):(u+=t.width/2,f+=t.height/2),"path"===t.type&&"percentage"!==this.gradientUnits&&(u-=t.pathOffset.x,f-=t.pathOffset.y),l[4]-=u,l[5]-=f,s='id="SVGID_'+this.id+'" gradientUnits="'+g+'"',s+=' gradientTransform="'+(d?i.additionalTransform+" ":"")+T.util.matrixToSVG(l)+'" ',"linear"===this.type?o=["\n']:"radial"===this.type&&(o=["\n']),"radial"===this.type){if(c)for((h=h.concat()).reverse(),r=0,n=h.length;r0){var v=p/Math.max(a.r1,a.r2);for(r=0,n=h.length;r\n')}return o.push("linear"===this.type?"\n":"\n"),o.join("")},toLive:function(t){var e,i,r,n=T.util.object.clone(this.coords);if(this.type){for("linear"===this.type?e=t.createLinearGradient(n.x1,n.y1,n.x2,n.y2):"radial"===this.type&&(e=t.createRadialGradient(n.x1,n.y1,n.r1,n.x2,n.y2,n.r2)),i=0,r=this.colorStops.length;i1?1:o,isNaN(o)&&(o=1);var s,a,h,c,l=e.getElementsByTagName("stop"),u="userSpaceOnUse"===e.getAttribute("gradientUnits")?"pixels":"percentage",f=e.getAttribute("gradientTransform")||"",d=[],g=0,p=0;for("linearGradient"===e.nodeName||"LINEARGRADIENT"===e.nodeName?(s="linear",a=function(t){return{x1:t.getAttribute("x1")||0,y1:t.getAttribute("y1")||0,x2:t.getAttribute("x2")||"100%",y2:t.getAttribute("y2")||0}}(e)):(s="radial",a=function(t){return{x1:t.getAttribute("fx")||t.getAttribute("cx")||"50%",y1:t.getAttribute("fy")||t.getAttribute("cy")||"50%",r1:0,x2:t.getAttribute("cx")||"50%",y2:t.getAttribute("cy")||"50%",r2:t.getAttribute("r")||"50%"}}(e)),h=l.length;h--;)d.push(t(l[h],o));return c=T.parseTransformAttribute(f),function(t,e,i,r){var n,o;Object.keys(e).forEach((function(t){"Infinity"===(n=e[t])?o=1:"-Infinity"===n?o=0:(o=parseFloat(e[t],10),"string"==typeof n&&/^(\d+\.\d+)%|(\d+)%$/.test(n)&&(o*=.01,"pixels"===r&&("x1"!==t&&"x2"!==t&&"r2"!==t||(o*=i.viewBoxWidth||i.width),"y1"!==t&&"y2"!==t||(o*=i.viewBoxHeight||i.height)))),e[t]=o}))}(0,a,n,u),"pixels"===u&&(g=-i.left,p=-i.top),new T.Gradient({id:e.getAttribute("id"),type:s,coords:a,colorStops:d,gradientUnits:u,gradientTransform:c,offsetX:g,offsetY:p})}})}(),_=T.util.toFixed,T.Pattern=T.util.createClass({repeat:"repeat",offsetX:0,offsetY:0,crossOrigin:"",patternTransform:null,initialize:function(t,e){if(t||(t={}),this.id=T.Object.__uid++,this.setOptions(t),!t.source||t.source&&"string"!=typeof t.source)e&&e(this);else{var i=this;this.source=T.util.createImage(),T.util.loadImage(t.source,(function(t,r){i.source=t,e&&e(i,r)}),null,this.crossOrigin)}},toObject:function(t){var e,i,r=T.Object.NUM_FRACTION_DIGITS;return"string"==typeof this.source.src?e=this.source.src:"object"==typeof this.source&&this.source.toDataURL&&(e=this.source.toDataURL()),i={type:"pattern",source:e,repeat:this.repeat,crossOrigin:this.crossOrigin,offsetX:_(this.offsetX,r),offsetY:_(this.offsetY,r),patternTransform:this.patternTransform?this.patternTransform.concat():null},T.util.populateWithProperties(this,i,t),i},toSVG:function(t){var e="function"==typeof this.source?this.source():this.source,i=e.width/t.width,r=e.height/t.height,n=this.offsetX/t.width,o=this.offsetY/t.height,s="";return"repeat-x"!==this.repeat&&"no-repeat"!==this.repeat||(r=1,o&&(r+=Math.abs(o))),"repeat-y"!==this.repeat&&"no-repeat"!==this.repeat||(i=1,n&&(i+=Math.abs(n))),e.src?s=e.src:e.toDataURL&&(s=e.toDataURL()),'\n\n\n'},setOptions:function(t){for(var e in t)this[e]=t[e]},toLive:function(t){var e=this.source;if(!e)return"";if(void 0!==e.src){if(!e.complete)return"";if(0===e.naturalWidth||0===e.naturalHeight)return""}return t.createPattern(e,this.repeat)}}),function(t){var e=t.fabric||(t.fabric={}),i=e.util.toFixed;e.Shadow?e.warn("fabric.Shadow is already defined."):(e.Shadow=e.util.createClass({color:"rgb(0,0,0)",blur:0,offsetX:0,offsetY:0,affectStroke:!1,includeDefaultValues:!0,nonScaling:!1,initialize:function(t){for(var i in"string"==typeof t&&(t=this._parseShadow(t)),t)this[i]=t[i];this.id=e.Object.__uid++},_parseShadow:function(t){var i=t.trim(),r=e.Shadow.reOffsetsAndBlur.exec(i)||[];return{color:(i.replace(e.Shadow.reOffsetsAndBlur,"")||"rgb(0,0,0)").trim(),offsetX:parseFloat(r[1],10)||0,offsetY:parseFloat(r[2],10)||0,blur:parseFloat(r[3],10)||0}},toString:function(){return[this.offsetX,this.offsetY,this.blur,this.color].join("px ")},toSVG:function(t){var r=40,n=40,o=e.Object.NUM_FRACTION_DIGITS,s=e.util.rotateVector({x:this.offsetX,y:this.offsetY},e.util.degreesToRadians(-t.angle)),a=new e.Color(this.color);return t.width&&t.height&&(r=100*i((Math.abs(s.x)+this.blur)/t.width,o)+20,n=100*i((Math.abs(s.y)+this.blur)/t.height,o)+20),t.flipX&&(s.x*=-1),t.flipY&&(s.y*=-1),'\n\t\n\t\n\t\n\t\n\t\n\t\t\n\t\t\n\t\n\n'},toObject:function(){if(this.includeDefaultValues)return{color:this.color,blur:this.blur,offsetX:this.offsetX,offsetY:this.offsetY,affectStroke:this.affectStroke,nonScaling:this.nonScaling};var t={},i=e.Shadow.prototype;return["color","blur","offsetX","offsetY","affectStroke","nonScaling"].forEach((function(e){this[e]!==i[e]&&(t[e]=this[e])}),this),t}}),e.Shadow.reOffsetsAndBlur=/(?:\s|^)(-?\d+(?:\.\d*)?(?:px)?(?:\s?|$))?(-?\d+(?:\.\d*)?(?:px)?(?:\s?|$))?(\d+(?:\.\d*)?(?:px)?)?(?:\s?|$)(?:$|\s)/)}(t),function(){if(T.StaticCanvas)T.warn("fabric.StaticCanvas is already defined.");else{var t=T.util.object.extend,e=T.util.getElementOffset,i=T.util.removeFromArray,r=T.util.toFixed,n=T.util.transformPoint,o=T.util.invertTransform,s=T.util.getNodeCanvas,a=T.util.createCanvasElement,h=new Error("Could not initialize `canvas` element");T.StaticCanvas=T.util.createClass(T.CommonMethods,{initialize:function(t,e){e||(e={}),this.renderAndResetBound=this.renderAndReset.bind(this),this.requestRenderAllBound=this.requestRenderAll.bind(this),this._initStatic(t,e)},backgroundColor:"",backgroundImage:null,overlayColor:"",overlayImage:null,includeDefaultValues:!0,stateful:!1,renderOnAddRemove:!0,controlsAboveOverlay:!1,allowTouchScrolling:!1,imageSmoothingEnabled:!0,viewportTransform:T.iMatrix.concat(),backgroundVpt:!0,overlayVpt:!0,enableRetinaScaling:!0,vptCoords:{},skipOffscreen:!0,clipPath:void 0,_initStatic:function(t,e){var i=this.requestRenderAllBound;this._objects=[],this._createLowerCanvas(t),this._initOptions(e),this.interactive||this._initRetinaScaling(),e.overlayImage&&this.setOverlayImage(e.overlayImage,i),e.backgroundImage&&this.setBackgroundImage(e.backgroundImage,i),e.backgroundColor&&this.setBackgroundColor(e.backgroundColor,i),e.overlayColor&&this.setOverlayColor(e.overlayColor,i),this.calcOffset()},_isRetinaScaling:function(){return 1!==T.devicePixelRatio&&this.enableRetinaScaling},getRetinaScaling:function(){return this._isRetinaScaling()?T.devicePixelRatio:1},_initRetinaScaling:function(){if(this._isRetinaScaling()){var t=T.devicePixelRatio;this.__initRetinaScaling(t,this.lowerCanvasEl,this.contextContainer),this.upperCanvasEl&&this.__initRetinaScaling(t,this.upperCanvasEl,this.contextTop)}},__initRetinaScaling:function(t,e,i){e.setAttribute("width",this.width*t),e.setAttribute("height",this.height*t),i.scale(t,t)},calcOffset:function(){return this._offset=e(this.lowerCanvasEl),this},setOverlayImage:function(t,e,i){return this.__setBgOverlayImage("overlayImage",t,e,i)},setBackgroundImage:function(t,e,i){return this.__setBgOverlayImage("backgroundImage",t,e,i)},setOverlayColor:function(t,e){return this.__setBgOverlayColor("overlayColor",t,e)},setBackgroundColor:function(t,e){return this.__setBgOverlayColor("backgroundColor",t,e)},__setBgOverlayImage:function(t,e,i,r){return"string"==typeof e?T.util.loadImage(e,(function(e,n){if(e){var o=new T.Image(e,r);this[t]=o,o.canvas=this}i&&i(e,n)}),this,r&&r.crossOrigin):(r&&e.setOptions(r),this[t]=e,e&&(e.canvas=this),i&&i(e,!1)),this},__setBgOverlayColor:function(t,e,i){return this[t]=e,this._initGradient(e,t),this._initPattern(e,t,i),this},_createCanvasElement:function(){var t=a();if(!t)throw h;if(t.style||(t.style={}),void 0===t.getContext)throw h;return t},_initOptions:function(t){var e=this.lowerCanvasEl;this._setOptions(t),this.width=this.width||parseInt(e.width,10)||0,this.height=this.height||parseInt(e.height,10)||0,this.lowerCanvasEl.style&&(e.width=this.width,e.height=this.height,e.style.width=this.width+"px",e.style.height=this.height+"px",this.viewportTransform=this.viewportTransform.slice())},_createLowerCanvas:function(t){t&&t.getContext?this.lowerCanvasEl=t:this.lowerCanvasEl=T.util.getById(t)||this._createCanvasElement(),T.util.addClass(this.lowerCanvasEl,"lower-canvas"),this._originalCanvasStyle=this.lowerCanvasEl.style,this.interactive&&this._applyCanvasStyle(this.lowerCanvasEl),this.contextContainer=this.lowerCanvasEl.getContext("2d")},getWidth:function(){return this.width},getHeight:function(){return this.height},setWidth:function(t,e){return this.setDimensions({width:t},e)},setHeight:function(t,e){return this.setDimensions({height:t},e)},setDimensions:function(t,e){var i;for(var r in e=e||{},t)i=t[r],e.cssOnly||(this._setBackstoreDimension(r,t[r]),i+="px",this.hasLostContext=!0),e.backstoreOnly||this._setCssDimension(r,i);return this._isCurrentlyDrawing&&this.freeDrawingBrush&&this.freeDrawingBrush._setBrushStyles(),this._initRetinaScaling(),this.calcOffset(),e.cssOnly||this.requestRenderAll(),this},_setBackstoreDimension:function(t,e){return this.lowerCanvasEl[t]=e,this.upperCanvasEl&&(this.upperCanvasEl[t]=e),this.cacheCanvasEl&&(this.cacheCanvasEl[t]=e),this[t]=e,this},_setCssDimension:function(t,e){return this.lowerCanvasEl.style[t]=e,this.upperCanvasEl&&(this.upperCanvasEl.style[t]=e),this.wrapperEl&&(this.wrapperEl.style[t]=e),this},getZoom:function(){return this.viewportTransform[0]},setViewportTransform:function(t){var e,i,r,n=this._activeObject,o=this.backgroundImage,s=this.overlayImage;for(this.viewportTransform=t,i=0,r=this._objects.length;i\n'),this._setSVGBgOverlayColor(i,"background"),this._setSVGBgOverlayImage(i,"backgroundImage",e),this._setSVGObjects(i,e),this.clipPath&&i.push("\n"),this._setSVGBgOverlayColor(i,"overlay"),this._setSVGBgOverlayImage(i,"overlayImage",e),i.push(""),i.join("")},_setSVGPreamble:function(t,e){e.suppressPreamble||t.push('\n','\n')},_setSVGHeader:function(t,e){var i,n=e.width||this.width,o=e.height||this.height,s='viewBox="0 0 '+this.width+" "+this.height+'" ',a=T.Object.NUM_FRACTION_DIGITS;e.viewBox?s='viewBox="'+e.viewBox.x+" "+e.viewBox.y+" "+e.viewBox.width+" "+e.viewBox.height+'" ':this.svgViewportTransformation&&(i=this.viewportTransform,s='viewBox="'+r(-i[4]/i[0],a)+" "+r(-i[5]/i[3],a)+" "+r(this.width/i[0],a)+" "+r(this.height/i[3],a)+'" '),t.push("\n',"Created with Fabric.js ",T.version,"\n","\n",this.createSVGFontFacesMarkup(),this.createSVGRefElementsMarkup(),this.createSVGClipPathMarkup(e),"\n")},createSVGClipPathMarkup:function(t){var e=this.clipPath;return e?(e.clipPathId="CLIPPATH_"+T.Object.__uid++,'\n'+this.clipPath.toClipPathSVG(t.reviver)+"\n"):""},createSVGRefElementsMarkup:function(){var t=this;return["background","overlay"].map((function(e){var i=t[e+"Color"];if(i&&i.toLive){var r=t[e+"Vpt"],n=t.viewportTransform,o={width:t.width/(r?n[0]:1),height:t.height/(r?n[3]:1)};return i.toSVG(o,{additionalTransform:r?T.util.matrixToSVG(n):""})}})).join("")},createSVGFontFacesMarkup:function(){var t,e,i,r,n,o,s,a,h="",c={},l=T.fontPaths,u=[];for(this._objects.forEach((function t(e){u.push(e),e._objects&&e._objects.forEach(t)})),s=0,a=u.length;s',"\n",h,"","\n"].join("")),h},_setSVGObjects:function(t,e){var i,r,n,o=this._objects;for(r=0,n=o.length;r\n")}else t.push('\n")},sendToBack:function(t){if(!t)return this;var e,r,n,o=this._activeObject;if(t===o&&"activeSelection"===t.type)for(e=(n=o._objects).length;e--;)r=n[e],i(this._objects,r),this._objects.unshift(r);else i(this._objects,t),this._objects.unshift(t);return this.renderOnAddRemove&&this.requestRenderAll(),this},bringToFront:function(t){if(!t)return this;var e,r,n,o=this._activeObject;if(t===o&&"activeSelection"===t.type)for(n=o._objects,e=0;e0+c&&(s=o-1,i(this._objects,n),this._objects.splice(s,0,n)),c++;else 0!==(o=this._objects.indexOf(t))&&(s=this._findNewLowerIndex(t,o,e),i(this._objects,t),this._objects.splice(s,0,t));return this.renderOnAddRemove&&this.requestRenderAll(),this},_findNewLowerIndex:function(t,e,i){var r,n;if(i)for(r=e,n=e-1;n>=0;--n){if(t.intersectsWithObject(this._objects[n])||t.isContainedWithinObject(this._objects[n])||this._objects[n].isContainedWithinObject(t)){r=n;break}}else r=e-1;return r},bringForward:function(t,e){if(!t)return this;var r,n,o,s,a,h=this._activeObject,c=0;if(t===h&&"activeSelection"===t.type)for(r=(a=h._objects).length;r--;)n=a[r],(o=this._objects.indexOf(n))"}}),t(T.StaticCanvas.prototype,T.Observable),t(T.StaticCanvas.prototype,T.Collection),t(T.StaticCanvas.prototype,T.DataURLExporter),t(T.StaticCanvas,{EMPTY_JSON:'{"objects": [], "background": "white"}',supports:function(t){var e=a();if(!e||!e.getContext)return null;var i=e.getContext("2d");return i&&"setLineDash"===t?void 0!==i.setLineDash:null}}),T.StaticCanvas.prototype.toJSON=T.StaticCanvas.prototype.toObject,T.isLikelyNode&&(T.StaticCanvas.prototype.createPNGStream=function(){var t=s(this.lowerCanvasEl);return t&&t.createPNGStream()},T.StaticCanvas.prototype.createJPEGStream=function(t){var e=s(this.lowerCanvasEl);return e&&e.createJPEGStream(t)})}}(),T.BaseBrush=T.util.createClass({color:"rgb(0, 0, 0)",width:1,shadow:null,strokeLineCap:"round",strokeLineJoin:"round",strokeMiterLimit:10,strokeDashArray:null,limitedToCanvasSize:!1,_setBrushStyles:function(){var t=this.canvas.contextTop;t.strokeStyle=this.color,t.lineWidth=this.width,t.lineCap=this.strokeLineCap,t.miterLimit=this.strokeMiterLimit,t.lineJoin=this.strokeLineJoin,t.setLineDash(this.strokeDashArray||[])},_saveAndTransform:function(t){var e=this.canvas.viewportTransform;t.save(),t.transform(e[0],e[1],e[2],e[3],e[4],e[5])},_setShadow:function(){if(this.shadow){var t=this.canvas,e=this.shadow,i=t.contextTop,r=t.getZoom();t&&t._isRetinaScaling()&&(r*=T.devicePixelRatio),i.shadowColor=e.color,i.shadowBlur=e.blur*r,i.shadowOffsetX=e.offsetX*r,i.shadowOffsetY=e.offsetY*r}},needsFullRender:function(){return new T.Color(this.color).getAlpha()<1||!!this.shadow},_resetShadow:function(){var t=this.canvas.contextTop;t.shadowColor="",t.shadowBlur=t.shadowOffsetX=t.shadowOffsetY=0},_isOutSideCanvas:function(t){return t.x<0||t.x>this.canvas.getWidth()||t.y<0||t.y>this.canvas.getHeight()}}),T.PencilBrush=T.util.createClass(T.BaseBrush,{decimate:.4,initialize:function(t){this.canvas=t,this._points=[]},_drawSegment:function(t,e,i){var r=e.midPointFrom(i);return t.quadraticCurveTo(e.x,e.y,r.x,r.y),r},onMouseDown:function(t,e){this.canvas._isMainEvent(e.e)&&(this._prepareForDrawing(t),this._captureDrawingPath(t),this._render())},onMouseMove:function(t,e){if(this.canvas._isMainEvent(e.e)&&(!0!==this.limitedToCanvasSize||!this._isOutSideCanvas(t))&&this._captureDrawingPath(t)&&this._points.length>1)if(this.needsFullRender())this.canvas.clearContext(this.canvas.contextTop),this._render();else{var i=this._points,r=i.length,n=this.canvas.contextTop;this._saveAndTransform(n),this.oldEnd&&(n.beginPath(),n.moveTo(this.oldEnd.x,this.oldEnd.y)),this.oldEnd=this._drawSegment(n,i[r-2],i[r-1],!0),n.stroke(),n.restore()}},onMouseUp:function(t){return!this.canvas._isMainEvent(t.e)||(this.oldEnd=void 0,this._finalizeAndAddPath(),!1)},_prepareForDrawing:function(t){var e=new T.Point(t.x,t.y);this._reset(),this._addPoint(e),this.canvas.contextTop.moveTo(e.x,e.y)},_addPoint:function(t){return!(this._points.length>1&&t.eq(this._points[this._points.length-1])||(this._points.push(t),0))},_reset:function(){this._points=[],this._setBrushStyles(),this._setShadow()},_captureDrawingPath:function(t){var e=new T.Point(t.x,t.y);return this._addPoint(e)},_render:function(){var t,e,i=this.canvas.contextTop,r=this._points[0],n=this._points[1];if(this._saveAndTransform(i),i.beginPath(),2===this._points.length&&r.x===n.x&&r.y===n.y){var o=this.width/1e3;r=new T.Point(r.x,r.y),n=new T.Point(n.x,n.y),r.x-=o,n.x+=o}for(i.moveTo(r.x,r.y),t=1,e=this._points.length;t=n&&(s=t[i],a.push(s));return a.push(t[o]),a},_finalizeAndAddPath:function(){this.canvas.contextTop.closePath(),this.decimate&&(this._points=this.decimatePoints(this._points,this.decimate));var t=this.convertPointsToSVGPath(this._points);if(this._isEmptySVGPath(t))this.canvas.requestRenderAll();else{var e=this.createPath(t);this.canvas.clearContext(this.canvas.contextTop),this.canvas.fire("before:path:created",{path:e}),this.canvas.add(e),this.canvas.requestRenderAll(),e.setCoords(),this._resetShadow(),this.canvas.fire("path:created",{path:e})}}}),T.CircleBrush=T.util.createClass(T.BaseBrush,{width:10,initialize:function(t){this.canvas=t,this.points=[]},drawDot:function(t){var e=this.addPoint(t),i=this.canvas.contextTop;this._saveAndTransform(i),this.dot(i,e),i.restore()},dot:function(t,e){t.fillStyle=e.fill,t.beginPath(),t.arc(e.x,e.y,e.radius,0,2*Math.PI,!1),t.closePath(),t.fill()},onMouseDown:function(t){this.points.length=0,this.canvas.clearContext(this.canvas.contextTop),this._setShadow(),this.drawDot(t)},_render:function(){var t,e,i=this.canvas.contextTop,r=this.points;for(this._saveAndTransform(i),t=0,e=r.length;t0&&!this.preserveObjectStacking){e=[],i=[];for(var n=0,o=this._objects.length;n1&&(this._activeObject._objects=i),e.push.apply(e,i)}else e=this._objects;return e},renderAll:function(){!this.contextTopDirty||this._groupSelector||this.isDrawingMode||(this.clearContext(this.contextTop),this.contextTopDirty=!1),this.hasLostContext&&this.renderTopLayer(this.contextTop);var t=this.contextContainer;return this.renderCanvas(t,this._chooseObjectsToRender()),this},renderTopLayer:function(t){t.save(),this.isDrawingMode&&this._isCurrentlyDrawing&&(this.freeDrawingBrush&&this.freeDrawingBrush._render(),this.contextTopDirty=!0),this.selection&&this._groupSelector&&(this._drawSelection(t),this.contextTopDirty=!0),t.restore()},renderTop:function(){var t=this.contextTop;return this.clearContext(t),this.renderTopLayer(t),this.fire("after:render"),this},_normalizePointer:function(t,e){var i=t.calcTransformMatrix(),r=T.util.invertTransform(i),n=this.restorePointerVpt(e);return T.util.transformPoint(n,r)},isTargetTransparent:function(t,e,i){if(t.shouldCache()&&t._cacheCanvas&&t!==this._activeObject){var r=this._normalizePointer(t,{x:e,y:i}),n=Math.max(t.cacheTranslationX+r.x*t.zoomX,0),o=Math.max(t.cacheTranslationY+r.y*t.zoomY,0);return T.util.isTransparent(t._cacheContext,Math.round(n),Math.round(o),this.targetFindTolerance)}var s=this.contextCache,a=t.selectionBackgroundColor,h=this.viewportTransform;return t.selectionBackgroundColor="",this.clearContext(s),s.save(),s.transform(h[0],h[1],h[2],h[3],h[4],h[5]),t.render(s),s.restore(),t.selectionBackgroundColor=a,T.util.isTransparent(s,e,i,this.targetFindTolerance)},_isSelectionKeyPressed:function(t){return"[object Array]"===Object.prototype.toString.call(this.selectionKey)?!!this.selectionKey.find((function(e){return!0===t[e]})):t[this.selectionKey]},_shouldClearSelection:function(t,e){var i=this.getActiveObjects(),r=this._activeObject;return!e||e&&r&&i.length>1&&-1===i.indexOf(e)&&r!==e&&!this._isSelectionKeyPressed(t)||e&&!e.evented||e&&!e.selectable&&r&&r!==e},_shouldCenterTransform:function(t,e,i){var r;if(t)return"scale"===e||"scaleX"===e||"scaleY"===e||"resizing"===e?r=this.centeredScaling||t.centeredScaling:"rotate"===e&&(r=this.centeredRotation||t.centeredRotation),r?!i:i},_getOriginFromCorner:function(t,e){var i={x:t.originX,y:t.originY};return"ml"===e||"tl"===e||"bl"===e?i.x="right":"mr"!==e&&"tr"!==e&&"br"!==e||(i.x="left"),"tl"===e||"mt"===e||"tr"===e?i.y="bottom":"bl"!==e&&"mb"!==e&&"br"!==e||(i.y="top"),i},_getActionFromCorner:function(t,e,i,r){if(!e||!t)return"drag";var n=r.controls[e];return n.getActionName(i,n,r)},_setupCurrentTransform:function(t,i,r){if(i){var n=this.getPointer(t),o=i.__corner,s=i.controls[o],a=r&&o?s.getActionHandler(t,i,s):T.controlsUtils.dragHandler,h=this._getActionFromCorner(r,o,t,i),c=this._getOriginFromCorner(i,o),l=t[this.centeredKey],u={target:i,action:h,actionHandler:a,corner:o,scaleX:i.scaleX,scaleY:i.scaleY,skewX:i.skewX,skewY:i.skewY,offsetX:n.x-i.left,offsetY:n.y-i.top,originX:c.x,originY:c.y,ex:n.x,ey:n.y,lastX:n.x,lastY:n.y,theta:e(i.angle),width:i.width*i.scaleX,shiftKey:t.shiftKey,altKey:l,original:T.util.saveObjectTransform(i)};this._shouldCenterTransform(i,h,l)&&(u.originX="center",u.originY="center"),u.original.originX=c.x,u.original.originY=c.y,this._currentTransform=u,this._beforeTransform(t)}},setCursor:function(t){this.upperCanvasEl.style.cursor=t},_drawSelection:function(t){var e=this._groupSelector,i=new T.Point(e.ex,e.ey),r=T.util.transformPoint(i,this.viewportTransform),n=new T.Point(e.ex+e.left,e.ey+e.top),o=T.util.transformPoint(n,this.viewportTransform),s=Math.min(r.x,o.x),a=Math.min(r.y,o.y),h=Math.max(r.x,o.x),c=Math.max(r.y,o.y),l=this.selectionLineWidth/2;this.selectionColor&&(t.fillStyle=this.selectionColor,t.fillRect(s,a,h-s,c-a)),this.selectionLineWidth&&this.selectionBorderColor&&(t.lineWidth=this.selectionLineWidth,t.strokeStyle=this.selectionBorderColor,s+=l,a+=l,h-=l,c-=l,T.Object.prototype._setLineDash.call(this,t,this.selectionDashArray),t.strokeRect(s,a,h-s,c-a))},findTarget:function(t,e){if(!this.skipTargetFind){var r,n,o=this.getPointer(t,!0),s=this._activeObject,a=this.getActiveObjects(),h=i(t),c=a.length>1&&!e||1===a.length;if(this.targets=[],c&&s._findTargetCorner(o,h))return s;if(a.length>1&&!e&&s===this._searchPossibleTargets([s],o))return s;if(1===a.length&&s===this._searchPossibleTargets([s],o)){if(!this.preserveObjectStacking)return s;r=s,n=this.targets,this.targets=[]}var l=this._searchPossibleTargets(this._objects,o);return t[this.altSelectionKey]&&l&&r&&l!==r&&(l=r,this.targets=n),l}},_checkTarget:function(t,e,i){if(e&&e.visible&&e.evented&&e.containsPoint(t)){if(!this.perPixelTargetFind&&!e.perPixelTargetFind||e.isEditing)return!0;if(!this.isTargetTransparent(e,i.x,i.y))return!0}},_searchPossibleTargets:function(t,e){for(var i,r,n=t.length;n--;){var o=t[n],s=o.group?this._normalizePointer(o.group,e):e;if(this._checkTarget(s,o,e)){(i=t[n]).subTargetCheck&&i instanceof T.Group&&(r=this._searchPossibleTargets(i._objects,e))&&this.targets.push(r);break}}return i},restorePointerVpt:function(t){return T.util.transformPoint(t,T.util.invertTransform(this.viewportTransform))},getPointer:function(e,i){if(this._absolutePointer&&!i)return this._absolutePointer;if(this._pointer&&i)return this._pointer;var r,n=t(e),o=this.upperCanvasEl,s=o.getBoundingClientRect(),a=s.width||0,h=s.height||0;a&&h||("top"in s&&"bottom"in s&&(h=Math.abs(s.top-s.bottom)),"right"in s&&"left"in s&&(a=Math.abs(s.right-s.left))),this.calcOffset(),n.x=n.x-this._offset.left,n.y=n.y-this._offset.top,i||(n=this.restorePointerVpt(n));var c=this.getRetinaScaling();return 1!==c&&(n.x/=c,n.y/=c),r=0===a||0===h?{width:1,height:1}:{width:o.width/a,height:o.height/h},{x:n.x*r.width,y:n.y*r.height}},_createUpperCanvas:function(){var t=this.lowerCanvasEl.className.replace(/\s*lower-canvas\s*/,""),e=this.lowerCanvasEl,i=this.upperCanvasEl;i?i.className="":(i=this._createCanvasElement(),this.upperCanvasEl=i),T.util.addClass(i,"upper-canvas "+t),this.wrapperEl.appendChild(i),this._copyCanvasStyle(e,i),this._applyCanvasStyle(i),this.contextTop=i.getContext("2d")},_createCacheCanvas:function(){this.cacheCanvasEl=this._createCanvasElement(),this.cacheCanvasEl.setAttribute("width",this.width),this.cacheCanvasEl.setAttribute("height",this.height),this.contextCache=this.cacheCanvasEl.getContext("2d")},_initWrapperElement:function(){this.wrapperEl=T.util.wrapElement(this.lowerCanvasEl,"div",{class:this.containerClass}),T.util.setStyle(this.wrapperEl,{width:this.width+"px",height:this.height+"px",position:"relative"}),T.util.makeElementUnselectable(this.wrapperEl)},_applyCanvasStyle:function(t){var e=this.width||t.width,i=this.height||t.height;T.util.setStyle(t,{position:"absolute",width:e+"px",height:i+"px",left:0,top:0,"touch-action":this.allowTouchScrolling?"manipulation":"none","-ms-touch-action":this.allowTouchScrolling?"manipulation":"none"}),t.width=e,t.height=i,T.util.makeElementUnselectable(t)},_copyCanvasStyle:function(t,e){e.style.cssText=t.style.cssText},getSelectionContext:function(){return this.contextTop},getSelectionElement:function(){return this.upperCanvasEl},getActiveObject:function(){return this._activeObject},getActiveObjects:function(){var t=this._activeObject;return t?"activeSelection"===t.type&&t._objects?t._objects.slice(0):[t]:[]},_onObjectRemoved:function(t){t===this._activeObject&&(this.fire("before:selection:cleared",{target:t}),this._discardActiveObject(),this.fire("selection:cleared",{target:t}),t.fire("deselected")),t===this._hoveredTarget&&(this._hoveredTarget=null,this._hoveredTargets=[]),this.callSuper("_onObjectRemoved",t)},_fireSelectionEvents:function(t,e){var i=!1,r=this.getActiveObjects(),n=[],o=[];t.forEach((function(t){-1===r.indexOf(t)&&(i=!0,t.fire("deselected",{e:e,target:t}),o.push(t))})),r.forEach((function(r){-1===t.indexOf(r)&&(i=!0,r.fire("selected",{e:e,target:r}),n.push(r))})),t.length>0&&r.length>0?i&&this.fire("selection:updated",{e:e,selected:n,deselected:o,updated:n[0]||o[0],target:this._activeObject}):r.length>0?this.fire("selection:created",{e:e,selected:n,target:this._activeObject}):t.length>0&&this.fire("selection:cleared",{e:e,deselected:o})},setActiveObject:function(t,e){var i=this.getActiveObjects();return this._setActiveObject(t,e),this._fireSelectionEvents(i,e),this},_setActiveObject:function(t,e){return this._activeObject!==t&&(!!this._discardActiveObject(e,t)&&(!t.onSelect({e:e})&&(this._activeObject=t,!0)))},_discardActiveObject:function(t,e){var i=this._activeObject;if(i){if(i.onDeselect({e:t,object:e}))return!1;this._activeObject=null}return!0},discardActiveObject:function(t){var e=this.getActiveObjects(),i=this.getActiveObject();return e.length&&this.fire("before:selection:cleared",{target:i,e:t}),this._discardActiveObject(t),this._fireSelectionEvents(e,t),this},dispose:function(){var t=this.wrapperEl;return this.removeListeners(),t.removeChild(this.upperCanvasEl),t.removeChild(this.lowerCanvasEl),this.contextCache=null,this.contextTop=null,["upperCanvasEl","cacheCanvasEl"].forEach(function(t){T.util.cleanUpJsdomNode(this[t]),this[t]=void 0}.bind(this)),t.parentNode&&t.parentNode.replaceChild(this.lowerCanvasEl,this.wrapperEl),delete this.wrapperEl,T.StaticCanvas.prototype.dispose.call(this),this},clear:function(){return this.discardActiveObject(),this.clearContext(this.contextTop),this.callSuper("clear")},drawControls:function(t){var e=this._activeObject;e&&e._renderControls(t)},_toObject:function(t,e,i){var r=this._realizeGroupTransformOnObject(t),n=this.callSuper("_toObject",t,e,i);return this._unwindGroupTransformOnObject(t,r),n},_realizeGroupTransformOnObject:function(t){if(t.group&&"activeSelection"===t.group.type&&this._activeObject===t.group){var e={};return["angle","flipX","flipY","left","scaleX","scaleY","skewX","skewY","top"].forEach((function(i){e[i]=t[i]})),T.util.addTransformToObject(t,this._activeObject.calcOwnMatrix()),e}return null},_unwindGroupTransformOnObject:function(t,e){e&&t.set(e)},_setSVGObject:function(t,e,i){var r=this._realizeGroupTransformOnObject(e);this.callSuper("_setSVGObject",t,e,i),this._unwindGroupTransformOnObject(e,r)},setViewportTransform:function(t){this.renderOnAddRemove&&this._activeObject&&this._activeObject.isEditing&&this._activeObject.clearContextTop(),T.StaticCanvas.prototype.setViewportTransform.call(this,t)}}),T.StaticCanvas)"prototype"!==r&&(T.Canvas[r]=T.StaticCanvas[r])}(),function(){var t=T.util.addListener,e=T.util.removeListener,i={passive:!1};function r(t,e){return t.button&&t.button===e-1}T.util.object.extend(T.Canvas.prototype,{mainTouchId:null,_initEventListeners:function(){this.removeListeners(),this._bindEvents(),this.addOrRemove(t,"add")},_getEventPrefix:function(){return this.enablePointerEvents?"pointer":"mouse"},addOrRemove:function(t,e){var r=this.upperCanvasEl,n=this._getEventPrefix();t(T.window,"resize",this._onResize),t(r,n+"down",this._onMouseDown),t(r,n+"move",this._onMouseMove,i),t(r,n+"out",this._onMouseOut),t(r,n+"enter",this._onMouseEnter),t(r,"wheel",this._onMouseWheel),t(r,"contextmenu",this._onContextMenu),t(r,"dblclick",this._onDoubleClick),t(r,"dragover",this._onDragOver),t(r,"dragenter",this._onDragEnter),t(r,"dragleave",this._onDragLeave),t(r,"drop",this._onDrop),this.enablePointerEvents||t(r,"touchstart",this._onTouchStart,i),void 0!==P&&e in P&&(P[e](r,"gesture",this._onGesture),P[e](r,"drag",this._onDrag),P[e](r,"orientation",this._onOrientationChange),P[e](r,"shake",this._onShake),P[e](r,"longpress",this._onLongPress))},removeListeners:function(){this.addOrRemove(e,"remove");var t=this._getEventPrefix();e(T.document,t+"up",this._onMouseUp),e(T.document,"touchend",this._onTouchEnd,i),e(T.document,t+"move",this._onMouseMove,i),e(T.document,"touchmove",this._onMouseMove,i)},_bindEvents:function(){this.eventsBound||(this._onMouseDown=this._onMouseDown.bind(this),this._onTouchStart=this._onTouchStart.bind(this),this._onMouseMove=this._onMouseMove.bind(this),this._onMouseUp=this._onMouseUp.bind(this),this._onTouchEnd=this._onTouchEnd.bind(this),this._onResize=this._onResize.bind(this),this._onGesture=this._onGesture.bind(this),this._onDrag=this._onDrag.bind(this),this._onShake=this._onShake.bind(this),this._onLongPress=this._onLongPress.bind(this),this._onOrientationChange=this._onOrientationChange.bind(this),this._onMouseWheel=this._onMouseWheel.bind(this),this._onMouseOut=this._onMouseOut.bind(this),this._onMouseEnter=this._onMouseEnter.bind(this),this._onContextMenu=this._onContextMenu.bind(this),this._onDoubleClick=this._onDoubleClick.bind(this),this._onDragOver=this._onDragOver.bind(this),this._onDragEnter=this._simpleEventHandler.bind(this,"dragenter"),this._onDragLeave=this._simpleEventHandler.bind(this,"dragleave"),this._onDrop=this._simpleEventHandler.bind(this,"drop"),this.eventsBound=!0)},_onGesture:function(t,e){this.__onTransformGesture&&this.__onTransformGesture(t,e)},_onDrag:function(t,e){this.__onDrag&&this.__onDrag(t,e)},_onMouseWheel:function(t){this.__onMouseWheel(t)},_onMouseOut:function(t){var e=this._hoveredTarget;this.fire("mouse:out",{target:e,e:t}),this._hoveredTarget=null,e&&e.fire("mouseout",{e:t});var i=this;this._hoveredTargets.forEach((function(r){i.fire("mouse:out",{target:e,e:t}),r&&e.fire("mouseout",{e:t})})),this._hoveredTargets=[],this._iTextInstances&&this._iTextInstances.forEach((function(t){t.isEditing&&t.hiddenTextarea.focus()}))},_onMouseEnter:function(t){this._currentTransform||this.findTarget(t)||(this.fire("mouse:over",{target:null,e:t}),this._hoveredTarget=null,this._hoveredTargets=[])},_onOrientationChange:function(t,e){this.__onOrientationChange&&this.__onOrientationChange(t,e)},_onShake:function(t,e){this.__onShake&&this.__onShake(t,e)},_onLongPress:function(t,e){this.__onLongPress&&this.__onLongPress(t,e)},_onDragOver:function(t){t.preventDefault();var e=this._simpleEventHandler("dragover",t);this._fireEnterLeaveEvents(e,t)},_onContextMenu:function(t){return this.stopContextMenu&&(t.stopPropagation(),t.preventDefault()),!1},_onDoubleClick:function(t){this._cacheTransformEventData(t),this._handleEvent(t,"dblclick"),this._resetTransformEventData(t)},getPointerId:function(t){var e=t.changedTouches;return e?e[0]&&e[0].identifier:this.enablePointerEvents?t.pointerId:-1},_isMainEvent:function(t){return!0===t.isPrimary||!1!==t.isPrimary&&("touchend"===t.type&&0===t.touches.length||(!t.changedTouches||t.changedTouches[0].identifier===this.mainTouchId))},_onTouchStart:function(r){r.preventDefault(),null===this.mainTouchId&&(this.mainTouchId=this.getPointerId(r)),this.__onMouseDown(r),this._resetTransformEventData();var n=this.upperCanvasEl,o=this._getEventPrefix();t(T.document,"touchend",this._onTouchEnd,i),t(T.document,"touchmove",this._onMouseMove,i),e(n,o+"down",this._onMouseDown)},_onMouseDown:function(r){this.__onMouseDown(r),this._resetTransformEventData();var n=this.upperCanvasEl,o=this._getEventPrefix();e(n,o+"move",this._onMouseMove,i),t(T.document,o+"up",this._onMouseUp),t(T.document,o+"move",this._onMouseMove,i)},_onTouchEnd:function(r){if(!(r.touches.length>0)){this.__onMouseUp(r),this._resetTransformEventData(),this.mainTouchId=null;var n=this._getEventPrefix();e(T.document,"touchend",this._onTouchEnd,i),e(T.document,"touchmove",this._onMouseMove,i);var o=this;this._willAddMouseDown&&clearTimeout(this._willAddMouseDown),this._willAddMouseDown=setTimeout((function(){t(o.upperCanvasEl,n+"down",o._onMouseDown),o._willAddMouseDown=0}),400)}},_onMouseUp:function(r){this.__onMouseUp(r),this._resetTransformEventData();var n=this.upperCanvasEl,o=this._getEventPrefix();this._isMainEvent(r)&&(e(T.document,o+"up",this._onMouseUp),e(T.document,o+"move",this._onMouseMove,i),t(n,o+"move",this._onMouseMove,i))},_onMouseMove:function(t){!this.allowTouchScrolling&&t.preventDefault&&t.preventDefault(),this.__onMouseMove(t)},_onResize:function(){this.calcOffset()},_shouldRender:function(t){var e=this._activeObject;return!!(!!e!=!!t||e&&t&&e!==t)||(e&&e.isEditing,!1)},__onMouseUp:function(t){var e,i=this._currentTransform,n=this._groupSelector,o=!1,s=!n||0===n.left&&0===n.top;if(this._cacheTransformEventData(t),e=this._target,this._handleEvent(t,"up:before"),r(t,3))this.fireRightClick&&this._handleEvent(t,"up",3,s);else{if(r(t,2))return this.fireMiddleClick&&this._handleEvent(t,"up",2,s),void this._resetTransformEventData();if(this.isDrawingMode&&this._isCurrentlyDrawing)this._onMouseUpInDrawingMode(t);else if(this._isMainEvent(t)){if(i&&(this._finalizeCurrentTransform(t),o=i.actionPerformed),!s){var a=e===this._activeObject;this._maybeGroupObjects(t),o||(o=this._shouldRender(e)||!a&&e===this._activeObject)}if(e){if(e.selectable&&e!==this._activeObject&&"up"===e.activeOn)this.setActiveObject(e,t),o=!0;else{var h=e._findTargetCorner(this.getPointer(t,!0),T.util.isTouchEvent(t)),c=e.controls[h],l=c&&c.getMouseUpHandler(t,e,c);if(l){var u=this.getPointer(t);l(t,i,u.x,u.y)}}e.isMoving=!1}this._setCursorFromEvent(t,e),this._handleEvent(t,"up",1,s),this._groupSelector=null,this._currentTransform=null,e&&(e.__corner=0),o?this.requestRenderAll():s||this.renderTop()}}},_simpleEventHandler:function(t,e){var i=this.findTarget(e),r=this.targets,n={e:e,target:i,subTargets:r};if(this.fire(t,n),i&&i.fire(t,n),!r)return i;for(var o=0;o1&&(e=new T.ActiveSelection(i.reverse(),{canvas:this}),this.setActiveObject(e,t))},_collectObjects:function(t){for(var e,i=[],r=this._groupSelector.ex,n=this._groupSelector.ey,o=r+this._groupSelector.left,s=n+this._groupSelector.top,a=new T.Point(b(r,o),b(n,s)),h=new T.Point(x(r,o),x(n,s)),c=!this.selectionFullyContained,l=r===o&&n===s,u=this._objects.length;u--&&!((e=this._objects[u])&&e.selectable&&e.visible&&(c&&e.intersectsWithRect(a,h,!0)||e.isContainedWithinRect(a,h,!0)||c&&e.containsPoint(a,null,!0)||c&&e.containsPoint(h,null,!0))&&(i.push(e),l)););return i.length>1&&(i=i.filter((function(e){return!e.onSelect({e:t})}))),i},_maybeGroupObjects:function(t){this.selection&&this._groupSelector&&this._groupSelectedObjects(t),this.setCursor(this.defaultCursor),this._groupSelector=null}}),T.util.object.extend(T.StaticCanvas.prototype,{toDataURL:function(t){t||(t={});var e=t.format||"png",i=t.quality||1,r=(t.multiplier||1)*(t.enableRetinaScaling?this.getRetinaScaling():1),n=this.toCanvasElement(r,t);return T.util.toDataURL(n,e,i)},toCanvasElement:function(t,e){t=t||1;var i=((e=e||{}).width||this.width)*t,r=(e.height||this.height)*t,n=this.getZoom(),o=this.width,s=this.height,a=n*t,h=this.viewportTransform,c=(h[4]-(e.left||0))*t,l=(h[5]-(e.top||0))*t,u=this.interactive,f=[a,0,0,a,c,l],d=this.enableRetinaScaling,g=T.util.createCanvasElement(),p=this.contextTop;return g.width=i,g.height=r,this.contextTop=null,this.enableRetinaScaling=!1,this.interactive=!1,this.viewportTransform=f,this.width=i,this.height=r,this.calcViewportBoundaries(),this.renderCanvas(g.getContext("2d"),this._objects),this.viewportTransform=h,this.width=o,this.height=s,this.calcViewportBoundaries(),this.interactive=u,this.enableRetinaScaling=d,this.contextTop=p,g}}),T.util.object.extend(T.StaticCanvas.prototype,{loadFromJSON:function(t,e,i){if(t){var r="string"==typeof t?JSON.parse(t):T.util.object.clone(t),n=this,o=r.clipPath,s=this.renderOnAddRemove;return this.renderOnAddRemove=!1,delete r.clipPath,this._enlivenObjects(r.objects,(function(t){n.clear(),n._setBgOverlay(r,(function(){o?n._enlivenObjects([o],(function(i){n.clipPath=i[0],n.__setupCanvas.call(n,r,t,s,e)})):n.__setupCanvas.call(n,r,t,s,e)}))}),i),this}},__setupCanvas:function(t,e,i,r){var n=this;e.forEach((function(t,e){n.insertAt(t,e)})),this.renderOnAddRemove=i,delete t.objects,delete t.backgroundImage,delete t.overlayImage,delete t.background,delete t.overlay,this._setOptions(t),this.renderAll(),r&&r()},_setBgOverlay:function(t,e){var i={backgroundColor:!1,overlayColor:!1,backgroundImage:!1,overlayImage:!1};if(t.backgroundImage||t.overlayImage||t.background||t.overlay){var r=function(){i.backgroundImage&&i.overlayImage&&i.backgroundColor&&i.overlayColor&&e&&e()};this.__setBgOverlay("backgroundImage",t.backgroundImage,i,r),this.__setBgOverlay("overlayImage",t.overlayImage,i,r),this.__setBgOverlay("backgroundColor",t.background,i,r),this.__setBgOverlay("overlayColor",t.overlay,i,r)}else e&&e()},__setBgOverlay:function(t,e,i,r){var n=this;if(!e)return i[t]=!0,void(r&&r());"backgroundImage"===t||"overlayImage"===t?T.util.enlivenObjects([e],(function(e){n[t]=e[0],i[t]=!0,r&&r()})):this["set"+T.util.string.capitalize(t,!0)](e,(function(){i[t]=!0,r&&r()}))},_enlivenObjects:function(t,e,i){t&&0!==t.length?T.util.enlivenObjects(t,(function(t){e&&e(t)}),null,i):e&&e([])},_toDataURL:function(t,e){this.clone((function(i){e(i.toDataURL(t))}))},_toDataURLWithMultiplier:function(t,e,i){this.clone((function(r){i(r.toDataURLWithMultiplier(t,e))}))},clone:function(t,e){var i=JSON.stringify(this.toJSON(e));this.cloneWithoutData((function(e){e.loadFromJSON(i,(function(){t&&t(e)}))}))},cloneWithoutData:function(t){var e=T.util.createCanvasElement();e.width=this.width,e.height=this.height;var i=new T.Canvas(e);this.backgroundImage?(i.setBackgroundImage(this.backgroundImage.src,(function(){i.renderAll(),t&&t(i)})),i.backgroundImageOpacity=this.backgroundImageOpacity,i.backgroundImageStretch=this.backgroundImageStretch):t&&t(i)}}),C=T.util.degreesToRadians,w=T.util.radiansToDegrees,T.util.object.extend(T.Canvas.prototype,{__onTransformGesture:function(t,e){if(!this.isDrawingMode&&t.touches&&2===t.touches.length&&"gesture"===e.gesture){var i=this.findTarget(t);void 0!==i&&(this.__gesturesParams={e:t,self:e,target:i},this.__gesturesRenderer()),this.fire("touch:gesture",{target:i,e:t,self:e})}},__gesturesParams:null,__gesturesRenderer:function(){if(null!==this.__gesturesParams&&null!==this._currentTransform){var t=this.__gesturesParams.self,e=this._currentTransform,i=this.__gesturesParams.e;e.action="scale",e.originX=e.originY="center",this._scaleObjectBy(t.scale,i),0!==t.rotation&&(e.action="rotate",this._rotateObjectByAngle(t.rotation,i)),this.requestRenderAll(),e.action="drag"}},__onDrag:function(t,e){this.fire("touch:drag",{e:t,self:e})},__onOrientationChange:function(t,e){this.fire("touch:orientation",{e:t,self:e})},__onShake:function(t,e){this.fire("touch:shake",{e:t,self:e})},__onLongPress:function(t,e){this.fire("touch:longpress",{e:t,self:e})},_scaleObjectBy:function(t,e){var i=this._currentTransform,r=i.target;return i.gestureScale=t,r._scaling=!0,T.controlsUtils.scalingEqually(e,i,0,0)},_rotateObjectByAngle:function(t,e){var i=this._currentTransform;i.target.get("lockRotation")||(i.target.rotate(w(C(t)+i.theta)),this._fire("rotating",{target:i.target,e:e,transform:i}))}}),function(t){var e=t.fabric||(t.fabric={}),i=e.util.object.extend,r=e.util.object.clone,n=e.util.toFixed,o=e.util.string.capitalize,s=e.util.degreesToRadians,a=!e.isLikelyNode;e.Object||(e.Object=e.util.createClass(e.CommonMethods,{type:"object",originX:"left",originY:"top",top:0,left:0,width:0,height:0,scaleX:1,scaleY:1,flipX:!1,flipY:!1,opacity:1,angle:0,skewX:0,skewY:0,cornerSize:13,touchCornerSize:24,transparentCorners:!0,hoverCursor:null,moveCursor:null,padding:0,borderColor:"rgb(178,204,255)",borderDashArray:null,cornerColor:"rgb(178,204,255)",cornerStrokeColor:null,cornerStyle:"rect",cornerDashArray:null,centeredScaling:!1,centeredRotation:!0,fill:"rgb(0,0,0)",fillRule:"nonzero",globalCompositeOperation:"source-over",backgroundColor:"",selectionBackgroundColor:"",stroke:null,strokeWidth:1,strokeDashArray:null,strokeDashOffset:0,strokeLineCap:"butt",strokeLineJoin:"miter",strokeMiterLimit:4,shadow:null,borderOpacityWhenMoving:.4,borderScaleFactor:1,minScaleLimit:0,selectable:!0,evented:!0,visible:!0,hasControls:!0,hasBorders:!0,perPixelTargetFind:!1,includeDefaultValues:!0,lockMovementX:!1,lockMovementY:!1,lockRotation:!1,lockScalingX:!1,lockScalingY:!1,lockSkewingX:!1,lockSkewingY:!1,lockScalingFlip:!1,excludeFromExport:!1,objectCaching:a,statefullCache:!1,noScaleCache:!0,strokeUniform:!1,dirty:!0,__corner:0,paintFirst:"fill",activeOn:"down",stateProperties:"top left width height scaleX scaleY flipX flipY originX originY transformMatrix stroke strokeWidth strokeDashArray strokeLineCap strokeDashOffset strokeLineJoin strokeMiterLimit angle opacity fill globalCompositeOperation shadow visible backgroundColor skewX skewY fillRule paintFirst clipPath strokeUniform".split(" "),cacheProperties:"fill stroke strokeWidth strokeDashArray width height paintFirst strokeUniform strokeLineCap strokeDashOffset strokeLineJoin strokeMiterLimit backgroundColor clipPath".split(" "),colorProperties:"fill stroke backgroundColor".split(" "),clipPath:void 0,inverted:!1,absolutePositioned:!1,initialize:function(t){t&&this.setOptions(t)},_createCacheCanvas:function(){this._cacheProperties={},this._cacheCanvas=e.util.createCanvasElement(),this._cacheContext=this._cacheCanvas.getContext("2d"),this._updateCacheCanvas(),this.dirty=!0},_limitCacheSize:function(t){var i=e.perfLimitSizeTotal,r=t.width,n=t.height,o=e.maxCacheSideLimit,s=e.minCacheSideLimit;if(r<=o&&n<=o&&r*n<=i)return rl&&(t.zoomX/=r/l,t.width=l,t.capped=!0),n>u&&(t.zoomY/=n/u,t.height=u,t.capped=!0),t},_getCacheCanvasDimensions:function(){var t=this.getTotalObjectScaling(),e=this._getTransformedDimensions(0,0),i=e.x*t.scaleX/this.scaleX,r=e.y*t.scaleY/this.scaleY;return{width:i+2,height:r+2,zoomX:t.scaleX,zoomY:t.scaleY,x:i,y:r}},_updateCacheCanvas:function(){var t=this.canvas;if(this.noScaleCache&&t&&t._currentTransform){var i=t._currentTransform.target,r=t._currentTransform.action;if(this===i&&r.slice&&"scale"===r.slice(0,5))return!1}var n,o,s=this._cacheCanvas,a=this._limitCacheSize(this._getCacheCanvasDimensions()),h=e.minCacheSideLimit,c=a.width,l=a.height,u=a.zoomX,f=a.zoomY,d=c!==this.cacheWidth||l!==this.cacheHeight,g=this.zoomX!==u||this.zoomY!==f,p=d||g,v=0,m=0,y=!1;if(d){var _=this._cacheCanvas.width,b=this._cacheCanvas.height,x=c>_||l>b;y=x||(c<.9*_||l<.9*b)&&_>h&&b>h,x&&!a.capped&&(c>h||l>h)&&(v=.1*c,m=.1*l)}return this instanceof e.Text&&this.path&&(p=!0,y=!0,v+=this.getHeightOfLine(0)*this.zoomX,m+=this.getHeightOfLine(0)*this.zoomY),!!p&&(y?(s.width=Math.ceil(c+v),s.height=Math.ceil(l+m)):(this._cacheContext.setTransform(1,0,0,1,0,0),this._cacheContext.clearRect(0,0,s.width,s.height)),n=a.x/2,o=a.y/2,this.cacheTranslationX=Math.round(s.width/2-n)+n,this.cacheTranslationY=Math.round(s.height/2-o)+o,this.cacheWidth=c,this.cacheHeight=l,this._cacheContext.translate(this.cacheTranslationX,this.cacheTranslationY),this._cacheContext.scale(u,f),this.zoomX=u,this.zoomY=f,!0)},setOptions:function(t){this._setOptions(t),this._initGradient(t.fill,"fill"),this._initGradient(t.stroke,"stroke"),this._initPattern(t.fill,"fill"),this._initPattern(t.stroke,"stroke")},transform:function(t){var e=this.group&&!this.group._transformDone||this.group&&this.canvas&&t===this.canvas.contextTop,i=this.calcTransformMatrix(!e);t.transform(i[0],i[1],i[2],i[3],i[4],i[5])},toObject:function(t){var i=e.Object.NUM_FRACTION_DIGITS,r={type:this.type,version:e.version,originX:this.originX,originY:this.originY,left:n(this.left,i),top:n(this.top,i),width:n(this.width,i),height:n(this.height,i),fill:this.fill&&this.fill.toObject?this.fill.toObject():this.fill,stroke:this.stroke&&this.stroke.toObject?this.stroke.toObject():this.stroke,strokeWidth:n(this.strokeWidth,i),strokeDashArray:this.strokeDashArray?this.strokeDashArray.concat():this.strokeDashArray,strokeLineCap:this.strokeLineCap,strokeDashOffset:this.strokeDashOffset,strokeLineJoin:this.strokeLineJoin,strokeUniform:this.strokeUniform,strokeMiterLimit:n(this.strokeMiterLimit,i),scaleX:n(this.scaleX,i),scaleY:n(this.scaleY,i),angle:n(this.angle,i),flipX:this.flipX,flipY:this.flipY,opacity:n(this.opacity,i),shadow:this.shadow&&this.shadow.toObject?this.shadow.toObject():this.shadow,visible:this.visible,backgroundColor:this.backgroundColor,fillRule:this.fillRule,paintFirst:this.paintFirst,globalCompositeOperation:this.globalCompositeOperation,skewX:n(this.skewX,i),skewY:n(this.skewY,i)};return this.clipPath&&!this.clipPath.excludeFromExport&&(r.clipPath=this.clipPath.toObject(t),r.clipPath.inverted=this.clipPath.inverted,r.clipPath.absolutePositioned=this.clipPath.absolutePositioned),e.util.populateWithProperties(this,r,t),this.includeDefaultValues||(r=this._removeDefaultValues(r)),r},toDatalessObject:function(t){return this.toObject(t)},_removeDefaultValues:function(t){var i=e.util.getKlass(t.type).prototype;return i.stateProperties.forEach((function(e){"left"!==e&&"top"!==e&&(t[e]===i[e]&&delete t[e],"[object Array]"===Object.prototype.toString.call(t[e])&&"[object Array]"===Object.prototype.toString.call(i[e])&&0===t[e].length&&0===i[e].length&&delete t[e])})),t},toString:function(){return"#"},getObjectScaling:function(){if(!this.group)return{scaleX:this.scaleX,scaleY:this.scaleY};var t=e.util.qrDecompose(this.calcTransformMatrix());return{scaleX:Math.abs(t.scaleX),scaleY:Math.abs(t.scaleY)}},getTotalObjectScaling:function(){var t=this.getObjectScaling(),e=t.scaleX,i=t.scaleY;if(this.canvas){var r=this.canvas.getZoom(),n=this.canvas.getRetinaScaling();e*=r*n,i*=r*n}return{scaleX:e,scaleY:i}},getObjectOpacity:function(){var t=this.opacity;return this.group&&(t*=this.group.getObjectOpacity()),t},_set:function(t,i){var r="scaleX"===t||"scaleY"===t,n=this[t]!==i,o=!1;return r&&(i=this._constrainScale(i)),"scaleX"===t&&i<0?(this.flipX=!this.flipX,i*=-1):"scaleY"===t&&i<0?(this.flipY=!this.flipY,i*=-1):"shadow"!==t||!i||i instanceof e.Shadow?"dirty"===t&&this.group&&this.group.set("dirty",i):i=new e.Shadow(i),this[t]=i,n&&(o=this.group&&this.group.isOnACache(),this.cacheProperties.indexOf(t)>-1?(this.dirty=!0,o&&this.group.set("dirty",!0)):o&&this.stateProperties.indexOf(t)>-1&&this.group.set("dirty",!0)),this},setOnGroup:function(){},getViewportTransform:function(){return this.canvas&&this.canvas.viewportTransform?this.canvas.viewportTransform:e.iMatrix.concat()},isNotVisible:function(){return 0===this.opacity||!this.width&&!this.height&&0===this.strokeWidth||!this.visible},render:function(t){this.isNotVisible()||this.canvas&&this.canvas.skipOffscreen&&!this.group&&!this.isOnScreen()||(t.save(),this._setupCompositeOperation(t),this.drawSelectionBackground(t),this.transform(t),this._setOpacity(t),this._setShadow(t,this),this.shouldCache()?(this.renderCache(),this.drawCacheOnCanvas(t)):(this._removeCacheCanvas(),this.dirty=!1,this.drawObject(t),this.objectCaching&&this.statefullCache&&this.saveState({propertySet:"cacheProperties"})),t.restore())},renderCache:function(t){t=t||{},this._cacheCanvas||this._createCacheCanvas(),this.isCacheDirty()&&(this.statefullCache&&this.saveState({propertySet:"cacheProperties"}),this.drawObject(this._cacheContext,t.forClipping),this.dirty=!1)},_removeCacheCanvas:function(){this._cacheCanvas=null,this.cacheWidth=0,this.cacheHeight=0},hasStroke:function(){return this.stroke&&"transparent"!==this.stroke&&0!==this.strokeWidth},hasFill:function(){return this.fill&&"transparent"!==this.fill},needsItsOwnCache:function(){return!("stroke"!==this.paintFirst||!this.hasFill()||!this.hasStroke()||"object"!=typeof this.shadow)||!!this.clipPath},shouldCache:function(){return this.ownCaching=this.needsItsOwnCache()||this.objectCaching&&(!this.group||!this.group.isOnACache()),this.ownCaching},willDrawShadow:function(){return!!this.shadow&&(0!==this.shadow.offsetX||0!==this.shadow.offsetY)},drawClipPathOnCache:function(t){var i=this.clipPath;if(t.save(),i.inverted?t.globalCompositeOperation="destination-out":t.globalCompositeOperation="destination-in",i.absolutePositioned){var r=e.util.invertTransform(this.calcTransformMatrix());t.transform(r[0],r[1],r[2],r[3],r[4],r[5])}i.transform(t),t.scale(1/i.zoomX,1/i.zoomY),t.drawImage(i._cacheCanvas,-i.cacheTranslationX,-i.cacheTranslationY),t.restore()},drawObject:function(t,e){var i=this.fill,r=this.stroke;e?(this.fill="black",this.stroke="",this._setClippingProperties(t)):this._renderBackground(t),this._render(t),this._drawClipPath(t),this.fill=i,this.stroke=r},_drawClipPath:function(t){var e=this.clipPath;e&&(e.canvas=this.canvas,e.shouldCache(),e._transformDone=!0,e.renderCache({forClipping:!0}),this.drawClipPathOnCache(t))},drawCacheOnCanvas:function(t){t.scale(1/this.zoomX,1/this.zoomY),t.drawImage(this._cacheCanvas,-this.cacheTranslationX,-this.cacheTranslationY)},isCacheDirty:function(t){if(this.isNotVisible())return!1;if(this._cacheCanvas&&!t&&this._updateCacheCanvas())return!0;if(this.dirty||this.clipPath&&this.clipPath.absolutePositioned||this.statefullCache&&this.hasStateChanged("cacheProperties")){if(this._cacheCanvas&&!t){var e=this.cacheWidth/this.zoomX,i=this.cacheHeight/this.zoomY;this._cacheContext.clearRect(-e/2,-i/2,e,i)}return!0}return!1},_renderBackground:function(t){if(this.backgroundColor){var e=this._getNonTransformedDimensions();t.fillStyle=this.backgroundColor,t.fillRect(-e.x/2,-e.y/2,e.x,e.y),this._removeShadow(t)}},_setOpacity:function(t){this.group&&!this.group._transformDone?t.globalAlpha=this.getObjectOpacity():t.globalAlpha*=this.opacity},_setStrokeStyles:function(t,e){var i=e.stroke;i&&(t.lineWidth=e.strokeWidth,t.lineCap=e.strokeLineCap,t.lineDashOffset=e.strokeDashOffset,t.lineJoin=e.strokeLineJoin,t.miterLimit=e.strokeMiterLimit,i.toLive?"percentage"===i.gradientUnits||i.gradientTransform||i.patternTransform?this._applyPatternForTransformedGradient(t,i):(t.strokeStyle=i.toLive(t,this),this._applyPatternGradientTransform(t,i)):t.strokeStyle=e.stroke)},_setFillStyles:function(t,e){var i=e.fill;i&&(i.toLive?(t.fillStyle=i.toLive(t,this),this._applyPatternGradientTransform(t,e.fill)):t.fillStyle=i)},_setClippingProperties:function(t){t.globalAlpha=1,t.strokeStyle="transparent",t.fillStyle="#000000"},_setLineDash:function(t,e){e&&0!==e.length&&(1&e.length&&e.push.apply(e,e),t.setLineDash(e))},_renderControls:function(t,i){var r,n,o,a=this.getViewportTransform(),h=this.calcTransformMatrix();n=void 0!==(i=i||{}).hasBorders?i.hasBorders:this.hasBorders,o=void 0!==i.hasControls?i.hasControls:this.hasControls,h=e.util.multiplyTransformMatrices(a,h),r=e.util.qrDecompose(h),t.save(),t.translate(r.translateX,r.translateY),t.lineWidth=1*this.borderScaleFactor,this.group||(t.globalAlpha=this.isMoving?this.borderOpacityWhenMoving:1),t.rotate(s(r.angle)),i.forActiveSelection||this.group?n&&this.drawBordersInGroup(t,r,i):n&&this.drawBorders(t,i),o&&this.drawControls(t,i),t.restore()},_setShadow:function(t){if(this.shadow){var i,r=this.shadow,n=this.canvas,o=n&&n.viewportTransform[0]||1,s=n&&n.viewportTransform[3]||1;i=r.nonScaling?{scaleX:1,scaleY:1}:this.getObjectScaling(),n&&n._isRetinaScaling()&&(o*=e.devicePixelRatio,s*=e.devicePixelRatio),t.shadowColor=r.color,t.shadowBlur=r.blur*e.browserShadowBlurConstant*(o+s)*(i.scaleX+i.scaleY)/4,t.shadowOffsetX=r.offsetX*o*i.scaleX,t.shadowOffsetY=r.offsetY*s*i.scaleY}},_removeShadow:function(t){this.shadow&&(t.shadowColor="",t.shadowBlur=t.shadowOffsetX=t.shadowOffsetY=0)},_applyPatternGradientTransform:function(t,e){if(!e||!e.toLive)return{offsetX:0,offsetY:0};var i=e.gradientTransform||e.patternTransform,r=-this.width/2+e.offsetX||0,n=-this.height/2+e.offsetY||0;return"percentage"===e.gradientUnits?t.transform(this.width,0,0,this.height,r,n):t.transform(1,0,0,1,r,n),i&&t.transform(i[0],i[1],i[2],i[3],i[4],i[5]),{offsetX:r,offsetY:n}},_renderPaintInOrder:function(t){"stroke"===this.paintFirst?(this._renderStroke(t),this._renderFill(t)):(this._renderFill(t),this._renderStroke(t))},_render:function(){},_renderFill:function(t){this.fill&&(t.save(),this._setFillStyles(t,this),"evenodd"===this.fillRule?t.fill("evenodd"):t.fill(),t.restore())},_renderStroke:function(t){if(this.stroke&&0!==this.strokeWidth){if(this.shadow&&!this.shadow.affectStroke&&this._removeShadow(t),t.save(),this.strokeUniform&&this.group){var e=this.getObjectScaling();t.scale(1/e.scaleX,1/e.scaleY)}else this.strokeUniform&&t.scale(1/this.scaleX,1/this.scaleY);this._setLineDash(t,this.strokeDashArray),this._setStrokeStyles(t,this),t.stroke(),t.restore()}},_applyPatternForTransformedGradient:function(t,i){var r,n=this._limitCacheSize(this._getCacheCanvasDimensions()),o=e.util.createCanvasElement(),s=this.canvas.getRetinaScaling(),a=n.x/this.scaleX/s,h=n.y/this.scaleY/s;o.width=a,o.height=h,(r=o.getContext("2d")).beginPath(),r.moveTo(0,0),r.lineTo(a,0),r.lineTo(a,h),r.lineTo(0,h),r.closePath(),r.translate(a/2,h/2),r.scale(n.zoomX/this.scaleX/s,n.zoomY/this.scaleY/s),this._applyPatternGradientTransform(r,i),r.fillStyle=i.toLive(t),r.fill(),t.translate(-this.width/2-this.strokeWidth/2,-this.height/2-this.strokeWidth/2),t.scale(s*this.scaleX/n.zoomX,s*this.scaleY/n.zoomY),t.strokeStyle=r.createPattern(o,"no-repeat")},_findCenterFromElement:function(){return{x:this.left+this.width/2,y:this.top+this.height/2}},_assignTransformMatrixProps:function(){if(this.transformMatrix){var t=e.util.qrDecompose(this.transformMatrix);this.flipX=!1,this.flipY=!1,this.set("scaleX",t.scaleX),this.set("scaleY",t.scaleY),this.angle=t.angle,this.skewX=t.skewX,this.skewY=0}},_removeTransformMatrix:function(t){var i=this._findCenterFromElement();this.transformMatrix&&(this._assignTransformMatrixProps(),i=e.util.transformPoint(i,this.transformMatrix)),this.transformMatrix=null,t&&(this.scaleX*=t.scaleX,this.scaleY*=t.scaleY,this.cropX=t.cropX,this.cropY=t.cropY,i.x+=t.offsetLeft,i.y+=t.offsetTop,this.width=t.width,this.height=t.height),this.setPositionByOrigin(i,"center","center")},clone:function(t,i){var r=this.toObject(i);this.constructor.fromObject?this.constructor.fromObject(r,t):e.Object._fromObject("Object",r,t)},cloneAsImage:function(t,i){var r=this.toCanvasElement(i);return t&&t(new e.Image(r)),this},toCanvasElement:function(t){t||(t={});var i=e.util,r=i.saveObjectTransform(this),n=this.group,o=this.shadow,s=Math.abs,a=(t.multiplier||1)*(t.enableRetinaScaling?e.devicePixelRatio:1);delete this.group,t.withoutTransform&&i.resetObjectTransform(this),t.withoutShadow&&(this.shadow=null);var h,c,l,u,f=e.util.createCanvasElement(),d=this.getBoundingRect(!0,!0),g=this.shadow,p={x:0,y:0};g&&(c=g.blur,h=g.nonScaling?{scaleX:1,scaleY:1}:this.getObjectScaling(),p.x=2*Math.round(s(g.offsetX)+c)*s(h.scaleX),p.y=2*Math.round(s(g.offsetY)+c)*s(h.scaleY)),l=d.width+p.x,u=d.height+p.y,f.width=Math.ceil(l),f.height=Math.ceil(u);var v=new e.StaticCanvas(f,{enableRetinaScaling:!1,renderOnAddRemove:!1,skipOffscreen:!1});"jpeg"===t.format&&(v.backgroundColor="#fff"),this.setPositionByOrigin(new e.Point(v.width/2,v.height/2),"center","center");var m=this.canvas;v.add(this);var y=v.toCanvasElement(a||1,t);return this.shadow=o,this.set("canvas",m),n&&(this.group=n),this.set(r).setCoords(),v._objects=[],v.dispose(),v=null,y},toDataURL:function(t){return t||(t={}),e.util.toDataURL(this.toCanvasElement(t),t.format||"png",t.quality||1)},isType:function(t){return this.type===t},complexity:function(){return 1},toJSON:function(t){return this.toObject(t)},rotate:function(t){var e=("center"!==this.originX||"center"!==this.originY)&&this.centeredRotation;return e&&this._setOriginToCenter(),this.set("angle",t),e&&this._resetOrigin(),this},centerH:function(){return this.canvas&&this.canvas.centerObjectH(this),this},viewportCenterH:function(){return this.canvas&&this.canvas.viewportCenterObjectH(this),this},centerV:function(){return this.canvas&&this.canvas.centerObjectV(this),this},viewportCenterV:function(){return this.canvas&&this.canvas.viewportCenterObjectV(this),this},center:function(){return this.canvas&&this.canvas.centerObject(this),this},viewportCenter:function(){return this.canvas&&this.canvas.viewportCenterObject(this),this},getLocalPointer:function(t,i){i=i||this.canvas.getPointer(t);var r=new e.Point(i.x,i.y),n=this._getLeftTopCoords();return this.angle&&(r=e.util.rotatePoint(r,n,s(-this.angle))),{x:r.x-n.x,y:r.y-n.y}},_setupCompositeOperation:function(t){this.globalCompositeOperation&&(t.globalCompositeOperation=this.globalCompositeOperation)}}),e.util.createAccessors&&e.util.createAccessors(e.Object),i(e.Object.prototype,e.Observable),e.Object.NUM_FRACTION_DIGITS=2,e.Object._fromObject=function(t,i,n,o){var s=e[t];i=r(i,!0),e.util.enlivenPatterns([i.fill,i.stroke],(function(t){void 0!==t[0]&&(i.fill=t[0]),void 0!==t[1]&&(i.stroke=t[1]),e.util.enlivenObjects([i.clipPath],(function(t){i.clipPath=t[0];var e=o?new s(i[o],i):new s(i);n&&n(e)}))}))},e.Object.__uid=0)}(t),function(){var t=T.util.degreesToRadians,e={left:-.5,center:0,right:.5},i={top:-.5,center:0,bottom:.5};T.util.object.extend(T.Object.prototype,{translateToGivenOrigin:function(t,r,n,o,s){var a,h,c,l=t.x,u=t.y;return"string"==typeof r?r=e[r]:r-=.5,"string"==typeof o?o=e[o]:o-=.5,"string"==typeof n?n=i[n]:n-=.5,"string"==typeof s?s=i[s]:s-=.5,h=s-n,((a=o-r)||h)&&(c=this._getTransformedDimensions(),l=t.x+a*c.x,u=t.y+h*c.y),new T.Point(l,u)},translateToCenterPoint:function(e,i,r){var n=this.translateToGivenOrigin(e,i,r,"center","center");return this.angle?T.util.rotatePoint(n,e,t(this.angle)):n},translateToOriginPoint:function(e,i,r){var n=this.translateToGivenOrigin(e,"center","center",i,r);return this.angle?T.util.rotatePoint(n,e,t(this.angle)):n},getCenterPoint:function(){var t=new T.Point(this.left,this.top);return this.translateToCenterPoint(t,this.originX,this.originY)},getPointByOrigin:function(t,e){var i=this.getCenterPoint();return this.translateToOriginPoint(i,t,e)},toLocalPoint:function(e,i,r){var n,o,s=this.getCenterPoint();return n=void 0!==i&&void 0!==r?this.translateToGivenOrigin(s,"center","center",i,r):new T.Point(this.left,this.top),o=new T.Point(e.x,e.y),this.angle&&(o=T.util.rotatePoint(o,s,-t(this.angle))),o.subtractEquals(n)},setPositionByOrigin:function(t,e,i){var r=this.translateToCenterPoint(t,e,i),n=this.translateToOriginPoint(r,this.originX,this.originY);this.set("left",n.x),this.set("top",n.y)},adjustPosition:function(i){var r,n,o=t(this.angle),s=this.getScaledWidth(),a=T.util.cos(o)*s,h=T.util.sin(o)*s;r="string"==typeof this.originX?e[this.originX]:this.originX-.5,n="string"==typeof i?e[i]:i-.5,this.left+=a*(n-r),this.top+=h*(n-r),this.setCoords(),this.originX=i},_setOriginToCenter:function(){this._originalOriginX=this.originX,this._originalOriginY=this.originY;var t=this.getCenterPoint();this.originX="center",this.originY="center",this.left=t.x,this.top=t.y},_resetOrigin:function(){var t=this.translateToOriginPoint(this.getCenterPoint(),this._originalOriginX,this._originalOriginY);this.originX=this._originalOriginX,this.originY=this._originalOriginY,this.left=t.x,this.top=t.y,this._originalOriginX=null,this._originalOriginY=null},_getLeftTopCoords:function(){return this.translateToOriginPoint(this.getCenterPoint(),"left","top")}})}(),function(){var t=T.util,e=t.degreesToRadians,i=t.multiplyTransformMatrices,r=t.transformPoint;t.object.extend(T.Object.prototype,{oCoords:null,aCoords:null,lineCoords:null,ownMatrixCache:null,matrixCache:null,controls:{},_getCoords:function(t,e){return e?t?this.calcACoords():this.calcLineCoords():(this.aCoords&&this.lineCoords||this.setCoords(!0),t?this.aCoords:this.lineCoords)},getCoords:function(t,e){return i=this._getCoords(t,e),[new T.Point(i.tl.x,i.tl.y),new T.Point(i.tr.x,i.tr.y),new T.Point(i.br.x,i.br.y),new T.Point(i.bl.x,i.bl.y)];var i},intersectsWithRect:function(t,e,i,r){var n=this.getCoords(i,r);return"Intersection"===T.Intersection.intersectPolygonRectangle(n,t,e).status},intersectsWithObject:function(t,e,i){return"Intersection"===T.Intersection.intersectPolygonPolygon(this.getCoords(e,i),t.getCoords(e,i)).status||t.isContainedWithinObject(this,e,i)||this.isContainedWithinObject(t,e,i)},isContainedWithinObject:function(t,e,i){for(var r=this.getCoords(e,i),n=e?t.aCoords:t.lineCoords,o=0,s=t._getImageLines(n);o<4;o++)if(!t.containsPoint(r[o],s))return!1;return!0},isContainedWithinRect:function(t,e,i,r){var n=this.getBoundingRect(i,r);return n.left>=t.x&&n.left+n.width<=e.x&&n.top>=t.y&&n.top+n.height<=e.y},containsPoint:function(t,e,i,r){var n=this._getCoords(i,r),o=(e=e||this._getImageLines(n),this._findCrossPoints(t,e));return 0!==o&&o%2==1},isOnScreen:function(t){if(!this.canvas)return!1;var e=this.canvas.vptCoords.tl,i=this.canvas.vptCoords.br;return!!this.getCoords(!0,t).some((function(t){return t.x<=i.x&&t.x>=e.x&&t.y<=i.y&&t.y>=e.y}))||(!!this.intersectsWithRect(e,i,!0,t)||this._containsCenterOfCanvas(e,i,t))},_containsCenterOfCanvas:function(t,e,i){var r={x:(t.x+e.x)/2,y:(t.y+e.y)/2};return!!this.containsPoint(r,null,!0,i)},isPartiallyOnScreen:function(t){if(!this.canvas)return!1;var e=this.canvas.vptCoords.tl,i=this.canvas.vptCoords.br;return!!this.intersectsWithRect(e,i,!0,t)||this.getCoords(!0,t).every((function(t){return(t.x>=i.x||t.x<=e.x)&&(t.y>=i.y||t.y<=e.y)}))&&this._containsCenterOfCanvas(e,i,t)},_getImageLines:function(t){return{topline:{o:t.tl,d:t.tr},rightline:{o:t.tr,d:t.br},bottomline:{o:t.br,d:t.bl},leftline:{o:t.bl,d:t.tl}}},_findCrossPoints:function(t,e){var i,r,n,o=0;for(var s in e)if(!((n=e[s]).o.y=t.y&&n.d.y>=t.y||(n.o.x===n.d.x&&n.o.x>=t.x?r=n.o.x:(0,i=(n.d.y-n.o.y)/(n.d.x-n.o.x),r=-(t.y-0*t.x-(n.o.y-i*n.o.x))/(0-i)),r>=t.x&&(o+=1),2!==o)))break;return o},getBoundingRect:function(e,i){var r=this.getCoords(e,i);return t.makeBoundingBoxFromPoints(r)},getScaledWidth:function(){return this._getTransformedDimensions().x},getScaledHeight:function(){return this._getTransformedDimensions().y},_constrainScale:function(t){return Math.abs(t)\n')}},toSVG:function(t){return this._createBaseSVGMarkup(this._toSVG(t),{reviver:t})},toClipPathSVG:function(t){return"\t"+this._createBaseClipPathSVGMarkup(this._toSVG(t),{reviver:t})},_createBaseClipPathSVGMarkup:function(t,e){var i=(e=e||{}).reviver,r=e.additionalTransform||"",n=[this.getSvgTransform(!0,r),this.getSvgCommons()].join(""),o=t.indexOf("COMMON_PARTS");return t[o]=n,i?i(t.join("")):t.join("")},_createBaseSVGMarkup:function(t,e){var i,r,n=(e=e||{}).noStyle,o=e.reviver,s=n?"":'style="'+this.getSvgStyles()+'" ',a=e.withShadow?'style="'+this.getSvgFilter()+'" ':"",h=this.clipPath,c=this.strokeUniform?'vector-effect="non-scaling-stroke" ':"",l=h&&h.absolutePositioned,u=this.stroke,f=this.fill,d=this.shadow,g=[],p=t.indexOf("COMMON_PARTS"),v=e.additionalTransform;return h&&(h.clipPathId="CLIPPATH_"+T.Object.__uid++,r='\n'+h.toClipPathSVG(o)+"\n"),l&&g.push("\n"),g.push("\n"),i=[s,c,n?"":this.addPaintOrder()," ",v?'transform="'+v+'" ':""].join(""),t[p]=i,f&&f.toLive&&g.push(f.toSVG(this)),u&&u.toLive&&g.push(u.toSVG(this)),d&&g.push(d.toSVG(this)),h&&g.push(r),g.push(t.join("")),g.push("\n"),l&&g.push("\n"),o?o(g.join("")):g.join("")},addPaintOrder:function(){return"fill"!==this.paintFirst?' paint-order="'+this.paintFirst+'" ':""}})}(),function(){var t=T.util.object.extend,e="stateProperties";function i(e,i,r){var n={};r.forEach((function(t){n[t]=e[t]})),t(e[i],n,!0)}function r(t,e,i){if(t===e)return!0;if(Array.isArray(t)){if(!Array.isArray(e)||t.length!==e.length)return!1;for(var n=0,o=t.length;n=0;h--)if(n=a[h],this.isControlVisible(n)&&(r=this._getImageLines(e?this.oCoords[n].touchCorner:this.oCoords[n].corner),0!==(i=this._findCrossPoints({x:o,y:s},r))&&i%2==1))return this.__corner=n,n;return!1},forEachControl:function(t){for(var e in this.controls)t(this.controls[e],e,this)},_setCornerCoords:function(){var t=this.oCoords;for(var e in t){var i=this.controls[e];t[e].corner=i.calcCornerCoords(this.angle,this.cornerSize,t[e].x,t[e].y,!1),t[e].touchCorner=i.calcCornerCoords(this.angle,this.touchCornerSize,t[e].x,t[e].y,!0)}},drawSelectionBackground:function(e){if(!this.selectionBackgroundColor||this.canvas&&!this.canvas.interactive||this.canvas&&this.canvas._activeObject!==this)return this;e.save();var i=this.getCenterPoint(),r=this._calculateCurrentDimensions(),n=this.canvas.viewportTransform;return e.translate(i.x,i.y),e.scale(1/n[0],1/n[3]),e.rotate(t(this.angle)),e.fillStyle=this.selectionBackgroundColor,e.fillRect(-r.x/2,-r.y/2,r.x,r.y),e.restore(),this},drawBorders:function(t,e){e=e||{};var i=this._calculateCurrentDimensions(),r=this.borderScaleFactor,n=i.x+r,o=i.y+r,s=void 0!==e.hasControls?e.hasControls:this.hasControls,a=!1;return t.save(),t.strokeStyle=e.borderColor||this.borderColor,this._setLineDash(t,e.borderDashArray||this.borderDashArray),t.strokeRect(-n/2,-o/2,n,o),s&&(t.beginPath(),this.forEachControl((function(e,i,r){e.withConnection&&e.getVisibility(r,i)&&(a=!0,t.moveTo(e.x*n,e.y*o),t.lineTo(e.x*n+e.offsetX,e.y*o+e.offsetY))})),a&&t.stroke()),t.restore(),this},drawBordersInGroup:function(t,e,i){i=i||{};var r=T.util.sizeAfterTransform(this.width,this.height,e),n=this.strokeWidth,o=this.strokeUniform,s=this.borderScaleFactor,a=r.x+n*(o?this.canvas.getZoom():e.scaleX)+s,h=r.y+n*(o?this.canvas.getZoom():e.scaleY)+s;return t.save(),this._setLineDash(t,i.borderDashArray||this.borderDashArray),t.strokeStyle=i.borderColor||this.borderColor,t.strokeRect(-a/2,-h/2,a,h),t.restore(),this},drawControls:function(t,e){e=e||{},t.save();var i,r,n=this.canvas.getRetinaScaling();return t.setTransform(n,0,0,n,0,0),t.strokeStyle=t.fillStyle=e.cornerColor||this.cornerColor,this.transparentCorners||(t.strokeStyle=e.cornerStrokeColor||this.cornerStrokeColor),this._setLineDash(t,e.cornerDashArray||this.cornerDashArray),this.setCoords(),this.group&&(i=this.group.calcTransformMatrix()),this.forEachControl((function(n,o,s){r=s.oCoords[o],n.getVisibility(s,o)&&(i&&(r=T.util.transformPoint(r,i)),n.render(t,r.x,r.y,e,s))})),t.restore(),this},isControlVisible:function(t){return this.controls[t]&&this.controls[t].getVisibility(this,t)},setControlVisible:function(t,e){return this._controlsVisibility||(this._controlsVisibility={}),this._controlsVisibility[t]=e,this},setControlsVisibility:function(t){for(var e in t||(t={}),t)this.setControlVisible(e,t[e]);return this},onDeselect:function(){},onSelect:function(){}})}(),T.util.object.extend(T.StaticCanvas.prototype,{FX_DURATION:500,fxCenterObjectH:function(t,e){var i=function(){},r=(e=e||{}).onComplete||i,n=e.onChange||i,o=this;return T.util.animate({startValue:t.left,endValue:this.getCenter().left,duration:this.FX_DURATION,onChange:function(e){t.set("left",e),o.requestRenderAll(),n()},onComplete:function(){t.setCoords(),r()}}),this},fxCenterObjectV:function(t,e){var i=function(){},r=(e=e||{}).onComplete||i,n=e.onChange||i,o=this;return T.util.animate({startValue:t.top,endValue:this.getCenter().top,duration:this.FX_DURATION,onChange:function(e){t.set("top",e),o.requestRenderAll(),n()},onComplete:function(){t.setCoords(),r()}}),this},fxRemove:function(t,e){var i=function(){},r=(e=e||{}).onComplete||i,n=e.onChange||i,o=this;return T.util.animate({startValue:t.opacity,endValue:0,duration:this.FX_DURATION,onChange:function(e){t.set("opacity",e),o.requestRenderAll(),n()},onComplete:function(){o.remove(t),r()}}),this}}),T.util.object.extend(T.Object.prototype,{animate:function(){if(arguments[0]&&"object"==typeof arguments[0]){var t,e,i=[];for(t in arguments[0])i.push(t);for(var r=0,n=i.length;r-1||n&&o.colorProperties.indexOf(n[1])>-1,a=n?this.get(n[0])[n[1]]:this.get(t);"from"in i||(i.from=a),s||(e=~e.indexOf("=")?a+parseFloat(e.replace("=","")):parseFloat(e));var h={startValue:i.from,endValue:e,byValue:i.by,easing:i.easing,duration:i.duration,abort:i.abort&&function(t,e,r){return i.abort.call(o,t,e,r)},onChange:function(e,s,a){n?o[n[0]][n[1]]=e:o.set(t,e),r||i.onChange&&i.onChange(e,s,a)},onComplete:function(t,e,n){r||(o.setCoords(),i.onComplete&&i.onComplete(t,e,n))}};return s?T.util.animateColor(h.startValue,h.endValue,h.duration,h):T.util.animate(h)}}),function(t){var e=t.fabric||(t.fabric={}),i=e.util.object.extend,r=e.util.object.clone,n={x1:1,x2:1,y1:1,y2:1};function o(t,e){var i=t.origin,r=t.axis1,n=t.axis2,o=t.dimension,s=e.nearest,a=e.center,h=e.farthest;return function(){switch(this.get(i)){case s:return Math.min(this.get(r),this.get(n));case a:return Math.min(this.get(r),this.get(n))+.5*this.get(o);case h:return Math.max(this.get(r),this.get(n))}}}e.Line?e.warn("fabric.Line is already defined"):(e.Line=e.util.createClass(e.Object,{type:"line",x1:0,y1:0,x2:0,y2:0,cacheProperties:e.Object.prototype.cacheProperties.concat("x1","x2","y1","y2"),initialize:function(t,e){t||(t=[0,0,0,0]),this.callSuper("initialize",e),this.set("x1",t[0]),this.set("y1",t[1]),this.set("x2",t[2]),this.set("y2",t[3]),this._setWidthHeight(e)},_setWidthHeight:function(t){t||(t={}),this.width=Math.abs(this.x2-this.x1),this.height=Math.abs(this.y2-this.y1),this.left="left"in t?t.left:this._getLeftToOriginX(),this.top="top"in t?t.top:this._getTopToOriginY()},_set:function(t,e){return this.callSuper("_set",t,e),void 0!==n[t]&&this._setWidthHeight(),this},_getLeftToOriginX:o({origin:"originX",axis1:"x1",axis2:"x2",dimension:"width"},{nearest:"left",center:"center",farthest:"right"}),_getTopToOriginY:o({origin:"originY",axis1:"y1",axis2:"y2",dimension:"height"},{nearest:"top",center:"center",farthest:"bottom"}),_render:function(t){t.beginPath();var e=this.calcLinePoints();t.moveTo(e.x1,e.y1),t.lineTo(e.x2,e.y2),t.lineWidth=this.strokeWidth;var i=t.strokeStyle;t.strokeStyle=this.stroke||t.fillStyle,this.stroke&&this._renderStroke(t),t.strokeStyle=i},_findCenterFromElement:function(){return{x:(this.x1+this.x2)/2,y:(this.y1+this.y2)/2}},toObject:function(t){return i(this.callSuper("toObject",t),this.calcLinePoints())},_getNonTransformedDimensions:function(){var t=this.callSuper("_getNonTransformedDimensions");return"butt"===this.strokeLineCap&&(0===this.width&&(t.y-=this.strokeWidth),0===this.height&&(t.x-=this.strokeWidth)),t},calcLinePoints:function(){var t=this.x1<=this.x2?-1:1,e=this.y1<=this.y2?-1:1,i=t*this.width*.5,r=e*this.height*.5;return{x1:i,x2:t*this.width*-.5,y1:r,y2:e*this.height*-.5}},_toSVG:function(){var t=this.calcLinePoints();return["\n']}}),e.Line.ATTRIBUTE_NAMES=e.SHARED_ATTRIBUTES.concat("x1 y1 x2 y2".split(" ")),e.Line.fromElement=function(t,r,n){n=n||{};var o=e.parseAttributes(t,e.Line.ATTRIBUTE_NAMES),s=[o.x1||0,o.y1||0,o.x2||0,o.y2||0];r(new e.Line(s,i(o,n)))},e.Line.fromObject=function(t,i){var n=r(t,!0);n.points=[t.x1,t.y1,t.x2,t.y2],e.Object._fromObject("Line",n,(function(t){delete t.points,i&&i(t)}),"points")})}(t),function(t){var e=t.fabric||(t.fabric={}),i=Math.PI;e.Circle?e.warn("fabric.Circle is already defined."):(e.Circle=e.util.createClass(e.Object,{type:"circle",radius:0,startAngle:0,endAngle:2*i,cacheProperties:e.Object.prototype.cacheProperties.concat("radius","startAngle","endAngle"),_set:function(t,e){return this.callSuper("_set",t,e),"radius"===t&&this.setRadius(e),this},toObject:function(t){return this.callSuper("toObject",["radius","startAngle","endAngle"].concat(t))},_toSVG:function(){var t,r=(this.endAngle-this.startAngle)%(2*i);if(0===r)t=["\n'];else{var n=e.util.cos(this.startAngle)*this.radius,o=e.util.sin(this.startAngle)*this.radius,s=e.util.cos(this.endAngle)*this.radius,a=e.util.sin(this.endAngle)*this.radius,h=r>i?"1":"0";t=['\n"]}return t},_render:function(t){t.beginPath(),t.arc(0,0,this.radius,this.startAngle,this.endAngle,!1),this._renderPaintInOrder(t)},getRadiusX:function(){return this.get("radius")*this.get("scaleX")},getRadiusY:function(){return this.get("radius")*this.get("scaleY")},setRadius:function(t){return this.radius=t,this.set("width",2*t).set("height",2*t)}}),e.Circle.ATTRIBUTE_NAMES=e.SHARED_ATTRIBUTES.concat("cx cy r".split(" ")),e.Circle.fromElement=function(t,i){var r,n=e.parseAttributes(t,e.Circle.ATTRIBUTE_NAMES);if(!("radius"in(r=n)&&r.radius>=0))throw new Error("value of `r` attribute is required and can not be negative");n.left=(n.left||0)-n.radius,n.top=(n.top||0)-n.radius,i(new e.Circle(n))},e.Circle.fromObject=function(t,i){e.Object._fromObject("Circle",t,i)})}(t),function(t){var e=t.fabric||(t.fabric={});e.Triangle?e.warn("fabric.Triangle is already defined"):(e.Triangle=e.util.createClass(e.Object,{type:"triangle",width:100,height:100,_render:function(t){var e=this.width/2,i=this.height/2;t.beginPath(),t.moveTo(-e,i),t.lineTo(0,-i),t.lineTo(e,i),t.closePath(),this._renderPaintInOrder(t)},_toSVG:function(){var t=this.width/2,e=this.height/2;return["']}}),e.Triangle.fromObject=function(t,i){return e.Object._fromObject("Triangle",t,i)})}(t),function(t){var e=t.fabric||(t.fabric={}),i=2*Math.PI;e.Ellipse?e.warn("fabric.Ellipse is already defined."):(e.Ellipse=e.util.createClass(e.Object,{type:"ellipse",rx:0,ry:0,cacheProperties:e.Object.prototype.cacheProperties.concat("rx","ry"),initialize:function(t){this.callSuper("initialize",t),this.set("rx",t&&t.rx||0),this.set("ry",t&&t.ry||0)},_set:function(t,e){switch(this.callSuper("_set",t,e),t){case"rx":this.rx=e,this.set("width",2*e);break;case"ry":this.ry=e,this.set("height",2*e)}return this},getRx:function(){return this.get("rx")*this.get("scaleX")},getRy:function(){return this.get("ry")*this.get("scaleY")},toObject:function(t){return this.callSuper("toObject",["rx","ry"].concat(t))},_toSVG:function(){return["\n']},_render:function(t){t.beginPath(),t.save(),t.transform(1,0,0,this.ry/this.rx,0,0),t.arc(0,0,this.rx,0,i,!1),t.restore(),this._renderPaintInOrder(t)}}),e.Ellipse.ATTRIBUTE_NAMES=e.SHARED_ATTRIBUTES.concat("cx cy rx ry".split(" ")),e.Ellipse.fromElement=function(t,i){var r=e.parseAttributes(t,e.Ellipse.ATTRIBUTE_NAMES);r.left=(r.left||0)-r.rx,r.top=(r.top||0)-r.ry,i(new e.Ellipse(r))},e.Ellipse.fromObject=function(t,i){e.Object._fromObject("Ellipse",t,i)})}(t),function(t){var e=t.fabric||(t.fabric={}),i=e.util.object.extend;e.Rect?e.warn("fabric.Rect is already defined"):(e.Rect=e.util.createClass(e.Object,{stateProperties:e.Object.prototype.stateProperties.concat("rx","ry"),type:"rect",rx:0,ry:0,cacheProperties:e.Object.prototype.cacheProperties.concat("rx","ry"),initialize:function(t){this.callSuper("initialize",t),this._initRxRy()},_initRxRy:function(){this.rx&&!this.ry?this.ry=this.rx:this.ry&&!this.rx&&(this.rx=this.ry)},_render:function(t){var e=this.rx?Math.min(this.rx,this.width/2):0,i=this.ry?Math.min(this.ry,this.height/2):0,r=this.width,n=this.height,o=-this.width/2,s=-this.height/2,a=0!==e||0!==i,h=.4477152502;t.beginPath(),t.moveTo(o+e,s),t.lineTo(o+r-e,s),a&&t.bezierCurveTo(o+r-h*e,s,o+r,s+h*i,o+r,s+i),t.lineTo(o+r,s+n-i),a&&t.bezierCurveTo(o+r,s+n-h*i,o+r-h*e,s+n,o+r-e,s+n),t.lineTo(o+e,s+n),a&&t.bezierCurveTo(o+h*e,s+n,o,s+n-h*i,o,s+n-i),t.lineTo(o,s+i),a&&t.bezierCurveTo(o,s+h*i,o+h*e,s,o+e,s),t.closePath(),this._renderPaintInOrder(t)},toObject:function(t){return this.callSuper("toObject",["rx","ry"].concat(t))},_toSVG:function(){return["\n']}}),e.Rect.ATTRIBUTE_NAMES=e.SHARED_ATTRIBUTES.concat("x y rx ry width height".split(" ")),e.Rect.fromElement=function(t,r,n){if(!t)return r(null);n=n||{};var o=e.parseAttributes(t,e.Rect.ATTRIBUTE_NAMES);o.left=o.left||0,o.top=o.top||0,o.height=o.height||0,o.width=o.width||0;var s=new e.Rect(i(n?e.util.object.clone(n):{},o));s.visible=s.visible&&s.width>0&&s.height>0,r(s)},e.Rect.fromObject=function(t,i){return e.Object._fromObject("Rect",t,i)})}(t),function(t){var e=t.fabric||(t.fabric={}),i=e.util.object.extend,r=e.util.array.min,n=e.util.array.max,o=e.util.toFixed;e.Polyline?e.warn("fabric.Polyline is already defined"):(e.Polyline=e.util.createClass(e.Object,{type:"polyline",points:null,cacheProperties:e.Object.prototype.cacheProperties.concat("points"),initialize:function(t,e){e=e||{},this.points=t||[],this.callSuper("initialize",e),this._setPositionDimensions(e)},_setPositionDimensions:function(t){var e,i=this._calcDimensions(t);this.width=i.width,this.height=i.height,t.fromSVG||(e=this.translateToGivenOrigin({x:i.left-this.strokeWidth/2,y:i.top-this.strokeWidth/2},"left","top",this.originX,this.originY)),void 0===t.left&&(this.left=t.fromSVG?i.left:e.x),void 0===t.top&&(this.top=t.fromSVG?i.top:e.y),this.pathOffset={x:i.left+this.width/2,y:i.top+this.height/2}},_calcDimensions:function(){var t=this.points,e=r(t,"x")||0,i=r(t,"y")||0;return{left:e,top:i,width:(n(t,"x")||0)-e,height:(n(t,"y")||0)-i}},toObject:function(t){return i(this.callSuper("toObject",t),{points:this.points.concat()})},_toSVG:function(){for(var t=[],i=this.pathOffset.x,r=this.pathOffset.y,n=e.Object.NUM_FRACTION_DIGITS,s=0,a=this.points.length;s\n']},commonRender:function(t){var e,i=this.points.length,r=this.pathOffset.x,n=this.pathOffset.y;if(!i||isNaN(this.points[i-1].y))return!1;t.beginPath(),t.moveTo(this.points[0].x-r,this.points[0].y-n);for(var o=0;o"},toObject:function(t){return n(this.callSuper("toObject",t),{path:this.path.map((function(t){return t.slice()}))})},toDatalessObject:function(t){var e=this.toObject(["sourcePath"].concat(t));return e.sourcePath&&delete e.path,e},_toSVG:function(){return["\n"]},_getOffsetTransform:function(){var t=e.Object.NUM_FRACTION_DIGITS;return" translate("+s(-this.pathOffset.x,t)+", "+s(-this.pathOffset.y,t)+")"},toClipPathSVG:function(t){var e=this._getOffsetTransform();return"\t"+this._createBaseClipPathSVGMarkup(this._toSVG(),{reviver:t,additionalTransform:e})},toSVG:function(t){var e=this._getOffsetTransform();return this._createBaseSVGMarkup(this._toSVG(),{reviver:t,additionalTransform:e})},complexity:function(){return this.path.length},_calcDimensions:function(){for(var t,n,o=[],s=[],a=0,h=0,c=0,l=0,u=0,f=this.path.length;u"},addWithUpdate:function(t){var i=!!this.group;return this._restoreObjectsState(),e.util.resetObjectTransform(this),t&&(i&&e.util.removeTransformFromObject(t,this.group.calcTransformMatrix()),this._objects.push(t),t.group=this,t._set("canvas",this.canvas)),this._calcBounds(),this._updateObjectsCoords(),this.dirty=!0,i?this.group.addWithUpdate():this.setCoords(),this},removeWithUpdate:function(t){return this._restoreObjectsState(),e.util.resetObjectTransform(this),this.remove(t),this._calcBounds(),this._updateObjectsCoords(),this.setCoords(),this.dirty=!0,this},_onObjectAdded:function(t){this.dirty=!0,t.group=this,t._set("canvas",this.canvas)},_onObjectRemoved:function(t){this.dirty=!0,delete t.group},_set:function(t,i){var r=this._objects.length;if(this.useSetOnGroup)for(;r--;)this._objects[r].setOnGroup(t,i);if("canvas"===t)for(;r--;)this._objects[r]._set(t,i);e.Object.prototype._set.call(this,t,i)},toObject:function(t){var i=this.includeDefaultValues,r=this._objects.filter((function(t){return!t.excludeFromExport})).map((function(e){var r=e.includeDefaultValues;e.includeDefaultValues=i;var n=e.toObject(t);return e.includeDefaultValues=r,n})),n=e.Object.prototype.toObject.call(this,t);return n.objects=r,n},toDatalessObject:function(t){var i,r=this.sourcePath;if(r)i=r;else{var n=this.includeDefaultValues;i=this._objects.map((function(e){var i=e.includeDefaultValues;e.includeDefaultValues=n;var r=e.toDatalessObject(t);return e.includeDefaultValues=i,r}))}var o=e.Object.prototype.toDatalessObject.call(this,t);return o.objects=i,o},render:function(t){this._transformDone=!0,this.callSuper("render",t),this._transformDone=!1},shouldCache:function(){var t=e.Object.prototype.shouldCache.call(this);if(t)for(var i=0,r=this._objects.length;i\n"],i=0,r=this._objects.length;i\n"),e},getSvgStyles:function(){var t=void 0!==this.opacity&&1!==this.opacity?"opacity: "+this.opacity+";":"",e=this.visible?"":" visibility: hidden;";return[t,this.getSvgFilter(),e].join("")},toClipPathSVG:function(t){for(var e=[],i=0,r=this._objects.length;i"},shouldCache:function(){return!1},isOnACache:function(){return!1},_renderControls:function(t,e,i){t.save(),t.globalAlpha=this.isMoving?this.borderOpacityWhenMoving:1,this.callSuper("_renderControls",t,e),void 0===(i=i||{}).hasControls&&(i.hasControls=!1),i.forActiveSelection=!0;for(var r=0,n=this._objects.length;r\n','\t\n',"\n"),s=' clip-path="url(#imageCrop_'+h+')" '}if(this.imageSmoothing||(a='" image-rendering="optimizeSpeed'),i.push("\t\n"),this.stroke||this.strokeDashArray){var c=this.fill;this.fill=null,t=["\t\n'],this.fill=c}return e="fill"!==this.paintFirst?e.concat(t,i):e.concat(i,t)},getSrc:function(t){var e=t?this._element:this._originalElement;return e?e.toDataURL?e.toDataURL():this.srcFromAttribute?e.getAttribute("src"):e.src:this.src||""},setSrc:function(t,e,i){return T.util.loadImage(t,(function(t,r){this.setElement(t,i),this._setWidthHeight(),e&&e(this,r)}),this,i&&i.crossOrigin),this},toString:function(){return'#'},applyResizeFilters:function(){var t=this.resizeFilter,e=this.minimumScaleTrigger,i=this.getTotalObjectScaling(),r=i.scaleX,n=i.scaleY,o=this._filteredEl||this._originalElement;if(this.group&&this.set("dirty",!0),!t||r>e&&n>e)return this._element=o,this._filterScalingX=1,this._filterScalingY=1,this._lastScaleX=r,void(this._lastScaleY=n);T.filterBackend||(T.filterBackend=T.initFilterBackend());var s=T.util.createCanvasElement(),a=this._filteredEl?this.cacheKey+"_filtered":this.cacheKey,h=o.width,c=o.height;s.width=h,s.height=c,this._element=s,this._lastScaleX=t.scaleX=r,this._lastScaleY=t.scaleY=n,T.filterBackend.applyFilters([t],o,h,c,this._element,a),this._filterScalingX=s.width/this._originalElement.width,this._filterScalingY=s.height/this._originalElement.height},applyFilters:function(t){if(t=(t=t||this.filters||[]).filter((function(t){return t&&!t.isNeutralState()})),this.set("dirty",!0),this.removeTexture(this.cacheKey+"_filtered"),0===t.length)return this._element=this._originalElement,this._filteredEl=null,this._filterScalingX=1,this._filterScalingY=1,this;var e=this._originalElement,i=e.naturalWidth||e.width,r=e.naturalHeight||e.height;if(this._element===this._originalElement){var n=T.util.createCanvasElement();n.width=i,n.height=r,this._element=n,this._filteredEl=n}else this._element=this._filteredEl,this._filteredEl.getContext("2d").clearRect(0,0,i,r),this._lastScaleX=1,this._lastScaleY=1;return T.filterBackend||(T.filterBackend=T.initFilterBackend()),T.filterBackend.applyFilters(t,this._originalElement,i,r,this._element,this.cacheKey),this._originalElement.width===this._element.width&&this._originalElement.height===this._element.height||(this._filterScalingX=this._element.width/this._originalElement.width,this._filterScalingY=this._element.height/this._originalElement.height),this},_render:function(t){T.util.setImageSmoothing(t,this.imageSmoothing),!0!==this.isMoving&&this.resizeFilter&&this._needsResize()&&this.applyResizeFilters(),this._stroke(t),this._renderPaintInOrder(t)},drawCacheOnCanvas:function(t){T.util.setImageSmoothing(t,this.imageSmoothing),T.Object.prototype.drawCacheOnCanvas.call(this,t)},shouldCache:function(){return this.needsItsOwnCache()},_renderFill:function(t){var e=this._element;if(e){var i=this._filterScalingX,r=this._filterScalingY,n=this.width,o=this.height,s=Math.min,a=Math.max,h=a(this.cropX,0),c=a(this.cropY,0),l=e.naturalWidth||e.width,u=e.naturalHeight||e.height,f=h*i,d=c*r,g=s(n*i,l-f),p=s(o*r,u-d),v=-n/2,m=-o/2,y=s(n,l/i-h),_=s(o,u/r-c);e&&t.drawImage(e,f,d,g,p,v,m,y,_)}},_needsResize:function(){var t=this.getTotalObjectScaling();return t.scaleX!==this._lastScaleX||t.scaleY!==this._lastScaleY},_resetWidthHeight:function(){this.set(this.getOriginalSize())},_initElement:function(t,e){this.setElement(T.util.getById(t),e),T.util.addClass(this.getElement(),T.Image.CSS_CANVAS)},_initConfig:function(t){t||(t={}),this.setOptions(t),this._setWidthHeight(t)},_initFilters:function(t,e){t&&t.length?T.util.enlivenObjects(t,(function(t){e&&e(t)}),"fabric.Image.filters"):e&&e()},_setWidthHeight:function(t){t||(t={});var e=this.getElement();this.width=t.width||e.naturalWidth||e.width||0,this.height=t.height||e.naturalHeight||e.height||0},parsePreserveAspectRatioAttribute:function(){var t,e=T.util.parsePreserveAspectRatioAttribute(this.preserveAspectRatio||""),i=this._element.width,r=this._element.height,n=1,o=1,s=0,a=0,h=0,c=0,l=this.width,u=this.height,f={width:l,height:u};return!e||"none"===e.alignX&&"none"===e.alignY?(n=l/i,o=u/r):("meet"===e.meetOrSlice&&(t=(l-i*(n=o=T.util.findScaleToFit(this._element,f)))/2,"Min"===e.alignX&&(s=-t),"Max"===e.alignX&&(s=t),t=(u-r*o)/2,"Min"===e.alignY&&(a=-t),"Max"===e.alignY&&(a=t)),"slice"===e.meetOrSlice&&(t=i-l/(n=o=T.util.findScaleToCover(this._element,f)),"Mid"===e.alignX&&(h=t/2),"Max"===e.alignX&&(h=t),t=r-u/o,"Mid"===e.alignY&&(c=t/2),"Max"===e.alignY&&(c=t),i=l/n,r=u/o)),{width:i,height:r,scaleX:n,scaleY:o,offsetLeft:s,offsetTop:a,cropX:h,cropY:c}}}),T.Image.CSS_CANVAS="canvas-img",T.Image.prototype.getSvgSrc=T.Image.prototype.getSrc,T.Image.fromObject=function(t,e){var i=T.util.object.clone(t);T.util.loadImage(i.src,(function(t,r){r?e&&e(null,!0):T.Image.prototype._initFilters.call(i,i.filters,(function(r){i.filters=r||[],T.Image.prototype._initFilters.call(i,[i.resizeFilter],(function(r){i.resizeFilter=r[0],T.util.enlivenObjects([i.clipPath],(function(r){i.clipPath=r[0];var n=new T.Image(t,i);e(n,!1)}))}))}))}),null,i.crossOrigin)},T.Image.fromURL=function(t,e,i){T.util.loadImage(t,(function(t,r){e&&e(new T.Image(t,i),r)}),null,i&&i.crossOrigin)},T.Image.ATTRIBUTE_NAMES=T.SHARED_ATTRIBUTES.concat("x y width height preserveAspectRatio xlink:href crossOrigin image-rendering".split(" ")),T.Image.fromElement=function(t,i,r){var n=T.parseAttributes(t,T.Image.ATTRIBUTE_NAMES);T.Image.fromURL(n["xlink:href"],i,e(r?T.util.object.clone(r):{},n))})}(t),T.util.object.extend(T.Object.prototype,{_getAngleValueForStraighten:function(){var t=this.angle%360;return t>0?90*Math.round((t-1)/90):90*Math.round(t/90)},straighten:function(){return this.rotate(this._getAngleValueForStraighten()),this},fxStraighten:function(t){var e=function(){},i=(t=t||{}).onComplete||e,r=t.onChange||e,n=this;return T.util.animate({startValue:this.get("angle"),endValue:this._getAngleValueForStraighten(),duration:this.FX_DURATION,onChange:function(t){n.rotate(t),r()},onComplete:function(){n.setCoords(),i()}}),this}}),T.util.object.extend(T.StaticCanvas.prototype,{straightenObject:function(t){return t.straighten(),this.requestRenderAll(),this},fxStraightenObject:function(t){return t.fxStraighten({onChange:this.requestRenderAllBound}),this}}),function(){function t(t,e){var i="precision "+e+" float;\nvoid main(){}",r=t.createShader(t.FRAGMENT_SHADER);return t.shaderSource(r,i),t.compileShader(r),!!t.getShaderParameter(r,t.COMPILE_STATUS)}function e(t){t&&t.tileSize&&(this.tileSize=t.tileSize),this.setupGLContext(this.tileSize,this.tileSize),this.captureGPUInfo()}T.isWebglSupported=function(e){if(T.isLikelyNode)return!1;e=e||T.WebglFilterBackend.prototype.tileSize;var i=document.createElement("canvas"),r=i.getContext("webgl")||i.getContext("experimental-webgl"),n=!1;if(r){T.maxTextureSize=r.getParameter(r.MAX_TEXTURE_SIZE),n=T.maxTextureSize>=e;for(var o=["highp","mediump","lowp"],s=0;s<3;s++)if(t(r,o[s])){T.webGlPrecision=o[s];break}}return this.isSupported=n,n},T.WebglFilterBackend=e,e.prototype={tileSize:2048,resources:{},setupGLContext:function(t,e){this.dispose(),this.createWebGLCanvas(t,e),this.aPosition=new Float32Array([0,0,0,1,1,0,1,1]),this.chooseFastestCopyGLTo2DMethod(t,e)},chooseFastestCopyGLTo2DMethod:function(t,e){var i,r=void 0!==window.performance;try{new ImageData(1,1),i=!0}catch(t){i=!1}var n="undefined"!=typeof ArrayBuffer,o="undefined"!=typeof Uint8ClampedArray;if(r&&i&&n&&o){var s=T.util.createCanvasElement(),a=new ArrayBuffer(t*e*4);if(T.forceGLPutImageData)return this.imageBuffer=a,void(this.copyGLTo2D=A);var h,c,l={imageBuffer:a,destinationWidth:t,destinationHeight:e,targetCanvas:s};s.width=t,s.height=e,h=window.performance.now(),k.call(l,this.gl,l),c=window.performance.now()-h,h=window.performance.now(),A.call(l,this.gl,l),c>window.performance.now()-h?(this.imageBuffer=a,this.copyGLTo2D=A):this.copyGLTo2D=k}},createWebGLCanvas:function(t,e){var i=T.util.createCanvasElement();i.width=t,i.height=e;var r={alpha:!0,premultipliedAlpha:!1,depth:!1,stencil:!1,antialias:!1},n=i.getContext("webgl",r);n||(n=i.getContext("experimental-webgl",r)),n&&(n.clearColor(0,0,0,0),this.canvas=i,this.gl=n)},applyFilters:function(t,e,i,r,n,o){var s,a=this.gl;o&&(s=this.getCachedTexture(o,e));var h={originalWidth:e.width||e.originalWidth,originalHeight:e.height||e.originalHeight,sourceWidth:i,sourceHeight:r,destinationWidth:i,destinationHeight:r,context:a,sourceTexture:this.createTexture(a,i,r,!s&&e),targetTexture:this.createTexture(a,i,r),originalTexture:s||this.createTexture(a,i,r,!s&&e),passes:t.length,webgl:!0,aPosition:this.aPosition,programCache:this.programCache,pass:0,filterBackend:this,targetCanvas:n},c=a.createFramebuffer();return a.bindFramebuffer(a.FRAMEBUFFER,c),t.forEach((function(t){t&&t.applyTo(h)})),function(t){var e=t.targetCanvas,i=e.width,r=e.height,n=t.destinationWidth,o=t.destinationHeight;i===n&&r===o||(e.width=n,e.height=o)}(h),this.copyGLTo2D(a,h),a.bindTexture(a.TEXTURE_2D,null),a.deleteTexture(h.sourceTexture),a.deleteTexture(h.targetTexture),a.deleteFramebuffer(c),n.getContext("2d").setTransform(1,0,0,1,0,0),h},dispose:function(){this.canvas&&(this.canvas=null,this.gl=null),this.clearWebGLCaches()},clearWebGLCaches:function(){this.programCache={},this.textureCache={}},createTexture:function(t,e,i,r){var n=t.createTexture();return t.bindTexture(t.TEXTURE_2D,n),t.texParameteri(t.TEXTURE_2D,t.TEXTURE_MAG_FILTER,t.NEAREST),t.texParameteri(t.TEXTURE_2D,t.TEXTURE_MIN_FILTER,t.NEAREST),t.texParameteri(t.TEXTURE_2D,t.TEXTURE_WRAP_S,t.CLAMP_TO_EDGE),t.texParameteri(t.TEXTURE_2D,t.TEXTURE_WRAP_T,t.CLAMP_TO_EDGE),r?t.texImage2D(t.TEXTURE_2D,0,t.RGBA,t.RGBA,t.UNSIGNED_BYTE,r):t.texImage2D(t.TEXTURE_2D,0,t.RGBA,e,i,0,t.RGBA,t.UNSIGNED_BYTE,null),n},getCachedTexture:function(t,e){if(this.textureCache[t])return this.textureCache[t];var i=this.createTexture(this.gl,e.width,e.height,e);return this.textureCache[t]=i,i},evictCachesForKey:function(t){this.textureCache[t]&&(this.gl.deleteTexture(this.textureCache[t]),delete this.textureCache[t])},copyGLTo2D:k,captureGPUInfo:function(){if(this.gpuInfo)return this.gpuInfo;var t=this.gl,e={renderer:"",vendor:""};if(!t)return e;var i=t.getExtension("WEBGL_debug_renderer_info");if(i){var r=t.getParameter(i.UNMASKED_RENDERER_WEBGL),n=t.getParameter(i.UNMASKED_VENDOR_WEBGL);r&&(e.renderer=r.toLowerCase()),n&&(e.vendor=n.toLowerCase())}return this.gpuInfo=e,e}}}(),function(){var t=function(){};function e(){}T.Canvas2dFilterBackend=e,e.prototype={evictCachesForKey:t,dispose:t,clearWebGLCaches:t,resources:{},applyFilters:function(t,e,i,r,n){var o=n.getContext("2d");o.drawImage(e,0,0,i,r);var s={sourceWidth:i,sourceHeight:r,imageData:o.getImageData(0,0,i,r),originalEl:e,originalImageData:o.getImageData(0,0,i,r),canvasEl:n,ctx:o,filterBackend:this};return t.forEach((function(t){t.applyTo(s)})),s.imageData.width===i&&s.imageData.height===r||(n.width=s.imageData.width,n.height=s.imageData.height),o.putImageData(s.imageData,0,0),s}}}(),T.Image=T.Image||{},T.Image.filters=T.Image.filters||{},T.Image.filters.BaseFilter=T.util.createClass({type:"BaseFilter",vertexSource:"attribute vec2 aPosition;\nvarying vec2 vTexCoord;\nvoid main() {\nvTexCoord = aPosition;\ngl_Position = vec4(aPosition * 2.0 - 1.0, 0.0, 1.0);\n}",fragmentSource:"precision highp float;\nvarying vec2 vTexCoord;\nuniform sampler2D uTexture;\nvoid main() {\ngl_FragColor = texture2D(uTexture, vTexCoord);\n}",initialize:function(t){t&&this.setOptions(t)},setOptions:function(t){for(var e in t)this[e]=t[e]},createProgram:function(t,e,i){e=e||this.fragmentSource,i=i||this.vertexSource,"highp"!==T.webGlPrecision&&(e=e.replace(/precision highp float/g,"precision "+T.webGlPrecision+" float"));var r=t.createShader(t.VERTEX_SHADER);if(t.shaderSource(r,i),t.compileShader(r),!t.getShaderParameter(r,t.COMPILE_STATUS))throw new Error("Vertex shader compile error for "+this.type+": "+t.getShaderInfoLog(r));var n=t.createShader(t.FRAGMENT_SHADER);if(t.shaderSource(n,e),t.compileShader(n),!t.getShaderParameter(n,t.COMPILE_STATUS))throw new Error("Fragment shader compile error for "+this.type+": "+t.getShaderInfoLog(n));var o=t.createProgram();if(t.attachShader(o,r),t.attachShader(o,n),t.linkProgram(o),!t.getProgramParameter(o,t.LINK_STATUS))throw new Error('Shader link error for "${this.type}" '+t.getProgramInfoLog(o));var s=this.getAttributeLocations(t,o),a=this.getUniformLocations(t,o)||{};return a.uStepW=t.getUniformLocation(o,"uStepW"),a.uStepH=t.getUniformLocation(o,"uStepH"),{program:o,attributeLocations:s,uniformLocations:a}},getAttributeLocations:function(t,e){return{aPosition:t.getAttribLocation(e,"aPosition")}},getUniformLocations:function(){return{}},sendAttributeData:function(t,e,i){var r=e.aPosition,n=t.createBuffer();t.bindBuffer(t.ARRAY_BUFFER,n),t.enableVertexAttribArray(r),t.vertexAttribPointer(r,2,t.FLOAT,!1,0,0),t.bufferData(t.ARRAY_BUFFER,i,t.STATIC_DRAW)},_setupFrameBuffer:function(t){var e,i,r=t.context;t.passes>1?(e=t.destinationWidth,i=t.destinationHeight,t.sourceWidth===e&&t.sourceHeight===i||(r.deleteTexture(t.targetTexture),t.targetTexture=t.filterBackend.createTexture(r,e,i)),r.framebufferTexture2D(r.FRAMEBUFFER,r.COLOR_ATTACHMENT0,r.TEXTURE_2D,t.targetTexture,0)):(r.bindFramebuffer(r.FRAMEBUFFER,null),r.finish())},_swapTextures:function(t){t.passes--,t.pass++;var e=t.targetTexture;t.targetTexture=t.sourceTexture,t.sourceTexture=e},isNeutralState:function(){var t=this.mainParameter,e=T.Image.filters[this.type].prototype;if(t){if(Array.isArray(e[t])){for(var i=e[t].length;i--;)if(this[t][i]!==e[t][i])return!1;return!0}return e[t]===this[t]}return!1},applyTo:function(t){t.webgl?(this._setupFrameBuffer(t),this.applyToWebGL(t),this._swapTextures(t)):this.applyTo2d(t)},retrieveShader:function(t){return t.programCache.hasOwnProperty(this.type)||(t.programCache[this.type]=this.createProgram(t.context)),t.programCache[this.type]},applyToWebGL:function(t){var e=t.context,i=this.retrieveShader(t);0===t.pass&&t.originalTexture?e.bindTexture(e.TEXTURE_2D,t.originalTexture):e.bindTexture(e.TEXTURE_2D,t.sourceTexture),e.useProgram(i.program),this.sendAttributeData(e,i.attributeLocations,t.aPosition),e.uniform1f(i.uniformLocations.uStepW,1/t.sourceWidth),e.uniform1f(i.uniformLocations.uStepH,1/t.sourceHeight),this.sendUniformData(e,i.uniformLocations),e.viewport(0,0,t.destinationWidth,t.destinationHeight),e.drawArrays(e.TRIANGLE_STRIP,0,4)},bindAdditionalTexture:function(t,e,i){t.activeTexture(i),t.bindTexture(t.TEXTURE_2D,e),t.activeTexture(t.TEXTURE0)},unbindAdditionalTexture:function(t,e){t.activeTexture(e),t.bindTexture(t.TEXTURE_2D,null),t.activeTexture(t.TEXTURE0)},getMainParameter:function(){return this[this.mainParameter]},setMainParameter:function(t){this[this.mainParameter]=t},sendUniformData:function(){},createHelpLayer:function(t){if(!t.helpLayer){var e=document.createElement("canvas");e.width=t.sourceWidth,e.height=t.sourceHeight,t.helpLayer=e}},toObject:function(){var t={type:this.type},e=this.mainParameter;return e&&(t[e]=this[e]),t},toJSON:function(){return this.toObject()}}),T.Image.filters.BaseFilter.fromObject=function(t,e){var i=new T.Image.filters[t.type](t);return e&&e(i),i},function(t){var e=t.fabric||(t.fabric={}),i=e.Image.filters,r=e.util.createClass;i.ColorMatrix=r(i.BaseFilter,{type:"ColorMatrix",fragmentSource:"precision highp float;\nuniform sampler2D uTexture;\nvarying vec2 vTexCoord;\nuniform mat4 uColorMatrix;\nuniform vec4 uConstants;\nvoid main() {\nvec4 color = texture2D(uTexture, vTexCoord);\ncolor *= uColorMatrix;\ncolor += uConstants;\ngl_FragColor = color;\n}",matrix:[1,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,1,0],mainParameter:"matrix",colorsOnly:!0,initialize:function(t){this.callSuper("initialize",t),this.matrix=this.matrix.slice(0)},applyTo2d:function(t){var e,i,r,n,o,s=t.imageData.data,a=s.length,h=this.matrix,c=this.colorsOnly;for(o=0;o=b||s<0||s>=_||(h=4*(a*_+s),c=v[d*m+f],e+=p[h]*c,i+=p[h+1]*c,r+=p[h+2]*c,w||(n+=p[h+3]*c));C[o]=e,C[o+1]=i,C[o+2]=r,C[o+3]=w?p[o+3]:n}t.imageData=x},getUniformLocations:function(t,e){return{uMatrix:t.getUniformLocation(e,"uMatrix"),uOpaque:t.getUniformLocation(e,"uOpaque"),uHalfSize:t.getUniformLocation(e,"uHalfSize"),uSize:t.getUniformLocation(e,"uSize")}},sendUniformData:function(t,e){t.uniform1fv(e.uMatrix,this.matrix)},toObject:function(){return i(this.callSuper("toObject"),{opaque:this.opaque,matrix:this.matrix})}}),e.Image.filters.Convolute.fromObject=e.Image.filters.BaseFilter.fromObject}(t),function(t){var e=t.fabric||(t.fabric={}),i=e.Image.filters,r=e.util.createClass;i.Grayscale=r(i.BaseFilter,{type:"Grayscale",fragmentSource:{average:"precision highp float;\nuniform sampler2D uTexture;\nvarying vec2 vTexCoord;\nvoid main() {\nvec4 color = texture2D(uTexture, vTexCoord);\nfloat average = (color.r + color.b + color.g) / 3.0;\ngl_FragColor = vec4(average, average, average, color.a);\n}",lightness:"precision highp float;\nuniform sampler2D uTexture;\nuniform int uMode;\nvarying vec2 vTexCoord;\nvoid main() {\nvec4 col = texture2D(uTexture, vTexCoord);\nfloat average = (max(max(col.r, col.g),col.b) + min(min(col.r, col.g),col.b)) / 2.0;\ngl_FragColor = vec4(average, average, average, col.a);\n}",luminosity:"precision highp float;\nuniform sampler2D uTexture;\nuniform int uMode;\nvarying vec2 vTexCoord;\nvoid main() {\nvec4 col = texture2D(uTexture, vTexCoord);\nfloat average = 0.21 * col.r + 0.72 * col.g + 0.07 * col.b;\ngl_FragColor = vec4(average, average, average, col.a);\n}"},mode:"average",mainParameter:"mode",applyTo2d:function(t){var e,i,r=t.imageData.data,n=r.length,o=this.mode;for(e=0;ec[0]&&n>c[1]&&o>c[2]&&r 0.0) {\n"+this.fragmentSource[t]+"}\n}"},retrieveShader:function(t){var e,i=this.type+"_"+this.mode;return t.programCache.hasOwnProperty(i)||(e=this.buildSource(this.mode),t.programCache[i]=this.createProgram(t.context,e)),t.programCache[i]},applyTo2d:function(t){var i,r,n,o,s,a,h,c=t.imageData.data,l=c.length,u=1-this.alpha;i=(h=new e.Color(this.color).getSource())[0]*this.alpha,r=h[1]*this.alpha,n=h[2]*this.alpha;for(var f=0;f=t||e<=-t)return 0;if(e<1.1920929e-7&&e>-1.1920929e-7)return 1;var i=(e*=Math.PI)/t;return a(e)/e*a(i)/i}},applyTo2d:function(t){var e=t.imageData,i=this.scaleX,r=this.scaleY;this.rcpScaleX=1/i,this.rcpScaleY=1/r;var n,o=e.width,a=e.height,h=s(o*i),c=s(a*r);"sliceHack"===this.resizeType?n=this.sliceByTwo(t,o,a,h,c):"hermite"===this.resizeType?n=this.hermiteFastResize(t,o,a,h,c):"bilinear"===this.resizeType?n=this.bilinearFiltering(t,o,a,h,c):"lanczos"===this.resizeType&&(n=this.lanczosResize(t,o,a,h,c)),t.imageData=n},sliceByTwo:function(t,i,n,o,s){var a,h,c=t.imageData,l=.5,u=!1,f=!1,d=i*l,g=n*l,p=e.filterBackend.resources,v=0,m=0,y=i,_=0;for(p.sliceByTwo||(p.sliceByTwo=document.createElement("canvas")),((a=p.sliceByTwo).width<1.5*i||a.height=e)){M=r(1e3*o(S-x.x)),b[M]||(b[M]={});for(var F=C.y-_;F<=C.y+_;F++)F<0||F>=s||(j=r(1e3*o(F-x.y)),b[M][j]||(b[M][j]=d(n(i(M*v,2)+i(j*m,2))/1e3)),(T=b[M][j])>0&&(P+=T,E+=T*l[O=4*(F*e+S)],k+=T*l[O+1],A+=T*l[O+2],D+=T*l[O+3]))}f[O=4*(w*a+h)]=E/P,f[O+1]=k/P,f[O+2]=A/P,f[O+3]=D/P}return++h1&&j<-1||(_=2*j*j*j-3*j*j+1)>0&&(T+=_*d[(M=4*(D+P*e))+3],x+=_,d[M+3]<255&&(_=_*d[M+3]/250),C+=_*d[M],w+=_*d[M+1],S+=_*d[M+2],b+=_)}p[y]=C/b,p[y+1]=w/b,p[y+2]=S/b,p[y+3]=T/x}return g},toObject:function(){return{type:this.type,scaleX:this.scaleX,scaleY:this.scaleY,resizeType:this.resizeType,lanczosLobes:this.lanczosLobes}}}),e.Image.filters.Resize.fromObject=e.Image.filters.BaseFilter.fromObject}(t),function(t){var e=t.fabric||(t.fabric={}),i=e.Image.filters,r=e.util.createClass;i.Contrast=r(i.BaseFilter,{type:"Contrast",fragmentSource:"precision highp float;\nuniform sampler2D uTexture;\nuniform float uContrast;\nvarying vec2 vTexCoord;\nvoid main() {\nvec4 color = texture2D(uTexture, vTexCoord);\nfloat contrastF = 1.015 * (uContrast + 1.0) / (1.0 * (1.015 - uContrast));\ncolor.rgb = contrastF * (color.rgb - 0.5) + 0.5;\ngl_FragColor = color;\n}",contrast:0,mainParameter:"contrast",applyTo2d:function(t){if(0!==this.contrast){var e,i=t.imageData.data,r=i.length,n=Math.floor(255*this.contrast),o=259*(n+255)/(255*(259-n));for(e=0;e1&&(e=1/this.aspectRatio):this.aspectRatio<1&&(e=this.aspectRatio),t=e*this.blur*.12,this.horizontal?i[0]=t:i[1]=t,i}}),i.Blur.fromObject=e.Image.filters.BaseFilter.fromObject}(t),function(t){var e=t.fabric||(t.fabric={}),i=e.Image.filters,r=e.util.createClass;i.Gamma=r(i.BaseFilter,{type:"Gamma",fragmentSource:"precision highp float;\nuniform sampler2D uTexture;\nuniform vec3 uGamma;\nvarying vec2 vTexCoord;\nvoid main() {\nvec4 color = texture2D(uTexture, vTexCoord);\nvec3 correction = (1.0 / uGamma);\ncolor.r = pow(color.r, correction.r);\ncolor.g = pow(color.g, correction.g);\ncolor.b = pow(color.b, correction.b);\ngl_FragColor = color;\ngl_FragColor.rgb *= color.a;\n}",gamma:[1,1,1],mainParameter:"gamma",initialize:function(t){this.gamma=[1,1,1],i.BaseFilter.prototype.initialize.call(this,t)},applyTo2d:function(t){var e,i=t.imageData.data,r=this.gamma,n=i.length,o=1/r[0],s=1/r[1],a=1/r[2];for(this.rVals||(this.rVals=new Uint8Array(256),this.gVals=new Uint8Array(256),this.bVals=new Uint8Array(256)),e=0,n=256;e'},_getCacheCanvasDimensions:function(){var t=this.callSuper("_getCacheCanvasDimensions"),e=this.fontSize;return t.width+=e*t.zoomX,t.height+=e*t.zoomY,t},_render:function(t){var e=this.path;e&&!e.isNotVisible()&&e._render(t),this._setTextStyles(t),this._renderTextLinesBackground(t),this._renderTextDecoration(t,"underline"),this._renderText(t),this._renderTextDecoration(t,"overline"),this._renderTextDecoration(t,"linethrough")},_renderText:function(t){"stroke"===this.paintFirst?(this._renderTextStroke(t),this._renderTextFill(t)):(this._renderTextFill(t),this._renderTextStroke(t))},_setTextStyles:function(t,e,i){t.textBaseline="alphabetic",t.font=this._getFontDeclaration(e,i)},calcTextWidth:function(){for(var t=this.getLineWidth(0),e=1,i=this._textLines.length;et&&(t=r)}return t},_renderTextLine:function(t,e,i,r,n,o){this._renderChars(t,e,i,r,n,o)},_renderTextLinesBackground:function(t){if(this.textBackgroundColor||this.styleHas("textBackgroundColor")){for(var e,i,r,n,o,s,a,h=t.fillStyle,c=this._getLeftOffset(),l=this._getTopOffset(),u=0,f=0,d=this.path,g=0,p=this._textLines.length;g=0:ia?u%=a:u<0&&(u+=a),this._setGraphemeOnPath(u,o,s),u+=o.kernedWidth}return{width:h,numOfSpaces:0}},_setGraphemeOnPath:function(t,i,r){var n=t+i.kernedWidth/2,o=this.path,s=e.util.getPointOnPath(o.path,n,o.segmentsInfo);i.renderLeft=s.x-r.x,i.renderTop=s.y-r.y,i.angle=s.angle+("right"===this.pathSide?Math.PI:0)},_getGraphemeBox:function(t,e,i,r,n){var o,s=this.getCompleteStyleDeclaration(e,i),a=r?this.getCompleteStyleDeclaration(e,i-1):{},h=this._measureChar(t,s,r,a),c=h.kernedWidth,l=h.width;0!==this.charSpacing&&(l+=o=this._getWidthOfCharSpacing(),c+=o);var u={width:l,left:0,height:s.fontSize,kernedWidth:c,deltaY:s.deltaY};if(i>0&&!n){var f=this.__charBounds[e][i-1];u.left=f.left+f.width+h.kernedWidth-h.width}return u},getHeightOfLine:function(t){if(this.__lineHeights[t])return this.__lineHeights[t];for(var e=this._textLines[t],i=this.getHeightOfChar(t,0),r=1,n=e.length;r0){var P=y+o+u;"rtl"===this.direction&&(P=this.width-P-f),c&&m&&(t.fillStyle=m,t.fillRect(P,l+C*r+s,f,this.fontSize/15)),u=d.left,f=d.width,c=g,m=v,r=n,s=a}else f+=d.kernedWidth;P=y+o+u;"rtl"===this.direction&&(P=this.width-P-f),t.fillStyle=v,g&&v&&t.fillRect(P,l+C*r+s,f-x,this.fontSize/15),_+=i}else _+=i;this._removeShadow(t)}},_getFontDeclaration:function(t,i){var r=t||this,n=this.fontFamily,o=e.Text.genericFonts.indexOf(n.toLowerCase())>-1,s=void 0===n||n.indexOf("'")>-1||n.indexOf(",")>-1||n.indexOf('"')>-1||o?r.fontFamily:'"'+r.fontFamily+'"';return[e.isLikelyNode?r.fontWeight:r.fontStyle,e.isLikelyNode?r.fontStyle:r.fontWeight,i?this.CACHE_FONT_SIZE+"px":r.fontSize+"px",s].join(" ")},render:function(t){this.visible&&(this.canvas&&this.canvas.skipOffscreen&&!this.group&&!this.isOnScreen()||(this._shouldClearDimensionCache()&&this.initDimensions(),this.callSuper("render",t)))},_splitTextIntoLines:function(t){for(var i=t.split(this._reNewline),r=new Array(i.length),n=["\n"],o=[],s=0;s-1&&(t.underline=!0),t.textDecoration.indexOf("line-through")>-1&&(t.linethrough=!0),t.textDecoration.indexOf("overline")>-1&&(t.overline=!0),delete t.textDecoration)}T.IText=T.util.createClass(T.Text,T.Observable,{type:"i-text",selectionStart:0,selectionEnd:0,selectionColor:"rgba(17,119,255,0.3)",isEditing:!1,editable:!0,editingBorderColor:"rgba(102,153,255,0.25)",cursorWidth:2,cursorColor:"",cursorDelay:1e3,cursorDuration:600,caching:!0,hiddenTextareaContainer:null,_reSpace:/\s|\n/,_currentCursorOpacity:0,_selectionDirection:null,_abortCursorAnimation:!1,__widthOfSpace:[],inCompositionMode:!1,initialize:function(t,e){this.callSuper("initialize",t,e),this.initBehavior()},setSelectionStart:function(t){t=Math.max(t,0),this._updateAndFire("selectionStart",t)},setSelectionEnd:function(t){t=Math.min(t,this.text.length),this._updateAndFire("selectionEnd",t)},_updateAndFire:function(t,e){this[t]!==e&&(this._fireSelectionChanged(),this[t]=e),this._updateTextarea()},_fireSelectionChanged:function(){this.fire("selection:changed"),this.canvas&&this.canvas.fire("text:selection:changed",{target:this})},initDimensions:function(){this.isEditing&&this.initDelayedCursor(),this.clearContextTop(),this.callSuper("initDimensions")},render:function(t){this.clearContextTop(),this.callSuper("render",t),this.cursorOffsetCache={},this.renderCursorOrSelection()},_render:function(t){this.callSuper("_render",t)},clearContextTop:function(t){if(this.isEditing&&this.canvas&&this.canvas.contextTop){var e=this.canvas.contextTop,i=this.canvas.viewportTransform;e.save(),e.transform(i[0],i[1],i[2],i[3],i[4],i[5]),this.transform(e),this._clearTextArea(e),t||e.restore()}},renderCursorOrSelection:function(){if(this.isEditing&&this.canvas&&this.canvas.contextTop){var t=this._getCursorBoundaries(),e=this.canvas.contextTop;this.clearContextTop(!0),this.selectionStart===this.selectionEnd?this.renderCursor(t,e):this.renderSelection(t,e),e.restore()}},_clearTextArea:function(t){var e=this.width+4,i=this.height+4;t.clearRect(-e/2,-i/2,e,i)},_getCursorBoundaries:function(t){void 0===t&&(t=this.selectionStart);var e=this._getLeftOffset(),i=this._getTopOffset(),r=this._getCursorBoundariesOffsets(t);return{left:e,top:i,leftOffset:r.left,topOffset:r.top}},_getCursorBoundariesOffsets:function(t){if(this.cursorOffsetCache&&"top"in this.cursorOffsetCache)return this.cursorOffsetCache;var e,i,r,n,o=0,s=0,a=this.get2DCursorLocation(t);r=a.charIndex,i=a.lineIndex;for(var h=0;h0?s:0)},"rtl"===this.direction&&(n.left*=-1),this.cursorOffsetCache=n,this.cursorOffsetCache},renderCursor:function(t,e){var i=this.get2DCursorLocation(),r=i.lineIndex,n=i.charIndex>0?i.charIndex-1:0,o=this.getValueOfPropertyAt(r,n,"fontSize"),s=this.scaleX*this.canvas.getZoom(),a=this.cursorWidth/s,h=t.topOffset,c=this.getValueOfPropertyAt(r,n,"deltaY");h+=(1-this._fontSizeFraction)*this.getHeightOfLine(r)/this.lineHeight-o*(1-this._fontSizeFraction),this.inCompositionMode&&this.renderSelection(t,e),e.fillStyle=this.cursorColor||this.getValueOfPropertyAt(r,n,"fill"),e.globalAlpha=this.__isMousedown?1:this._currentCursorOpacity,e.fillRect(t.left+t.leftOffset-a/2,h+t.top+c,a,o)},renderSelection:function(t,e){for(var i=this.inCompositionMode?this.hiddenTextarea.selectionStart:this.selectionStart,r=this.inCompositionMode?this.hiddenTextarea.selectionEnd:this.selectionEnd,n=-1!==this.textAlign.indexOf("justify"),o=this.get2DCursorLocation(i),s=this.get2DCursorLocation(r),a=o.lineIndex,h=s.lineIndex,c=o.charIndex<0?0:o.charIndex,l=s.charIndex<0?0:s.charIndex,u=a;u<=h;u++){var f,d=this._getLineLeftOffset(u)||0,g=this.getHeightOfLine(u),p=0,v=0;if(u===a&&(p=this.__charBounds[a][c].left),u>=a&&u1)&&(g/=this.lineHeight);var y=t.left+d+p,_=v-p,b=g,x=0;this.inCompositionMode?(e.fillStyle=this.compositionColor||"black",b=1,x=g):e.fillStyle=this.selectionColor,"rtl"===this.direction&&(y=this.width-y-_),e.fillRect(y,t.top+t.topOffset+x,_,b),t.topOffset+=f}},getCurrentCharFontSize:function(){var t=this._getCurrentCharIndex();return this.getValueOfPropertyAt(t.l,t.c,"fontSize")},getCurrentCharColor:function(){var t=this._getCurrentCharIndex();return this.getValueOfPropertyAt(t.l,t.c,"fill")},_getCurrentCharIndex:function(){var t=this.get2DCursorLocation(this.selectionStart,!0),e=t.charIndex>0?t.charIndex-1:0;return{l:t.lineIndex,c:e}}}),T.IText.fromObject=function(e,i){if(t(e),e.styles)for(var r in e.styles)for(var n in e.styles[r])t(e.styles[r][n]);T.Object._fromObject("IText",e,i,"text")}}(),S=T.util.object.clone,T.util.object.extend(T.IText.prototype,{initBehavior:function(){this.initAddedHandler(),this.initRemovedHandler(),this.initCursorSelectionHandlers(),this.initDoubleClickSimulation(),this.mouseMoveHandler=this.mouseMoveHandler.bind(this)},onDeselect:function(){this.isEditing&&this.exitEditing(),this.selected=!1},initAddedHandler:function(){var t=this;this.on("added",(function(){var e=t.canvas;e&&(e._hasITextHandlers||(e._hasITextHandlers=!0,t._initCanvasHandlers(e)),e._iTextInstances=e._iTextInstances||[],e._iTextInstances.push(t))}))},initRemovedHandler:function(){var t=this;this.on("removed",(function(){var e=t.canvas;e&&(e._iTextInstances=e._iTextInstances||[],T.util.removeFromArray(e._iTextInstances,t),0===e._iTextInstances.length&&(e._hasITextHandlers=!1,t._removeCanvasHandlers(e)))}))},_initCanvasHandlers:function(t){t._mouseUpITextHandler=function(){t._iTextInstances&&t._iTextInstances.forEach((function(t){t.__isMousedown=!1}))},t.on("mouse:up",t._mouseUpITextHandler)},_removeCanvasHandlers:function(t){t.off("mouse:up",t._mouseUpITextHandler)},_tick:function(){this._currentTickState=this._animateCursor(this,1,this.cursorDuration,"_onTickComplete")},_animateCursor:function(t,e,i,r){var n;return n={isAborted:!1,abort:function(){this.isAborted=!0}},t.animate("_currentCursorOpacity",e,{duration:i,onComplete:function(){n.isAborted||t[r]()},onChange:function(){t.canvas&&t.selectionStart===t.selectionEnd&&t.renderCursorOrSelection()},abort:function(){return n.isAborted}}),n},_onTickComplete:function(){var t=this;this._cursorTimeout1&&clearTimeout(this._cursorTimeout1),this._cursorTimeout1=setTimeout((function(){t._currentTickCompleteState=t._animateCursor(t,0,this.cursorDuration/2,"_tick")}),100)},initDelayedCursor:function(t){var e=this,i=t?0:this.cursorDelay;this.abortCursorAnimation(),this._currentCursorOpacity=1,this._cursorTimeout2=setTimeout((function(){e._tick()}),i)},abortCursorAnimation:function(){var t=this._currentTickState||this._currentTickCompleteState,e=this.canvas;this._currentTickState&&this._currentTickState.abort(),this._currentTickCompleteState&&this._currentTickCompleteState.abort(),clearTimeout(this._cursorTimeout1),clearTimeout(this._cursorTimeout2),this._currentCursorOpacity=0,t&&e&&e.clearContext(e.contextTop||e.contextContainer)},selectAll:function(){return this.selectionStart=0,this.selectionEnd=this._text.length,this._fireSelectionChanged(),this._updateTextarea(),this},getSelectedText:function(){return this._text.slice(this.selectionStart,this.selectionEnd).join("")},findWordBoundaryLeft:function(t){var e=0,i=t-1;if(this._reSpace.test(this._text[i]))for(;this._reSpace.test(this._text[i]);)e++,i--;for(;/\S/.test(this._text[i])&&i>-1;)e++,i--;return t-e},findWordBoundaryRight:function(t){var e=0,i=t;if(this._reSpace.test(this._text[i]))for(;this._reSpace.test(this._text[i]);)e++,i++;for(;/\S/.test(this._text[i])&&i-1;)e++,i--;return t-e},findLineBoundaryRight:function(t){for(var e=0,i=t;!/\n/.test(this._text[i])&&i0&&rthis.__selectionStartOnMouseDown?(this.selectionStart=this.__selectionStartOnMouseDown,this.selectionEnd=e):(this.selectionStart=e,this.selectionEnd=this.__selectionStartOnMouseDown),this.selectionStart===i&&this.selectionEnd===r||(this.restartCursorIfNeeded(),this._fireSelectionChanged(),this._updateTextarea(),this.renderCursorOrSelection()))}},_setEditingProps:function(){this.hoverCursor="text",this.canvas&&(this.canvas.defaultCursor=this.canvas.moveCursor="text"),this.borderColor=this.editingBorderColor,this.hasControls=this.selectable=!1,this.lockMovementX=this.lockMovementY=!0},fromStringToGraphemeSelection:function(t,e,i){var r=i.slice(0,t),n=T.util.string.graphemeSplit(r).length;if(t===e)return{selectionStart:n,selectionEnd:n};var o=i.slice(t,e);return{selectionStart:n,selectionEnd:n+T.util.string.graphemeSplit(o).length}},fromGraphemeToStringSelection:function(t,e,i){var r=i.slice(0,t).join("").length;return t===e?{selectionStart:r,selectionEnd:r}:{selectionStart:r,selectionEnd:r+i.slice(t,e).join("").length}},_updateTextarea:function(){if(this.cursorOffsetCache={},this.hiddenTextarea){if(!this.inCompositionMode){var t=this.fromGraphemeToStringSelection(this.selectionStart,this.selectionEnd,this._text);this.hiddenTextarea.selectionStart=t.selectionStart,this.hiddenTextarea.selectionEnd=t.selectionEnd}this.updateTextareaPosition()}},updateFromTextArea:function(){if(this.hiddenTextarea){this.cursorOffsetCache={},this.text=this.hiddenTextarea.value,this._shouldClearDimensionCache()&&(this.initDimensions(),this.setCoords());var t=this.fromStringToGraphemeSelection(this.hiddenTextarea.selectionStart,this.hiddenTextarea.selectionEnd,this.hiddenTextarea.value);this.selectionEnd=this.selectionStart=t.selectionEnd,this.inCompositionMode||(this.selectionStart=t.selectionStart),this.updateTextareaPosition()}},updateTextareaPosition:function(){if(this.selectionStart===this.selectionEnd){var t=this._calcTextareaPosition();this.hiddenTextarea.style.left=t.left,this.hiddenTextarea.style.top=t.top}},_calcTextareaPosition:function(){if(!this.canvas)return{x:1,y:1};var t=this.inCompositionMode?this.compositionStart:this.selectionStart,e=this._getCursorBoundaries(t),i=this.get2DCursorLocation(t),r=i.lineIndex,n=i.charIndex,o=this.getValueOfPropertyAt(r,n,"fontSize")*this.lineHeight,s=e.leftOffset,a=this.calcTransformMatrix(),h={x:e.left+s,y:e.top+e.topOffset+o},c=this.canvas.getRetinaScaling(),l=this.canvas.upperCanvasEl,u=l.width/c,f=l.height/c,d=u-o,g=f-o,p=l.clientWidth/u,v=l.clientHeight/f;return h=T.util.transformPoint(h,a),(h=T.util.transformPoint(h,this.canvas.viewportTransform)).x*=p,h.y*=v,h.x<0&&(h.x=0),h.x>d&&(h.x=d),h.y<0&&(h.y=0),h.y>g&&(h.y=g),h.x+=this.canvas._offset.left,h.y+=this.canvas._offset.top,{left:h.x+"px",top:h.y+"px",fontSize:o+"px",charHeight:o}},_saveEditingProps:function(){this._savedProps={hasControls:this.hasControls,borderColor:this.borderColor,lockMovementX:this.lockMovementX,lockMovementY:this.lockMovementY,hoverCursor:this.hoverCursor,selectable:this.selectable,defaultCursor:this.canvas&&this.canvas.defaultCursor,moveCursor:this.canvas&&this.canvas.moveCursor}},_restoreEditingProps:function(){this._savedProps&&(this.hoverCursor=this._savedProps.hoverCursor,this.hasControls=this._savedProps.hasControls,this.borderColor=this._savedProps.borderColor,this.selectable=this._savedProps.selectable,this.lockMovementX=this._savedProps.lockMovementX,this.lockMovementY=this._savedProps.lockMovementY,this.canvas&&(this.canvas.defaultCursor=this._savedProps.defaultCursor,this.canvas.moveCursor=this._savedProps.moveCursor))},exitEditing:function(){var t=this._textBeforeEdit!==this.text,e=this.hiddenTextarea;return this.selected=!1,this.isEditing=!1,this.selectionEnd=this.selectionStart,e&&(e.blur&&e.blur(),e.parentNode&&e.parentNode.removeChild(e)),this.hiddenTextarea=null,this.abortCursorAnimation(),this._restoreEditingProps(),this._currentCursorOpacity=0,this._shouldClearDimensionCache()&&(this.initDimensions(),this.setCoords()),this.fire("editing:exited"),t&&this.fire("modified"),this.canvas&&(this.canvas.off("mouse:move",this.mouseMoveHandler),this.canvas.fire("text:editing:exited",{target:this}),t&&this.canvas.fire("object:modified",{target:this})),this},_removeExtraneousStyles:function(){for(var t in this.styles)this._textLines[t]||delete this.styles[t]},removeStyleFromTo:function(t,e){var i,r,n=this.get2DCursorLocation(t,!0),o=this.get2DCursorLocation(e,!0),s=n.lineIndex,a=n.charIndex,h=o.lineIndex,c=o.charIndex;if(s!==h){if(this.styles[s])for(i=a;i=c&&(r[l-f]=r[u],delete r[u])}},shiftLineStyles:function(t,e){var i=S(this.styles);for(var r in this.styles){var n=parseInt(r,10);n>t&&(this.styles[n+e]=i[n],i[n-e]||delete this.styles[n])}},restartCursorIfNeeded:function(){this._currentTickState&&!this._currentTickState.isAborted&&this._currentTickCompleteState&&!this._currentTickCompleteState.isAborted||this.initDelayedCursor()},insertNewlineStyleObject:function(t,e,i,r){var n,o={},s=!1,a=this._unwrappedTextLines[t].length===e;for(var h in i||(i=1),this.shiftLineStyles(t,i),this.styles[t]&&(n=this.styles[t][0===e?e:e-1]),this.styles[t]){var c=parseInt(h,10);c>=e&&(s=!0,o[c-e]=this.styles[t][h],a&&0===e||delete this.styles[t][h])}var l=!1;for(s&&!a&&(this.styles[t+i]=o,l=!0),l&&i--;i>0;)r&&r[i-1]?this.styles[t+i]={0:S(r[i-1])}:n?this.styles[t+i]={0:S(n)}:delete this.styles[t+i],i--;this._forceClearCache=!0},insertCharStyleObject:function(t,e,i,r){this.styles||(this.styles={});var n=this.styles[t],o=n?S(n):{};for(var s in i||(i=1),o){var a=parseInt(s,10);a>=e&&(n[a+i]=o[a],o[a-i]||delete n[a])}if(this._forceClearCache=!0,r)for(;i--;)Object.keys(r[i]).length&&(this.styles[t]||(this.styles[t]={}),this.styles[t][e+i]=S(r[i]));else if(n)for(var h=n[e?e-1:1];h&&i--;)this.styles[t][e+i]=S(h)},insertNewStyleBlock:function(t,e,i){for(var r=this.get2DCursorLocation(e,!0),n=[0],o=0,s=0;s0&&(this.insertCharStyleObject(r.lineIndex,r.charIndex,n[0],i),i=i&&i.slice(n[0]+1)),o&&this.insertNewlineStyleObject(r.lineIndex,r.charIndex+n[0],o),s=1;s0?this.insertCharStyleObject(r.lineIndex+s,0,n[s],i):i&&(this.styles[r.lineIndex+s][0]=i[0]),i=i&&i.slice(n[s]+1);n[s]>0&&this.insertCharStyleObject(r.lineIndex+s,0,n[s],i)},setSelectionStartEndWithShift:function(t,e,i){i<=t?(e===t?this._selectionDirection="left":"right"===this._selectionDirection&&(this._selectionDirection="left",this.selectionEnd=t),this.selectionStart=i):i>t&&it?this.selectionStart=t:this.selectionStart<0&&(this.selectionStart=0),this.selectionEnd>t?this.selectionEnd=t:this.selectionEnd<0&&(this.selectionEnd=0)}}),T.util.object.extend(T.IText.prototype,{initDoubleClickSimulation:function(){this.__lastClickTime=+new Date,this.__lastLastClickTime=+new Date,this.__lastPointer={},this.on("mousedown",this.onMouseDown)},onMouseDown:function(t){if(this.canvas){this.__newClickTime=+new Date;var e=t.pointer;this.isTripleClick(e)&&(this.fire("tripleclick",t),this._stopEvent(t.e)),this.__lastLastClickTime=this.__lastClickTime,this.__lastClickTime=this.__newClickTime,this.__lastPointer=e,this.__lastIsEditing=this.isEditing,this.__lastSelected=this.selected}},isTripleClick:function(t){return this.__newClickTime-this.__lastClickTime<500&&this.__lastClickTime-this.__lastLastClickTime<500&&this.__lastPointer.x===t.x&&this.__lastPointer.y===t.y},_stopEvent:function(t){t.preventDefault&&t.preventDefault(),t.stopPropagation&&t.stopPropagation()},initCursorSelectionHandlers:function(){this.initMousedownHandler(),this.initMouseupHandler(),this.initClicks()},doubleClickHandler:function(t){this.isEditing&&this.selectWord(this.getSelectionStartFromPointer(t.e))},tripleClickHandler:function(t){this.isEditing&&this.selectLine(this.getSelectionStartFromPointer(t.e))},initClicks:function(){this.on("mousedblclick",this.doubleClickHandler),this.on("tripleclick",this.tripleClickHandler)},_mouseDownHandler:function(t){!this.canvas||!this.editable||t.e.button&&1!==t.e.button||(this.__isMousedown=!0,this.selected&&(this.inCompositionMode=!1,this.setCursorByClick(t.e)),this.isEditing&&(this.__selectionStartOnMouseDown=this.selectionStart,this.selectionStart===this.selectionEnd&&this.abortCursorAnimation(),this.renderCursorOrSelection()))},_mouseDownHandlerBefore:function(t){!this.canvas||!this.editable||t.e.button&&1!==t.e.button||(this.selected=this===this.canvas._activeObject)},initMousedownHandler:function(){this.on("mousedown",this._mouseDownHandler),this.on("mousedown:before",this._mouseDownHandlerBefore)},initMouseupHandler:function(){this.on("mouseup",this.mouseUpHandler)},mouseUpHandler:function(t){if(this.__isMousedown=!1,!(!this.editable||this.group||t.transform&&t.transform.actionPerformed||t.e.button&&1!==t.e.button)){if(this.canvas){var e=this.canvas._activeObject;if(e&&e!==this)return}this.__lastSelected&&!this.__corner?(this.selected=!1,this.__lastSelected=!1,this.enterEditing(t.e),this.selectionStart===this.selectionEnd?this.initDelayedCursor(!0):this.renderCursorOrSelection()):this.selected=!0}},setCursorByClick:function(t){var e=this.getSelectionStartFromPointer(t),i=this.selectionStart,r=this.selectionEnd;t.shiftKey?this.setSelectionStartEndWithShift(i,r,e):(this.selectionStart=e,this.selectionEnd=e),this.isEditing&&(this._fireSelectionChanged(),this._updateTextarea())},getSelectionStartFromPointer:function(t){for(var e,i=this.getLocalPointer(t),r=0,n=0,o=0,s=0,a=0,h=0,c=this._textLines.length;h0&&(s+=this._textLines[h-1].length+this.missingNewlineOffset(h-1));n=this._getLineLeftOffset(a)*this.scaleX,e=this._textLines[a],"rtl"===this.direction&&(i.x=this.width*this.scaleX-i.x+n);for(var l=0,u=e.length;lo||s<0?0:1);return this.flipX&&(a=n-a),a>this._text.length&&(a=this._text.length),a}}),T.util.object.extend(T.IText.prototype,{initHiddenTextarea:function(){this.hiddenTextarea=T.document.createElement("textarea"),this.hiddenTextarea.setAttribute("autocapitalize","off"),this.hiddenTextarea.setAttribute("autocorrect","off"),this.hiddenTextarea.setAttribute("autocomplete","off"),this.hiddenTextarea.setAttribute("spellcheck","false"),this.hiddenTextarea.setAttribute("data-fabric-hiddentextarea",""),this.hiddenTextarea.setAttribute("wrap","off");var t=this._calcTextareaPosition();this.hiddenTextarea.style.cssText="position: absolute; top: "+t.top+"; left: "+t.left+"; z-index: -999; opacity: 0; width: 1px; height: 1px; font-size: 1px; paddingーtop: "+t.fontSize+";",this.hiddenTextareaContainer?this.hiddenTextareaContainer.appendChild(this.hiddenTextarea):T.document.body.appendChild(this.hiddenTextarea),T.util.addListener(this.hiddenTextarea,"keydown",this.onKeyDown.bind(this)),T.util.addListener(this.hiddenTextarea,"keyup",this.onKeyUp.bind(this)),T.util.addListener(this.hiddenTextarea,"input",this.onInput.bind(this)),T.util.addListener(this.hiddenTextarea,"copy",this.copy.bind(this)),T.util.addListener(this.hiddenTextarea,"cut",this.copy.bind(this)),T.util.addListener(this.hiddenTextarea,"paste",this.paste.bind(this)),T.util.addListener(this.hiddenTextarea,"compositionstart",this.onCompositionStart.bind(this)),T.util.addListener(this.hiddenTextarea,"compositionupdate",this.onCompositionUpdate.bind(this)),T.util.addListener(this.hiddenTextarea,"compositionend",this.onCompositionEnd.bind(this)),!this._clickHandlerInitialized&&this.canvas&&(T.util.addListener(this.canvas.upperCanvasEl,"click",this.onClick.bind(this)),this._clickHandlerInitialized=!0)},keysMap:{9:"exitEditing",27:"exitEditing",33:"moveCursorUp",34:"moveCursorDown",35:"moveCursorRight",36:"moveCursorLeft",37:"moveCursorLeft",38:"moveCursorUp",39:"moveCursorRight",40:"moveCursorDown"},keysMapRtl:{9:"exitEditing",27:"exitEditing",33:"moveCursorUp",34:"moveCursorDown",35:"moveCursorLeft",36:"moveCursorRight",37:"moveCursorRight",38:"moveCursorUp",39:"moveCursorLeft",40:"moveCursorDown"},ctrlKeysMapUp:{67:"copy",88:"cut"},ctrlKeysMapDown:{65:"selectAll"},onClick:function(){this.hiddenTextarea&&this.hiddenTextarea.focus()},onKeyDown:function(t){if(this.isEditing){var e="rtl"===this.direction?this.keysMapRtl:this.keysMap;if(t.keyCode in e)this[e[t.keyCode]](t);else{if(!(t.keyCode in this.ctrlKeysMapDown)||!t.ctrlKey&&!t.metaKey)return;this[this.ctrlKeysMapDown[t.keyCode]](t)}t.stopImmediatePropagation(),t.preventDefault(),t.keyCode>=33&&t.keyCode<=40?(this.inCompositionMode=!1,this.clearContextTop(),this.renderCursorOrSelection()):this.canvas&&this.canvas.requestRenderAll()}},onKeyUp:function(t){!this.isEditing||this._copyDone||this.inCompositionMode?this._copyDone=!1:t.keyCode in this.ctrlKeysMapUp&&(t.ctrlKey||t.metaKey)&&(this[this.ctrlKeysMapUp[t.keyCode]](t),t.stopImmediatePropagation(),t.preventDefault(),this.canvas&&this.canvas.requestRenderAll())},onInput:function(t){var e=this.fromPaste;if(this.fromPaste=!1,t&&t.stopPropagation(),this.isEditing){var i,r,n,o,s,a=this._splitTextIntoLines(this.hiddenTextarea.value).graphemeText,h=this._text.length,c=a.length,l=c-h,u=this.selectionStart,f=this.selectionEnd,d=u!==f;if(""===this.hiddenTextarea.value)return this.styles={},this.updateFromTextArea(),this.fire("changed"),void(this.canvas&&(this.canvas.fire("text:changed",{target:this}),this.canvas.requestRenderAll()));var g=this.fromStringToGraphemeSelection(this.hiddenTextarea.selectionStart,this.hiddenTextarea.selectionEnd,this.hiddenTextarea.value),p=u>g.selectionStart;d?(i=this._text.slice(u,f),l+=f-u):c0&&(r+=(i=this.__charBounds[t][e-1]).left+i.width),r},getDownCursorOffset:function(t,e){var i=this._getSelectionForOffset(t,e),r=this.get2DCursorLocation(i),n=r.lineIndex;if(n===this._textLines.length-1||t.metaKey||34===t.keyCode)return this._text.length-i;var o=r.charIndex,s=this._getWidthBeforeCursor(n,o),a=this._getIndexOnLine(n+1,s);return this._textLines[n].slice(o).length+a+1+this.missingNewlineOffset(n)},_getSelectionForOffset:function(t,e){return t.shiftKey&&this.selectionStart!==this.selectionEnd&&e?this.selectionEnd:this.selectionStart},getUpCursorOffset:function(t,e){var i=this._getSelectionForOffset(t,e),r=this.get2DCursorLocation(i),n=r.lineIndex;if(0===n||t.metaKey||33===t.keyCode)return-i;var o=r.charIndex,s=this._getWidthBeforeCursor(n,o),a=this._getIndexOnLine(n-1,s),h=this._textLines[n].slice(0,o),c=this.missingNewlineOffset(n-1);return-this._textLines[n-1].length+a-h.length+(1-c)},_getIndexOnLine:function(t,e){for(var i,r,n=this._textLines[t],o=this._getLineLeftOffset(t),s=0,a=0,h=n.length;ae){r=!0;var c=o-i,l=o,u=Math.abs(c-e);s=Math.abs(l-e)=this._text.length&&this.selectionEnd>=this._text.length||this._moveCursorUpOrDown("Down",t)},moveCursorUp:function(t){0===this.selectionStart&&0===this.selectionEnd||this._moveCursorUpOrDown("Up",t)},_moveCursorUpOrDown:function(t,e){var i=this["get"+t+"CursorOffset"](e,"right"===this._selectionDirection);e.shiftKey?this.moveCursorWithShift(i):this.moveCursorWithoutShift(i),0!==i&&(this.setSelectionInBoundaries(),this.abortCursorAnimation(),this._currentCursorOpacity=1,this.initDelayedCursor(),this._fireSelectionChanged(),this._updateTextarea())},moveCursorWithShift:function(t){var e="left"===this._selectionDirection?this.selectionStart+t:this.selectionEnd+t;return this.setSelectionStartEndWithShift(this.selectionStart,this.selectionEnd,e),0!==t},moveCursorWithoutShift:function(t){return t<0?(this.selectionStart+=t,this.selectionEnd=this.selectionStart):(this.selectionEnd+=t,this.selectionStart=this.selectionEnd),0!==t},moveCursorLeft:function(t){0===this.selectionStart&&0===this.selectionEnd||this._moveCursorLeftOrRight("Left",t)},_move:function(t,e,i){var r;if(t.altKey)r=this["findWordBoundary"+i](this[e]);else{if(!t.metaKey&&35!==t.keyCode&&36!==t.keyCode)return this[e]+="Left"===i?-1:1,!0;r=this["findLineBoundary"+i](this[e])}if(void 0!==typeof r&&this[e]!==r)return this[e]=r,!0},_moveLeft:function(t,e){return this._move(t,e,"Left")},_moveRight:function(t,e){return this._move(t,e,"Right")},moveCursorLeftWithoutShift:function(t){var e=!0;return this._selectionDirection="left",this.selectionEnd===this.selectionStart&&0!==this.selectionStart&&(e=this._moveLeft(t,"selectionStart")),this.selectionEnd=this.selectionStart,e},moveCursorLeftWithShift:function(t){return"right"===this._selectionDirection&&this.selectionStart!==this.selectionEnd?this._moveLeft(t,"selectionEnd"):0!==this.selectionStart?(this._selectionDirection="left",this._moveLeft(t,"selectionStart")):void 0},moveCursorRight:function(t){this.selectionStart>=this._text.length&&this.selectionEnd>=this._text.length||this._moveCursorLeftOrRight("Right",t)},_moveCursorLeftOrRight:function(t,e){var i="moveCursor"+t+"With";this._currentCursorOpacity=1,e.shiftKey?i+="Shift":i+="outShift",this[i](e)&&(this.abortCursorAnimation(),this.initDelayedCursor(),this._fireSelectionChanged(),this._updateTextarea())},moveCursorRightWithShift:function(t){return"left"===this._selectionDirection&&this.selectionStart!==this.selectionEnd?this._moveRight(t,"selectionStart"):this.selectionEnd!==this._text.length?(this._selectionDirection="right",this._moveRight(t,"selectionEnd")):void 0},moveCursorRightWithoutShift:function(t){var e=!0;return this._selectionDirection="right",this.selectionStart===this.selectionEnd?(e=this._moveRight(t,"selectionStart"),this.selectionEnd=this.selectionStart):this.selectionStart=this.selectionEnd,e},removeChars:function(t,e){void 0===e&&(e=t+1),this.removeStyleFromTo(t,e),this._text.splice(t,e-t),this.text=this._text.join(""),this.set("dirty",!0),this._shouldClearDimensionCache()&&(this.initDimensions(),this.setCoords()),this._removeExtraneousStyles()},insertChars:function(t,e,i,r){void 0===r&&(r=i),r>i&&this.removeStyleFromTo(i,r);var n=T.util.string.graphemeSplit(t);this.insertNewStyleBlock(n,i,e),this._text=[].concat(this._text.slice(0,i),n,this._text.slice(r)),this.text=this._text.join(""),this.set("dirty",!0),this._shouldClearDimensionCache()&&(this.initDimensions(),this.setCoords()),this._removeExtraneousStyles()}}),function(){var t=T.util.toFixed,e=/ +/g;T.util.object.extend(T.Text.prototype,{_toSVG:function(){var t=this._getSVGLeftTopOffsets(),e=this._getSVGTextAndBg(t.textTop,t.textLeft);return this._wrapSVGTextAndBg(e)},toSVG:function(t){return this._createBaseSVGMarkup(this._toSVG(),{reviver:t,noStyle:!0,withShadow:!0})},_getSVGLeftTopOffsets:function(){return{textLeft:-this.width/2,textTop:-this.height/2,lineTop:this.getHeightOfLine(0)}},_wrapSVGTextAndBg:function(t){var e=this.getSvgTextDecoration(this);return[t.textBgRects.join(""),'\t\t",t.textSpans.join(""),"\n"]},_getSVGTextAndBg:function(t,e){var i,r=[],n=[],o=t;this._setSVGBg(n);for(var s=0,a=this._textLines.length;s",T.util.string.escapeXml(i),""].join("")},_setSVGTextLineText:function(t,e,i,r){var n,o,s,a,h,c=this.getHeightOfLine(e),l=-1!==this.textAlign.indexOf("justify"),u="",f=0,d=this._textLines[e];r+=c*(1-this._fontSizeFraction)/this.lineHeight;for(var g=0,p=d.length-1;g<=p;g++)h=g===p||this.charSpacing,u+=d[g],s=this.__charBounds[e][g],0===f?(i+=s.kernedWidth-s.width,f+=s.width):f+=s.kernedWidth,l&&!h&&this._reSpaceAndTab.test(d[g])&&(h=!0),h||(n=n||this.getCompleteStyleDeclaration(e,g),o=this.getCompleteStyleDeclaration(e,g+1),h=this._hasStyleChangedForSvg(n,o)),h&&(a=this._getStyleDeclaration(e,g)||{},t.push(this._createTextCharSpan(u,a,i,r)),u="",n=o,i+=f,f=0)},_pushTextBgRect:function(e,i,r,n,o,s){var a=T.Object.NUM_FRACTION_DIGITS;e.push("\t\t\n')},_setSVGTextLineBg:function(t,e,i,r){for(var n,o,s=this._textLines[e],a=this.getHeightOfLine(e)/this.lineHeight,h=0,c=0,l=this.getValueOfPropertyAt(e,0,"textBackgroundColor"),u=0,f=s.length;uthis.width&&this._set("width",this.dynamicMinWidth),-1!==this.textAlign.indexOf("justify")&&this.enlargeSpaces(),this.height=this.calcTextHeight(),this.saveState({propertySet:"_dimensionAffectingProps"}))},_generateStyleMap:function(t){for(var e=0,i=0,r=0,n={},o=0;o0?(i=0,r++,e++):!this.splitByGrapheme&&this._reSpaceAndTab.test(t.graphemeText[r])&&o>0&&(i++,r++),n[o]={line:e,offset:i},r+=t.graphemeLines[o].length,i+=t.graphemeLines[o].length;return n},styleHas:function(t,i){if(this._styleMap&&!this.isWrapping){var r=this._styleMap[i];r&&(i=r.line)}return e.Text.prototype.styleHas.call(this,t,i)},isEmptyStyles:function(t){if(!this.styles)return!0;var e,i,r=0,n=!1,o=this._styleMap[t],s=this._styleMap[t+1];for(var a in o&&(t=o.line,r=o.offset),s&&(n=s.line===t,e=s.offset),i=void 0===t?this.styles:{line:this.styles[t]})for(var h in i[a])if(h>=r&&(!n||hr&&!v?(a.push(h),h=[],o=d,v=!0):o+=m,v||s||h.push(f),h=h.concat(l),g=s?0:this._measureWord([f],i,u),u++,v=!1,d>p&&(p=d);return y&&a.push(h),p+n>this.dynamicMinWidth&&(this.dynamicMinWidth=p-m+n),a},isEndOfWrapping:function(t){return!this._styleMap[t+1]||this._styleMap[t+1].line!==this._styleMap[t].line},missingNewlineOffset:function(t){return this.splitByGrapheme?this.isEndOfWrapping(t)?1:0:1},_splitTextIntoLines:function(t){for(var i=e.Text.prototype._splitTextIntoLines.call(this,t),r=this._wrapText(i.lines,this.width),n=new Array(r.length),o=0;o","",e.toSVG(t.reviver),"",""];return e._objects[0].fill=i,r.join("\n")}return""},_createBaseSVGMarkup:function(t,e){var i=this.getEraser();if(i){var n=this.eraserToSVG(e);this.clipPath=null;var o=r.call(this,t,e);return this.clipPath=i,[n,o.replace(">",'mask="url(#'+i.clipPathId+')" >')].join("\n")}return r.call(this,t,e)}});var n=T.Group.prototype._restoreObjectsState,o=T.Group.prototype.toObject,s=T.Group.prototype._getBounds;T.util.object.extend(T.Group.prototype,{_getBounds:function(t,e,i){if(this.eraser)return this.width=this._objects[0].width,void(this.height=this._objects[0].height);s.call(this,t,e,i)},_addEraserPathToObjects:function(t){this._objects.forEach((function(e){T.EraserBrush.prototype._addPathToObjectEraser.call(T.EraserBrush.prototype,e,t)}))},applyEraserToObjects:function(){var t=this;if(this.getEraser()){var e=t.calcTransformMatrix();t.getEraser().clone((function(i){var r=i._objects[0].clipPath;t.clipPath=r||void 0,i.getObjects("path").forEach((function(i){var n=T.util.multiplyTransformMatrices(e,i.calcTransformMatrix());T.util.applyTransformToObject(i,n),r?r.clone((function(r){T.EraserBrush.prototype.applyClipPathToPath.call(T.EraserBrush.prototype,i,r,e),t._addEraserPathToObjects(i)})):t._addEraserPathToObjects(i)}))}))}},_restoreObjectsState:function(){return!0===this.erasable&&this.applyEraserToObjects(),n.call(this)},toObject:function(t){return o.call(this,["eraser"].concat(t))}}),T.util.object.extend(T.Canvas.prototype,{isErasing:function(){return this.isDrawingMode&&this.freeDrawingBrush&&"eraser"===this.freeDrawingBrush.type&&this.freeDrawingBrush._isErasing},renderAll:function(){if(!this.contextTopDirty||this._groupSelector||this.isDrawingMode||(this.clearContext(this.contextTop),this.contextTopDirty=!1),!this.isErasing()){this.hasLostContext&&this.renderTopLayer(this.contextTop);var t=this.contextContainer;return this.renderCanvas(t,this._chooseObjectsToRender()),this}this.freeDrawingBrush._render()}}),T.EraserBrush=T.util.createClass(T.PencilBrush,{type:"eraser",_ready:!1,_drawOverlayOnTop:!1,_isErasing:!1,initialize:function(t){this.callSuper("initialize",t),this._renderBound=this._render.bind(this),this.render=this.render.bind(this)},hideObject:function(t){t&&(t._originalOpacity=t.opacity,t.set({opacity:0}))},restoreObjectVisibility:function(t){t&&t._originalOpacity&&(t.set({opacity:t._originalOpacity}),t._originalOpacity=void 0)},_isErasable:function(t){return!1!==t.erasable},prepareCanvasBackgroundForLayer:function(t){if("overlay"!==t){var e=this.canvas.backgroundImage,i="top"===t;e&&this._isErasable(e)===!i&&this.hideObject(e)}},prepareCanvasOverlayForLayer:function(t){var e=this.canvas,i=e.overlayImage,r=!!e.overlayColor;if(e.overlayColor&&"overlay"!==t&&(this.__overlayColor=e.overlayColor,delete e.overlayColor),"bottom"===t)return this.hideObject(i),!1;var n="top"===t,o=i&&!this._isErasable(i)||r;return i&&this._isErasable(i)===!n&&this.hideObject(i),o},restoreCanvasDrawables:function(){var t=this.canvas;this.__overlayColor&&(t.overlayColor=this.__overlayColor,delete this.__overlayColor),this.restoreObjectVisibility(t.backgroundImage),this.restoreObjectVisibility(t.overlayImage)},prepareCollectionTraversal:function(t){var e=this;t.forEachObject((function(t){t.forEachObject&&"deep"===t.erasable?e.prepareCollectionTraversal(t):t.erasable&&e.hideObject(t)}))},restoreCollectionTraversal:function(t){var e=this;t.forEachObject((function(t){t.forEachObject&&"deep"===t.erasable?e.restoreCollectionTraversal(t):e.restoreObjectVisibility(t)}))},prepareCanvasObjectsForLayer:function(t){"bottom"===t&&this.prepareCollectionTraversal(this.canvas)},restoreCanvasObjectsFromLayer:function(t){"bottom"===t&&this.restoreCollectionTraversal(this.canvas)},prepareCanvasForLayer:function(t){return this.prepareCanvasBackgroundForLayer(t),this.prepareCanvasObjectsForLayer(t),this.prepareCanvasOverlayForLayer(t)},restoreCanvasFromLayer:function(t){this.restoreCanvasDrawables(),this.restoreCanvasObjectsFromLayer(t)},renderBottomLayer:function(){var t=this.canvas;this.prepareCanvasForLayer("bottom"),t.renderCanvas(t.getContext(),t.getObjects().filter((function(t){return!t.erasable||t.forEachObject}))),this.restoreCanvasFromLayer("bottom")},renderTopLayer:function(){var t=this.canvas;this._drawOverlayOnTop=this.prepareCanvasForLayer("top"),t.renderCanvas(t.contextTop,t.getObjects()),this.callSuper("_render"),this.restoreCanvasFromLayer("top")},renderOverlay:function(){this.prepareCanvasForLayer("overlay");var t=this.canvas,e=t.contextTop;t._renderOverlay(e),this.restoreCanvasFromLayer("overlay")},_saveAndTransform:function(t){this.callSuper("_saveAndTransform",t),t.globalCompositeOperation="destination-out"},needsFullRender:function(){return this.callSuper("needsFullRender")||this._drawOverlayOnTop},onMouseDown:function(t,e){this.canvas._isMainEvent(e.e)&&(this._prepareForDrawing(t),this._captureDrawingPath(t),this._isErasing=!0,this.canvas.fire("erasing:start"),this._ready=!0,this._render())},_render:function(){this._ready&&(this.isRendering=1,this.renderBottomLayer(),this.renderTopLayer(),this.renderOverlay(),this.isRendering=0)},render:function(){return!!this._isErasing&&(this.isRendering?this.isRendering=T.util.requestAnimFrame(this._renderBound):this._render(),!0)},applyClipPathToPath:function(t,e,i){var r=t.calcTransformMatrix(),n=e.calcTransformMatrix(),o=T.util.multiplyTransformMatrices(T.util.invertTransform(r),i);return T.util.applyTransformToObject(e,T.util.multiplyTransformMatrices(o,n)),t.clipPath=e,t},clonePathWithClipPath:function(t,e,i){var r=e.calcTransformMatrix(),n=e.getClipPath(),o=this;t.clone((function(t){n.clone((function(e){i(o.applyClipPathToPath(t,e,r))}))}))},_addPathToObjectEraser:function(t,e){var i,r=this;if(t.forEachObject&&"deep"===t.erasable){var n=t._objects.filter((function(t){return t.erasable}));n.length>0&&t.clipPath?this.clonePathWithClipPath(e,t,(function(t){n.forEach((function(e){r._addPathToObjectEraser(e,t)}))})):n.length>0&&n.forEach((function(t){r._addPathToObjectEraser(t,e)}))}else{if(t.getEraser())i=t.clipPath;else{var o=t._getNonTransformedDimensions(),s=new T.Rect({fill:"rgb(0,0,0)",width:o.x,height:o.y,clipPath:t.clipPath,originX:"center",originY:"center"});i=new T.Group([s],{eraser:!0})}e.clone((function(e){e.globalCompositeOperation="destination-out";var n=T.util.multiplyTransformMatrices(T.util.invertTransform(t.calcTransformMatrix()),e.calcTransformMatrix());T.util.applyTransformToObject(e,n),i.addWithUpdate(e),t.set({clipPath:i,dirty:!0}),t.fire("erasing:end",{path:e}),t.group&&Array.isArray(r.__subTargets)&&r.__subTargets.push(t)}))}},applyEraserToCanvas:function(t){var e=this.canvas,i={};return["backgroundImage","overlayImage"].forEach((function(r){var n=e[r];n&&n.erasable&&(this._addPathToObjectEraser(n,t),i[r]=n)}),this),i},_finalizeAndAddPath:function(){var t=this.canvas.contextTop,e=this.canvas;t.closePath(),this.decimate&&(this._points=this.decimatePoints(this._points,this.decimate)),e.clearContext(e.contextTop),this._isErasing=!1;var i=this._points&&this._points.length>1?this.convertPointsToSVGPath(this._points):null;if(!i||this._isEmptySVGPath(i))return e.fire("erasing:end"),void e.requestRenderAll();var r=this.createPath(i);r.setCoords(),e.fire("before:path:created",{path:r});var n=this.applyEraserToCanvas(r),o=this;this.__subTargets=[];var s=[];e.forEachObject((function(t){t.erasable&&t.intersectsWithObject(r,!0,!0)&&(o._addPathToObjectEraser(t,r),s.push(t))})),e.fire("erasing:end",{path:r,targets:s,subTargets:this.__subTargets,drawables:n}),delete this.__subTargets,e.requestRenderAll(),r.setCoords(),this._resetShadow(),e.fire("path:created",{path:r})}})}()}(tt);var rt=tt.fabric;export{tt as default,rt as fabric}; +//# sourceMappingURL=/sm/4595deba885e6b95e012cf1ed9deed8a7b30edf3ae4c7d4bd5cc11d619b75c11.map \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v2/assets/extensions-nX1P6R6o.js b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v2/assets/extensions-nX1P6R6o.js new file mode 100644 index 0000000000000000000000000000000000000000..16afb8feedf8d574f79e8e681efc07d074f31617 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v2/assets/extensions-nX1P6R6o.js @@ -0,0 +1 @@ +var e,t,s,i,a,n,o,l,r,d,u,c,A,p,h,m,f=Object.defineProperty,g=e=>{throw TypeError(e)},v=(e,t,s)=>((e,t,s)=>t in e?f(e,t,{enumerable:!0,configurable:!0,writable:!0,value:s}):e[t]=s)(e,"symbol"!=typeof t?t+"":t,s),y=(e,t,s)=>t.has(e)||g("Cannot "+s),w=(e,t,s)=>(y(e,t,"read from private field"),s?s.call(e):t.get(e)),b=(e,t,s)=>t.has(e)?g("Cannot add the same private member more than once"):t instanceof WeakSet?t.add(e):t.set(e,s),_=(e,t,s,i)=>(y(e,t,"write to private field"),i?i.call(e,s):t.set(e,s),s);import{d as S,h as L,s as x,a as k,T as E,b as N,c as C,e as I,f as T,g as O,i as M,j as D}from"./vendor-DrOEdrAG.js";import{c as R}from"./lodash-DkvwM0Z9.js";import{r as P,m as G,w as z,o as V,a as j,c as U,b as W,d as F,u as X,e as Z,f as H,g as q,h as Y,F as K,i as J,n as B,j as Q,v as $,t as ee,k as te,l as se,p as ie,q as ae,s as ne,x as oe,y as le,z as re,A as de,B as ue}from"./vue-DfGw76ex.js";import{u as ce,a as Ae,b as pe,c as he}from"./vueuse-Bf7Wy5dB.js";import{P as me}from"./primevue-keNb8gkp.js";let fe=(null==(t=null==(e=window.comfyAPI)?void 0:e.app)?void 0:t.app)||null,ge=(null==(i=null==(s=window.comfyAPI)?void 0:s.api)?void 0:i.api)||null,ve=(null==(n=null==(a=window.comfyAPI)?void 0:a.ui)?void 0:n.$el)||null,ye=(null==(l=null==(o=window.comfyAPI)?void 0:o.dialog)?void 0:l.ComfyDialog)||null,we=(null==(d=null==(r=window.comfyAPI)?void 0:r.widgets)?void 0:d.ComfyWidgets)||null,be=(null==(c=null==(u=window.comfyAPI)?void 0:u.utils)?void 0:c.applyTextReplacements)||null,_e=(null==(p=null==(A=window.comfyAPI)?void 0:A.groupNode)?void 0:p.GroupNodeConfig)||null;const Se=e=>{var t,s;return e?null==(s=null==(t=null==fe?void 0:fe.ui)?void 0:t.settings)?void 0:s.getSettingValue(e):null};function Le(e,t=null){try{let s=e?Se(e):null;return null==s&&(s=t?localStorage[t]:localStorage[e]||null),s}catch(s){return null}}function xe(e,t=e=>{}){var s;const i=null==(s=fe.ui.settings.settingsLookup)?void 0:s[e];i&&(i.onChange=e=>t(e))}async function ke(e,t,s=null){var i,a;if(!e)throw new Error("Invalid arguments");try{(null==(a=null==(i=null==fe?void 0:fe.ui)?void 0:i.settings)?void 0:a.setSettingValue)?fe.ui.settings.setSettingValue(e,t):await ge.storeSetting(e,t),s&&(localStorage[s]="object"==typeof t?JSON.stringify(t):t)}catch(n){}}const Ee={"Workflow created by":"工作流创建者","Watch more video content":"观看更多视频内容","Workflow Guide":"工作流指南","💎 View Checkpoint Info...":"💎 查看 Checkpoint 信息...","💎 View Lora Info...":"💎 查看 Lora 信息...","🔃 Reload Node":"🔃 刷新节点","Updated At:":"最近更新:","Created At:":"首次发布:","✏️ Edit":"✏️ 编辑","💾 Save":"💾 保存","No notes":"当前还没有备注内容","Saving Notes...":"正在保存备注...","Type your notes here":"在这里输入备注内容",ModelName:"模型名称","Models Required":"所需模型","Download Model":"下载模型","Source Url":"模型源地址",Notes:"备注",Type:"类型","Trained Words":"训练词",BaseModel:"基础算法",Details:"详情",Description:"描述",Download:"下载量",Source:"来源","Saving Preview...":"正在保存预览图...","Saving Succeed":"保存成功","Clean SuccessFully":"清理成功","Clean Failed":"清理失败","Saving Failed":"保存失败","No COMBO link":"沒有找到COMBO连接","Reboot ComfyUI":"重启ComfyUI","Are you sure you'd like to reboot the server?":"是否要重启ComfyUI?","NODES MAP":"管理节点与组",NodesMap:"节点与组","Nodes map sorting mode":"管理节点组排序模式","Search by Node ID/Name...":"通过节点ID/名称搜索...","No Nodes":"未找到节点","No nodes found in the map":"在工作流程中没有找到节点","No nodes found in the search":"未找到与搜索匹配的节点","Jump to this node":"跳转到该节点","Rename node":"重命名节点","Delete node":"删除节点","Rename group":"重命名组","Delete group":"删除组","Expand All":"展开所有组","Collapse All":"折叠所有组",Close:"关闭","Default automatic sorting, if set to manual, groups can be dragged and dropped and the sorting results saved.":"默认自动排序,如果设置为手动,组可以拖放并保存排序结果。","For drag and drop sorting, please find Nodes map sorting mode in Settings->EasyUse and change it to manual":"如需拖拽排序请在设置->EasyUse节点中找到管理节点组排序模式并修改成 manual",Queue:"队列","Cleanup Of VRAM Usage":"清理显存占用","Please stop all running tasks before cleaning GPU":"请在清理GPU之前停止所有运行中的任务",Always:"启用中",Bypass:"已忽略",Never:"已停用","Auto Sorting":"自动排序","Toggle `Show/Hide` can set mode of group, LongPress can set group nodes to never":"点击`启用中/已忽略`可设置组模式, 长按可停用该组节点","Enable Shift+Up/Down/Left/Right key and Shift+Ctrl+Alt+Left/Right to align selected nodes":"启用 Shift+上/下/左/右 和 Shift+Ctrl+Alt+左/右 键对齐选中的节点","Enable Shift+Ctrl+Left/Right key to normalize selected nodes":"启用 Shift+Ctrl+左/右 键规范化选中的节点","Enable Shift+g to add selected nodes to a group":"启用 Shift+g 键将选中的节点添加一个组","Enable Shift+r to unload models and node cache":"启用 Shift+r 键卸载模型和节点缓存","Enable Shift+m to toggle nodes map":"启用 Shift+m 键显隐管理节点组","Enable Up/Down/Left/Right key to jump nearest nodes":"启用 上/下/左/右 键跳转到最近的前后节点","Enable Alt+1~9 to paste nodes from nodes template":"启用 Alt+1~9 从节点模板粘贴到工作流中","Enable contextMenu auto nest subdirectories":"启用上下文菜单自动嵌套子目录","Enable right-click menu to add node A~Z sorting":"启用右键菜单中新建节点A~Z排序","Enable model thumbnails display":"启动模型预览图显示","Enable nodes runtime display":"启动节点运行时间显示","Enable chain get node and set node with parent nodes":"启用将获取点和设置点与父节点链在一起","Maximum number of model thumbnails displayed":"显示的模型缩略图的最大数量","Too many thumbnails will affect the first loading time, set the maximum value to not load the thumbnail function when there are too many models's thumbnail":"太多的缩略图会影响首次加载时间,当模型缩略图太多时,设置最大值以不加载缩略图功能","Too many thumbnails, have closed the display":"模型缩略图太多啦,为您关闭了显示","Shift+Up/Down/Left/Right can align selected nodes, Shift+Ctrl+Alt+Left/Right can distribute horizontal/vertical nodes":"Shift+上/下/左/右 可以对齐选中的节点, Shift+Ctrl+Alt+左/右 可以水平/垂直分布节点","Enable Shift+Ctrl+Left key to normalize width and Shift+Ctrl+Right key to normalize height":"启用 Shift+Ctrl+左 键规范化宽度和 Shift+Ctrl+右 键规范化高度","After v1.2.39, Ctrl+g can be used instead of it":"从v1.2.39开始,可以使用Ctrl+g代替","Styles Selector Display Type":"样式选择器显示类型","Styles Selector Display Type, if set to 'Grid', it will display as a Grid, if set to 'List', it will display as a list":"样式选择器显示类型,如果设置为“网格”,则显示为网格,如果设置为“列表”,则显示为列表","Use three shortcut buttons in the right-click menu":"在右键菜单中使用三个快捷按钮","Enable Nodes Map":"启用节点组管理","Display Groups Only":"仅显示组","Display Node ID":"显示节点ID","You need to refresh the page to update successfully":"您需要刷新页面以成功更新","Get styles list Failed":"获取样式列表失败","Get style image Failed":"获取样式图片失败","Empty All":"清空所有","Type here to search styles ...":"在此处输入以搜索样式 ...","Loading UserInfo...":"正在获取用户信息...","Please set the APIKEY first":"请先设置APIKEY","Setting APIKEY":"设置APIKEY","Save Account Info":"保存账号信息",Choose:"选择",Delete:"删除",Edit:"编辑","At least one account is required":"删除失败: 至少需要一个账户","APIKEY is not Empty":"APIKEY 不能为空","Add Account":"添加账号","Getting Your APIKEY":"获取您的APIKEY",Canceling:"正在取消","Cancel current run":"取消当前运行","Waiting for selection...":"等待选择中...","Using last selection":"使用上次选择","Pass Through":"跳过选择","Unknown Mode":"未知模式","Choose Selected Images":"选择选中的图片","Choose images to continue":"选择图片以继续","Please run first":"请先运行队列","Progress selected":"处理选中的图片","Progress selected image":"处理选中的图片","Please select images to continue":"请选择图片以继续",Background:"背景",Hat:"帽子",Hair:"头发",Body:"身体",Face:"脸部",Clothes:"衣服",Others:"其他",Glove:"手套",Glasses:"眼镜",Sunglasses:"太阳镜","Upper-clothes":"上衣","Top-clothes":"上衣","Bottom-clothes":"下身装","Torso-skin":"皮肤",Dress:"连衣裙",Belt:"腰带",Coat:"外套",Socks:"袜子",Pants:"裤子",Jumpsuits:"连体衣",Bag:"包",Scarf:"围巾",Skirt:"裙子","Left-arm":"左臂","Right-arm":"右臂","Left-leg":"左腿","Right-leg":"右腿","Left-foot":"左脚","Right-foot":"右脚","Left-shoe":"左鞋","Right-shoe":"右鞋","select mask components":"选择分割部位","select items":"选择项",Unlabelled:"未标记","shirt, blouse":"衬衫,女式衬衫","top, t-shirt, sweatshirt":"上衣,T恤,运动衫",sweater:"毛衣",cardigan:"开襟毛衣",jacket:"夹克",vest:"背心",pants:"长裤",shorts:"短裤",skirt:"裙子",coat:"外套",dress:"连衣裙",jumpsuit:"连体衣",cape:"斗篷",glasses:"眼镜",hat:"帽子","headband, head covering, hair accessory":"头带,头饰,发饰",tie:"领带",glove:"手套",watch:"手表",belt:"腰带","leg warmer":"护腿","tights, stockings":"紧身袜,丝袜",sock:"袜子",shoe:"鞋子","bag, wallet":"包,钱包",scarf:"围巾",umbrella:"雨伞",hood:"帽兜",collar:"领子",lapel:"翻领",epaulette:"肩章",sleeve:"袖子",pocket:"口袋",neckline:"领口",buckle:"扣环",zipper:"拉链",applique:"贴花",bead:"珠子",bow:"蝴蝶结",flower:"花朵装饰",fringe:"流苏",ribbon:"丝带",rivet:"铆钉",ruffle:"褶边",sequin:"亮片",tassel:"流苏穗",background:"背景",skin:"皮肤",nose:"鼻子",eyeglasses:"眼镜",left_eye:"左眼",right_eye:"右眼",left_eyebrow:"左眉毛",right_eyebrow:"右眉毛",left_ear:"左耳",right_ear:"右耳",mouth:"嘴巴",upper_lip:"上唇",lower_lip:"下唇",hair:"头发",earring:"耳环",necklace:"项链",neck:"脖子",clothing:"服装",s:"秒","No Node Templates Found":"未找到节点模板预设","Get Node Templates File Failed":"获取节点模板文件失败","Node template with {key} not set":"未设置快捷键为{key}的节点预设","ComfyUI Basic":"ComfyUI 基础节点","Recommend Nodes":"推荐节点","Others A~Z":"其他节点 A~Z","The maximum number of inputs is 20":"输入数量最大20个","The maximum number of outputs is 20":"输出数量最大20个","Stop recording":"停止录音","Voice input":"语音输入",Continue:"继续",Stop:"停止",now:"当前",prev:"上一步","Add New Tab":"添加新标签","Add Angle Prompt":"添加角度提示词","Invert Rotate Mode":"反转旋转",Hollow:"镂空展示",Reset:"重置","Angle Prompt":"角度词",Image:"图像",F:"前",B:"后",U:"顶",D:"底",L:"左",R:"右",Rotate:"旋转",Vertical:"垂直",Zoom:"缩放"},Ne=(e,t=!1)=>{switch(Le("Comfy.Locale")){case"zh-CN":case"zh":return Ee[e]||e;default:return e}};const Ce="comfyui-easyuse-",Ie="dark-theme",Te={PIPE_LINE:"#7737AA",PIPE_LINE_SDXL:"#7737AA",INT:"#29699C",X_Y:"#38291f",XYPLOT:"#74DA5D",LORA_STACK:"#94dccd",CONTROL_NET_STACK:"#94dccd",FLOW_CONTROL:"#373780"},Oe=0x4000000000000,Me=["loaders","latent","image","mask","sampling","_for_testing","advanced","utils","api","api node","加载器","Latent","图像","遮罩","采样","_用于测试","高级","工具","API","api 节点","chargeurs","latent","image","masque","échantillonnage","_pour_test","avancé","outils","nœud api","загрузчики","латентный","изображение","маска","выборка","_для_тестирования","расширенный","инструменты","api","api узел","로더","잠재","이미지","마스크","샘플링","_테스트용","고급","도구","api","api 노드","ローダー","潜在","画像","マスク","サンプリング","_テスト用","高度な","ツール","api","apiノード","cargadores","latente","imagen","máscara","muestreo","_para_pruebas","avanzado","herramientas","api","nodo api"],De={selfie_multiclass_256x256:["Background","Hair","Body","Face","Clothes","Others"],human_parsing_lip:["Background","Hat","Hair","Glove","Sunglasses","Upper-clothes","Dress","Coat","Socks","Pants","Jumpsuits","Scarf","Skirt","Face","Left-arm","Right-arm","Left-leg","Right-leg","Left-shoe","Right-shoe"],"human_parts (deeplabv3p)":["Background","Face","Hair","Glasses","Top-clothes","Bottom-clothes","Torso-skin","Left-arm","Right-arm","Left-leg","Right-leg","Left-foot","Right-foot"],segformer_b3_clothes:{Background:0,Hat:1,Hair:2,Sunglasses:3,"Upper-clothes":4,Skirt:5,Pants:6,Dress:7,Belt:8,"Left-shoe":9,"Right-shoe":10,Face:11,"Left-leg":12,"Right-leg":13,"Left-arm":14,"Right-arm":15,Bag:16,Scarf:17},segformer_b3_fashion:{Unlabelled:0,"shirt, blouse":1,"top, t-shirt, sweatshirt":2,sweater:3,cardigan:4,jacket:5,vest:6,pants:7,shorts:8,skirt:9,coat:10,dress:11,jumpsuit:12,cape:13,glasses:14,hat:15,"headband, head covering, hair accessory":16,tie:17,glove:18,watch:19,belt:20,"leg warmer":21,"tights, stockings":22,sock:23,shoe:24,"bag, wallet":25,scarf:26,umbrella:27,hood:28,collar:29,lapel:30,epaulette:31,sleeve:32,pocket:33,neckline:34,buckle:35,zipper:36,applique:37,bead:38,bow:39,flower:40,fringe:41,ribbon:42,rivet:43,ruffle:44,sequin:45,tassel:46},face_parsing:["background","skin","nose","eyeglasses","left_eye","right_eye","left_eyebrow","right_eyebrow","left_ear","right_ear","mouth","upper_lip","lower_lip","hair","hat","earring","necklace","neck","clothing"]},Re={ALWAYS:0,NEVER:2,BYPASS:4},Pe="easyuse_nodes_map",Ge="data:image/jpeg;base64,/9j/4QAYRXhpZgAASUkqAAgAAAAAAAAAAAAAAP/sABFEdWNreQABAAQAAABkAAD/4QMyaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLwA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA5LjEtYzAwMSA3OS4xNDYyODk5Nzc3LCAyMDIzLzA2LzI1LTIzOjU3OjE0ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgMjUuMyAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo2RkFERDc2MUNDRkIxMUVGQjFENkE0NzFCNTUzRkQ0MCIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo2RkFERDc2MkNDRkIxMUVGQjFENkE0NzFCNTUzRkQ0MCI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjUyMkMxRkIyQ0M0QzExRUZCMUQ2QTQ3MUI1NTNGRDQwIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjZGQURENzYwQ0NGQjExRUZCMUQ2QTQ3MUI1NTNGRDQwIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+/+4ADkFkb2JlAGTAAAAAAf/bAIQAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgICAwMDAwMDAwMDAwEBAQEBAQECAQECAgIBAgIDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMD/8AAEQgBgAGAAwERAAIRAQMRAf/EAMoAAQADAQEBAQEBAQAAAAAAAAAICQoHBgUEAwECAQEAAwEBAQEAAAAAAAAAAAAABQYHBAMCARAAAAYCAQIDBAQJBwoGAwAAAAECAwQFBgcIERITFAkhFRZXIpYXGTEy1bbWd5cYOEEjM9Q2djdRsbMktHWVtbd4QlIlJ9dYNJQ5EQACAQICBQYJCQYFAwUBAAAAAQIRAwQFITFREgZBYXGBkROhsdEiMpJTFBbBQlJy0nNUFTXworIjMzThYoKDJMLidPFDk7MmNv/aAAwDAQACEQMRAD8AxHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+lZU1vSrht3FVZVLljWwbmvRZQZUFc6otGEya21hplNNHJrbGMonGH0dzTyDJSFGXtH1KE4UU002k1VUqnqa5nyM+YyjKu606Omjata6T5o+T6LYfT19PfAOY+B59lmXZ7mGJTMQy6Hj0WJjkOlfiyosmmYsjkyHLOPId8cnXTT0T2pJKSP2mfst3DnDmHzrD3L165OEoTUfNS2V5Sv5xnF7LbsLduEZKUa6a7acg9Qr098A4cYHgOWYjnuYZbMy/LpmPSomRw6ViLFixqZ+yKTHcrI8d3xzdaJPRXck0qM/YZe1xHw5h8lw9u9ZuTnKc3HzktleQZPnF7Mrs7dyEYqMa6K7acpU8KiWA/fAqrS1OYVXWz7I6+BKtZ5QIciYcGrgoJybZTCjtuHGgQ0GSnXl9rbZH1UZD6jCc67iboquirRLW+jnPxyjGm80qunXsPwD5P0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC1T0sOKGpeR20re72pk1HZRNblDuYWmnnTTa5wvxEdlzbsvEhEzCKaUptEqOwbipEhxtuR4bCuyTbOE8oweZ4uU8XOLVqjVvllzvbFcqWt0TotcBn+YYjBWFDDxac9G/yR5l/mfJ4NOq/Dmtwc19y616zWeFXYls3Eq9xrXGcMQ0oRXIQk1oxW+ZiN+LMw6a57DbQlTkBxXjxyM/FZf0HPMhw+cYbd0QxUF5kqav8AK9sX4Na5U6jlea3suvb2mViT86Pyrn8ep82P3amq880rnd/rfZWPTMZy3HJao0+vlp6tvNH1VFsq2WjrHsqixY6OxpTKlsvtKJSVGQxvF4TEYHESw2Ki43ovSvlW1Pka0M0fD4izirSv2JKVuX7UexrlRog9DH/Bvdv6zaj81Yo0fgP+yv8A3q/hRTeK/wC5tfUfjHrnf4N6S/Wbb/mrKDjz+ysfev8AhY4U/ubv1F4zO/qvVeebqzug1vrXHpmTZbkctMaBXxE9G2Wi6KlWVlLX0j1tRXMdXZMp5SGWGkmpSiIZxhMJiMdiI4bCxcr0noXyvYlyt6EXLEYizhbTv35KNuP7UW1vkRr/AOF3BfXXEzXEulejVeZ7HzOsKLs3MpkBt+PbR32/5/EaWNNaUqNhUVSjSbLiSXYLLxpKf6JljZMkyHDZRhnBpTxM158mtf8AlVfm+PW+RLOMzzW9mF9SVY2IvzY7Od/5vFqXPQD6oXErWPGbalVY6vymkj02xW5l2rUvm1O5FgR+MolS4jCScMsItJCXEQDfWh1p1pxlHiob7kZ5xVk+FyvFqWFnHcuVfd8sP+18ledaaFvyHMb+Ow7jfi96Gjf5Jf8AcuXtKvBVSeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPY6+2BmOq8yx/YGAX87GMvxee3ZUtzXOEh+NIQSm3G3G1pWxLhS47i2ZEd1K2JDDi23EqQpST9sPiL2EvxxGHk43oOqa/bVyNamtDPK9ZtYi07N5KVuSo0/2/9DTFr71i9OzuONnsDP4XkN34yzHppWqapTrSc1yKQysq+6xee+mSiDiM1bSnZy3zcfqe1bRpkqOKqVqGH4zwUsseIxCpj46O7XznyOL5Ivlrpjq06K0e9w3iVjVZsuuFlp3381bHz7NvNppnS33vnY3JDZVztHZ1sVlfWZJiwocdKmajHKKO8+7XY5QQ1LcOFT1pyVmhJqW4444t51bjzjjis2zDMMTmeKli8U63HqXJFckUuRL/ABelsumEwlnBWFYsKkF2t7Xzv/DUNZ8iN5aarLKm1VtTNMAqreeizs4GMXUmrjTrBuOmKiXIbYUROPpjISjr/wCVJf5CDC5lj8FBwwl2duEnVqLpVn5fwWExMlLEW4zklRVVRszkRvLctZW021dqZpn9VUT12dZAye6k2kaDYOR1RVy47b6jJt9UZakdf/Ko/wDKYYrMsfjYKGLuzuQi6pSdaMWMFhMNJyw9uMJNUdFQaE3zsbjfsqm2jrG2KtvqwlRZsOQlT1RkdFIeYdsccv4aVtnNp7I4yDWklIcbcbQ80tt5ttxLL8wxOWYqOLwrpcWtcklyxa5U/wDFaUj9xeEs42w7F9Vg+1Paudf4ajRbsH1i9OweONZsDAIXn935MzIpouqbVTrqcKyKOygrC6yiewmMidiMJbqXYK2Dbftu5DRJjKKUqLpOI4zwUcsWIw6rj5aO7fzXyuT5Yrkpplq0aaUuzw3iXjXZvOmFjp3185bFz7dnPormd2DsDMdqZlkGwM/v52T5flE9yyurmxcJb8mQsktttttoShiJCiR20Mx47SUMR2G0NtpShKUll+IxF7F35YjEScr03Vt/tq5EtSWhF4s2bWHtKzZSjbiqJL9v/U8cPE9QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACWfG3hNyE5UrlStW4kynF6+ScKyzrKJxUOHw5pIStUJuwWzJm201pC0m6zAjS3WErSbqUJUkzl8syPMc2q8JD+UnRzk6RT2V1t8yTpykfjc0weX6MRL+Y9UVpf8Ah1tE5vuQeTvs/wDcXSH4C6/+t5x+H+Uv7B/yGJ74FzX2ljtl9givinAfQu9kftD7kHk78xdH/wDGs4/QMPgXNfaWO2X2B8U4D6F3sj9ofcg8nfmLo/8A41nH6Bh8C5r7Sx2y+wPinAfQu9kftD7kHk78xdH/APGs4/QMPgXNfaWO2X2B8U4D6F3sj9ofcg8nfmLo/wD41nH6Bh8C5r7Sx2y+wPinAfQu9kftD7kHk77f/cXSH4D6f+t5x+H+Qv7B/wAph8C5r7Sx2y+wPinAfQu9kftEHOSvCPkFxS8jN2ljEN3F7SV7vrc4xWx9+4nKsTQ66mudmHHhWFXPdZYWtpqbFjKfQlRtd/YvtgszyLMcppLFwXdN0Uousa7ORp9KVeQlMDmmDzCqsSfeLXFqjpt2PqbIkiHJEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOxaB0plfIfbuFagw0kN3GX2hRnbF9px2FR1EVl2dd39glr6Zw6eqjOvqSRkp1SSbT9NaSPty/A3syxkMHY9Ob18iWtt8yWnwHNjMVbweGlibvoxXa+RdbNxWrdaYjp3XuJaywStRVYrhtNFpqqMXap5xEdPWRPnPJSjzVpZy1rkSnjLuekOrWftUN2wmFs4LDQwuHVLMI0XlfO9bfKzLMRfuYm9K/ddbknV/tsXIRcu/Ui4S47dW+P2u+6Buzo7KZU2KIeP5vaw0ToD640pEW0qcYm1diwh5tRJejvOsudOqFqT0MRVzibI7c3bniI70W06KTVVzqLT6U6HdDJM0nFTjZe61VaUvA3VdZ8z7zrgt8/aj6obG/Q4fHxTkP4iPqz+yfX5FmvsX2x8o+864LfP2o+qGxv0OD4pyH8RH1Z/ZH5FmvsX2x8o+864LfP2o+qGxv0OD4pyH8RH1Z/ZH5FmvsX2x8o+864LfP2o+qGxv0OD4pyH8RH1Z/ZH5FmvsX2x8p9Ok9SPhNkV1UY/U74o37W9s4NPWMv43ndew7PsZLcSG09PscWi18JpyQ6lJuvutst9eq1JSRmPuHE2R3JxtwxEd+TSXmyWl6tLjRdeg/J5JmkIucrT3UqvTHUusk/tbWOI7m11l+r86rm7PF8zpZdNZMqQ0p+N46O6JaV63m3URranmoblQ3ySamJLKHC9qSEpi8LZxuGnhcQq2pxo/KudPSnyM4MPfuYa9G/adLkXVeToep8xh75AaUyrjxt7N9RZg0s7PEbd6NEsfBNiNf0UgilUORwEmtwih3dS81ISnuUplS1NL6OIWRYVmOBu5bjLmDvelCWvauSS5mtPgNTweKt4zDQxNv0ZLVsfKupnGxxHSAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABqS9H/iYnVmrZHIPMa5TWebfr22cXjTYqW5WN62ZlKeiOsqWgn2pGcSmm5zntNKoLEI09qjcI9W4Nyj3TCPMby/5F5ebX5sP+7X0KPOULiPMPeMR7nbf8m29PPL/t1dNTuPqd8sS416GlUOL2hwtsbbasMYwxUcz83SU7aI7eXZclZJ7Y7lXXzkR4i+5LhTpbTiCUllzt7+Kc3/ACzL3btOmLvVjHmXzpdSdFztPkZy5Fl/v2L37irh7dHLnfIut6+Zc5j8GNGjgAAAAAAABsB9MLli3yR0JBx3JrEn9q6iYrsTy1MqV41jkFK1GJrFs1X4i1yH1WkKOqNMdWpS12MR5xXal5sj2ThbN/zPL1buv/l2aRlta+bLrWh86b5UZxnuX+5Ytzgv+Pcq1sT5Y9WtczWw4h6wHExG09WNchcPrvFz3T9YtvKm2CPxr3VqH5M2etSSIyck4VNlu2CD6oIoLswzNSktJHDxllHveE/MbK/5Fledz29b9V6eje5jq4czD3fEe53H/JuPRzT/AO7V00MtoykvoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABN7gDxWlcrN+0eM2cN9zWuInHyzaE1txyOg8eiPl5XHm5LS2nW52WT0Jho8JaXm45vvo/oDE7w9lLzbMI2pp+6w86b5tnTJ6NtKvkIvN8wWX4N3Iv+fLRHp29WvsXKbLbi4xvBsZsby7nVWMYlilNIsLKwlLYraaioqaGp6Q+6v+bjQoFfCYM+hESUIT0Iv5BtU52rFp3JtQswjVvUkl4kkZpGM7s1CKcrknRLW22YneZfJS05U76yzZ75TYmM97VBgFFNcJSqLDaglNVrK2kqW2zOtX1Oz5aUqWRS5biUqNCUdMOzrM55tmE8U6q1qgnyRWrret87NPyzAxy/CRsKjnrk9snr7NS5kRWESSAAAAAAAAASr4Y8lLXitv3EtnRzffxl1w8a2HUsEa1W+C3MiMVy200SkeLYVTkdmwhp7kEqZDaSo/DUsjlskzOeU5hDFKvdejNbYPX1rQ1zpchH5ngY5hhJWH6euL2SWrqep8zNr1Pb4znuK1t5Ty6zJ8PzGhi2VbMaJqfT3+PX0BEiM+lDqFMy6+yr5RH2rSaVtr6KL2mQ3GE7WItK5Bqdmcap60014U0ZhKM7NxwlWNyLo9qa8jMa3qA8WZHFXkDe4zWRn/ALOcwJ7MdaTlNOeCigsJbpSsbVIPvbcnYjYd0RZd5urjeXfWSfMJIYrxDlLynMZWor/jT86D5m/R6YvR0UfKaVk+PWYYNTl/Wj5sunb16+mq5CD4giVAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADXh6SeoqPXPELFMtix4qsk29Y22Z5FZIjNImORYdnPx/HKhyWk1PSINZW1xvNNqMktPzZBpSRrUati4QwdvDZNC8ku9vNyk+XQ2kuhJV6WzOuIsTO9mMrb9C2lFLqq31t+BFkt3R0uS1M+hyOorL+jtY64lpTXUCLaVVlEc6eJFn105p+JMjOdPpNuIUhX8pCzXLcLsHbuxUrbWlNVT6U9ZCRlKElODamtTWhrrOYfu6cfPkVpv9mOE/kMcv5bl34ex6kfIe/vuM9rd9aXlH7unHz5Fab/ZjhP5DD8ty78PY9SPkHvuM9rd9aXlH7unHz5Fab/ZjhP5DD8ty78PY9SPkHvuM9rd9aXlKDfVm4QXtHnNHu/R+sULwG4x2HS5xRa7xhCGsYyankPMxr+VRUEJtEWpyCokx2TfbZNCJUNw31JU+13Z9xfkVy3fjjsBa/48o0koR9GS5aJamqKtNa060W7h7NYTtPC4q5/OUqxcnrT5KvlTr1PRqZ/vpL8I7y8zfId27w1iprBKfHpFHg1HsTFm1xMryC7eNiwvYlLfxlFKrcdq4jrJPOR/CckzUmytS2HOxwhkVy5fljsfa/48Y0gpx9JvW6PkitFaa3o1McQ5pCNqOFwtz+a5Vk4vUlyVXK34Fp1l+P7unHz5Fab/AGY4T+QxoP5bl34ex6kfIVH33Ge1u+tLyj93Tj58itN/sxwn8hh+W5d+HsepHyD33Ge1u+tLyj93Tj58itN/sxwn8hh+W5d+HsepHyD33Ge1u+tLynUKWjpcaqoFDjtRWUNHVx0RKympoEWrqq6I318OLAr4TTESHGb6/RbbQlCf5CHVC3C1BW7aUbaWhJUS6EtR4SnKcnObbm9belvrK2/Vp1Dj+xuIWW5dPYJOSagnVWaYvYIS2TzaJlrXY/kNY64ror3fY1Nkbq2yMu6REYV7ewiFZ4vwdvE5NO9L+rZalF9LSa6Gn2pE1w9iZ2MxjbXoXE4vsbT6muxsyGjHTRgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA2oenH/BHx6/udM/Oa+G38NfoWG+o/4mZjnX6pe+t8iKzvVF5qcmOPnI2lwbUOy3sQxaTqvGsifq2saxC2J24sMhy6FLmKlX1BaTSU5Gq2E9hOE2kkdSSRmZnV+Ks8zTLsyjYwd3ctO1F03YvS3JV0pvkROZDlmBxmCd3Ew3riuNVq1oSWxraVw/ekc6vnpK+o2tf0OFa+K8+9v+5D7JNfkGU+y/el9ofekc6vnpK+o2tf0OD4rz72/7kPsj8gyn2X70vtErOIPq5bYx7Z7VZyny9/NtY5M2zWvXrON0FfZ4BYm8go2QojYvSVz9xSdqlInxux2Qls0vMdy2zYflsm4wxdvF7ubT38LPRXdScHt81Kq2rXyrVRx+Y8O4edjewEd2/HTSraktmluj2Pqe1aWMQzLE8/x6tyzCMkpcsxm3YRJrb3H7GLaVkxpaSV1alxHHWvER3dFoMyW2rqlREZGQ06zfs4i2r1iUZ2pamnVPsKRctXLM3buxcZrWmqMZfmWJ4Bj1llmcZJS4njNQwuTZXuQWMWqrIbSEmr+dlzHGmvEX29EIIzW4r6KSMzIgvX7OHtu9flGFpa23ReEW7Vy9NW7UXKb1JKrM0/L71ctr5DtByt4sZdIwnWWMtSaxm9exygsLLYFgb5ePkTkXKKSxep6ZCWiRAj9jUhTZqekElbqWGMxznjDF3MXu5TPcwsNFd1Nze3zk6LYtfK9dFd8u4dw8LFcfHevy00q6RWzQ1V7fBtcU/vSOdXz0lfUbWv6HCJ+K8+9v+5D7JIfkGU+y/el9ofekc6vnpK+o2tf0OD4rz72/7kPsj8gyn2X70vtFj3pdc1OTHIPkbdYNt7Zb2X4tG1XkuRMVbuNYhUk1cV+Q4jCiTEyqGgq5pqbjWj6ew3DbUS+ppMyIysvCueZpmOZSsYy7v2lak6bsVpTiq6Enyshc+yzA4PBK7hobtx3Eq1b0NPa3sLMfUc/gj5C/3Oh/nNQi0cS/oWJ+ov4kQeS/qln63yMxXjEDTgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA2oenH/BHx6/udM/Oa+G38NfoWG+o/wCJmY51+qXvrfIiij1rP4use/Uhhv51Z8KDxx+sR+4j/FMtfC/6dL72XiiVDCnFjAAAD2mI7I2Hr9x57A88zPCXZCiVIcxLKLvHFyFERJI31U86GbxkkiL6XX2D3s4nE4d1w9ydtv6MnHxNHlcsWb2i9CMlzpPxjL9kbE2C6y/nueZnmz0bp5d3Lcnu8jcjmSCaI2F3E6YpoybLt+j09nsC9icTiXXEXJza+lJvxti3Ys2dFmEYrmSXiPFjwPUAAALefRT/AIush/UhmX51YCLjwP8ArEvuJfxQK5xR+nR+9j4pF6/qOfwR8hf7nQ/zmoRfuJf0LE/UX8SKpkv6pZ+t8jMV4xA04AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANiHpV7Ix3POGWtKmolMKt9cqvMJymtS8lcmuso13YWsF15v2OIZtKWzjyG1dO0zWpJGZoV02bhPE28RklqEH59qsZLY6trtTTM3z+xOzmc5S9GdJJ7VSngaaOr744IcbOSeax9g7cxG3vsoi0EDGWJkLLslo2G6itl2M6LH8nT2UOMpaJNq+o1mk1q7iIz6EQ7MwyDLMzvrEYyDldUVGqlJaFV6k1tZz4TNsdgbXc4eSVtuupPS6bVzHFvuj+Dfy1yH9o+dfl0cPwfkPspevPynT8RZr9OPqx8g+6P4N/LXIf2j51+XQ+D8h9lL15+UfEWa/Tj6sfIPuj+Dfy1yH9o+dfl0Pg/IfZS9eflHxFmv04+rHyD7o/g38tch/aPnX5dD4PyH2UvXn5R8RZr9OPqx8g+6P4N/LXIf2j51+XQ+D8h9lL15+UfEWa/Tj6sfIPuj+Dfy1yH9o+dfl0Pg/IfZS9eflHxFmv04+rHyD7o/g38tch/aPnX5dD4PyH2UvXn5R8RZr9OPqx8g+6P4N/LXIf2j51+XQ+D8h9lL15+UfEWa/Tj6sfIdq0NwS418a80lbA1Jh1rRZRMoJuMvTp2XZNete6LCZXT5bCIdvZy4iXHZNUwfidnekkmRGRKPr3ZfkGWZZfeIwcHG64uNXKT0Np6m+ZHNi82x2OtdziJJ2066ktKrsXOcm9VTZOOYFwz2TT3EpgrnZCqXCsVrDfbRKsLB+7rrWwfaaNXirj1VNWvvuqSk0kokIUZG4nrx8WYq1h8kuwm/Pu0jFbXVN9iTZ0ZBYnezOEorzYVk3s0NLtZjwGMmkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB3vj5yY3Fxhy9zMdRZS7SyZrbEe+pJjRWOMZRCjKdUxDyGkeWmPNTHN9zwHkm3KjeKs2HWzWozkMuzTG5Ve77Bz3W9aemMlsa5eZ61yNHJjMDhsfb7rExqlqeprof7J8pahC9c7cDcZpFho/W0qWSUk8/Du8ngx3FElJKU3FedsHGiNZGZEby+hGRdT6dTtkePMal51i03zOS8pAPhTDV0XZ06Efq+/R2n8h8A+s+Rf1cfvx7i/w9v1mfnwph/az7EPv0dp/IfAPrPkX9XD49xf4e36zHwph/az7EPv0dp/IfAPrPkX9XD49xf4e36zHwph/az7EPv0dp/IfAPrPkX9XD49xf4e36zHwph/az7EPv0dp/IfAPrPkX9XD49xf4e36zHwph/az7EPv0dp/IfAPrPkX9XD49xf4e36zHwph/az7EPv0dp/IfAPrPkX9XD49xf4e36zHwph/az7EPv0dp/IfAPrPkX9XD49xf4e36zHwph/az7Efmmeuft9yM6iv0fraLLNKiZfl3mTzo7azQokKcisu17jqUrMjMidR1IjLqXXqX5LjzGNebYtJ87k/Ifq4Uw1dN2dOhFWfInk7uHlHmDWY7byT3o9AZdiY/QVrCq3FsXhPm0qTGoKYnXkRTlrYQp99xb0qQbaPFdWTaCTVMyzTG5re77GSq1qS0RiuZc/K9b5WT2CwOGwFrusPGietvS30v8AZIj+I47AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAmTwx4dX/MrMcvw/H81p8JfxHGmMlfm3FXNtGZjL9pGqyistQpEdbTiVyCX3GZl0Lp0E3kmS3M6vTs27ituEa1abrppyEZmeZRyy3G5KDmpSpodOSp7Dmb6fmzeG8PEr+8va3O8Nyp6RV/FVDWza+JTZGylySzRWseY9IcZesa5pb8Vzu7XiZeSREbR93tnfDuKyWMLk5K5Yno3kmqPY+laV0M8sszixmTlCKcLsdNG61W1dD19RAYV4mD2OvMQe2DsDBsBjTWq2Rm+Y4ziDFi+yuQzXvZLdQqZqa8w2ttb7UVc0nFISpJqJPQjLr1HvhrLxOJt4dOjuTjGuzeaVfCeV64rNmd5qqhFunQqlh/Lb0xcx4m6lVti82pjOYQU5LS417nqsftK2X41yiatuV5mZMfZ8NjyR9ye3qfcXQxZM44WvZRg/fJ3ozjvJUUWtfWQ2XZ7bzDE+7xtuL3W6tp6uoq/FUJ4AAAAAAAAAAu3wv0UNiZnh2J5hH3phcFjK8aoslYhPYpeOvQ2b2ri2jcV11FilDjkdEokKURERmXUiF6s8DYi9ZheWIglOKdN18qrtKtd4otW7krbsybjJr0lyOmw9N9xZsn5+4P9UL78pD1+AsT+Ih6r8p5/Fdn2MvWXkPMZR6HW96+CuRiW29W5LMbLuOBbMZPjBukXf3IjymqzIWVPH0T2k54SDMz6qT09vld4Ex8Y1s3rUpbGpR8NJHpb4qwrdLtu5Fc1H5CrbevHTcXG3KmsQ3Dhk7FbGa1Jk0003YtjR5DBiv8Al3Z1Dd1z0musGUKUg3GyWUiOTqCebbUokiqZhluNyy73ONg4Sep61JbU1ofjXKkT+ExuGx1vvMNJSS18jXSv2T5DiQ4DqAAAAAAAAAAAAAAALAeD/ADMeaLmX2cTNa3XWFYZIr62xyOXSuZNPnXVkw9KarKqgZt6PxiixGUuSHXpbCEE82SPEM1kixZFw7ezvfmritWINJypvNt8iVVqWurWvlIfNc4t5ZuxcXO7LTStNG1uj8RzrmTw+zThxsauwrJb2sy2kySoO9xHLauM5Wpt4LEg4U9iwpX5Mx+ntK+WkicaJ+SyppxtaHlGpaW+bOsmv5LiVYuyU7clWMloqtTqtNGulrVp2e2WZlazKy7sE4zi6NPTTZp5U+oiGIYkgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALwvQz/xt3R+qyB+dtYL3wH/e3/ul/EVXir+1tfePxGgTcut9c7/wbO9E5wuLPjX+MwZFrXsOx1XdAxbS7RrFssgtupd8pMiXmOyHYLykKbVIgrSZKSS0nomNw2GzCxcwF+jjKKquVVruyXOmnR7UVDDX72DuwxdrQ1LRsdKVXY9PSYn+QOjsx457ay/Umbx1ptMZsFIg2ZRnI0HJKGSZvUmS1RLU4S6+4gmlwiJazZd72Vn4jSyLDsxwF7LcZPB3/Si9D5JLkkuZrsdVrRp+DxdvG4eOItejJaVsfKn0f4n9eNf8Rmgf116r/PqiDK/1PD/f2/40Md/ZXvup/wALNMPrL/wbOfrTwb/QXw1DjT9Ef3sPlKPw1+p/7cvkMmYyE0I6RrzTu2Nty3IOsNbZxn8hl1DMr4Sxi4vWIK3CSaTsZdfEfiVrfaojNb620ER9TMiHVhsFjMY93CWrlx/5Yt06WtC6zwvYnD4ZVvzhDpaXZtOg5lxH5P6/rFXWYaD2vS0zTUh6TbO4VeSqyCzFShbztlPgRJUWsaShfUlSFtEskqNPXsV06b+TZrh4797D3lDbutpdLVadZ42sxwF6W7bvW3LZvKvVXX1EdhGHaAAAAAbb7zJ73CeBdvmeL2C6nJsR4i2GT47atNR5DlZe0OmnrWosG2JjMiI8uHYRG3CQ62ttRp6KSpPUj3S7duWMglftOl2GDck9jVuqenYzLoW4Xc2Vq4qwliEmtqc6MzAfebc6Pn/dfVPXn6IDK/inPvxEvVh9kvf5FlXsV2y8p0rWHq4cxsGyGLYZbmVVtbHfMNKssXyrGsaqykxiUkpCa+9xelp7atmOMkZNrWqSw2v6SmF+0j6cJxhnWHuKV6avW+WMoxXY4pNeFcx4Yjh3LbsKW4u3Pkabfam2n4Okvv5c4ZhPMDgpkeV10BM0p+qmt46xlyW4zVpV3ddiy8tqo7b8okogSrOvU7Vy+5SCJqU6lRp6dydBzixYznIZXYqtbPew2pqO8teqq819LKjl127luaxtydKXNyWxqtH001roRj7xzC8xzA5acSxPJcpVXkwc9OOUVpdnCKUbpRjllWRZRxikHHc7O/t7+xXTr0PpjVqxfv17mE5017qbp00NHuXbVqneyjGu1peM+tXau2Xb5ZHwKr17m9hnEpKHI+Hw8VvH8neacSlxDyKJuCqzNlTayV3+F29p9evT2j7jhMXO8sPC1cd9/N3XvdlKny8RYjb76U4K19Kqp26j2myuNW/9O1Ma/wBoaf2BhFDLlJgs3l9jdhFpvPOEamYTtqTS4EeZISlRtNOOJcdJCjQSiSrp74rK8xwUFcxdm5C23SrTpXZXUeVjHYPEy3LFyEp7E9PYcerKuzurCHU01dOtrWwfRFgVlZEkT7CdJdPtbjw4cVt2TJfcP2JQhKlGf4CHFCE7klC2nKb1JKrfQjqlKMIuU2lFcr0IktH4R8vZdSm6Y43biXBXEcmoJWDXbc9yO2lazNuodiot3HnEo6ttJYN13qXYlXcnrKLIc5cO8WGvbtK+i69mvqpU4XmuWqW479utfpKnbqI85Ji2T4bbP0OX45fYpeRSSqTTZJUWFHbR0r6khT9daR4sxol9p9DUguvQRt2zdsT7u/GULi5JJp9j0nZbuW7sd+1JShtTTXaj+2O4bl+YLlt4limSZQ5ASyucjHaK0u1w0SDcSwuWmsiyVR0vqaWSDX0JRpPp16GP21Yv369zCc6a91N06aH5cu2rVO9lGNdrS8Z+e/xjJMTmorcpx68xqxcjImNwL+pn001yI64603KRFsY8Z9cZx1haUrJPaakKIj6kY/Llq7ZluXoyhKlaNNOnWfsLlu4t63JSjtTT8R8MeZ9nv06n2mutTco1rn66hcErNNqnDciVWqrVMeaTYJnFXHFOCcU/EJ0leH4f0uvT2jp9zxm7v91d3KVruypTbWmo8PecNvbveQ3q0pvKtdmsm5wC5k7n4vzc3qcA1TO3Rh+THW2mQ4nXR7pqwqbeIl6JBuod1SU18uuTMiqWy82/Eebf8Js0GhSFd07w9nWOyt3IYey79mVG4qtU9SdUnSq0Oq06CLzjLcLjlCV64rVyNUm6Ua2UbVe05nzi5M7Y5ObVgZJtHCH9Zox7H2ajE8BkRLWM9T08mS9MkT5ci5iV820n201SjXJKOw0ptltCGy8MzVy59mmMzTFq5i7btKMaRhR6Ft0pNtvlotS2HvlWBw+Bw7hh595vOrlo0vZorSmwhgIMlCReHcROUGwKlu9xDQe1bqlfQy5FtmsNuY1dOafR4rT1dLnRYrFiwpvofewpxBEZdTLqXWTs5Nm2Ih3lnD3XDbutJ9FaV6jiuZlgLMty5etqWyq8NNRznYGoNraolphbN1tnOASHJDsaOWX4tdY+zNdZNXf7vk2cOPGsWjSg1JcYW42tH0kqNJkY5cTgsZg3TFWrlt1p50Wq9Dao+o9rOJw+IVbE4T6Gn201Hi6mntr+xjVFFV2N1bTVLRDq6mFJsbGWttpbziI0KG09JfUhltSzJCTMkpM/wEY8IQnckoW05TepJVfYj1lKMI702lFcr0I+9kOvs+xKIzYZXhGX4zAkSUw2JuQ41c0sR6Wtp15MVmTZQozLklTLC1kglGo0oUfToRj1u4bE2Y7163OEW6VlFpV60fEL9m6923OMnzNPxHkB4Hqe4stY7Kpqp29t9e5xVUbDTL79zZYnfwapliQttth52xlV7URtp9x5CUKNZEo1kRdTMh0SwmKhDvJ2ritrlcWl20oeMcRh5S3Izg57FJV7Knhxznse9qtVbQvK+LbUmt89uKqa2bsKzqsPyGwr5bRLU2bkWZErno0hsnEGnqhRl1Iy/kHTDB4u5FTt2rkoPU1GTT66HjLE4eEt2dyCkuRySfjPDOtOsOuMvNuMvMuLadadQpt1p1tRocbcbWRLQ4hZGRkZEZGXQxztNOj1nsmmqrUfzH4AAAAAAAAAAAAAAAC8L0M/8bd0fqsgfnbWC98B/wB7f+6X8RVeKv7W194/ESB538psk4meozqLYFeubOw+x4+YdRbIxeO6Xh5FiUnZ+1zkKaYcUlg7ujcPzle4ZoMn2zaNaWXniVI5/m1zKOJbOIjV2Hhoqcdsd+54VrXZqbOPKcvhmOS3LLorqvNxex7sPA9T7daRIr1C+LuM819AUG6dOOR8m2BimMnlOu7OlUTidgYRZtNWlhihpNKXH5jjRHJrW1kTrM4nI/ag5LvSS4jyq1nmXRxuDpLEQjvQa+fF6XH5Y7Ho5WcWT4+eV4x4bE+bZlKkk/myWivyPm08iMz/ABuQtvkdoRtxKkLRu3VqFoWk0rQtOd0RKSpJkRpUky6GR+0jGYZYqZnh09ff2/40XnG/2V77qf8ACzS/6y/8Gzn608G/0F8NP40/RH97D5Sj8Nfqf+3L5CiD0+uIv73W7Cx2+dnQtaYVAayfYc+vcJiXIhKlJjVWMwZRko4s/I5aVp8UkmpqKw+tJktKOtA4dyf84x3d3KrC21vTa8EVzy8SZbM4zH8uwu/CjvzdI18L6vHQ0DcqOamjvTyxjFNT4FryutspeqPPY5rLF3oWMUOO0JvOx27rJbFqJPfinZzGXTaSmM/KnONOrcWj+kVombZ5gOHLUMJYtp3d2sYRpFJbZPTSr1aG3pKfl+V4vObksRdm1brpk9Lb2Lo6aIipov1tMdyzNarF92ati6+oLucxBbzrHcmft66gdkqSyy/kNPZVkR9NSl9RG/LZlKVHb6qNhZEZlE4Djm1evq1jrSt25Om8pVS+smlo2uujYSGL4XuW7TuYW5vzS9FqjfQ6vTzeE/l6rPBPC7PX9nyj01j9dTZDjhMWezajHmo8WmynFJaiS7mkWDEQUVN5TvPNuy3WSQmXBU4851cZI3Pzi3ILE8O81wUVG7DTNLVKP0qbVrb5VV8g4fza7G8sBiW3bloi3rT+j0Pk2PpM3gzMuwAAAbW84hTbP08cvrq6JKsLCw4YZBCgQIUd2VNmzZWj5bEWJEisIcfkypL7iUNtoSpa1qIiIzMbjiIynw7cjFNyeCkklrb7p6EZfZajnEZSdIrEqr/1mPH7FNy/KTZv1Cyr8lDGfcMd7G76kvIaR73hfa2/WXlOk6x4d8mtuZFBxzENLbB8SZKbjP3V9jFzjeL1KVLaJ6TcZHdQodVBZjNvE4pBuKfWj+jbWoySfThclzXGXVas2Lml63FxiultJfLsR438zwGHg53LsNHImm30JaTVXut+j4m+n7k+Oz7WJI+z3j63q+mmTlLJq/yyTiLeE0hFHStmQ8VrfykOrabNCksmsyNCUmpOtY128o4enbbX8rD7ib5Zbu6u1lAwqnmGbxmk/PvbzpyKu8+xFXHoR/2i5K/7l1b/ALdngqfAP9TFfVt+OZPcWehY6Z/9Javv/dHGfhdZZRvDY5KLYm2Cr4MWHTQIltn2VV+HU9bWR6LH2X3oDUPHqk1JkPuSJDEZMqZ1ccNSmUFbsxx2V5JKWOxP9xdotCrOSikqLVoWt1aVXpeor+DwuOzNRwtn+jb26IqrrV877aLpKbec3qRai5e6GVqDX+DbRpsxlZ5iNxWNX9Tjz0KzahHZRnoUZdDklvPOzdentEy0UcydMzIlEroR0rPeJsHnOXvBYe3djfdyLVUtNK6NEm66dhZcqyTE5bi/eb07btKDTo3o7UtBZNxh426Z9OjjtZbg2w3U/aJAxor/AGZnZwkWFnWOzksoYwDC1OEb6IyJclqAhLJtKtJqvEd7UG22zZ8qyzBcN5a8Zi6e8KNbk6Va/wAsebk0ek9L5oTH43E5zjVh8PXuXKkY8n1n4+ZEG7n117lORSFY9x3rHcTbfNMVq5z+UxkUuKnuInpEiDjUmtrn3/Yo2ktykte1PiOfjCBnx9PvH3eGXc889Pgi0vCSseFI7nn3n3nNHR46+In4hnjV6r3HCfZs0yqy+gLm00SwsokIs91PmjMcpMXtmw3VefpJiX23vCJw4lhGWZLQ2+gyZsNMr4tyxySpNVVXTfty6uTl2Nc+qIrjuH8ao1rF6dHozj5fCnza6SfTxzzIOJnO5rWOePFUMZDe3+h89jKlPJrGb47XyuOWLPipYYktpzGsitR5TiE9IM11aDJLhkqjcOX7mUZ/7piPNUpO1PTorXzXz+ckk9je0tGc2oZjlPvFnS4pXI7aU0rsbqtqJ4euFpMp+Naq5A1cM1S6Ce/rLLZDMdtTh09uU2+xSTLkJ6PNxK22Ynsp7u5Hi2KSI0qV0XP8dYHes2sxgvOi9yXQ6uNeh1X+oieFsVu3LmDk9ElvLpWh9qp2FFHH/Vc3d+7NYangk53ZzmVNSznmVpbchUi5KZGQ2SVqSsi910MeTJ/FUZk17CM+hHQcuwjx+PtYRf8AuTSfR859Sqy2YzELC4W5iH8yLfXydroakPVN29B0Xw4vcNx426202gmBqHG4TCVKRExqRDUrKiSg+vSGjD4D0DqauqVzGz9o1bivGRwGSys29E7tLcVzP0v3U11ooWQ4Z4vMo3J6Y2/PfTyfvUfUV2ehZ/iTv3+4+If8+shW+Av7nEfUj42TPFf9Gz9aXiRHn1kv4zJv6ssE/wA1uI3jX9a/2o/KdvDP6b/uS+QmX6THBTFJuJQ+U24aGvvpVvJlfZNjd7CJ+tp62pmPQ5Od2EKc35eVZTbCK4mt70Kbjx2fNIUpbzSmZvhDIbTsrNsZFSlJ/wAtNaElo3mnyt+jsWnl0RnEOa3FceAwzaS9Nrlb+b0Ll59HJp9Vvv1r6DC88ucR0tq6HsGix+wk1sjOchySTUV19JiLNiQ7j1RAq5Mg6lMhCvBlvSCVJR0UTKEmSle2YccWrGIlZwVpXIRdN5yonTYkno529Ow88JwxO7ZVzE3NyclXdSq10uuvm5CSPFrm7o71D6TKNL7K1nApcpXSSLC119kcqNk+OZNSNvKiSLPGLhcOsmJsqkpDTjiDjxpcQ3ScjuuE2txuTynPcBxHbngsTaUbu7Vwl5yktVYui0rl0Jquius4swyvFZNOOJszbt10SWhp7GtOvpafKUY8ldQ3Xp5czsbsMSenS8boL/Gds6ylvSF+bn4qi5cVJxuzmKZNpyTElVsysk9e/wAeKaHXEkT/AGFQszwc+G87hOzV2oyjchzxrpi31OL2qjestWBxMc5yyUblO8acJdNNa8D6egv+9RPEqXkLwKzfKsWejW0SrxfHN64bZoJEplypo47OQTrGO5HU8j/WsCnWCUuIUaCJ3qauzqY0PiOzDMeH7l2004qCuxevQvOb64V7SoZNclg82hbuaG5O21zvRT1qGVTj3rF7c+8dU6sabWtrN86x6jsjQ4ppTFG/YMuZBMJxBktPkaNqQ99H6X837Pb0GS5bhXjsfZwnJO4k+ivndiqaBjb/ALrhLmI5YwbXTyeGhte3prmp25pbaumGlRG38o1vb0cOG0tpK6p+0rbGLith5YjLwo8a5rCWyaiJClRVJLr2q6bjj8NDGYK7gtFZ2muiqai+1aOgy/C3pYfE28T9GafTR6fB4zCazV2MizapWYMpdu/ORVtVpMOFOXYuSCiIglGUknSlKkqJvwzIld/s6dRgahNz7tJ77dKctdVDV3KKjvtrdpWvMbMtoZBF4P8AAeX5CVHZuNUaco8KxySpLi0Ttgzq2Di9TPU1E8J1/wA1l1iU1/wzb6N+IrubSRqTteLuLIuH3utb9myox550UU9G2Tq/kM0sQeaZsq+jcuuT+rWr8CojGCta3FqccUpa1qUta1qNS1rUZmpSlGZmpSjPqZn7TMYk3XS9Zpp/yAAAAAAAAAAAAAAAALwvQz/xt3R+qyB+dtYL3wH/AHt/7pfxFV4q/tbX3j8R431vv4rtff8Ab1in/UjbI8OOv1e3/wCNH+O4evC36fP75/wwOnekDzTPFb1vivsq57cayaY7I09YzT/mqXKZbr0mzwx2UtXRqvydxZvwEq6JRYktpPcuWgk9fBued1P8pxUv5cn/AC2+SXLHoetc9Vyo5+I8s34/mFhefFefzrkl1cvN0HsObnC8tU8xNCcjdd1TiNe7J5BauTnVbBiF5PD8/m51RvLsi8AukamzZwlvdVp7GbMnUd5FIjtJ989yT3TOsPmeGX/Hu4m3vpL0Zua09Ev4ulI88rzP3jLb2CvP+dCzPdb5Y7r0dMfF0Ml96y/8Gzn608G/0F8JjjT9Ef3sPlI7hr9T/wBuXyHHfQ0pYDGkNz5E2yhNpabVhUsyQSEEt2BQ4jUzq1lThJ8RSGJGSSlJIzMkm4ZkRGZ9eLgOEVgL9xek71OpRTXjZ08VSbxdqHzVbr2t18SKavUVyOzybmpyBl2kh19ddmqschJcdU4iNWY1WV9JXx2Eq6JZaKPBJXakiLvWo/aZmZ0viW7K7nmIc3qnuroSSRZclhG3ldlR5Y1622yFQgiUNm/Fd09p+ndrqDmDiLFjItBW+H3Dln/rzcqph1FziHWeTy2zlIXVQk+IS1kai69Vf+IbZlT974dtRvaVLDuLrpqqOOnqRmWPXu+cTdvQ43k1TbVPxmMgYmaaAAAG6HFc5rtYcTcc2VcRJs+o15x3p85tYNYlhVjNrsS1rHv50SvTKfjRlTZEavUhonHG2zcMu5SS6mW9W78cLlMcVNNwt4dSaWtqMKunPoMpnalfzB2I0Up3nFV1VcqFb334vHP5U7r/AP08F/TQVn47yz2V/sh9om/hXG+0tfvfZPIZL66OsI8OWeH6Jz24nl0KC3kuSY9jcNzqj6Tkt6rayp5nw1/gQhtzvIvxk9fZ43ePMIovucPclLk3nGK8G8ekOFMQ3/MuwUeZN+OhTzyz5y7q5e2kNGczIOP4NSS3JmO66xkn2MfgS1k40m1s35Djs7IL0ojnheYkK8NpJr8uywTrpLpmcZ9js5klfajh4uqhHUud8rfO9XIlVlky7KsLl0X3VZXnrk9fQti/Ztll/oR/2i5K/wC5dW/7dngs/AP9TFfVt+OZB8WehY6Z/wDSRa9Y6+tLXmXZ1c6Ut6BjGvMHqqaOal+FEiTYkq/lEhBqNBOPWVu8tSiIjUXaR/ikIrjS5OedOEn5sbUUuur8bO/hqEY5apLXKcm/F8hErhRR12R8t+OdTatG/Ad27hUt5jqRJfVV3Ma1YZdJSVEuO7IhIS4np9JszLqXXqIjIrcbucYaE9Me+i+x1+Qkc0nKGXXpR193Lwqhrl5e6a1LvnVDGvN07JttZYTLyuotHrSnynEMSfurGqi2b0ChkWOZ0t7XPxFuLOYbLTSJCnYaFEsm0OJXsGcYLCZhhPdsbdlasOadVKMatVoqyTVOXRp0dJneXYnEYTEd9hoKd1Rao03StNNItPm2aSsH7rT05P8A7R5l+23RX/x6Kt8J8N/irn/y2vsE7+f517CPqXPtExuJmlOIPDdWclrHkXHvWNgFj3vmHnm29XWkSK9jZ3HkZNW1j9RjHl5DyLt1LynDe70pQREnt9s1lGAyfJe891xO8rlKqdyDXm1pSijt09RG5hisxzLc7+zRwrTdhJa6a6t7Cg31PpGNwObOaZprfJ6q0j5BX4FmMe5xS7g2kSBkMSgrquQ5Dn1SltR5qJ1CUpZeK44l5019xdxITn3FTtwz2d/DTT3lCVYtOjSS0Ncvm16S25CpyyuNq/Frdco0apVVry9NDQLbLgc/PTwlSYrMSVf7L1QcxiOkvGRXbaw9Xj+UbNlKH2kR8/x02SNKUrXGV+IaV9h6HPd4h4dbVHO7Z7LkfJNdhUI72UZxR13Ldztg/wDtZU96JmknMh3DsLd1tEWmDrDH04pj5vxkkR5dmhPtWD8d5wjW3Ip8YgSGHkJIldtojqoi6pVUeBsC7mMu46a821HdX1pa+yKo/rFg4oxW5h4YWL03HV9C1dr8Ry/1j93Fsjk1D1rWyEvUGj8eRRK7O5SV5hlSIN7lLyFmrtNLMJushrSSSNL8NzqZ+wi5ONMd7zmiwsf6diNP9UqOXg3V0pnRw1he5wLvy9O7Kv8ApWheGr6zt3oWf4k79/uPiH/PrId/AX9ziPqR8bOTiv8Ao2frS8SI8+sl/GZN/Vlgn+a3Ebxr+tf7UflO3hn9N/3JfIaPNVYjjcPiBr7CHbuXhuLr4743QTMnhz6ytscdrJ2u4kSwyRi0tocunh2cBuQ5MKTKjux0Pp73G1I7knpeEs245Pbsbzha93it5NJxW5StWmqrXVqldaKViLk3mM7tFKffN000b3tVFpo9VFpKs/utPTk/+0eZftt0V/8AHoqnwnw3+Kuf/La+wT/5/nXsI+pc+0di0Fwk4K8cdsYtuLA+TlzMybEjtvJQMi3NpyVQzmbqksaGbGtYlTiVLOkxzh2a1pSiS10eQhRmfb0Pty7IsiyzFwxuHxUndhXQ7lujqmnWkU+XbrObGZrmuNw8sNesJQlTVCddDT0Vb2bCLnrY2+tM4xzQuXYdm2H5Pd0d3muOT2MZyeku5JVV3Ao7OM7OjVj8x5DESXRrJpaltpSqQsuizURoiuOZYa/aw96zOEpxlKL3ZJ6Gk9NOjwnfwvG/auXrdyMoxai9Ka0ptcvT4CZPpR7Nhbv4YlrbJlKspWtpmQ6qvI8lLJKm4jbRV2WP9Cb7kKhIorpda2akpWfkFdxK/HXNcJYpY7JPdrul2m7b54vSuqj3eojOILDwuZ99DQppTXTqfhVesrr9LLjjPoOdm2mr+IbzfGiPnFCUqYx0d+KrC7l4NSyUN9PCIrDHm7WQ26RqSaUpNHUlEoq3wplkrWf3lcWjC7y0/Sb3U+uO8yZz7GqeU2tx6b+6+pLefY6E9OP3Kj409TzlBq5yeh7HJmJVOGYklDpqbReaNffauoSEKcNC3pNnlORPqW2RfzcdKTJXTuFgy/Nu/wCKcVhG62txRj02ta7ZT7CJxmA7rIrGIp5+82+ierwKPaV9VXFbt9YV/XqqhfwfX7Jk786Gskwm8b93FsyvQgjNtb1ajMJDFR4aSX9LqhXVBLWK7DKf/wBn7vu/yFc77mpTfXVv0j/gTEsw/wDzfe1/muHd9dd3t3dJIX1xdzeUodRaCrJna/cTZu0MsjMzPDdTXVaZWO4gxLiN/TkQrGwlWjxeIZNk9XoUSVKSRtyHHeN3bdnL4PTJuctPItEarY3vPpj2cfCuGrO5jJLQlurpel9ip2mdAZsXQAAAAAAAAAAAAAAAAC8L0M/8bd0fqsgfnbWC98B/3t/7pfxFV4q/tbX3j8R431vv4rtff9vWKf8AUjbI8OOv1e3/AONH+O4evC36fP75/wAMCnmFNm1k2JY10uVX2FfKjzYE+FIdizYU2K6h+LLiSmFtvxpUZ9tK23EKStC0kZGRkKZGUoSUotqSdU1rT2osjSknGSrF60bA+AfKrHeaGimqXPk11ltHATo67Y9LLQylVxIq5UewxbYcGM0TSW0Wk2rbkKUylvydtGc7CQgmFK2bh7Nred4Dcv0eKt0U1tppjNdNK80k+YzfN8vnlmL3rVVYnVxeyuuPVWnOn0nPvWX/AINnP1p4N/oL4c3Gn6I/vYfKe/DX6n/ty+Qgb6JW+qbGc02NoDIZ0WC7sJuBmOCqkLaY89kWPRJMTIqRpxayVJnWFCpmUw0RexuufPr1MiFf4GzCFq9dy640ncpKHO0qSXS1RroZLcUYSU7cMZBN7nmy5k9T7arrR8T1ZeFWx6/cVxyK1xid1l2C5/Er5eaFj8J+1m4fltbCi1EmRPrYbTs1uhvYUNiQiWlLjTcvx0PG0RseL8cX5HiVjXmWFhKdi4lvUVXGSVNKWmjVHXbWtNFfrh7NLDwywV+Sjdg/Nroqnp17U+TZTnKvdD8ZNz8jcwrcR1nhdxYlLnph2mTya+fHxHF2EOITNn5Jf+WXCr2ILa+5TXVUl1XRtptx1SUKquX5VjsyvKzhoSabo5NPdjtbepU2a9iqT2Lx+FwVt3L8ktGhV0voX7I1Pcnc0wzg/wAE5mI1tk2mdVazj6b1szJUhNlkGVWGPOUaLfy3iEbrsVJyLeZ29EJS0si6dySPWM0v2MiyF2ovzo2u7htcmqV8cmUHA2rua5qrkloc9+WxKtafIjHIMWNKAAADajsX/wDnJnf/AGT5P/0KnDcMV/8Azdz/AMGX/wBTMwsfrUP/ACl/9hiuGHmngAABf16Ef9ouSv8AuXVv+3Z4NC4B/qYr6tvxzKhxZ6Fjpn/0kQfV9/jbzP8Aubr382owh+Mv1yf3cPESPDn6XH60vGQE1Nn03Ve0ddbLr0uuzMAzfF8wZjsu+CqX8PXUK1cgqX+L4U9qKplxKuqFIWaVEaTMhXsHiJYTF2sVHXbuRl00adOvUTGJsrEYedh6pwa7VTwGubmjqODzj4cOFqedAyKwsI+Obb1RKN1DUe4sYMKSpquJ155hqBYXGPXE6AkpBoTGlvkl7s7VmnYc7wcc9yamEalJqNy29rXJzNptadTekzrLMS8qzL/kJqKrCfMv8GkzIneau2VjOUJwnIdf5pS5i5Ifis4vZYzcxL+TIjGvx24lU9DTNlm2SDMzbQsu0uvXp7RjtzCYq1d7i5buRvV9FxdXTYqaeo0WGIsXLfewnB2tqap2l4XCf0lMczfVk3PeV1Tm+J3d/Nak4liMO3Ti9tRYvFjLNyxyuFLrJEmBZW8l01JivdjkaOwhTiUrdUhF7yPhC1fwjxGbxnC5J+bGu61HbJU0N7HqS2sq2acRTtYhWcvcZQitLpVN82nUtvKymPkBB1TVbm2JU6PesZeqqjIX6fDrC1sW7eVawKphivlXSbJpphEuDdWcZ+XFUSE/6q82Rl1IUnMY4OGOuQwFXhIypFt1qlobrypurXNQs2DeIlhYSxdFiHGrSVKV00pzLQ+cvL9D/efm6faPHa3mGp+ofb2hhjLz3eo62eqFQ5jCjNqaLwWINiVdISgnFEpya8skJ6LUq+cC4/et3ctm9MXvx6Hokup0fWyqcU4Sk7eNitD82XStK7VVdRabQ4XrbhPpve+cQmyaoTyja2+chQRJbcem3Lj9nCx+EajJS/Agw4dbDQZ9VLJJfhUZnbLdnC5Jg79+Oi3vXLsul6aLqSSICd2/meJtWn6e7C2urRXxtmKvM8sus9y/Kc4yN8pV/mGQ3OT3UhPidjtpe2EiznrbJ5151LRyZKuxKlqNKehdT6DD796eIvTxF3TcnJyfS3Vmn2rcbNqNqHoRikuhKhdx6Fn+JO/f7j4h/wA+sheuAv7nEfUj42Vbiv8Ao2frS8SI8+sl/GZN/Vlgn+a3Ebxr+tf7UflO3hn9N/3JfIXc8E9iYnyr4L4/hFrOTPl1mup+g9l1jjpvWEZELHHMTZfmdykPPlkGIvR5aXuvRxbziDWbjbnS9ZBibObZDGzN1atu1NcuhbunpjR9ZVs2s3MvzWVyKonPvIvrr4HoMuPIHjHuDjbmd7iexsQu4UGssX41TmKKqb8JZRXeOtuvuKS7Jtde+xYsklfg+L47CzNp1KHUqSWVZjlWNyy/K1iYSUU9EqPdkuRp6tOzWtT0l9wePw2OtK5Zkt5rTGulPlTWsm36f3pwZPyIyublO7cVzTDNK09ZLJqU+1KxS6zPIJTRNVsTHDtK5b8inriWuRKmttmz4jSGEqUpbhNznDvDN3Mbzu4+E4YGK54uT5KVWpa29XJtpFZxndvB21bwsoyxTfSorlrR63qS69hzT1HtHcdOOe18d1PoqRkky6p6B202Y5e5Cxfor7K6VDfxujb8GHDVBsYtQ0uXIQoj7mZ8c+vsMhzcTYDLMtxccJgN7vIxrOrrRum6uZ00vmaPfJMVjcbh5YjF03G6RoqVprfRXR1M7T6Mu5kYDyXt9ZWUtqNTbqxR6sjJc8VJOZjhpS8gx0jdI/AbJymdt2Ek4RG48+2lCiUZIc7eCcasPmcsLJ0hfhT/AFRq14N7rp18vE2G77Aq/H0rUv3ZaH4d00E2GI4PxmRyx5KmiO29mkKLsnJ0JjEg/A1trpmthVhONJKRJXYWkabJSgjPrIsF9v0lmNElZw+WLF5norNb8v8ARCiXam+llPVy7jvd8FyRe6v9Uq18S6jIrxk3XY615Was3Xf2aVvx9nRbXM7eWpMcnajLpsiqzqe8pjwGWVvUd7NWfQiaJR+1PZ1SMeyrHSw2b2sddenvayb2SdJPsbNFx+Fjfy+5hYLR3dIrnjpiu1I2Sx9J4w1yDn8iENM/FM/UlfqjvQnqaqqNlczKXpRn+J4z61x2u8uq1NspSZ9qUkNpWBtLMHmK/rOyrfVvOXk7DNXirnuawf8A7aub/XShkG587qXvjldtrMWJXmcfqr53BsP7DbVHLGMKUujiSoq2+prj3c2NIsiNSlK7ppl7CIkpxviHHe/5vevJ1txluR+rHR4XWXWaNk+F90y+3bfptbz6ZafAqLqIciFJMAAAAAAAAAAAAAAAACcPBnmZ+5fm2aZj9m/2k/F+Kx8Z93fGHwd7u8C3i2nnfOfC2Veb7vLeH4fhNdO7u7/Z0OeyHO/yS/cvd13u/DdpvbtNNa+jKpFZrln5najb39zdlWtN6uim1Hxub/LX98ra+P7P+z/7OPcWvKnA/cfxV8X+a915JlmQ+9fefw3i/geP8UeD4Hl19vgd/iH39qPPPc4/OsXHFd33W7bUKb29qlJ1rux+lqpyaz7yrLvyzDuxv79ZuVaU1pKlKvYQ3EKSRILjFyNzbi1t3HtrYUZTFQFHX5Ljb8lUWvy/FJjrKrjHJz6WZPlfNIZS5Hk+E6cSW008SF9nYqRyrM7+U4yOLsaaaJR5JR5U/kfI6PTqOPH4K1j8M8Pd0V0p8qfI/KuVaCcnMv1RP3uNNK1J9hv2fd2VUWTfEH2mfFfT3KiejyXur7Psa/8AyfPdfE8z9Dt/EV19k9nXFf5xgvc+47vz1Ku/vauSm5HxkVlmQ/l2J9473f8ANapu018+8/EVV01zbY7bVt9Q2U6mu6adFs6m2rJT0GxrbGC8iRDnQZkdbb8WVFfbStC0KJSVERkYqdu5O1NXLbcbkXVNaGmuVE/KMZxcJpODVGnqaL0dC+tvleO0ldj3ILWys9kwY6WHM/wqfAo7+xJrw0oeuMUlxWaGXYPJNRuPxJVeyZkRFHLqaivuX8c3bcFbzG13jS9OLSb6Y6q86aXMVTF8LQnNzwc9xP5stKXQ9fan0ncMw9c/V0aqeVgGj8+ubs0EmO1mF3j2M1Ta1K7TdekUr2Wy3kNIPu7EtNm4Zdvejr3l3XuPMIof8exclc/zOMV4N5+A5bfCmIcv512ChzJt+GhR3yW5U7e5WZqWYbRum3Y8An2MYxKpQ7DxXEoMg2jejUtc48+4b8s2UKkSn3HpUhSUktzsQ2hFEzTN8Zm9/vsVLzV6MV6MVzLxt6X2FpwOX4bL7Xd2FpetvW+n5FqI4CMO4AAALqcj9YH3/wAcr3j/APu8eU996TtNO/Fv2teP5b3lgj+FfEXuH7MmfG8HxvM+T86ju6eH45f0gvF3jPvctll/u1N6w7e93m2O7Wm510rzVKxDhvcxqxnfVpdU6bmyW9Su91Vp1FKwo5ZwAAAn7wR5y/uUWOyrD7LvtM+0OFi0Pwvjb4M9z/DT9894nifCOV+8PO+++nTtY8PwuvVXd9Gw5Bn35HK7Luu97xR+du03a/5ZVrXmIjNsq/NIwXebm4383erWnOthyDl9yP8A3rN3XO4/g34C9702PVHw58RfFHl/cNa1XeY97+4sd8Xzfh9/Z5VPh9encr8I485zP83xzxu53dYpUrvalTXSPiOjLcF+X4VYbe36NutKa+ar8ZGERR3lg/Df1FtvcQ40jFYtfE2PqyZLdsFYDfWcmsXTT5BqXLl4lftRrFyg94PGS5LKosqK651cJpDq3HFWPJeJcZk6dlJXcI3XcbpR/wCV6aV5VRrlpWpDZnkuHzF943uYhfOSrXpWitOTSn1FtEX1ytErgE5O07tuPaeEozhxX8OmQPHJHVDZWT17BkG0pz2GvynUk+3tM/oi3rjzL92srN7e/wBNO2q8RXnwri66Llvd/wBXk+Ur/wCWPq27a33jtpr3WuPp03gVw1Ig3smNcLuc4ySqfbNl2uk3bcOti0NZOaUfmGIbSnlpPwzkqaNaHK9m/GGLzC28NhY9zh5aG61m1sroST5UtPPQmMv4dw+Emr1997dWrRSKfRyvp7CpAU4sRILi5yBu+MG7sO3LS1asgPG12ce1xo7RVMzklJcVcursKiRZFAtSipWiUl5tw4z5NSGW19hmkhI5TmM8qx0MbBb27Wsa03k1RqtH06npRx4/Bxx+FlhpOm9SjpWjTrWlV4yeHMH1W7zlNpex03U6eLWEK+vKOfkdz9ozuXrtKWkkLtG6JFe3hOJJjeYu48OQp5bzySRGNHhGaycbsGc8XXM1wLwULPdRlJOT396qWmlN2PLR1q9VKETlvD8MBiliZXe8cU6LdpRvRX0nyV7dZUaKcWIndwV5sfuV5Ln+Q/Zn9pXxzR09N5P4z+DfdfuqfJneZ8x8KZV53zHmO3s7Gezp17j69Cn8gzz8juXLndd73kUvS3aUbf0ZV8BE5tlf5pCEO83Nxt+jvVr1o57zJ5Ofvbbmf278EfZ/42MUWOfD3xL8V9vuQpZec97e4Mb6+Z81/R+WLs7fxldfZz51mv5xjffO77vzFGm9vaq6a0jt2HtlmA/LsN7vv7/nN1pTXzVfjPN8auT+1uKufJzzV1s00qWwiBkuMWqHpeMZbVIWpxuDeV7T8da3Ijq1LjSWltSYy1K8NZIcdQ55ZXmuLynEe8YV69Eov0ZLY1zcj1rrdfvHYDD5hZ7q+tWprWnzfKtT7C8vD/XP1dJqmVZ/o/Pqa7JBpkNYfd49k1U4tKu0nWZF09iUtlDqC7uxTThtmfb3r6d53yzx5hHD/kWLkbn+VxkvDuvwFVucKYhS/k3YOHOmn4KnHd6et5fXdJYUXH/V7uHz50d6OznWe2EG2tKsnTNspNXiVay9UIsWmvptuSpsthLh9FMOJT9Ljx/HU523by61uSfzptNroitFeltczOnCcLRjNTxlzeS+bHQn0t6exLpKKMjyO+y++t8oyi3sL/Ir+wk2t1dWsl2ZY2djMdU9Jly5LylOOvOuKMzMz9n4C6EREKDdu3L1yV27JyuydW3rbLXCELUFbtpRhFUSWpI+1rfObjWOwcJ2Nj6zTd4LlVDllYXirZQ7LobONZNRnlt9VeWlnH8J0uhkptakmRkZkf3hcRPCYm3ibfp25qS6nWnXqPm/ajfszsT9GcWn1qha5yn9XO15H6OzHTFVpE9cfGnueNZZOjZ6sodbqq26gXMyuaq06+xrqVt7vTHcWckiJha0mlXd7LdmvGM8ywE8FCx3XeUTlv72hNNqm5HXSmvUV/AcOxwWKjiZXd/dro3aaaUrXeerXqKbxSiyl4kf1rcui6YY1y1pJBZzH1o1hbezT2es0lkzOMpo0ZueKlr4lGv3ikp3kfef43834/T6Yvi45vLBe7qx/PVrd39/527Te3dzbppvc1Sqvhe28T33e/yt/e3d3krXdrvbNFadRR2KGWoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/9k=",ze="rgba(24,24,27,.9)",Ve={ColorPalette:{version:112,id:"obsidian",name:"Obsidian",colors:{node_slot:{CLIP:"#FFD500",CLIP_VISION:"#A8DADC",CLIP_VISION_OUTPUT:"#ad7452",CONDITIONING:"#FFA931",CONTROL_NET:"#6EE7B7",IMAGE:"#64B5F6",LATENT:"#FF9CF9",MASK:"#81C784",MODEL:"#B39DDB",STYLE_MODEL:"#C2FFAE",VAE:"#FF6E6E",TAESD:"#DCC274",PIPE_LINE:"#7737AA",PIPE_LINE_SDXL:"#7737AA",INT:"#29699C",X_Y:"#38291f",XYPLOT:"#74DA5D",LORA_STACK:"#94dccd",CONTROL_NET_STACK:"#94dccd",FAST_MODEL_LOADER:"#ffd399",SAMPLING:"#60a5fa"},litegraph_base:{BACKGROUND_IMAGE:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAQBJREFUeNrs1rEKwjAUhlETUkj3vP9rdmr1Ysammk2w5wdxuLgcMHyptfawuZX4pJSWZTnfnu/lnIe/jNNxHHGNn//HNbbv+4dr6V+11uF527arU7+u63qfa/bnmh8sWLBgwYJlqRf8MEptXPBXJXa37BSl3ixYsGDBMliwFLyCV/DeLIMFCxYsWLBMwSt4Be/NggXLYMGCBUvBK3iNruC9WbBgwYJlsGApeAWv4L1ZBgsWLFiwYJmCV/AK3psFC5bBggULloJX8BpdwXuzYMGCBctgwVLwCl7Be7MMFixYsGDBsu8FH1FaSmExVfAxBa/gvVmwYMGCZbBg/W4vAQYA5tRF9QYlv/QAAAAASUVORK5CYII=",CLEAR_BACKGROUND_COLOR:"#222222",NODE_TITLE_COLOR:"#d4d4d8",NODE_SELECTED_TITLE_COLOR:"#ffffff",NODE_TEXT_SIZE:14,NODE_TEXT_COLOR:"#ffffff",NODE_SUBTEXT_SIZE:12,NODE_DEFAULT_COLOR:"#09090b",NODE_DEFAULT_BGCOLOR:"rgba(24,24,27,.9)",NODE_DEFAULT_BOXCOLOR:"rgba(255,255,255,.75)",NODE_DEFAULT_SHAPE:2,NODE_BOX_OUTLINE_COLOR:"#60a5fa",NODE_BYPASS_BGCOLOR:"#FF00FF",NODE_ERROR_COLOUR:"#E00",DEFAULT_SHADOW_COLOR:"rgba(0,0,0,0)",DEFAULT_GROUP_FONT:24,WIDGET_BGCOLOR:"#242427",WIDGET_OUTLINE_COLOR:"#3f3f46",WIDGET_TEXT_COLOR:"#d4d4d8",WIDGET_SECONDARY_TEXT_COLOR:"#d4d4d8",LINK_COLOR:"#9A9",EVENT_LINK_COLOR:"#A86",CONNECTING_LINK_COLOR:"#AFA"},comfy_base:{"fg-color":"#fff","bg-color":"#09090b","comfy-menu-bg":"rgba(24,24,24,.9)","comfy-input-bg":"#262626","input-text":"#ddd","descrip-text":"#999","drag-text":"#ccc","error-text":"#ff4444","border-color":"#29292c","tr-even-bg-color":"rgba(28,28,28,.9)","tr-odd-bg-color":"rgba(19,19,19,.9)"}}},NODE_COLORS:{red:{color:"#af3535",bgcolor:ze,groupcolor:"#A88"},brown:{color:"#38291f",bgcolor:ze,groupcolor:"#b06634"},green:{color:"#346434",bgcolor:ze,groupcolor:"#8A8"},blue:{color:"#1f1f48",bgcolor:ze,groupcolor:"#88A"},pale_blue:{color:"#006691",bgcolor:ze,groupcolor:"#3f789e"},cyan:{color:"#008181",bgcolor:ze,groupcolor:"#8AA"},purple:{color:"#422342",bgcolor:ze,groupcolor:"#a1309b"},yellow:{color:"#c09430",bgcolor:ze,groupcolor:"#b58b2a"},black:{color:"rgba(0,0,0,.8)",bgcolor:ze,groupcolor:"#444"}}};let je=JSON.parse(JSON.stringify(Ve));delete je.NODE_COLORS,je.ColorPalette.id="obsidian_dark",je.ColorPalette.name="Obsidian Dark",je.ColorPalette.colors.litegraph_base.BACKGROUND_IMAGE="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAACXBIWXMAAAsTAAALEwEAmpwYAAAGlmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgOS4xLWMwMDEgNzkuMTQ2Mjg5OSwgMjAyMy8wNi8yNS0yMDowMTo1NSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0RXZ0PSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VFdmVudCMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIDI1LjEgKFdpbmRvd3MpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAyMy0xMS0xM1QwMDoxODowMiswMTowMCIgeG1wOk1vZGlmeURhdGU9IjIwMjMtMTEtMTVUMDI6MDQ6NTkrMDE6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMjMtMTEtMTVUMDI6MDQ6NTkrMDE6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOmIyYzRhNjA5LWJmYTctYTg0MC1iOGFlLTk3MzE2ZjM1ZGIyNyIgeG1wTU06RG9jdW1lbnRJRD0iYWRvYmU6ZG9jaWQ6cGhvdG9zaG9wOjk0ZmNlZGU4LTE1MTctZmQ0MC04ZGU3LWYzOTgxM2E3ODk5ZiIgeG1wTU06T3JpZ2luYWxEb2N1bWVudElEPSJ4bXAuZGlkOjIzMWIxMGIwLWI0ZmItMDI0ZS1iMTJlLTMwNTMwM2NkMDdjOCI+IDx4bXBNTTpIaXN0b3J5PiA8cmRmOlNlcT4gPHJkZjpsaSBzdEV2dDphY3Rpb249ImNyZWF0ZWQiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6MjMxYjEwYjAtYjRmYi0wMjRlLWIxMmUtMzA1MzAzY2QwN2M4IiBzdEV2dDp3aGVuPSIyMDIzLTExLTEzVDAwOjE4OjAyKzAxOjAwIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgMjUuMSAoV2luZG93cykiLz4gPHJkZjpsaSBzdEV2dDphY3Rpb249InNhdmVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOjQ4OWY1NzlmLTJkNjUtZWQ0Zi04OTg0LTA4NGE2MGE1ZTMzNSIgc3RFdnQ6d2hlbj0iMjAyMy0xMS0xNVQwMjowNDo1OSswMTowMCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWRvYmUgUGhvdG9zaG9wIDI1LjEgKFdpbmRvd3MpIiBzdEV2dDpjaGFuZ2VkPSIvIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDpiMmM0YTYwOS1iZmE3LWE4NDAtYjhhZS05NzMxNmYzNWRiMjciIHN0RXZ0OndoZW49IjIwMjMtMTEtMTVUMDI6MDQ6NTkrMDE6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCAyNS4xIChXaW5kb3dzKSIgc3RFdnQ6Y2hhbmdlZD0iLyIvPiA8L3JkZjpTZXE+IDwveG1wTU06SGlzdG9yeT4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz4OTe6GAAAAx0lEQVR42u3WMQoAIQxFwRzJys77X8vSLiRgITif7bYbgrwYc/mKXyBoY4VVBgsWLFiwYFmOlTv+9jfDOjHmr8u6eVkGCxYsWLBgmc5S8ApewXvgYRksWLBgKXidpeBdloL3wMOCBctgwVLwCl7BuyyDBQsWLFiwTGcpeAWv4D3wsAwWLFiwFLzOUvAuS8F74GHBgmWwYCl4Ba/gXZbBggULFixYprMUvIJX8B54WAYLFixYCl5nKXiXpeA98LBgwTJYsGC9tg1o8f4TTtqzNQAAAABJRU5ErkJggg==",je.ColorPalette.colors.litegraph_base.CLEAR_BACKGROUND_COLOR="#09090b";const Ue="#FFFFFF",We={ColorPalette:{id:"milk_white",name:"Milk White",colors:{node_slot:{CLIP:"#FFA726",CLIP_VISION:"#5C6BC0",CLIP_VISION_OUTPUT:"#8D6E63",CONDITIONING:"#EF5350",CONTROL_NET:"#66BB6A",IMAGE:"#42A5F5",LATENT:"#AB47BC",MASK:"#9CCC65",MODEL:"#7E57C2",STYLE_MODEL:"#D4E157",VAE:"#FF7043",PIPE_LINE:"#7737AA",PIPE_LINE_SDXL:"#7737AA",INT:"#29699C",X_Y:"#38291f",XYPLOT:"#74DA5D",LORA_STACK:"#94dccd",CONTROL_NET_STACK:"#94dccd",FAST_MODEL_LOADER:"#ffd399",SAMPLING:"#60a5fa"},litegraph_base:{BACKGROUND_IMAGE:"data:image/gif;base64,R0lGODlhZABkALMAAAAAAP///+vr6+rq6ujo6Ofn5+bm5uXl5d3d3f///wAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAkALAAAAABkAGQAAAT/UMhJq7046827HkcoHkYxjgZhnGG6si5LqnIM0/fL4qwwIMAg0CAsEovBIxKhRDaNy2GUOX0KfVFrssrNdpdaqTeKBX+dZ+jYvEaTf+y4W66mC8PUdrE879f9d2mBeoNLfH+IhYBbhIx2jkiHiomQlGKPl4uZe3CaeZifnnijgkESBqipqqusra6vsLGys62SlZO4t7qbuby7CLa+wqGWxL3Gv3jByMOkjc2lw8vOoNSi0czAncXW3Njdx9Pf48/Z4Kbbx+fQ5evZ4u3k1fKR6cn03vHlp7T9/v8A/8Gbp4+gwXoFryXMB2qgwoMMHyKEqA5fxX322FG8tzBcRnMW/zlulPbRncmQGidKjMjyYsOSKEF2FBlJQMCbOHP6c9iSZs+UnGYCdbnSo1CZI5F64kn0p1KnTH02nSoV3dGTV7FFHVqVq1dtWcMmVQZTbNGu72zqXMuW7danVL+6e4t1bEy6MeueBYLXrNO5Ze36jQtWsOG97wIj1vt3St/DjTEORss4nNq2mDP3e7w4r1bFkSET5hy6s2TRlD2/mSxXtSHQhCunXo26NevCpmvD/UU6tuullzULH76q92zdZG/Ltv1a+W+osI/nRmyc+fRi1Xdbh+68+0vv10dH3+77KD/i6IdnX669/frn5Zsjh4/2PXju8+8bzc9/6fj27LFnX11/+IUnXWl7BJfegm79FyB9JOl3oHgSklefgxAC+FmFGpqHIYcCfkhgfCohSKKJVo044YUMttggiBkmp6KFXw1oII24oYhjiDByaKOOHcp3Y5BD/njikSkO+eBREQAAOw==",CLEAR_BACKGROUND_COLOR:"lightgray",NODE_TITLE_COLOR:"#222",NODE_SELECTED_TITLE_COLOR:"#000",NODE_TEXT_SIZE:14,NODE_TEXT_COLOR:"#444",NODE_SUBTEXT_SIZE:12,NODE_DEFAULT_COLOR:"#F7F7F7",NODE_DEFAULT_BGCOLOR:"#F5F5F5",NODE_DEFAULT_BOXCOLOR:"#555",NODE_DEFAULT_SHAPE:2,NODE_BOX_OUTLINE_COLOR:"#000",DEFAULT_SHADOW_COLOR:"rgba(0,0,0,0.1)",DEFAULT_GROUP_FONT:24,WIDGET_BGCOLOR:"#D4D4D4",WIDGET_OUTLINE_COLOR:"#999",WIDGET_TEXT_COLOR:"#222",WIDGET_SECONDARY_TEXT_COLOR:"#555",LINK_COLOR:"#9A9",EVENT_LINK_COLOR:"#FF9800",CONNECTING_LINK_COLOR:"#222"},comfy_base:{"fg-color":"#222","bg-color":"#DDD","comfy-menu-bg":"#F5F5F5","comfy-input-bg":"#C9C9C9","input-text":"#222","descrip-text":"#444","drag-text":"#555","error-text":"#F44336","border-color":"#bbb","tr-even-bg-color":"#f9f9f9","tr-odd-bg-color":"#fff","content-bg":"#e0e0e0","content-fg":"#222","content-hover-bg":"#adadad","content-hover-fg":"#222"}}},NODE_COLORS:{red:{color:"#af3535",bgcolor:Ue,groupcolor:"#A88"},brown:{color:"#38291f",bgcolor:Ue,groupcolor:"#b06634"},green:{color:"#346434",bgcolor:Ue,groupcolor:"#8A8"},blue:{color:"#1f1f48",bgcolor:Ue,groupcolor:"#88A"},pale_blue:{color:"#006691",bgcolor:Ue,groupcolor:"#3f789e"},cyan:{color:"#008181",bgcolor:Ue,groupcolor:"#8AA"},purple:{color:"#422342",bgcolor:Ue,groupcolor:"#a1309b"},yellow:{color:"#c09430",bgcolor:Ue,groupcolor:"#b58b2a"},black:{color:"rgba(0,0,0,.8)",bgcolor:Ue,groupcolor:"#444"}}},Fe={addGroup:{id:"EasyUse.Hotkeys.AddGroup",name:"Enable Shift+g to add selected nodes to a group",tooltip:"After v1.2.39, Ctrl+g can be used instead of it",type:"boolean",defaultValue:!0},cleanVRAMUsed:{id:"EasyUse.Hotkeys.cleanVRAMUsed",name:"Enable Shift+r to unload models and node cache",type:"boolean",defaultValue:!0},toggleSiteMap:{id:"EasyUse.Hotkeys.toggleNodesMap",name:"Enable Shift+m to toggle nodes map",type:"boolean",defaultValue:!0},alignSelectedNodes:{id:"EasyUse.Hotkeys.AlignSelectedNodes",name:"Enable Shift+Up/Down/Left/Right key and Shift+Ctrl+Alt+Left/Right to align selected nodes",tooltip:"Shift+Up/Down/Left/Right can align selected nodes, Shift+Ctrl+Alt+Left/Right can distribute horizontal/vertical nodes",type:"boolean",defaultValue:!0},NormalizeSelectedNodes:{id:"EasyUse.Hotkeys.NormalizeSelectedNodes",name:"Enable Shift+Ctrl+Left/Right key to normalize selected nodes",tooltip:"Enable Shift+Ctrl+Left key to normalize width and Shift+Ctrl+Right key to normalize height",type:"boolean",defaultValue:!0},nodesTemplate:{id:"EasyUse.Hotkeys.NodesTemplate",name:"Enable Alt+1~9 to paste nodes from nodes template",type:"boolean",defaultValue:!0},jumpNearestNodes:{id:"EasyUse.Hotkeys.JumpNearestNodes",name:"Enable Up/Down/Left/Right key to jump nearest nodes",type:"boolean",defaultValue:!0},subDirectories:{id:"EasyUse.ContextMenu.SubDirectories",name:"Enable contextMenu auto nest subdirectories",type:"boolean",defaultValue:!1},modelsThumbnails:{id:"EasyUse.ContextMenu.ModelsThumbnails",name:"Enable model thumbnails display",type:"boolean",defaultValue:!1},rightMenuNodesSort:{id:"EasyUse.ContextMenu.NodesSort",name:"Enable right-click menu to add node A~Z sorting",type:"boolean",defaultValue:!0},quickOptions:{id:"EasyUse.ContextMenu.QuickOptions",name:"Use three shortcut buttons in the right-click menu",type:"combo",options:["At the forefront","At the end","Disable"],defaultValue:"At the forefront"},nodesRuntime:{id:"EasyUse.Nodes.Runtime",name:"Enable nodes runtime display",type:"boolean",defaultValue:!0},chainGetSet:{id:"EasyUse.Nodes.ChainGetSet",name:"Enable chain get node and set node with parent nodes",type:"boolean",defaultValue:!0},nodesMap:{id:"EasyUse.NodesMap.Sorting",name:"Nodes map sorting mode",tooltip:"Default automatic sorting, if set to manual, groups can be dragged and dropped and the sorting results saved.",type:"combo",options:["Auto sorting","Manual drag&drop sorting"],defaultValue:"Auto sorting"},displayNodesID:{id:"EasyUse.NodesMap.DisplayNodeID",name:"Display Node ID",type:"boolean",defaultValue:!0},onlyDisplayGroup:{id:"EasyUse.NodesMap.DisplayGroupOnly",name:"Display Groups Only",type:"boolean",defaultValue:!1},enableNodesMap:{id:"EasyUse.NodesMap.Enable",name:"Enable Nodes Map",tooltip:"You need to refresh the page to update successfully",type:"boolean",defaultValue:!0},stylesSelectorDisplay:{id:"EasyUse.StylesSelector.DisplayType",name:"Styles Selector Display Type",tooltip:"Styles Selector Display Type, if set to 'Grid', it will display as a Grid, if set to 'List', it will display as a list",type:"combo",options:["Grid","List"],defaultValue:"Grid"},enableAddAnglePrompt:{id:"EasyUse.MultiAngle.AddAnglePrompt",name:"Add angle prompt in easy multiAngle",type:"boolean",defaultValue:!1},enableHollowMode:{id:"EasyUse.MultiAngle.HollowMode",name:"Enable hollow display mode in easy multiAngle",type:"boolean",defaultValue:!1},enableInvertRotate:{id:"EasyUse.MultiAngle.InvertRotate",name:"Invert rotate mode in easy multiAngle",tooltip:"Enable invert rotate mode in easy multiAngle to match the rotation direction of most 3D software",type:"boolean",defaultValue:!1}};function Xe(e=100,t){return new Promise((s=>{setTimeout((()=>{s(t)}),e)}))}function Ze(e,t){if(e="number"==typeof e?e:e instanceof Date?e.getTime():parseInt(e),isNaN(e))return null;let s=new Date(e);(e=s.toString().split(/[\s\:]/g).slice(0,-2))[1]=["01","02","03","04","05","06","07","08","09","10","11","12"][s.getMonth()];let i={MM:1,dd:2,yyyy:3,hh:4,mm:5,ss:6};return t.replace(/([Mmdhs]|y{2})\1/g,(t=>e[i[t]]))}const He=/Mac|iPod|iPhone|iPad/.test(navigator.platform),qe=e=>He?e.replace(/Ctrl/g,"⌘").replace(/Alt/g,"⌥").replace(/Shift/g,"⇧"):e;const Ye=new class{constructor(){v(this,"element",ve(`div.${Ce}toast`)),v(this,"children",HTMLElement),v(this,"container",document.body),v(this,"info_icon",`mdi mdi-information ${Ce}theme`),v(this,"success_icon",`mdi mdi-check-circle ${Ce}success`),v(this,"error_icon",`mdi mdi-close-circle ${Ce}error`),v(this,"warn_icon",`mdi mdi-alert-circle ${Ce}warning`),v(this,"loading_icon","mdi mdi-rotate-right loading"),this.container.appendChild(this.element)}async show(e){let t=ve(`div.${Ce}toast-container`,[ve("div",[ve("span",[...e.icon?[ve("i",{className:e.icon})]:[],ve("span",e.content)])])]);t.setAttribute("toast-id",e.id),this.element.replaceChildren(t),this.container.appendChild(this.element),await Xe(64),t.style.marginTop=`-${t.offsetHeight}px`,await Xe(64),t.classList.add("show"),e.duration&&(await Xe(e.duration),this.hide(e.id))}async hide(e){const t=document.querySelector(`.${Ce}toast > [toast-id="${e}"]`);(null==t?void 0:t.classList.contains("show"))&&(t.classList.remove("show"),await Xe(750)),t&&t.remove()}async clearAllMessages(){let e=document.querySelector(`.${Ce}container`);e&&(e.innerHTML="")}async info(e,t=3e3,s=[]){this.show({id:"toast-info",icon:this.info_icon,content:e,duration:t})}async success(e,t=3e3){this.show({id:"toast-success",icon:this.success_icon,content:e,duration:t})}async error(e,t=3e3){this.show({id:"toast-error",icon:this.error_icon,content:e,duration:t})}async warn(e,t=3e3){this.show({id:"toast-warn",icon:this.warn_icon,content:e,duration:t})}async showLoading(e,t=0){this.show({id:"toast-loading",icon:this.loading_icon,content:e,duration:t})}async hideLoading(){this.hide("toast-loading")}},Ke=S("groups",{state:e=>({groups:[],nodes:[],isWatching:!1}),getters:{groups_nodes(){var e;let t=[],s=[];const i=e=>{var t;return{info:e,children:(null==(t=e.sub_groups)?void 0:t.length)>0?e.sub_groups.map(i):[],bounds:{x1:e.pos[0],y1:e.pos[1],x2:e.pos[0]+e.size[0],y2:e.pos[1]+e.size[1]}}};if(t=this.groups.map(i),(null==(e=this.nodes)?void 0:e.length)>0){const e=(e,t)=>e[0]>=t.x1&&e[0]<=t.x2&&e[1]>=t.y1&&e[1]<=t.y2,i=(t,s)=>{let i=null,a=-1;const n=(s,o)=>{s.bounds&&e(t.pos,s.bounds)&&(o>a&&(a=o,i=s),s.children.forEach((e=>n(e,o+1))))};return s.forEach((e=>n(e,0))),i};this.nodes.forEach((e=>{const a=i(e,t);a?(a.children||(a.children=[]),a.children.push(e)):s.push({info:e})}))}return[...t,...s]}},actions:{setGroups(e){let t=R(e.map((e=>new Object({id:e.id,pos:Array.isArray(e.pos)?[...e.pos]:e.pos,size:Array.isArray(e.size)?[...e.size]:e.size,title:e.title,pinned:e.pinned,show_nodes:e.show_nodes}))));t.forEach((e=>{e.sub_groups=[],t.forEach((t=>{t!==e&&t.pos[0]>e.pos[0]&&t.pos[0]e.pos[1]&&t.pos[1]{t=t.filter((t=>t!==e))}))})),this.groups="Manual drag&drop sorting"==Le("EasyUse.NodesMap.Sorting")?R(t):R(t.sort(((e,t)=>e.pos[0]-t.pos[0])).sort(((e,t)=>e.pos[1]-t.pos[1])))},setNodes(e){this.nodes=R(e.map((e=>new Object({id:e.id,pos:e.pos,size:e.size,type:e.type,is_edit:e.is_edit,title:e.title,mode:e.mode}))))},update(){var e,t,s;(((null==(e=fe.extensionManager)?void 0:e.activeSidebarTab)||(null==(s=null==(t=fe.extensionManager.sidebarTab)?void 0:t.activeSidebarTab)?void 0:s.id))===Pe||this.isWatching)&&setTimeout((e=>{this.setGroups(fe.canvas.graph._groups),this.setNodes(fe.canvas.graph._nodes)}),1)},watchGraph(e=!1){e&&(this.isWatching=!0);let t=this;this.update();const s=fe.graph.onNodeAdded;fe.graph.onNodeAdded=function(e){t.update();const i=e.onRemoved;return e.onRemoved=function(){return t.update(),null==i?void 0:i.apply(this,arguments)},null==s?void 0:s.apply(this,arguments)},fe.canvas.onNodeMoved=function(e){t.update()};const i=LGraphCanvas.onNodeAlign;LGraphCanvas.onNodeAlign=function(e){return t.update(),null==i?void 0:i.apply(this,arguments)};const a=LGraphCanvas.onGroupAdd;LGraphCanvas.onGroupAdd=function(){return t.update(),null==a?void 0:a.apply(this,arguments)};const n=LGraphCanvas.onGroupAlign;LGraphCanvas.onGroupAlign=function(e){return t.update(),null==n?void 0:n.apply(this,arguments)};const o=LGraphCanvas.onMenuNodeRemove;LGraphCanvas.onMenuNodeRemove=function(e){return t.update(),null==o?void 0:o.apply(this,arguments)}},unwatchGraph(){this.isWatching=!1}}});const Je={BOX:1,ROUND:2,CIRCLE:3,CARD:4,ARROW:5,GRID:6,HollowCircle:7};function Be(e,t,s={}){const{shape:i=Je.BOX,round_radius:a=LiteGraph.ROUND_RADIUS,title_height:n=LiteGraph.NODE_TITLE_HEIGHT,title_mode:o=LiteGraph.NORMAL_TITLE,color:l=LiteGraph.NODE_BOX_OUTLINE_COLOR,padding:r=6,collapsed:d=!1,thickness:u=1}=s;o===LiteGraph.TRANSPARENT_TITLE&&(t[1]-=n,t[3]+=n);const{lineWidth:c,strokeStyle:A}=e;e.lineWidth=u,e.globalAlpha=.8,e.strokeStyle=l,e.beginPath();const[p,h,m,f]=t;switch(i){case Je.BOX:e.rect(p-r,h-r,m+2*r,f+2*r);break;case Je.ROUND:case Je.CARD:{const t=a+r,s=i===Je.CARD&&d||i===Je.ROUND?[t]:[t,2,t,2];e.roundRect(p-r,h-r,m+2*r,f+2*r,s);break}case Je.CIRCLE:{const t=p+m/2,s=h+f/2,i=Math.max(m,f)/2+r;e.arc(t,s,i,0,2*Math.PI);break}}e.stroke(),e.lineWidth=c,e.strokeStyle=A,e.globalAlpha=1}function Qe(e,t,s){var i,a;let n=0,o=null==(i=e[0])?void 0:i.naturalWidth,l=null==(a=e[0])?void 0:a.naturalHeight;const r=e.length;let d,u,c,A,p;for(let h=1;h<=r;h++){const e=Math.ceil(r/h),i=t/h,a=i/o,m=s/e/l,f=Math.min(a,m,1),g=o*f,v=l*f,y=g*v*r;y>n&&(n=y,d=g,u=v,c=h,A=e,p=h*((i-g)/2))}return{cellWidth:d,cellHeight:u,cols:c,rows:A,shiftX:p}}class $e{constructor(e,t){this.type="custom",this.name=e,this.options=t,this.value=""}draw(e,t,s,i,a){((e,t,s)=>{const i=fe.canvas,a=i.graph_mouse;!i.pointer_is_down&&t.pointerDown&&(a[0]===t.pointerDown.pos[0]&&a[1]===t.pointerDown.pos[1]&&(t.imageIndex=t.pointerDown.index),t.pointerDown=null);const n=t.imgs??[];let{imageIndex:o}=t;const l=n.length;1!==l||o||(t.imageIndex=o=0);const r=t.size[0],d=t.size[1]-s-15;if(null==o){let o,u,c,A,p;const h=(e=>{if(!e.length||1===e.length)return!0;const t=e[0].naturalWidth/e[0].naturalHeight;for(let s=1;sMath.max(e,t.naturalWidth,t.naturalHeight)),0),t=[];t.length=n.length,t[0]={naturalWidth:e,naturalHeight:e},({cellWidth:o,cellHeight:u,cols:p,shiftX:c}=Qe(t,r,d))}let m=!1;t.imageRects=[];for(let r=0;r{let r="#333",d="#fff",u=!1;return LiteGraph.isInsideRectangle(a[0],a[1],s+t.pos[0],n+t.pos[1],o,o)&&(i.canvas.style.cursor="pointer",i.pointer_is_down?(r="#1e90ff",u=!0):(r="#eee",d="#000")),e.fillStyle=r,e.beginPath(),e.roundRect(s,n,o,o,[4]),e.fill(),e.fillStyle=d,e.font="12px Arial",e.textAlign="center",e.fillText(l,s+15,n+20),u};if(!(l>1))return;const b=(t.imageIndex??0)+1;if(w(r-40,d+s-40,30,`${b}/${l}`)){const e=b>=l?0:b;t.pointerDown&&t.pointerDown.index===e||(t.pointerDown={index:e,pos:[...a]})}w(r-40,s+10,30,"x")&&(t.pointerDown&&null===t.pointerDown.index||(t.pointerDown={index:null,pos:[...a]}))})(e,t,i)}computeLayoutSize(e,t){return{minHeight:220,minWidth:1}}}const et=()=>(e,t)=>e.addCustomWidget(new $e(t.name,{serialize:!1}));function tt(e,t,s,i,a,n){return e>=s&&e=i&&t=s&&e=i&&t=e[0]&&this.y+this.height>=e[1]}containsRect(e){return this.x<=e[0]&&this.y<=e[1]&&this.x+this.width>=e[0]+e[2]&&this.y+this.height>=e[1]+e[3]}overlaps(e){return this.xe[0]&&this.y+this.height>e[1]}findContainingCorner(e,t,s){return this.isInTopLeftCorner(e,t,s)?"NW":this.isInTopRightCorner(e,t,s)?"NE":this.isInBottomLeftCorner(e,t,s)?"SW":this.isInBottomRightCorner(e,t,s)?"SE":void 0}isInTopLeftCorner(e,t,s){return tt(e,t,this.x,this.y,s,s)}isInTopRightCorner(e,t,s){return tt(e,t,this.right-s,this.y,s,s)}isInBottomLeftCorner(e,t,s){return tt(e,t,this.x,this.bottom-s,s,s)}isInBottomRightCorner(e,t,s){return tt(e,t,this.right-s,this.bottom-s,s,s)}isInTopEdge(e,t,s){return tt(e,t,this.x,this.y,this.width,s)}isInBottomEdge(e,t,s){return tt(e,t,this.x,this.bottom-s,this.width,s)}isInLeftEdge(e,t,s){return tt(e,t,this.x,this.y,s,this.height)}isInRightEdge(e,t,s){return tt(e,t,this.right-s,this.y,s,this.height)}getCentre(){return[this.centreX,this.centreY]}getArea(){return this.width*this.height}getPerimeter(){return 2*(this.width+this.height)}getTopLeft(){return[this[0],this[1]]}getBottomRight(){return[this.right,this.bottom]}getSize(){return[this[2],this[3]]}getOffsetTo([e,t]){return[e-this[0],t-this[1]]}getOffsetFrom([e,t]){return[this[0]-e,this[1]-t]}resizeTopLeft(e,t){this[2]+=this[0]-e,this[3]+=this[1]-t,this[0]=e,this[1]=t}resizeBottomLeft(e,t){this[2]+=this[0]-e,this[3]=t-this[1],this[0]=e}resizeTopRight(e,t){this[2]=e-this[0],this[3]+=this[1]-t,this[1]=t}resizeBottomRight(e,t){this[2]=e-this[0],this[3]=t-this[1]}setWidthRightAnchored(e){const t=this[2];this[2]=e,this[0]+=t-e}setHeightBottomAnchored(e){const t=this[3];this[3]=e,this[1]+=t-e}toArray(){return this.export()}export(){return[this[0],this[1],this[2],this[3]]}_drawDebug(e,t="red"){const{strokeStyle:s,lineWidth:i}=e;try{e.strokeStyle=t,e.lineWidth=.5,e.beginPath(),e.strokeRect(this[0],this[1],this[2],this[3])}finally{e.strokeStyle=s,e.lineWidth=i}}}h=new WeakMap,m=new WeakMap;const it=(e,...t)=>function(...s){try{t.forEach((e=>null==e?void 0:e.call(this,...s)))}catch(i){null==e||e.call(this,...s,i)}};let at=null;const nt=new Image;nt.src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='16' height='16'%3E%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3E%3Cpath stroke='white' stroke-linecap='round' stroke-width='1.3' d='M9.18613 3.09999H6.81377M9.18613 12.9H7.55288c-3.08678 0-5.35171-2.99581-4.60305-6.08843l.3054-1.26158M14.7486 2.1721l-.5931 2.45c-.132.54533-.6065.92789-1.1508.92789h-2.2993c-.77173 0-1.33797-.74895-1.1508-1.5221l.5931-2.45c.132-.54533.6065-.9279 1.1508-.9279h2.2993c.7717 0 1.3379.74896 1.1508 1.52211Zm-8.3033 0-.59309 2.45c-.13201.54533-.60646.92789-1.15076.92789H2.4021c-.7717 0-1.33793-.74895-1.15077-1.5221l.59309-2.45c.13201-.54533.60647-.9279 1.15077-.9279h2.29935c.77169 0 1.33792.74896 1.15076 1.52211Zm8.3033 9.8-.5931 2.45c-.132.5453-.6065.9279-1.1508.9279h-2.2993c-.77173 0-1.33797-.749-1.1508-1.5221l.5931-2.45c.132-.5453.6065-.9279 1.1508-.9279h2.2993c.7717 0 1.3379.7489 1.1508 1.5221Z'/%3E%3C/svg%3E %3C/svg%3E";const ot=["custom_obsidian","custom_obsidian_dark","custom_milk_white","obsidian_dark","obsidian","milk_white"],lt={"easy positive":"green","easy negative":"red","easy promptList":"cyan","easy promptLine":"cyan","easy promptConcat":"cyan","easy promptReplace":"cyan","easy forLoopStart":"blue","easy forLoopEnd":"blue","easy whileLoopStart":"blue","easy whileLoopEnd":"blue","easy loadImagesForLoop":"blue"};let rt=LGraphCanvas.node_colors,dt=null,ut=null,ct=null,At=new st;for(let An in Fe)pt=Fe[An],fe.ui.settings.settingsLookup[pt.id]||fe.ui.settings.addSetting(pt);var pt;function ht(e,t,s,i,a,n){var o,l,r;t.strokeStyle=i,t.fillStyle=a;const d=LiteGraph.NODE_TITLE_HEIGHT,{low_quality:u}=this,{collapsed:c}=e.flags,A=e.renderingShape,{title_mode:p}=e.constructor,h=p!=LiteGraph.TRANSPARENT_TITLE&&p!=LiteGraph.NO_TITLE,m=At;m.set(e.boundingRect),m[0]-=e.pos[0],m[1]-=e.pos[1];let f=t.globalAlpha;if(t.lineWidth=1,t.beginPath(),A==LiteGraph.BOX_SHAPE||u?t.fillRect(m[0],m[1],m[2],m[3]):A==LiteGraph.ROUND_SHAPE||A==LiteGraph.CARD_SHAPE?t.roundRect(m[0],m[1],m[2],m[3],A==LiteGraph.CARD_SHAPE?[this.round_radius,this.round_radius,0,0]:[this.round_radius]):A==LiteGraph.CIRCLE_SHAPE&&t.arc(.5*s[0],.5*s[1],.5*s[0],0,2*Math.PI),t.strokeStyle=LiteGraph.WIDGET_OUTLINE_COLOR,t.stroke(),t.strokeStyle=i,t.fill(),e.has_errors&&!LiteGraph.use_legacy_node_error_indicator&&Be(t,m,{shape:A,title_mode:p,title_height:d,padding:12,color:LiteGraph.NODE_ERROR_COLOUR,collapsed:c,thickness:10}),!c&&h&&(t.shadowColor="transparent",t.fillStyle="rgba(0,0,0,0.2)",t.fillRect(0,-1,m[2],2)),t.shadowColor="transparent",e.onDrawBackground&&e.onDrawBackground(t,this,this.canvas,this.graph_mouse),h||p==LiteGraph.TRANSPARENT_TITLE){const i="dark"==function(e){let t=e.replace("#","");return s=parseInt(t.substring(0,2),16),i=parseInt(t.substring(2,4),16),a=parseInt(t.substring(4,6),16),.299*s+.587*i+.114*a>127.5?"light":"dark";var s,i,a}((null==e?void 0:e.color)||"#ffffff");e.drawTitleBarBackground(t,{scale:this.ds.scale,low_quality:u});let a=!1;LiteGraph.node_box_coloured_by_mode&&LiteGraph.NODE_MODES_COLORS[e.mode]&&(a=LiteGraph.NODE_MODES_COLORS[e.mode]),LiteGraph.node_box_coloured_when_on&&(a=e.action_triggered?"#FFF":e.execute_triggered?"#AAA":a);let l=10;if(e.onDrawTitleBox)e.onDrawTitleBox(t,d,s,this.ds.scale);else if([LiteGraph.ROUND_SHAPE,LiteGraph.CIRCLE_SHAPE,LiteGraph.CARD_SHAPE].includes(A))if(u&&(t.fillStyle="black",t.beginPath(),t.arc(.5*d,-.5*d,.5*l+1,0,2*Math.PI),t.fill()),e.subgraph&&!e.skip_subgraph_button)t.save(),t.fillStyle="#3b82f6",t.beginPath(),t.roundRect(6,-24.5,22,20,5),t.fill(),u||(t.translate(25,23),t.scale(-1.5,1.5),t.drawImage(nt,0,-d,l,l)),t.restore();else{const s=i?"#ffffff":LiteGraph.NODE_SELECTED_TITLE_COLOR,o=i?"#eeeeee":e.boxcolor||a||LiteGraph.NODE_DEFAULT_BOXCOLOR;t.fillStyle=n?s:o,t.beginPath(),t.fillRect(10,0-1.05*l-1,1.1*l,.125*l),t.fillRect(10,0-1.45*l-1,1.1*l,.125*l),t.fillRect(10,0-1.85*l-1,1.1*l,.125*l)}else u&&(t.fillStyle="black",t.fillRect(.5*(d-l)-1,-.5*(d+l)-1,l+2,l+2)),t.fillStyle=e.renderingBoxColor,t.fillRect(.5*(d-l),-.5*(d+l),l,l);if(t.globalAlpha=f,e.onDrawTitleText&&e.onDrawTitleText(t,d,s,this.ds.scale,this.title_text_font,n),!u){t.font=this.title_text_font;const s=e.getTitle()??`❌ ${e.type}`,a=String(s)+(e.pinned?"📌":"");a&&(t.fillStyle=n?i?"#ffffff":LiteGraph.NODE_SELECTED_TITLE_COLOR:i?"#ffffff":e.constructor.title_text_color||this.node_title_color,e.flags.collapsed?(t.textAlign="left",t.measureText(a),t.fillText(a.substr(0,20),d,LiteGraph.NODE_TITLE_TEXT_Y-d),t.textAlign="left"):(t.textAlign="left",t.fillText(a,d,LiteGraph.NODE_TITLE_TEXT_Y-d)))}null==(o=e.onDrawTitle)||o.call(e,t)}if(null==e?void 0:e.strokeStyles){for(const s of Object.values(e.strokeStyles)){const i=s.call(e);i&&Be(t,m,{shape:A,title_height:d,title_mode:p,collapsed:c,...i})}null==e||e.drawProgressBar(t)}else if(n){null==(l=e.onBounding)||l.call(e,m),p==LiteGraph.TRANSPARENT_TITLE&&(m[1]-=d,m[3]+=d),t.lineWidth=2,t.globalAlpha=.8,t.beginPath();let a=0,n=0,o=1;A==LiteGraph.BOX_SHAPE?t.rect(a+m[0],a+m[1],n+m[2],n+m[3]):A==LiteGraph.ROUND_SHAPE||A==LiteGraph.CARD_SHAPE&&e.flags.collapsed?t.roundRect(a+m[0],a+m[1],n+m[2],n+m[3],[this.round_radius*o]):A==LiteGraph.CARD_SHAPE?t.roundRect(a+m[0],a+m[1],n+m[2],n+m[3],[this.round_radius*o,o,this.round_radius*o,o]):A==LiteGraph.CIRCLE_SHAPE&&t.arc(.5*s[0],.5*s[1],.5*s[0]+6,0,2*Math.PI),t.strokeStyle=LiteGraph.NODE_BOX_OUTLINE_COLOR,t.stroke(),t.strokeStyle=i,t.globalAlpha=1}null==(r=e.drawProgressBar)||r.call(e,t),e.execute_triggered>0&&e.execute_triggered--,e.action_triggered>0&&e.action_triggered--}function mt(e,t,s,i,a){return new LiteGraph.ContextMenu(LiteGraph.NODE_MODES,{event:s,callback:function(e){if(!a)return;var t=Object.values(LiteGraph.NODE_MODES).indexOf(e),s=function(e){t>=0&&LiteGraph.NODE_MODES[t]?e.changeMode(t):e.changeMode(LiteGraph.ALWAYS),at||(at=Ke()),at.update()},i=LGraphCanvas.active_canvas;if(!i.selected_nodes||Object.keys(i.selected_nodes).length<=1)s(a);else for(var n in i.selected_nodes)s(i.selected_nodes[n])},parentMenu:i,node:a}),!1}function ft(e,t,s,i,a){if(!a)throw"no node for color";var n=[];for(var o in n.push({value:null,content:"No color"}),LGraphCanvas.node_colors){var l=LGraphCanvas.node_colors[o];e={value:o,content:""+o+""};n.push(e)}return new LiteGraph.ContextMenu(n,{event:s,callback:function(e){if(!a)return;var t=e.value?LGraphCanvas.node_colors[e.value]:null,s=function(e){t?e.constructor===LiteGraph.LGraphGroup?e.color=t.groupcolor:(e.color=t.color,e.bgcolor=t.bgcolor):(delete e.color,delete e.bgcolor),at||(at=Ke()),at.update()},i=LGraphCanvas.active_canvas;if(!i.selected_nodes||Object.keys(i.selected_nodes).length<=1)s(a);else for(var n in i.selected_nodes)s(i.selected_nodes[n]);a.setDirtyCanvas(!0,!0)},parentMenu:i,node:a}),!1}function gt(e,t,s,i,a){var n=e.property||"title",o=a[n],l=document.createElement("div");l.is_modified=!1,l.className="graphdialog",l.innerHTML="",l.close=function(){l.parentNode&&l.parentNode.removeChild(l)},l.querySelector(".name").innerText=n;var r=l.querySelector(".value");r&&(r.value=o,r.addEventListener("blur",(function(e){this.focus()})),r.addEventListener("keydown",(function(e){if(l.is_modified=!0,27==e.keyCode)l.close();else if(13==e.keyCode)h();else if(13!=e.keyCode&&"textarea"!=e.target.localName)return;e.preventDefault(),e.stopPropagation()})));var d=LGraphCanvas.active_canvas.canvas,u=d.getBoundingClientRect(),c=-20,A=-20;u&&(c-=u.left,A-=u.top),event?(l.style.left=event.clientX+c+"px",l.style.top=event.clientY+A+"px"):(l.style.left=.5*d.width+c+"px",l.style.top=.5*d.height+A+"px"),l.querySelector("button").addEventListener("click",h),d.parentNode.appendChild(l),r&&r.focus();var p=null;function h(){r&&function(t){"Number"==e.type?t=Number(t):"Boolean"==e.type&&(t=Boolean(t));a[n]=t,l.parentNode&&l.parentNode.removeChild(l);a.setDirtyCanvas(!0,!0),at||(at=Ke());at.update()}(r.value)}l.addEventListener("mouseleave",(function(e){LiteGraph.dialog_close_on_mouse_leave&&!l.is_modified&&LiteGraph.dialog_close_on_mouse_leave&&(p=setTimeout(l.close,LiteGraph.dialog_close_on_mouse_leave_delay))})),l.addEventListener("mouseenter",(function(e){LiteGraph.dialog_close_on_mouse_leave&&p&&clearTimeout(p)}))}(e=>{try{fe.registerExtension(e)}catch(t){}})({name:"Comfy.EasyUse.UI",init(){var e,t;const s="Comfy.CustomColorPalettes",i="Comfy.Settings.Comfy.CustomColorPalettes";if(dt||(dt=Le(s,i)),ut||(ut=Le("Comfy.ColorPalette","Comfy.Settings.Comfy.ColorPalette")||"dark"),(!(null==(e=null==dt?void 0:dt.obsidian)?void 0:e.version)||dt.obsidian.version{(null==e?void 0:e.value)&&(null==e?void 0:e.oldValue)&&(await Xe(1),Object.assign(fe.canvas.default_connection_color_byType,Te),Object.assign(LGraphCanvas.link_type_colors,Te)),"custom_milk_white"==e.value&&document.body.classList.remove(Ie)})),setTimeout((e=>vt(Le("Comfy.UseNewMenu")||"Disabled")),1);const t=null==(e=fe.ui.settings.settingsLookup)?void 0:e["Comfy.UseNewMenu"];t&&(t.onChange=e=>vt(e)),ge.addEventListener("easyuse-toast",(e=>{const t=e.detail.content,s=e.detail.type,i=e.detail.duration;s?Ye.show({id:`toast-${s}`,icon:Ye[s+"_icon"],content:`${t}`,duration:i||3e3}):Ye.info(t,i)}))},async nodeCreated(e){if(lt.hasOwnProperty(e.comfyClass)){const t=lt[e.comfyClass],s=rt[t];if(!s)return;!e.bgcolor&&s.bgcolor&&(e.bgcolor=s.bgcolor),!e.color&&s.color&&(e.color=s.color)}}});const vt=e=>{var t;const s=(null==(t=document.getElementById("crystools-root"))?void 0:t.children)||null,i=Le("Comfy.Workflow.WorkflowTabsPosition",null);if((null==s?void 0:s.length)>0&&i)if(ct||(ct=document.getElementById("MonitorUI")),"Disabled"==e){document.getElementById("crystools-root").appendChild(ct)}else{let e=document.getElementById("crystools-root-easyuse");if(e)e.appendChild(ct);else{const e=document.getElementsByClassName("comfyui-menu-right");e.length>0&&e[0].before(ve("div",{id:"crystools-root-easyuse"},ct))}}};let yt={};const wt=(e,t)=>e.widgets.find((e=>e.name===t)),bt=(e,t,s=!1,i="")=>{var a;if(!t||((e,t,s)=>!!e.inputs&&e.inputs.some((e=>e.name===t&&e.link&&!s)))(e,t.name,s))return;yt[t.name]||(yt[t.name]={origType:t.type,origComputeSize:t.computeSize});const n=e.size;t.hidden=!s,t.type=s?yt[t.name].origType:"easyHidden"+i,t.computeSize=s?yt[t.name].origComputeSize:()=>[0,-4],null==(a=t.linkedWidgets)||a.forEach((i=>bt(e,i,":"+t.name,s)));const o=s?Math.max(e.computeSize()[1],n[1]):e.size[1];e.setSize([e.size[0],o])},_t=(e,t=0)=>{var s,i;if(e)return(null==(s=e.widgets)?void 0:s[t])?e.widgets[t].value:e.widgets_values?null==(i=e.widgets_values)?void 0:i[t]:void 0},St=e=>e.setSize([e.size[0],e.computeSize()[1]]),Lt=(e,t)=>graph.getNodeById(e),xt=e=>{var t;try{return Object.values(null==(t=null==graph?void 0:graph.list_of_graphcanvas[0])?void 0:t.selected_nodes)}catch(s){return[]}};function kt(e,t,s){return e+(i=s,(.5-.5*Math.cos(Math.PI*i))*(t-e));var i}const Et=(e,t=!0)=>{var s,i;const a=(null==(i=null==(s=e.graph)?void 0:s.list_of_graphcanvas)?void 0:i[0])||null;if(!a)return;const[n,o]=e.pos,[l,r]=e.size;(([e,t],s)=>{const i=s.ds,a=document.body.clientWidth,n=document.body.clientHeight,o=i.scale,l=.5*a/o-e,r=.5*n/o-t,d=Date.now()+250,u=i.offset[0],c=i.offset[1],A=()=>{const e=d-Date.now();if(!(Date.now(){const t=Lt(e);t&&Et(t)},Ct=(e,t=(()=>graph.links??[])())=>t[e],It=e=>e.toLowerCase().replace(/_./g,(e=>e.replace("_","").toUpperCase())),Tt=e=>"easy getNode"===e.type,Ot=e=>"easy setNode"===e.type,Mt=e=>Tt(e)||Ot(e),Dt=(e=(()=>graph._nodes??[])())=>e.filter((e=>Mt(e)));let Rt={},Pt={};const Gt=(e,t,s=0)=>{e.widgets_values||(e.widgets_values=[]),e.widgets_values[s]=t,e.widgets[s].value=t},zt=e=>graph.add(e),Vt=e=>graph.remove(e),jt=(e,t=0)=>{var s,i;if("Reroute"!==e.type)return[e,t];const a=e,n=null==(i=null==(s=a.inputs)?void 0:s[0])?void 0:i.link;if(!n)return[a,t];const o=Ct(n);if(!o)return[a,t];const l=Lt(o.origin_id);return l?(setTimeout((()=>{Vt(a)})),jt(l,o.origin_slot)):[a,t]},Ut=e=>{var t,s,i;if("Reroute"!==e.type)return e;const a=e,n=null==(s=null==(t=a.outputs)?void 0:t[0])?void 0:s.links;if(!n)return a;const o=n[0];if(!o)return a;const l=Ct(o);if(!l)return a;const r=Lt(l.target_id);return r?(1===(null==(i=a.outputs[0].links)?void 0:i.length)&&setTimeout((()=>{Vt(a)})),Ut(r)):a},Wt=(e,t="width")=>{var s;const i=e[0],a="width"==t?0:1,n=null==(s=i.size)?void 0:s[a];n&&(e.forEach((e=>{e.size[a]=n})),LGraphCanvas.active_canvas.setDirty(!0,!0))},Ft=(e,t="horizontal")=>{if(e.length<3)return;const s="horizontal"===t?0:1;e.sort(((e,t)=>e.pos[s]-t.pos[s]));const i=Math.min(...e.map((e=>e.pos[s]))),a=(Math.max(...e.map((e=>e.pos[s]+e.size[s])))-i-e.reduce(((e,t)=>e+t.size[s]),0))/(e.length-1);let n=i;e.forEach((e=>{e.pos[s]=n,n+=e.size[s]+a})),LGraphCanvas.active_canvas.setDirty(!0,!0)},Xt="$$canvas-image-preview";const Zt="$$comfy_animation_preview";const Ht="video_upload",qt=(e,t)=>{if(!e)return!1;const s=(e=>{var t;return null==(t=e.constructor)?void 0:t.nodeData})(e);if(!(null==s?void 0:s.input))return!1;return(e=>{if(!e)return[];const{required:t,optional:s}=e,i=[];if(t)for(const a of Object.values(t))i.push(a);if(s)for(const a of Object.values(s))i.push(a);return i})(s.input).some((e=>{var s;return null==(s=null==e?void 0:e[1])?void 0:s[t]}))};const Yt=e=>null==e?void 0:e.id.toString(),Kt=e=>fe.nodeOutputs[Yt(e)],Jt=e=>fe.nodePreviewImages[Yt(e)];function Bt(e){var t;const s=Jt(e),i=Kt(e);if((null==s?void 0:s.length)&&!e.isOutputFinal)return s;if(!(null==(t=null==i?void 0:i.images)?void 0:t.length))return(null==s?void 0:s.length)?s:void 0;const a=fe.getRandParam(),n=(e=>e.animatedImages?"":fe.getPreviewFormatParam())(e);return i.images.map((e=>{const t=new URLSearchParams(e);return ge.apiURL(`/view?${t}${n}${a}`)}))}const Qt="video-preview",$t={playsInline:!0,controls:!0,loop:!0},es=(e,t)=>{const{loadElement:s,onLoaded:i,onFailedLoading:a}=t,n=async(e,t=0)=>{const i=await Promise.race([s(e),(a=8192,new Promise((e=>setTimeout((()=>e(null)),a))))]);var a;return null===i&&t<1?n(e,t+1):i};return{showPreview:function(){var s;if(e.isLoading)return;const o=(null==(s=e.videos)?void 0:s.length)>0?e.videos.map((e=>{return t=e,ge.apiURL(`/view?filename=${encodeURIComponent(t.filename)}&type=${t.type}&subfolder=${t.subfolder}&rand=${Math.random()}`);var t})):Bt(e);(null==o?void 0:o.length)&&((null==t?void 0:t.block)&&(e.isLoading=!0),(async e=>Promise.all(e.map((e=>n(e)))))(o).then((e=>{const t=e.filter((e=>null!==e));t.length&&(null==i||i(t))})).catch((()=>{null==a||a()})).finally((()=>{e.isLoading=!1})))}}},ts=e=>{e.previewMediaType="image";return es(e,{loadElement:e=>new Promise((t=>{const s=new Image;s.onload=()=>t(s),s.onerror=()=>t(null),s.src=e})),onLoaded:t=>{e.imageIndex=null,e.imgs=t},onFailedLoading:()=>{e.imgs=void 0}})},ss=e=>{e.previewMediaType="video";return es(e,{loadElement:e=>new Promise((t=>{const s=document.createElement("video");Object.assign(s,$t),s.onloadeddata=()=>t(s),s.onerror=()=>t(null),s.src=e})),onLoaded:t=>{const s=t[0];var i,a;s&&(e.videoContainer||(e.imgs&&(e.imgs=void 0),e.videoContainer=(()=>{const e=document.createElement("div");return e.classList.add("comfy-img-preview"),e})(),e.videoContainer.style.pointerEvents="auto",i=e.videoContainer,(null==(a=e.widgets)?void 0:a.some((e=>e.name===Qt)))||e.addDOMWidget(Qt,"video",i,{hideOnZoom:!1,serialize:!1})),e.videoContainer.replaceChildren(s),e.imageOffset=64)},onFailedLoading:()=>{e.videoContainer=void 0}})};function is(e,t){var s;if(e.flags.collapsed)return;const{showCanvasImagePreview:i,removeCanvasImagePreview:a}=function(){const e=et();return{showCanvasImagePreview:function(t){var s;(null==(s=t.imgs)?void 0:s.length)&&t.widgets&&(t.widgets.find((e=>e.name===Xt))||e(t,{type:"IMAGE_PREVIEW",name:Xt}))},removeCanvasImagePreview:function(e){var t,s;if(!e.widgets)return;const i=e.widgets.findIndex((e=>e.name===Xt));i>-1&&(null==(s=(t=e.widgets[i]).onRemove)||s.call(t),e.widgets.splice(i,1))}}}(),{showVideoPreview:n,removeVideoPreview:o}={showVideoPreview:function(e){var t;(null==(t=e.video)?void 0:t.length)&&e.widgets&&e.widgets.findIndex((e=>e.name===Zt))},removeVideoPreview:function(e){var t,s;if(!e.widgets)return;const i=e.widgets.findIndex((e=>"video-preview"===e.name));i>-1&&(null==(s=(t=e.widgets[i]).onRemove)||s.call(t),e.widgets.splice(i,1))}},l=Kt(e),r=Jt(e),d=l&&(e.images!==l.images||e.videos!==l.videos),u=r&&e.preview!==r;if(u&&(e.isOutputFinal=!1,e.isTwiceRendered&&(e.isTwiceRendered=!1),e.preview=r),d&&(e.isOutputFinal=!0,l.images&&(e.images=l.images),l.videos&&(e.videos=l.videos)),d||u){e.animatedImages=null==l?void 0:l.animated;const t=e.animatedImages&&l.images.some((e=>{var t;return null==(t=e.filename)?void 0:t.includes("webp")})),s=e.animatedImages&&!t||!!(c=this)&&(!!c.videoContainer||!!c.widgets&&qt(c,Ht));d&&(null==l?void 0:l.videos)||s?ss(e).showPreview():ts(e).showPreview()}var c;e.id!=fe.runningNodeId&&!e.isTwiceRendered&&e.isOutputFinal&&(e.videos?ss(e.videos):ts(e).showPreview(),e.isTwiceRendered=!0),(null==(s=e.imgs)?void 0:s.length)&&(e.animatedImages||d&&(null==l?void 0:l.videos)?(a(e),n(e)):(o(e),i(e)))}const as=["rescale_after_model","rescale","lora_name","upscale_method","image_output","add_noise","info","sampler_name","ckpt_B_name","ckpt_C_name","save_model","refiner_ckpt_name","num_loras","num_controlnet","mode","toggle","resolution","ratio","target_parameter","input_count","replace_count","downscale_mode","range_mode","text_combine_mode","input_mode","lora_count","ckpt_count","conditioning_mode","preset","use_tiled","use_batch","num_embeds","easing_mode","guider","scheduler","inpaint_mode","t5_type","rem_mode","encode","method"],ns=["LIGHT - SD1.5 only (low strength)","STANDARD (medium strength)","VIT-G (medium strength)","REGULAR - FLUX and SD3.5 only (high strength)","PLUS (high strength)","PLUS (kolors genernal)","PLUS FACE (portraits)","FULL FACE - SD1.5 only (portraits stronger)","COMPOSITION"],os=["FACEID","FACEID PLUS - SD1.5 only","FACEID PLUS V2","FACEID PLUS KOLORS","FACEID PORTRAIT (style transfer)","FACEID PORTRAIT UNNORM - SDXL only (strong)"],ls=["easy seed","easy latentNoisy","easy wildcards","easy preSampling","easy preSamplingAdvanced","easy preSamplingNoiseIn","easy preSamplingSdTurbo","easy preSamplingCascade","easy preSamplingDynamicCFG","easy preSamplingLayerDiffusion","easy fullkSampler","easy fullCascadeKSampler"],rs=["easy fullLoader","easy a1111Loader","easy comfyLoader","easy hyditLoader","easy pixArtLoader"],ds=["easy fullkSampler","easy kSampler","easy kSamplerCustom","easy kSamplerTiled","easy kSamplerLayerDiffusion","easy kSamplerInpainting","easy kSamplerDownscaleUnet","easy kSamplerSDTurbo","easy fullCascadeKSampler","easy cascadeKSampler"],us=["easy imageSize","easy imageSizeBySide","easy imageSizeByLongerSide","easy imageSizeShow","easy imageRatio","easy imagePixelPerfect"],cs=["easy forLoopStart","easy forLoopEnd","easy whileLoopStart","easy whileLoopEnd"],As=["easy anythingIndexSwitch","easy imageIndexSwitch","easy textIndexSwitch","easy conditioningIndexSwitch"],ps=["easy anythingInversedSwitch"],hs=["easy loadImagesForLoop",...cs,...As,...ps],ms={"easy anythingInversedSwitch":"out","easy anythingIndexSwitch":"value","easy imageIndexSwitch":"image","easy textIndexSwitch":"text","easy conditioningIndexSwitch":"cond"};function fs(e,t){var s;const i=e.comfyClass;let a=t.value;switch(t.name){case"range_mode":bt(e,wt(e,"step"),"step"==a),bt(e,wt(e,"num_steps"),"num_steps"==a),St(e);break;case"text_combine_mode":bt(e,wt(e,"replace_text"),"replace"==a);break;case"lora_name":["lora_model_strength","lora_clip_strength"].map((t=>bt(e,wt(e,t),"None"!==a)));break;case"resolution":"自定义 x 自定义"===a&&(t.value="width x height (custom)"),["empty_latent_width","empty_latent_height","width","height"].map((t=>bt(e,wt(e,t),"width x height (custom)"===a)));break;case"ratio":["empty_latent_width","empty_latent_height"].map((t=>bt(e,wt(e,t),"custom"===a)));break;case"num_loras":var n=a+1,o=null==(s=wt(e,"mode"))?void 0:s.value;requestAnimationFrame((t=>{for(let s=0;sbt(e,wt(e,t),"simple"!==o)));for(let s=n;s<99;s++)["lora_"+s+"_name","lora_"+s+"_strength","lora_"+s+"_model_strength","lora_"+s+"_clip_strength"].map((t=>bt(e,wt(e,t),!1)));St(e)}));break;case"num_controlnet":n=a+1,o=wt(e,"mode").value;for(let t=0;tbt(e,wt(e,t),!0))),["start_percent_"+t,"end_percent_"+t].map((t=>bt(e,wt(e,t),"simple"!==o)));for(let t=n;t<21;t++)["controlnet_"+t,"controlnet_"+t+"_strength","scale_soft_weight_"+t,"start_percent_"+t,"end_percent_"+t].map((t=>bt(e,wt(e,t),!1)));St(e);break;case"mode":switch(null==e?void 0:e.comfyClass){case"easy loraStack":n=wt(e,"num_loras").value+1,o=a;for(let t=0;tbt(e,wt(e,t),"simple"!==o)));St(e);break;case"easy controlnetStack":n=wt(e,"num_controlnet").value+1,o=a;for(let t=0;tbt(e,wt(e,t),"simple"!==o)));St(e);break;case"easy icLightApply":o=a;["lighting","remove_bg"].map((t=>bt(e,wt(e,t),"Foreground"===o))),bt(e,wt(e,"source"),"Foreground"!==o),St(e)}break;case"toggle":t.type="toggle",t.options={on:"Enabled",off:"Disabled"};break;case"t5_type":["clip_name","padding"].map((t=>bt(e,wt(e,t),"sd3"==a))),["t5_name","device","dtype"].map((t=>bt(e,wt(e,t),"t5v11"==a))),St(e);break;case"preset":if("FLUX.1-dev"==a&&(t.value="REGULAR - FLUX and SD3.5 only (high strength)"),ns.includes(a)){let t=wt(e,"use_tiled");bt(e,wt(e,"lora_strength")),bt(e,wt(e,"provider"),!!["REGULAR - FLUX and SD3.5 only (high strength)"].includes(a)),bt(e,wt(e,"weight_faceidv2")),bt(e,wt(e,"weight_kolors")),bt(e,wt(e,"use_tiled"),!0),bt(e,wt(e,"sharpening"),t&&t.value)}else os.includes(a)&&(bt(e,wt(e,"weight_faceidv2"),!!["FACEID PLUS V2","FACEID PLUS KOLORS"].includes(a)),bt(e,wt(e,"weight_kolors"),!!["FACEID PLUS KOLORS"].includes(t.value)),["FACEID PLUS KOLORS","FACEID PORTRAIT (style transfer)","FACEID PORTRAIT UNNORM - SDXL only (strong)"].includes(a)?bt(e,wt(e,"lora_strength"),!1):bt(e,wt(e,"lora_strength"),!0),bt(e,wt(e,"provider"),!0),bt(e,wt(e,"use_tiled")),bt(e,wt(e,"sharpening")));St(e);break;case"use_tiled":bt(e,wt(e,"sharpening"),!!a),St(e);break;case"num_embeds":n=a+1;for(let t=0;tbt(e,wt(e,t),!1)));break;case"brushnet_random":case"brushnet_segmentation":["dtype","scale","start_at","end_at"].map((t=>bt(e,wt(e,t),!0))),["fitting","function"].map((t=>bt(e,wt(e,t),!1)));break;case"powerpaint":["dtype","fitting","function","scale","start_at","end_at"].map((t=>bt(e,wt(e,t),!0)))}St(e);break;case"encode":bt(e,wt(e,"noise_mask"),!!["inpaint_model_conditioning","different_diffusion"].includes(a)),St(e);break;case"image_output":bt(e,wt(e,"link_id"),!!["Sender","Sender&Save"].includes(a)),bt(e,wt(e,"decode_vae_name"),!!["Hide","Hide&Save"].includes(a)),["save_prefix","output_path","embed_workflow","number_padding","overwrite_existing"].map((t=>bt(e,wt(e,t),!!["Save","Hide&Save","Sender&Save"].includes(a))));break;case"add_noise":var l=wt(e,"control_before_generate"),r=wt(e,"control_after_generate")||l;"disable"===a?(bt(e,wt(e,"seed")),r&&(r.last_value=r.value,r.value="fixed",bt(e,r))):("enable"===a&&(t.value="enable (CPU)"),bt(e,wt(e,"seed"),!0),r&&((null==r?void 0:r.last_value)&&(r.value=r.last_value),bt(e,r,!0))),St(e);break;case"guider":switch(a){case"Basic":case"IP2P+Basic":["cfg_negative"].map((t=>bt(e,wt(e,t))));break;case"CFG":case"IP2P+CFG":bt(e,wt(e,"cfg"),!0),bt(e,wt(e,"cfg_negative"));break;case"DualCFG":case"IP2P+DualCFG":["cfg","cfg_negative"].map((t=>bt(e,wt(e,t),!0)))}St(e);break;case"scheduler":"easy preSamplingCustom"==i&&(["karrasADV","exponentialADV","polyExponential"].includes(a)?(["sigma_max","sigma_min"].map((t=>bt(e,wt(e,t),!0))),["denoise","beta_d","beta_min","eps_s","coeff"].map((t=>bt(e,wt(e,t))),!1),bt(e,wt(e,"rho"),"exponentialADV"!=a)):"vp"==a?(["sigma_max","sigma_min","denoise","rho","coeff"].map((t=>bt(e,wt(e,t)))),["beta_d","beta_min","eps_s"].map((t=>bt(e,wt(e,t),!0)))):(["sigma_max","sigma_min","beta_d","beta_min","eps_s","rho"].map((t=>bt(e,wt(e,t)))),bt(e,wt(e,"coeff"),"gits"==a),bt(e,wt(e,"denoise"),!0)),St(e));break;case"conditioning_mode":["replace","concat","combine"].includes(a)?["average_strength","old_cond_start","old_cond_end","new_cond_start","new_cond_end"].map((t=>bt(e,wt(e,t)))):"average"==a?(bt(e,wt(e,"average_strength"),!0),["old_cond_start","old_cond_end","new_cond_start","new_cond_end"].map((t=>bt(e,wt(e,t),!1)))):"timestep"==a&&(["average_strength"].map((t=>bt(e,wt(e,t),!1))),["old_cond_start","old_cond_end","new_cond_start","new_cond_end"].map((t=>bt(e,wt(e,t),!0))));break;case"rescale":wt(e,"rescale_after_model").value,bt(e,wt(e,"width"),"to Width/Height"===a),bt(e,wt(e,"height"),"to Width/Height"===a),bt(e,wt(e,"percent"),"by percentage"===a),bt(e,wt(e,"longer_side"),"to longer side - maintain aspect"===a),St(e);break;case"upscale_method":["factor","crop"].map((t=>bt(e,wt(e,t),"None"!==a)));break;case"target_parameter":switch(i){case"easy XYInputs: Steps":["first_step","last_step"].map((t=>bt(e,wt(e,t),"steps"==a))),["first_start_step","last_start_step"].map((t=>bt(e,wt(e,t),"start_at_step"==a))),["first_end_step","last_end_step"].map((t=>bt(e,wt(e,t),"end_at_step"==a)));break;case"easy XYInputs: Sampler/Scheduler":let t=wt(e,"input_count").value+1;for(let s=0;sbt(e,wt(e,t),"strength"==a))),["first_start_percent","last_start_percent"].map((t=>bt(e,wt(e,t),"start_percent"==a))),["first_end_percent","last_end_percent"].map((t=>bt(e,wt(e,t),"end_percent"==a))),["strength","start_percent","end_percent"].map((t=>bt(e,wt(e,t),a!=t))),St(e)}case"replace_count":n=a+1;for(let t=0;tbt(e,wt(e,t),!d)));for(let t=n;t<11;t++)["lora_name_"+t,"model_str_"+t,"clip_str_"+t].map((t=>bt(e,wt(e,t),!1)));St(e);break;case"ckpt_count":n=a+1;var u=-1!=wt(e,"input_mode").value.indexOf("ClipSkip"),c=-1!=wt(e,"input_mode").value.indexOf("VAE");for(let t=0;tbt(e,wt(e,t),!1)));St(e);break;case"input_count":n=a+1;var A=wt(e,"target_parameter").value;for(let t=0;tbt(e,wt(e,s),!!t)));["model_strength","clip_strength"].map((s=>bt(e,wt(e,s),!t)));break;case"easy XYInputs: Checkpoint":n=wt(e,"ckpt_count").value+1,u=-1!=wt(e,"input_mode").value.indexOf("ClipSkip"),c=-1!=wt(e,"input_mode").value.indexOf("VAE");for(let s=0;se.name===t));if(-1!==e){for(let t=e;t"info"===e.name));if(-1!==t&&this.widgets[t]){this.widgets[t].value=e}}};e.prototype.onExecuted=function(e){null==r||r.apply(this,arguments);var s="";for(let t=0;t"prompt"==e.name));this.addWidget("button","get values from COMBO link","",(()=>{var t,i;const a=(null==(i=null==(t=this.outputs[1])?void 0:t.links)?void 0:i.length)>0?this.outputs[1].links[0]:null,n=s.graph._nodes.find((e=>{var t;return null==(t=e.inputs)?void 0:t.find((e=>e.link==a))}));if(a&&n){const t=n.inputs.find((e=>e.link==a)).widget.name,s=n.widgets.find((e=>e.name==t));let i=(null==s?void 0:s.options.values)||null;i&&(i=i.join("\n"),e.value=i)}else Ye.error(Ne("No COMBO link"),3e3)}),{serialize:!1})}),rs.includes(t.name)){let t=function(e){var t="";for(let s=0;se.name===t+"_prompt")),i="comfy-multiline-input wildcard_"+t+"_"+this.id.toString();if(-1==s&&e){const s=document.createElement("textarea");s.className=i,s.placeholder="Wildcard Prompt ("+t+")";const a=this.addDOMWidget(t+"_prompt","customtext",s,{getValue:e=>s.value,setValue(e){s.value=e},serialize:!1});a.inputEl=s,a.inputEl.readOnly=!0,s.addEventListener("input",(()=>{var e;null==(e=a.callback)||e.call(a,a.value)})),a.value=e}else if(this.widgets[s])if(e){this.widgets[s].value=e}else{this.widgets.splice(s,1);const e=document.getElementsByClassName(i);e&&e[0]&&e[0].remove()}}};e.prototype.onExecuted=function(e){null==r||r.apply(this,arguments);const i=t(e.positive),a=t(e.negative);s.call(this,i,"positive"),s.call(this,a,"negative")}}if(["easy sv3dLoader"].includes(t.name)){let t=function(e,t,s){switch(e){case"azimuth":return s.readOnly=!0,s.style.opacity=.6,"0:(0.0,0.0)"+(t>1?`\n${t-1}:(360.0,0.0)`:"");case"elevation":return s.readOnly=!0,s.style.opacity=.6,"0:(-90.0,0.0)"+(t>1?`\n${t-1}:(90.0,0.0)`:"");case"custom":return s.readOnly=!1,s.style.opacity=1,"0:(0.0,0.0)\n9:(180.0,0.0)\n20:(360.0,0.0)"}};e.prototype.onNodeCreated=async function(){o&&o.apply(this,[]);const e=this.widgets.find((e=>"easing_mode"==e.name)),s=this.widgets.find((e=>"batch_size"==e.name)),i=this.widgets.find((e=>"scheduler"==e.name));setTimeout((a=>{i.value||(i.value=t(e.value,s.value,i.inputEl))}),1),e.callback=e=>{i.value=t(e,s.value,i.inputEl)},s.callback=s=>{i.value=t(e.value,s,i.inputEl)}}}if(ls.includes(a)&&(e.prototype.onNodeCreated=async function(){o&&o.apply(this,[]);const e=this.widgets.find((e=>["seed_num","seed"].includes(e.name))),i=this.widgets.find((e=>["control before generate","control after generate","control_before_generate","control_after_generate"].includes(e.name)));if("easy seed"==t.name&&i){const t=this.addWidget("button","🎲 Manual Random Seed",null,(t=>{"fixed"!=i.value&&(i.value="fixed"),e.value=Math.floor(Math.random()*Oe),s.queuePrompt(0,1)}),{serialize:!1});e.linkedWidgets=[t,i]}},e.prototype.onAdded=async function(){n&&n.apply(this,[]);const e=this.widgets.find((e=>["seed_num","seed"].includes(e.name))),t=this.widgets.find((e=>["control before generate","control after generate","control_before_generate","control_after_generate"].includes(e.name)));t&&setTimeout((s=>{["control before generate","control_before_generate"].includes(t.name)&&0===e.value&&(e.value=Math.floor(Math.random()*Oe))}),1)}),"easy convertAnything"==a&&(e.prototype.onNodeCreated=async function(){o&&o.apply(this,[]);const e=this.widgets.find((e=>"output_type"==e.name)),t=t=>{this.outputs[0].type=e.value.toUpperCase(),this.outputs[0].name=e.value,this.outputs[0].label=e.value};setTimeout((e=>t()),10),e.callback=e=>t()}),"easy imageInsetCrop"==a){let t=function(e){const t=e.widgets[0];for(let s=1;s<=4;s++)"Pixels"===t.value?(e.widgets[s].options.step=80,void 0!==e.widgets[s].options.step2&&(e.widgets[s].options.step2=8),e.widgets[s].options.max=8192):(e.widgets[s].options.step=10,void 0!==e.widgets[s].options.step2&&(e.widgets[s].options.step2=1),e.widgets[s].options.max=99)};e.prototype.onAdded=async function(e){const s=this.widgets[0];let i=s.callback;s.callback=(...e)=>{t(this),i&&i.apply(s,[...e])},setTimeout((e=>{t(this)}),1)}}if(hs.includes(a)){const t=e=>{switch(a){case"easy forLoopStart":case"easy whileLoopStart":return 0;case"easy forLoopEnd":case"easy whileLoopEnd":return 1}},s=e=>{switch(a){case"easy forLoopStart":return 2;case"easy forLoopEnd":case"easy whileLoopEnd":return 0;case"easy whileLoopStart":return 1}};e.prototype.onNodeCreated=async function(){if("easy loadImagesForLoop"==a&&(this.outputs[0].shape=5),cs.includes(a)){const e=this.inputs.findIndex((e=>"flow"===e.name)),i=this.outputs.findIndex((e=>"flow"===e.name));-1!==e&&(this.inputs[e].shape=5),-1!==i&&(this.outputs[i].shape=5),this.inputs=this.inputs.filter(((e,s)=>s<=t()||e.widget)),this.outputs=this.outputs.filter(((e,t)=>t<=s())),St(this)}return As.includes(a)&&("easy textIndexSwitch"==a&&(this.widgets=this.widgets.filter(((e,t)=>t<=2))),this.inputs=this.inputs.filter(((e,t)=>t<=1||e.widget)),St(this)),null==o?void 0:o.apply(this,arguments)},e.prototype.onConnectionsChange=function(e,i,n,o){var l,r;if(o)if(1==e){let e=this.inputs.every((e=>null!==e.link||e.widget)),s=this.inputs.filter((e=>!["condition","index","total"].includes(e.name)));if(cs.includes(a)){if(e){if(s.length>=20)return void Ye.warn(Ne("The maximum number of inputs is 20"));let e=s[s.length-1],t=parseInt(e.name.split("initial_value")[1])+1;if(this.inputs.find((e=>e.name==="initial_value"+t)))return;let i="initial_value"+t,a="value"+t;this.addInput(i,"*"),this.addOutput(a,"*")}else if(!n){const e=t();let s=this.inputs.findLastIndex((e=>e.link));if(i>=e&&(-1===s||i>=s)){let e=this.inputs[i];if(!e.name||["condition","total"].includes(e.name))return;let t=parseInt(e.name.split("initial_value")[1])+1,s=this.inputs.findIndex((e=>e.name==="initial_value"+t)),a=this.outputs.findIndex((e=>e.name==="value"+t));-1!==s&&this.removeInput(s),-1!==a&&this.removeOutput(a)}}}else if(As.includes(a))if(e){if(s.length>=20)return void Ye.warn(Ne("The maximum number of inputs is 20"));let e=ms[a]+s.length;this.addInput(e,"*")}else n||i==this.inputs.length-2&&this.removeInput(i+1)}else if(2==e){let e=this.outputs.filter((e=>!["flow","index"].includes(e.name))),t=e.every((e=>{var t;return(null==(t=e.links)?void 0:t.length)>0}));if(cs.includes(a)){if(t){if(e.length>=20)return void Ye.warn(Ne("The maximum number of outputs is 20"));let t=e[e.length-1],s=parseInt(t.name.split("value")[1])+1;if(this.inputs.find((e=>e.name==="initial_value"+s)))return;if(this.outputs.find((e=>e.name==="value"+s)))return;let i="initial_value"+s,a="value"+s;this.addInput(i,"*"),this.addOutput(a,"*")}else if(!n){const e=s();let t=o.origin_slot,i=this.outputs.findLastIndex((e=>{var t;return(null==(t=e.links)?void 0:t.length)>0}));if(t>=e&&(-1===i||t>=i)){let e=this.outputs[t];if(!e.name||["flow","index"].includes(e.name))return;let s=parseInt(e.name.split("value")[1])+1,i=this.inputs.findIndex((e=>e.name==="initial_value"+s)),a=this.outputs.findIndex((e=>e.name==="value"+s));if(-1!==i&&(null==(l=this.inputs[i])?void 0:l.link))return;-1!==i&&this.removeInput(i),-1!==a&&this.removeOutput(a)}}}else if(ps.includes(a))if(t){if(e.length>=20)return void Ye.warn(Ne("The maximum number of outputs is 20"));let t=ms[a]+e.length;this.addOutput(t,"*")}else if(!n){let t=o.origin_slot;t==this.outputs.length-2&&0==(null==(r=e[t].links)?void 0:r.length)&&this.removeOutput(t+1)}}}}if(["easy fluxLoader","easy fullLoader"].includes(a)&&(e.prototype.onConnectionsChange=async function(e,t){d&&d.apply(this,[]);const s=this.inputs.find((e=>"model_override"===e.name)),i=this.inputs.find((e=>"vae_override"===e.name));let a=wt(this,"ckpt_name"),n=wt(this,"vae_name");if((null==s?void 0:s.link)&&0==t){const e=(null==a?void 0:a.options.values)||[];setTimeout((t=>{this.widgets[0].value=(null==e?void 0:e[0])||"None",bt(this,a,!(null==s?void 0:s.link))}),1)}else bt(this,a,!(null==s?void 0:s.link));if(!(null==i?void 0:i.link)||1!=t&&2!=t)bt(this,n,!(null==i?void 0:i.link));else{let e=this.widgets.findIndex((e=>"vae_name"==e.name));setTimeout((t=>{this.widgets[e].value="Baked VAE",bt(this,n,!(null==i?void 0:i.link))}),1)}}),ds.includes(a)||-1!==a.indexOf("workflow")){const s=(null==(i=null==t?void 0:t.input)?void 0:i.required)||{};if(!s.image_output&&!s["图像输出"]&&!s["视频输出"])return;e.prototype.onDrawBackground=function(e){try{is(this)}catch(t){}}}},nodeCreated(e){if(e.comfyClass.startsWith("easy ")){if(e.widgets)for(const s of e.widgets){if(!as.includes(s.name))continue;let t=s.value;fs(e,s),Object.defineProperty(s,"value",{get:e=>t,set(i){i!==t&&(t=i,requestAnimationFrame((t=>{fs(e,s)})))}})}const t=e.comfyClass;if("easy preDetailerFix"==t){const t=e.widgets.find((e=>"customtext"===e.type));if(!t)return;t.dynamicPrompts=!1,t.inputEl.placeholder="wildcard spec: if kept empty, this option will be ignored",t.serializeValue=()=>t.value}if(["easy wildcards","easy wildcardsMatrix"].includes(t)){const t=e.widgets.find((e=>"text"==e.name));let s=1;Object.defineProperty(e.widgets[s],"value",{set:e=>{if("Select the LoRA to add to the text"!=e){let s=e;s.endsWith(".safetensors")&&(s=s.slice(0,-12)),t.value+=``}},get:e=>"Select the LoRA to add to the text"}),Object.defineProperty(e.widgets[s+1],"value",{set:e=>{"Select the Wildcard to add to the text"!=e&&(""!=t.value&&(t.value+=", "),t.value+=e)},get:e=>"Select the Wildcard to add to the text"}),e.widgets[s].serializeValue=e=>"Select the LoRA to add to the text",e.widgets[s+1].serializeValue=e=>"Select the Wildcard to add to the text"}if("easy prompt"==t){const t=e.widgets.find((e=>"text"==e.name));let s=1;Object.defineProperty(e.widgets[s],"value",{set:i=>{var a,n;"Select the prefix to add to the text"!=i&&(null==(n=null==(a=e.widgets[s])?void 0:a.options)?void 0:n.values.includes(i))&&(""==t.value?t.value+=i:t.value+=" "+i)},get:e=>"Select the prefix to add to the text"}),Object.defineProperty(e.widgets[s+1],"value",{set:i=>{var a,n;"👤Select the subject add to the text"!=i&&(null==(n=null==(a=e.widgets[s+1])?void 0:a.options)?void 0:n.values.includes(i))&&(""==t.value?t.value+=i:t.value+=" "+i)},get:e=>"👤Select the subject add to the text"}),Object.defineProperty(e.widgets[s+2],"value",{set:i=>{var a,n;"🎬Select the action add to the text"!=i&&(null==(n=null==(a=e.widgets[s+2])?void 0:a.options)?void 0:n.values.includes(i))&&(""==t.value?t.value+=i:t.value+=","+i)},get:e=>"🎬Select the action add to the text"}),Object.defineProperty(e.widgets[s+3],"value",{set:i=>{var a,n;"👚Select the clothes add to the text"!=i&&(null==(n=null==(a=e.widgets[s+3])?void 0:a.options)?void 0:n.values.includes(i))&&(""==t.value?t.value+=i:t.value+=","+i)},get:e=>"👚Select the clothes add to the text"}),Object.defineProperty(e.widgets[s+4],"value",{set:i=>{var a,n;"☀️Select the illumination environment add to the text"!=i&&(null==(n=null==(a=e.widgets[s+4])?void 0:a.options)?void 0:n.values.includes(i))&&(""==t.value?t.value+=i:t.value+=","+i)},get:e=>"☀️Select the illumination environment add to the text"}),Object.defineProperty(e.widgets[s+5],"value",{set:i=>{var a,n;"🎞️Select the background add to the text"!=i&&(null==(n=null==(a=e.widgets[s+5])?void 0:a.options)?void 0:n.values.includes(i))&&(""==t.value?t.value+=i:t.value+=" "+i)},get:e=>"🎞️Select the background add to the text"}),Object.defineProperty(e.widgets[s+6],"value",{set:i=>{var a,n;"🔞Select the nsfw add to the text"!=i&&(null==(n=null==(a=e.widgets[s+6])?void 0:a.options)?void 0:n.values.includes(i))&&(""==t.value?t.value+=i:t.value+=","+i)},get:e=>"🔞Select the nsfw add to the text"}),e.widgets[s].serializeValue=e=>"Select the prefix add to the text",e.widgets[s+1].serializeValue=e=>"👤Select the subject add to the text",e.widgets[s+2].serializeValue=e=>"🎬Select the action add to the text",e.widgets[s+3].serializeValue=e=>"👚Select the clothes add to the text",e.widgets[s+4].serializeValue=e=>"☀️Select the illumination environment add to the text",e.widgets[s+5].serializeValue=e=>"🎞️Select the background add to the text",e.widgets[s+6].serializeValue=e=>"🔞Select the nsfw add to the text"}if(us.includes(t)){const t=document.createElement("textarea");t.className="comfy-multiline-input",t.readOnly=!0;const s=e.addDOMWidget("info","customtext",t,{getValue:e=>t.value,setValue:e=>t.value=e,serialize:!1});s.inputEl=t,t.addEventListener("input",(()=>{var e;null==(e=s.callback)||e.call(s,s.value)}))}}}});const gs=LiteGraph.LGraphNode;fe.registerExtension({name:"easy bookmark",registerCustomNodes(){class e extends gs{constructor(){super("🔖"),v(this,"type","easy bookmark"),v(this,"title","🔖"),v(this,"slot_start_y",-20),v(this,"___collapsed_width",0),v(this,"isVirtualNode",!0),v(this,"serialize_widgets",!0),v(this,"keypressBound",null),this.addWidget("text","shortcut_key","1",(e=>{""!==(e=e.trim()[0]||"1")&&(this.title="🔖 "+e)}),{y:8}),this.addWidget("number","zoom",1,(e=>{}),{y:8+LiteGraph.NODE_WIDGET_HEIGHT+4,max:2,min:.5,precision:2}),this.keypressBound=this.onKeypress.bind(this)}get _collapsed_width(){return this.___collapsed_width}set _collapsed_width(e){const t=fe.canvas,s=t.canvas.getContext("2d");if(s){const e=s.font;s.font=t.title_text_font,this.___collapsed_width=40+s.measureText(this.title).width,s.font=e}}onAdded(){setTimeout((e=>{const t=this.widgets[0].value;t&&(this.title="🔖 "+t)}),1),window.addEventListener("keydown",this.keypressBound)}onRemoved(){window.removeEventListener("keydown",this.keypressBound)}onKeypress(e){const t=e.target;["input","textarea"].includes(t.localName)||this.widgets[0]&&e.key.toLocaleLowerCase()===this.widgets[0].value.toLocaleLowerCase()&&this.canvasToBookmark()}canvasToBookmark(){var e,t;const s=fe.canvas;(null==(e=null==s?void 0:s.ds)?void 0:e.offset)&&(s.ds.offset[0]=16-this.pos[0],s.ds.offset[1]=40-this.pos[1]),null!=(null==(t=null==s?void 0:s.ds)?void 0:t.scale)&&(s.ds.scale=Number(this.widgets[1].value||1)),s.setDirty(!0,!0)}}LiteGraph.registerNodeType("easy bookmark",Object.assign(e,{title:"Bookmark 🔖"})),e.category="EasyUse/Util"}}),fe.registerExtension({name:"Comfy.EasyUse.ChainNode",init(){fe.canvas._mousemove_callback=e=>{Le("EasyUse.Nodes.ChainGetSet",null)&&((e=!1,t={})=>{var s,i,a,n,o;const l=Dt();if(!l||l.length<1)return;const r=xt();if(0===r.length)return;let d=t.inputX||160,u=t.ouputX||60;if(r.filter((e=>Mt(e))).length>1)return;for(const A of r){let n=t.inputY||10,o=t.outputY||30;const l=[],c=A.id;if(A.graph){Rt[c]||(Rt[c]=[]);for(const e of A.inputs??[]){const t=e.link;if(!t)continue;const{origin_id:s,target_slot:i}=Ct(t),a=Lt(s);if(!a)continue;if(!Mt(a))continue;let o=A.getConnectionPos(!0,i);Rt[c][i]||(Rt[c][i]=o),!Rt[c]||Rt[c][i][1]===o[1]&&Rt[c][i][0]===o[0]||(d=o[0]-Rt[c][i][0],n=o[1]-Rt[c][i][1],a.pos=[a.pos[0]+d,a.pos[1]+n]),Rt[c][i]=o,l.push(a)}Pt[c]||(Pt[c]=[]);for(const e of A.outputs??[])if(e.links&&A.graph)for(const t of e.links){const{target_id:e,target_slot:i,origin_slot:a}=Ct(t),n=Lt(e);if(!n)continue;if(!Mt(n))continue;const r=null==(s=n.outputs)?void 0:s.links;if((null==r?void 0:r.length)>1)return;const d=A.getConnectionPos(!1,a);Pt[c][a]||(Pt[c][a]=d),!Pt[c]||Pt[c][a][0]===d[0]&&Pt[c][a][1]===d[1]||(u=d[0]-Pt[c][a][0],o=d[1]-Pt[c][a][1],n.pos=[n.pos[0]+u,n.pos[1]+o]),Pt[c][a]=d,l.push(n)}if(e&&1===r.length){const e=[A,...l];(null==(a=null==(i=A.graph)?void 0:i.list_of_graphcanvas)?void 0:a[0]).selectNodes(e)}}}const c=r[0];if(!c)return;(null==(o=null==(n=c.graph)?void 0:n.list_of_graphcanvas)?void 0:o[0]).setDirty(!0,!0)})()};const e=LGraphCanvas.prototype.showLinkMenu;LGraphCanvas.prototype.showLinkMenu=it(e,(function(t,s){return s.shiftKey?(((e,t=!1)=>{var s,i,a,n,o,l,r,d,u,c;const{type:A}=e;if("*"===A)return;let{origin_id:p,target_id:h,origin_slot:m,target_slot:f}=e,g=Lt(p),v=Lt(h);if(!g||!v)return!1;if("Reroute"===g.type){let e=0;[g,e]=jt(g),p=null==g?void 0:g.id,m=e,void 0!==m&&-1!==m||(m=0)}if("Reroute"===v.type&&(v=Ut(v),h=null==v?void 0:v.id,f=null==v?void 0:v.inputs.findIndex((e=>e.type===A)),void 0!==f&&-1!==f||(f=0)),void 0===p||void 0===h||!g||!v)return!1;if(t&&(Mt(g)||Mt(v)))return!1;let y=It((null==(s=v.getInputInfo(f))?void 0:s.name)??A.toLowerCase());y||(y=It((null==(a=null==(i=null==g?void 0:g.outputs)?void 0:i[m])?void 0:a.name)??(null==(o=null==(n=null==g?void 0:g.outputs)?void 0:n[m])?void 0:o.type.toString())??y+`_from_${p}_to_${h}`));let w,b=!1,_=!1;if(Mt(g))y=_t(g),_=!0;else{const e=null==(r=null==(l=g.outputs)?void 0:l[m])?void 0:r.links;if(e)for(const t of e){const e=Lt((null==(d=Ct(t))?void 0:d.target_id)??-1);e&&Mt(e)&&Ot(e)&&(y=_t(e),_=!0)}if(!_){for(const e of Dt()){if(y!==_t(e)||!Ot(e))continue;const t=null==(u=e.inputs[0])?void 0:u.link;(null==(c=Ct(t))?void 0:c.origin_id)===g.id?_=!0:b=!0}b&&(y+=`_from_${p}_to_${h}`)}}if(!_){w=LiteGraph.createNode("easy setNode"),w.is_auto_link=!0;const e=g.getConnectionPos(!1,m);w.pos=[e[0]+20,e[1]],w.inputs[0].name=y,w.inputs[0].type=A,w.inputs[0].widget=v.inputs[f].widget,Gt(w,y),zt(w),w.flags.collapsed=!0;let t=[];g.widgets?t=Object.values(g.widgets).map((e=>e.value)):g.widgets_values&&(t=JSON.parse(JSON.stringify(g.widgets_values))),g.connect(m,w,0),g.widgets_values=t,"PrimitiveNode"===g.type&&setTimeout((()=>{if(g){g.connect(m,w,0);for(const[e,s]of t.entries())Gt(g,s,e);null!==w&&w.setSize(w.computeSize())}}))}const S=LiteGraph.createNode("easy getNode"),L=v.getConnectionPos(!0,f);S.pos=[L[0]-150,L[1]],S.outputs[0].name=y,S.outputs[0].type=A,S.outputs[0].widget=v.inputs[f].widget,zt(S),Gt(S,y),null===S||(S.flags.collapsed=!0,S.setSize(S.computeSize()),S.connect(0,v,f))})(t),!1):(e.apply(this,[t,s]),!1)}))}});const vs=async()=>{try{const{Running:e,Pending:t}=await ge.getQueue();if(e.length>0||t.length>0)return void Ye.error(Ne("Clean Failed")+":"+Ne("Please stop all running tasks before cleaning GPU"));200==(await ge.fetchApi("/easyuse/cleangpu",{method:"POST"})).status?Ye.success(Ne("Clean SuccessFully")):Ye.error(Ne("Clean Failed"))}catch(e){}};let ys={},ws=!1;function bs(e,t,s,i,a){var n=LGraphCanvas.active_canvas,o=n.getCanvasWindow(),l=n.graph;if(l)return function e(t,i){var r=LiteGraph.getNodeTypesCategories(n.filter||l.filter).filter((function(e){return e.startsWith(t)})),d=[];r.map((function(s){if(s){var i=new RegExp("^("+t+")"),a=s.replace(i,"").split("/")[0],n=""===t?a+"/":t+a+"/",o=a;-1!=o.indexOf("::")&&(o=o.split("::")[1]),-1===d.findIndex((function(e){return e.value===n}))&&d.push({value:n,content:o,has_submenu:!0,callback:function(t,s,i,a){e(t.value,a)}})}})),LiteGraph.getNodeTypesInCategory(t.slice(0,-1),n.filter||l.filter).map((function(e){if(!e.skip_list){var t={value:e.type,content:e.title,has_submenu:!1,callback:function(e,t,s,i){var o=i.getFirstEvent();n.graph.beforeChange();var l=LiteGraph.createNode(e.value);l&&(l.pos=n.convertEventToCanvasOffset(o),n.graph.add(l)),a&&a(l),n.graph.afterChange()}};d.push(t)}}));const u=Le("EasyUse.ContextMenu.NodesSort",null);""===t&&u&&(d=function(e){let t=[],s=[];return e.forEach((e=>{(null==e?void 0:e.value)&&Me.includes(e.value.split("/")[0])?t.push(e):s.push(e)})),[{title:Ne("ComfyUI Basic"),is_category_title:!0},...t,{title:Ne("Others A~Z"),is_category_title:!0},...s.sort(((e,t)=>e.content.localeCompare(t.content)))]}(d)),new LiteGraph.ContextMenu(d,{event:s,parentMenu:i},o)}("",i),!1}function _s(e){try{return encodeURIComponent(e).replace(/[!'()*]/g,(e=>`%${e.charCodeAt(0).toString(16).toUpperCase()}`))}catch(t){return e}}function Ss(e,t,s){var i=this;s=s||{};var a=document.createElement("div");a.className="litemenu-entry submenu";var n=!1;function o(e){var t=this.value,a=!0;(i.current_submenu&&i.current_submenu.close(e),s.callback)&&(!0===s.callback.call(this,t,s,e,i,s.node)&&(a=!1));if(t){if(t.callback&&!s.ignore_item_callbacks&&!0!==t.disabled)!0===t.callback.call(this,t,s,e,i,s.extra)&&(a=!1);if(t.submenu){if(!t.submenu.options)throw"ContextMenu submenu needs options";new i.constructor(t.submenu.options,{callback:t.submenu.callback,event:e,parentMenu:i,ignore_item_callbacks:t.submenu.ignore_item_callbacks,title:t.submenu.title,extra:t.submenu.extra,autoopen:s.autoopen}),a=!1}}a&&!i.lock&&i.close()}return null===t?a.classList.add("separator"):t.is_category_title?(a.classList.remove("litemenu-entry"),a.classList.remove("submenu"),a.classList.add("litemenu-title"),a.innerHTML=t.title):(a.innerHTML=t&&t.title?t.title:e,a.value=t,t&&(t.disabled&&(n=!0,a.classList.add("disabled")),(t.submenu||t.has_submenu)&&a.classList.add("has_submenu")),"function"==typeof t?(a.dataset.value=e,a.onclick_callback=t):a.dataset.value=t,t.className&&(a.className+=" "+t.className)),a&&(null==t?void 0:t.thumbnail)&&(a.addEventListener("mouseenter",xs(a,t,this.root),{passive:!0}),a.addEventListener("mouseleave",ks(),{passive:!0}),a.addEventListener("click",ks(),{passive:!0})),this.root.appendChild(a),n||a.addEventListener("click",o),!n&&s.autoopen&&LiteGraph.pointerListenerAdd(a,"enter",(function(e){var t=this.value;if(!t||!t.has_submenu)return;o.call(this,e)})),a}fe.registerExtension({name:"Comfy.EasyUse.ContextMenu",async setup(){LGraphCanvas.onMenuAdd=bs,Ls();const e=LiteGraph.ContextMenu;LiteGraph.ContextMenu=function(t,s){if(!(null==s?void 0:s.callback)||t.some((e=>"string"!=typeof e))){if(s.parentMenu);else if(s.extra);else if(s.scale);else{const e=Le("EasyUse.ContextMenu.QuickOptions",null);if(s.hasOwnProperty("extra")&&"Disable"!==e){if("At the forefront"==e?t.unshift(null):t.push(null),i=window.location.host,["192.168.","10.","127.",/^172\.((1[6-9]|2[0-9]|3[0-1])\.)/].some((e=>"string"==typeof e?i.startsWith(e):e.test(i)))){const s={content:`${Ne("Reboot ComfyUI")}`,callback:e=>(async()=>{if(confirm(Ne("Are you sure you'd like to reboot the server?")))try{ge.fetchApi("/easyuse/reboot")}catch(e){}})()};"At the forefront"==e?t.unshift(s):t.push(s)}const s=Le("EasyUse.Hotkeys.cleanVRAMUsed",null)?"("+qe("Shift+r")+")":"",a={content:`${Ne("Cleanup Of VRAM Usage")} ${s}`,callback:e=>vs()};"At the forefront"==e?t.unshift(a):t.push(a);const n=Le("EasyUse.Hotkeys.toggleNodesMap",null)?"("+qe("Shift+m")+")":"",o={content:`${Ne("Nodes Map")} ${n}`,callback:e=>{var t,s,i;const a=(null==(t=fe.extensionManager)?void 0:t.sidebarTab)||fe.extensionManager,n=(null==(s=fe.extensionManager.sidebarTab)?void 0:s.activeSidebarTabId)||(null==(i=fe.extensionManager)?void 0:i.activeSidebarTab);a.activeSidebarTabId=n==Pe?null:Pe}};"At the forefront"==e?t.unshift(o):t.push(o)}}return e.apply(this,[...arguments])}{const i=function(e,t){const s=Le("EasyUse.ContextMenu.ModelsThumbnails",null),i=Le("EasyUse.ContextMenu.SubDirectories",null);if(!s&&!i)return null;if(ws)return null;const a=["ckpt","pt","bin","pth","safetensors","gguf"];if((null==e?void 0:e.length)>0){const t=null==(n=e[e.length-1])?void 0:n.substring(n.lastIndexOf(".")+1);if(!a.includes(t))return null}var n;const o=t.callback,l=[...e];t.callback=null;const r=(e,t)=>{["None","无","無","なし"].includes(e.content)?o("None",t):o(l.find((t=>t.endsWith(e.content)),t))};if(s&&!i)return e.map((e=>{var t,s;let i=null==(t=ys[e])?void 0:t.folder,a=null==(s=ys[e])?void 0:s.pathIndex;const n=window.location.protocol,o=window.location.host;let l=i?`${`${n}//${o}`}/api/experiment/models/preview/${i}/${a}/${_s(e)}`:"";return{folder:i,content:e,fullName:e,title:ve("div.comfyui-easyuse-contextmenu-model",{},[ve("span",{},e)]).outerHTML,thumbnail:l,callback:r}}));const d=e,u={},c=[],A=[];for(const f of d){const e=f.indexOf("/")>-1?"/":"\\",t=f.split(e);if(t.length>1){const s=t.shift();u[s]=u[s]||[],u[s].push({value:t.join(e),fullValue:f})}else"CHOOSE"===f||f.startsWith("DISABLE ")?c.push({value:f,fullValue:f}):A.push({value:f,fullValue:f})}const p=Object.values(u).length,h=[],m=(e,t="",i)=>{var a,n;let o;o=ve("div.comfyui-easyuse-contextmenu-model",{},[ve("span",{},e)]);let l=null==(a=ys[i])?void 0:a.folder,d=null==(n=ys[i])?void 0:n.pathIndex;const u=window.location.protocol,c=window.location.host;let A=l?`${`${u}//${c}`}/api/experiment/models/preview/${l}/${d}/${_s(i)}`:"";return{folder:l,content:e,fullName:i,thumbnail:s?A:null,title:o.outerHTML,callback:r}};if(p>0){const e=(t,s)=>{let i=[],a=[];const n=t.map((({value:e,fullValue:t})=>{const n={},o=e.indexOf("/")>-1?"/":"\\",l=e.split(o);if(l.length>1){const e=l.shift();n[e]=n[e]||[],n[e].push({value:l.join(o),fullValue:t})}if(Object.values(u).length>0){let s=Object.keys(n)[0];s&&n[s]?i.push({key:s,value:n[s][0]}):a.push(m(e,s,t))}return m(e,s,t)}));if(i.length>0){let t={};return i.forEach((e=>{t[e.key]=t[e.key]||[],t[e.key].push(e.value)})),[...Object.entries(t).map((t=>({content:t[0],has_submenu:!0,callback:()=>{},submenu:{options:e(t[1],t[0])}}))),...a]}return n};for(const[t,s]of Object.entries(u))h.push({content:t,has_submenu:!0,callback:()=>{},submenu:{options:e(s,t)}})}h.push(...A.map((e=>m(e.value,"",e.fullValue)))),c.length>0&&h.push(...c.map((e=>m(e.value,"",e.fullValue))));return h}(t,s);return i?e.call(this,i,s):e.apply(this,[...arguments])}var i},LiteGraph.ContextMenu.prototype=e.prototype,Le("EasyUse.ContextMenu.NodesSort")&&(LiteGraph.ContextMenu.prototype.addItem=Ss),document.getElementById("graph-canvas").addEventListener("mouseenter",(e=>{setTimeout((e=>{const t=document.getElementById("easyuse-model-thumbnail");t&&0!=t.style.opacity&&(t.style.display="none",t.style.opacity=0,t.style.left="0px",t.style.top="0px")}),100)}))},async beforeRegisterNodeDef(e,t,s){const i=e.prototype.onNodeCreated;["CheckpointLoader|pysssss","LoraLoader|pysssss"].includes(t.name)&&(e.prototype.onNodeCreated=async function(){i&&i.apply(this,[]);let e=wt(this,"lora_name")||wt(this,"ckpt_name");if(e){let t=e.onClick;e.onClick=function(e){return ws=!0,setTimeout((e=>{ws=!1}),500),t.call(this,e)}}})}});const Ls=async()=>{["checkpoints","loras","diffusion_models"].map((async e=>{const t=await ge.getModels(e);(null==t?void 0:t.length)>0&&t.map((t=>{ys[t.name]={folder:e,pathIndex:t.pathIndex}}))}))},xs=(e,t,s)=>i=>{var a;const n=t=>{setTimeout((i=>{const a=s.getBoundingClientRect();if(!a)return;const n=document.body.getBoundingClientRect();if(!n)return;const{left:o,top:l}=((e,t,s)=>{const{x:i}=e.getBoundingClientRect();let{top:a,left:n}=t;const{width:o,height:l}=s;return i<=o&&(n+=t.width),t.top<=l||(a=l),{left:n,top:a}})(e,a,n),r=document.getElementById("easyuse-model-thumbnail");r.src=t,r.style.left=`${o}px`,r.style.top=`${l}px`,r.style.display="block",r.style.opacity=1,r.onerror=e=>{r.src=Ge}}),10)};if(null==(a=null==ys?void 0:ys[t.fullName])?void 0:a.img){let e=ys[t.fullName].img;n("no_preview_image"==e?Ge:e.src)}else{let e=new Image;e.src=t.thumbnail,e.onload=s=>{ys[t.fullName].img=e,n(t.thumbnail)},e.onerror=s=>{e=null,ys[t.fullName].img="no_preview_image",n(Ge)}}},ks=()=>e=>{const t=document.getElementById("easyuse-model-thumbnail");t&&0!=t.style.opacity&&(t.style.display="none",t.style.opacity=0,t.style.left="0px",t.style.top="0px")};class Es extends ye{constructor(){super(),this.element.classList.add("easyuse-model-metadata")}show(e){super.show(ve("div",Object.keys(e).map((t=>ve("div",[ve("label",{textContent:t}),ve("span",{textContent:e[t]})])))))}}class Ns extends ye{constructor(e){super(),this.name=e,this.element.classList.add("easyuse-model-info")}get customNotes(){return this.metadata["easyuse.notes"]}set customNotes(e){this.metadata["easyuse.notes"]=e}get hash(){return this.metadata["easyuse.sha256"]}async show(e,t){this.type=e;const s=ge.fetchApi("/easyuse/metadata/"+encodeURIComponent(`${e}/${t}`));this.info=ve("div",{style:{flex:"auto"}}),this.imgCurrent=0,this.imgList=ve("div.easyuse-preview-list",{style:{display:"none"}}),this.imgWrapper=ve("div.easyuse-preview",[ve("div.easyuse-preview-group",[this.imgList])]),this.main=ve("main",{style:{display:"flex"}},[this.imgWrapper,this.info]),this.content=ve("div.easyuse-model-content",[ve("div.easyuse-model-header",[ve("h2",{textContent:this.name})]),this.main]);const i=ve("div",{textContent:"ℹ️ Loading...",parent:this.content});super.show(this.content),this.metadata=await(await s).json(),this.viewMetadata.style.cursor=this.viewMetadata.style.opacity="",this.viewMetadata.removeAttribute("disabled"),i.remove(),this.addInfo()}createButtons(){const e=super.createButtons();return this.viewMetadata=ve("button",{type:"button",textContent:"View raw metadata",disabled:"disabled",style:{opacity:.5,cursor:"not-allowed"},onclick:e=>{this.metadata&&(new Es).show(this.metadata)}}),e.unshift(this.viewMetadata),e}parseNote(){if(!this.customNotes)return[];let e=[];const t=new RegExp("(\\bhttps?:\\/\\/[^\\s]+)","g");let s,i=0;do{let a;s=t.exec(this.customNotes);let n=0;s?(a=s.index,n=s.index+s[0].length):a=this.customNotes.length;let o=this.customNotes.substring(i,a);o&&(o=o.replaceAll("\n","
"),e.push(ve("span",{innerHTML:o}))),s&&e.push(ve("a",{href:s[0],textContent:s[0],target:"_blank"})),i=n}while(s);return e}addInfoEntry(e,t){return ve("p",{parent:this.info},["string"==typeof e?ve("label",{textContent:e+": "}):e,"string"==typeof t?ve("span",{textContent:t}):t])}async getCivitaiDetails(){const e=await fetch("https://civitai.com/api/v1/model-versions/by-hash/"+this.hash);if(200===e.status)return await e.json();throw 404===e.status?new Error("Model not found"):new Error(`Error loading info (${e.status}) ${e.statusText}`)}addCivitaiInfo(){const e=this.getCivitaiDetails(),t=ve("span",{textContent:"ℹ️ Loading..."});return this.addInfoEntry(ve("label",[ve("img",{style:{width:"18px",position:"relative",top:"3px",margin:"0 5px 0 0"},src:"https://civitai.com/favicon.ico"}),ve("span",{textContent:"Civitai: "})]),t),e.then((e=>{var t,s;this.imgWrapper.style.display="block";let i=this.element.querySelector(".easyuse-model-header");i&&i.replaceChildren(ve("h2",{textContent:this.name}),ve("div.easyuse-model-header-remark",[ve("h5",{textContent:Ne("Updated At:")+Ze(new Date(e.updatedAt),"yyyy/MM/dd")}),ve("h5",{textContent:Ne("Created At:")+Ze(new Date(e.updatedAt),"yyyy/MM/dd")})]));let a=null,n=this.parseNote.call(this),o=Ne("✏️ Edit"),l=ve("div.easyuse-model-detail-textarea",[ve("p",(null==n?void 0:n.length)>0?n:{textContent:Ne("No notes")})]);if(n&&0!=n.length?l.classList.remove("empty"):l.classList.add("empty"),this.info.replaceChildren(ve("div.easyuse-model-detail",[ve("div.easyuse-model-detail-head.flex-b",[ve("span",Ne("Notes")),ve("a",{textContent:o,href:"#",style:{fontSize:"12px",float:"right",color:"var(--warning-color)",textDecoration:"none"},onclick:async e=>{if(e.preventDefault(),a){if(a.value!=this.customNotes){Ye.showLoading(Ne("Saving Notes...")),this.customNotes=a.value;const e=await ge.fetchApi("/easyuse/metadata/notes/"+encodeURIComponent(`${this.type}/${this.name}`),{method:"POST",body:this.customNotes});if(Ye.hideLoading(),200!==e.status)return Ye.error(Ne("Saving Failed")),void alert(`Error saving notes (${e.status}) ${e.statusText}`);Ye.success(Ne("Saving Succeed")),n=this.parseNote.call(this),l.replaceChildren(ve("p",(null==n?void 0:n.length)>0?n:{textContent:Ne("No notes")})),a.value?l.classList.remove("empty"):l.classList.add("empty")}else l.replaceChildren(ve("p",{textContent:Ne("No notes")})),l.classList.add("empty");e.target.textContent=o,a.remove(),a=null}else e.target.textContent="💾 Save",a=ve("textarea",{placeholder:Ne("Type your notes here"),style:{width:"100%",minWidth:"200px",minHeight:"50px",height:"100px"},textContent:this.customNotes}),l.replaceChildren(a),a.focus()}})]),l]),ve("div.easyuse-model-detail",[ve("div.easyuse-model-detail-head",{textContent:Ne("Details")}),ve("div.easyuse-model-detail-body",[ve("div.easyuse-model-detail-item",[ve("div.easyuse-model-detail-item-label",{textContent:Ne("Type")}),ve("div.easyuse-model-detail-item-value",{textContent:e.model.type})]),ve("div.easyuse-model-detail-item",[ve("div.easyuse-model-detail-item-label",{textContent:Ne("BaseModel")}),ve("div.easyuse-model-detail-item-value",{textContent:e.baseModel})]),ve("div.easyuse-model-detail-item",[ve("div.easyuse-model-detail-item-label",{textContent:Ne("Download")}),ve("div.easyuse-model-detail-item-value",{textContent:(null==(t=e.stats)?void 0:t.downloadCount)||0})]),ve("div.easyuse-model-detail-item",[ve("div.easyuse-model-detail-item-label",{textContent:Ne("Trained Words")}),ve("div.easyuse-model-detail-item-value",{textContent:(null==e?void 0:e.trainedWords.join(","))||"-"})]),ve("div.easyuse-model-detail-item",[ve("div.easyuse-model-detail-item-label",{textContent:Ne("Source")}),ve("div.easyuse-model-detail-item-value",[ve("label",[ve("img",{style:{width:"14px",position:"relative",top:"3px",margin:"0 5px 0 0"},src:"https://civitai.com/favicon.ico"}),ve("a",{href:"https://civitai.com/models/"+e.modelId,textContent:"View "+e.model.name,target:"_blank"})])])])])])),null==(s=e.images)?void 0:s.length){this.imgCurrent=0,this.isSaving=!1,e.images.map((e=>e.url&&this.imgList.appendChild(ve("div.easyuse-preview-slide",[ve("div.easyuse-preview-slide-content",[ve("img",{src:e.url}),ve("div.save",{textContent:"Save as preview",onclick:async()=>{if(this.isSaving)return;this.isSaving=!0,Ye.showLoading(Ne("Saving Preview..."));const t=await(await fetch(e.url)).blob(),s="temp_preview."+new URL(e.url).pathname.split(".")[1],i=new FormData;i.append("image",new File([t],s)),i.append("overwrite","true"),i.append("type","temp");if(200!==(await ge.fetchApi("/upload/image",{method:"POST",body:i})).status)return this.isSaving=!1,Ye.error(Ne("Saving Failed")),Ye.hideLoading(),void alert(`Error saving preview (${req.status}) ${req.statusText}`);await ge.fetchApi("/easyuse/save/"+encodeURIComponent(`${this.type}/${this.name}`),{method:"POST",body:JSON.stringify({filename:s,type:"temp"}),headers:{"content-type":"application/json"}}).then((e=>{Ye.success(Ne("Saving Succeed")),Ye.hideLoading()})),this.isSaving=!1,app.refreshComboInNodes()}})])]))));let t=this;this.imgDistance=(-660*this.imgCurrent).toString(),this.imgList.style.display="",this.imgList.style.transform="translate3d("+this.imgDistance+"px, 0px, 0px)",this.slides=this.imgList.querySelectorAll(".easyuse-preview-slide"),this.slideLeftButton=ve("button.left",{parent:this.imgWrapper,style:{display:e.images.length<=2?"none":"block"},innerHTML:'',onclick:()=>{e.images.length<=2||(t.imgList.classList.remove("no-transition"),0==t.imgCurrent?(t.imgCurrent=e.images.length/2-1,this.slides[this.slides.length-1].style.transform="translate3d("+(-660*(this.imgCurrent+1)).toString()+"px, 0px, 0px)",this.slides[this.slides.length-2].style.transform="translate3d("+(-660*(this.imgCurrent+1)).toString()+"px, 0px, 0px)",t.imgList.style.transform="translate3d(660px, 0px, 0px)",setTimeout((e=>{this.slides[this.slides.length-1].style.transform="translate3d(0px, 0px, 0px)",this.slides[this.slides.length-2].style.transform="translate3d(0px, 0px, 0px)",t.imgDistance=(-660*this.imgCurrent).toString(),t.imgList.style.transform="translate3d("+t.imgDistance+"px, 0px, 0px)",t.imgList.classList.add("no-transition")}),500)):(t.imgCurrent=t.imgCurrent-1,t.imgDistance=(-660*this.imgCurrent).toString(),t.imgList.style.transform="translate3d("+t.imgDistance+"px, 0px, 0px)"))}}),this.slideRightButton=ve("button.right",{parent:this.imgWrapper,style:{display:e.images.length<=2?"none":"block"},innerHTML:'',onclick:()=>{if(!(e.images.length<=2))if(t.imgList.classList.remove("no-transition"),t.imgCurrent>=e.images.length/2-1){t.imgCurrent=0;const s=e.images.length/2;this.slides[0].style.transform="translate3d("+(660*s).toString()+"px, 0px, 0px)",this.slides[1].style.transform="translate3d("+(660*s).toString()+"px, 0px, 0px)",t.imgList.style.transform="translate3d("+(-660*s).toString()+"px, 0px, 0px)",setTimeout((e=>{this.slides[0].style.transform="translate3d(0px, 0px, 0px)",this.slides[1].style.transform="translate3d(0px, 0px, 0px)",t.imgDistance=(-660*this.imgCurrent).toString(),t.imgList.style.transform="translate3d("+t.imgDistance+"px, 0px, 0px)",t.imgList.classList.add("no-transition")}),500)}else t.imgCurrent=t.imgCurrent+1,t.imgDistance=(-660*this.imgCurrent).toString(),t.imgList.style.transform="translate3d("+t.imgDistance+"px, 0px, 0px)"}})}return e.description&&ve("div",{parent:this.content,innerHTML:e.description,style:{marginTop:"10px"}}),e})).catch((e=>{this.imgWrapper.style.display="none",t.textContent="⚠️ "+e.message})).finally((e=>{}))}}class Cs extends Ns{async addInfo(){await this.addCivitaiInfo()}}class Is extends Ns{getTagFrequency(){if(!this.metadata.ss_tag_frequency)return[];const e=JSON.parse(this.metadata.ss_tag_frequency),t={};for(const s in e){const i=e[s];for(const e in i)e in t?t[e]+=i[e]:t[e]=i[e]}return Object.entries(t).sort(((e,t)=>t[1]-e[1]))}getResolutions(){let e=[];if(this.metadata.ss_bucket_info){const t=JSON.parse(this.metadata.ss_bucket_info);if(null==t?void 0:t.buckets)for(const{resolution:s,count:i}of Object.values(t.buckets))e.push([i,`${s.join("x")} * ${i}`])}e=e.sort(((e,t)=>t[0]-e[0])).map((e=>e[1]));let t=this.metadata.ss_resolution;if(t){const s=t.split(","),i=s[0].replace("(",""),a=s[1].replace(")","");e.push(`${i.trim()}x${a.trim()} (Base res)`)}else(t=this.metadata["modelspec.resolution"])&&e.push(t+" (Base res");return e.length||e.push("⚠️ Unknown"),e}getTagList(e){return e.map((e=>ve("li.easyuse-model-tag",{dataset:{tag:e[0]},$:e=>{e.onclick=()=>{e.classList.toggle("easyuse-model-tag--selected")}}},[ve("p",{textContent:e[0]}),ve("span",{textContent:e[1]})])))}addTags(){let e,t=this.getTagFrequency();if(null==t?void 0:t.length){const s=t.length;let i;s>500&&(t=t.slice(0,500),e=ve("p",[ve("span",{textContent:"⚠️ Only showing first 500 tags "}),ve("a",{href:"#",textContent:`Show all ${s}`,onclick:()=>{i.replaceChildren(...this.getTagList(this.getTagFrequency())),e.remove()}})])),i=ve("ol.easyuse-model-tags-list",this.getTagList(t)),this.tags=ve("div",[i])}else this.tags=ve("p",{textContent:"⚠️ No tag frequency metadata found"});this.content.append(this.tags),e&&this.content.append(e)}async addInfo(){const e=this.addCivitaiInfo();this.addTags();const t=await e;t&&ve("div",{parent:this.content,innerHTML:t.description,style:{maxHeight:"250px",overflow:"auto"}})}createButtons(){const e=super.createButtons();function t(e,t){const s=ve("textarea",{parent:document.body,style:{position:"fixed"},textContent:t.map((e=>e.dataset.tag)).join(", ")});s.select();try{document.execCommand("copy"),e.target.dataset.text||(e.target.dataset.text=e.target.textContent),e.target.textContent="Copied "+t.length+" tags",setTimeout((()=>{e.target.textContent=e.target.dataset.text}),1e3)}catch(i){prompt("Copy to clipboard: Ctrl+C, Enter",text)}finally{document.body.removeChild(s)}}return e.unshift(ve("button",{type:"button",textContent:"Copy Selected",onclick:e=>{t(e,[...this.tags.querySelectorAll(".easyuse-model-tag--selected")])}}),ve("button",{type:"button",textContent:"Copy All",onclick:e=>{t(e,[...this.tags.querySelectorAll(".easyuse-model-tag")])}})),e}}const Ts={pipe:{category:"Easy Pipe",nodes:["easy pipeIn","easy pipeOut","easy pipeEdit","easy pipeEditPrompt","easy pipeBatchIndex"],input:{pipe:"pipe"},output:{pipe:"pipe"},widget:{optional_positive:"optional_positive",optional_negative:"optional_negative"}},loaders:{category:"Easy Loaders",nodes:["easy fullLoader","easy a1111Loader","easy comfyLoader","easy kolorsLoader","easy hunyuanDiTLoader","easy pixArtLoader","easy fluxLoader"],input:{optional_lora_stack:"optional_lora_stack",optional_controlnet_stack:"optional_controlnet_stack",positive:"positive",negative:"negative"},output:{pipe:"pipe",model:"model",vae:"vae",clip:null,positive:null,negative:null,latent:null},widget:{ckpt_name:"ckpt_name",vae_name:"vae_name",clip_skip:"clip_skip",lora_name:"lora_name",resolution:"resolution",empty_latent_width:"empty_latent_width",empty_latent_height:"empty_latent_height",positive:"positive",negative:"negative",batch_size:"batch_size",a1111_prompt_style:"a1111_prompt_style"}},preSampling:{category:"Easy PreSampling",nodes:["easy preSampling","easy preSamplingAdvanced","easy preSamplingDynamicCFG","easy preSamplingNoiseIn","easy preSamplingCustom","easy preSamplingLayerDiffusion","easy fullkSampler"],input:{pipe:"pipe",image_to_latent:"image_to_latent",latent:"latent"},output:{pipe:"pipe"},widget:{steps:"steps",cfg:"cfg",cfg_scale_min:"cfg",sampler_name:"sampler_name",scheduler:"scheduler",denoise:"denoise",seed_num:"seed_num",seed:"seed"}},samplers:{category:"Custom Sampler",nodes:["KSamplerSelect","SamplerEulerAncestral","SamplerEulerAncestralCFG++","SamplerLMS","SamplerDPMPP_3M_SDE","SamplerDPMPP_2M_SDE","SamplerDPMPP_SDE","SamplerDPMAdaptative","SamplerLCMUpscale","SamplerTCD","SamplerTCD EulerA"],output:{SAMPLER:"SAMPLER"}},sigmas:{category:"Custom Sigmas",nodes:["BasicScheduler","KarrasScheduler","ExponentialScheduler","PolyexponentialScheduler","VPScheduler","BetaSamplingScheduler","SDTurboScheduler","SplitSigmas","SplitSigmasDenoise","FlipSigmas","AlignYourStepsScheduler","GITSScheduler"],output:{SIGMAS:"SIGMAS"}},kSampler:{category:"Easy kSampler",nodes:["easy kSampler","easy kSamplerTiled","easy kSamplerCustom","easy kSamplerInpainting","easy kSamplerDownscaleUnet","easy kSamplerLayerDiffusion"],input:{pipe:"pipe",model:"model"},output:{pipe:"pipe",image:"image"},widget:{image_output:"image_output",save_prefix:"save_prefix",link_id:"link_id"}},controlNet:{category:"Easy ControlNet",nodes:["easy controlnetLoader","easy controlnetLoaderADV","easy controlnetLoader++","easy instantIDApply","easy instantIDApplyADV"],input:{pipe:"pipe",image:"image",image_kps:"image_kps",control_net:"control_net",positive:"positive",negative:"negative",mask:"mask"},output:{pipe:"pipe",positive:"positive",negative:"negative"},widget:{control_net_name:"control_net_name",strength:["strength","cn_strength"],scale_soft_weights:["scale_soft_weights","cn_soft_weights"],cn_strength:["strength","cn_strength"],cn_soft_weights:["scale_soft_weights","cn_soft_weights"]}},adapter:{category:"Easy Adapter",nodes:["easy ipadapterApply","easy ipadapterApplyADV","easy ipadapterApplyFaceIDKolors","easy ipadapterStyleComposition","easy ipadapterApplyFromParams","easy pulIDApply","easy pulIDApplyADV"],input:{model:"model",image:"image",image_style:"image",attn_mask:"attn_mask",optional_ipadapter:"optional_ipadapter"},output:{model:"model",tiles:"tiles",masks:"masks",ipadapter:"ipadapter"},widget:{preset:"preset",lora_strength:"lora_strength",provider:"provider",weight:"weight",weight_faceidv2:"weight_faceidv2",start_at:"start_at",end_at:"end_at",cache_mode:"cache_mode",use_tiled:"use_tiled",insightface:"insightface",pulid_file:"pulid_file"}},positive:{category:"Easy Positive",nodes:["easy positive","easy wildcards"],input:{},output:{text:"positive",positive:"text"},widget:{text:"positive",positive:"text"}},loadImage:{category:"Easy LoadImage",nodes:["easy loadImageBase64","LoadImage","LoadImageMask"],input:{pipe:"pipe",image:"image",mask:"mask"},output:{IMAGE:"IMAGE",MASK:"MASK"},widget:{image:"image",base64_data:"base64_data",channel:"channel"}},saveImage:{category:"Save/Preview Image",nodes:["SaveImage","PreviewImage"]},inPaint:{category:"Easy Inpaint",nodes:["easy applyBrushNet","easy applyPowerPaint","easy applyInpaint"],input:{},output:{pipe:"pipe"},widget:{dtype:"dtype",fitting:"fitting",function:"function",scale:"scale",start_at:"start_at",end_at:"end_at"}},showAny:{category:"Show Anything",nodes:["easy showAnything","Preview Any"],input:{anything:"anything"},output:{output:"output"}},saveText:{category:"Save Text",nodes:["easy saveText"],input:{image:"image",text:"text",output_file_path:"output_file_path",file_name:"file_name",file_extension:"file_extension",overwrite:"overwrite"},output:{text:"text",image:"image"},widget:{image:"image",text:"text",output_file_path:"output_file_path",file_name:"file_name",file_extension:"file_extension",overwrite:"overwrite"}},persona:{category:"LLM Party Persona",nodes:["load_persona","classify_persona","classify_persona_plus","custom_persona","translate_persona","flux_persona"],input:{file_content:"file_content"},output:{system_prompt:"system_prompt"},widget:{is_enable:"is_enable"}},llmModelLoader:{category:"LLM Model Loader",nodes:["LLM_api_loader","genai_api_loader","LLM_local_loader"],output:{model:"model"}},llmModelChain:{category:"LLM Model Chain",nodes:["LLM","LLM_local"],input:{model:"model",image:"images",images:"image",extra_parameters:"extra_parameters",system_prompt_input:"system_prompt_input",user_prompt_input:"user_prompt_input",tools:"tools",file_content:"file_content"},output:{assistant_response:"assistant_response",history:"history",tool:"tool",image:"image"},widget:{system_prompt:"system_prompt",user_prompt:"user_prompt",temperature:"temperature",is_memory:"is_memory",is_tools_in_sys_prompt:"is_tools_in_sys_prompt",max_length:"max_length",main_brain:"main_brain",conversation_rounds:"conversation_rounds",history_record:"history_record",is_enable:"is_enable"}},maskModify:{category:"Mask Modify",nodes:["CropMask","ThresholdMask","GrowMask","FeatherMask","LayerMask: MaskGrain","LayerMask: MaskEdgeUltraDetail","LayerMask: MaskEdgeUltraDetail V2"],input:{mask:"mask"},output:{MASK:"MASK",mask:"mask",image:"image"}},maskModifyWAS:{category:"Mask Modify (WAS)",nodes:["Mask Dilate Region","Mask Gaussian Region"],input:{masks:"masks"},output:{MASKS:"MASKS"}},indextts_emotions:{category:"IndexTTS Emotions",nodes:["easy indexTTSEmotionVector","easy indexTTSEmotionText","easy indexTTSEmotionAudio"],input:{reference_audio:"reference_audio"},output:{emotions:"emotions"}},indextts_generate:{category:"IndexTTS Generate",nodes:["easy indexTTSGenerate","easy indexTTSGenerateSimple"],input:{indextts_model:"indextts_model",reference_audio:"reference_audio",reference_audios:"reference_audios",emotions:"emotions"},output:{audio:"audio",seed:"seed",subtitle:"subtitle"},widget:{text:"text",unload_model:"unload_model",seed:"seed"}},fast_loaders:{category:"Fast Loaders",nodes:["fast ckptLoader","fast unetLoader"],input:{"开始加载":"开始加载"},output:{"模型加载器":"模型加载器",MODEL:"MODEL",VAE:"VAE",CLIP:"CLIP"}},fast_inputs:{category:"Fast Inputs",nodes:["fast sdInput","fast fluxInput","fast hiDreamInput","fast videoInput","fast qwenImageInput"],input:{"模型加载器":"模型加载器","更多Controlnet":"更多Controlnet","图像":"图像","遮罩":"遮罩","功能节点":"功能节点"},output:{"采样":"采样","正面条件":"正面条件","负面条件":"负面条件","潜空间":"潜空间"},widget:{Sigmas:"Sigmas","分离Sigmas步数":"分离Sigmas步数","分离降噪":"分离降噪","降噪":"降噪","添加噪声":"添加噪声",seed:"seed","生成数量":"生成数量"}},fast_outputResult:{category:"Fast Outputs",nodes:["fast outputResult","fast outputResultVideo","fast outputLatent"],input:{"采样":"采样","噪声":"噪声","模型加载器":"模型加载器"},output:{"图像":"图像序列","图像序列":"图像","输出":"输出","降噪输出":"降噪输出",VAE:"VAE"},widget:{"采样预览":"采样预览","VAE解码":"VAE解码","图像输出":"视频输出","视频输出":"图像输出","保存路径":"保存路径"}},fast_loadImage:{category:"Fast LoadImage",nodes:["fast loadImage","fast loadImageList"],output:{"图像":"图像","遮罩":"遮罩","文件名":"文件名"},widget:{"模式":"模式","图像":"图像","路径":"路径","最大张数":"最大张数","开始索引":"开始索引",Base64:"Base64","链接":"链接"}}};function Os(e,t,s){return function(){!function(e,t,s){var i;const a=LiteGraph.createNode(t);if(a){if(fe.graph.add(a),a.pos=e.pos.slice(),a.size=e.size.slice(),(null==(i=e.widgets)?void 0:i.length)>0&&e.widgets.forEach((e=>{var t,i,n;if(null==(i=null==(t=Ts[s])?void 0:t.widget)?void 0:i[e.name]){const t=Ts[s].widget[e.name];if(t&&a.widgets){const s=(n=t,a.widgets.find((e=>"object"==typeof n?n.includes(e.name):e.name===n)));s&&(s.value=e.value,"seed_num"==e.name&&(s.linkedWidgets[0].value=e.linkedWidgets[0].value),"converted-widget"==e.type&&Vs(a,s,e))}}})),e.inputs&&e.inputs.forEach(((t,i)=>{var n,o,l;if(t&&t.link&&(null==(o=null==(n=Ts[s])?void 0:n.input)?void 0:o[t.name])){const i=null==(l=Ts[s])?void 0:l.input[t.name];if(null===i)return;const n=a.findInputSlot(i);if(-1!==n){const s=e.graph.links[t.link];if(s){const t=e.graph.getNodeById(s.origin_id);t&&t.connect(s.origin_slot,a,n)}}}})),e.outputs&&e.outputs.forEach(((t,i)=>{var n,o;if(t&&t.links&&(null==(o=null==(n=Ts[s])?void 0:n.output)?void 0:o[t.name])){const i=Ts[s].output[t.name];if(null===i)return;const n=a.findOutputSlot(i);-1!==n&&t.links.forEach((t=>{const s=e.graph.links[t];if(s){const t=e.graph.getNodeById(s.target_id);t&&a.connect(n,t,s.target_slot)}}))}})),fe.graph.remove(e),"easy fullkSampler"==a.type){const e=a.outputs[0].links;if(e&&e[0]){const t=fe.graph._nodes.find((t=>t.inputs&&t.inputs[0]&&t.inputs[0].link==e[0]));t&&fe.graph.remove(t)}}else if(Ts.preSampling.nodes.includes(a.type)){const e=a.outputs[0].links;if(!e||!e[0]){const e=LiteGraph.createNode("easy kSampler");fe.graph.add(e),e.pos=a.pos.slice(),e.pos[0]=e.pos[0]+a.size[0]+20;const t=a.findInputSlot("pipe");-1!==t&&a&&a.connect(0,e,t)}}a.setSize([a.size[0],a.computeSize()[1]])}}(e,t,s)}}const Ms=(e,t)=>{const s=e.prototype.getExtraMenuOptions;e.prototype.getExtraMenuOptions=function(){const e=s.apply(this,arguments);return t.apply(this,arguments),e}},Ds=(e,t,s,i,a=!0)=>{Ms(i,(function(i,n){n.unshift({content:e,has_submenu:a,callback:(e,i,a,n,o)=>Rs(e,i,a,n,o,t,s)}),"loaders"==t&&(n.unshift({content:Ne("💎 View Lora Info..."),callback:(e,t,s,i,a)=>{let n=a.widgets.find((e=>"lora_name"==e.name)).value;n&&"None"!=n&&new Is(n).show("loras",n)}}),n.unshift({content:Ne("💎 View Checkpoint Info..."),callback:(e,t,s,i,a)=>{let n=a.widgets[0].value;n&&"None"!=n&&new Cs(n).show("checkpoints",n)}}))}))},Rs=(e,t,s,i,a,n,o)=>{const l=[];return o.map((e=>{a.type!==e&&l.push({content:`${e}`,callback:Os(a,e,n)})})),new LiteGraph.ContextMenu(l,{event:s,callback:null,parentMenu:i,node:a}),!1},Ps="converted-widget",Gs=Symbol();function zs(e,t,s=""){if(t.origType=t.type,t.origComputeSize=t.computeSize,t.origSerializeValue=t.serializeValue,t.computeSize=()=>[0,-4],t.type=Ps+s,t.serializeValue=()=>{if(!e.inputs)return;let s=e.inputs.find((e=>{var s;return(null==(s=e.widget)?void 0:s.name)===t.name}));return s&&s.link?t.origSerializeValue?t.origSerializeValue():t.value:void 0},t.linkedWidgets)for(const i of t.linkedWidgets)zs(e,i,":"+t.name)}function Vs(e,t,s){zs(e,t);const{type:i}=function(e){let t=e[0];t instanceof Array&&(t="COMBO");return{type:t}}(s),a=e.size;t.options&&t.options.forceInput||e.addInput(t.name,i,{widget:{name:t.name,[Gs]:()=>s}});for(const n of e.widgets)n.last_y+=LiteGraph.NODE_SLOT_HEIGHT;e.setSize([Math.max(a[0],e.size[0]),Math.max(a[1],e.size[1])])}const js=function(e){var t,s,i,a;const n=e.constructor.type,o=e.properties.origVals||{},l=o.title||e.title,r=o.color||e.color,d=o.bgcolor||e.bgcolor,u=e,c={size:[...e.size],color:r,bgcolor:d,pos:[...e.pos]};let A=[],p=[];if(e.inputs)for(const g of e.inputs)if(g.link){const t=g.name,s=e.findInputSlot(t),i=e.getInputNode(s),a=e.getInputLink(s);A.push([a.origin_slot,i,t])}if(e.outputs)for(const g of e.outputs)if(g.links){const e=g.name;for(const t of g.links){const s=graph.links[t],i=graph._nodes_by_id[s.target_id];p.push([e,i,s.target_slot])}}fe.graph.remove(e);let h=fe.graph.add(LiteGraph.createNode(n,l,c));function m(){if(u.widgets)for(let e of u.widgets)if("converted-widget"===e.type){const t=h.widgets.find((t=>t.name===e.name));for(let s of u.inputs)s.name===e.name&&Vs(h,t,s.widget)}for(let e of A){const[t,s,i]=e;s.connect(t,h.id,i)}for(let e of p){const[t,s,i]=e;h.connect(t,s,i)}}h.inputs.map(((t,s)=>{h.inputs[s].label=e.inputs[s].label})),h.outputs.map(((t,s)=>{h.outputs[s].label=e.outputs[s].label}));let f=u.widgets_values;if(!f&&(null==(t=h.widgets)?void 0:t.length)>0)return h.widgets.forEach(((e,t)=>{const s=u.widgets[t];e.name===s.name&&e.type===s.type&&(e.value=s.value)})),void m();if(f){let e=function(e,t){var s,i,a,n,o,l;if(!0===e||!1===e){if((null==(s=t.options)?void 0:s.on)&&(null==(i=t.options)?void 0:i.off))return{value:e,pass:!0}}else if("number"==typeof e){if((null==(a=t.options)?void 0:a.min)<=e&&e<=(null==(n=t.options)?void 0:n.max))return{value:e,pass:!0}}else{if(null==(l=null==(o=t.options)?void 0:o.values)?void 0:l.includes(e))return{value:e,pass:!0};if(t.inputEl&&"string"==typeof e)return{value:e,pass:!0}}return{value:t.value,pass:!1}},t=!1;const n=(null==f?void 0:f.length)<=(null==(s=h.widgets)?void 0:s.length);let o=n?0:f.length-1;const l=s=>{var i;const a=u.widgets[s];let l=h.widgets[s];if(l.name===a.name&&l.type===a.type){for(;(n?o=0)&&!t;){let{value:t,pass:s}=e(f[o],l);if(s&&null!==t){l.value=t;break}o+=n?1:-1}o++,n||(o=f.length-((null==(i=h.widgets)?void 0:i.length)-1-s))}};if(n&&(null==(i=h.widgets)?void 0:i.length)>0)for(let s=0;s0)for(let s=h.widgets.length-1;s>=0;s--)l(s)}m()};fe.registerExtension({name:"Comfy.EasyUse.ExtraMenu",async beforeRegisterNodeDef(e,t,s){Ms(e,(function(e,s){s.unshift({content:Ne("🔃 Reload Node"),callback:(e,t,s,i,a)=>{let n=LGraphCanvas.active_canvas;if(!n.selected_nodes||Object.keys(n.selected_nodes).length<=1)js(a);else for(let o in n.selected_nodes)js(n.selected_nodes[o])}}),"easy ckptNames"==t.name&&s.unshift({content:Ne("💎 View Checkpoint Info..."),callback:(e,t,s,i,a)=>{a.widgets[0].value}})}));for(const i in Ts)Ts[i].nodes.includes(t.name)&&Ds(`↪️ Swap ${Ts[i].category}`,i,Ts[i].nodes,e)}});const Us=LiteGraph.LGraphNode,Ws="➡️";fe.registerExtension({name:"easy setNode",registerCustomNodes(){class e extends Us{constructor(t){super("Set"),v(this,"defaultVisibility",!0),v(this,"serialize_widgets",!0),this.properties||(this.properties={previousName:""}),this.properties.showOutputText=e.defaultVisibility;const s=this;s.color=LGraphCanvas.node_colors.blue.color,this.addWidget("text","Constant","",((e,t,i,a,n)=>{s.validateName(s.graph),""!==this.widgets[0].value&&(this.title=Ws+this.widgets[0].value),this.properties.previousName=this.widgets[0].value,this.update()}),{}),this.addInput("*","*"),this.onConnectionsChange=function(e,t,i,a,n){if(1!=e||i||(this.inputs[t].type="*",this.inputs[t].name="*",this.title="Set"),a&&s.graph&&1==e&&i){const e=s.graph._nodes.find((e=>e.id==a.origin_id)).outputs[a.origin_slot];if(!e)return;const t=e.type,i=s.is_auto_link?this.widgets[0].value:e.name;"Set"===this.title&&(this.title=Ws+i,this.widgets[0].value=i),"*"===this.widgets[0].value&&(this.widgets[0].value=i),this.validateName(s.graph),this.inputs[0]&&(this.inputs[0].type=t,this.inputs[0].name=i),setTimeout((e=>{this.title=Ws+this.widgets[0].value,this.properties.previousName=this.widgets[0].value}),1)}this.update()},this.validateName=function(e){let t=s.widgets[0].value;if(""!=t){let i=0,a=[];do{a=e._nodes.filter((e=>e!=this&&("easy setNode"==e.type&&e.widgets[0].value===t))),a.length>0&&(t=s.widgets[0].value+i),i++}while(a.length>0);s.widgets[0].value=t,this.update()}},this.clone=function(){const t=e.prototype.clone.apply(this);return t.inputs[0].name="*",t.inputs[0].type="*",t.properties.previousName="",t.size=t.computeSize(),t},this.onAdded=function(e){this.validateName(e)},this.update=function(){if(s.graph){this.findGetters(s.graph).forEach((e=>{e.setType(this.inputs[0].type)})),this.widgets[0].value&&this.findGetters(s.graph,!0).forEach((e=>{e.setName(this.widgets[0].value)}));s.graph._nodes.filter((e=>"easy getNode"==e.type)).forEach((e=>{e.setComboValues&&e.setComboValues()}))}},this.findGetters=function(e,t){const s=t?this.properties.previousName:this.widgets[0].value;return e._nodes.filter((e=>"easy getNode"==e.type&&e.widgets[0].value===s&&""!=s))},this.isVirtualNode=!0}onRemoved(){this.graph._nodes.filter((e=>"easy getNode"==e.type)).forEach((e=>{e.setComboValues&&e.setComboValues([this])}))}}LiteGraph.registerNodeType("easy setNode",Object.assign(e,{title:"Set"})),e.category="EasyUse/Util"}}),fe.registerExtension({name:"easy getNode",registerCustomNodes(){class e extends Us{constructor(t){super("Get"),v(this,"defaultVisibility",!0),v(this,"serialize_widgets",!0),this.properties||(this.properties={}),this.properties.showOutputText=e.defaultVisibility;const s=this;s.color=LGraphCanvas.node_colors.blue.color,this.addWidget("combo","Constant","",(e=>{this.onRename()}),{values:()=>s.graph._nodes.filter((e=>"easy setNode"==e.type)).map((e=>e.widgets[0].value)).sort()}),this.addOutput("*","*"),this.onConnectionsChange=function(e,t,s,i,a){this.validateLinks(),2!=e||s?(this.onRename(),setTimeout((e=>{this.title="⬅️"+this.widgets[0].value}),1)):(this.outputs[t].type="*",this.outputs[t].name="*",this.title="Get")},this.setName=function(e){s.widgets[0].value=e,s.onRename(),s.serialize()},this.onRename=function(e=0){const t=this.findSetter(s.graph);if(t){const s=t.inputs[0].type,i=t.inputs[0].name;this.setType(s,i),this.outputs[e].type=s,this.outputs[e].name=i,this.title="⬅️"+t.widgets[0].value}else this.setType("*","*"),this.outputs[e].type="*",this.outputs[e].name="*"},this.clone=function(){const t=e.prototype.clone.apply(this);return t.size=t.computeSize(),t},this.validateLinks=function(){"*"!=this.outputs[0].type&&this.outputs[0].links&&this.outputs[0].links.forEach((e=>{const t=s.graph.links[e];t&&t.type!=this.outputs[0].type&&"*"!=t.type&&s.graph.removeLink(e)}))},this.setType=function(e,t){this.outputs[0].name=t,this.outputs[0].type=e,this.validateLinks()},this.findSetter=function(e){const t=this.widgets[0].value;return e._nodes.find((e=>"easy setNode"==e.type&&e.widgets[0].value===t&&""!=t))},this.isVirtualNode=!0}getInputLink(e){const t=this.findSetter(this.graph);if(t){const s=t.inputs[e];return this.graph.links[s.link]}throw new Error("No setter found for "+this.widgets[0].value+"("+this.type+")")}onAdded(e){}}LiteGraph.registerNodeType("easy getNode",Object.assign(e,{title:"Get"})),e.category="EasyUse/Util"}}),ge.addEventListener("easyuse-global-seed",(function(e){let t=app.graph._nodes_by_id;for(let s in t){let i=t[s];if("easy globalSeed"==i.type){if(i.widgets){const t=i.widgets.find((e=>"value"==e.name));i.widgets.find((e=>"last_seed"==e.name)).value=t.value,t.value=e.detail.value}}else if(i.widgets){const t=i.widgets.find((e=>"seed_num"==e.name||"seed"==e.name||"noise_seed"==e.name));t&&null!=e.detail.seed_map[i.id]&&(t.value=e.detail.seed_map[i.id])}}}));const Fs=ge.queuePrompt;ge.queuePrompt=async function(e,{output:t,workflow:s},i={}){s.seed_widgets={};for(let a in app.graph._nodes_by_id){let e=app.graph._nodes_by_id[a].widgets;if(e)for(let t in e)"seed_num"!=e[t].name&&"seed"!=e[t].name&&"noise_seed"!=e[t].name||"converted-widget"==e[t].type||(s.seed_widgets[a]=parseInt(t))}return await Fs.call(ge,e,{output:t,workflow:s},i)};const Xs=["easy imageSave","easy fullkSampler","easy kSampler","easy kSamplerTiled","easy kSamplerInpainting","easy kSamplerDownscaleUnet","easy kSamplerSDTurbo","easy detailerFix"];fe.registerExtension({name:"Comfy.EasyUse.SaveImageExtraOutput",async beforeRegisterNodeDef(e,t,s){if(Xs.includes(t.name)){const t=e.prototype.onNodeCreated;e.prototype.onNodeCreated=function(){const e=t?t.apply(this,arguments):void 0,i=this.widgets.find((e=>"filename_prefix"===e.name||"save_prefix"===e.name));return i.serializeValue=()=>be(s,i.value),e}}else{const t=e.prototype.onNodeCreated;e.prototype.onNodeCreated=function(){const e=t?t.apply(this,arguments):void 0;return this.properties&&"Node name for S&R"in this.properties||this.addProperty("Node name for S&R",this.constructor.type,"string"),e}}}});const Zs=["easy wildcards","easy positive","easy negative","easy stylesSelector","easy promptConcat","easy promptReplace"],Hs=["easy preSampling","easy preSamplingAdvanced","easy preSamplingNoiseIn","easy preSamplingCustom","easy preSamplingDynamicCFG","easy preSamplingSdTurbo","easy preSamplingLayerDiffusion"],qs=["easy kSampler","easy kSamplerTiled","easy kSamplerInpainting","easy kSamplerDownscaleUnet","easy kSamplerSDTurbo"],Ys=["easy controlnetLoader","easy controlnetLoaderADV"],Ks=["easy instantIDApply","easy instantIDApplyADV"],Js=["easy ipadapterApply","easy ipadapterApplyADV","easy ipadapterApplyFaceIDKolors","easy ipadapterStyleComposition"],Bs=["easy pipeIn","easy pipeOut","easy pipeEdit","easy pipeEditPrompt"],Qs=["easy XYPlot","easy XYPlotAdvanced"],$s=["easy setNode"],ei=["RescaleCFG","LoraLoaderModelOnly","LoraLoader","FreeU","FreeU_v2",...Js,...$s],ti={"easy seed":{from:{INT:[...Hs,"easy fullkSampler"]}},"easy positive":{from:{STRING:[...Zs]}},"easy negative":{from:{STRING:[...Zs]}},"easy wildcards":{from:{STRING:["easy showAnything",...Zs]}},"easy stylesSelector":{from:{STRING:["easy showAnything",...Zs]}},"easy promptConcat":{from:{STRING:["easy showAnything",...Zs]}},"easy promptReplace":{from:{STRING:["easy showAnything",...Zs]}},"easy fullLoader":{from:{PIPE_LINE:[...Hs,"easy fullkSampler",...Bs,...$s],MODEL:ei},to:{STRING:[...Zs]}},"easy a1111Loader":{from:{PIPE_LINE:[...Hs,...Ys,...Ks,...Bs,...$s],MODEL:ei},to:{STRING:[...Zs]}},"easy comfyLoader":{from:{PIPE_LINE:[...Hs,...Ys,...Ks,...Bs,...$s],MODEL:ei},to:{STRING:[...Zs]}},"easy hunyuanDiTLoader":{from:{PIPE_LINE:[...Hs,...Ys,...Ks,...Bs,...$s],MODEL:ei},to:{STRING:[...Zs]}},"easy kolorsLoader":{from:{PIPE_LINE:[...Hs,...Ys,...Ks,...Bs,...$s],MODEL:ei},to:{STRING:[...Zs]}},"easy pixArtLoader":{from:{PIPE_LINE:[...Hs,...Ys,...Ks,...Bs,...$s],MODEL:ei},to:{STRING:[...Zs]}},"easy fluxLoader":{from:{PIPE_LINE:[...Hs,...Ys,...Ks,...Bs,...$s],MODEL:ei},to:{STRING:[...Zs]}},"easy svdLoader":{from:{PIPE_LINE:["easy preSampling","easy preSamplingAdvanced","easy preSamplingDynamicCFG",...Bs,...$s],MODEL:ei},to:{STRING:[...Zs]}},"easy zero123Loader":{from:{PIPE_LINE:["easy preSampling","easy preSamplingAdvanced","easy preSamplingDynamicCFG",...Bs,...$s],MODEL:ei},to:{STRING:[...Zs]}},"easy sv3dLoader":{from:{PIPE_LINE:["easy preSampling","easy preSamplingAdvanced","easy preSamplingDynamicCFG",...Bs,...$s],MODEL:ei},to:{STRING:[...Zs]}},"easy preSampling":{from:{PIPE_LINE:[...qs,...Bs,...Ys,...Qs,...$s]}},"easy preSamplingAdvanced":{from:{PIPE_LINE:[...qs,...Bs,...Ys,...Qs,...$s]}},"easy preSamplingDynamicCFG":{from:{PIPE_LINE:[...qs,...Bs,...Ys,...Qs,...$s]}},"easy preSamplingCustom":{from:{PIPE_LINE:[...qs,...Bs,...Ys,...Qs,...$s]}},"easy preSamplingLayerDiffusion":{from:{PIPE_LINE:["easy kSamplerLayerDiffusion",...qs,...Bs,...Ys,...Qs,...$s]}},"easy preSamplingNoiseIn":{from:{PIPE_LINE:[...qs,...Bs,...Ys,...Qs,...$s]}},"easy fullkSampler":{from:{PIPE_LINE:[...Bs.reverse(),"easy preDetailerFix","easy preMaskDetailerFix",...Hs,...$s]}},"easy kSampler":{from:{PIPE_LINE:[...Bs.reverse(),"easy preDetailerFix","easy preMaskDetailerFix","easy hiresFix",...Hs,...$s]}},"easy controlnetLoader":{from:{PIPE_LINE:[...Hs,...Ys,...Ks,...Bs,...$s]}},"easy controlnetLoaderADV":{from:{PIPE_LINE:[...Hs,...Ys,...Ks,...Bs,...$s]}},"easy instantIDApply":{from:{PIPE_LINE:[...Hs,...Ys,...Ks,...Bs,...$s],MODEL:ei},to:{COMBO:["easy promptLine"]}},"easy instantIDApplyADV":{from:{PIPE_LINE:[...Hs,...Ys,...Ks,...Bs,...$s],MODEL:ei},to:{COMBO:["easy promptLine"]}},"easy ipadapterApply":{to:{COMBO:["easy promptLine"]}},"easy ipadapterApplyADV":{to:{STRING:["easy sliderControl",...Zs],COMBO:["easy promptLine"]}},"easy ipadapterStyleComposition":{to:{COMBO:["easy promptLine"]}},"easy preDetailerFix":{from:{PIPE_LINE:["easy detailerFix",...Bs,...$s]},to:{PIPE_LINE:["easy ultralyticsDetectorPipe","easy samLoaderPipe","easy kSampler","easy fullkSampler"]}},"easy preMaskDetailerFix":{from:{PIPE_LINE:["easy detailerFix",...Bs,...$s]}},"easy samLoaderPipe":{from:{PIPE_LINE:["easy preDetailerFix",...Bs,...$s]}},"easy ultralyticsDetectorPipe":{from:{PIPE_LINE:["easy preDetailerFix",...Bs,...$s]}},"easy cascadeLoader":{from:{PIPE_LINE:["easy fullCascadeKSampler","easy preSamplingCascade",...Ys,...Bs,...$s],MODEL:ei.filter((e=>!Js.includes(e)))}},"easy fullCascadeKSampler":{from:{PIPE_LINE:["easy preSampling","easy preSamplingAdvanced",...Bs,...$s]}},"easy preSamplingCascade":{from:{PIPE_LINE:["easy cascadeKSampler",...Bs,...$s]}},"easy cascadeKSampler":{from:{PIPE_LINE:["easy preSampling","easy preSamplingAdvanced",...Bs,...$s]}},"easy pipeEdit":{from:{PIPE_LINE:[...Hs,...Ys,...Ks,...$s]},to:{PIPE_LINE:[...Hs,...Ys,...Ks,...Bs,...$s]}},"easy pipeEditPrompt":{from:{PIPE_LINE:[...Hs,...Ys,...Ks,...$s]},to:{PIPE_LINE:[...Hs,...Ys,...Ks,...Bs,...$s]}}};class si extends Error{constructor(e="Attempted to access LGraph reference that was null or undefined.",t){super(e,{cause:t}),this.name="NullGraphError"}}fe.registerExtension({name:"Comfy.EasyUse.Suggestions",async setup(e){const t=LGraphCanvas.prototype.createDefaultNodeForSlot;LGraphCanvas.prototype.createDefaultNodeForSlot=function(e){var s;const i=Object.assign({nodeFrom:null,slotFrom:null,nodeTo:null,slotTo:null,position:[],nodeType:null,posAdd:[0,0],posSizeFix:[0,0]},e||{}),a=this,n=i.nodeFrom&&null!==i.slotFrom,o=!n&&i.nodeTo&&null!==i.slotTo,l=n?i.nodeFrom:i.nodeTo;if(!l||!Object.keys(ti).includes(l.type))return t.call(this,e);if(!n&&!o)return!1;if(!i.nodeType)return!1;const r=n?i.nodeFrom:i.nodeTo;if(!r)throw new TypeError("nodeX was null when creating default node for slot.");const d=r.type;let u=n?i.slotFrom:i.slotTo,c=!1;switch(typeof u){case"string":c=n?r.findOutputSlot(u,!1):r.findInputSlot(u,!1),u=n?r.outputs[u]:r.inputs[u];break;case"object":if(null===u)return!1;c=n?r.findOutputSlot(u.name):r.findInputSlot(u.name);break;case"number":c=u,u=n?r.outputs[u]:r.inputs[u];break;default:return!1}var A=u.type==LiteGraph.EVENT?"_event_":u.type,p=n?LiteGraph.slot_types_default_out:LiteGraph.slot_types_default_in;if(null==p?void 0:p[A]){let e=!1;const t=n?"from":"to";if(ti[d]&&ti[d][t]&&(null==(s=ti[d][t][A])?void 0:s.length)>0){for(var h in ti[d][t][A])if(i.nodeType==ti[d][t][A][h]||"AUTO"==i.nodeType){e=ti[d][t][A][h];break}}else if("object"==typeof p[A]||"array"==typeof p[A]){for(var h in p[A])if(i.nodeType==p[A][h]||"AUTO"==i.nodeType){e=p[A][h];break}}else i.nodeType!=p[A]&&"AUTO"!=i.nodeType||(e=p[A]);if(e){var m=!1;"object"==typeof e&&e.node&&(m=e,e=e.node);var f=LiteGraph.createNode(e);if(f){if(m){if(m.properties)for(var g in m.properties)f.addProperty(g,m.properties[g]);if(m.inputs)for(var g in f.inputs=[],m.inputs)f.addOutput(m.inputs[g][0],m.inputs[g][1]);if(m.outputs)for(var g in f.outputs=[],m.outputs)f.addOutput(m.outputs[g][0],m.outputs[g][1]);m.title&&(f.title=m.title),m.json&&f.configure(m.json)}a.graph.add(f),f.pos=[i.position[0]+i.posAdd[0]+(i.posSizeFix[0]?i.posSizeFix[0]*f.size[0]:0),i.position[1]+i.posAdd[1]+(i.posSizeFix[1]?i.posSizeFix[1]*f.size[1]:0)];const e={node:f,opts:i};if(!this.canvas.dispatchEvent(new CustomEvent("connect-new-default-node",{detail:e,cancelable:!0})))return!0;if(n){if(!i.nodeFrom)throw new TypeError("createDefaultNodeForSlot - nodeFrom was null");i.nodeFrom.connectByType(c,f,A)}else{if(!i.nodeTo)throw new TypeError("createDefaultNodeForSlot - nodeTo was null");i.nodeTo.connectByTypeOutput(c,f,A)}return!0}}}return!1};let s=LGraphCanvas.prototype.showConnectionMenu;LGraphCanvas.prototype.showConnectionMenu=function(e){var t;const i=Object.assign({nodeFrom:null,slotFrom:null,nodeTo:null,slotTo:null,e:void 0,allow_searchbox:this.allow_searchbox,showSearchBox:this.showSearchBox},e||{}),a=this,{graph:n}=this,{afterRerouteId:o}=i,l=i.nodeFrom&&i.slotFrom,r=!l&&i.nodeTo&&i.slotTo,d=l?i.nodeFrom:i.nodeTo;if(!d||!Object.keys(ti).includes(d.type))return s.call(this,e);if(!l&&!r)return!1;const u=l?i.nodeFrom:i.nodeTo;if(!u)throw new TypeError("nodeX was null when creating default node for slot.");let c=l?i.slotFrom:i.slotTo,A=!1;switch(typeof c){case"string":A=l?u.findOutputSlot(c,!1):u.findInputSlot(c,!1),c=l?u.outputs[c]:u.inputs[c];break;case"object":if(null===c)return;A=l?u.findOutputSlot(c.name):u.findInputSlot(c.name);break;case"number":A=c,c=l?u.outputs[c]:u.inputs[c];break;default:return!1}const p=["Add Node","Add Reroute",null];i.allow_searchbox&&p.push("Search",null);var h=c.type==LiteGraph.EVENT?"_event_":c.type,m=l?LiteGraph.slot_types_default_out:LiteGraph.slot_types_default_in,f=u.type;if(m&&m[h]){const e=l?"from":"to";if(ti[f]&&ti[f][e]&&(null==(t=ti[f][e][h])?void 0:t.length)>0)for(var g in ti[f][e][h])p.push(ti[f][e][h][g]);else if("object"==typeof m[h]||"array"==typeof m[h])for(var g in m[h])p.push(m[h][g]);else p.push(m[h])}var v=new LiteGraph.ContextMenu(p,{event:i.e,extra:c,title:(c&&""!=c.name?c.name+(h?" | ":""):"")+(c&&h?h:""),callback:function(e,t,s){var r,d;switch(e){case"Add Node":LGraphCanvas.onMenuAdd(null,null,s,v,(function(e){var t,s;e&&(l?null==(t=i.nodeFrom)||t.connectByType(A,e,h,{afterRerouteId:o}):null==(s=i.nodeTo)||s.connectByTypeOutput(A,e,h,{afterRerouteId:o}))}));break;case"Add Reroute":const u=l?i.nodeFrom:i.nodeTo,p=t.extra;if(!n)throw new si;if(!u)throw new TypeError("Cannot add reroute: node was null");if(!p)throw new TypeError("Cannot add reroute: slot was null");if(!i.e)throw new TypeError("Cannot add reroute: CanvasPointerEvent was null");if(!u.connectFloatingReroute([i.e.canvasX,i.e.canvasY],p,o))throw new Error("Failed to create reroute");a.dirty_canvas=!0,a.dirty_bgcanvas=!0;break;case"Search":l?i.showSearchBox(s,{node_from:i.nodeFrom,slot_from:c,type_filter_in:h}):i.showSearchBox(s,{node_to:i.nodeTo,slot_from:c,type_filter_out:h});break;default:const m={position:[(null==(r=i.e)?void 0:r.canvasX)??0,(null==(d=i.e)?void 0:d.canvasY)??0],nodeType:e,afterRerouteId:o};a.createDefaultNodeForSlot(Object.assign(i,m))}}});return v}}}),fe.registerExtension({name:"Comfy.EasyUse.TimeTaken",setup(){let e=new Map,t=0;ge.addEventListener("execution_start",(e=>{graph&&graph._nodes.forEach((e=>{e.executionDuration&&delete e.executionDuration}))})),ge.addEventListener("executing",(s=>{if(!Le("EasyUse.Nodes.Runtime",null))return;const i=(null==s?void 0:s.node)||(null==s?void 0:s.detail)||null,a=e.get(t);if(e.delete(t),t&&a){const e=Date.now()-a,s=Lt(t);s&&(s.executionDuration||(s.executionDuration=0),s.executionDuration=s.executionDuration+e/1e3)}t=i,e.set(i,Date.now())}))},beforeRegisterNodeDef(e,t){const s=e.prototype.onDrawForeground;e.prototype.onDrawForeground=function(...e){const[t]=e;return function(e,t){if(!t)return;t=parseFloat(t).toFixed(3)+Ne("s"),e.save(),e.fillStyle=LiteGraph.NODE_DEFAULT_BGCOLOR,function(e,t,s,i,a,n){e.beginPath(),e.moveTo(t+n,s),e.lineTo(t+i-n,s),e.arcTo(t+i,s,t+i,s+n,n),e.lineTo(t+i,s+a-n),e.arcTo(t+i,s+a,t+i-n,s+a,n),e.lineTo(t+n,s+a),e.arcTo(t,s+a,t,s+a-n,n),e.lineTo(t,s+n),e.arcTo(t,s,t+n,s,n),e.closePath()}(e,0,-LiteGraph.NODE_TITLE_HEIGHT-20,e.measureText(t).width+10,LiteGraph.NODE_TITLE_HEIGHT-10,4),e.fill(),function(e,t,s,i,a="#000",n=12,o="Inter"){e.font=`${n}px ${o}`,e.fillStyle=a,e.fillText(t,s,i)}(e,t,8,-LiteGraph.NODE_TITLE_HEIGHT-6,LiteGraph.NODE_TITLE_COLOR),e.restore()}(t,this.executionDuration),null==s?void 0:s.apply(this,e)}}});let ii=null;fe.registerExtension({name:"Comfy.EasyUse.HotKeys",setup(){if(void 0!==L){L("up,down,left,right",(function(e,t){var s,i,a,n,o,l,r,d,u,c,A,p,h,m,f;e.preventDefault();if(!Le("EasyUse.Hotkeys.JumpNearestNodes",null))return;const g=xt();if(0===g.length)return;const v=g[0];switch(t.key){case"up":case"left":let e=null;if(Tt(v)){const e=null==(s=v.widgets_values)?void 0:s[0],t=null==(i=v.graph)?void 0:i._nodes,a=null==t?void 0:t.find((t=>{var s;if(Ot(t)){if((null==(s=t.widgets_values)?void 0:s[0])===e)return t}return null}));a&&Et(a)}else if((null==(a=v.inputs)?void 0:a.length)>0){for(let t=0;t{var s;if(Tt(t)){if((null==(s=t.widgets_values)?void 0:s[0])===e)return t}return null}));s&&Et(s)}else if((null==(c=v.outputs)?void 0:c.length)>0){for(let e=0;e0&&v.outputs[e].links[0]){t=v.outputs[e].links[0];break}if(t){const e=null==(p=v.graph)?void 0:p.links;if(e[t]){const s=null==(h=e[t])?void 0:h.target_id,i=null==(f=null==(m=v.graph)?void 0:m._nodes_by_id)?void 0:f[s];i&&Et(i)}}}}})),L("shift+up,shift+down,shift+left,shift+right,shift+alt+⌘+left,shift+alt+⌘+right,shift+alt+ctrl+left,shift+alt+ctrl+right",(function(e,t){e.preventDefault();if(!Le("EasyUse.Hotkeys.AlignSelectedNodes",null))return;const s=xt();if(s.length<=1)return;const i=s;switch(t.key){case"shift+up":LGraphCanvas.alignNodes(i,"top",i[0]);break;case"shift+down":LGraphCanvas.alignNodes(i,"bottom",i[0]);break;case"shift+left":LGraphCanvas.alignNodes(i,"left",i[0]);break;case"shift+right":LGraphCanvas.alignNodes(i,"right",i[0]);break;case"shift+alt+ctrl+left":case"shift+alt+⌘+left":Ft(i,"horizontal");break;case"shift+alt+ctrl+right":case"shift+alt+⌘+right":Ft(i,"vertical")}ii||(ii=Ke()),ii&&ii.update()})),L("shift+⌘+left,shift+⌘+right,shift+ctrl+left,shift+ctrl+right",(function(e,t){e.preventDefault();if(!Le("EasyUse.Hotkeys.NormalizeSelectedNodes",null))return;const s=xt();if(s.length<=1)return;const i=s;switch(t.key){case"shift+ctrl+left":case"shift+⌘+left":Wt(i,"width");break;case"shift+ctrl+right":case"shift+⌘+right":Wt(i,"height")}ii||(ii=Ke()),ii&&ii.update()})),L("shift+g",(function(e,t){e.preventDefault();Le("EasyUse.Hotkeys.AddGroup",null)&&(ni(),ii||(ii=Ke()),ii&&ii.update())})),L("shift+r",(function(e,t){e.preventDefault();Le("EasyUse.Hotkeys.cleanVRAMused",null)&&vs()})),L("shift+m",(function(e,t){var s,i,a;if(!Le("EasyUse.Hotkeys.toggleNodesMap",null))return;let n=(null==(s=fe.extensionManager)?void 0:s.sidebarTab)||fe.extensionManager,o=(null==(i=fe.extensionManager.sidebarTab)?void 0:i.activeSidebarTabId)||(null==(a=fe.extensionManager)?void 0:a.activeSidebarTab);n.activeSidebarTabId=o==Pe?null:Pe}));const e=[];Array.from(Array(10).keys()).forEach((t=>e.push(`alt+${t}`))),L(e.join(","),(async function(e,t){e.preventDefault();if(!Le("EasyUse.Hotkeys.NodesTemplate",null))return;const s=t.key;let i=parseInt(s.split("+")[1]);const a=await ge.getUserData("comfy.templates.json");let n=null;if(200==a.status)try{n=await a.json()}catch(l){Ye.error(Ne("Get Node Templates File Failed"))}else localStorage["Comfy.NodeTemplates"]?n=JSON.parse(localStorage["Comfy.NodeTemplates"]):Ye.warn(Ne("No Node Templates Found"));if(!n)return void Ye.warn(Ne("No Node Templates Found"));i=0===i?9:i-1;const o=n[i];if(o)try{const e=(null==o?void 0:o.name)||"Group",t=(null==o?void 0:o.data)?JSON.parse(o.data):[];ai((async()=>{await _e.registerFromWorkflow(t.groupNodes,{}),localStorage.litegrapheditor_clipboard=o.data,fe.canvas.pasteFromClipboard(),t.groupNodes||ni(e)}))}catch(l){Ye.error(l)}else Ye.warn(Ne("Node template with {key} not set").replace("{key}",s))}));const t=async function(e){if(("b"===e.key||"m"==e.key)&&(e.metaKey||e.ctrlKey)){if(0===xt().length)return;ii||(ii=Ke()),ii&&ii.update()}};window.addEventListener("keydown",t,!0)}}});const ai=async e=>{const t=localStorage.litegrapheditor_clipboard;await e(),localStorage.litegrapheditor_clipboard=t},ni=e=>{const t=xt();if(0===t.length)return;const s=t;let i=new LiteGraph.LGraphGroup;i.title=e||"Group",((e,t=[],s=20)=>{var i,a,n,o,l,r,d,u,c,A;for(var p of(a=n=o=l=-1,r=d=u=c=-1,[e._nodes,t]))for(var h in p)r=(A=p[h]).pos[0],d=A.pos[1],u=A.pos[0]+A.size[0],c=A.pos[1]+A.size[1],"Reroute"!=A.type&&(d-=LiteGraph.NODE_TITLE_HEIGHT),(null==(i=A.flags)?void 0:i.collapsed)&&(c=d+LiteGraph.NODE_TITLE_HEIGHT,(null==A?void 0:A._collapsed_width)&&(u=r+Math.round(A._collapsed_width))),(-1==a||ro)&&(o=u),(-1==l||c>l)&&(l=c);n-=Math.round(1.4*e.font_size),e.pos=[a-s,n-s],e.size=[o-a+2*s,l-n+2*s]})(i,s),fe.canvas.graph.add(i)},oi="easy imageChooser",li=e=>(null==e?void 0:e.comfyClass)===oi;fe.registerExtension({name:"Comfy.EasyUse.imageChooser",setup(){const e=ge.interrupt;ge.interrupt=function(){var t;(null==(t=fe.graph)?void 0:t._nodes_by_id)&&(()=>{var e;Object.values(null==(e=fe.graph)?void 0:e._nodes_by_id).forEach((e=>{li(e)&&e.cancelSelection("interrupt")}))})(),e.apply(this,arguments)},ge.addEventListener("easyuse-image-choose",(e=>{var t;const s=e.detail,i=fe.graph._nodes_by_id[s.id];if(!i||!li(i))return;i.setExecutingState(!1),i.selected_images.clear(),i.anti_selected.clear(),i.isWaitingSelection=["Always Pause","Keep Last Selection"].includes(i.currentMode),i.isCanceling=!1,i.overIndex=null;const a=(null==(t=s.urls)?void 0:t.map(((e,t)=>({index:t,filename:e.filename,subfolder:e.subfolder,type:e.type}))))||[];i.imageData=a,i.imgs=[],a.forEach((e=>{const t=new Image;t.onload=()=>{fe.graph.setDirtyCanvas(!0)},t.src=`/view?filename=${encodeURIComponent(e.filename)}${(e=>`&type=${e.type}&subfolder=${e.subfolder}&rand=${e.random}`)(e)}`,i.imgs.push(t)})),i.update(),"Always Pause"===i.currentMode&&i.setExecutingState(!0)})),ge.addEventListener("easyuse-image-keep-selection",(e=>{const t=e.detail,s=fe.graph._nodes_by_id[t.id];s&&li(s)&&(s.isWaitingSelection=!1,s.isCancelling=!1,t.selected&&Array.isArray(t.selected)&&(s.selected_images.clear(),t.selected.forEach((e=>{s.selected_images.add(e)}))),s.update())}))},beforeRegisterNodeDef(e,t){if(t.name==oi){e.prototype.confirmSelection=function(){ge.fetchApi("/easyuse/image_chooser_message",{method:"POST",body:JSON.stringify({node_id:this.id.toString(),action:"select",selected:Array.from(this.selected_images)})}).catch((e=>{})).finally((e=>{this.isWaitingSelection=!1,this.update(),this.setExecutingState(!1)}))},e.prototype.cancelSelection=function(e){ge.fetchApi("/easyuse/image_chooser_message",{method:"POST",body:JSON.stringify({node_id:this.id.toString(),action:"cancel",selected:[]})}).catch((e=>{})).finally((e=>{this.isCanceling=!1,this.isWaitingSelection=!1,this.update(),this.setExecutingState(!1)}))},e.prototype.updateWidgets=function(){var e;if(!this.confirm_button_widget||!this.cancel_button_widget)return;const t=this.selected_images.size,s=(null==(e=this.imgs)?void 0:e.length)||0;if(this.isCanceling)this.confirm_button_widget.name=Ne("Canceling"),this.cancel_button_widget.name="",this.confirm_button_widget.disabled=!0,this.cancel_button_widget.disabled=!0;else if(this.isWaitingSelection)t>0?(this.confirm_button_widget.name=t>1?`${Ne("Progress selected")} (${t}/${s})`:`${Ne("Progress selected image")}`,this.confirm_button_widget.disabled=!1):(this.confirm_button_widget.name=Ne("Please select images to continue"),this.confirm_button_widget.disabled=!0),this.cancel_button_widget.name=Ne("Cancel current run"),this.cancel_button_widget.disabled=!1;else{const e={"Always Pause":Ne("Waiting for selection..."),"Keep Last Selection":Ne("Using last selection")};this.confirm_button_widget.name=(t>0&&"Always Pause"==this.currentMode?Ne("Please run first"):null==e?void 0:e[this.currentMode])||Ne("Unknown Mode"),this.cancel_button_widget.name="",this.confirm_button_widget.disabled=!0,this.cancel_button_widget.disabled=!0}this.setDirtyCanvas(!0,!0)},e.prototype.serialize=function(){const e=LiteGraph.LGraphNode.prototype.serialize.call(this);return e.isWaitingSelection=this.isWaitingSelection,e.currentMode=this.currentMode,this.imageData&&this.imageData.length>0&&(e.imageData=this.imageData),this.selected_images&&this.selected_images.size>0&&(e.selected_images=Array.from(this.selected_images)),e.isExecuting=this.isExecuting||!1,e},e.prototype.configure=function(e){LiteGraph.LGraphNode.prototype.configure.call(this,e),this.isWaitingSelection=e.isWaitingSelection||!1,this.currentMode=e.currentMode||"Always Pause",this.imgs=[],this.updateWidgets()},e.prototype.setExecutingState=function(e){this.isExecuting=e,fe.graph&&fe.graph.setDirtyCanvas(!0,!1)};const t=e.prototype.update;e.prototype.update=function(){null==t||t.call(this,arguments),this.updateWidgets()};const s=e.prototype.onNodeCreated;e.prototype.onNodeCreated=function(){null==s||s.call(this,arguments),this.selected_images=new Set,this.anti_selected=new Set,this.currentMode="Always Pause",this.isWaitingSelection=!1,this.isCanceling=!1,this.imageData=[],this.imgs,this.confirm_button_widget=this.addWidget("button","","",(()=>{this.confirmSelection()})),this.cancel_button_widget=this.addWidget("button","","",(()=>{this.cancelSelection()})),this.confirm_button_widget.serialize=!1,this.cancel_button_widget.serialize=!1,setTimeout((e=>{const t=wt(this,"mode");t.callback=e=>{requestAnimationFrame((t=>{this.currentMode=e,this.updateWidgets(),this.setDirtyCanvas(!0,!0)}))},this.currentMode=t.value||"Always Pause",this.updateWidgets()}),1),Object.defineProperty(this.confirm_button_widget,"clicked",{get:function(){return this._clicked},set:function(e){this._clicked=e&&""!=this.name}}),Object.defineProperty(this.cancel_button_widget,"clicked",{get:function(){return this._clicked},set:function(e){this._clicked=e&&""!=this.name}}),Object.defineProperty(this,"imageIndex",{get:function(){return null},set:function(e){this.overIndex=e}}),Object.defineProperty(this,"imagey",{get:function(){var e,t;return(null==(t=null==(e=this.widgets)?void 0:e[this.widgets.length-1])?void 0:t.last_y)+LiteGraph.NODE_WIDGET_HEIGHT},set:function(e){this.imagey=e}});const e=this.onMouseDown;this.onMouseDown=function(t,s,i){if(t.isPrimary){const e=ri(this,s);e>=0&&this.toggleImageSelection(e)}return e&&e.apply(this,arguments)},this.update()},e.prototype.onDrawBackground,e.prototype.onDrawBackground=function(e){!function(e,t,s=0){var i,a;if(!e.imgs||!e.imgs.length)return;const n=fe.canvas,o=n.graph_mouse,l=15,r=e.size[0];let d=e.size[1]-s-l;d<200&&(d=200,requestAnimationFrame((t=>{e.size[1]=s+l+200})));if(null==e.imageIndex){let a,l,u,c;if(ui(e.imgs))({cellWidth:a,cellHeight:l,cols:c,shiftX:u}=ci(e.imgs,r,d));else{const t=e.imgs.reduce(((e,t)=>Math.max(e,t.naturalWidth,t.naturalHeight)),0),s=[];s.length=null==(i=e.imgs)?void 0:i.length,s[0]={naturalWidth:t,naturalHeight:t},({cellWidth:a,cellHeight:l,cols:c,shiftX:u}=ci(s,r,d))}let A=!1,p=e.imgs.length;e.imageRects=[];for(let i=0;i{di(e,s,t)}));t.strokeStyle="#F88",null==(a=null==e?void 0:e.anti_selected)||a.forEach((s=>{di(e,s,t)}))}(this,e,this.imagey||0)},e.prototype.toggleImageSelection=function(e){this.selected_images.has(e)?this.selected_images.delete(e):this.selected_images.add(e),this.update()}}}});const ri=(e,t)=>{var s,i;if((null==(s=e.imgs)?void 0:s.length)>1)for(var a=0;a0&&s0&&ie.imagey)return 0;return-1};function di(e,t,s){var i;if(e.imageRects)i=e.imageRects[t];else{const t=e.imagey;i=[1,t+1,e.size[0]-2,e.size[1]-t-2]}s.strokeRect(i[0]+1,i[1]+1,i[2]-2,i[3]-2)}const ui=e=>{if(!e.length||1===e.length)return!0;const t=e[0].naturalWidth/e[0].naturalHeight;for(let s=1;si&&(i=y,l=g,r=v,d=A,u=e,c=A*((p-g)/2))}return{cellWidth:l,cellHeight:r,cols:d,rows:u,shiftX:c}}Number.prototype.div=function(e){return function(e,t){let s,i,a=0,n=0,o="string"==typeof e?e:e.toString(),l="string"==typeof t?t:t.toString();try{a=o.toString().split(".")[1].length}catch(r){}try{n=l.toString().split(".")[1].length}catch(r){}return s=Number(o.toString().replace(".","")),i=Number(l.toString().replace(".","")),s/i*Math.pow(10,n-a)}(this,e)};let Ai=[],pi=0;const hi={sd3:6.5,"sd3-turbo":4};class mi extends ye{constructor(){super(),this.lists=[],this.dialog_div=null,this.user_div=null}addItem(e,t){return ve("div.easyuse-account-dialog-item",[ve("input",{type:"text",placeholder:"Enter name",oninput:e=>{const t=Array.prototype.indexOf.call(this.dialog_div.querySelectorAll(".easyuse-account-dialog-item"),e.target.parentNode);Ai[t].name=e.target.value},value:Ai[e].name}),ve("input.key",{type:"text",oninput:e=>{const t=Array.prototype.indexOf.call(this.dialog_div.querySelectorAll(".easyuse-account-dialog-item"),e.target.parentNode);Ai[t].key=e.target.value},placeholder:"Enter APIKEY",value:Ai[e].key}),ve("button.choose",{textContent:Ne("Choose"),onclick:async e=>{var s,i,a;const n=Array.prototype.indexOf.call(this.dialog_div.querySelectorAll(".easyuse-account-dialog-item"),e.target.parentNode);let o=Ai[n].name,l=Ai[n].key;if(!o)return void Ye.error(Ne("Please enter the account name"));if(!l)return void Ye.error(Ne("Please enter the APIKEY"));let r=!0;for(let t=0;t{(new mi).show(t)}},[ve("div.user",[ve("div.avatar",n?[ve("img",{src:n})]:"😀"),ve("div.info",[ve("h5.name",o),ve("h6.remark","Credits: "+l)])]),ve("div.edit",{textContent:Ne("Edit")})])),Ye.success(Ne("Save Succeed"))}else Ye.success(Ne("Save Succeed"));this.close()}else Ye.error(Ne("Save Failed"))}}),ve("button.delete",{textContent:Ne("Delete"),onclick:e=>{const t=Array.prototype.indexOf.call(this.dialog_div.querySelectorAll(".easyuse-account-dialog-item"),e.target.parentNode);Ai.length<=1?Ye.error(Ne("At least one account is required")):(Ai.splice(t,1),this.dialog_div.removeChild(e.target.parentNode))}})])}show(e){Ai.forEach(((t,s)=>{this.lists.push(this.addItem(s,e))})),this.dialog_div=ve("div.easyuse-account-dialog",this.lists),super.show(ve("div.easyuse-account-dialog-main",[ve("div",[ve("a",{href:"https://platform.stability.ai/account/keys",target:"_blank",textContent:Ne("Getting Your APIKEY")})]),this.dialog_div]))}createButtons(){const e=super.createButtons();return e.unshift(ve("button",{type:"button",textContent:Ne("Save Account Info"),onclick:e=>{let t=!0;for(let s=0;s{200==e.status?Ye.success(Ne("Save Succeed")):Ye.error(Ne("Save Failed"))}))}else Ye.error(Ne("APIKEY is not Empty"))}})),e.unshift(ve("button",{type:"button",textContent:Ne("Add Account"),onclick:e=>{const t="Account "+Ai.length.toString();Ai.push({name:t,key:""});const s=this.addItem(Ai.length-1);this.lists.push(s),this.dialog_div.appendChild(s)}})),e}}fe.registerExtension({name:"Comfy.EasyUse.API.SD3",async beforeRegisterNodeDef(e,t,s){if("easy stableDiffusion3API"==t.name){const t=e.prototype.onNodeCreated;e.prototype.onNodeCreated=async function(){var e,s,i;t&&(null==t||t.apply(this,arguments));const a=this.widgets.find((e=>["seed_num","seed"].includes(e.name))),n=this.widgets.find((e=>["control_before_generate","control_after_generate"].includes(e.name)));let o=this.widgets.find((e=>"model"==e.name));o.callback=e=>{l.value="-"+hi[e]};const l=this.addWidget("text","cost_credit","0",(e=>{}),{serialize:!1});l.disabled=!0,n&&setTimeout((e=>{"control before generate"==n.name&&0===a.value&&(a.value=Math.floor(4294967294*Math.random())),l.value="-"+hi[o.value]}),100);let r=ve("div.easyuse-account-user",[Ne("Loading UserInfo...")]);this.addDOMWidget("account","btn",ve("div.easyuse-account",r)),ge.addEventListener("stable-diffusion-api-generate-succeed",(async({detail:e})=>{var t;let s=r.querySelectorAll(".remark");if(s&&s[0]){const t=(null==e?void 0:e.model)?hi[e.model]:0;if(t){let e=function(e,t){let s,i,a,n,o,l;o="string"==typeof e?e:e.toString(),l="string"==typeof t?t:t.toString();try{s=o.split(".")[1].length}catch(r){s=0}try{i=l.split(".")[1].length}catch(r){i=0}return a=Math.pow(10,Math.max(s,i)),n=s>=i?s:i,((e*a-t*a)/a).toFixed(n)}(parseFloat(s[0].innerText.replace(/Credits: /g,"")),t);e>0&&(s[0].innerText="Credits: "+e.toString())}}await Xe(1e4);const i=await ge.fetchApi("/easyuse/stability/balance");if(200==i.status){const e=await i.json();if(null==e?void 0:e.balance){const i=(null==(t=e.balance)?void 0:t.credits)||0;s&&s[0]&&(s[0].innerText="Credits: "+i)}}}));const d=await ge.fetchApi("/easyuse/stability/api_keys");if(200==d.status){let t=await d.json();if(Ai=t.keys,pi=t.current,Ai.length>0&&void 0!==pi){const t=Ai[pi].key,a=Ai[pi].name;if(t){const t=await ge.fetchApi("/easyuse/stability/user_info");if(200==t.status){const a=await t.json();if((null==a?void 0:a.account)&&(null==a?void 0:a.balance)){const t=(null==(e=a.account)?void 0:e.profile_picture)||null,n=(null==(s=a.account)?void 0:s.email)||null,o=(null==(i=a.balance)?void 0:i.credits)||0;r.replaceChildren(ve("div.easyuse-account-user-info",{onclick:e=>{(new mi).show(r)}},[ve("div.user",[ve("div.avatar",t?[ve("img",{src:t})]:"😀"),ve("div.info",[ve("h5.name",n),ve("h6.remark","Credits: "+o)])]),ve("div.edit",{textContent:Ne("Edit")})]))}}}else r.replaceChildren(ve("div.easyuse-account-user-info",{onclick:e=>{(new mi).show(r)}},[ve("div.user",[ve("div.avatar","😀"),ve("div.info",[ve("h5.name",a),ve("h6.remark",Ne("Click to set the APIKEY first"))])]),ve("div.edit",{textContent:Ne("Edit")})]))}}}}}});let fi=null;function gi(){fi&&(fi.removeEventListeners(),fi.dropdown.remove(),fi=null)}function vi(e,t,s,i=!1){gi(),new yi(e,t,s,i)}class yi{constructor(e,t,s,i=!1){this.dropdown=document.createElement("ul"),this.dropdown.setAttribute("role","listbox"),this.dropdown.classList.add("easy-dropdown"),this.selectedIndex=-1,this.inputEl=e,this.suggestions=t,this.onSelect=s,this.isDict=i,this.focusedDropdown=this.dropdown,this.buildDropdown(),this.onKeyDownBound=this.onKeyDown.bind(this),this.onWheelBound=this.onWheel.bind(this),this.onClickBound=this.onClick.bind(this),this.addEventListeners()}buildDropdown(){this.isDict?this.buildNestedDropdown(this.suggestions,this.dropdown):this.suggestions.forEach(((e,t)=>{this.addListItem(e,t,this.dropdown)}));const e=this.inputEl.getBoundingClientRect();this.dropdown.style.top=e.top+e.height-10+"px",this.dropdown.style.left=e.left+"px",document.body.appendChild(this.dropdown),fi=this}buildNestedDropdown(e,t){let s=0;Object.keys(e).forEach((i=>{const a=e[i];if("object"==typeof a&&null!==a){const e=document.createElement("ul");e.setAttribute("role","listbox"),e.classList.add("easy-nested-dropdown");const n=document.createElement("li");n.classList.add("folder"),n.textContent=i,n.appendChild(e),n.addEventListener("mouseover",this.onMouseOver.bind(this,s,t)),t.appendChild(n),this.buildNestedDropdown(a,e),s+=1}else{const e=document.createElement("li");e.classList.add("item"),e.setAttribute("role","option"),e.textContent=i,e.addEventListener("mouseover",this.onMouseOver.bind(this,s,t)),e.addEventListener("mousedown",this.onMouseDown.bind(this,i)),t.appendChild(e),s+=1}}))}addListItem(e,t,s){const i=document.createElement("li");i.setAttribute("role","option"),i.textContent=e,i.addEventListener("mouseover",this.onMouseOver.bind(this,t)),i.addEventListener("mousedown",this.onMouseDown.bind(this,e)),s.appendChild(i)}addEventListeners(){document.addEventListener("keydown",this.onKeyDownBound),this.dropdown.addEventListener("wheel",this.onWheelBound),document.addEventListener("click",this.onClickBound)}removeEventListeners(){document.removeEventListener("keydown",this.onKeyDownBound),this.dropdown.removeEventListener("wheel",this.onWheelBound),document.removeEventListener("click",this.onClickBound)}onMouseOver(e,t){t&&(this.focusedDropdown=t),this.selectedIndex=e,this.updateSelection()}onMouseOut(){this.selectedIndex=-1,this.updateSelection()}onMouseDown(e,t){t.preventDefault(),this.onSelect(e),this.dropdown.remove(),this.removeEventListeners()}onKeyDown(e){const t=Array.from(this.focusedDropdown.children),s=t[this.selectedIndex];if(fi)if(38===e.keyCode)e.preventDefault(),this.selectedIndex=Math.max(0,this.selectedIndex-1),this.updateSelection();else if(40===e.keyCode)e.preventDefault(),this.selectedIndex=Math.min(t.length-1,this.selectedIndex+1),this.updateSelection();else if(39===e.keyCode){if(e.preventDefault(),s&&s.classList.contains("folder")){const e=s.querySelector(".easy-nested-dropdown");e&&(this.focusedDropdown=e,this.selectedIndex=0,this.updateSelection())}}else if(37===e.keyCode&&this.focusedDropdown!==this.dropdown){const e=this.focusedDropdown.closest(".easy-dropdown, .easy-nested-dropdown").parentNode.closest(".easy-dropdown, .easy-nested-dropdown");e&&(this.focusedDropdown=e,this.selectedIndex=Array.from(e.children).indexOf(this.focusedDropdown.parentNode),this.updateSelection())}else if((13===e.keyCode||9===e.keyCode)&&this.selectedIndex>=0){e.preventDefault(),s.classList.contains("item")&&(this.onSelect(t[this.selectedIndex].textContent),this.dropdown.remove(),this.removeEventListeners());const i=s.querySelector(".easy-nested-dropdown");i&&(this.focusedDropdown=i,this.selectedIndex=0,this.updateSelection())}else 27===e.keyCode&&(this.dropdown.remove(),this.removeEventListeners())}onWheel(e){const t=parseInt(this.dropdown.style.top);localStorage.getItem("Comfy.Settings.Comfy.InvertMenuScrolling")?this.dropdown.style.top=t+(e.deltaY<0?10:-10)+"px":this.dropdown.style.top=t+(e.deltaY<0?-10:10)+"px"}onClick(e){this.dropdown.contains(e.target)||e.target===this.inputEl||(this.dropdown.remove(),this.removeEventListeners())}updateSelection(){Array.from(this.focusedDropdown.children).forEach(((e,t)=>{t===this.selectedIndex?e.classList.add("selected"):e.classList.remove("selected")}))}}function wi(e){const t=e.min||0,s=e.max||0,i=e.step||1;if(0===i)return[];const a=[];let n=t;for(;n<=s;){if(Number.isInteger(i))a.push(Math.round(n)+"; ");else{let e=n.toFixed(3);-0==e&&(e="0.000"),/\.\d{3}$/.test(e)||(e+="0"),a.push(e+"; ")}n+=i}return s>=0&&t>=0?a:a.reverse()}let bi={},_i={};function Si(e,t){String(e.id);const s=t.name,i=t.value.replace(/^(loader|preSampling):\s/,"");_i[s]?_i[s]!=bi[i]&&(_i[s]=bi[i]):_i={..._i,[s]:bi[i]}}fe.registerExtension({name:"Comfy.EasyUse.XYPlot",async beforeRegisterNodeDef(e,t,s){if("easy XYPlot"===t.name){bi=t.input.hidden.plot_dict[0];for(const e in bi){const t=bi[e];if(Array.isArray(t)){let s=[];for(const e of t)s.push(e+"; ");bi[e]=s}else bi[e]="object"==typeof t?"seed"==e?t+"; ":wi(t):t+"; "}bi.None=[],bi["---------------------"]=[]}},nodeCreated(e){"easy XYPlot"===e.comfyClass&&(function(e){if(e.widgets)for(const t of e.widgets)if("x_axis"===t.name||"y_axis"===t.name){let s=t.value;Object.defineProperty(t,"value",{get:()=>s,set(i){i!==s&&(s=i,Si(e,t))}})}}(e),function(e){if(e.widgets){const t=e.widgets.filter((e=>"customtext"===e.type&&!1!==e.dynamicPrompts||e.dynamicPrompts));for(const e of t){let t=function(e,t,i,a){return e&&(t[i]=e),t.map((e=>s(e,a))).filter((e=>""!==e)).join("")},s=function(e,t){if(e=i(e),a(e,t))return e+"; ";let s=n(e,t);return 1===s.length||2===s.length?s[0]:a(o(e),t)?o(e)+"; ":""},i=function(e){return e.replace(/(\n|;| )/g,"")},a=function(e,t){return t.includes(e+"; ")},n=function(e,t){return t.filter((t=>t.toLowerCase().includes(e.toLowerCase())))},o=function(e){return Number(e)?Number(e).toFixed(3):["0","0.","0.0","0.00","00"].includes(e)?"0.000":e};const l=function(){const s=e.name[0]+"_axis";let i=(null==_i?void 0:_i[s])||[];if(0===i.length)return;const a=e.inputEl.value,n=e.inputEl.selectionStart;let o=a.split("; ");const l=a.substring(0,n).split("; ").length-1,r=o[l].replace(/\n/g,"").toLowerCase(),d=i.filter((e=>e.toLowerCase().includes(r))).map((e=>e.replace(/; /g,"")));if(d.length>0)vi(e.inputEl,d,(s=>{const a=t(s,o,l,i);e.inputEl.value=a}));else{gi();const s=t(null,o,l,i);e.inputEl.value=s}};e.inputEl.removeEventListener("input",l),e.inputEl.addEventListener("input",l),e.inputEl.removeEventListener("mouseup",l),e.inputEl.addEventListener("mouseup",l)}}}(e))}}),S("domWidget",(()=>{const e=P(new Map);return{widgetStates:e,registerWidget:t=>{e.value.set(t.id,{widget:G(t),visible:!0,readonly:!1,zIndex:0,pos:[0,0],size:[0,0]})},unregisterWidget:t=>{e.value.delete(t)}}}));const Li={class:"w-full flex justify-between items-center"},xi={class:"flex justify-start items-center flex-1 gap-2"},ki={class:"flex justify-end items-center tool ml-2 position-relative"},Ei={__name:"promptAwait",props:["widget"],setup(e){const t=P({select:"now",unlock:!0,last_seed:0,seed:0}),s=P(!1),i=P(!1);let a=null;const n=()=>{if(i.value)return;const t=window.SpeechRecognition||window.webkitSpeechRecognition;t&&(i.value=!0,a||(a=new t,a.interimResults=!1),a.start(),a.addEventListener("result",(t=>{var s;const i=t.results[0][0].transcript;let a=null==(s=e.widget.value)?void 0:s.node,n=wt(a,"prompt");n&&(n.value+=i)})),a.addEventListener("end",(()=>{i.value=!1,a=null})),a.addEventListener("error",(e=>{})))},o=()=>{i.value&&a&&a.stop()};z((t=>e.widget.value),(e=>{t.value="object"==typeof e?e:JSON.parse(e)}),{immediate:!0}),V((i=>{ge.addEventListener("easyuse_prompt_await",(t=>{var i,a,n,o,l,d;const u=t.detail.id,c=-1!==(null==(i=e.widget.value)?void 0:i.node.id).toString().indexOf(":")?(null==(a=e.widget.value)?void 0:a.node.id).toString().split(":")[0]:null==(n=e.widget.value)?void 0:n.node.id;parseInt(u)===parseInt(c)&&(s.value=!0,r("last_seed",(null==(o=e.widget.value.value)?void 0:o.seed)||0),(null==(l=e.widget.value.value)?void 0:l.unlock)?r("seed",Math.floor(Math.random()*Oe)):r("seed",(null==(d=e.widget.value.value)?void 0:d.last_seed)||0))}));const a=ge.interrupt;ge.interrupt=function(){!s.value&&fe.runningNodeId||(l(JSON.stringify({result:-1,prompt:"",select:"now"}),!0),s.value=!1),a.apply(this,arguments)},e.widget.serializeValue=async({node:e},s)=>{try{let i=JSON.stringify(t.value);return(null==e?void 0:e.widgets_values)&&(e.widgets_values[s]=i,e.widgets[s].value=i),i}catch(i){return[]}}}));const l=(i,a)=>{var n,o,l,d,u,c,A,p,h;if(!s.value&&!a)return;const m=new FormData,f=null==(n=e.widget.value)?void 0:n.node,g=-1!==(null==(o=e.widget.value)?void 0:o.node.id).toString().indexOf(":")?(null==(l=e.widget.value)?void 0:l.node.id).toString().split(":")[0]:null==(d=e.widget.value)?void 0:d.node.id,v=(null==(u=wt(f,"prompt"))?void 0:u.value)||"";let y=null==(c=t.value)?void 0:c.select,w=(null==(A=t.value)?void 0:A.last_seed)||0,b=(null==(p=t.value)?void 0:p.seed)||0,_=(null==(h=t.value)?void 0:h.unlock)||!1;m.append("message",JSON.stringify({result:i,prompt:v,select:y,last_seed:w,seed:b,unlock:_})),m.append("id",g),s.value=!1,ge.fetchApi("/easyuse/message_callback",{method:"POST",body:m}).then((e=>{r("select","now")}))},r=(e,s)=>{t.value.value||(t.value={}),t.value[e]=s};return(e,a)=>(j(),U("div",Li,[W("div",xi,[F(X(x),{disabled:!Boolean(s.value),label:X(Ne)("Continue"),size:"small",style:{height:"20px","font-size":"12px","white-space":"nowrap",padding:"0 8px"},onClick:a[0]||(a[0]=e=>l(1))},null,8,["disabled","label"]),F(X(x),{disabled:!Boolean(s.value),label:X(Ne)("Stop"),severity:"danger",size:"small",style:{height:"20px","font-size":"12px","white-space":"nowrap",padding:"0 8px"},onClick:a[1]||(a[1]=e=>l(-1))},null,8,["disabled","label"]),F(X(k),{class:"easyuse-prompt-await-select",modelValue:t.value.select,"onUpdate:modelValue":a[2]||(a[2]=e=>t.value.select=e),options:[{name:X(Ne)("now"),value:"now"},{name:X(Ne)("prev"),value:"prev"}],optionLabel:"name",optionValue:"value",size:"small",style:{flex:"1",height:"24px","line-height":"10px","min-width":"70px","max-width":"100px"}},null,8,["modelValue","options"])]),W("div",ki,[Z(F(X(x),{style:{"--p-button-sm-font-size":"11px"},size:"small",icon:t.value.unlock?"pi pi-unlock":"pi pi-lock",severity:t.value.unlock?"contrast":"warn",variant:"text",onClick:a[3]||(a[3]=e=>t.value.unlock=!t.value.value.unlock),rounded:""},null,8,["icon","severity"]),[[X(E),{value:t.value.unlock?"随机种子":"锁定种子值",class:"jm-tooltip"},"top"]]),i.value?Z((j(),H(X(x),{key:0,size:"small",icon:"pi pi-pause-circle",severity:"info",variant:"outlined",onClick:o,rounded:""},null,512)),[[X(E),{value:X(Ne)("Stop Recording"),class:"jm-tooltip"},"top"]]):Z((j(),H(X(x),{key:1,size:"small",icon:"pi pi-microphone",severity:"contrast",variant:"text",onClick:n,rounded:""},null,512)),[[X(E),{value:X(Ne)("Voice input"),class:"jm-tooltip"},"top"]])])]))}},Ni={class:"easyuse-multiselect"},Ci={__name:"multiSelectWidget",props:["widget"],setup(e){var t,s,i,a;const n=Object.keys(De).reduce(((e,t)=>(Array.isArray(De[t])?e[t]=De[t].map(((e,t)=>({label:e,value:t}))):"object"==typeof De[t]?e[t]=Object.entries(De[t]).map((([e,t])=>({label:e,value:t}))):e[t]=[],e)),{}),o=P([]),l=e.widget.inputSpec,r=(null==(t=null==l?void 0:l.multi_select)?void 0:t.max_selected_labels)??3,d=(null==l?void 0:l.optionLabel)??"label",u=(null==l?void 0:l.optionValue)??"value",c=(null==(s=null==l?void 0:l.multi_select)?void 0:s.placeholder)?Ne(null==(i=l.multi_select)?void 0:i.placeholder):Ne("select items"),A=(null==(a=null==l?void 0:l.multi_select)?void 0:a.chip)?"chip":"comma",p=P((null==l?void 0:l.options)??[]),h=e=>{e&&n[e]?p.value=n[e].map(((e,t)=>({label:Ne(e.label),value:e.value}))):p.value=[]};return z((t=>e.widget.value),(e=>{o.value=e.split(",").map((e=>e&&parseInt(e.trim())))})),V((t=>{setTimeout((t=>{const s=wt(e.widget.node,"method");h(s.value),s.callback=e=>{o.value=[],h(e)}}),1),e.widget.serializeValue=async({node:e},t)=>{try{let s=o.value||[];return s=s.join(","),(null==e?void 0:e.widgets_values)&&(e.widgets_values[t]=s,e.widgets[t].value=s),s}catch(s){return[]}}})),(e,t)=>(j(),U("div",Ni,[F(X(N),{modelValue:o.value,"onUpdate:modelValue":t[0]||(t[0]=e=>o.value=e),options:p.value,optionLabel:X(d),optionValue:X(u),appendTo:"self",filter:"",placeholder:X(c),"max-selected-labels":X(r),display:X(A),class:"w-full easyuse-multiselect-container"},null,8,["modelValue","options","optionLabel","optionValue","placeholder","max-selected-labels","display"])]))}},Ii={class:"flex flex-col easyuse-multiangle-widget"},Ti={class:"easyuse-multiangle-tabs flex items-center gap-1 px-1 relative z-10 overflow-x-auto no-scrollbar w-full min-w-0"},Oi=["onClick"],Mi={class:"tab-number"},Di=["onClick"],Ri=["title"],Pi={class:"easyuse-multiangle-content flex flex-col gap-2"},Gi=["title"],zi={class:"settings-item flex items-center"},Vi={for:"add-angle-prompt",class:"whitespace-nowrap"},ji={class:"settings-item flex items-center"},Ui={for:"hollow-mode",class:"whitespace-nowrap"},Wi={class:"settings-item flex items-center"},Fi={for:"rotate-3d-mode",class:"whitespace-nowrap"},Xi=["title"],Zi=["onDblclick"],Hi={key:0},qi={key:0,style:{filter:"grayscale(10)"}},Yi={key:0,class:"absolute inset-0 flex flex-col items-center justify-center",style:{"pointer-events":"none"}},Ki={key:1,class:"easyuse-cube-face-label"},Ji={class:"flex flex-col gap-2 px-2 w-full easyuse-mulitangle-slider"},Bi={class:"flex flex-col gap-2 w-full"},Qi={class:"flex justify-between items-center"},$i={class:"font-semibold opacity-80"},ea={class:"font-mono text-primary font-bold"},ta={class:"flex flex-col gap-2 w-full"},sa={class:"flex justify-between items-center"},ia={class:"font-semibold opacity-80"},aa={class:"font-mono text-primary font-bold"},na={class:"flex flex-col gap-2 w-full"},oa={class:"flex justify-between items-center"},la={class:"font-semibold opacity-80"},ra={class:"font-mono text-primary font-bold"},da={__name:"multiAngleWidget",props:["widget"],emits:["update:value","change"],setup(e,{emit:t}){const s=P([{rotate:0,vertical:0,zoom:5,add_angle_prompt:!1}]),i=P(0),a=s.value[i.value]||{rotate:0,vertical:0,zoom:5,add_angle_prompt:!1},n=P(a.rotate??0),o=P(a.vertical??0),l=P(a.zoom??5),r=P(a.add_angle_prompt??!1),d=P(Le("EasyUse.MultiAngle.HollowMode")||!1),u=P(Le("EasyUse.MultiAngle.3DRotate")||!1),c=P(!1),A=()=>{c.value=!c.value},p=()=>{c.value&&(c.value=!1)};V((()=>{window.addEventListener("click",p)})),q((()=>{window.removeEventListener("click",p),x()})),z((t=>e.widget.value),(e=>{Array.isArray(e)?s.value=e:s.value=JSON.parse(e)}),{immediate:!0}),z((()=>{var e;return null==(e=s.value)?void 0:e[i.value]}),(e=>{if(e){let t=Le("EasyUse.MultiAngle.AddAnglePrompt")??!1;void 0!==e.rotate&&(n.value=e.rotate),void 0!==e.vertical&&(o.value=e.vertical),void 0!==e.zoom&&(l.value=e.zoom),null!=r&&(r.value=t)}}),{deep:!0}),z(d,(e=>{ke("EasyUse.MultiAngle.HollowMode",e)})),z(u,(e=>{ke("EasyUse.MultiAngle.InvertRotate",e)})),z(r,(e=>{ke("EasyUse.MultiAngle.AddAnglePrompt",e),s.value.forEach((t=>{t.add_angle_prompt=e}))}));const h=e=>{i.value=e;const t=s.value[e];t&&(n.value=t.rotate??0,o.value=t.vertical??0,l.value=t.zoom??5,r.value=t.add_angle_prompt??!0)},m=()=>{s.value[i.value];const e={rotate:n.value,vertical:o.value,zoom:l.value,add_angle_prompt:r.value};s.value.push(JSON.parse(JSON.stringify(e))),i.value=s.value.length-1,h(i.value)},f=(e=!1)=>{e&&(c.value=!1);const t={rotate:n.value,vertical:o.value,zoom:l.value,add_angle_prompt:r.value};Array.isArray(s.value)||(s.value=[]),s.value[i.value]=JSON.parse(JSON.stringify(t))},g=()=>{n.value=0,o.value=0,l.value=5,f()},v=Y((()=>[{name:"front",text:"",style:{transform:"translateZ(40px)"}},{name:"back",text:Ne("B"),style:{transform:"rotateY(180deg) translateZ(40px)"}},{name:"up",text:Ne("U"),style:{transform:"rotateX(90deg) translateZ(40px)"}},{name:"down",text:Ne("D"),style:{transform:"rotateX(-90deg) translateZ(40px)"}},{name:"left",text:Ne("L"),style:{transform:"rotateY(-90deg) translateZ(40px)"}},{name:"right",text:Ne("R"),style:{transform:"rotateY(90deg) translateZ(40px)"}},{name:"center",style:{transform:"translateZ(0.1px)",border:"none",background:"transparent",backfaceVisibility:"hidden"}},{name:"center-back",style:{transform:"rotateY(180deg) translateZ(0.1px)",border:"none",backfaceVisibility:"hidden",background:"transparent"}}])),y=P(!1),w={x:0,y:0},b={rotate:0,vertical:0},_=e=>{const t=e.deltaY;let s=l.value-.05*t;s=Math.max(0,Math.min(10,s)),l.value=Math.round(10*s)/10,f()},S=e=>{y.value=!0,w.x=e.clientX??e.touches[0].clientX,w.y=e.clientY??e.touches[0].clientY,b.rotate=n.value,b.vertical=o.value,window.addEventListener("mousemove",L),window.addEventListener("mouseup",x),window.addEventListener("touchmove",L),window.addEventListener("touchend",x)},L=e=>{if(!y.value)return;const t=e.clientX??e.touches[0].clientX,s=e.clientY??e.touches[0].clientY;let i=t-w.x,a=s-w.y;u.value&&(i=-i,a=-a);let l=b.rotate+.5*i,r=b.vertical+.5*a;l=(l%360+360)%360,r=Math.max(-90,Math.min(90,r)),n.value=Math.round(l),o.value=Math.round(r),f()},x=()=>{y.value=!1,window.removeEventListener("mousemove",L),window.removeEventListener("mouseup",x),window.removeEventListener("touchmove",L),window.removeEventListener("touchend",x)},k=Y((()=>({transform:`scale(${1+.1*l.value}) rotateX(${-o.value}deg) rotateY(${-n.value}deg)`})));return V((t=>{d.value=Le("EasyUse.MultiAngle.HollowMode"),u.value=Le("EasyUse.MultiAngle.InvertRotate"),r.value=Le("EasyUse.MultiAngle.AddAnglePrompt")||!1,e.widget.serializeValue=async({node:e},t)=>{try{let i=JSON.stringify(s.value);return(null==e?void 0:e.widgets_values)&&(e.widgets_values[t]=i,e.widgets[t].value=i),i}catch(i){return[]}}})),(e,t)=>(j(),U("div",Ii,[W("div",Ti,[(j(!0),U(K,null,J(s.value,((e,t)=>(j(),U("div",{key:t,class:B(["tab-item whitespace-nowrap flex-shrink-0",{active:i.value===t}]),onClick:e=>h(t)},[W("span",Mi,ee(t+1),1),s.value.length>1&&0!==t?(j(),U("i",{key:0,class:"pi pi-times tab-close",onClick:Q((e=>(e=>{s.value.length<=1||(s.value.splice(e,1),i.value>=s.value.length&&(i.value=s.value.length-1),h(i.value))})(t)),["stop"])},null,8,Di)):te("",!0)],10,Oi)))),128)),W("button",{class:"tab-add-btn flex-shrink-0",onClick:m,title:X(Ne)("Add New Tab")},t[10]||(t[10]=[W("i",{class:"pi pi-plus"},null,-1)]),8,Ri)]),W("div",Pi,[W("div",{class:B(["easyuse-multiangle-cube w-full flex items-center justify-center bg-black overflow-hidden select-none",{"is-hollow":d.value}]),onMousedown:S,onTouchstart:S,onWheel:Q(_,["prevent"])},[W("div",{class:"settings-icon",onMousedown:t[5]||(t[5]=Q((()=>{}),["stop"])),onClick:Q(A,["stop"]),title:X(Ne)("Settings")},[t[11]||(t[11]=W("i",{class:"pi pi-cog"},null,-1)),c.value?(j(),U("div",{key:0,class:"settings-dropdown",onClick:t[4]||(t[4]=Q((()=>{}),["stop"]))},[W("div",zi,[Z(W("input",{type:"checkbox","onUpdate:modelValue":t[0]||(t[0]=e=>r.value=e),onChange:t[1]||(t[1]=e=>f(!1)),id:"add-angle-prompt"},null,544),[[$,r.value]]),W("label",Vi,ee(X(Ne)("Angle Prompt")),1)]),W("div",ji,[Z(W("input",{type:"checkbox","onUpdate:modelValue":t[2]||(t[2]=e=>d.value=e),id:"hollow-mode"},null,512),[[$,d.value]]),W("label",Ui,ee(X(Ne)("Hollow")),1)]),W("div",Wi,[Z(W("input",{type:"checkbox","onUpdate:modelValue":t[3]||(t[3]=e=>u.value=e),id:"rotate-3d-mode"},null,512),[[$,u.value]]),W("label",Fi,ee(X(Ne)("Invert Rotate Mode")),1)])])):te("",!0)],40,Gi),W("div",{class:"reset-icon",onMousedown:t[6]||(t[6]=Q((()=>{}),["stop"])),onClick:Q(g,["stop"]),title:X(Ne)("Reset")},t[12]||(t[12]=[W("i",{class:"pi pi-refresh"},null,-1)]),40,Xi),W("div",{class:"relative transition-transform duration-75 ease-out",style:se([{"transform-style":"preserve-3d",width:"80px",height:"80px"},k.value])},[(j(!0),U(K,null,J(v.value,(e=>(j(),U("div",{key:e.name,class:"absolute flex items-center justify-center font-bold text-xs easyuse-multiangle-cube-face",style:se([{width:"80px",height:"80px","backface-visibility":"visible"},e.style]),onDblclick:Q((t=>(e=>{switch(e){case"front":n.value=0;break;case"back":n.value=180;break;case"left":n.value=270;break;case"right":n.value=90;break;case"up":o.value=90;break;case"down":o.value=-90}f()})(e.name)),["stop"])},["center"===e.name?(j(),U(K,{key:0},[d.value?(j(),U("div",Hi,t[13]||(t[13]=[W("div",{style:{"font-size":"20px","text-align":"center"}},"🤓",-1),W("div",{style:{"font-size":"12px","text-align":"center","margin-top":"-6px"}},"👕",-1),W("div",{style:{"font-size":"12px","text-align":"center","margin-top":"-6px"}},"👖",-1)]))):te("",!0)],64)):"center-back"===e.name?(j(),U(K,{key:1},[d.value?(j(),U("div",qi,t[14]||(t[14]=[W("div",{style:{"font-size":"20px","text-align":"center"}},"🌕",-1),W("div",{style:{"font-size":"12px","text-align":"center","margin-top":"-6px"}},"👕",-1),W("div",{style:{"font-size":"12px","text-align":"center","margin-top":"-6px"}},"👖",-1)]))):te("",!0)],64)):(j(),U(K,{key:2},["front"!==e.name||d.value?te("",!0):(j(),U("div",Yi,t[15]||(t[15]=[W("div",{style:{"font-size":"20px","text-align":"center"}},"🤓",-1),W("div",{style:{"font-size":"12px","text-align":"center","margin-top":"-6px"}},"👕",-1),W("div",{style:{"font-size":"12px","text-align":"center","margin-top":"-6px"}},"👖",-1)]))),e.text?(j(),U("div",Ki,ee(e.text),1)):te("",!0)],64))],44,Zi)))),128))],4)],34),W("div",Ji,[W("div",Bi,[W("div",Qi,[W("span",$i,ee(X(Ne)("Rotate")),1),W("span",ea,ee(n.value)+"°",1)]),F(X(C),{modelValue:n.value,"onUpdate:modelValue":[t[7]||(t[7]=e=>n.value=e),f],min:0,max:360,class:"w-full"},null,8,["modelValue"])]),W("div",ta,[W("div",sa,[W("span",ia,ee(X(Ne)("Vertical")),1),W("span",aa,ee(o.value)+"°",1)]),F(X(C),{modelValue:o.value,"onUpdate:modelValue":[t[8]||(t[8]=e=>o.value=e),f],min:-90,max:90,class:"w-full"},null,8,["modelValue"])]),W("div",na,[W("div",oa,[W("span",la,ee(X(Ne)("Zoom")),1),W("span",ra,ee(l.value),1)]),F(X(C),{modelValue:l.value,"onUpdate:modelValue":[t[9]||(t[9]=e=>l.value=e),f],min:0,max:10,step:.1,class:"w-full"},null,8,["modelValue"])])])])]))}},ua=(e,t)=>{const s=e.__vccOpts||e;for(const[i,a]of t)s[i]=a;return s},ca={class:"tew-toolbar"},Aa={class:"tew-mode-tabs"},pa={key:0,class:"tew-actions"},ha={key:1,class:"tew-actions"},ma={key:0,class:"tew-table-wrapper"},fa={class:"tew-table"},ga={class:"tew-cell-inner"},va=["value","onInput","onKeydown"],ya=["onClick"],wa={class:"tew-row-handle"},ba=["onClick"],_a=["value","onInput","onKeydown"],Sa={key:1,class:"tew-md-wrapper"},La=ua({__name:"tableEditorWidget",props:["widget","node"],setup(e){const t=P("table"),s=ie({headers:["列1","列2","列3"],rows:[["","",""],["","",""]]}),i=P("");function a(e){const t=e.headers.map(((t,s)=>{let i=String(t).length;for(const a of e.rows)i=Math.max(i,String(a[s]??"").replace(/\n/g,"
").length);return Math.max(i,3)})),s=(e,t)=>String(e).padEnd(t," ");return["| "+e.headers.map(((e,i)=>s(e,t[i]))).join(" | ")+" |","| "+t.map((e=>"-".repeat(e))).join(" | ")+" |",...e.rows.map((i=>"| "+e.headers.map(((e,a)=>s(String(i[a]??"").replace(/\n/g,"
"),t[a]))).join(" | ")+" |"))].join("\n")}function n(e){const t=e.split("\n").map((e=>e.trim())).filter(Boolean);if(t.length<2)return null;const s=e=>e.replace(/^\||\|$/g,"").split("|").map((e=>e.trim().replace(//gi,"\n")));let i=-1;for(let r=0;r{for(;e.lengthv()))}}function l(e){let t=0;for(const s of String(e||"").replace(/\*\*/g,""))t+=/[\u4e00-\u9fff\u3000-\u303f\uff00-\uffef]/.test(s)?14:8;return t}const r=Y((()=>{const e=Math.max(s.headers.length,...s.rows.map((e=>e.length)));return Array.from({length:e},((e,t)=>{let i=120;for(const a of s.rows){const e=String(a[t]??"");for(const t of e.split("\n"))i=Math.max(i,l(t)+28)}return Math.min(i,200)+"px"}))}));function d(e){if(!e)return;const t=Array.from(e.querySelectorAll(".tew-cell-textarea"));if(0===t.length)return;t.forEach((e=>{e.style.height="auto"}));const s=Math.max(...t.map((e=>e.scrollHeight)));t.forEach((e=>{e.style.height=s+"px"}))}function u(){s.rows.push(Array(s.headers.length).fill("")),y()}function c(){s.headers.push(`列${s.headers.length+1}`),s.rows.forEach((e=>e.push(""))),y()}function A(e,t,i,a=!1){oe((()=>{const t=e.target.closest("table");if(!t)return;const i=Array.from(t.querySelectorAll(".tew-cell-input")),n=i.indexOf(e.target);if(-1===n)return;const o=i[n+(a?s.headers.length:1)];o&&o.focus()}))}let p=null;function h(){clearTimeout(p),p=setTimeout((()=>{y()}),300)}function m(){const e=n(i.value);e&&(s.headers=e.headers,s.rows=e.rows,i.value=a(s)),y()}function f(){return JSON.stringify({mode:t.value,headers:s.headers,rows:s.rows,markdown:"markdown"===t.value?i.value:a(s)})}function g(e){try{const n="string"==typeof e?JSON.parse(e):e;if(n&&n.headers)return s.headers=n.headers,s.rows=n.rows||[],t.value=n.mode||"table",i.value=n.markdown||a(s),void oe((()=>v()))}catch(o){}if("string"==typeof e&&e.includes("|")){const t=n(e);t&&(s.headers=t.headers,s.rows=t.rows,i.value=e,oe((()=>v())))}}function v(){const e=document.querySelector(".tew-table");if(!e)return;Array.from(e.querySelectorAll("tbody tr")).forEach((e=>d(e)))}function y(){e.widget&&(e.widget.value=f())}return V((()=>{var n;(null==(n=e.widget)?void 0:n.value)&&g(e.widget.value),e.widget.serializeValue=async({node:e},n)=>{const o="markdown"===t.value?i.value:a(s);return(null==e?void 0:e.widgets_values)&&(e.widgets_values[n]=o,e.widgets[n].value=f()),o},z((()=>e.widget.value),(e=>{e!==f()&&g(e)}))})),(e,a)=>(j(),U("div",{class:"table-editor-widget",onMousedown:a[3]||(a[3]=Q((()=>{}),["stop"])),onWheel:a[4]||(a[4]=Q((()=>{}),["stop"]))},[W("div",ca,[W("div",Aa,[W("button",{class:B(["tew-tab","table"===t.value?"active":""]),onClick:a[0]||(a[0]=e=>o("table"))},a[5]||(a[5]=[W("i",{class:"pi pi-table"},null,-1),ae(" 表格 ")]),2),W("button",{class:B(["tew-tab","markdown"===t.value?"active":""]),onClick:a[1]||(a[1]=e=>o("markdown"))},a[6]||(a[6]=[W("i",{class:"pi pi-code"},null,-1),ae(" Markdown ")]),2)]),"table"===t.value?(j(),U("div",pa,[W("button",{class:"tew-btn",onClick:u,title:"添加行"},a[7]||(a[7]=[W("i",{class:"pi pi-plus"},null,-1),ae(" 行 ")])),W("button",{class:"tew-btn",onClick:c,title:"添加列"},a[8]||(a[8]=[W("i",{class:"pi pi-plus"},null,-1),ae(" 列 ")]))])):(j(),U("div",ha,[W("button",{class:"tew-btn",onClick:m,title:"格式化"},a[9]||(a[9]=[W("i",{class:"pi pi-align-left"},null,-1),ae(" 格式化 ")]))]))]),"table"===t.value?(j(),U("div",ma,[W("table",fa,[W("thead",null,[W("tr",null,[a[10]||(a[10]=W("th",{class:"tew-row-handle"},null,-1)),(j(!0),U(K,null,J(s.headers,((e,t)=>(j(),U("th",{key:"h"+t,class:"tew-header-cell",style:se({width:r.value[t]})},[W("div",ga,[W("input",{class:"tew-cell-input tew-header-input",value:e,onInput:e=>function(e,t){s.headers[e]=t,y()}(t,e.target.value),onKeydown:le(Q((e=>A(e)),["prevent"]),["tab"]),placeholder:"列标题"},null,40,va),W("button",{class:"tew-col-del",onClick:e=>function(e){s.headers.length<=1||(s.headers.splice(e,1),s.rows.forEach((t=>t.splice(e,1))),y())}(t),title:"删除列"},"×",8,ya)])],4)))),128))])]),W("tbody",null,[(j(!0),U(K,null,J(s.rows,((e,t)=>(j(),U("tr",{key:"r"+t},[W("td",wa,[W("button",{class:"tew-row-del",onClick:e=>function(e){s.rows.length<=1||(s.rows.splice(e,1),y())}(t),title:"删除行"},"×",8,ba)]),(j(!0),U(K,null,J(e,((e,i)=>(j(),U("td",{key:"c"+i,class:"tew-body-cell",style:se({width:r.value[i]})},[W("textarea",{class:"tew-cell-input tew-cell-textarea",value:e,onInput:e=>{var a;!function(e,t,i){s.rows[e][t]=i,y()}(t,i,e.target.value),(a=e.target)&&(a.style.height="auto",a.style.height=a.scrollHeight+"px",d(a.closest("tr")))},onKeydown:le(Q((e=>A(e)),["prevent"]),["tab"]),rows:"1"},null,40,_a)],4)))),128))])))),128))])])])):(j(),U("div",Sa,[Z(W("textarea",{class:"tew-md-textarea","onUpdate:modelValue":a[2]||(a[2]=e=>i.value=e),onInput:h,placeholder:"| 列1 | 列2 | 列3 |\n| --- | --- | --- |\n| 值1 | 值2 | 值3 |",spellcheck:"false"},null,544),[[ne,i.value]])]))],32))}},[["__scopeId","data-v-7e0ef36e"]]),xa={class:"relative rounded-t-lg overflow-hidden select-none"},ka={key:1,class:"w-full h-full flex items-center justify-center"},Ea=ua({__name:"baseThumbnail",props:{hoverZoom:{type:"number",default:4},isHovered:{type:Boolean,default:!1}},setup(e){const t=P(!1),s=P(null);return V((()=>{var e;Array.from((null==(e=s.value)?void 0:e.getElementsByTagName("img"))??[]).forEach((e=>{ce(e,"error",(()=>{t.value=!0}))}))})),(i,a)=>(j(),U("div",xa,[t.value?(j(),U("div",ka,a[0]||(a[0]=[W("i",{class:"pi pi-file text-4xl"},null,-1)]))):(j(),U("div",{key:0,ref_key:"contentRef",ref:s,class:"w-full h-full transform-gpu transition-transform duration-1000 ease-out",style:se(e.isHovered?{transform:`scale(${1+e.hoverZoom/100})`}:void 0)},[re(i.$slots,"default",{},void 0,!0)],4))]))}},[["__scopeId","data-v-604a4248"]]),Na=["src","alt"],Ca=ua({__name:"lazyImage",props:{src:{type:String,required:!0},alt:{type:String,default:""},imageClass:{type:[String,Array,Object],default:""},imageStyle:{type:[String,Object],default:()=>({})},placeholderClass:{type:[String,Array,Object],default:""},errorClass:{type:[String,Array,Object],default:""},rootMargin:{type:String,default:"50px"},threshold:{type:Number,default:.1},immediate:{type:Boolean,default:!1}},emits:["load","error","visible"],setup(e,{emit:t}){const s=e,i=t,a=P(null),n=P(s.immediate),o=P(!1),l=P(!1),r=Y((()=>s.src));z((()=>s.src),(()=>{o.value=!1,l.value=!1,s.immediate||(n.value=!1)}),{immediate:!0});const{stop:d}=Ae(a,(([e])=>{(null==e?void 0:e.isIntersecting)&&!n.value&&(n.value=!0,i("visible"),d())}),{rootMargin:s.rootMargin,threshold:s.threshold}),u=e=>{o.value=!0,l.value=!1,i("load",e)},c=e=>{l.value=!0,o.value=!1,i("error",e)};return(t,s)=>(j(),U("div",{ref_key:"containerRef",ref:a,class:"lazy-image-container"},[l.value?(j(),U("div",{key:0,class:B(["lazy-image-error","w-full h-full flex items-center justify-center bg-surface-100 dark:bg-surface-800",e.errorClass])},[re(t.$slots,"error",{},(()=>[s[0]||(s[0]=W("i",{class:"pi pi-exclamation-triangle text-2xl text-surface-400"},null,-1))]),!0)],2)):n.value&&r.value?(j(),U("img",{key:1,src:r.value,alt:e.alt,class:B(e.imageClass),style:se(e.imageStyle),draggable:"false",onLoad:u,onError:c},null,46,Na)):n.value?te("",!0):(j(),U("div",{key:2,class:B(["lazy-image-placeholder","w-full h-full flex items-center justify-center bg-surface-100 dark:bg-surface-800",e.placeholderClass])},[re(t.$slots,"placeholder",{},(()=>[s[1]||(s[1]=W("i",{class:"pi pi-image text-2xl text-surface-400"},null,-1))]),!0)],2))],512))}},[["__scopeId","data-v-4bfc03bf"]]),Ia={class:"overflow-hidden w-full h-full flex items-center justify-center"},Ta={__name:"defaultThumbnail",props:{src:{type:String,required:!0},alt:{type:String,required:!0},hoverZoom:{type:Number,required:!0},isHovered:{type:Boolean,default:!1},isVideo:{type:Boolean,default:!1},rootMargin:{type:String,default:"50px"},threshold:{type:Number,default:.1},immediate:{type:Boolean,default:!1}},emits:["load","error","visible"],setup(e,{emit:t}){const s=e,{src:i,isVideo:a}=s;return a??(null==i||i.toLowerCase().endsWith(".webp")),(t,s)=>(j(),H(Ea,{"hover-zoom":e.hoverZoom,"is-hovered":e.isHovered},{default:de((()=>[W("div",Ia,[F(Ca,{src:X(i),"image-class":["transform-gpu transition-transform duration-300 ease-out","w-full h-full object-cover"],"image-style":e.isHovered?{transform:`scale(${1+e.hoverZoom/100})`}:void 0,"root-margin":e.rootMargin,threshold:e.threshold,immediate:e.immediate,onLoad:s[0]||(s[0]=e=>t.$emit("load",e)),onError:s[1]||(s[1]=e=>t.$emit("error",e)),onVisible:s[2]||(s[2]=e=>t.$emit("visible",e))},{placeholder:de((()=>[re(t.$slots,"placeholder",{},(()=>[s[3]||(s[3]=W("i",{class:"pi pi-image text-2xl text-surface-400"},null,-1))]))])),error:de((()=>[re(t.$slots,"error",{},(()=>[s[4]||(s[4]=W("i",{class:"pi pi-exclamation-triangle text-2xl text-surface-400"},null,-1))]))])),_:3},8,["src","image-style","root-margin","threshold","immediate"])])])),_:3},8,["hover-zoom","is-hovered"]))}},Oa={class:"overflow-hidden w-full h-full flex items-center justify-center relative"},Ma={__name:"compareSliderThumbnail",props:{baseImageSrc:{type:String,required:!0},overlayImageSrc:{type:String,required:!0},alt:{type:String,required:!0},isHovered:{type:Boolean,default:!1},isVideo:{type:Boolean,default:!1},rootMargin:{type:String,default:"50px"},threshold:{type:Number,default:.1},immediate:{type:Boolean,default:!1}},emits:["load","error","visible"],setup(e,{emit:t}){var s,i;const a=e;a.isVideo||(null==(s=a.baseImageSrc)?void 0:s.toLowerCase().endsWith(".webp"))||null==(i=a.overlayImageSrc)||i.toLowerCase().endsWith(".webp");const n=P(50),o=P(null),{elementX:l,elementWidth:r,isOutside:d}=pe(o);return z([()=>a.isHovered,l,r,d],(([e,t,s,i])=>{e&&(i||(n.value=t/s*100))})),(t,s)=>(j(),H(Ea,{"is-hovered":e.isHovered},{default:de((()=>[W("div",Oa,[F(Ca,{src:e.baseImageSrc,"image-class":["transform-gpu transition-transform duration-300 ease-out","w-full h-full object-cover block"],"root-margin":e.rootMargin,threshold:e.threshold,immediate:e.immediate,onLoad:s[0]||(s[0]=e=>t.$emit("load",e)),onError:s[1]||(s[1]=e=>t.$emit("error",e)),onVisible:s[2]||(s[2]=e=>t.$emit("visible",e))},{placeholder:de((()=>[re(t.$slots,"placeholder",{},(()=>[s[3]||(s[3]=W("i",{class:"pi pi-image text-2xl text-surface-400"},null,-1))]))])),error:de((()=>[re(t.$slots,"error",{},(()=>[s[4]||(s[4]=W("i",{class:"pi pi-exclamation-triangle text-2xl text-surface-400"},null,-1))]))])),_:3},8,["src","root-margin","threshold","immediate"]),W("div",{ref_key:"containerRef",ref:o,class:"absolute inset-0"},[F(Ca,{src:e.overlayImageSrc,"image-class":["transform-gpu transition-transform duration-300 ease-out","w-full h-full object-cover block"],"image-style":{clipPath:`inset(0 ${100-n.value}% 0 0)`},"root-margin":e.rootMargin,threshold:e.threshold,immediate:e.immediate},{placeholder:de((()=>[re(t.$slots,"placeholder",{},(()=>[s[5]||(s[5]=W("i",{class:"pi pi-image text-2xl text-surface-400"},null,-1))]))])),error:de((()=>[re(t.$slots,"error",{},(()=>[s[6]||(s[6]=W("i",{class:"pi pi-exclamation-triangle text-2xl text-surface-400"},null,-1))]))])),_:3},8,["src","image-style","root-margin","threshold","immediate"]),W("div",{class:"absolute inset-y-0 bg-white/30 backdrop-blur-sm z-10 pointer-events-none",style:se({left:`${n.value}%`,width:"1px"})},null,4)],512)])])),_:3},8,["is-hovered"]))}},Da={class:"selector-card-wrapper"},Ra={class:"card-image-container"},Pa={key:2,class:"selection-badge"},Ga={class:"card-content"},za={class:"card-title"},Va=ua({__name:"selectorCard",props:{item:{type:Object,required:!0},uniqueKey:{type:String,default:"id"},selectable:{type:Boolean,default:!1},isSelected:{type:Boolean,default:!1},multiple:{type:Boolean,default:!1},showDescription:{type:Boolean,default:!0},showMeta:{type:Boolean,default:!1},showActions:{type:Boolean,default:!1},actions:{type:Array,default:()=>[]},itemFields:{type:Object,default:()=>({id:"id",title:"title",description:"description",image:"image",meta:"meta"})},locale:{type:String,default:"en"}},emits:["select","action","item-hover","item-leave"],setup(e,{emit:t}){const s=e,i=t,a=P(null),n=he(a),o=()=>{s.selectable&&i("select",s.item)},l=()=>{i("item-hover",s.item)},r=()=>{i("item-leave",s.item)};return(t,s)=>(j(),U("div",Da,[W("div",{ref_key:"cardRef",ref:a,class:B(["card-item",{selected:e.isSelected,clickable:e.selectable}]),onClick:o,onMouseenter:l,onMouseleave:r},[W("div",Ra,["compareSlider"===e.item.thumbnailVariant?(j(),H(Ma,{key:0,"base-image-src":e.item.thumbnail[0],"overlay-image-src":e.item.thumbnail[1],alt:t.title,"is-hovered":X(n),"is-video":"video"===e.item.mediaType||"webp"===e.item.mediaSubtype,class:"card-image"},null,8,["base-image-src","overlay-image-src","alt","is-hovered","is-video"])):(j(),H(Ta,{key:1,src:e.item.thumbnail||"",alt:"zh"==e.locale?e.item.name_cn:e.item.name,"hover-zoom":4,"is-hovered":!1,class:"card-image"},null,8,["src","alt"])),e.selectable&&e.isSelected?(j(),U("div",Pa,s[0]||(s[0]=[W("i",{class:"pi pi-check text-white"},null,-1)]))):te("",!0)]),W("div",Ga,[W("div",za,ee("zh"==e.locale?e.item.name_cn:e.item.name),1)])],34)]))}},[["__scopeId","data-v-88af28b0"]]),ja={class:"list-item"},Ua=["name","checked"],Wa={class:"list-item-content"},Fa={class:"list-title"},Xa=ua({__name:"selectorList",props:{item:{type:Object,required:!0},uniqueKey:{type:String,default:"id"},selectable:{type:Boolean,default:!1},isSelected:{type:Boolean,default:!1},multiple:{type:Boolean,default:!1},showDescription:{type:Boolean,default:!0},showActions:{type:Boolean,default:!1},actions:{type:Array,default:()=>[]},itemFields:{type:Object,default:()=>({id:"id",title:"title",description:"description"})},locale:{type:String,default:"en"}},emits:["select","action","item-hover","item-leave"],setup(e,{emit:t}){const s=e,i=t,a=()=>{s.selectable&&i("select",s.item)},n=()=>{i("item-hover",s.item)},o=()=>{i("item-leave",s.item)};return(t,s)=>(j(),U("div",ja,[W("span",{class:B(["list-item__tag",{selected:e.isSelected}]),onClick:a,onMouseenter:n,onMouseleave:o},[e.selectable?(j(),U("input",{key:0,type:"checkbox",name:e.item.name,checked:e.isSelected,onClick:s[0]||(s[0]=Q((()=>{}),["stop"]))},null,8,Ua)):te("",!0),W("span",Wa,[W("div",Fa,ee("zh"==e.locale?e.item.name_cn:e.item.name),1)])],34)]))}},[["__scopeId","data-v-c93b1734"]]),Za=S("graphStore",{state:e=>({selectors:[],selectors_styles:{},seg_selectors:[],slider_controls:[]}),actions:{setSelectors(e){this.selectors=R(e)},setStyles(e,t){this.selectors_styles[e]||(this.selectors_styles[e]=t)},setSegSelectors(e){this.seg_selectors=R(e)},setSliderControls(e){this.slider_controls=R(e)}}}),Ha={class:"easyuse-styles-selector"},qa={class:"easyuse-styles-selector-header flex justify-between gap-2"},Ya={class:"flex align-center gap-1"},Ka={class:"flex-1"},Ja={key:0,class:"grid-container gap-2"},Ba={key:1,class:"list-container"},Qa={key:0,class:"easyuse-styles-selector-previewer"},$a={class:"easyuse-styles-selector-previewer__text"},en=["src"],tn={class:"title"},sn={class:"easyuse-styles-selector-previewer__prompt"},an={key:0},nn={class:"comfyui-easyuse-success"},on={key:1},ln={class:"comfyui-easyuse-error"},rn={__name:"stylesSelector",props:["widget"],setup(e){const t=Za(),{selectors_styles:s}=I(t),i=P(""),a=P([]),n=Y((e=>Le("Comfy.Locale")||"en")),o=P(Le("EasyUse.StylesSelector.DisplayType")||"Grid");"Gird"==o.value&&(o.value="Grid",ke("EasyUse.StylesSelector.DisplayType","Grid"));const l=P([]),r=P("");z((e=>o.value),(e=>{ke("EasyUse.StylesSelector.DisplayType",e)})),z((t=>e.widget.value),(e=>{a.value=e.split(",").map((e=>e.trim()))}));const d=Y((()=>{if(!r.value.trim())return l.value;const e=a.value||[],t=r.value.toLowerCase(),s=[],i=[];return l.value.forEach((a=>{if(e.includes(a.name))s.push(a);else{const e="zh"===n.value&&a.name_cn?a.name_cn:a.name,s=a.prompt||"",o=a.negative_prompt||"";(e.toLowerCase().includes(t)||s.toLowerCase().includes(t)||o.toLowerCase().includes(t))&&i.push(a)}})),[...s,...i]})),u=ie({name:"",positive:"",negative:"",src:""}),c=async e=>{if(s.value[e])return!0;const i=await ge.fetchApi(`/easyuse/prompt/styles?name=${e}`);if(200===i.status){let s=(await i.json()).map(((e,t)=>(e.index=t,e)));return await t.setStyles(e,s),!0}return Ye.error(Ne("Get styles list Failed")),!1},A=e=>{var t,s;u.name="zh"==n.value&&e.name_cn?e.name_cn:e.name,u.src=e.thumbnailVariant&&"default"!=e.thumbnailVariant?(null==(t=e.thumbnail)?void 0:t[1])||(null==(s=e.thumbnail)?void 0:s[0]):e.thumbnail,u.positive=e.prompt,u.negative=e.negative_prompt},p=()=>{u.name="",u.src="",u.positive="",u.negative=""},h=e=>{const t=a.value||[],n=R(s.value[i.value]);l.value=[...n.sort(((e,t)=>e.index-t.index)).sort(((e,s)=>t.includes(s.name)-t.includes(e.name)))]},m=e=>(a.value||[]).includes(e.name),f=e=>{let t=[...a.value||[]];const s=e.name,i=t.findIndex((e=>e===s));i>-1?t.splice(i,1):t.push(s),a.value=t},g=()=>{a.value=[]};return V((t=>{setTimeout((t=>{const s=wt(e.widget.node,"styles");let a=s.value;i.value=a,c(a).then((e=>e&&h())),s.callback=e=>{i.value=e,g(),c(e).then((e=>e&&h()))}}),1),e.widget.serializeValue=async({node:e},t)=>{try{let s=a.value||[];return s=s.join(","),(null==e?void 0:e.widgets_values)&&(e.widgets_values[t]=s,e.widgets[t].value=s),s}catch(s){return[]}}})),(t,s)=>(j(),U("div",Ha,[W("div",qa,[W("div",Ya,[Z(F(X(x),{icon:"pi pi-trash",severity:"secondary",outlined:"",size:"small",onClick:g},null,512),[[X(E),X(Ne)("Reset")]])]),W("div",Ka,[F(X(M),null,{default:de((()=>[F(X(T),{class:"pi pi-search"}),F(X(O),{modelValue:r.value,"onUpdate:modelValue":s[0]||(s[0]=e=>r.value=e),placeholder:X(Ne)("Search styles..."),class:"w-full"},null,8,["modelValue","placeholder"])])),_:1})]),F(X(D),{modelValue:o.value,"onUpdate:modelValue":s[1]||(s[1]=e=>o.value=e),options:["Grid","List"],"allow-empty":!1},{option:de((({option:e})=>[W("i",{class:B(["List"===e?"pi pi-bars":"pi pi-table"])},null,2)])),_:1},8,["modelValue"])]),W("div",{class:"easyuse-styles-selector-content",onMouseleave:s[2]||(s[2]=t=>h(e.widget.node.name))},["Grid"===o.value?(j(),U("div",Ja,[(j(!0),U(K,null,J(d.value,((e,t)=>(j(),H(Va,{key:`${e.name}-${t}`,item:e,"unique-key":"name",locale:n.value,selectable:!0,"is-selected":m(e),"show-description":!0,"preview-enabled":!0,onSelect:f,onItemHover:A,onItemLeave:p},null,8,["item","locale","is-selected"])))),128))])):(j(),U("div",Ba,[(j(!0),U(K,null,J(d.value,((e,t)=>(j(),H(Xa,{key:`${e.name}-${t}`,item:e,"unique-key":"name",locale:n.value,selectable:!0,"is-selected":m(e),"show-description":!0,onSelect:f,onItemHover:A,onItemLeave:p},null,8,["item","locale","is-selected"])))),128))]))],32),(null==u?void 0:u.positive)||(null==u?void 0:u.negative)?(j(),U("div",Qa,[W("div",$a,[W("img",{src:u.src,alt:"",style:{width:"100%",height:"auto","object-fit":"contain","border-radius":"6px"}},null,8,en),W("span",tn,ee(u.name),1),W("div",sn,[u.positive?(j(),U("h6",an,[W("span",nn,ee(X(Ne)("Positive"))+":",1),W("span",null,ee(u.positive),1)])):te("",!0),u.negative?(j(),U("h6",on,[W("span",ln,ee(X(Ne)("Negative"))+":",1),W("span",null,ee(u.negative),1)])):te("",!0)])])])):te("",!0)]))}},dn=new Map,un={width:"100%",height:"100%",display:"flex",flexDirection:"column",overflow:"hidden"},cn=(e,t)=>{const s=document.createElement("div"),i=e.node,a=e.options||{getMinHeight:()=>100,hideOnZoom:!0,serialize:!0},n=e.styles||null;s.id=`easyuse-vue-widget-${i.id}`,Object.assign(s.style,un,n||{});const o=i.addDOMWidget(e.name||"ustom_vue_component_basic","vue-basic",s,a);e.inputSpec&&(o.inputSpec=e.inputSpec);const l=ue(t,{widget:o,node:i});return l.use(me),l.mount(s),dn.set(i.id,l),o.onRemove=()=>{const e=dn.get(i.id);e&&(e.unmount(),dn.delete(i.id))},{widget:o}};fe.registerExtension({name:"Comfy.EasyUse.CustomWidget",getCustomWidgets:e=>({EASY_PROMPT_AWAIT_BAR:e=>{const t=P(JSON.stringify({select:"now",unlock:!0,last_seed:0,seed:0})),s=!!["obsidian","obsidian_dark","milk_white"].includes(Le("Comfy.ColorPalette"))?48:54;return cn({name:"toolbar",node:e,options:{getMinHeight:()=>s,getMaxHeight:()=>s,getValue:()=>t.value,setValue:e=>{try{t.value="object"==typeof e?JSON.stringify(e):e}catch(s){}}}},Ei)},EASY_PROMPT_STYLES:e=>{const t=P("");return cn({name:"select_styles",node:e,options:{getMinHeight:()=>180,getMaxHeight:()=>e.size[1]-75,getValue:()=>t.value,setValue:e=>{t.value=Array.isArray(e)?e.join(","):e}},styles:{overflow:"visible"}},rn)},EASY_COMBO:(e,t,s)=>{const i=P(""),a=!!["obsidian","obsidian_dark","milk_white"].includes(Le("Comfy.ColorPalette"))?22:26;return cn({name:"mask_components",node:e,options:{margin:0,getMinHeight:()=>a,getMaxHeight:()=>a,getValue:()=>i.value,setValue:e=>{i.value=Array.isArray(e)?e.join(","):e}},inputSpec:null==s?void 0:s[1],styles:{overflow:"visible"}},Ci)},EASY_MULTI_ANGLE:e=>{const t=P(JSON.stringify([{rotate:0,vertical:0,zoom:5,add_angle_prompt:!0}]));return cn({name:"multi_angle",node:e,options:{getMinHeight:()=>350,getMaxHeight:()=>e.size[1]-80,getValue:()=>t.value,setValue:e=>{try{t.value=Array.isArray(e)?JSON.stringify(e):e}catch(s){}}},styles:{overflow:"visible"}},da)},EASY_TABLE_EDITOR:(e,t,s,i)=>{const a=P(JSON.stringify({mode:"table",headers:["列1","列2","列3"],rows:[["","",""],["","",""]],markdown:"| 列1 | 列2 | 列3 |\n| --- | --- | --- |\n| | | |\n| | | |"}));return cn({name:"table_data",node:e,options:{getMinHeight:()=>200,getMaxHeight:()=>e.size[1],getValue:()=>a.value,setValue:e=>{a.value="object"==typeof e?JSON.stringify(e):e}},styles:{overflow:"hidden"}},La)}})});export{Ne as $,Re as N,fe as a,Ke as b,Le as c,vs as d,Pe as e,ge as f,wt as g,xe as h,Nt as j,Xe as s,Ye as t,Za as u}; diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v2/assets/lodash-DkvwM0Z9.js b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v2/assets/lodash-DkvwM0Z9.js new file mode 100644 index 0000000000000000000000000000000000000000..d8c64ec8188d45c84c31267f31bcba7d86c0a346 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v2/assets/lodash-DkvwM0Z9.js @@ -0,0 +1,9 @@ +var t="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{};function n(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var r=function(){this.__data__=[],this.size=0};var e=function(t,n){return t===n||t!=t&&n!=n},u=e;var o=function(t,n){for(var r=t.length;r--;)if(u(t[r][0],n))return r;return-1},i=o,a=Array.prototype.splice;var c=o;var f=o;var l=o;var s=r,p=function(t){var n=this.__data__,r=i(n,t);return!(r<0)&&(r==n.length-1?n.pop():a.call(n,r,1),--this.size,!0)},h=function(t){var n=this.__data__,r=c(n,t);return r<0?void 0:n[r][1]},v=function(t){return f(this.__data__,t)>-1},_=function(t,n){var r=this.__data__,e=l(r,t);return e<0?(++this.size,r.push([t,n])):r[e][1]=n,this};function g(t){var n=-1,r=null==t?0:t.length;for(this.clear();++n-1&&t%1==0&&t-1&&t%1==0&&t<=9007199254740991},Tn=P,Wn=Un,Bn=ln,Cn={};Cn["[object Float32Array]"]=Cn["[object Float64Array]"]=Cn["[object Int8Array]"]=Cn["[object Int16Array]"]=Cn["[object Int32Array]"]=Cn["[object Uint8Array]"]=Cn["[object Uint8ClampedArray]"]=Cn["[object Uint16Array]"]=Cn["[object Uint32Array]"]=!0,Cn["[object Arguments]"]=Cn["[object Array]"]=Cn["[object ArrayBuffer]"]=Cn["[object Boolean]"]=Cn["[object DataView]"]=Cn["[object Date]"]=Cn["[object Error]"]=Cn["[object Function]"]=Cn["[object Map]"]=Cn["[object Number]"]=Cn["[object Object]"]=Cn["[object RegExp]"]=Cn["[object Set]"]=Cn["[object String]"]=Cn["[object WeakMap]"]=!1;var Ln=function(t){return Bn(t)&&Wn(t.length)&&!!Cn[Tn(t)]};var Pn=function(t){return function(n){return t(n)}},Fn={exports:{}};!function(t,n){var r=x,e=n&&!n.nodeType&&n,u=e&&t&&!t.nodeType&&t,o=u&&u.exports===e&&r.process,i=function(){try{var t=u&&u.require&&u.require("util").types;return t||o&&o.binding&&o.binding("util")}catch(n){}}();t.exports=i}(Fn,Fn.exports);var $n=Fn.exports,Dn=Ln,Mn=Pn,Nn=$n&&$n.isTypedArray,qn=Nn?Mn(Nn):Dn,Vn=fn,Zn=zn,Gn=Sn,Kn=kn,Hn=Rn,Jn=qn,Yn=Object.prototype.hasOwnProperty;var Qn=function(t,n){var r=Gn(t),e=!r&&Zn(t),u=!r&&!e&&Kn(t),o=!r&&!e&&!u&&Jn(t),i=r||e||u||o,a=i?Vn(t.length,String):[],c=a.length;for(var f in t)!n&&!Yn.call(t,f)||i&&("length"==f||u&&("offset"==f||"parent"==f)||o&&("buffer"==f||"byteLength"==f||"byteOffset"==f)||Hn(f,c))||a.push(f);return a},Xn=Object.prototype;var tr=function(t){var n=t&&t.constructor;return t===("function"==typeof n&&n.prototype||Xn)};var nr=function(t,n){return function(r){return t(n(r))}},rr=nr(Object.keys,Object),er=tr,ur=rr,or=Object.prototype.hasOwnProperty;var ir=N,ar=Un;var cr=function(t){return null!=t&&ar(t.length)&&!ir(t)},fr=Qn,lr=function(t){if(!er(t))return ur(t);var n=[];for(var r in Object(t))or.call(t,r)&&"constructor"!=r&&n.push(r);return n},sr=cr;var pr=function(t){return sr(t)?fr(t):lr(t)},hr=cn,vr=pr;var _r=function(t,n){return t&&hr(n,vr(n),t)};var gr=F,yr=tr,dr=function(t){var n=[];if(null!=t)for(var r in Object(t))n.push(r);return n},br=Object.prototype.hasOwnProperty;var wr=Qn,jr=function(t){if(!gr(t))return dr(t);var n=yr(t),r=[];for(var e in t)("constructor"!=e||!n&&br.call(t,e))&&r.push(e);return r},mr=cr;var xr=function(t){return mr(t)?wr(t,!0):jr(t)},Ar=cn,Or=xr;var zr=function(t,n){return t&&Ar(n,Or(n),t)},Sr={exports:{}};!function(t,n){var r=z,e=n&&!n.nodeType&&n,u=e&&t&&!t.nodeType&&t,o=u&&u.exports===e?r.Buffer:void 0,i=o?o.allocUnsafe:void 0;t.exports=function(t,n){if(n)return t.slice();var r=t.length,e=i?i(r):new t.constructor(r);return t.copy(e),e}}(Sr,Sr.exports);var Ir=Sr.exports;var kr=function(t,n){var r=-1,e=t.length;for(n||(n=Array(e));++r + * Copyright OpenJS Foundation and other contributors + * Released under MIT license + * Based on Underscore.js 1.8.3 + * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors + */!function(n,r){(function(){var e,u="Expected a function",o="__lodash_hash_undefined__",i="__lodash_placeholder__",a=16,c=32,f=64,l=128,s=256,p=1/0,h=9007199254740991,v=NaN,_=4294967295,g=[["ary",l],["bind",1],["bindKey",2],["curry",8],["curryRight",a],["flip",512],["partial",c],["partialRight",f],["rearg",s]],y="[object Arguments]",d="[object Array]",b="[object Boolean]",w="[object Date]",j="[object Error]",m="[object Function]",x="[object GeneratorFunction]",A="[object Map]",O="[object Number]",z="[object Object]",S="[object Promise]",I="[object RegExp]",k="[object Set]",E="[object String]",R="[object Symbol]",U="[object WeakMap]",T="[object ArrayBuffer]",W="[object DataView]",B="[object Float32Array]",C="[object Float64Array]",L="[object Int8Array]",P="[object Int16Array]",F="[object Int32Array]",$="[object Uint8Array]",D="[object Uint8ClampedArray]",M="[object Uint16Array]",N="[object Uint32Array]",q=/\b__p \+= '';/g,V=/\b(__p \+=) '' \+/g,Z=/(__e\(.*?\)|\b__t\)) \+\n'';/g,G=/&(?:amp|lt|gt|quot|#39);/g,K=/[&<>"']/g,H=RegExp(G.source),J=RegExp(K.source),Y=/<%-([\s\S]+?)%>/g,Q=/<%([\s\S]+?)%>/g,X=/<%=([\s\S]+?)%>/g,tt=/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/,nt=/^\w*$/,rt=/[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g,et=/[\\^$.*+?()[\]{}|]/g,ut=RegExp(et.source),ot=/^\s+/,it=/\s/,at=/\{(?:\n\/\* \[wrapped with .+\] \*\/)?\n?/,ct=/\{\n\/\* \[wrapped with (.+)\] \*/,ft=/,? & /,lt=/[^\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\x7f]+/g,st=/[()=,{}\[\]\/\s]/,pt=/\\(\\)?/g,ht=/\$\{([^\\}]*(?:\\.[^\\}]*)*)\}/g,vt=/\w*$/,_t=/^[-+]0x[0-9a-f]+$/i,gt=/^0b[01]+$/i,yt=/^\[object .+?Constructor\]$/,dt=/^0o[0-7]+$/i,bt=/^(?:0|[1-9]\d*)$/,wt=/[\xc0-\xd6\xd8-\xf6\xf8-\xff\u0100-\u017f]/g,jt=/($^)/,mt=/['\n\r\u2028\u2029\\]/g,xt="\\ud800-\\udfff",At="\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff",Ot="\\u2700-\\u27bf",zt="a-z\\xdf-\\xf6\\xf8-\\xff",St="A-Z\\xc0-\\xd6\\xd8-\\xde",It="\\ufe0e\\ufe0f",kt="\\xac\\xb1\\xd7\\xf7\\x00-\\x2f\\x3a-\\x40\\x5b-\\x60\\x7b-\\xbf\\u2000-\\u206f \\t\\x0b\\f\\xa0\\ufeff\\n\\r\\u2028\\u2029\\u1680\\u180e\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200a\\u202f\\u205f\\u3000",Et="['’]",Rt="["+xt+"]",Ut="["+kt+"]",Tt="["+At+"]",Wt="\\d+",Bt="["+Ot+"]",Ct="["+zt+"]",Lt="[^"+xt+kt+Wt+Ot+zt+St+"]",Pt="\\ud83c[\\udffb-\\udfff]",Ft="[^"+xt+"]",$t="(?:\\ud83c[\\udde6-\\uddff]){2}",Dt="[\\ud800-\\udbff][\\udc00-\\udfff]",Mt="["+St+"]",Nt="\\u200d",qt="(?:"+Ct+"|"+Lt+")",Vt="(?:"+Mt+"|"+Lt+")",Zt="(?:['’](?:d|ll|m|re|s|t|ve))?",Gt="(?:['’](?:D|LL|M|RE|S|T|VE))?",Kt="(?:"+Tt+"|"+Pt+")"+"?",Ht="["+It+"]?",Jt=Ht+Kt+("(?:"+Nt+"(?:"+[Ft,$t,Dt].join("|")+")"+Ht+Kt+")*"),Yt="(?:"+[Bt,$t,Dt].join("|")+")"+Jt,Qt="(?:"+[Ft+Tt+"?",Tt,$t,Dt,Rt].join("|")+")",Xt=RegExp(Et,"g"),tn=RegExp(Tt,"g"),nn=RegExp(Pt+"(?="+Pt+")|"+Qt+Jt,"g"),rn=RegExp([Mt+"?"+Ct+"+"+Zt+"(?="+[Ut,Mt,"$"].join("|")+")",Vt+"+"+Gt+"(?="+[Ut,Mt+qt,"$"].join("|")+")",Mt+"?"+qt+"+"+Zt,Mt+"+"+Gt,"\\d*(?:1ST|2ND|3RD|(?![123])\\dTH)(?=\\b|[a-z_])","\\d*(?:1st|2nd|3rd|(?![123])\\dth)(?=\\b|[A-Z_])",Wt,Yt].join("|"),"g"),en=RegExp("["+Nt+xt+At+It+"]"),un=/[a-z][A-Z]|[A-Z]{2}[a-z]|[0-9][a-zA-Z]|[a-zA-Z][0-9]|[^a-zA-Z0-9 ]/,on=["Array","Buffer","DataView","Date","Error","Float32Array","Float64Array","Function","Int8Array","Int16Array","Int32Array","Map","Math","Object","Promise","RegExp","Set","String","Symbol","TypeError","Uint8Array","Uint8ClampedArray","Uint16Array","Uint32Array","WeakMap","_","clearTimeout","isFinite","parseInt","setTimeout"],an=-1,cn={};cn[B]=cn[C]=cn[L]=cn[P]=cn[F]=cn[$]=cn[D]=cn[M]=cn[N]=!0,cn[y]=cn[d]=cn[T]=cn[b]=cn[W]=cn[w]=cn[j]=cn[m]=cn[A]=cn[O]=cn[z]=cn[I]=cn[k]=cn[E]=cn[U]=!1;var fn={};fn[y]=fn[d]=fn[T]=fn[W]=fn[b]=fn[w]=fn[B]=fn[C]=fn[L]=fn[P]=fn[F]=fn[A]=fn[O]=fn[z]=fn[I]=fn[k]=fn[E]=fn[R]=fn[$]=fn[D]=fn[M]=fn[N]=!0,fn[j]=fn[m]=fn[U]=!1;var ln={"\\":"\\","'":"'","\n":"n","\r":"r","\u2028":"u2028","\u2029":"u2029"},sn=parseFloat,pn=parseInt,hn="object"==typeof t&&t&&t.Object===Object&&t,vn="object"==typeof self&&self&&self.Object===Object&&self,_n=hn||vn||Function("return this")(),gn=r&&!r.nodeType&&r,yn=gn&&n&&!n.nodeType&&n,dn=yn&&yn.exports===gn,bn=dn&&hn.process,wn=function(){try{var t=yn&&yn.require&&yn.require("util").types;return t||bn&&bn.binding&&bn.binding("util")}catch(n){}}(),jn=wn&&wn.isArrayBuffer,mn=wn&&wn.isDate,xn=wn&&wn.isMap,An=wn&&wn.isRegExp,On=wn&&wn.isSet,zn=wn&&wn.isTypedArray;function Sn(t,n,r){switch(r.length){case 0:return t.call(n);case 1:return t.call(n,r[0]);case 2:return t.call(n,r[0],r[1]);case 3:return t.call(n,r[0],r[1],r[2])}return t.apply(n,r)}function In(t,n,r,e){for(var u=-1,o=null==t?0:t.length;++u-1}function Wn(t,n,r){for(var e=-1,u=null==t?0:t.length;++e-1;);return r}function er(t,n){for(var r=t.length;r--&&Nn(n,t[r],0)>-1;);return r}var ur=Kn({"À":"A","Á":"A","Â":"A","Ã":"A","Ä":"A","Å":"A","à":"a","á":"a","â":"a","ã":"a","ä":"a","å":"a","Ç":"C","ç":"c","Ð":"D","ð":"d","È":"E","É":"E","Ê":"E","Ë":"E","è":"e","é":"e","ê":"e","ë":"e","Ì":"I","Í":"I","Î":"I","Ï":"I","ì":"i","í":"i","î":"i","ï":"i","Ñ":"N","ñ":"n","Ò":"O","Ó":"O","Ô":"O","Õ":"O","Ö":"O","Ø":"O","ò":"o","ó":"o","ô":"o","õ":"o","ö":"o","ø":"o","Ù":"U","Ú":"U","Û":"U","Ü":"U","ù":"u","ú":"u","û":"u","ü":"u","Ý":"Y","ý":"y","ÿ":"y","Æ":"Ae","æ":"ae","Þ":"Th","þ":"th","ß":"ss","Ā":"A","Ă":"A","Ą":"A","ā":"a","ă":"a","ą":"a","Ć":"C","Ĉ":"C","Ċ":"C","Č":"C","ć":"c","ĉ":"c","ċ":"c","č":"c","Ď":"D","Đ":"D","ď":"d","đ":"d","Ē":"E","Ĕ":"E","Ė":"E","Ę":"E","Ě":"E","ē":"e","ĕ":"e","ė":"e","ę":"e","ě":"e","Ĝ":"G","Ğ":"G","Ġ":"G","Ģ":"G","ĝ":"g","ğ":"g","ġ":"g","ģ":"g","Ĥ":"H","Ħ":"H","ĥ":"h","ħ":"h","Ĩ":"I","Ī":"I","Ĭ":"I","Į":"I","İ":"I","ĩ":"i","ī":"i","ĭ":"i","į":"i","ı":"i","Ĵ":"J","ĵ":"j","Ķ":"K","ķ":"k","ĸ":"k","Ĺ":"L","Ļ":"L","Ľ":"L","Ŀ":"L","Ł":"L","ĺ":"l","ļ":"l","ľ":"l","ŀ":"l","ł":"l","Ń":"N","Ņ":"N","Ň":"N","Ŋ":"N","ń":"n","ņ":"n","ň":"n","ŋ":"n","Ō":"O","Ŏ":"O","Ő":"O","ō":"o","ŏ":"o","ő":"o","Ŕ":"R","Ŗ":"R","Ř":"R","ŕ":"r","ŗ":"r","ř":"r","Ś":"S","Ŝ":"S","Ş":"S","Š":"S","ś":"s","ŝ":"s","ş":"s","š":"s","Ţ":"T","Ť":"T","Ŧ":"T","ţ":"t","ť":"t","ŧ":"t","Ũ":"U","Ū":"U","Ŭ":"U","Ů":"U","Ű":"U","Ų":"U","ũ":"u","ū":"u","ŭ":"u","ů":"u","ű":"u","ų":"u","Ŵ":"W","ŵ":"w","Ŷ":"Y","ŷ":"y","Ÿ":"Y","Ź":"Z","Ż":"Z","Ž":"Z","ź":"z","ż":"z","ž":"z","IJ":"IJ","ij":"ij","Œ":"Oe","œ":"oe","ʼn":"'n","ſ":"s"}),or=Kn({"&":"&","<":"<",">":">",'"':""","'":"'"});function ir(t){return"\\"+ln[t]}function ar(t){return en.test(t)}function cr(t){var n=-1,r=Array(t.size);return t.forEach((function(t,e){r[++n]=[e,t]})),r}function fr(t,n){return function(r){return t(n(r))}}function lr(t,n){for(var r=-1,e=t.length,u=0,o=[];++r",""":'"',"'":"'"});var gr=function t(n){var r=(n=null==n?_n:gr.defaults(_n.Object(),n,gr.pick(_n,on))).Array,it=n.Date,xt=n.Error,At=n.Function,Ot=n.Math,zt=n.Object,St=n.RegExp,It=n.String,kt=n.TypeError,Et=r.prototype,Rt=At.prototype,Ut=zt.prototype,Tt=n["__core-js_shared__"],Wt=Rt.toString,Bt=Ut.hasOwnProperty,Ct=0,Lt=function(){var t=/[^.]+$/.exec(Tt&&Tt.keys&&Tt.keys.IE_PROTO||"");return t?"Symbol(src)_1."+t:""}(),Pt=Ut.toString,Ft=Wt.call(zt),$t=_n._,Dt=St("^"+Wt.call(Bt).replace(et,"\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,"$1.*?")+"$"),Mt=dn?n.Buffer:e,Nt=n.Symbol,qt=n.Uint8Array,Vt=Mt?Mt.allocUnsafe:e,Zt=fr(zt.getPrototypeOf,zt),Gt=zt.create,Kt=Ut.propertyIsEnumerable,Ht=Et.splice,Jt=Nt?Nt.isConcatSpreadable:e,Yt=Nt?Nt.iterator:e,Qt=Nt?Nt.toStringTag:e,nn=function(){try{var t=lo(zt,"defineProperty");return t({},"",{}),t}catch(n){}}(),en=n.clearTimeout!==_n.clearTimeout&&n.clearTimeout,ln=it&&it.now!==_n.Date.now&&it.now,hn=n.setTimeout!==_n.setTimeout&&n.setTimeout,vn=Ot.ceil,gn=Ot.floor,yn=zt.getOwnPropertySymbols,bn=Mt?Mt.isBuffer:e,wn=n.isFinite,$n=Et.join,Kn=fr(zt.keys,zt),yr=Ot.max,dr=Ot.min,br=it.now,wr=n.parseInt,jr=Ot.random,mr=Et.reverse,xr=lo(n,"DataView"),Ar=lo(n,"Map"),Or=lo(n,"Promise"),zr=lo(n,"Set"),Sr=lo(n,"WeakMap"),Ir=lo(zt,"create"),kr=Sr&&new Sr,Er={},Rr=$o(xr),Ur=$o(Ar),Tr=$o(Or),Wr=$o(zr),Br=$o(Sr),Cr=Nt?Nt.prototype:e,Lr=Cr?Cr.valueOf:e,Pr=Cr?Cr.toString:e;function Fr(t){if(ea(t)&&!Zi(t)&&!(t instanceof Nr)){if(t instanceof Mr)return t;if(Bt.call(t,"__wrapped__"))return Do(t)}return new Mr(t)}var $r=function(){function t(){}return function(n){if(!ra(n))return{};if(Gt)return Gt(n);t.prototype=n;var r=new t;return t.prototype=e,r}}();function Dr(){}function Mr(t,n){this.__wrapped__=t,this.__actions__=[],this.__chain__=!!n,this.__index__=0,this.__values__=e}function Nr(t){this.__wrapped__=t,this.__actions__=[],this.__dir__=1,this.__filtered__=!1,this.__iteratees__=[],this.__takeCount__=_,this.__views__=[]}function qr(t){var n=-1,r=null==t?0:t.length;for(this.clear();++n=n?t:n)),t}function ae(t,n,r,u,o,i){var a,c=1&n,f=2&n,l=4&n;if(r&&(a=o?r(t,u,o,i):r(t)),a!==e)return a;if(!ra(t))return t;var s=Zi(t);if(s){if(a=function(t){var n=t.length,r=new t.constructor(n);n&&"string"==typeof t[0]&&Bt.call(t,"index")&&(r.index=t.index,r.input=t.input);return r}(t),!c)return Iu(t,a)}else{var p=ho(t),h=p==m||p==x;if(Ji(t))return mu(t,c);if(p==z||p==y||h&&!o){if(a=f||h?{}:_o(t),!c)return f?function(t,n){return ku(t,po(t),n)}(t,function(t,n){return t&&ku(n,Wa(n),t)}(a,t)):function(t,n){return ku(t,so(t),n)}(t,ee(a,t))}else{if(!fn[p])return o?t:{};a=function(t,n,r){var e=t.constructor;switch(n){case T:return xu(t);case b:case w:return new e(+t);case W:return function(t,n){var r=n?xu(t.buffer):t.buffer;return new t.constructor(r,t.byteOffset,t.byteLength)}(t,r);case B:case C:case L:case P:case F:case $:case D:case M:case N:return Au(t,r);case A:return new e;case O:case E:return new e(t);case I:return function(t){var n=new t.constructor(t.source,vt.exec(t));return n.lastIndex=t.lastIndex,n}(t);case k:return new e;case R:return u=t,Lr?zt(Lr.call(u)):{}}var u}(t,p,c)}}i||(i=new Kr);var v=i.get(t);if(v)return v;i.set(t,a),ca(t)?t.forEach((function(e){a.add(ae(e,n,r,e,t,i))})):ua(t)&&t.forEach((function(e,u){a.set(u,ae(e,n,r,u,t,i))}));var _=s?e:(l?f?eo:ro:f?Wa:Ta)(t);return kn(_||t,(function(e,u){_&&(e=t[u=e]),te(a,u,ae(e,n,r,u,t,i))})),a}function ce(t,n,r){var u=r.length;if(null==t)return!u;for(t=zt(t);u--;){var o=r[u],i=n[o],a=t[o];if(a===e&&!(o in t)||!i(a))return!1}return!0}function fe(t,n,r){if("function"!=typeof t)throw new kt(u);return Eo((function(){t.apply(e,r)}),n)}function le(t,n,r,e){var u=-1,o=Tn,i=!0,a=t.length,c=[],f=n.length;if(!a)return c;r&&(n=Bn(n,Xn(r))),e?(o=Wn,i=!1):n.length>=200&&(o=nr,i=!1,n=new Gr(n));t:for(;++u-1},Vr.prototype.set=function(t,n){var r=this.__data__,e=ne(r,t);return e<0?(++this.size,r.push([t,n])):r[e][1]=n,this},Zr.prototype.clear=function(){this.size=0,this.__data__={hash:new qr,map:new(Ar||Vr),string:new qr}},Zr.prototype.delete=function(t){var n=co(this,t).delete(t);return this.size-=n?1:0,n},Zr.prototype.get=function(t){return co(this,t).get(t)},Zr.prototype.has=function(t){return co(this,t).has(t)},Zr.prototype.set=function(t,n){var r=co(this,t),e=r.size;return r.set(t,n),this.size+=r.size==e?0:1,this},Gr.prototype.add=Gr.prototype.push=function(t){return this.__data__.set(t,o),this},Gr.prototype.has=function(t){return this.__data__.has(t)},Kr.prototype.clear=function(){this.__data__=new Vr,this.size=0},Kr.prototype.delete=function(t){var n=this.__data__,r=n.delete(t);return this.size=n.size,r},Kr.prototype.get=function(t){return this.__data__.get(t)},Kr.prototype.has=function(t){return this.__data__.has(t)},Kr.prototype.set=function(t,n){var r=this.__data__;if(r instanceof Vr){var e=r.__data__;if(!Ar||e.length<199)return e.push([t,n]),this.size=++r.size,this;r=this.__data__=new Zr(e)}return r.set(t,n),this.size=r.size,this};var se=Uu(be),pe=Uu(we,!0);function he(t,n){var r=!0;return se(t,(function(t,e,u){return r=!!n(t,e,u)})),r}function ve(t,n,r){for(var u=-1,o=t.length;++u0&&r(a)?n>1?ge(a,n-1,r,e,u):Cn(u,a):e||(u[u.length]=a)}return u}var ye=Tu(),de=Tu(!0);function be(t,n){return t&&ye(t,n,Ta)}function we(t,n){return t&&de(t,n,Ta)}function je(t,n){return Un(n,(function(n){return Xi(t[n])}))}function me(t,n){for(var r=0,u=(n=du(n,t)).length;null!=t&&rn}function ze(t,n){return null!=t&&Bt.call(t,n)}function Se(t,n){return null!=t&&n in zt(t)}function Ie(t,n,u){for(var o=u?Wn:Tn,i=t[0].length,a=t.length,c=a,f=r(a),l=1/0,s=[];c--;){var p=t[c];c&&n&&(p=Bn(p,Xn(n))),l=dr(p.length,l),f[c]=!u&&(n||i>=120&&p.length>=120)?new Gr(c&&p):e}p=t[0];var h=-1,v=f[0];t:for(;++h=a?c:c*("desc"==r[e]?-1:1)}return t.index-n.index}(t,n,r)}))}function qe(t,n,r){for(var e=-1,u=n.length,o={};++e-1;)a!==t&&Ht.call(a,c,1),Ht.call(t,c,1);return t}function Ze(t,n){for(var r=t?n.length:0,e=r-1;r--;){var u=n[r];if(r==e||u!==o){var o=u;yo(u)?Ht.call(t,u,1):lu(t,u)}}return t}function Ge(t,n){return t+gn(jr()*(n-t+1))}function Ke(t,n){var r="";if(!t||n<1||n>h)return r;do{n%2&&(r+=t),(n=gn(n/2))&&(t+=t)}while(n);return r}function He(t,n){return Ro(zo(t,n,oc),t+"")}function Je(t){return Jr(Ma(t))}function Ye(t,n){var r=Ma(t);return Wo(r,ie(n,0,r.length))}function Qe(t,n,r,u){if(!ra(t))return t;for(var o=-1,i=(n=du(n,t)).length,a=i-1,c=t;null!=c&&++oo?0:o+n),(e=e>o?o:e)<0&&(e+=o),o=n>e?0:e-n>>>0,n>>>=0;for(var i=r(o);++u>>1,i=t[o];null!==i&&!la(i)&&(r?i<=n:i=200){var f=n?null:Ku(t);if(f)return sr(f);i=!1,u=nr,c=new Gr}else c=n?[]:a;t:for(;++e=u?t:ru(t,n,r)}var ju=en||function(t){return _n.clearTimeout(t)};function mu(t,n){if(n)return t.slice();var r=t.length,e=Vt?Vt(r):new t.constructor(r);return t.copy(e),e}function xu(t){var n=new t.constructor(t.byteLength);return new qt(n).set(new qt(t)),n}function Au(t,n){var r=n?xu(t.buffer):t.buffer;return new t.constructor(r,t.byteOffset,t.length)}function Ou(t,n){if(t!==n){var r=t!==e,u=null===t,o=t==t,i=la(t),a=n!==e,c=null===n,f=n==n,l=la(n);if(!c&&!l&&!i&&t>n||i&&a&&f&&!c&&!l||u&&a&&f||!r&&f||!o)return 1;if(!u&&!i&&!l&&t1?r[o-1]:e,a=o>2?r[2]:e;for(i=t.length>3&&"function"==typeof i?(o--,i):e,a&&bo(r[0],r[1],a)&&(i=o<3?e:i,o=1),n=zt(n);++u-1?o[i?n[a]:a]:e}}function Pu(t){return no((function(n){var r=n.length,o=r,i=Mr.prototype.thru;for(t&&n.reverse();o--;){var a=n[o];if("function"!=typeof a)throw new kt(u);if(i&&!c&&"wrapper"==oo(a))var c=new Mr([],!0)}for(o=c?o:r;++o1&&w.reverse(),h&&sc))return!1;var l=i.get(t),s=i.get(n);if(l&&s)return l==n&&s==t;var p=-1,h=!0,v=2&r?new Gr:e;for(i.set(t,n),i.set(n,t);++p-1&&t%1==0&&t1?"& ":"")+n[e],n=n.join(r>2?", ":" "),t.replace(at,"{\n/* [wrapped with "+n+"] */\n")}(e,function(t,n){return kn(g,(function(r){var e="_."+r[0];n&r[1]&&!Tn(t,e)&&t.push(e)})),t.sort()}(function(t){var n=t.match(ct);return n?n[1].split(ft):[]}(e),r)))}function To(t){var n=0,r=0;return function(){var u=br(),o=16-(u-r);if(r=u,o>0){if(++n>=800)return arguments[0]}else n=0;return t.apply(e,arguments)}}function Wo(t,n){var r=-1,u=t.length,o=u-1;for(n=n===e?u:n;++r1?t[n-1]:e;return r="function"==typeof r?(t.pop(),r):e,ai(t,r)}));function vi(t){var n=Fr(t);return n.__chain__=!0,n}function _i(t,n){return n(t)}var gi=no((function(t){var n=t.length,r=n?t[0]:0,u=this.__wrapped__,o=function(n){return oe(n,t)};return!(n>1||this.__actions__.length)&&u instanceof Nr&&yo(r)?((u=u.slice(r,+r+(n?1:0))).__actions__.push({func:_i,args:[o],thisArg:e}),new Mr(u,this.__chain__).thru((function(t){return n&&!t.length&&t.push(e),t}))):this.thru(o)}));var yi=Eu((function(t,n,r){Bt.call(t,r)?++t[r]:ue(t,r,1)}));var di=Lu(Vo),bi=Lu(Zo);function wi(t,n){return(Zi(t)?kn:se)(t,ao(n,3))}function ji(t,n){return(Zi(t)?En:pe)(t,ao(n,3))}var mi=Eu((function(t,n,r){Bt.call(t,r)?t[r].push(n):ue(t,r,[n])}));var xi=He((function(t,n,e){var u=-1,o="function"==typeof n,i=Ki(t)?r(t.length):[];return se(t,(function(t){i[++u]=o?Sn(n,t,e):ke(t,n,e)})),i})),Ai=Eu((function(t,n,r){ue(t,r,n)}));function Oi(t,n){return(Zi(t)?Bn:Pe)(t,ao(n,3))}var zi=Eu((function(t,n,r){t[r?0:1].push(n)}),(function(){return[[],[]]}));var Si=He((function(t,n){if(null==t)return[];var r=n.length;return r>1&&bo(t,n[0],n[1])?n=[]:r>2&&bo(n[0],n[1],n[2])&&(n=[n[0]]),Ne(t,ge(n,1),[])})),Ii=ln||function(){return _n.Date.now()};function ki(t,n,r){return n=r?e:n,n=t&&null==n?t.length:n,Ju(t,l,e,e,e,e,n)}function Ei(t,n){var r;if("function"!=typeof n)throw new kt(u);return t=ga(t),function(){return--t>0&&(r=n.apply(this,arguments)),t<=1&&(n=e),r}}var Ri=He((function(t,n,r){var e=1;if(r.length){var u=lr(r,io(Ri));e|=c}return Ju(t,e,n,r,u)})),Ui=He((function(t,n,r){var e=3;if(r.length){var u=lr(r,io(Ui));e|=c}return Ju(n,e,t,r,u)}));function Ti(t,n,r){var o,i,a,c,f,l,s=0,p=!1,h=!1,v=!0;if("function"!=typeof t)throw new kt(u);function _(n){var r=o,u=i;return o=i=e,s=n,c=t.apply(u,r)}function g(t){var r=t-l;return l===e||r>=n||r<0||h&&t-s>=a}function y(){var t=Ii();if(g(t))return d(t);f=Eo(y,function(t){var r=n-(t-l);return h?dr(r,a-(t-s)):r}(t))}function d(t){return f=e,v&&o?_(t):(o=i=e,c)}function b(){var t=Ii(),r=g(t);if(o=arguments,i=this,l=t,r){if(f===e)return function(t){return s=t,f=Eo(y,n),p?_(t):c}(l);if(h)return ju(f),f=Eo(y,n),_(l)}return f===e&&(f=Eo(y,n)),c}return n=da(n)||0,ra(r)&&(p=!!r.leading,a=(h="maxWait"in r)?yr(da(r.maxWait)||0,n):a,v="trailing"in r?!!r.trailing:v),b.cancel=function(){f!==e&&ju(f),s=0,o=l=i=f=e},b.flush=function(){return f===e?c:d(Ii())},b}var Wi=He((function(t,n){return fe(t,1,n)})),Bi=He((function(t,n,r){return fe(t,da(n)||0,r)}));function Ci(t,n){if("function"!=typeof t||null!=n&&"function"!=typeof n)throw new kt(u);var r=function(){var e=arguments,u=n?n.apply(this,e):e[0],o=r.cache;if(o.has(u))return o.get(u);var i=t.apply(this,e);return r.cache=o.set(u,i)||o,i};return r.cache=new(Ci.Cache||Zr),r}function Li(t){if("function"!=typeof t)throw new kt(u);return function(){var n=arguments;switch(n.length){case 0:return!t.call(this);case 1:return!t.call(this,n[0]);case 2:return!t.call(this,n[0],n[1]);case 3:return!t.call(this,n[0],n[1],n[2])}return!t.apply(this,n)}}Ci.Cache=Zr;var Pi=bu((function(t,n){var r=(n=1==n.length&&Zi(n[0])?Bn(n[0],Xn(ao())):Bn(ge(n,1),Xn(ao()))).length;return He((function(e){for(var u=-1,o=dr(e.length,r);++u=n})),Vi=Ee(function(){return arguments}())?Ee:function(t){return ea(t)&&Bt.call(t,"callee")&&!Kt.call(t,"callee")},Zi=r.isArray,Gi=jn?Xn(jn):function(t){return ea(t)&&Ae(t)==T};function Ki(t){return null!=t&&na(t.length)&&!Xi(t)}function Hi(t){return ea(t)&&Ki(t)}var Ji=bn||dc,Yi=mn?Xn(mn):function(t){return ea(t)&&Ae(t)==w};function Qi(t){if(!ea(t))return!1;var n=Ae(t);return n==j||"[object DOMException]"==n||"string"==typeof t.message&&"string"==typeof t.name&&!ia(t)}function Xi(t){if(!ra(t))return!1;var n=Ae(t);return n==m||n==x||"[object AsyncFunction]"==n||"[object Proxy]"==n}function ta(t){return"number"==typeof t&&t==ga(t)}function na(t){return"number"==typeof t&&t>-1&&t%1==0&&t<=h}function ra(t){var n=typeof t;return null!=t&&("object"==n||"function"==n)}function ea(t){return null!=t&&"object"==typeof t}var ua=xn?Xn(xn):function(t){return ea(t)&&ho(t)==A};function oa(t){return"number"==typeof t||ea(t)&&Ae(t)==O}function ia(t){if(!ea(t)||Ae(t)!=z)return!1;var n=Zt(t);if(null===n)return!0;var r=Bt.call(n,"constructor")&&n.constructor;return"function"==typeof r&&r instanceof r&&Wt.call(r)==Ft}var aa=An?Xn(An):function(t){return ea(t)&&Ae(t)==I};var ca=On?Xn(On):function(t){return ea(t)&&ho(t)==k};function fa(t){return"string"==typeof t||!Zi(t)&&ea(t)&&Ae(t)==E}function la(t){return"symbol"==typeof t||ea(t)&&Ae(t)==R}var sa=zn?Xn(zn):function(t){return ea(t)&&na(t.length)&&!!cn[Ae(t)]};var pa=Vu(Le),ha=Vu((function(t,n){return t<=n}));function va(t){if(!t)return[];if(Ki(t))return fa(t)?hr(t):Iu(t);if(Yt&&t[Yt])return function(t){for(var n,r=[];!(n=t.next()).done;)r.push(n.value);return r}(t[Yt]());var n=ho(t);return(n==A?cr:n==k?sr:Ma)(t)}function _a(t){return t?(t=da(t))===p||t===-1/0?17976931348623157e292*(t<0?-1:1):t==t?t:0:0===t?t:0}function ga(t){var n=_a(t),r=n%1;return n==n?r?n-r:n:0}function ya(t){return t?ie(ga(t),0,_):0}function da(t){if("number"==typeof t)return t;if(la(t))return v;if(ra(t)){var n="function"==typeof t.valueOf?t.valueOf():t;t=ra(n)?n+"":n}if("string"!=typeof t)return 0===t?t:+t;t=Qn(t);var r=gt.test(t);return r||dt.test(t)?pn(t.slice(2),r?2:8):_t.test(t)?v:+t}function ba(t){return ku(t,Wa(t))}function wa(t){return null==t?"":cu(t)}var ja=Ru((function(t,n){if(xo(n)||Ki(n))ku(n,Ta(n),t);else for(var r in n)Bt.call(n,r)&&te(t,r,n[r])})),ma=Ru((function(t,n){ku(n,Wa(n),t)})),xa=Ru((function(t,n,r,e){ku(n,Wa(n),t,e)})),Aa=Ru((function(t,n,r,e){ku(n,Ta(n),t,e)})),Oa=no(oe);var za=He((function(t,n){t=zt(t);var r=-1,u=n.length,o=u>2?n[2]:e;for(o&&bo(n[0],n[1],o)&&(u=1);++r1),n})),ku(t,eo(t),r),e&&(r=ae(r,7,Xu));for(var u=n.length;u--;)lu(r,n[u]);return r}));var Pa=no((function(t,n){return null==t?{}:function(t,n){return qe(t,n,(function(n,r){return ka(t,r)}))}(t,n)}));function Fa(t,n){if(null==t)return{};var r=Bn(eo(t),(function(t){return[t]}));return n=ao(n),qe(t,r,(function(t,r){return n(t,r[0])}))}var $a=Hu(Ta),Da=Hu(Wa);function Ma(t){return null==t?[]:tr(t,Ta(t))}var Na=Bu((function(t,n,r){return n=n.toLowerCase(),t+(r?qa(n):n)}));function qa(t){return Qa(wa(t).toLowerCase())}function Va(t){return(t=wa(t))&&t.replace(wt,ur).replace(tn,"")}var Za=Bu((function(t,n,r){return t+(r?"-":"")+n.toLowerCase()})),Ga=Bu((function(t,n,r){return t+(r?" ":"")+n.toLowerCase()})),Ka=Wu("toLowerCase");var Ha=Bu((function(t,n,r){return t+(r?"_":"")+n.toLowerCase()}));var Ja=Bu((function(t,n,r){return t+(r?" ":"")+Qa(n)}));var Ya=Bu((function(t,n,r){return t+(r?" ":"")+n.toUpperCase()})),Qa=Wu("toUpperCase");function Xa(t,n,r){return t=wa(t),(n=r?e:n)===e?function(t){return un.test(t)}(t)?function(t){return t.match(rn)||[]}(t):function(t){return t.match(lt)||[]}(t):t.match(n)||[]}var tc=He((function(t,n){try{return Sn(t,e,n)}catch(r){return Qi(r)?r:new xt(r)}})),nc=no((function(t,n){return kn(n,(function(n){n=Fo(n),ue(t,n,Ri(t[n],t))})),t}));function rc(t){return function(){return t}}var ec=Pu(),uc=Pu(!0);function oc(t){return t}function ic(t){return We("function"==typeof t?t:ae(t,1))}var ac=He((function(t,n){return function(r){return ke(r,t,n)}})),cc=He((function(t,n){return function(r){return ke(t,r,n)}}));function fc(t,n,r){var e=Ta(n),u=je(n,e);null!=r||ra(n)&&(u.length||!e.length)||(r=n,n=t,t=this,u=je(n,Ta(n)));var o=!(ra(r)&&"chain"in r&&!r.chain),i=Xi(t);return kn(u,(function(r){var e=n[r];t[r]=e,i&&(t.prototype[r]=function(){var n=this.__chain__;if(o||n){var r=t(this.__wrapped__);return(r.__actions__=Iu(this.__actions__)).push({func:e,args:arguments,thisArg:t}),r.__chain__=n,r}return e.apply(t,Cn([this.value()],arguments))})})),t}function lc(){}var sc=Mu(Bn),pc=Mu(Rn),hc=Mu(Fn);function vc(t){return wo(t)?Gn(Fo(t)):function(t){return function(n){return me(n,t)}}(t)}var _c=qu(),gc=qu(!0);function yc(){return[]}function dc(){return!1}var bc=Du((function(t,n){return t+n}),0),wc=Gu("ceil"),jc=Du((function(t,n){return t/n}),1),mc=Gu("floor");var xc,Ac=Du((function(t,n){return t*n}),1),Oc=Gu("round"),zc=Du((function(t,n){return t-n}),0);return Fr.after=function(t,n){if("function"!=typeof n)throw new kt(u);return t=ga(t),function(){if(--t<1)return n.apply(this,arguments)}},Fr.ary=ki,Fr.assign=ja,Fr.assignIn=ma,Fr.assignInWith=xa,Fr.assignWith=Aa,Fr.at=Oa,Fr.before=Ei,Fr.bind=Ri,Fr.bindAll=nc,Fr.bindKey=Ui,Fr.castArray=function(){if(!arguments.length)return[];var t=arguments[0];return Zi(t)?t:[t]},Fr.chain=vi,Fr.chunk=function(t,n,u){n=(u?bo(t,n,u):n===e)?1:yr(ga(n),0);var o=null==t?0:t.length;if(!o||n<1)return[];for(var i=0,a=0,c=r(vn(o/n));io?0:o+r),(u=u===e||u>o?o:ga(u))<0&&(u+=o),u=r>u?0:ya(u);r>>0)?(t=wa(t))&&("string"==typeof n||null!=n&&!aa(n))&&!(n=cu(n))&&ar(t)?wu(hr(t),0,r):t.split(n,r):[]},Fr.spread=function(t,n){if("function"!=typeof t)throw new kt(u);return n=null==n?0:yr(ga(n),0),He((function(r){var e=r[n],u=wu(r,0,n);return e&&Cn(u,e),Sn(t,this,u)}))},Fr.tail=function(t){var n=null==t?0:t.length;return n?ru(t,1,n):[]},Fr.take=function(t,n,r){return t&&t.length?ru(t,0,(n=r||n===e?1:ga(n))<0?0:n):[]},Fr.takeRight=function(t,n,r){var u=null==t?0:t.length;return u?ru(t,(n=u-(n=r||n===e?1:ga(n)))<0?0:n,u):[]},Fr.takeRightWhile=function(t,n){return t&&t.length?pu(t,ao(n,3),!1,!0):[]},Fr.takeWhile=function(t,n){return t&&t.length?pu(t,ao(n,3)):[]},Fr.tap=function(t,n){return n(t),t},Fr.throttle=function(t,n,r){var e=!0,o=!0;if("function"!=typeof t)throw new kt(u);return ra(r)&&(e="leading"in r?!!r.leading:e,o="trailing"in r?!!r.trailing:o),Ti(t,n,{leading:e,maxWait:n,trailing:o})},Fr.thru=_i,Fr.toArray=va,Fr.toPairs=$a,Fr.toPairsIn=Da,Fr.toPath=function(t){return Zi(t)?Bn(t,Fo):la(t)?[t]:Iu(Po(wa(t)))},Fr.toPlainObject=ba,Fr.transform=function(t,n,r){var e=Zi(t),u=e||Ji(t)||sa(t);if(n=ao(n,4),null==r){var o=t&&t.constructor;r=u?e?new o:[]:ra(t)&&Xi(o)?$r(Zt(t)):{}}return(u?kn:be)(t,(function(t,e,u){return n(r,t,e,u)})),r},Fr.unary=function(t){return ki(t,1)},Fr.union=ei,Fr.unionBy=ui,Fr.unionWith=oi,Fr.uniq=function(t){return t&&t.length?fu(t):[]},Fr.uniqBy=function(t,n){return t&&t.length?fu(t,ao(n,2)):[]},Fr.uniqWith=function(t,n){return n="function"==typeof n?n:e,t&&t.length?fu(t,e,n):[]},Fr.unset=function(t,n){return null==t||lu(t,n)},Fr.unzip=ii,Fr.unzipWith=ai,Fr.update=function(t,n,r){return null==t?t:su(t,n,yu(r))},Fr.updateWith=function(t,n,r,u){return u="function"==typeof u?u:e,null==t?t:su(t,n,yu(r),u)},Fr.values=Ma,Fr.valuesIn=function(t){return null==t?[]:tr(t,Wa(t))},Fr.without=ci,Fr.words=Xa,Fr.wrap=function(t,n){return Fi(yu(n),t)},Fr.xor=fi,Fr.xorBy=li,Fr.xorWith=si,Fr.zip=pi,Fr.zipObject=function(t,n){return _u(t||[],n||[],te)},Fr.zipObjectDeep=function(t,n){return _u(t||[],n||[],Qe)},Fr.zipWith=hi,Fr.entries=$a,Fr.entriesIn=Da,Fr.extend=ma,Fr.extendWith=xa,fc(Fr,Fr),Fr.add=bc,Fr.attempt=tc,Fr.camelCase=Na,Fr.capitalize=qa,Fr.ceil=wc,Fr.clamp=function(t,n,r){return r===e&&(r=n,n=e),r!==e&&(r=(r=da(r))==r?r:0),n!==e&&(n=(n=da(n))==n?n:0),ie(da(t),n,r)},Fr.clone=function(t){return ae(t,4)},Fr.cloneDeep=function(t){return ae(t,5)},Fr.cloneDeepWith=function(t,n){return ae(t,5,n="function"==typeof n?n:e)},Fr.cloneWith=function(t,n){return ae(t,4,n="function"==typeof n?n:e)},Fr.conformsTo=function(t,n){return null==n||ce(t,n,Ta(n))},Fr.deburr=Va,Fr.defaultTo=function(t,n){return null==t||t!=t?n:t},Fr.divide=jc,Fr.endsWith=function(t,n,r){t=wa(t),n=cu(n);var u=t.length,o=r=r===e?u:ie(ga(r),0,u);return(r-=n.length)>=0&&t.slice(r,o)==n},Fr.eq=Mi,Fr.escape=function(t){return(t=wa(t))&&J.test(t)?t.replace(K,or):t},Fr.escapeRegExp=function(t){return(t=wa(t))&&ut.test(t)?t.replace(et,"\\$&"):t},Fr.every=function(t,n,r){var u=Zi(t)?Rn:he;return r&&bo(t,n,r)&&(n=e),u(t,ao(n,3))},Fr.find=di,Fr.findIndex=Vo,Fr.findKey=function(t,n){return Dn(t,ao(n,3),be)},Fr.findLast=bi,Fr.findLastIndex=Zo,Fr.findLastKey=function(t,n){return Dn(t,ao(n,3),we)},Fr.floor=mc,Fr.forEach=wi,Fr.forEachRight=ji,Fr.forIn=function(t,n){return null==t?t:ye(t,ao(n,3),Wa)},Fr.forInRight=function(t,n){return null==t?t:de(t,ao(n,3),Wa)},Fr.forOwn=function(t,n){return t&&be(t,ao(n,3))},Fr.forOwnRight=function(t,n){return t&&we(t,ao(n,3))},Fr.get=Ia,Fr.gt=Ni,Fr.gte=qi,Fr.has=function(t,n){return null!=t&&vo(t,n,ze)},Fr.hasIn=ka,Fr.head=Ko,Fr.identity=oc,Fr.includes=function(t,n,r,e){t=Ki(t)?t:Ma(t),r=r&&!e?ga(r):0;var u=t.length;return r<0&&(r=yr(u+r,0)),fa(t)?r<=u&&t.indexOf(n,r)>-1:!!u&&Nn(t,n,r)>-1},Fr.indexOf=function(t,n,r){var e=null==t?0:t.length;if(!e)return-1;var u=null==r?0:ga(r);return u<0&&(u=yr(e+u,0)),Nn(t,n,u)},Fr.inRange=function(t,n,r){return n=_a(n),r===e?(r=n,n=0):r=_a(r),function(t,n,r){return t>=dr(n,r)&&t=-9007199254740991&&t<=h},Fr.isSet=ca,Fr.isString=fa,Fr.isSymbol=la,Fr.isTypedArray=sa,Fr.isUndefined=function(t){return t===e},Fr.isWeakMap=function(t){return ea(t)&&ho(t)==U},Fr.isWeakSet=function(t){return ea(t)&&"[object WeakSet]"==Ae(t)},Fr.join=function(t,n){return null==t?"":$n.call(t,n)},Fr.kebabCase=Za,Fr.last=Qo,Fr.lastIndexOf=function(t,n,r){var u=null==t?0:t.length;if(!u)return-1;var o=u;return r!==e&&(o=(o=ga(r))<0?yr(u+o,0):dr(o,u-1)),n==n?function(t,n,r){for(var e=r+1;e--;)if(t[e]===n)return e;return e}(t,n,o):Mn(t,Vn,o,!0)},Fr.lowerCase=Ga,Fr.lowerFirst=Ka,Fr.lt=pa,Fr.lte=ha,Fr.max=function(t){return t&&t.length?ve(t,oc,Oe):e},Fr.maxBy=function(t,n){return t&&t.length?ve(t,ao(n,2),Oe):e},Fr.mean=function(t){return Zn(t,oc)},Fr.meanBy=function(t,n){return Zn(t,ao(n,2))},Fr.min=function(t){return t&&t.length?ve(t,oc,Le):e},Fr.minBy=function(t,n){return t&&t.length?ve(t,ao(n,2),Le):e},Fr.stubArray=yc,Fr.stubFalse=dc,Fr.stubObject=function(){return{}},Fr.stubString=function(){return""},Fr.stubTrue=function(){return!0},Fr.multiply=Ac,Fr.nth=function(t,n){return t&&t.length?Me(t,ga(n)):e},Fr.noConflict=function(){return _n._===this&&(_n._=$t),this},Fr.noop=lc,Fr.now=Ii,Fr.pad=function(t,n,r){t=wa(t);var e=(n=ga(n))?pr(t):0;if(!n||e>=n)return t;var u=(n-e)/2;return Nu(gn(u),r)+t+Nu(vn(u),r)},Fr.padEnd=function(t,n,r){t=wa(t);var e=(n=ga(n))?pr(t):0;return n&&en){var u=t;t=n,n=u}if(r||t%1||n%1){var o=jr();return dr(t+o*(n-t+sn("1e-"+((o+"").length-1))),n)}return Ge(t,n)},Fr.reduce=function(t,n,r){var e=Zi(t)?Ln:Hn,u=arguments.length<3;return e(t,ao(n,4),r,u,se)},Fr.reduceRight=function(t,n,r){var e=Zi(t)?Pn:Hn,u=arguments.length<3;return e(t,ao(n,4),r,u,pe)},Fr.repeat=function(t,n,r){return n=(r?bo(t,n,r):n===e)?1:ga(n),Ke(wa(t),n)},Fr.replace=function(){var t=arguments,n=wa(t[0]);return t.length<3?n:n.replace(t[1],t[2])},Fr.result=function(t,n,r){var u=-1,o=(n=du(n,t)).length;for(o||(o=1,t=e);++uh)return[];var r=_,e=dr(t,_);n=ao(n),t-=_;for(var u=Yn(e,n);++r=i)return t;var c=r-pr(u);if(c<1)return u;var f=a?wu(a,0,c).join(""):t.slice(0,c);if(o===e)return f+u;if(a&&(c+=f.length-c),aa(o)){if(t.slice(c).search(o)){var l,s=f;for(o.global||(o=St(o.source,wa(vt.exec(o))+"g")),o.lastIndex=0;l=o.exec(s);)var p=l.index;f=f.slice(0,p===e?c:p)}}else if(t.indexOf(cu(o),c)!=c){var h=f.lastIndexOf(o);h>-1&&(f=f.slice(0,h))}return f+u},Fr.unescape=function(t){return(t=wa(t))&&H.test(t)?t.replace(G,_r):t},Fr.uniqueId=function(t){var n=++Ct;return wa(t)+n},Fr.upperCase=Ya,Fr.upperFirst=Qa,Fr.each=wi,Fr.eachRight=ji,Fr.first=Ko,fc(Fr,(xc={},be(Fr,(function(t,n){Bt.call(Fr.prototype,n)||(xc[n]=t)})),xc),{chain:!1}),Fr.VERSION="4.17.21",kn(["bind","bindKey","curry","curryRight","partial","partialRight"],(function(t){Fr[t].placeholder=Fr})),kn(["drop","take"],(function(t,n){Nr.prototype[t]=function(r){r=r===e?1:yr(ga(r),0);var u=this.__filtered__&&!n?new Nr(this):this.clone();return u.__filtered__?u.__takeCount__=dr(r,u.__takeCount__):u.__views__.push({size:dr(r,_),type:t+(u.__dir__<0?"Right":"")}),u},Nr.prototype[t+"Right"]=function(n){return this.reverse()[t](n).reverse()}})),kn(["filter","map","takeWhile"],(function(t,n){var r=n+1,e=1==r||3==r;Nr.prototype[t]=function(t){var n=this.clone();return n.__iteratees__.push({iteratee:ao(t,3),type:r}),n.__filtered__=n.__filtered__||e,n}})),kn(["head","last"],(function(t,n){var r="take"+(n?"Right":"");Nr.prototype[t]=function(){return this[r](1).value()[0]}})),kn(["initial","tail"],(function(t,n){var r="drop"+(n?"":"Right");Nr.prototype[t]=function(){return this.__filtered__?new Nr(this):this[r](1)}})),Nr.prototype.compact=function(){return this.filter(oc)},Nr.prototype.find=function(t){return this.filter(t).head()},Nr.prototype.findLast=function(t){return this.reverse().find(t)},Nr.prototype.invokeMap=He((function(t,n){return"function"==typeof t?new Nr(this):this.map((function(r){return ke(r,t,n)}))})),Nr.prototype.reject=function(t){return this.filter(Li(ao(t)))},Nr.prototype.slice=function(t,n){t=ga(t);var r=this;return r.__filtered__&&(t>0||n<0)?new Nr(r):(t<0?r=r.takeRight(-t):t&&(r=r.drop(t)),n!==e&&(r=(n=ga(n))<0?r.dropRight(-n):r.take(n-t)),r)},Nr.prototype.takeRightWhile=function(t){return this.reverse().takeWhile(t).reverse()},Nr.prototype.toArray=function(){return this.take(_)},be(Nr.prototype,(function(t,n){var r=/^(?:filter|find|map|reject)|While$/.test(n),u=/^(?:head|last)$/.test(n),o=Fr[u?"take"+("last"==n?"Right":""):n],i=u||/^find/.test(n);o&&(Fr.prototype[n]=function(){var n=this.__wrapped__,a=u?[1]:arguments,c=n instanceof Nr,f=a[0],l=c||Zi(n),s=function(t){var n=o.apply(Fr,Cn([t],a));return u&&p?n[0]:n};l&&r&&"function"==typeof f&&1!=f.length&&(c=l=!1);var p=this.__chain__,h=!!this.__actions__.length,v=i&&!p,_=c&&!h;if(!i&&l){n=_?n:new Nr(this);var g=t.apply(n,a);return g.__actions__.push({func:_i,args:[s],thisArg:e}),new Mr(g,p)}return v&&_?t.apply(this,a):(g=this.thru(s),v?u?g.value()[0]:g.value():g)})})),kn(["pop","push","shift","sort","splice","unshift"],(function(t){var n=Et[t],r=/^(?:push|sort|unshift)$/.test(t)?"tap":"thru",e=/^(?:pop|shift)$/.test(t);Fr.prototype[t]=function(){var t=arguments;if(e&&!this.__chain__){var u=this.value();return n.apply(Zi(u)?u:[],t)}return this[r]((function(r){return n.apply(Zi(r)?r:[],t)}))}})),be(Nr.prototype,(function(t,n){var r=Fr[n];if(r){var e=r.name+"";Bt.call(Er,e)||(Er[e]=[]),Er[e].push({name:n,func:r})}})),Er[Fu(e,2).name]=[{name:"wrapper",func:e}],Nr.prototype.clone=function(){var t=new Nr(this.__wrapped__);return t.__actions__=Iu(this.__actions__),t.__dir__=this.__dir__,t.__filtered__=this.__filtered__,t.__iteratees__=Iu(this.__iteratees__),t.__takeCount__=this.__takeCount__,t.__views__=Iu(this.__views__),t},Nr.prototype.reverse=function(){if(this.__filtered__){var t=new Nr(this);t.__dir__=-1,t.__filtered__=!0}else(t=this.clone()).__dir__*=-1;return t},Nr.prototype.value=function(){var t=this.__wrapped__.value(),n=this.__dir__,r=Zi(t),e=n<0,u=r?t.length:0,o=function(t,n,r){var e=-1,u=r.length;for(;++e=this.__values__.length;return{done:t,value:t?e:this.__values__[this.__index__++]}},Fr.prototype.plant=function(t){for(var n,r=this;r instanceof Dr;){var u=Do(r);u.__index__=0,u.__values__=e,n?o.__wrapped__=u:n=u;var o=u;r=r.__wrapped__}return o.__wrapped__=t,n},Fr.prototype.reverse=function(){var t=this.__wrapped__;if(t instanceof Nr){var n=t;return this.__actions__.length&&(n=new Nr(this)),(n=n.reverse()).__actions__.push({func:_i,args:[ri],thisArg:e}),new Mr(n,this.__chain__)}return this.thru(ri)},Fr.prototype.toJSON=Fr.prototype.valueOf=Fr.prototype.value=function(){return hu(this.__wrapped__,this.__actions__)},Fr.prototype.first=Fr.prototype.head,Yt&&(Fr.prototype[Yt]=function(){return this}),Fr}();yn?((yn.exports=gr)._=gr,gn._=gr):_n._=gr}).call(t)}(Bu,Bu.exports);export{Wu as c}; diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v2/assets/primeuix-CNMLIuxO.js b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v2/assets/primeuix-CNMLIuxO.js new file mode 100644 index 0000000000000000000000000000000000000000..12144472b75f9e4c8f01201833fa299639df4e7b --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v2/assets/primeuix-CNMLIuxO.js @@ -0,0 +1 @@ +var e=Object.defineProperty,t=Object.getOwnPropertySymbols,n=Object.prototype.hasOwnProperty,r=Object.prototype.propertyIsEnumerable,s=(t,n,r)=>n in t?e(t,n,{enumerable:!0,configurable:!0,writable:!0,value:r}):t[n]=r;function o(e){return null==e||""===e||Array.isArray(e)&&0===e.length||!(e instanceof Date)&&"object"==typeof e&&0===Object.keys(e).length}function i(e,t,n=new WeakSet){if(e===t)return!0;if(!e||!t||"object"!=typeof e||"object"!=typeof t)return!1;if(n.has(e)||n.has(t))return!1;n.add(e).add(t);let r,s,o,a=Array.isArray(e),l=Array.isArray(t);if(a&&l){if(s=e.length,s!=t.length)return!1;for(r=s;0!=r--;)if(!i(e[r],t[r],n))return!1;return!0}if(a!=l)return!1;let u=e instanceof Date,c=t instanceof Date;if(u!=c)return!1;if(u&&c)return e.getTime()==t.getTime();let d=e instanceof RegExp,h=t instanceof RegExp;if(d!=h)return!1;if(d&&h)return e.toString()==t.toString();let f=Object.keys(e);if(s=f.length,s!==Object.keys(t).length)return!1;for(r=s;0!=r--;)if(!Object.prototype.hasOwnProperty.call(t,f[r]))return!1;for(r=s;0!=r--;)if(o=f[r],!i(e[o],t[o],n))return!1;return!0}function a(e,t){return i(e,t)}function l(e){return!!(e&&e.constructor&&e.call&&e.apply)}function u(e){return!o(e)}function c(e,t){if(!e||!t)return null;try{const n=e[t];if(u(n))return n}catch(n){}if(Object.keys(e).length){if(l(t))return t(e);if(-1===t.indexOf("."))return e[t];{let n=t.split("."),r=e;for(let e=0,t=n.length;ey(e)===s))||""],n),r.join("."),n):void 0:m(e,n)}function b(e,t=!0){return Array.isArray(e)&&(t||0!==e.length)}function S(e=""){return u(e)&&1===e.length&&!!e.match(/\S| /)}function x(e,t){if(t){const n=t.test(e);return t.lastIndex=0,n}return!1}function $(...e){const o=(e={},i={})=>{const a=((e,o)=>{for(var i in o||(o={}))n.call(o,i)&&s(e,i,o[i]);if(t)for(var i of t(o))r.call(o,i)&&s(e,i,o[i]);return e})({},e);return Object.keys(i).forEach((t=>{p(i[t])&&t in e&&p(e[t])?a[t]=o(e[t],i[t]):a[t]=i[t]})),a};return e.reduce(((e,t,n)=>0===n?t:o(e,t)),{})}function k(e){return e?e.replace(/\/\*(?:(?!\*\/)[\s\S])*\*\/|[\r\n\t]+/g,"").replace(/ {2,}/g," ").replace(/ ([{:}]) /g,"$1").replace(/([;,]) /g,"$1").replace(/ !/g,"!").replace(/: /g,":"):e}function w(e){if(e&&/[\xC0-\xFF\u0100-\u017E]/.test(e)){const t={A:/[\xC0-\xC5\u0100\u0102\u0104]/g,AE:/[\xC6]/g,C:/[\xC7\u0106\u0108\u010A\u010C]/g,D:/[\xD0\u010E\u0110]/g,E:/[\xC8-\xCB\u0112\u0114\u0116\u0118\u011A]/g,G:/[\u011C\u011E\u0120\u0122]/g,H:/[\u0124\u0126]/g,I:/[\xCC-\xCF\u0128\u012A\u012C\u012E\u0130]/g,IJ:/[\u0132]/g,J:/[\u0134]/g,K:/[\u0136]/g,L:/[\u0139\u013B\u013D\u013F\u0141]/g,N:/[\xD1\u0143\u0145\u0147\u014A]/g,O:/[\xD2-\xD6\xD8\u014C\u014E\u0150]/g,OE:/[\u0152]/g,R:/[\u0154\u0156\u0158]/g,S:/[\u015A\u015C\u015E\u0160]/g,T:/[\u0162\u0164\u0166]/g,U:/[\xD9-\xDC\u0168\u016A\u016C\u016E\u0170\u0172]/g,W:/[\u0174]/g,Y:/[\xDD\u0176\u0178]/g,Z:/[\u0179\u017B\u017D]/g,a:/[\xE0-\xE5\u0101\u0103\u0105]/g,ae:/[\xE6]/g,c:/[\xE7\u0107\u0109\u010B\u010D]/g,d:/[\u010F\u0111]/g,e:/[\xE8-\xEB\u0113\u0115\u0117\u0119\u011B]/g,g:/[\u011D\u011F\u0121\u0123]/g,i:/[\xEC-\xEF\u0129\u012B\u012D\u012F\u0131]/g,ij:/[\u0133]/g,j:/[\u0135]/g,k:/[\u0137,\u0138]/g,l:/[\u013A\u013C\u013E\u0140\u0142]/g,n:/[\xF1\u0144\u0146\u0148\u014B]/g,p:/[\xFE]/g,o:/[\xF2-\xF6\xF8\u014D\u014F\u0151]/g,oe:/[\u0153]/g,r:/[\u0155\u0157\u0159]/g,s:/[\u015B\u015D\u015F\u0161]/g,t:/[\u0163\u0165\u0167]/g,u:/[\xF9-\xFC\u0169\u016B\u016D\u016F\u0171\u0173]/g,w:/[\u0175]/g,y:/[\xFD\xFF\u0177]/g,z:/[\u017A\u017C\u017E]/g};for(let n in t)e=e.replace(t[n],n)}return e}function C(e){return g(e,!1)?e[0].toUpperCase()+e.slice(1):e}function O(e){return g(e)?e.replace(/(_)/g,"-").replace(/[A-Z]/g,((e,t)=>0===t?e:"-"+e.toLowerCase())).toLowerCase():e}function L(e){return g(e)?e.replace(/[A-Z]/g,((e,t)=>0===t?e:"."+e.toLowerCase())).toLowerCase():e}function E(){const e=new Map;return{on(t,n){let r=e.get(t);return r?r.push(n):r=[n],e.set(t,r),this},off(t,n){let r=e.get(t);return r&&r.splice(r.indexOf(n)>>>0,1),this},emit(t,n){let r=e.get(t);r&&r.slice().map((e=>{e(n)}))},clear(){e.clear()}}}var j=Object.defineProperty,N=Object.defineProperties,_=Object.getOwnPropertyDescriptors,A=Object.getOwnPropertySymbols,P=Object.prototype.hasOwnProperty,T=Object.prototype.propertyIsEnumerable,D=(e,t,n)=>t in e?j(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n,F=(e,t)=>{for(var n in t||(t={}))P.call(t,n)&&D(e,n,t[n]);if(A)for(var n of A(t))T.call(t,n)&&D(e,n,t[n]);return e},R=(e,t)=>N(e,_(t)),B=(e,t)=>{var n={};for(var r in e)P.call(e,r)&&t.indexOf(r)<0&&(n[r]=e[r]);if(null!=e&&A)for(var r of A(e))t.indexOf(r)<0&&T.call(e,r)&&(n[r]=e[r]);return n},W=E();function V(e,t){b(e)?e.push(...t||[]):p(e)&&Object.assign(e,t)}function H(e="",t=""){return function(e){return e.replaceAll(/ /g,"").replace(/[^\w]/g,"-")}(`${g(e,!1)&&g(t,!1)?`${e}-`:e}${t}`)}function I(e="",t=""){return`--${H(e,t)}`}function M(e,t="",n="",r=[],s){if(g(e)){const t=/{([^}]*)}/g,o=e.trim();if(function(e=""){return((e.match(/{/g)||[]).length+(e.match(/}/g)||[]).length)%2!=0}(o))return;if(x(o,t)){const e=o.replaceAll(t,(e=>{const t=e.replace(/{|}/g,"").split(".").filter((e=>!r.some((t=>x(e,t)))));return`var(${I(n,O(t.join("-")))}${u(s)?`, ${s}`:""})`})),i=/(\d+\s+[\+\-\*\/]\s+\d+)/g,a=/var\([^)]+\)/g;return x(e.replace(a,"0"),i)?`calc(${e})`:e}return o}if(function(e){return u(e)&&!isNaN(e)}(e))return e}function z(e,t){return e?`${e}{${t}}`:""}var K=(...e)=>Z(U.getTheme(),...e),Z=(e={},t,n,r)=>{if(t){const{variable:s,options:i}=U.defaults||{},{prefix:a,transform:l}=(null==e?void 0:e.options)||i||{},u=x(t,/{([^}]*)}/g)?t:`{${t}}`;return"value"===r||o(r)&&"strict"===l?U.getTokenValue(t):M(u,void 0,a,[s.excludedKeyRegex],n)}return""};function q(e,t={}){const n=U.defaults.variable,{prefix:r=n.prefix,selector:s=n.selector,excludedKeyRegex:o=n.excludedKeyRegex}=t,i=(e,t="")=>Object.entries(e).reduce(((e,[n,s])=>{const a=x(n,o)?H(t):H(t,O(n)),l=function(e){return p(e)&&e.hasOwnProperty("value")&&e.hasOwnProperty("type")?e.value:e}(s);if(p(l)){const{variables:t,tokens:n}=i(l,a);V(e.tokens,n),V(e.variables,t)}else e.tokens.push((r?a.replace(`${r}-`,""):a).replaceAll("-",".")),function(e,t,n){g(t,!1)&&e.push(`${t}:${n};`)}(e.variables,I(a),M(l,a,r,[o]));return e}),{variables:[],tokens:[]}),{variables:a,tokens:l}=i(e,r);return{value:a,tokens:l,declarations:a.join(""),css:z(s,a.join(""))}}var Y={regex:{rules:{class:{pattern:/^\.([a-zA-Z][\w-]*)$/,resolve(e){return{type:"class",selector:e,matched:this.pattern.test(e.trim())}}},attr:{pattern:/^\[(.*)\]$/,resolve(e){return{type:"attr",selector:`:root${e}`,matched:this.pattern.test(e.trim())}}},media:{pattern:/^@media (.*)$/,resolve(e){return{type:"media",selector:`${e}{:root{[CSS]}}`,matched:this.pattern.test(e.trim())}}},system:{pattern:/^system$/,resolve(e){return{type:"system",selector:"@media (prefers-color-scheme: dark){:root{[CSS]}}",matched:this.pattern.test(e.trim())}}},custom:{resolve:e=>({type:"custom",selector:e,matched:!0})}},resolve(e){const t=Object.keys(this.rules).filter((e=>"custom"!==e)).map((e=>this.rules[e]));return[e].flat().map((e=>{var n;return null!=(n=t.map((t=>t.resolve(e))).find((e=>e.matched)))?n:this.rules.custom.resolve(e)}))}},_toVariables:(e,t)=>q(e,{prefix:null==t?void 0:t.prefix}),getCommon({name:e="",theme:t={},params:n,set:r,defaults:s}){var o,i,a,l,c,d,h;const{preset:f,options:p}=t;let g,y,v,b,S,x,$;if(u(f)&&"strict"!==p.transform){const{primitive:t,semantic:n,extend:k}=f,w=n||{},{colorScheme:C}=w,O=B(w,["colorScheme"]),L=k||{},{colorScheme:E}=L,j=B(L,["colorScheme"]),N=C||{},{dark:_}=N,A=B(N,["dark"]),P=E||{},{dark:T}=P,D=B(P,["dark"]),F=u(t)?this._toVariables({primitive:t},p):{},R=u(O)?this._toVariables({semantic:O},p):{},W=u(A)?this._toVariables({light:A},p):{},V=u(_)?this._toVariables({dark:_},p):{},H=u(j)?this._toVariables({semantic:j},p):{},I=u(D)?this._toVariables({light:D},p):{},M=u(T)?this._toVariables({dark:T},p):{},[z,Z]=[null!=(o=F.declarations)?o:"",F.tokens],[q,Y]=[null!=(i=R.declarations)?i:"",R.tokens||[]],[U,X]=[null!=(a=W.declarations)?a:"",W.tokens||[]],[J,G]=[null!=(l=V.declarations)?l:"",V.tokens||[]],[Q,ee]=[null!=(c=H.declarations)?c:"",H.tokens||[]],[te,ne]=[null!=(d=I.declarations)?d:"",I.tokens||[]],[re,se]=[null!=(h=M.declarations)?h:"",M.tokens||[]];g=this.transformCSS(e,z,"light","variable",p,r,s),y=Z;v=`${this.transformCSS(e,`${q}${U}`,"light","variable",p,r,s)}${this.transformCSS(e,`${J}`,"dark","variable",p,r,s)}`,b=[...new Set([...Y,...X,...G])];S=`${this.transformCSS(e,`${Q}${te}color-scheme:light`,"light","variable",p,r,s)}${this.transformCSS(e,`${re}color-scheme:dark`,"dark","variable",p,r,s)}`,x=[...new Set([...ee,...ne,...se])],$=m(f.css,{dt:K})}return{primitive:{css:g,tokens:y},semantic:{css:v,tokens:b},global:{css:S,tokens:x},style:$}},getPreset({name:e="",preset:t={},options:n,params:r,set:s,defaults:o,selector:i}){var a,l,c;let d,h,f;if(u(t)&&"strict"!==n.transform){const r=e.replace("-directive",""),p=t,{colorScheme:g,extend:y,css:v}=p,b=B(p,["colorScheme","extend","css"]),S=y||{},{colorScheme:x}=S,$=B(S,["colorScheme"]),k=g||{},{dark:w}=k,C=B(k,["dark"]),O=x||{},{dark:L}=O,E=B(O,["dark"]),j=u(b)?this._toVariables({[r]:F(F({},b),$)},n):{},N=u(C)?this._toVariables({[r]:F(F({},C),E)},n):{},_=u(w)?this._toVariables({[r]:F(F({},w),L)},n):{},[A,P]=[null!=(a=j.declarations)?a:"",j.tokens||[]],[T,D]=[null!=(l=N.declarations)?l:"",N.tokens||[]],[R,W]=[null!=(c=_.declarations)?c:"",_.tokens||[]];d=`${this.transformCSS(r,`${A}${T}`,"light","variable",n,s,o,i)}${this.transformCSS(r,R,"dark","variable",n,s,o,i)}`,h=[...new Set([...P,...D,...W])],f=m(v,{dt:K})}return{css:d,tokens:h,style:f}},getPresetC({name:e="",theme:t={},params:n,set:r,defaults:s}){var o;const{preset:i,options:a}=t,l=null==(o=null==i?void 0:i.components)?void 0:o[e];return this.getPreset({name:e,preset:l,options:a,params:n,set:r,defaults:s})},getPresetD({name:e="",theme:t={},params:n,set:r,defaults:s}){var o;const i=e.replace("-directive",""),{preset:a,options:l}=t,u=null==(o=null==a?void 0:a.directives)?void 0:o[i];return this.getPreset({name:i,preset:u,options:l,params:n,set:r,defaults:s})},applyDarkColorScheme:e=>!("none"===e.darkModeSelector||!1===e.darkModeSelector),getColorSchemeOption(e,t){var n;return this.applyDarkColorScheme(e)?this.regex.resolve(!0===e.darkModeSelector?t.options.darkModeSelector:null!=(n=e.darkModeSelector)?n:t.options.darkModeSelector):[]},getLayerOrder(e,t={},n,r){const{cssLayer:s}=t;if(s){return`@layer ${m(s.order||"primeui",n)}`}return""},getCommonStyleSheet({name:e="",theme:t={},params:n,props:r={},set:s,defaults:o}){const i=this.getCommon({name:e,theme:t,params:n,set:s,defaults:o}),a=Object.entries(r).reduce(((e,[t,n])=>e.push(`${t}="${n}"`)&&e),[]).join(" ");return Object.entries(i||{}).reduce(((e,[t,n])=>{if(null==n?void 0:n.css){const r=k(null==n?void 0:n.css),s=`${t}-variables`;e.push(``)}return e}),[]).join("")},getStyleSheet({name:e="",theme:t={},params:n,props:r={},set:s,defaults:o}){var i;const a={name:e,theme:t,params:n,set:s,defaults:o},l=null==(i=e.includes("-directive")?this.getPresetD(a):this.getPresetC(a))?void 0:i.css,u=Object.entries(r).reduce(((e,[t,n])=>e.push(`${t}="${n}"`)&&e),[]).join(" ");return l?``:""},createTokens(e={},t,n="",r="",s={}){return Object.entries(e).forEach((([e,i])=>{const a=x(e,t.variable.excludedKeyRegex)?n:n?`${n}.${L(e)}`:L(e),l=r?`${r}.${e}`:e;p(i)?this.createTokens(i,t,a,l,s):(s[a]||(s[a]={paths:[],computed(e,t={}){var n,r;return 1===this.paths.length?null==(n=this.paths[0])?void 0:n.computed(this.paths[0].scheme,t.binding):e&&"none"!==e?null==(r=this.paths.find((t=>t.scheme===e)))?void 0:r.computed(e,t.binding):this.paths.map((e=>e.computed(e.scheme,t[e.scheme])))}}),s[a].paths.push({path:l,value:i,scheme:l.includes("colorScheme.light")?"light":l.includes("colorScheme.dark")?"dark":"none",computed(e,t={}){const n=/{([^}]*)}/g;let r=i;if(t.name=this.path,t.binding||(t.binding={}),x(i,n)){const o=i.trim().replaceAll(n,(n=>{var r;const o=n.replace(/{|}/g,""),i=null==(r=s[o])?void 0:r.computed(e,t);return b(i)&&2===i.length?`light-dark(${i[0].value},${i[1].value})`:null==i?void 0:i.value})),a=/(\d+\w*\s+[\+\-\*\/]\s+\d+\w*)/g,l=/var\([^)]+\)/g;r=x(o.replace(l,"0"),a)?`calc(${o})`:o}return o(t.binding)&&delete t.binding,{colorScheme:e,path:this.path,paths:t,value:r.includes("undefined")?void 0:r}}}))})),s},getTokenValue(e,t,n){var r;const s=t.split(".").filter((e=>!x(e.toLowerCase(),n.variable.excludedKeyRegex))).join(".");const o=t.includes("colorScheme.light")?"light":t.includes("colorScheme.dark")?"dark":void 0,i=[null==(r=e[s])?void 0:r.computed(o)].flat().filter((e=>e));return 1===i.length?i[0].value:i.reduce(((e={},t)=>{const n=t,{colorScheme:r}=n,s=B(n,["colorScheme"]);return e[r]=s,e}),void 0)},getSelectorRule:(e,t,n,r)=>"class"===n||"attr"===n?z(u(t)?`${e}${t},${e} ${t}`:e,r):z(e,u(t)?z(t,r):r),transformCSS(e,t,n,r,s={},o,i,a){if(u(t)){const{cssLayer:l}=s;if("style"!==r){const e=this.getColorSchemeOption(s,i);t="dark"===n?e.reduce(((e,{type:n,selector:r})=>(u(r)&&(e+=r.includes("[CSS]")?r.replace("[CSS]",t):this.getSelectorRule(r,a,n,t)),e)),""):z(null!=a?a:":root",t)}if(l){const n={name:"primeui",order:"primeui"};p(l)&&(n.name=m(l.name,{name:e,type:r})),u(n.name)&&(t=z(`@layer ${n.name}`,t),null==o||o.layerNames(n.name))}return t}return""}},U={defaults:{variable:{prefix:"p",selector:":root",excludedKeyRegex:/^(primitive|semantic|components|directives|variables|colorscheme|light|dark|common|root|states|extend|css)$/gi},options:{prefix:"p",darkModeSelector:"system",cssLayer:!1}},_theme:void 0,_layerNames:new Set,_loadedStyleNames:new Set,_loadingStyles:new Set,_tokens:{},update(e={}){const{theme:t}=e;t&&(this._theme=R(F({},t),{options:F(F({},this.defaults.options),t.options)}),this._tokens=Y.createTokens(this.preset,this.defaults),this.clearLoadedStyleNames())},get theme(){return this._theme},get preset(){var e;return(null==(e=this.theme)?void 0:e.preset)||{}},get options(){var e;return(null==(e=this.theme)?void 0:e.options)||{}},get tokens(){return this._tokens},getTheme(){return this.theme},setTheme(e){this.update({theme:e}),W.emit("theme:change",e)},getPreset(){return this.preset},setPreset(e){this._theme=R(F({},this.theme),{preset:e}),this._tokens=Y.createTokens(e,this.defaults),this.clearLoadedStyleNames(),W.emit("preset:change",e),W.emit("theme:change",this.theme)},getOptions(){return this.options},setOptions(e){this._theme=R(F({},this.theme),{options:e}),this.clearLoadedStyleNames(),W.emit("options:change",e),W.emit("theme:change",this.theme)},getLayerNames(){return[...this._layerNames]},setLayerNames(e){this._layerNames.add(e)},getLoadedStyleNames(){return this._loadedStyleNames},isStyleNameLoaded(e){return this._loadedStyleNames.has(e)},setLoadedStyleName(e){this._loadedStyleNames.add(e)},deleteLoadedStyleName(e){this._loadedStyleNames.delete(e)},clearLoadedStyleNames(){this._loadedStyleNames.clear()},getTokenValue(e){return Y.getTokenValue(this.tokens,e,this.defaults)},getCommon(e="",t){return Y.getCommon({name:e,theme:this.theme,params:t,defaults:this.defaults,set:{layerNames:this.setLayerNames.bind(this)}})},getComponent(e="",t){const n={name:e,theme:this.theme,params:t,defaults:this.defaults,set:{layerNames:this.setLayerNames.bind(this)}};return Y.getPresetC(n)},getDirective(e="",t){const n={name:e,theme:this.theme,params:t,defaults:this.defaults,set:{layerNames:this.setLayerNames.bind(this)}};return Y.getPresetD(n)},getCustomPreset(e="",t,n,r){const s={name:e,preset:t,options:this.options,selector:n,params:r,defaults:this.defaults,set:{layerNames:this.setLayerNames.bind(this)}};return Y.getPreset(s)},getLayerOrderCSS(e=""){return Y.getLayerOrder(e,this.options,{names:this.getLayerNames()},this.defaults)},transformCSS(e="",t,n="style",r){return Y.transformCSS(e,t,r,n,this.options,{layerNames:this.setLayerNames.bind(this)},this.defaults)},getCommonStyleSheet(e="",t,n={}){return Y.getCommonStyleSheet({name:e,theme:this.theme,params:t,props:n,defaults:this.defaults,set:{layerNames:this.setLayerNames.bind(this)}})},getStyleSheet(e,t,n={}){return Y.getStyleSheet({name:e,theme:this.theme,params:t,props:n,defaults:this.defaults,set:{layerNames:this.setLayerNames.bind(this)}})},onStyleMounted(e){this._loadingStyles.add(e)},onStyleUpdated(e){this._loadingStyles.add(e)},onStyleLoaded(e,{name:t}){this._loadingStyles.size&&(this._loadingStyles.delete(t),W.emit(`theme:${t}:load`,e),!this._loadingStyles.size&&W.emit("theme:load"))}};function X(e,t){return!!e&&(e.classList?e.classList.contains(t):new RegExp("(^| )"+t+"( |$)","gi").test(e.className))}function J(e,t){if(e&&t){const n=t=>{X(e,t)||(e.classList?e.classList.add(t):e.className+=" "+t)};[t].flat().filter(Boolean).forEach((e=>e.split(" ").forEach(n)))}}function G(e){for(const n of null==document?void 0:document.styleSheets)try{for(const t of null==n?void 0:n.cssRules)for(const n of null==t?void 0:t.style)if(e.test(n))return{name:n,value:t.style.getPropertyValue(n).trim()}}catch(t){}return null}function Q(e,t){if(e&&t){const n=t=>{e.classList?e.classList.remove(t):e.className=e.className.replace(new RegExp("(^|\\b)"+t.split(" ").join("|")+"(\\b|$)","gi")," ")};[t].flat().filter(Boolean).forEach((e=>e.split(" ").forEach(n)))}}function ee(e){let t={width:0,height:0};return e&&(e.style.visibility="hidden",e.style.display="block",t.width=e.offsetWidth,t.height=e.offsetHeight,e.style.display="none",e.style.visibility="visible"),t}function te(){let e=window,t=document,n=t.documentElement,r=t.getElementsByTagName("body")[0];return{width:e.innerWidth||n.clientWidth||r.clientWidth,height:e.innerHeight||n.clientHeight||r.clientHeight}}function ne(){let e=document.documentElement;return(window.pageXOffset||e.scrollLeft)-(e.clientLeft||0)}function re(){let e=document.documentElement;return(window.pageYOffset||e.scrollTop)-(e.clientTop||0)}function se(e,t,n=!0){var r,s,o,i;if(e){const a=e.offsetParent?{width:e.offsetWidth,height:e.offsetHeight}:ee(e),l=a.height,u=a.width,c=t.offsetHeight,d=t.offsetWidth,h=t.getBoundingClientRect(),f=re(),p=ne(),m=te();let g,y,v="top";h.top+c+l>m.height?(g=h.top+f-l,v="bottom",g<0&&(g=f)):g=c+h.top+f,y=h.left+u>m.width?Math.max(0,h.left+p+d-u):h.left+p,e.style.top=g+"px",e.style.left=y+"px",e.style.transformOrigin=v,n&&(e.style.marginTop="bottom"===v?`calc(${null!=(s=null==(r=G(/-anchor-gutter$/))?void 0:r.value)?s:"2px"} * -1)`:null!=(i=null==(o=G(/-anchor-gutter$/))?void 0:o.value)?i:"")}}function oe(e,t){e&&("string"==typeof t?e.style.cssText=t:Object.entries(t||{}).forEach((([t,n])=>e.style[t]=n)))}function ie(e,t){if(e instanceof HTMLElement){return e.offsetWidth}return 0}function ae(e,t,n=!0){var r,s,o,i;if(e){const a=e.offsetParent?{width:e.offsetWidth,height:e.offsetHeight}:ee(e),l=t.offsetHeight,u=t.getBoundingClientRect(),c=te();let d,h,f="top";u.top+l+a.height>c.height?(d=-1*a.height,f="bottom",u.top+d<0&&(d=-1*u.top)):d=l,h=a.width>c.width?-1*u.left:u.left+a.width>c.width?-1*(u.left+a.width-c.width):0,e.style.top=d+"px",e.style.left=h+"px",e.style.transformOrigin=f,n&&(e.style.marginTop="bottom"===f?`calc(${null!=(s=null==(r=G(/-anchor-gutter$/))?void 0:r.value)?s:"2px"} * -1)`:null!=(i=null==(o=G(/-anchor-gutter$/))?void 0:o.value)?i:"")}}function le(e){return"object"==typeof HTMLElement?e instanceof HTMLElement:e&&"object"==typeof e&&null!==e&&1===e.nodeType&&"string"==typeof e.nodeName}var ue=void 0;function ce(e){{if(null!=ue)return ue;let e=document.createElement("div");oe(e,{width:"100px",height:"100px",overflow:"scroll",position:"absolute",top:"-9999px"}),document.body.appendChild(e);let t=e.offsetWidth-e.clientWidth;return document.body.removeChild(e),ue=t,t}}function de(e,t={}){if(le(e)){const n=(t,r)=>{var s,o;const i=(null==(s=null==e?void 0:e.$attrs)?void 0:s[t])?[null==(o=null==e?void 0:e.$attrs)?void 0:o[t]]:[];return[r].flat().reduce(((e,r)=>{if(null!=r){const s=typeof r;if("string"===s||"number"===s)e.push(r);else if("object"===s){const s=Array.isArray(r)?n(t,r):Object.entries(r).map((([e,n])=>"style"!==t||!n&&0!==n?n?e:void 0:`${e.replace(/([a-z])([A-Z])/g,"$1-$2").toLowerCase()}:${n}`));e=s.length?e.concat(s.filter((e=>!!e))):e}}return e}),i)};Object.entries(t).forEach((([t,r])=>{if(null!=r){const s=t.match(/^on(.+)/);s?e.addEventListener(s[1].toLowerCase(),r):"p-bind"===t||"pBind"===t?de(e,r):(r="class"===t?[...new Set(n("class",r))].join(" ").trim():"style"===t?n("style",r).join(";").trim():r,(e.$attrs=e.$attrs||{})&&(e.$attrs[t]=r),e.setAttribute(t,r))}}))}}function he(e,t={},...n){if(e){const r=document.createElement(e);return de(r,t),r.append(...n),r}}function fe(e,t){if(e){e.style.opacity="0";let n=+new Date,r="0",s=function(){r=""+(+e.style.opacity+((new Date).getTime()-n)/t),e.style.opacity=r,n=+new Date,+r<1&&(window.requestAnimationFrame&&requestAnimationFrame(s)||setTimeout(s,16))};s()}}function pe(e,t){return le(e)?e.matches(t)?e:e.querySelector(t):null}function me(e,t){e&&document.activeElement!==e&&e.focus(t)}function ge(e,t){if(le(e)){const n=e.getAttribute(t);return isNaN(n)?"true"===n||"false"===n?"true"===n:n:+n}}function ye(e,t=""){let n=function(e,t){return le(e)?Array.from(e.querySelectorAll(t)):[]}(e,`button:not([tabindex = "-1"]):not([disabled]):not([style*="display:none"]):not([hidden])${t},\n [href][clientHeight][clientWidth]:not([tabindex = "-1"]):not([disabled]):not([style*="display:none"]):not([hidden])${t},\n input:not([tabindex = "-1"]):not([disabled]):not([style*="display:none"]):not([hidden])${t},\n select:not([tabindex = "-1"]):not([disabled]):not([style*="display:none"]):not([hidden])${t},\n textarea:not([tabindex = "-1"]):not([disabled]):not([style*="display:none"]):not([hidden])${t},\n [tabIndex]:not([tabIndex = "-1"]):not([disabled]):not([style*="display:none"]):not([hidden])${t},\n [contenteditable]:not([tabIndex = "-1"]):not([disabled]):not([style*="display:none"]):not([hidden])${t}`),r=[];for(let s of n)"none"!=getComputedStyle(s).display&&"hidden"!=getComputedStyle(s).visibility&&r.push(s);return r}function ve(e,t){const n=ye(e,t);return n.length>0?n[0]:null}function be(e){if(e){let t=e.offsetHeight,n=getComputedStyle(e);return t-=parseFloat(n.paddingTop)+parseFloat(n.paddingBottom)+parseFloat(n.borderTopWidth)+parseFloat(n.borderBottomWidth),t}return 0}function Se(e){if(e){e.style.visibility="hidden",e.style.display="block";let t=e.offsetHeight;return e.style.display="none",e.style.visibility="visible",t}return 0}function xe(e){if(e){e.style.visibility="hidden",e.style.display="block";let t=e.offsetWidth;return e.style.display="none",e.style.visibility="visible",t}return 0}function $e(e){if(e){let t=e.parentNode;return t&&t instanceof ShadowRoot&&t.host&&(t=t.host),t}return null}function ke(e,t){const n=ye(e,t);return n.length>0?n[n.length-1]:null}function we(e){if(e){let t=e.getBoundingClientRect();return{top:t.top+(window.pageYOffset||document.documentElement.scrollTop||document.body.scrollTop||0),left:t.left+(window.pageXOffset||document.documentElement.scrollLeft||document.body.scrollLeft||0)}}return{top:"auto",left:"auto"}}function Ce(e,t){if(e){return e.offsetHeight}return 0}function Oe(e,t=[]){const n=$e(e);return null===n?t:Oe(n,t.concat([n]))}function Le(e){let t=[];if(e){let n=Oe(e);const r=/(auto|scroll)/,s=e=>{try{let t=window.getComputedStyle(e,null);return r.test(t.getPropertyValue("overflow"))||r.test(t.getPropertyValue("overflowX"))||r.test(t.getPropertyValue("overflowY"))}catch(t){return!1}};for(let e of n){let n=1===e.nodeType&&e.dataset.scrollselectors;if(n){let r=n.split(",");for(let n of r){let r=pe(e,n);r&&s(r)&&t.push(r)}}9!==e.nodeType&&s(e)&&t.push(e)}}return t}function Ee(e){return!(null==e||!e.nodeName||!$e(e))}function je(e){if(e){let t=e.offsetWidth,n=getComputedStyle(e);return t-=parseFloat(n.paddingLeft)+parseFloat(n.paddingRight)+parseFloat(n.borderLeftWidth)+parseFloat(n.borderRightWidth),t}return 0}function Ne(){return/(android)/i.test(navigator.userAgent)}function _e(){return!("undefined"==typeof window||!window.document||!window.document.createElement)}function Ae(e){return!(!e||null==e.offsetParent)}function Pe(e){return!!e&&"rtl"===getComputedStyle(e).direction}function Te(){return"ontouchstart"in window||navigator.maxTouchPoints>0||navigator.msMaxTouchPoints>0}function De(e,t){var n,r;if(e){const s=e.parentElement,o=we(s),i=te(),a=e.offsetParent?e.offsetWidth:xe(e),l=e.offsetParent?e.offsetHeight:Se(e),u=ie(null==(n=null==s?void 0:s.children)?void 0:n[0]),c=Ce(null==(r=null==s?void 0:s.children)?void 0:r[0]);let d="",h="";o.left+u+a>i.width-ce()?o.lefti.height?`-${l-c}px`:"0px",e.style.top=h,e.style.left=d}}function Fe(e,t="",n){le(e)&&null!=n&&e.setAttribute(t,n)}var Re={};function Be(e="pui_id_"){return Re.hasOwnProperty(e)||(Re[e]=0),Re[e]++,`${e}${Re[e]}`}var We=function(){let e=[];const t=(t,n,r=0)=>[...e].reverse().find((e=>!0))||{key:t,value:r},n=e=>e&&parseInt(e.style.zIndex,10)||0;return{get:n,set:(n,r,s)=>{r&&(r.style.zIndex=String(((n,r,s=999)=>{const o=t(n,r,s),i=o.value+(o.key===n?0:s)+1;return e.push({key:n,value:i}),i})(n,!0,s)))},clear:t=>{var r;t&&(r=n(t),e=e.filter((e=>e.value!==r)),t.style.zIndex="")},getCurrent:e=>(e=>t(e).value)(e)}}();export{h as $,Q as A,be as B,je as C,ie as D,E,Ce as F,we as G,J as H,ge as I,Ae as J,me as K,Ne as L,S as M,ve as N,ke as O,oe as P,ae as Q,se as R,Te as S,ye as T,f as U,fe as V,ne as W,re as X,te as Y,We as Z,X as _,w as a,Pe as a0,xe as a1,Se as a2,De as a3,Fe as b,_e as c,m as d,d as e,u as f,U as g,K as h,Ee as i,$ as j,W as k,pe as l,k as m,l as n,v as o,g as p,b as q,c as r,de as s,y as t,Be as u,o as v,p as w,C as x,Le as y,he as z}; diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v2/assets/primevue-keNb8gkp.js b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v2/assets/primevue-keNb8gkp.js new file mode 100644 index 0000000000000000000000000000000000000000..c9b9dd45df70486e9a169889689550b0e9ace61b --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v2/assets/primevue-keNb8gkp.js @@ -0,0 +1 @@ +import{r as e,a as t,e as n,s as o,b as i,i as r,c as l,d as a,f as u,m as s,g as c,h as d,E as v,j as f,k as m,u as p,l as h,t as g,n as y,o as b,p as C,q as S,v as $,w,x as P,y as O}from"./primeuix-CNMLIuxO.js";import{r as T,C as _,D as j,o as L,x,w as N,p as V,E as M,a as k,c as D,b as E}from"./vue-DfGw76ex.js";var A="startsWith",I="contains",B="notContains",F="endsWith",U="equals",R="notEquals",z="lt",W="lte",Z="gt",H="gte",Y="dateIs",q="dateIsNot",G="dateBefore",J="dateAfter";function K(e,t){var n="undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(!n){if(Array.isArray(e)||(n=function(e,t){if(e){if("string"==typeof e)return Q(e,t);var n={}.toString.call(e).slice(8,-1);return"Object"===n&&e.constructor&&(n=e.constructor.name),"Map"===n||"Set"===n?Array.from(e):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?Q(e,t):void 0}}(e))||t){n&&(e=n);var o=0,i=function(){};return{s:i,n:function(){return o>=e.length?{done:!0}:{done:!1,value:e[o++]}},e:function(e){throw e},f:i}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var r,l=!0,a=!1;return{s:function(){n=n.call(e)},n:function(){var e=n.next();return l=e.done,e},e:function(e){a=!0,r=e},f:function(){try{l||null==n.return||n.return()}finally{if(a)throw r}}}}function Q(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,o=Array(t);nt.getTime():e>t)},gte:function(e,t){return null==t||null!=e&&(e.getTime&&t.getTime?e.getTime()>=t.getTime():e>=t)},dateIs:function(e,t){return null==t||null!=e&&e.toDateString()===t.toDateString()},dateIsNot:function(e,t){return null==t||null!=e&&e.toDateString()!==t.toDateString()},dateBefore:function(e,t){return null==t||null!=e&&e.getTime()t.getTime()}},register:function(e,t){this.filters[e]=t}};function ee(e){return(ee="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function te(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);t&&(o=o.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,o)}return n}function ne(e){for(var t=1;t1&&void 0!==arguments[1]?arguments[1]:{},n=T(!1),a=T(e),u=T(null),s=l()?window.document:void 0,c=t.document,d=void 0===c?s:c,v=t.immediate,f=void 0===v||v,m=t.manual,p=void 0!==m&&m,h=t.name,g=void 0===h?"style_".concat(++ie):h,y=t.id,b=void 0===y?void 0:y,C=t.media,S=void 0===C?void 0:C,$=t.nonce,w=void 0===$?void 0:$,P=t.first,O=void 0!==P&&P,V=t.onMounted,M=void 0===V?void 0:V,k=t.onUpdated,D=void 0===k?void 0:k,E=t.onLoad,A=void 0===E?void 0:E,I=t.props,B=void 0===I?{}:I,F=function(){},U=function(t){var r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};if(d){var l=ne(ne({},B),r),s=l.name||g,c=l.id||b,v=l.nonce||w;u.value=d.querySelector('style[data-primevue-style-id="'.concat(s,'"]'))||d.getElementById(c)||d.createElement("style"),u.value.isConnected||(a.value=t||e,o(u.value,{type:"text/css",id:c,media:S,nonce:v}),O?d.head.prepend(u.value):d.head.appendChild(u.value),i(u.value,"data-primevue-style-id",s),o(u.value,l),u.value.onload=function(e){return null==A?void 0:A(e,{name:s})},null==M||M(s)),n.value||(F=N(a,(function(e){u.value.textContent=e,null==D||D(s)}),{immediate:!0}),n.value=!0)}};return f&&!p&&function(e){var t=!(arguments.length>1&&void 0!==arguments[1])||arguments[1];j()?L(e):t?e():x(e)}(U),{id:b,name:g,el:u,css:a,unload:function(){d&&n.value&&(F(),r(u.value)&&d.head.removeChild(u.value),n.value=!1)},load:U,isLoaded:_(n)}}function le(e){return(le="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function ae(e,t){return function(e){if(Array.isArray(e))return e}(e)||function(e,t){var n=null==e?null:"undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(null!=n){var o,i,r,l,a=[],u=!0,s=!1;try{if(r=(n=n.call(e)).next,0===t);else for(;!(u=(o=r.call(n)).done)&&(a.push(o.value),a.length!==t);u=!0);}catch(c){s=!0,i=c}finally{try{if(!u&&null!=n.return&&(l=n.return(),Object(l)!==l))return}finally{if(s)throw i}}return a}}(e,t)||function(e,t){if(e){if("string"==typeof e)return ue(e,t);var n={}.toString.call(e).slice(8,-1);return"Object"===n&&e.constructor&&(n=e.constructor.name),"Map"===n||"Set"===n?Array.from(e):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?ue(e,t):void 0}}(e,t)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function ue(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,o=Array(t);n1&&void 0!==arguments[1]?arguments[1]:{},n=(arguments.length>2&&void 0!==arguments[2]?arguments[2]:function(e){return e})(a(e,{dt:d}));return u(n)?re(s(n),ce({name:this.name},t)):{}},loadCSS:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return this.load(this.css,e)},loadTheme:function(){var e=this,t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"";return this.load(this.theme,t,(function(){var o=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"";return c.transformCSS(t.name||e.name,"".concat(o).concat(n))}))},getCommonTheme:function(e){return c.getCommon(this.name,e)},getComponentTheme:function(e){return c.getComponent(this.name,e)},getDirectiveTheme:function(e){return c.getDirective(this.name,e)},getPresetTheme:function(e,t,n){return c.getCustomPreset(this.name,e,t,n)},getLayerOrderThemeCSS:function(){return c.getLayerOrderCSS(this.name)},getStyleSheet:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"",t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};if(this.css){var n=a(this.css,{dt:d})||"",o=s("".concat(n).concat(e)),i=Object.entries(t).reduce((function(e,t){var n=ae(t,2),o=n[0],i=n[1];return e.push("".concat(o,'="').concat(i,'"'))&&e}),[]).join(" ");return u(o)?'"):""}return""},getCommonThemeStyleSheet:function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return c.getCommonStyleSheet(this.name,e,t)},getThemeStyleSheet:function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=[c.getStyleSheet(this.name,e,t)];if(this.theme){var o="base"===this.name?"global-style":"".concat(this.name,"-style"),i=a(this.theme,{dt:d}),r=s(c.transformCSS(o,i)),l=Object.entries(t).reduce((function(e,t){var n=ae(t,2),o=n[0],i=n[1];return e.push("".concat(o,'="').concat(i,'"'))&&e}),[]).join(" ");u(r)&&n.push('"))}return n.join("")},extend:function(e){return ce(ce({},this),{},{css:void 0,theme:void 0},e)}},fe=v();function me(e){return(me="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function pe(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);t&&(o=o.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,o)}return n}function he(e){for(var t=1;te.length)&&(t=e.length);for(var n=0,o=Array(t);n1?t-1:0),o=1;o0&&void 0!==arguments[0]?arguments[0]:function(){};we.clearLoadedStyleNames(),m.on("theme:change",e)},_getHostInstance:function(e){return e?this.$options.hostName?e.$.type.name===this.$options.hostName?e:this._getHostInstance(e.$parentInstance):e.$parentInstance:void 0},_getPropValue:function(e){var t;return this[e]||(null===(t=this._getHostInstance(this))||void 0===t?void 0:t[e])},_getOptionValue:function(e){return b(e,arguments.length>1&&void 0!==arguments[1]?arguments[1]:"",arguments.length>2&&void 0!==arguments[2]?arguments[2]:{})},_getPTValue:function(){var e,t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"",o=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},i=!(arguments.length>3&&void 0!==arguments[3])||arguments[3],r=/./g.test(n)&&!!o[n.split(".")[0]],l=this._getPropValue("ptOptions")||(null===(e=this.$primevueConfig)||void 0===e?void 0:e.ptOptions)||{},a=l.mergeSections,u=void 0===a||a,s=l.mergeProps,c=void 0!==s&&s,d=i?r?this._useGlobalPT(this._getPTClassValue,n,o):this._useDefaultPT(this._getPTClassValue,n,o):void 0,v=r?void 0:this._getPTSelf(t,this._getPTClassValue,n,Me(Me({},o),{},{global:d||{}})),f=this._getPTDatasets(n);return u||!u&&v?c?this._mergeProps(c,d,v,f):Me(Me(Me({},d),v),f):Me(Me({},v),f)},_getPTSelf:function(){for(var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=arguments.length,n=new Array(t>1?t-1:0),o=1;o0&&void 0!==arguments[0]?arguments[0]:"",o="data-pc-",i="root"===n&&u(null===(e=this.pt)||void 0===e?void 0:e["data-pc-section"]);return"transition"!==n&&Me(Me({},"root"===n&&Me(Me(ke({},"".concat(o,"name"),g(i?null===(t=this.pt)||void 0===t?void 0:t["data-pc-section"]:this.$.type.name)),i&&ke({},"".concat(o,"extend"),g(this.$.type.name))),l()&&ke({},"".concat(this.$attrSelector),""))),{},ke({},"".concat(o,"section"),g(n)))},_getPTClassValue:function(){var e=this._getOptionValue.apply(this,arguments);return C(e)||S(e)?{class:e}:e},_getPT:function(e){var t=this,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"",o=arguments.length>2?arguments[2]:void 0,i=function(e){var i,r=arguments.length>1&&void 0!==arguments[1]&&arguments[1],l=o?o(e):e,a=g(n),u=g(t.$name);return null!==(i=r?a!==u?null==l?void 0:l[a]:void 0:null==l?void 0:l[a])&&void 0!==i?i:l};return null!=e&&e.hasOwnProperty("_usept")?{_usept:e._usept,originalValue:i(e.originalValue),value:i(e.value)}:i(e,!0)},_usePT:function(e,t,n,o){var i=function(e){return t(e,n,o)};if(null!=e&&e.hasOwnProperty("_usept")){var r,l=e._usept||(null===(r=this.$primevueConfig)||void 0===r?void 0:r.ptOptions)||{},a=l.mergeSections,u=void 0===a||a,s=l.mergeProps,c=void 0!==s&&s,d=i(e.originalValue),v=i(e.value);if(void 0===d&&void 0===v)return;return C(v)?v:C(d)?d:u||!u&&v?c?this._mergeProps(c,d,v):Me(Me({},d),v):v}return i(e)},_useGlobalPT:function(e,t,n){return this._usePT(this.globalPT,e,t,n)},_useDefaultPT:function(e,t,n){return this._usePT(this.defaultPT,e,t,n)},ptm:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"",t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return this._getPTValue(this.pt,e,Me(Me({},this.$params),t))},ptmi:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"",t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return M(this.$_attrsWithoutPT,this.ptm(e,t))},ptmo:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"",n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return this._getPTValue(e,t,Me({instance:this},n),!1)},cx:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"",t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return this.isUnstyled?void 0:this._getOptionValue(this.$style.classes,e,Me(Me({},this.$params),t))},sx:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"",t=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};if(!(arguments.length>1&&void 0!==arguments[1])||arguments[1]){var n=this._getOptionValue(this.$style.inlineStyles,e,Me(Me({},this.$params),t));return[this._getOptionValue(Pe.inlineStyles,e,Me(Me({},this.$params),t)),n]}}},computed:{globalPT:function(){var e,t=this;return this._getPT(null===(e=this.$primevueConfig)||void 0===e?void 0:e.pt,void 0,(function(e){return a(e,{instance:t})}))},defaultPT:function(){var e,t=this;return this._getPT(null===(e=this.$primevueConfig)||void 0===e?void 0:e.pt,void 0,(function(e){return t._getOptionValue(e,t.$name,Me({},t.$params))||a(e,Me({},t.$params))}))},isUnstyled:function(){var e;return void 0!==this.unstyled?this.unstyled:null===(e=this.$primevueConfig)||void 0===e?void 0:e.unstyled},$inProps:function(){var e,t=Object.keys((null===(e=this.$.vnode)||void 0===e?void 0:e.props)||{});return Object.fromEntries(Object.entries(this.$props).filter((function(e){var n=_e(e,1)[0];return null==t?void 0:t.includes(n)})))},$theme:function(){var e;return null===(e=this.$primevueConfig)||void 0===e?void 0:e.theme},$style:function(){return Me(Me({classes:void 0,inlineStyles:void 0,load:function(){},loadCSS:function(){},loadTheme:function(){}},(this._getHostInstance(this)||{}).$style),this.$options.style)},$styleOptions:function(){var e;return{nonce:null===(e=this.$primevueConfig)||void 0===e||null===(e=e.csp)||void 0===e?void 0:e.nonce}},$primevueConfig:function(){var e;return null===(e=this.$primevue)||void 0===e?void 0:e.config},$name:function(){return this.$options.hostName||this.$.type.name},$params:function(){var e=this._getHostInstance(this)||this.$parent;return{instance:this,props:this.$props,state:this.$data,attrs:this.$attrs,parent:{instance:e,props:null==e?void 0:e.$props,state:null==e?void 0:e.$data,attrs:null==e?void 0:e.$attrs}}},$_attrsPT:function(){return Object.entries(this.$attrs||{}).filter((function(e){var t=_e(e,1)[0];return null==t?void 0:t.startsWith("pt:")})).reduce((function(e,t){var n=_e(t,2),o=n[0],i=n[1],r=Te(o.split(":")).slice(1);return null==r||r.reduce((function(e,t,n,o){return!e[t]&&(e[t]=n===o.length-1?i:{}),e[t]}),e),e}),{})},$_attrsWithoutPT:function(){return Object.entries(this.$attrs||{}).filter((function(e){var t=_e(e,1)[0];return!(null!=t&&t.startsWith("pt:"))})).reduce((function(e,t){var n=_e(t,2),o=n[0],i=n[1];return e[o]=i,e}),{})}}},Ee=ve.extend({name:"baseicon",css:"\n.p-icon {\n display: inline-block;\n vertical-align: baseline;\n}\n\n.p-icon-spin {\n -webkit-animation: p-icon-spin 2s infinite linear;\n animation: p-icon-spin 2s infinite linear;\n}\n\n@-webkit-keyframes p-icon-spin {\n 0% {\n -webkit-transform: rotate(0deg);\n transform: rotate(0deg);\n }\n 100% {\n -webkit-transform: rotate(359deg);\n transform: rotate(359deg);\n }\n}\n\n@keyframes p-icon-spin {\n 0% {\n -webkit-transform: rotate(0deg);\n transform: rotate(0deg);\n }\n 100% {\n -webkit-transform: rotate(359deg);\n transform: rotate(359deg);\n }\n}\n"});function Ae(e){return(Ae="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function Ie(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);t&&(o=o.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,o)}return n}function Be(e){for(var t=1;te.length)&&(t=e.length);for(var n=0,o=Array(t);n0&&void 0!==arguments[0]?arguments[0]:{},o=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"",r=arguments.length>3&&void 0!==arguments[3]?arguments[3]:{},l=!(arguments.length>4&&void 0!==arguments[4])||arguments[4],a=function(){var e=Ge._getOptionValue.apply(Ge,arguments);return C(e)||S(e)?{class:e}:e},u=(null===(e=n.binding)||void 0===e||null===(e=e.value)||void 0===e?void 0:e.ptOptions)||(null===(t=n.$primevueConfig)||void 0===t?void 0:t.ptOptions)||{},s=u.mergeSections,c=void 0===s||s,d=u.mergeProps,v=void 0!==d&&d,f=l?Ge._useDefaultPT(n,n.defaultPT(),a,i,r):void 0,m=Ge._usePT(n,Ge._getPT(o,n.$name),a,i,Ye(Ye({},r),{},{global:f||{}})),p=Ge._getPTDatasets(n,i);return c||!c&&m?v?Ge._mergeProps(n,v,f,m,p):Ye(Ye(Ye({},f),m),p):Ye(Ye({},m),p)},_getPTDatasets:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"",n="data-pc-";return Ye(Ye({},"root"===t&&qe({},"".concat(n,"name"),g(e.$name))),{},qe({},"".concat(n,"section"),g(t)))},_getPT:function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"",n=arguments.length>2?arguments[2]:void 0,o=function(e){var o,i=n?n(e):e,r=g(t);return null!==(o=null==i?void 0:i[r])&&void 0!==o?o:i};return null!=e&&e.hasOwnProperty("_usept")?{_usept:e._usept,originalValue:o(e.originalValue),value:o(e.value)}:o(e)},_usePT:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=arguments.length>1?arguments[1]:void 0,n=arguments.length>2?arguments[2]:void 0,o=arguments.length>3?arguments[3]:void 0,i=arguments.length>4?arguments[4]:void 0,r=function(e){return n(e,o,i)};if(null!=t&&t.hasOwnProperty("_usept")){var l,a=t._usept||(null===(l=e.$primevueConfig)||void 0===l?void 0:l.ptOptions)||{},u=a.mergeSections,s=void 0===u||u,c=a.mergeProps,d=void 0!==c&&c,v=r(t.originalValue),f=r(t.value);if(void 0===v&&void 0===f)return;return C(f)?f:C(v)?v:s||!s&&f?d?Ge._mergeProps(e,d,v,f):Ye(Ye({},v),f):f}return r(t)},_useDefaultPT:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=arguments.length>2?arguments[2]:void 0,o=arguments.length>3?arguments[3]:void 0,i=arguments.length>4?arguments[4]:void 0;return Ge._usePT(e,t,n,o,i)},_loadStyles:function(e,t,n){var o,i=Ge._getConfig(t,n),r={nonce:null==i||null===(o=i.csp)||void 0===o?void 0:o.nonce};Ge._loadCoreStyles(e.$instance,r),Ge._loadThemeStyles(e.$instance,r),Ge._loadScopedThemeStyles(e.$instance,r),Ge._themeChangeListener((function(){return Ge._loadThemeStyles(e.$instance,r)}))},_loadCoreStyles:function(){var e,t,n,o=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},i=arguments.length>1?arguments[1]:void 0;!we.isStyleNameLoaded(null===(e=o.$style)||void 0===e?void 0:e.name)&&null!==(t=o.$style)&&void 0!==t&&t.name&&(ve.loadCSS(i),null===(n=o.$style)||void 0===n||n.loadCSS(i),we.setLoadedStyleName(o.$style.name))},_loadThemeStyles:function(){var e,t,n,o=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},i=arguments.length>1?arguments[1]:void 0;if(!(null!=o&&o.isUnstyled()||"none"===(null==o||null===(e=o.theme)||void 0===e?void 0:e.call(o)))){if(!c.isStyleNameLoaded("common")){var r,l,a=(null===(r=o.$style)||void 0===r||null===(l=r.getCommonTheme)||void 0===l?void 0:l.call(r))||{},u=a.primitive,s=a.semantic,d=a.global,v=a.style;ve.load(null==u?void 0:u.css,Ye({name:"primitive-variables"},i)),ve.load(null==s?void 0:s.css,Ye({name:"semantic-variables"},i)),ve.load(null==d?void 0:d.css,Ye({name:"global-variables"},i)),ve.loadTheme(Ye({name:"global-style"},i),v),c.setLoadedStyleName("common")}if(!c.isStyleNameLoaded(null===(t=o.$style)||void 0===t?void 0:t.name)&&null!==(n=o.$style)&&void 0!==n&&n.name){var f,m,p,h,g=(null===(f=o.$style)||void 0===f||null===(m=f.getDirectiveTheme)||void 0===m?void 0:m.call(f))||{},y=g.css,b=g.style;null===(p=o.$style)||void 0===p||p.load(y,Ye({name:"".concat(o.$style.name,"-variables")},i)),null===(h=o.$style)||void 0===h||h.loadTheme(Ye({name:"".concat(o.$style.name,"-style")},i),b),c.setLoadedStyleName(o.$style.name)}if(!c.isStyleNameLoaded("layer-order")){var C,S,$=null===(C=o.$style)||void 0===C||null===(S=C.getLayerOrderThemeCSS)||void 0===S?void 0:S.call(C);ve.load($,Ye({name:"layer-order",first:!0},i)),c.setLoadedStyleName("layer-order")}}},_loadScopedThemeStyles:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=arguments.length>1?arguments[1]:void 0,n=e.preset();if(n&&e.$attrSelector){var o,i,r,l=((null===(o=e.$style)||void 0===o||null===(i=o.getPresetTheme)||void 0===i?void 0:i.call(o,n,"[".concat(e.$attrSelector,"]")))||{}).css,a=null===(r=e.$style)||void 0===r?void 0:r.load(l,Ye({name:"".concat(e.$attrSelector,"-").concat(e.$style.name)},t));e.scopedStyleEl=a.el}},_themeChangeListener:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:function(){};we.clearLoadedStyleNames(),m.on("theme:change",e)},_hook:function(e,t,n,o,i,r){var l,a,u="on".concat(P(t)),s=Ge._getConfig(o,i),c=null==n?void 0:n.$instance,d=Ge._usePT(c,Ge._getPT(null==o||null===(l=o.value)||void 0===l?void 0:l.pt,e),Ge._getOptionValue,"hooks.".concat(u)),v=Ge._useDefaultPT(c,null==s||null===(a=s.pt)||void 0===a||null===(a=a.directives)||void 0===a?void 0:a[e],Ge._getOptionValue,"hooks.".concat(u)),f={el:n,binding:o,vnode:i,prevVnode:r};null==d||d(c,f),null==v||v(c,f)},_mergeProps:function(){for(var e=arguments.length>1?arguments[1]:void 0,t=arguments.length,n=new Array(t>2?t-2:0),o=2;o1&&void 0!==arguments[1]?arguments[1]:{},n=function(n,o,i,r,l){var a,u,s,c;o._$instances=o._$instances||{};var d=Ge._getConfig(i,r),v=o._$instances[e]||{},f=$(v)?Ye(Ye({},t),null==t?void 0:t.methods):{};o._$instances[e]=Ye(Ye({},v),{},{$name:e,$host:o,$binding:i,$modifiers:null==i?void 0:i.modifiers,$value:null==i?void 0:i.value,$el:v.$el||o||void 0,$style:Ye({classes:void 0,inlineStyles:void 0,load:function(){},loadCSS:function(){},loadTheme:function(){}},null==t?void 0:t.style),$primevueConfig:d,$attrSelector:null===(a=o.$pd)||void 0===a||null===(a=a[e])||void 0===a?void 0:a.attrSelector,defaultPT:function(){return Ge._getPT(null==d?void 0:d.pt,void 0,(function(t){var n;return null==t||null===(n=t.directives)||void 0===n?void 0:n[e]}))},isUnstyled:function(){var e,t;return void 0!==(null===(e=o.$instance)||void 0===e||null===(e=e.$binding)||void 0===e||null===(e=e.value)||void 0===e?void 0:e.unstyled)?null===(t=o.$instance)||void 0===t||null===(t=t.$binding)||void 0===t||null===(t=t.value)||void 0===t?void 0:t.unstyled:null==d?void 0:d.unstyled},theme:function(){var e;return null===(e=o.$instance)||void 0===e||null===(e=e.$primevueConfig)||void 0===e?void 0:e.theme},preset:function(){var e;return null===(e=o.$instance)||void 0===e||null===(e=e.$binding)||void 0===e||null===(e=e.value)||void 0===e?void 0:e.dt},ptm:function(){var e,t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"",n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return Ge._getPTValue(o.$instance,null===(e=o.$instance)||void 0===e||null===(e=e.$binding)||void 0===e||null===(e=e.value)||void 0===e?void 0:e.pt,t,Ye({},n))},ptmo:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"",n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return Ge._getPTValue(o.$instance,e,t,n,!1)},cx:function(){var e,t,n=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"",i=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return null!==(e=o.$instance)&&void 0!==e&&e.isUnstyled()?void 0:Ge._getOptionValue(null===(t=o.$instance)||void 0===t||null===(t=t.$style)||void 0===t?void 0:t.classes,n,Ye({},i))},sx:function(){var e,t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"",n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return!(arguments.length>1&&void 0!==arguments[1])||arguments[1]?Ge._getOptionValue(null===(e=o.$instance)||void 0===e||null===(e=e.$style)||void 0===e?void 0:e.inlineStyles,t,Ye({},n)):void 0}},f),o.$instance=o._$instances[e],null===(u=(s=o.$instance)[n])||void 0===u||u.call(s,o,i,r,l),o["$".concat(e)]=o.$instance,Ge._hook(e,n,o,i,r,l),o.$pd||(o.$pd={}),o.$pd[e]=Ye(Ye({},null===(c=o.$pd)||void 0===c?void 0:c[e]),{},{name:e,instance:o.$instance})};return{created:function(t,o,i,r){t.$pd||(t.$pd={}),t.$pd[e]={name:e,attrSelector:p("pd")},n("created",t,o,i,r)},beforeMount:function(e,t,o,i){Ge._loadStyles(e,t,o),n("beforeMount",e,t,o,i),function(e){var t,n,o,i,r,l=null===(t=e.$instance)||void 0===t?void 0:t.watch;null==l||null===(n=l.config)||void 0===n||n.call(e.$instance,null===(o=e.$instance)||void 0===o?void 0:o.$primevueConfig),fe.on("config:change",(function(t){var n,o=t.newValue,i=t.oldValue;return null==l||null===(n=l.config)||void 0===n?void 0:n.call(e.$instance,o,i)})),null==l||null===(i=l["config.ripple"])||void 0===i||i.call(e.$instance,null===(r=e.$instance)||void 0===r||null===(r=r.$primevueConfig)||void 0===r?void 0:r.ripple),fe.on("config:ripple:change",(function(t){var n,o=t.newValue,i=t.oldValue;return null==l||null===(n=l["config.ripple"])||void 0===n?void 0:n.call(e.$instance,o,i)}))}(e)},mounted:function(e,t,o,i){Ge._loadStyles(e,t,o),n("mounted",e,t,o,i)},beforeUpdate:function(e,t,o,i){n("beforeUpdate",e,t,o,i)},updated:function(e,t,o,i){Ge._loadStyles(e,t,o),n("updated",e,t,o,i)},beforeUnmount:function(e,t,o,i){n("beforeUnmount",e,t,o,i)},unmounted:function(e,t,o,i){var r;null===(r=e.$instance)||void 0===r||null===(r=r.scopedStyleEl)||void 0===r||null===(r=r.value)||void 0===r||r.remove(),n("unmounted",e,t,o,i)}}},extend:function(){var e=We(Ge._getMeta.apply(Ge,arguments),2),t=e[0],n=e[1];return Ye({extend:function(){var e=We(Ge._getMeta.apply(Ge,arguments),2),t=e[0],o=e[1];return Ge.extend(t,Ye(Ye(Ye({},n),null==n?void 0:n.methods),o))}},Ge._extend(t,n))}};function Je(e){return(Je="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function Ke(e,t,n){return t&&function(e,t){for(var n=0;n1&&void 0!==arguments[1]?arguments[1]:function(){};!function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}(this,e),this.element=t,this.listener=n}),[{key:"bindScrollListener",value:function(){this.scrollableParents=O(this.element);for(var e=0;e0&&void 0!==arguments[0]?arguments[0]:"pv_id_")}var tt={name:"BlankIcon",extends:Ue};tt.render=function(e,t,n,o,i,r){return k(),D("svg",M({width:"14",height:"14",viewBox:"0 0 14 14",fill:"none",xmlns:"http://www.w3.org/2000/svg"},e.pti()),t[0]||(t[0]=[E("rect",{width:"1",height:"1",fill:"currentColor","fill-opacity":"0"},null,-1)]),16)};var nt={name:"CheckIcon",extends:Ue};nt.render=function(e,t,n,o,i,r){return k(),D("svg",M({width:"14",height:"14",viewBox:"0 0 14 14",fill:"none",xmlns:"http://www.w3.org/2000/svg"},e.pti()),t[0]||(t[0]=[E("path",{d:"M4.86199 11.5948C4.78717 11.5923 4.71366 11.5745 4.64596 11.5426C4.57826 11.5107 4.51779 11.4652 4.46827 11.4091L0.753985 7.69483C0.683167 7.64891 0.623706 7.58751 0.580092 7.51525C0.536478 7.44299 0.509851 7.36177 0.502221 7.27771C0.49459 7.19366 0.506156 7.10897 0.536046 7.03004C0.565935 6.95111 0.613367 6.88 0.674759 6.82208C0.736151 6.76416 0.8099 6.72095 0.890436 6.69571C0.970973 6.67046 1.05619 6.66385 1.13966 6.67635C1.22313 6.68886 1.30266 6.72017 1.37226 6.76792C1.44186 6.81567 1.4997 6.8786 1.54141 6.95197L4.86199 10.2503L12.6397 2.49483C12.7444 2.42694 12.8689 2.39617 12.9932 2.40745C13.1174 2.41873 13.2343 2.47141 13.3251 2.55705C13.4159 2.64268 13.4753 2.75632 13.4938 2.87973C13.5123 3.00315 13.4888 3.1292 13.4271 3.23768L5.2557 11.4091C5.20618 11.4652 5.14571 11.5107 5.07801 11.5426C5.01031 11.5745 4.9368 11.5923 4.86199 11.5948Z",fill:"currentColor"},null,-1)]),16)};var ot={name:"ChevronDownIcon",extends:Ue};ot.render=function(e,t,n,o,i,r){return k(),D("svg",M({width:"14",height:"14",viewBox:"0 0 14 14",fill:"none",xmlns:"http://www.w3.org/2000/svg"},e.pti()),t[0]||(t[0]=[E("path",{d:"M7.01744 10.398C6.91269 10.3985 6.8089 10.378 6.71215 10.3379C6.61541 10.2977 6.52766 10.2386 6.45405 10.1641L1.13907 4.84913C1.03306 4.69404 0.985221 4.5065 1.00399 4.31958C1.02276 4.13266 1.10693 3.95838 1.24166 3.82747C1.37639 3.69655 1.55301 3.61742 1.74039 3.60402C1.92777 3.59062 2.11386 3.64382 2.26584 3.75424L7.01744 8.47394L11.769 3.75424C11.9189 3.65709 12.097 3.61306 12.2748 3.62921C12.4527 3.64535 12.6199 3.72073 12.7498 3.84328C12.8797 3.96582 12.9647 4.12842 12.9912 4.30502C13.0177 4.48162 12.9841 4.662 12.8958 4.81724L7.58083 10.1322C7.50996 10.2125 7.42344 10.2775 7.32656 10.3232C7.22968 10.3689 7.12449 10.3944 7.01744 10.398Z",fill:"currentColor"},null,-1)]),16)};var it={name:"SearchIcon",extends:Ue};it.render=function(e,t,n,o,i,r){return k(),D("svg",M({width:"14",height:"14",viewBox:"0 0 14 14",fill:"none",xmlns:"http://www.w3.org/2000/svg"},e.pti()),t[0]||(t[0]=[E("path",{"fill-rule":"evenodd","clip-rule":"evenodd",d:"M2.67602 11.0265C3.6661 11.688 4.83011 12.0411 6.02086 12.0411C6.81149 12.0411 7.59438 11.8854 8.32483 11.5828C8.87005 11.357 9.37808 11.0526 9.83317 10.6803L12.9769 13.8241C13.0323 13.8801 13.0983 13.9245 13.171 13.9548C13.2438 13.985 13.3219 14.0003 13.4007 14C13.4795 14.0003 13.5575 13.985 13.6303 13.9548C13.7031 13.9245 13.7691 13.8801 13.8244 13.8241C13.9367 13.7116 13.9998 13.5592 13.9998 13.4003C13.9998 13.2414 13.9367 13.089 13.8244 12.9765L10.6807 9.8328C11.053 9.37773 11.3573 8.86972 11.5831 8.32452C11.8857 7.59408 12.0414 6.81119 12.0414 6.02056C12.0414 4.8298 11.6883 3.66579 11.0268 2.67572C10.3652 1.68564 9.42494 0.913972 8.32483 0.45829C7.22472 0.00260857 6.01418 -0.116618 4.84631 0.115686C3.67844 0.34799 2.60568 0.921393 1.76369 1.76338C0.921698 2.60537 0.348296 3.67813 0.115991 4.84601C-0.116313 6.01388 0.00291375 7.22441 0.458595 8.32452C0.914277 9.42464 1.68595 10.3649 2.67602 11.0265ZM3.35565 2.0158C4.14456 1.48867 5.07206 1.20731 6.02086 1.20731C7.29317 1.20731 8.51338 1.71274 9.41304 2.6124C10.3127 3.51206 10.8181 4.73226 10.8181 6.00457C10.8181 6.95337 10.5368 7.88088 10.0096 8.66978C9.48251 9.45868 8.73328 10.0736 7.85669 10.4367C6.98011 10.7997 6.01554 10.8947 5.08496 10.7096C4.15439 10.5245 3.2996 10.0676 2.62869 9.39674C1.95778 8.72583 1.50089 7.87104 1.31579 6.94046C1.13068 6.00989 1.22568 5.04532 1.58878 4.16874C1.95187 3.29215 2.56675 2.54292 3.35565 2.0158Z",fill:"currentColor"},null,-1)]),16)};var rt={name:"TimesIcon",extends:Ue};rt.render=function(e,t,n,o,i,r){return k(),D("svg",M({width:"14",height:"14",viewBox:"0 0 14 14",fill:"none",xmlns:"http://www.w3.org/2000/svg"},e.pti()),t[0]||(t[0]=[E("path",{d:"M8.01186 7.00933L12.27 2.75116C12.341 2.68501 12.398 2.60524 12.4375 2.51661C12.4769 2.42798 12.4982 2.3323 12.4999 2.23529C12.5016 2.13827 12.4838 2.0419 12.4474 1.95194C12.4111 1.86197 12.357 1.78024 12.2884 1.71163C12.2198 1.64302 12.138 1.58893 12.0481 1.55259C11.9581 1.51625 11.8617 1.4984 11.7647 1.50011C11.6677 1.50182 11.572 1.52306 11.4834 1.56255C11.3948 1.60204 11.315 1.65898 11.2488 1.72997L6.99067 5.98814L2.7325 1.72997C2.59553 1.60234 2.41437 1.53286 2.22718 1.53616C2.03999 1.53946 1.8614 1.61529 1.72901 1.74767C1.59663 1.88006 1.5208 2.05865 1.5175 2.24584C1.5142 2.43303 1.58368 2.61419 1.71131 2.75116L5.96948 7.00933L1.71131 11.2675C1.576 11.403 1.5 11.5866 1.5 11.7781C1.5 11.9696 1.576 12.1532 1.71131 12.2887C1.84679 12.424 2.03043 12.5 2.2219 12.5C2.41338 12.5 2.59702 12.424 2.7325 12.2887L6.99067 8.03052L11.2488 12.2887C11.3843 12.424 11.568 12.5 11.7594 12.5C11.9509 12.5 12.1346 12.424 12.27 12.2887C12.4053 12.1532 12.4813 11.9696 12.4813 11.7781C12.4813 11.5866 12.4053 11.403 12.27 11.2675L8.01186 7.00933Z",fill:"currentColor"},null,-1)]),16)};var lt={name:"BaseEditableHolder",extends:De,emits:["update:modelValue","value-change"],props:{modelValue:{type:null,default:void 0},defaultValue:{type:null,default:void 0},name:{type:String,default:void 0},invalid:{type:Boolean,default:void 0},disabled:{type:Boolean,default:!1},formControl:{type:Object,default:void 0}},inject:{$parentInstance:{default:void 0},$pcForm:{default:void 0},$pcFormField:{default:void 0}},data:function(){return{d_value:this.defaultValue||this.modelValue}},watch:{modelValue:function(e){this.d_value=e},defaultValue:function(e){this.d_value=e},$formName:{immediate:!0,handler:function(e){var t,n;this.formField=(null===(t=this.$pcForm)||void 0===t||null===(n=t.register)||void 0===n?void 0:n.call(t,e,this.$formControl))||{}}},$formControl:{immediate:!0,handler:function(e){var t,n;this.formField=(null===(t=this.$pcForm)||void 0===t||null===(n=t.register)||void 0===n?void 0:n.call(t,this.$formName,e))||{}}},$formDefaultValue:{immediate:!0,handler:function(e){this.d_value!==e&&(this.d_value=e)}}},formField:{},methods:{writeValue:function(e,t){var n,o;this.controlled&&(this.d_value=e,this.$emit("update:modelValue",e)),this.$emit("value-change",e),null===(n=(o=this.formField).onChange)||void 0===n||n.call(o,{originalEvent:t,value:e})}},computed:{$filled:function(){return u(this.d_value)},$invalid:function(){var e,t,n,o;return null!==(e=null!==(t=this.invalid)&&void 0!==t?t:null===(n=this.$pcFormField)||void 0===n||null===(n=n.$field)||void 0===n?void 0:n.invalid)&&void 0!==e?e:null===(o=this.$pcForm)||void 0===o||null===(o=o.states)||void 0===o||null===(o=o[this.$formName])||void 0===o?void 0:o.invalid},$formName:function(){var e;return this.name||(null===(e=this.$formControl)||void 0===e?void 0:e.name)},$formControl:function(){var e;return this.formControl||(null===(e=this.$pcFormField)||void 0===e?void 0:e.formControl)},$formDefaultValue:function(){var e,t,n,o;return null!==(e=null!==(t=this.d_value)&&void 0!==t?t:null===(n=this.$pcFormField)||void 0===n?void 0:n.initialValue)&&void 0!==e?e:null===(o=this.$pcForm)||void 0===o||null===(o=o.initialValues)||void 0===o?void 0:o[this.$formName]},controlled:function(){return this.$inProps.hasOwnProperty("modelValue")||!this.$inProps.hasOwnProperty("modelValue")&&!this.$inProps.hasOwnProperty("defaultValue")},filled:function(){return this.$filled}}},at={name:"BaseInput",extends:lt,props:{size:{type:String,default:null},fluid:{type:Boolean,default:null},variant:{type:String,default:null}},inject:{$parentInstance:{default:void 0},$pcFluid:{default:void 0}},computed:{$variant:function(){var e;return null!==(e=this.variant)&&void 0!==e?e:this.$primevue.config.inputStyle||this.$primevue.config.inputVariant},$fluid:function(){var e;return null!==(e=this.fluid)&&void 0!==e?e:!!this.$pcFluid},hasFluid:function(){return this.$fluid}}},ut={name:"MinusIcon",extends:Ue};ut.render=function(e,t,n,o,i,r){return k(),D("svg",M({width:"14",height:"14",viewBox:"0 0 14 14",fill:"none",xmlns:"http://www.w3.org/2000/svg"},e.pti()),t[0]||(t[0]=[E("path",{d:"M13.2222 7.77778H0.777778C0.571498 7.77778 0.373667 7.69584 0.227806 7.54998C0.0819442 7.40412 0 7.20629 0 7.00001C0 6.79373 0.0819442 6.5959 0.227806 6.45003C0.373667 6.30417 0.571498 6.22223 0.777778 6.22223H13.2222C13.4285 6.22223 13.6263 6.30417 13.7722 6.45003C13.9181 6.5959 14 6.79373 14 7.00001C14 7.20629 13.9181 7.40412 13.7722 7.54998C13.6263 7.69584 13.4285 7.77778 13.2222 7.77778Z",fill:"currentColor"},null,-1)]),16)};var st={name:"TimesCircleIcon",extends:Ue};st.render=function(e,t,n,o,i,r){return k(),D("svg",M({width:"14",height:"14",viewBox:"0 0 14 14",fill:"none",xmlns:"http://www.w3.org/2000/svg"},e.pti()),t[0]||(t[0]=[E("path",{"fill-rule":"evenodd","clip-rule":"evenodd",d:"M7 14C5.61553 14 4.26215 13.5895 3.11101 12.8203C1.95987 12.0511 1.06266 10.9579 0.532846 9.67879C0.00303296 8.3997 -0.13559 6.99224 0.134506 5.63437C0.404603 4.2765 1.07129 3.02922 2.05026 2.05026C3.02922 1.07129 4.2765 0.404603 5.63437 0.134506C6.99224 -0.13559 8.3997 0.00303296 9.67879 0.532846C10.9579 1.06266 12.0511 1.95987 12.8203 3.11101C13.5895 4.26215 14 5.61553 14 7C14 8.85652 13.2625 10.637 11.9497 11.9497C10.637 13.2625 8.85652 14 7 14ZM7 1.16667C5.84628 1.16667 4.71846 1.50879 3.75918 2.14976C2.79989 2.79074 2.05222 3.70178 1.61071 4.76768C1.16919 5.83358 1.05367 7.00647 1.27876 8.13803C1.50384 9.26958 2.05941 10.309 2.87521 11.1248C3.69102 11.9406 4.73042 12.4962 5.86198 12.7212C6.99353 12.9463 8.16642 12.8308 9.23232 12.3893C10.2982 11.9478 11.2093 11.2001 11.8502 10.2408C12.4912 9.28154 12.8333 8.15373 12.8333 7C12.8333 5.45291 12.2188 3.96918 11.1248 2.87521C10.0308 1.78125 8.5471 1.16667 7 1.16667ZM4.66662 9.91668C4.58998 9.91704 4.51404 9.90209 4.44325 9.87271C4.37246 9.84333 4.30826 9.8001 4.2544 9.74557C4.14516 9.6362 4.0838 9.48793 4.0838 9.33335C4.0838 9.17876 4.14516 9.0305 4.2544 8.92113L6.17553 7L4.25443 5.07891C4.15139 4.96832 4.09529 4.82207 4.09796 4.67094C4.10063 4.51982 4.16185 4.37563 4.26872 4.26876C4.3756 4.16188 4.51979 4.10066 4.67091 4.09799C4.82204 4.09532 4.96829 4.15142 5.07887 4.25446L6.99997 6.17556L8.92106 4.25446C9.03164 4.15142 9.1779 4.09532 9.32903 4.09799C9.48015 4.10066 9.62434 4.16188 9.73121 4.26876C9.83809 4.37563 9.89931 4.51982 9.90198 4.67094C9.90464 4.82207 9.84855 4.96832 9.74551 5.07891L7.82441 7L9.74554 8.92113C9.85478 9.0305 9.91614 9.17876 9.91614 9.33335C9.91614 9.48793 9.85478 9.6362 9.74554 9.74557C9.69168 9.8001 9.62748 9.84333 9.55669 9.87271C9.4859 9.90209 9.40996 9.91704 9.33332 9.91668C9.25668 9.91704 9.18073 9.90209 9.10995 9.87271C9.03916 9.84333 8.97495 9.8001 8.9211 9.74557L6.99997 7.82444L5.07884 9.74557C5.02499 9.8001 4.96078 9.84333 4.88999 9.87271C4.81921 9.90209 4.74326 9.91704 4.66662 9.91668Z",fill:"currentColor"},null,-1)]),16)};var ct={name:"AngleRightIcon",extends:Ue};ct.render=function(e,t,n,o,i,r){return k(),D("svg",M({width:"14",height:"14",viewBox:"0 0 14 14",fill:"none",xmlns:"http://www.w3.org/2000/svg"},e.pti()),t[0]||(t[0]=[E("path",{d:"M5.25 11.1728C5.14929 11.1694 5.05033 11.1455 4.9592 11.1025C4.86806 11.0595 4.78666 10.9984 4.72 10.9228C4.57955 10.7822 4.50066 10.5916 4.50066 10.3928C4.50066 10.1941 4.57955 10.0035 4.72 9.86283L7.72 6.86283L4.72 3.86283C4.66067 3.71882 4.64765 3.55991 4.68275 3.40816C4.71785 3.25642 4.79932 3.11936 4.91585 3.01602C5.03238 2.91268 5.17819 2.84819 5.33305 2.83149C5.4879 2.81479 5.64411 2.84671 5.78 2.92283L9.28 6.42283C9.42045 6.56346 9.49934 6.75408 9.49934 6.95283C9.49934 7.15158 9.42045 7.34221 9.28 7.48283L5.78 10.9228C5.71333 10.9984 5.63193 11.0595 5.5408 11.1025C5.44966 11.1455 5.35071 11.1694 5.25 11.1728Z",fill:"currentColor"},null,-1)]),16)};export{ve as B,Xe as C,X as F,$e as P,et as U,Ge as a,Re as b,at as c,rt as d,ot as e,it as f,nt as g,tt as h,ut as i,st as j,lt as k,ct as l,De as s}; diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v2/assets/vendor-DrOEdrAG.js b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v2/assets/vendor-DrOEdrAG.js new file mode 100644 index 0000000000000000000000000000000000000000..e43991496554ed0875dd0b02b6b46fb63028bec5 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v2/assets/vendor-DrOEdrAG.js @@ -0,0 +1,7 @@ +import{G as t,r as e,m as n,w as o,p as i,H as r,I as s,J as l,K as a,L as c,M as u,x as d,N as p,O as h,h as f,P as b,a as m,c as g,z as v,q as y,t as x,E as I,Q as k,R as w,e as O,f as S,A as L,k as $,b as C,n as z,S as E,T,F as _,i as M,d as V,U as P,V as F,W as A}from"./vue-DfGw76ex.js";import{f as K,v as D,z as B,A as H,B as j,C as R,D as G,F as U,G as N,H as W,I as q,E as X,c as Y,J as Z,l as J,Z as Q,r as tt,K as et,L as nt,M as ot,N as it,O as rt,P as st,Q as lt,R as at,S as ct,T as ut,e as dt,U as pt,i as ht,V as ft,W as bt,X as mt,Y as gt,_ as vt,$ as yt,a0 as xt,d as It,a1 as kt,a2 as wt,a3 as Ot}from"./primeuix-CNMLIuxO.js";import{B as St,s as Lt,a as $t,b as Ct,c as zt,U as Et,C as Tt,F as _t,d as Mt,e as Vt,f as Pt,g as Ft,h as At,i as Kt,j as Dt,k as Bt,l as Ht}from"./primevue-keNb8gkp.js"; +/*! + * pinia v2.2.1 + * (c) 2024 Eduardo San Martin Morote + * @license MIT + */ +let jt;const Rt=t=>jt=t,Gt=Symbol();function Ut(t){return t&&"object"==typeof t&&"[object Object]"===Object.prototype.toString.call(t)&&"function"!=typeof t.toJSON}var Nt,Wt;function qt(){const o=t(!0),i=o.run((()=>e({})));let r=[],s=[];const l=n({install(t){Rt(l),l._a=t,t.provide(Gt,l),t.config.globalProperties.$pinia=l,s.forEach((t=>r.push(t))),s=[]},use(t){return this._a?r.push(t):s.push(t),this},_p:r,_a:null,_e:o,_s:new Map,state:i});return l}(Wt=Nt||(Nt={})).direct="direct",Wt.patchObject="patch object",Wt.patchFunction="patch function";const Xt=()=>{};function Yt(t,e,n,o=Xt){t.push(e);const i=()=>{const n=t.indexOf(e);n>-1&&(t.splice(n,1),o())};return!n&&c()&&u(i),i}function Zt(t,...e){t.slice().forEach((t=>{t(...e)}))}const Jt=t=>t(),Qt=Symbol(),te=Symbol();function ee(t,e){t instanceof Map&&e instanceof Map?e.forEach(((e,n)=>t.set(n,e))):t instanceof Set&&e instanceof Set&&e.forEach(t.add,t);for(const n in e){if(!e.hasOwnProperty(n))continue;const o=e[n],i=t[n];Ut(i)&&Ut(o)&&t.hasOwnProperty(n)&&!r(o)&&!s(o)?t[n]=ee(i,o):t[n]=o}return t}const ne=Symbol();const{assign:oe}=Object;function ie(n,a,c={},u,p,h){let f;const b=oe({actions:{}},c),m={deep:!0};let g,v,y,x=[],I=[];const k=u.state.value[n];let w;function O(t){let e;g=v=!1,"function"==typeof t?(t(u.state.value[n]),e={type:Nt.patchFunction,storeId:n,events:y}):(ee(u.state.value[n],t),e={type:Nt.patchObject,payload:t,storeId:n,events:y});const o=w=Symbol();d().then((()=>{w===o&&(g=!0)})),v=!0,Zt(x,e,u.state.value[n])}h||k||(u.state.value[n]={}),e({});const S=h?function(){const{state:t}=c,e=t?t():{};this.$patch((t=>{oe(t,e)}))}:Xt;const L=(t,e="")=>{if(Qt in t)return t[te]=e,t;const o=function(){Rt(u);const e=Array.from(arguments),i=[],r=[];let s;Zt(I,{args:e,name:o[te],store:C,after:function(t){i.push(t)},onError:function(t){r.push(t)}});try{s=t.apply(this&&this.$id===n?this:C,e)}catch(l){throw Zt(r,l),l}return s instanceof Promise?s.then((t=>(Zt(i,t),t))).catch((t=>(Zt(r,t),Promise.reject(t)))):(Zt(i,s),s)};return o[Qt]=!0,o[te]=e,o},$={_p:u,$id:n,$onAction:Yt.bind(null,I),$patch:O,$reset:S,$subscribe(t,e={}){const i=Yt(x,t,e.detached,(()=>r())),r=f.run((()=>o((()=>u.state.value[n]),(o=>{("sync"===e.flush?v:g)&&t({storeId:n,type:Nt.direct,events:y},o)}),oe({},m,e))));return i},$dispose:function(){f.stop(),x=[],I=[],u._s.delete(n)}},C=i($);u._s.set(n,C);const z=(u._a&&u._a.runWithContext||Jt)((()=>u._e.run((()=>(f=t()).run((()=>a({action:L})))))));for(const t in z){const e=z[t];if(r(e)&&(!r(T=e)||!T.effect)||s(e))h||(!k||Ut(E=e)&&E.hasOwnProperty(ne)||(r(e)?e.value=k[t]:ee(e,k[t])),u.state.value[n][t]=e);else if("function"==typeof e){const n=L(e,t);z[t]=n,b.actions[t]=e}}var E,T;return oe(C,z),oe(l(C),z),Object.defineProperty(C,"$state",{get:()=>u.state.value[n],set:t=>{O((e=>{oe(e,t)}))}}),u._p.forEach((t=>{oe(C,f.run((()=>t({store:C,app:u._a,pinia:u,options:b}))))})),k&&h&&c.hydrate&&c.hydrate(C.$state,k),g=!0,v=!0,C}function re(t,e,o){let i,r;const s="function"==typeof e;function l(t,o){const l=b();(t=t||(l?p(Gt,null):null))&&Rt(t),(t=jt)._s.has(i)||(s?ie(i,e,r,t):function(t,e,o){const{state:i,actions:r,getters:s}=e,l=o.state.value[t];let a;a=ie(t,(function(){l||(o.state.value[t]=i?i():{});const e=h(o.state.value[t]);return oe(e,r,Object.keys(s||{}).reduce(((e,i)=>(e[i]=n(f((()=>{Rt(o);const e=o._s.get(t);return s[i].call(e,e)}))),e)),{}))}),e,o,0,!0)}(i,r,t));return t._s.get(i)}return"string"==typeof t?(i=t,r=s?o:e):(r=t,i=t.id),l.$id=i,l}function se(t){{t=l(t);const e={};for(const n in t){const o=t[n];(r(o)||s(o))&&(e[n]=a(t,n))}return e}}const le="undefined"!=typeof navigator&&navigator.userAgent.toLowerCase().indexOf("firefox")>0;function ae(t,e,n,o){t.addEventListener?t.addEventListener(e,n,o):t.attachEvent&&t.attachEvent("on".concat(e),n)}function ce(t,e,n,o){t.removeEventListener?t.removeEventListener(e,n,o):t.detachEvent&&t.detachEvent("on".concat(e),n)}function ue(t,e){const n=e.slice(0,e.length-1);for(let o=0;o=0;)e[n-1]+=",",e.splice(n,1),n=e.lastIndexOf("");return e}const pe={backspace:8,"⌫":8,tab:9,clear:12,enter:13,"↩":13,return:13,esc:27,escape:27,space:32,left:37,up:38,right:39,down:40,del:46,delete:46,ins:45,insert:45,home:36,end:35,pageup:33,pagedown:34,capslock:20,num_0:96,num_1:97,num_2:98,num_3:99,num_4:100,num_5:101,num_6:102,num_7:103,num_8:104,num_9:105,num_multiply:106,num_add:107,num_enter:108,num_subtract:109,num_decimal:110,num_divide:111,"⇪":20,",":188,".":190,"/":191,"`":192,"-":le?173:189,"=":le?61:187,";":le?59:186,"'":222,"[":219,"]":221,"\\":220},he={"⇧":16,shift:16,"⌥":18,alt:18,option:18,"⌃":17,ctrl:17,control:17,"⌘":91,cmd:91,command:91},fe={16:"shiftKey",18:"altKey",17:"ctrlKey",91:"metaKey",shiftKey:16,ctrlKey:17,altKey:18,metaKey:91},be={16:!1,18:!1,17:!1,91:!1},me={};for(let Oo=1;Oo<20;Oo++)pe["f".concat(Oo)]=111+Oo;let ge=[],ve=null,ye="all";const xe=new Map,Ie=t=>pe[t.toLowerCase()]||he[t.toLowerCase()]||t.toUpperCase().charCodeAt(0);function ke(t){ye=t||"all"}function we(){return ye||"all"}function Oe(t){if(void 0===t)Object.keys(me).forEach((t=>{Array.isArray(me[t])&&me[t].forEach((t=>Se(t))),delete me[t]})),ze(null);else if(Array.isArray(t))t.forEach((t=>{t.key&&Se(t)}));else if("object"==typeof t)t.key&&Se(t);else if("string"==typeof t){for(var e=arguments.length,n=new Array(e>1?e-1:0),o=1;o{let{key:e,scope:n,method:o,splitKey:i="+"}=t;de(e).forEach((t=>{const e=t.split(i),r=e.length,s=e[r-1],l="*"===s?"*":Ie(s);if(!me[l])return;n||(n=we());const a=r>1?ue(he,e):[],c=[];me[l]=me[l].filter((t=>{const e=(!o||t.method===o)&&t.scope===n&&function(t,e){const n=t.length>=e.length?t:e,o=t.length>=e.length?e:t;let i=!0;for(let r=0;rze(t)))}))};function Le(t,e,n,o){if(e.element!==o)return;let i;if(e.scope===n||"all"===e.scope){i=e.mods.length>0;for(const t in be)Object.prototype.hasOwnProperty.call(be,t)&&(!be[t]&&e.mods.indexOf(+t)>-1||be[t]&&-1===e.mods.indexOf(+t))&&(i=!1);(0!==e.mods.length||be[16]||be[18]||be[17]||be[91])&&!i&&"*"!==e.shortcut||(e.keys=[],e.keys=e.keys.concat(ge),!1===e.method(t,e)&&(t.preventDefault?t.preventDefault():t.returnValue=!1,t.stopPropagation&&t.stopPropagation(),t.cancelBubble&&(t.cancelBubble=!0)))}}function $e(t,e){const n=me["*"];let o=t.keyCode||t.which||t.charCode;if(!Ce.filter.call(this,t))return;if(93!==o&&224!==o||(o=91),-1===ge.indexOf(o)&&229!==o&&ge.push(o),["ctrlKey","altKey","shiftKey","metaKey"].forEach((e=>{const n=fe[e];t[e]&&-1===ge.indexOf(n)?ge.push(n):!t[e]&&ge.indexOf(n)>-1?ge.splice(ge.indexOf(n),1):"metaKey"===e&&t[e]&&3===ge.length&&(t.ctrlKey||t.shiftKey||t.altKey||(ge=ge.slice(ge.indexOf(n))))})),o in be){be[o]=!0;for(const t in he)he[t]===o&&(Ce[t]=!0);if(!n)return}for(const l in be)Object.prototype.hasOwnProperty.call(be,l)&&(be[l]=t[fe[l]]);t.getModifierState&&(!t.altKey||t.ctrlKey)&&t.getModifierState("AltGraph")&&(-1===ge.indexOf(17)&&ge.push(17),-1===ge.indexOf(18)&&ge.push(18),be[17]=!0,be[18]=!0);const i=we();if(n)for(let l=0;l1&&(i=ue(he,t)),(t="*"===(t=t[t.length-1])?"*":Ie(t))in me||(me[t]=[]),me[t].push({keyup:a,keydown:c,scope:r,mods:i,shortcut:o[l],method:n,key:o[l],splitKey:u,element:s});if(void 0!==s&&window){if(!xe.has(s)){const t=function(){return $e(arguments.length>0&&void 0!==arguments[0]?arguments[0]:window.event,s)},e=function(){let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:window.event;$e(t,s),function(t){let e=t.keyCode||t.which||t.charCode;const n=ge.indexOf(e);if(n>=0&&ge.splice(n,1),t.key&&"meta"===t.key.toLowerCase()&&ge.splice(0,ge.length),93!==e&&224!==e||(e=91),e in be){be[e]=!1;for(const t in he)he[t]===e&&(Ce[t]=!1)}}(t)};xe.set(s,{keydownListener:t,keyupListenr:e,capture:d}),ae(s,"keydown",t,d),ae(s,"keyup",e,d)}if(!ve){const t=()=>{ge=[]};ve={listener:t,capture:d},ae(window,"focus",t,d)}}}function ze(t){const e=Object.values(me).flat();if(e.findIndex((e=>{let{element:n}=e;return n===t}))<0){const{keydownListener:e,keyupListenr:n,capture:o}=xe.get(t)||{};e&&n&&(ce(t,"keyup",n,o),ce(t,"keydown",e,o),xe.delete(t))}if(e.length<=0||xe.size<=0){if(Object.keys(xe).forEach((t=>{const{keydownListener:e,keyupListenr:n,capture:o}=xe.get(t)||{};e&&n&&(ce(t,"keyup",n,o),ce(t,"keydown",e,o),xe.delete(t))})),xe.clear(),Object.keys(me).forEach((t=>delete me[t])),ve){const{listener:t,capture:e}=ve;ce(window,"focus",t,e),ve=null}}}const Ee={getPressedKeyString:function(){return ge.map((t=>{return e=t,Object.keys(pe).find((t=>pe[t]===e))||(t=>Object.keys(he).find((e=>he[e]===t)))(t)||String.fromCharCode(t);var e}))},setScope:ke,getScope:we,deleteScope:function(t,e){let n,o;t||(t=we());for(const i in me)if(Object.prototype.hasOwnProperty.call(me,i))for(n=me[i],o=0;o{let{element:e}=t;return ze(e)}))}else o++;we()===t&&ke(e||"all")},getPressedKeyCodes:function(){return ge.slice(0)},getAllKeyCodes:function(){const t=[];return Object.keys(me).forEach((e=>{me[e].forEach((e=>{let{key:n,scope:o,mods:i,shortcut:r}=e;t.push({scope:o,shortcut:r,mods:i,keys:n.split("+").map((t=>Ie(t)))})}))})),t},isPressed:function(t){return"string"==typeof t&&(t=Ie(t)),-1!==ge.indexOf(t)},filter:function(t){const e=t.target||t.srcElement,{tagName:n}=e;let o=!0;const i="INPUT"===n&&!["checkbox","radio","range","button","file","reset","submit","color"].includes(e.type);return(e.isContentEditable||(i||"TEXTAREA"===n||"SELECT"===n)&&!e.readOnly)&&(o=!1),o},trigger:function(t){let e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"all";Object.keys(me).forEach((n=>{me[n].filter((n=>n.scope===e&&n.shortcut===t)).forEach((t=>{t&&t.method&&t.method()}))}))},unbind:Oe,keyMap:pe,modifier:he,modifierMap:fe};for(const Oo in Ee)Object.prototype.hasOwnProperty.call(Ee,Oo)&&(Ce[Oo]=Ee[Oo]);if("undefined"!=typeof window){const t=window.hotkeys;Ce.noConflict=e=>(e&&window.hotkeys===Ce&&(window.hotkeys=t),Ce),window.hotkeys=Ce}var Te={root:function(t){var e=t.props,n=t.instance;return["p-badge p-component",{"p-badge-circle":K(e.value)&&1===String(e.value).length,"p-badge-dot":D(e.value)&&!n.$slots.default,"p-badge-sm":"small"===e.size,"p-badge-lg":"large"===e.size,"p-badge-xl":"xlarge"===e.size,"p-badge-info":"info"===e.severity,"p-badge-success":"success"===e.severity,"p-badge-warn":"warn"===e.severity,"p-badge-danger":"danger"===e.severity,"p-badge-secondary":"secondary"===e.severity,"p-badge-contrast":"contrast"===e.severity}]}},_e=St.extend({name:"badge",theme:function(t){var e=t.dt;return"\n.p-badge {\n display: inline-flex;\n border-radius: ".concat(e("badge.border.radius"),";\n align-items: center;\n justify-content: center;\n padding: ").concat(e("badge.padding"),";\n background: ").concat(e("badge.primary.background"),";\n color: ").concat(e("badge.primary.color"),";\n font-size: ").concat(e("badge.font.size"),";\n font-weight: ").concat(e("badge.font.weight"),";\n min-width: ").concat(e("badge.min.width"),";\n height: ").concat(e("badge.height"),";\n}\n\n.p-badge-dot {\n width: ").concat(e("badge.dot.size"),";\n min-width: ").concat(e("badge.dot.size"),";\n height: ").concat(e("badge.dot.size"),";\n border-radius: 50%;\n padding: 0;\n}\n\n.p-badge-circle {\n padding: 0;\n border-radius: 50%;\n}\n\n.p-badge-secondary {\n background: ").concat(e("badge.secondary.background"),";\n color: ").concat(e("badge.secondary.color"),";\n}\n\n.p-badge-success {\n background: ").concat(e("badge.success.background"),";\n color: ").concat(e("badge.success.color"),";\n}\n\n.p-badge-info {\n background: ").concat(e("badge.info.background"),";\n color: ").concat(e("badge.info.color"),";\n}\n\n.p-badge-warn {\n background: ").concat(e("badge.warn.background"),";\n color: ").concat(e("badge.warn.color"),";\n}\n\n.p-badge-danger {\n background: ").concat(e("badge.danger.background"),";\n color: ").concat(e("badge.danger.color"),";\n}\n\n.p-badge-contrast {\n background: ").concat(e("badge.contrast.background"),";\n color: ").concat(e("badge.contrast.color"),";\n}\n\n.p-badge-sm {\n font-size: ").concat(e("badge.sm.font.size"),";\n min-width: ").concat(e("badge.sm.min.width"),";\n height: ").concat(e("badge.sm.height"),";\n}\n\n.p-badge-lg {\n font-size: ").concat(e("badge.lg.font.size"),";\n min-width: ").concat(e("badge.lg.min.width"),";\n height: ").concat(e("badge.lg.height"),";\n}\n\n.p-badge-xl {\n font-size: ").concat(e("badge.xl.font.size"),";\n min-width: ").concat(e("badge.xl.min.width"),";\n height: ").concat(e("badge.xl.height"),";\n}\n")},classes:Te}),Me={name:"Badge",extends:{name:"BaseBadge",extends:Lt,props:{value:{type:[String,Number],default:null},severity:{type:String,default:null},size:{type:String,default:null}},style:_e,provide:function(){return{$pcBadge:this,$parentInstance:this}}},inheritAttrs:!1};Me.render=function(t,e,n,o,i,r){return m(),g("span",I({class:t.cx("root")},t.ptmi("root")),[v(t.$slots,"default",{},(function(){return[y(x(t.value),1)]}))],16)};var Ve=St.extend({name:"ripple-directive",theme:function(t){var e=t.dt;return"\n.p-ink {\n display: block;\n position: absolute;\n background: ".concat(e("ripple.background"),";\n border-radius: 100%;\n transform: scale(0);\n pointer-events: none;\n}\n\n.p-ink-active {\n animation: ripple 0.4s linear;\n}\n\n@keyframes ripple {\n 100% {\n opacity: 0;\n transform: scale(2.5);\n }\n}\n")},classes:{root:"p-ink"}});function Pe(t){return(Pe="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}function Fe(t){return function(t){if(Array.isArray(t))return Ae(t)}(t)||function(t){if("undefined"!=typeof Symbol&&null!=t[Symbol.iterator]||null!=t["@@iterator"])return Array.from(t)}(t)||function(t,e){if(t){if("string"==typeof t)return Ae(t,e);var n={}.toString.call(t).slice(8,-1);return"Object"===n&&t.constructor&&(n=t.constructor.name),"Map"===n||"Set"===n?Array.from(t):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?Ae(t,e):void 0}}(t)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function Ae(t,e){(null==e||e>t.length)&&(e=t.length);for(var n=0,o=Array(e);n .p-virtualscroller-content {\n display: flex;\n}\n\n.p-virtualscroller-inline .p-virtualscroller-content {\n position: static;\n}\n",theme:function(t){var e=t.dt;return"\n.p-virtualscroller-loader {\n background: ".concat(e("virtualscroller.loader.mask.background"),";\n color: ").concat(e("virtualscroller.loader.mask.color"),";\n}\n\n.p-virtualscroller-loading-icon {\n font-size: ").concat(e("virtualscroller.loader.icon.size"),";\n width: ").concat(e("virtualscroller.loader.icon.size"),";\n height: ").concat(e("virtualscroller.loader.icon.size"),";\n}\n")}});function Je(t){return(Je="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}function Qe(t,e){var n=Object.keys(t);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(t);e&&(o=o.filter((function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable}))),n.push.apply(n,o)}return n}function tn(t){for(var e=1;e1&&void 0!==arguments[1]?arguments[1]:"auto",o=this.isBoth(),i=this.isHorizontal();if(o?t.every((function(t){return t>-1})):t>-1){var r=this.first,s=this.element,l=s.scrollTop,a=void 0===l?0:l,c=s.scrollLeft,u=void 0===c?0:c,d=this.calculateNumItems().numToleratedItems,p=this.getContentPosition(),h=this.itemSize,f=function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:0;return t<=(arguments.length>1?arguments[1]:void 0)?0:t},b=function(t,e,n){return t*e+n},m=function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:0,o=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0;return e.scrollTo({left:t,top:o,behavior:n})},g=o?{rows:0,cols:0}:0,v=!1,y=!1;o?(m(b((g={rows:f(t[0],d[0]),cols:f(t[1],d[1])}).cols,h[1],p.left),b(g.rows,h[0],p.top)),y=this.lastScrollPos.top!==a||this.lastScrollPos.left!==u,v=g.rows!==r.rows||g.cols!==r.cols):(g=f(t,d),i?m(b(g,h,p.left),a):m(u,b(g,h,p.top)),y=this.lastScrollPos!==(i?u:a),v=g!==r),this.isRangeChanged=v,y&&(this.first=g)}},scrollInView:function(t,e){var n=this,o=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"auto";if(e){var i=this.isBoth(),r=this.isHorizontal();if(i?t.every((function(t){return t>-1})):t>-1){var s=this.getRenderedRange(),l=s.first,a=s.viewport,c=function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:0,e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0;return n.scrollTo({left:t,top:e,behavior:o})},u="to-end"===e;if("to-start"===e){if(i)a.first.rows-l.rows>t[0]?c(a.first.cols*this.itemSize[1],(a.first.rows-1)*this.itemSize[0]):a.first.cols-l.cols>t[1]&&c((a.first.cols-1)*this.itemSize[1],a.first.rows*this.itemSize[0]);else if(a.first-l>t){var d=(a.first-1)*this.itemSize;r?c(d,0):c(0,d)}}else if(u)if(i)a.last.rows-l.rows<=t[0]+1?c(a.first.cols*this.itemSize[1],(a.first.rows+1)*this.itemSize[0]):a.last.cols-l.cols<=t[1]+1&&c((a.first.cols+1)*this.itemSize[1],a.first.rows*this.itemSize[0]);else if(a.last-l<=t+1){var p=(a.first+1)*this.itemSize;r?c(p,0):c(0,p)}}}else this.scrollToIndex(t,o)},getRenderedRange:function(){var t=function(t,e){return Math.floor(t/(e||t))},e=this.first,n=0;if(this.element){var o=this.isBoth(),i=this.isHorizontal(),r=this.element,s=r.scrollTop,l=r.scrollLeft;if(o)n={rows:(e={rows:t(s,this.itemSize[0]),cols:t(l,this.itemSize[1])}).rows+this.numItemsInViewport.rows,cols:e.cols+this.numItemsInViewport.cols};else n=(e=t(i?l:s,this.itemSize))+this.numItemsInViewport}return{first:this.first,last:this.last,viewport:{first:e,last:n}}},calculateNumItems:function(){var t=this.isBoth(),e=this.isHorizontal(),n=this.itemSize,o=this.getContentPosition(),i=this.element?this.element.offsetWidth-o.left:0,r=this.element?this.element.offsetHeight-o.top:0,s=function(t,e){return Math.ceil(t/(e||t))},l=function(t){return Math.ceil(t/2)},a=t?{rows:s(r,n[0]),cols:s(i,n[1])}:s(e?i:r,n);return{numItemsInViewport:a,numToleratedItems:this.d_numToleratedItems||(t?[l(a.rows),l(a.cols)]:l(a))}},calculateOptions:function(){var t=this,e=this.isBoth(),n=this.first,o=this.calculateNumItems(),i=o.numItemsInViewport,r=o.numToleratedItems,s=function(e,n,o){var i=arguments.length>3&&void 0!==arguments[3]&&arguments[3];return t.getLast(e+n+(e0&&void 0!==arguments[0]?arguments[0]:0,o=arguments.length>1?arguments[1]:void 0;return this.items?Math.min(o?(null===(t=this.columns||this.items[0])||void 0===t?void 0:t.length)||0:(null===(e=this.items)||void 0===e?void 0:e.length)||0,n):0},getContentPosition:function(){if(this.content){var t=getComputedStyle(this.content),e=parseFloat(t.paddingLeft)+Math.max(parseFloat(t.left)||0,0),n=parseFloat(t.paddingRight)+Math.max(parseFloat(t.right)||0,0),o=parseFloat(t.paddingTop)+Math.max(parseFloat(t.top)||0,0),i=parseFloat(t.paddingBottom)+Math.max(parseFloat(t.bottom)||0,0);return{left:e,right:n,top:o,bottom:i,x:e+n,y:o+i}}return{left:0,right:0,top:0,bottom:0,x:0,y:0}},setSize:function(){var t=this;if(this.element){var e=this.isBoth(),n=this.isHorizontal(),o=this.element.parentElement,i=this.scrollWidth||"".concat(this.element.offsetWidth||o.offsetWidth,"px"),r=this.scrollHeight||"".concat(this.element.offsetHeight||o.offsetHeight,"px"),s=function(e,n){return t.element.style[e]=n};e||n?(s("height",r),s("width",i)):s("height",r)}},setSpacerSize:function(){var t=this,e=this.items;if(e){var n=this.isBoth(),o=this.isHorizontal(),i=this.getContentPosition(),r=function(e,n,o){var i=arguments.length>3&&void 0!==arguments[3]?arguments[3]:0;return t.spacerStyle=tn(tn({},t.spacerStyle),en({},"".concat(e),(n||[]).length*o+i+"px"))};n?(r("height",e,this.itemSize[0],i.y),r("width",this.columns||e[1],this.itemSize[1],i.x)):o?r("width",this.columns||e,this.itemSize,i.x):r("height",e,this.itemSize,i.y)}},setContentPosition:function(t){var e=this;if(this.content&&!this.appendOnly){var n=this.isBoth(),o=this.isHorizontal(),i=t?t.first:this.first,r=function(t,e){return t*e},s=function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:0,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0;return e.contentStyle=tn(tn({},e.contentStyle),{transform:"translate3d(".concat(t,"px, ").concat(n,"px, 0)")})};if(n)s(r(i.cols,this.itemSize[1]),r(i.rows,this.itemSize[0]));else{var l=r(i,this.itemSize);o?s(l,0):s(0,l)}}},onScrollPositionChange:function(t){var e=this,n=t.target,o=this.isBoth(),i=this.isHorizontal(),r=this.getContentPosition(),s=function(t,e){return t?t>e?t-e:t:0},l=function(t,e){return Math.floor(t/(e||t))},a=function(t,e,n,o,i,r){return t<=i?i:r?n-o-i:e+i-1},c=function(t,e,n,o,i,r,s){return t<=r?0:Math.max(0,s?te?n:t-2*r)},u=function(t,n,o,i,r,s){var l=n+i+2*r;return t>=r&&(l+=r+1),e.getLast(l,s)},d=s(n.scrollTop,r.top),p=s(n.scrollLeft,r.left),h=o?{rows:0,cols:0}:0,f=this.last,b=!1,m=this.lastScrollPos;if(o){var g=this.lastScrollPos.top<=d,v=this.lastScrollPos.left<=p;if(!this.appendOnly||this.appendOnly&&(g||v)){var y={rows:l(d,this.itemSize[0]),cols:l(p,this.itemSize[1])},x={rows:a(y.rows,this.first.rows,this.last.rows,this.numItemsInViewport.rows,this.d_numToleratedItems[0],g),cols:a(y.cols,this.first.cols,this.last.cols,this.numItemsInViewport.cols,this.d_numToleratedItems[1],v)};h={rows:c(y.rows,x.rows,this.first.rows,this.last.rows,this.numItemsInViewport.rows,this.d_numToleratedItems[0],g),cols:c(y.cols,x.cols,this.first.cols,this.last.cols,this.numItemsInViewport.cols,this.d_numToleratedItems[1],v)},f={rows:u(y.rows,h.rows,this.last.rows,this.numItemsInViewport.rows,this.d_numToleratedItems[0]),cols:u(y.cols,h.cols,this.last.cols,this.numItemsInViewport.cols,this.d_numToleratedItems[1],!0)},b=h.rows!==this.first.rows||f.rows!==this.last.rows||h.cols!==this.first.cols||f.cols!==this.last.cols||this.isRangeChanged,m={top:d,left:p}}}else{var I=i?p:d,k=this.lastScrollPos<=I;if(!this.appendOnly||this.appendOnly&&k){var w=l(I,this.itemSize);f=u(w,h=c(w,a(w,this.first,this.last,this.numItemsInViewport,this.d_numToleratedItems,k),this.first,this.last,this.numItemsInViewport,this.d_numToleratedItems,k),this.last,this.numItemsInViewport,this.d_numToleratedItems),b=h!==this.first||f!==this.last||this.isRangeChanged,m=I}}return{first:h,last:f,isRangeChanged:b,scrollPos:m}},onScrollChange:function(t){var e=this.onScrollPositionChange(t),n=e.first,o=e.last,i=e.isRangeChanged,r=e.scrollPos;if(i){var s={first:n,last:o};if(this.setContentPosition(s),this.first=n,this.last=o,this.lastScrollPos=r,this.$emit("scroll-index-change",s),this.lazy&&this.isPageChanged(n)){var l,a,c={first:this.step?Math.min(this.getPageByFirst(n)*this.step,((null===(l=this.items)||void 0===l?void 0:l.length)||0)-this.step):n,last:Math.min(this.step?(this.getPageByFirst(n)+1)*this.step:o,(null===(a=this.items)||void 0===a?void 0:a.length)||0)};(this.lazyLoadState.first!==c.first||this.lazyLoadState.last!==c.last)&&this.$emit("lazy-load",c),this.lazyLoadState=c}}},onScroll:function(t){var e=this;if(this.$emit("scroll",t),this.delay){if(this.scrollTimeout&&clearTimeout(this.scrollTimeout),this.isPageChanged()){if(!this.d_loading&&this.showLoader)(this.onScrollPositionChange(t).isRangeChanged||!!this.step&&this.isPageChanged())&&(this.d_loading=!0);this.scrollTimeout=setTimeout((function(){e.onScrollChange(t),!e.d_loading||!e.showLoader||e.lazy&&void 0!==e.loading||(e.d_loading=!1,e.page=e.getPageByFirst())}),this.delay)}}else this.onScrollChange(t)},onResize:function(){var t=this;this.resizeTimeout&&clearTimeout(this.resizeTimeout),this.resizeTimeout=setTimeout((function(){if(Z(t.element)){var e=t.isBoth(),n=t.isVertical(),o=t.isHorizontal(),i=[R(t.element),j(t.element)],r=i[0],s=i[1],l=r!==t.defaultWidth,a=s!==t.defaultHeight;(e?l||a:o?l:!!n&&a)&&(t.d_numToleratedItems=t.numToleratedItems,t.defaultWidth=r,t.defaultHeight=s,t.defaultContentWidth=R(t.content),t.defaultContentHeight=j(t.content),t.init())}}),this.resizeDelay)},bindResizeListener:function(){this.resizeListener||(this.resizeListener=this.onResize.bind(this),window.addEventListener("resize",this.resizeListener),window.addEventListener("orientationchange",this.resizeListener))},unbindResizeListener:function(){this.resizeListener&&(window.removeEventListener("resize",this.resizeListener),window.removeEventListener("orientationchange",this.resizeListener),this.resizeListener=null)},getOptions:function(t){var e=(this.items||[]).length,n=this.isBoth()?this.first.rows+t:this.first+t;return{index:n,count:e,first:0===n,last:n===e-1,even:n%2==0,odd:n%2!=0}},getLoaderOptions:function(t,e){var n=this.loaderArr.length;return tn({index:t,count:n,first:0===t,last:t===n-1,even:t%2==0,odd:t%2!=0},e)},getPageByFirst:function(t){return Math.floor(((null!=t?t:this.first)+4*this.d_numToleratedItems)/(this.step||1))},isPageChanged:function(t){return!(this.step&&!this.lazy)||this.page!==this.getPageByFirst(null!=t?t:this.first)},setContentEl:function(t){this.content=t||this.content||J(this.element,'[data-pc-section="content"]')},elementRef:function(t){this.element=t},contentRef:function(t){this.content=t}},computed:{containerClass:function(){return["p-virtualscroller",this.class,{"p-virtualscroller-inline":this.inline,"p-virtualscroller-both p-both-scroll":this.isBoth(),"p-virtualscroller-horizontal p-horizontal-scroll":this.isHorizontal()}]},contentClass:function(){return["p-virtualscroller-content",{"p-virtualscroller-loading":this.d_loading}]},loaderClass:function(){return["p-virtualscroller-loader",{"p-virtualscroller-loader-mask":!this.$slots.loader}]},loadedItems:function(){var t=this;return this.items&&!this.d_loading?this.isBoth()?this.items.slice(this.appendOnly?0:this.first.rows,this.last.rows).map((function(e){return t.columns?e:e.slice(t.appendOnly?0:t.first.cols,t.last.cols)})):this.isHorizontal()&&this.columns?this.items:this.items.slice(this.appendOnly?0:this.first,this.last):[]},loadedRows:function(){return this.d_loading?this.loaderDisabled?this.loaderArr:[]:this.loadedItems},loadedColumns:function(){if(this.columns){var t=this.isBoth(),e=this.isHorizontal();if(t||e)return this.d_loading&&this.loaderDisabled?t?this.loaderArr[0]:this.loaderArr:this.columns.slice(t?this.first.cols:this.first,t?this.last.cols:this.last)}return this.columns}},components:{SpinnerIcon:Ct}},on=["tabindex"];nn.render=function(t,e,n,o,i,r){var s=k("SpinnerIcon");return t.disabled?(m(),g(_,{key:1},[v(t.$slots,"default"),v(t.$slots,"content",{items:t.items,rows:t.items,columns:r.loadedColumns})],64)):(m(),g("div",I({key:0,ref:r.elementRef,class:r.containerClass,tabindex:t.tabindex,style:t.style,onScroll:e[0]||(e[0]=function(){return r.onScroll&&r.onScroll.apply(r,arguments)})},t.ptmi("root")),[v(t.$slots,"content",{styleClass:r.contentClass,items:r.loadedItems,getItemOptions:r.getOptions,loading:i.d_loading,getLoaderOptions:r.getLoaderOptions,itemSize:t.itemSize,rows:r.loadedRows,columns:r.loadedColumns,contentRef:r.contentRef,spacerStyle:i.spacerStyle,contentStyle:i.contentStyle,vertical:r.isVertical(),horizontal:r.isHorizontal(),both:r.isBoth()},(function(){return[C("div",I({ref:r.contentRef,class:r.contentClass,style:i.contentStyle},t.ptm("content")),[(m(!0),g(_,null,M(r.loadedItems,(function(e,n){return v(t.$slots,"item",{key:n,item:e,options:r.getOptions(n)})})),128))],16)]})),t.showSpacer?(m(),g("div",I({key:0,class:"p-virtualscroller-spacer",style:i.spacerStyle},t.ptm("spacer")),null,16)):$("",!0),!t.loaderDisabled&&t.showLoader&&i.d_loading?(m(),g("div",I({key:1,class:r.loaderClass},t.ptm("loader")),[t.$slots&&t.$slots.loader?(m(!0),g(_,{key:0},M(i.loaderArr,(function(e,n){return v(t.$slots,"loader",{key:n,options:r.getLoaderOptions(n,r.isBoth()&&{numCols:t.d_numItemsInViewport.cols})})})),128)):$("",!0),v(t.$slots,"loadingicon",{},(function(){return[V(s,I({spin:"",class:"p-virtualscroller-loading-icon"},t.ptm("loadingIcon")),null,16)]}))],16)):$("",!0)],16,on))};var rn=St.extend({name:"select",theme:function(t){var e=t.dt;return"\n.p-select {\n display: inline-flex;\n cursor: pointer;\n position: relative;\n user-select: none;\n background: ".concat(e("select.background"),";\n border: 1px solid ").concat(e("select.border.color"),";\n transition: background ").concat(e("select.transition.duration"),", color ").concat(e("select.transition.duration"),", border-color ").concat(e("select.transition.duration"),",\n outline-color ").concat(e("select.transition.duration"),", box-shadow ").concat(e("select.transition.duration"),";\n border-radius: ").concat(e("select.border.radius"),";\n outline-color: transparent;\n box-shadow: ").concat(e("select.shadow"),";\n}\n\n.p-select:not(.p-disabled):hover {\n border-color: ").concat(e("select.hover.border.color"),";\n}\n\n.p-select:not(.p-disabled).p-focus {\n border-color: ").concat(e("select.focus.border.color"),";\n box-shadow: ").concat(e("select.focus.ring.shadow"),";\n outline: ").concat(e("select.focus.ring.width")," ").concat(e("select.focus.ring.style")," ").concat(e("select.focus.ring.color"),";\n outline-offset: ").concat(e("select.focus.ring.offset"),";\n}\n\n.p-select.p-variant-filled {\n background: ").concat(e("select.filled.background"),";\n}\n\n.p-select.p-variant-filled:not(.p-disabled):hover {\n background: ").concat(e("select.filled.hover.background"),";\n}\n\n.p-select.p-variant-filled:not(.p-disabled).p-focus {\n background: ").concat(e("select.filled.focus.background"),";\n}\n\n.p-select.p-invalid {\n border-color: ").concat(e("select.invalid.border.color"),";\n}\n\n.p-select.p-disabled {\n opacity: 1;\n background: ").concat(e("select.disabled.background"),";\n}\n\n.p-select-clear-icon {\n position: absolute;\n top: 50%;\n margin-top: -0.5rem;\n color: ").concat(e("select.clear.icon.color"),";\n inset-inline-end: ").concat(e("select.dropdown.width"),";\n}\n\n.p-select-dropdown {\n display: flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n background: transparent;\n color: ").concat(e("select.dropdown.color"),";\n width: ").concat(e("select.dropdown.width"),";\n border-start-end-radius: ").concat(e("select.border.radius"),";\n border-end-end-radius: ").concat(e("select.border.radius"),";\n}\n\n.p-select-label {\n display: block;\n white-space: nowrap;\n overflow: hidden;\n flex: 1 1 auto;\n width: 1%;\n padding: ").concat(e("select.padding.y")," ").concat(e("select.padding.x"),";\n text-overflow: ellipsis;\n cursor: pointer;\n color: ").concat(e("select.color"),";\n background: transparent;\n border: 0 none;\n outline: 0 none;\n}\n\n.p-select-label.p-placeholder {\n color: ").concat(e("select.placeholder.color"),";\n}\n\n.p-select.p-invalid .p-select-label.p-placeholder {\n color: ").concat(e("select.invalid.placeholder.color"),";\n}\n\n.p-select:has(.p-select-clear-icon) .p-select-label {\n padding-inline-end: calc(1rem + ").concat(e("select.padding.x"),");\n}\n\n.p-select.p-disabled .p-select-label {\n color: ").concat(e("select.disabled.color"),";\n}\n\n.p-select-label-empty {\n overflow: hidden;\n opacity: 0;\n}\n\ninput.p-select-label {\n cursor: default;\n}\n\n.p-select .p-select-overlay {\n min-width: 100%;\n}\n\n.p-select-overlay {\n position: absolute;\n top: 0;\n left: 0;\n background: ").concat(e("select.overlay.background"),";\n color: ").concat(e("select.overlay.color"),";\n border: 1px solid ").concat(e("select.overlay.border.color"),";\n border-radius: ").concat(e("select.overlay.border.radius"),";\n box-shadow: ").concat(e("select.overlay.shadow"),";\n}\n\n.p-select-header {\n padding: ").concat(e("select.list.header.padding"),";\n}\n\n.p-select-filter {\n width: 100%;\n}\n\n.p-select-list-container {\n overflow: auto;\n}\n\n.p-select-option-group {\n cursor: auto;\n margin: 0;\n padding: ").concat(e("select.option.group.padding"),";\n background: ").concat(e("select.option.group.background"),";\n color: ").concat(e("select.option.group.color"),";\n font-weight: ").concat(e("select.option.group.font.weight"),";\n}\n\n.p-select-list {\n margin: 0;\n padding: 0;\n list-style-type: none;\n padding: ").concat(e("select.list.padding"),";\n gap: ").concat(e("select.list.gap"),";\n display: flex;\n flex-direction: column;\n}\n\n.p-select-option {\n cursor: pointer;\n font-weight: normal;\n white-space: nowrap;\n position: relative;\n overflow: hidden;\n display: flex;\n align-items: center;\n padding: ").concat(e("select.option.padding"),";\n border: 0 none;\n color: ").concat(e("select.option.color"),";\n background: transparent;\n transition: background ").concat(e("select.transition.duration"),", color ").concat(e("select.transition.duration"),", border-color ").concat(e("select.transition.duration"),",\n box-shadow ").concat(e("select.transition.duration"),", outline-color ").concat(e("select.transition.duration"),";\n border-radius: ").concat(e("select.option.border.radius"),";\n}\n\n.p-select-option:not(.p-select-option-selected):not(.p-disabled).p-focus {\n background: ").concat(e("select.option.focus.background"),";\n color: ").concat(e("select.option.focus.color"),";\n}\n\n.p-select-option.p-select-option-selected {\n background: ").concat(e("select.option.selected.background"),";\n color: ").concat(e("select.option.selected.color"),";\n}\n\n.p-select-option.p-select-option-selected.p-focus {\n background: ").concat(e("select.option.selected.focus.background"),";\n color: ").concat(e("select.option.selected.focus.color"),";\n}\n\n.p-select-option-check-icon {\n position: relative;\n margin-inline-start: ").concat(e("select.checkmark.gutter.start"),";\n margin-inline-end: ").concat(e("select.checkmark.gutter.end"),";\n color: ").concat(e("select.checkmark.color"),";\n}\n\n.p-select-empty-message {\n padding: ").concat(e("select.empty.message.padding"),";\n}\n\n.p-select-fluid {\n display: flex;\n width: 100%;\n}\n\n.p-select-sm .p-select-label {\n font-size: ").concat(e("select.sm.font.size"),";\n padding-block: ").concat(e("select.sm.padding.y"),";\n padding-inline: ").concat(e("select.sm.padding.x"),";\n}\n\n.p-select-sm .p-select-dropdown .p-icon {\n font-size: ").concat(e("select.sm.font.size"),";\n width: ").concat(e("select.sm.font.size"),";\n height: ").concat(e("select.sm.font.size"),";\n}\n\n.p-select-lg .p-select-label {\n font-size: ").concat(e("select.lg.font.size"),";\n padding-block: ").concat(e("select.lg.padding.y"),";\n padding-inline: ").concat(e("select.lg.padding.x"),";\n}\n\n.p-select-lg .p-select-dropdown .p-icon {\n font-size: ").concat(e("select.lg.font.size"),";\n width: ").concat(e("select.lg.font.size"),";\n height: ").concat(e("select.lg.font.size"),";\n}\n")},classes:{root:function(t){var e=t.instance,n=t.props,o=t.state;return["p-select p-component p-inputwrapper",{"p-disabled":n.disabled,"p-invalid":e.$invalid,"p-variant-filled":"filled"===e.$variant,"p-focus":o.focused,"p-inputwrapper-filled":e.$filled,"p-inputwrapper-focus":o.focused||o.overlayVisible,"p-select-open":o.overlayVisible,"p-select-fluid":e.$fluid,"p-select-sm p-inputfield-sm":"small"===n.size,"p-select-lg p-inputfield-lg":"large"===n.size}]},label:function(t){var e=t.instance,n=t.props;return["p-select-label",{"p-placeholder":!n.editable&&e.label===n.placeholder,"p-select-label-empty":!(n.editable||e.$slots.value||"p-emptylabel"!==e.label&&0!==e.label.length)}]},clearIcon:"p-select-clear-icon",dropdown:"p-select-dropdown",loadingicon:"p-select-loading-icon",dropdownIcon:"p-select-dropdown-icon",overlay:"p-select-overlay p-component",header:"p-select-header",pcFilter:"p-select-filter",listContainer:"p-select-list-container",list:"p-select-list",optionGroup:"p-select-option-group",optionGroupLabel:"p-select-option-group-label",option:function(t){var e=t.instance,n=t.props,o=t.state,i=t.option,r=t.focusedOption;return["p-select-option",{"p-select-option-selected":e.isSelected(i)&&n.highlightOnSelect,"p-focus":o.focusedOptionIndex===r,"p-disabled":e.isOptionDisabled(i)}]},optionLabel:"p-select-option-label",optionCheckIcon:"p-select-option-check-icon",optionBlankIcon:"p-select-option-blank-icon",emptyMessage:"p-select-empty-message"}});function sn(t){return(sn="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}function ln(t){return function(t){if(Array.isArray(t))return an(t)}(t)||function(t){if("undefined"!=typeof Symbol&&null!=t[Symbol.iterator]||null!=t["@@iterator"])return Array.from(t)}(t)||function(t,e){if(t){if("string"==typeof t)return an(t,e);var n={}.toString.call(t).slice(8,-1);return"Object"===n&&t.constructor&&(n=t.constructor.name),"Map"===n||"Set"===n?Array.from(t):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?an(t,e):void 0}}(t)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function an(t,e){(null==e||e>t.length)&&(e=t.length);for(var n=0,o=Array(e);n2&&void 0!==arguments[2])||arguments[2],o=this.getOptionValue(e);this.updateModel(t,o),n&&this.hide(!0)},onOptionMouseMove:function(t,e){this.focusOnHover&&this.changeFocusedOptionIndex(t,e)},onFilterChange:function(t){var e=t.target.value;this.filterValue=e,this.focusedOptionIndex=-1,this.$emit("filter",{originalEvent:t,value:e}),!this.virtualScrollerDisabled&&this.virtualScroller.scrollToIndex(0)},onFilterKeyDown:function(t){if(!t.isComposing)switch(t.code){case"ArrowDown":this.onArrowDownKey(t);break;case"ArrowUp":this.onArrowUpKey(t,!0);break;case"ArrowLeft":case"ArrowRight":this.onArrowLeftKey(t,!0);break;case"Home":this.onHomeKey(t,!0);break;case"End":this.onEndKey(t,!0);break;case"Enter":case"NumpadEnter":this.onEnterKey(t);break;case"Escape":this.onEscapeKey(t);break;case"Tab":this.onTabKey(t,!0)}},onFilterBlur:function(){this.focusedOptionIndex=-1},onFilterUpdated:function(){this.overlayVisible&&this.alignOverlay()},onOverlayClick:function(t){Xe.emit("overlay-click",{originalEvent:t,target:this.$el})},onOverlayKeyDown:function(t){if("Escape"===t.code)this.onEscapeKey(t)},onArrowDownKey:function(t){if(this.overlayVisible){var e=-1!==this.focusedOptionIndex?this.findNextOptionIndex(this.focusedOptionIndex):this.clicked?this.findFirstOptionIndex():this.findFirstFocusedOptionIndex();this.changeFocusedOptionIndex(t,e)}else this.show(),this.editable&&this.changeFocusedOptionIndex(t,this.findSelectedOptionIndex());t.preventDefault()},onArrowUpKey:function(t){var e=arguments.length>1&&void 0!==arguments[1]&&arguments[1];if(t.altKey&&!e)-1!==this.focusedOptionIndex&&this.onOptionSelect(t,this.visibleOptions[this.focusedOptionIndex]),this.overlayVisible&&this.hide(),t.preventDefault();else{var n=-1!==this.focusedOptionIndex?this.findPrevOptionIndex(this.focusedOptionIndex):this.clicked?this.findLastOptionIndex():this.findLastFocusedOptionIndex();this.changeFocusedOptionIndex(t,n),!this.overlayVisible&&this.show(),t.preventDefault()}},onArrowLeftKey:function(t){arguments.length>1&&void 0!==arguments[1]&&arguments[1]&&(this.focusedOptionIndex=-1)},onHomeKey:function(t){if(arguments.length>1&&void 0!==arguments[1]&&arguments[1]){var e=t.currentTarget;t.shiftKey?e.setSelectionRange(0,t.target.selectionStart):(e.setSelectionRange(0,0),this.focusedOptionIndex=-1)}else this.changeFocusedOptionIndex(t,this.findFirstOptionIndex()),!this.overlayVisible&&this.show();t.preventDefault()},onEndKey:function(t){if(arguments.length>1&&void 0!==arguments[1]&&arguments[1]){var e=t.currentTarget;if(t.shiftKey)e.setSelectionRange(t.target.selectionStart,e.value.length);else{var n=e.value.length;e.setSelectionRange(n,n),this.focusedOptionIndex=-1}}else this.changeFocusedOptionIndex(t,this.findLastOptionIndex()),!this.overlayVisible&&this.show();t.preventDefault()},onPageUpKey:function(t){this.scrollInView(0),t.preventDefault()},onPageDownKey:function(t){this.scrollInView(this.visibleOptions.length-1),t.preventDefault()},onEnterKey:function(t){this.overlayVisible?(-1!==this.focusedOptionIndex&&this.onOptionSelect(t,this.visibleOptions[this.focusedOptionIndex]),this.hide()):(this.focusedOptionIndex=-1,this.onArrowDownKey(t)),t.preventDefault()},onSpaceKey:function(t){!(arguments.length>1&&void 0!==arguments[1]&&arguments[1])&&this.onEnterKey(t)},onEscapeKey:function(t){this.overlayVisible&&this.hide(!0),t.preventDefault(),t.stopPropagation()},onTabKey:function(t){arguments.length>1&&void 0!==arguments[1]&&arguments[1]||(this.overlayVisible&&this.hasFocusableElements()?(et(this.$refs.firstHiddenFocusableElementOnOverlay),t.preventDefault()):(-1!==this.focusedOptionIndex&&this.onOptionSelect(t,this.visibleOptions[this.focusedOptionIndex]),this.overlayVisible&&this.hide(this.filter)))},onBackspaceKey:function(t){arguments.length>1&&void 0!==arguments[1]&&arguments[1]&&!this.overlayVisible&&this.show()},onOverlayEnter:function(t){var e=this;Q.set("overlay",t,this.$primevue.config.zIndex.overlay),st(t,{position:"absolute",top:"0",left:"0"}),this.alignOverlay(),this.scrollInView(),setTimeout((function(){e.autoFilterFocus&&e.filter&&et(e.$refs.filterInput.$el)}),1)},onOverlayAfterEnter:function(){this.bindOutsideClickListener(),this.bindScrollListener(),this.bindResizeListener(),this.$emit("show")},onOverlayLeave:function(){var t=this;this.unbindOutsideClickListener(),this.unbindScrollListener(),this.unbindResizeListener(),this.autoFilterFocus&&this.filter&&!this.editable&&this.$nextTick((function(){et(t.$refs.filterInput.$el)})),this.$emit("hide"),this.overlay=null},onOverlayAfterLeave:function(t){Q.clear(t)},alignOverlay:function(){"self"===this.appendTo?lt(this.overlay,this.$el):(this.overlay.style.minWidth=G(this.$el)+"px",at(this.overlay,this.$el))},bindOutsideClickListener:function(){var t=this;this.outsideClickListener||(this.outsideClickListener=function(e){t.overlayVisible&&t.overlay&&!t.$el.contains(e.target)&&!t.overlay.contains(e.target)&&t.hide()},document.addEventListener("click",this.outsideClickListener))},unbindOutsideClickListener:function(){this.outsideClickListener&&(document.removeEventListener("click",this.outsideClickListener),this.outsideClickListener=null)},bindScrollListener:function(){var t=this;this.scrollHandler||(this.scrollHandler=new Tt(this.$refs.container,(function(){t.overlayVisible&&t.hide()}))),this.scrollHandler.bindScrollListener()},unbindScrollListener:function(){this.scrollHandler&&this.scrollHandler.unbindScrollListener()},bindResizeListener:function(){var t=this;this.resizeListener||(this.resizeListener=function(){t.overlayVisible&&!ct()&&t.hide()},window.addEventListener("resize",this.resizeListener))},unbindResizeListener:function(){this.resizeListener&&(window.removeEventListener("resize",this.resizeListener),this.resizeListener=null)},bindLabelClickListener:function(){var t=this;if(!this.editable&&!this.labelClickListener){var e=document.querySelector('label[for="'.concat(this.labelId,'"]'));e&&Z(e)&&(this.labelClickListener=function(){et(t.$refs.focusInput)},e.addEventListener("click",this.labelClickListener))}},unbindLabelClickListener:function(){if(this.labelClickListener){var t=document.querySelector('label[for="'.concat(this.labelId,'"]'));t&&Z(t)&&t.removeEventListener("click",this.labelClickListener)}},hasFocusableElements:function(){return ut(this.overlay,':not([data-p-hidden-focusable="true"])').length>0},isOptionMatched:function(t){var e;return this.isValidOption(t)&&"string"==typeof this.getOptionLabel(t)&&(null===(e=this.getOptionLabel(t))||void 0===e?void 0:e.toLocaleLowerCase(this.filterLocale).startsWith(this.searchValue.toLocaleLowerCase(this.filterLocale)))},isValidOption:function(t){return K(t)&&!(this.isOptionDisabled(t)||this.isOptionGroup(t))},isValidSelectedOption:function(t){return this.isValidOption(t)&&this.isSelected(t)},isSelected:function(t){return dt(this.d_value,this.getOptionValue(t),this.equalityKey)},findFirstOptionIndex:function(){var t=this;return this.visibleOptions.findIndex((function(e){return t.isValidOption(e)}))},findLastOptionIndex:function(){var t=this;return pt(this.visibleOptions,(function(e){return t.isValidOption(e)}))},findNextOptionIndex:function(t){var e=this,n=t-1?n+t+1:t},findPrevOptionIndex:function(t){var e=this,n=t>0?pt(this.visibleOptions.slice(0,t),(function(t){return e.isValidOption(t)})):-1;return n>-1?n:t},findSelectedOptionIndex:function(){var t=this;return this.$filled?this.visibleOptions.findIndex((function(e){return t.isValidSelectedOption(e)})):-1},findFirstFocusedOptionIndex:function(){var t=this.findSelectedOptionIndex();return t<0?this.findFirstOptionIndex():t},findLastFocusedOptionIndex:function(){var t=this.findSelectedOptionIndex();return t<0?this.findLastOptionIndex():t},searchOptions:function(t,e){var n=this;this.searchValue=(this.searchValue||"")+e;var o=-1,i=!1;return K(this.searchValue)&&(-1!==(o=-1!==this.focusedOptionIndex?-1===(o=this.visibleOptions.slice(this.focusedOptionIndex).findIndex((function(t){return n.isOptionMatched(t)})))?this.visibleOptions.slice(0,this.focusedOptionIndex).findIndex((function(t){return n.isOptionMatched(t)})):o+this.focusedOptionIndex:this.visibleOptions.findIndex((function(t){return n.isOptionMatched(t)})))&&(i=!0),-1===o&&-1===this.focusedOptionIndex&&(o=this.findFirstFocusedOptionIndex()),-1!==o&&this.changeFocusedOptionIndex(t,o)),this.searchTimeout&&clearTimeout(this.searchTimeout),this.searchTimeout=setTimeout((function(){n.searchValue="",n.searchTimeout=null}),500),i},changeFocusedOptionIndex:function(t,e){this.focusedOptionIndex!==e&&(this.focusedOptionIndex=e,this.scrollInView(),this.selectOnFocus&&this.onOptionSelect(t,this.visibleOptions[e],!1))},scrollInView:function(){var t=this,e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:-1;this.$nextTick((function(){var n=-1!==e?"".concat(t.id,"_").concat(e):t.focusedOptionId,o=J(t.list,'li[id="'.concat(n,'"]'));o?o.scrollIntoView&&o.scrollIntoView({block:"nearest",inline:"start"}):t.virtualScrollerDisabled||t.virtualScroller&&t.virtualScroller.scrollToIndex(-1!==e?e:t.focusedOptionIndex)}))},autoUpdateModel:function(){this.selectOnFocus&&this.autoOptionFocus&&!this.$filled&&(this.focusedOptionIndex=this.findFirstFocusedOptionIndex(),this.onOptionSelect(null,this.visibleOptions[this.focusedOptionIndex],!1))},updateModel:function(t,e){this.writeValue(e,t),this.$emit("change",{originalEvent:t,value:e})},flatOptions:function(t){var e=this;return(t||[]).reduce((function(t,n,o){t.push({optionGroup:n,group:!0,index:o});var i=e.getOptionGroupChildren(n);return i&&i.forEach((function(e){return t.push(e)})),t}),[])},overlayRef:function(t){this.overlay=t},listRef:function(t,e){this.list=t,e&&e(t)},virtualScrollerRef:function(t){this.virtualScroller=t}},computed:{visibleOptions:function(){var t=this,e=this.optionGroupLabel?this.flatOptions(this.options):this.options||[];if(this.filterValue){var n=_t.filter(e,this.searchFields,this.filterValue,this.filterMatchMode,this.filterLocale);if(this.optionGroupLabel){var o=this.options||[],i=[];return o.forEach((function(e){var o=t.getOptionGroupChildren(e).filter((function(t){return n.includes(t)}));o.length>0&&i.push(un(un({},e),{},dn({},"string"==typeof t.optionGroupChildren?t.optionGroupChildren:"items",ln(o))))})),this.flatOptions(i)}return n}return e},hasSelectedOption:function(){return this.$filled},label:function(){var t=this.findSelectedOptionIndex();return-1!==t?this.getOptionLabel(this.visibleOptions[t]):this.placeholder||"p-emptylabel"},editableInputValue:function(){var t=this.findSelectedOptionIndex();return-1!==t?this.getOptionLabel(this.visibleOptions[t]):this.d_value||""},equalityKey:function(){return this.optionValue?null:this.dataKey},searchFields:function(){return this.filterFields||[this.optionLabel]},filterResultMessageText:function(){return K(this.visibleOptions)?this.filterMessageText.replaceAll("{0}",this.visibleOptions.length):this.emptyFilterMessageText},filterMessageText:function(){return this.filterMessage||this.$primevue.config.locale.searchMessage||""},emptyFilterMessageText:function(){return this.emptyFilterMessage||this.$primevue.config.locale.emptySearchMessage||this.$primevue.config.locale.emptyFilterMessage||""},emptyMessageText:function(){return this.emptyMessage||this.$primevue.config.locale.emptyMessage||""},selectionMessageText:function(){return this.selectionMessage||this.$primevue.config.locale.selectionMessage||""},emptySelectionMessageText:function(){return this.emptySelectionMessage||this.$primevue.config.locale.emptySelectionMessage||""},selectedMessageText:function(){return this.$filled?this.selectionMessageText.replaceAll("{0}","1"):this.emptySelectionMessageText},focusedOptionId:function(){return-1!==this.focusedOptionIndex?"".concat(this.id,"_").concat(this.focusedOptionIndex):null},ariaSetSize:function(){var t=this;return this.visibleOptions.filter((function(e){return!t.isOptionGroup(e)})).length},isClearIconVisible:function(){return this.showClear&&null!=this.d_value&&K(this.options)},virtualScrollerDisabled:function(){return!this.virtualScrollerOptions}},directives:{ripple:De},components:{InputText:We,VirtualScroller:nn,Portal:Ye,InputIcon:Ne,IconField:Ue,TimesIcon:Mt,ChevronDownIcon:Vt,SpinnerIcon:Ct,SearchIcon:Pt,CheckIcon:Ft,BlankIcon:At}},hn=["id"],fn=["id","value","placeholder","tabindex","disabled","aria-label","aria-labelledby","aria-expanded","aria-controls","aria-activedescendant","aria-invalid"],bn=["id","tabindex","aria-label","aria-labelledby","aria-expanded","aria-controls","aria-activedescendant","aria-disabled"],mn=["id"],gn=["id"],vn=["id","aria-label","aria-selected","aria-disabled","aria-setsize","aria-posinset","onClick","onMousemove","data-p-selected","data-p-focused","data-p-disabled"];pn.render=function(t,e,n,o,i,r){var s=k("SpinnerIcon"),l=k("InputText"),a=k("SearchIcon"),c=k("InputIcon"),u=k("IconField"),d=k("CheckIcon"),p=k("BlankIcon"),h=k("VirtualScroller"),f=k("Portal"),b=w("ripple");return m(),g("div",I({ref:"container",id:i.id,class:t.cx("root"),onClick:e[11]||(e[11]=function(){return r.onContainerClick&&r.onContainerClick.apply(r,arguments)})},t.ptmi("root")),[t.editable?(m(),g("input",I({key:0,ref:"focusInput",id:t.labelId||t.inputId,type:"text",class:[t.cx("label"),t.inputClass,t.labelClass],style:[t.inputStyle,t.labelStyle],value:r.editableInputValue,placeholder:t.placeholder,tabindex:t.disabled?-1:t.tabindex,disabled:t.disabled,autocomplete:"off",role:"combobox","aria-label":t.ariaLabel,"aria-labelledby":t.ariaLabelledby,"aria-haspopup":"listbox","aria-expanded":i.overlayVisible,"aria-controls":i.id+"_list","aria-activedescendant":i.focused?r.focusedOptionId:void 0,"aria-invalid":t.invalid||void 0,onFocus:e[0]||(e[0]=function(){return r.onFocus&&r.onFocus.apply(r,arguments)}),onBlur:e[1]||(e[1]=function(){return r.onBlur&&r.onBlur.apply(r,arguments)}),onKeydown:e[2]||(e[2]=function(){return r.onKeyDown&&r.onKeyDown.apply(r,arguments)}),onInput:e[3]||(e[3]=function(){return r.onEditableInput&&r.onEditableInput.apply(r,arguments)})},t.ptm("label")),null,16,fn)):(m(),g("span",I({key:1,ref:"focusInput",id:t.labelId||t.inputId,class:[t.cx("label"),t.inputClass,t.labelClass],style:[t.inputStyle,t.labelStyle],tabindex:t.disabled?-1:t.tabindex,role:"combobox","aria-label":t.ariaLabel||("p-emptylabel"===r.label?void 0:r.label),"aria-labelledby":t.ariaLabelledby,"aria-haspopup":"listbox","aria-expanded":i.overlayVisible,"aria-controls":i.id+"_list","aria-activedescendant":i.focused?r.focusedOptionId:void 0,"aria-disabled":t.disabled,onFocus:e[4]||(e[4]=function(){return r.onFocus&&r.onFocus.apply(r,arguments)}),onBlur:e[5]||(e[5]=function(){return r.onBlur&&r.onBlur.apply(r,arguments)}),onKeydown:e[6]||(e[6]=function(){return r.onKeyDown&&r.onKeyDown.apply(r,arguments)})},t.ptm("label")),[v(t.$slots,"value",{value:t.d_value,placeholder:t.placeholder},(function(){var t;return[y(x("p-emptylabel"===r.label?" ":null!==(t=r.label)&&void 0!==t?t:"empty"),1)]}))],16,bn)),r.isClearIconVisible?v(t.$slots,"clearicon",{key:2,class:z(t.cx("clearIcon")),clearCallback:r.onClearClick},(function(){return[(m(),S(E(t.clearIcon?"i":"TimesIcon"),I({ref:"clearIcon",class:[t.cx("clearIcon"),t.clearIcon],onClick:r.onClearClick},t.ptm("clearIcon"),{"data-pc-section":"clearicon"}),null,16,["class","onClick"]))]})):$("",!0),C("div",I({class:t.cx("dropdown")},t.ptm("dropdown")),[t.loading?v(t.$slots,"loadingicon",{key:0,class:z(t.cx("loadingIcon"))},(function(){return[t.loadingIcon?(m(),g("span",I({key:0,class:[t.cx("loadingIcon"),"pi-spin",t.loadingIcon],"aria-hidden":"true"},t.ptm("loadingIcon")),null,16)):(m(),S(s,I({key:1,class:t.cx("loadingIcon"),spin:"","aria-hidden":"true"},t.ptm("loadingIcon")),null,16,["class"]))]})):v(t.$slots,"dropdownicon",{key:1,class:z(t.cx("dropdownIcon"))},(function(){return[(m(),S(E(t.dropdownIcon?"span":"ChevronDownIcon"),I({class:[t.cx("dropdownIcon"),t.dropdownIcon],"aria-hidden":"true"},t.ptm("dropdownIcon")),null,16,["class"]))]}))],16),V(f,{appendTo:t.appendTo},{default:L((function(){return[V(P,I({name:"p-connected-overlay",onEnter:r.onOverlayEnter,onAfterEnter:r.onOverlayAfterEnter,onLeave:r.onOverlayLeave,onAfterLeave:r.onOverlayAfterLeave},t.ptm("transition")),{default:L((function(){return[i.overlayVisible?(m(),g("div",I({key:0,ref:r.overlayRef,class:[t.cx("overlay"),t.panelClass,t.overlayClass],style:[t.panelStyle,t.overlayStyle],onClick:e[9]||(e[9]=function(){return r.onOverlayClick&&r.onOverlayClick.apply(r,arguments)}),onKeydown:e[10]||(e[10]=function(){return r.onOverlayKeyDown&&r.onOverlayKeyDown.apply(r,arguments)})},t.ptm("overlay")),[C("span",I({ref:"firstHiddenFocusableElementOnOverlay",role:"presentation","aria-hidden":"true",class:"p-hidden-accessible p-hidden-focusable",tabindex:0,onFocus:e[7]||(e[7]=function(){return r.onFirstHiddenFocus&&r.onFirstHiddenFocus.apply(r,arguments)})},t.ptm("hiddenFirstFocusableEl"),{"data-p-hidden-accessible":!0,"data-p-hidden-focusable":!0}),null,16),v(t.$slots,"header",{value:t.d_value,options:r.visibleOptions}),t.filter?(m(),g("div",I({key:0,class:t.cx("header")},t.ptm("header")),[V(u,{unstyled:t.unstyled,pt:t.ptm("pcFilterContainer")},{default:L((function(){return[V(l,{ref:"filterInput",type:"text",value:i.filterValue,onVnodeMounted:r.onFilterUpdated,onVnodeUpdated:r.onFilterUpdated,class:z(t.cx("pcFilter")),placeholder:t.filterPlaceholder,variant:t.variant,unstyled:t.unstyled,role:"searchbox",autocomplete:"off","aria-owns":i.id+"_list","aria-activedescendant":r.focusedOptionId,onKeydown:r.onFilterKeyDown,onBlur:r.onFilterBlur,onInput:r.onFilterChange,pt:t.ptm("pcFilter")},null,8,["value","onVnodeMounted","onVnodeUpdated","class","placeholder","variant","unstyled","aria-owns","aria-activedescendant","onKeydown","onBlur","onInput","pt"]),V(c,{unstyled:t.unstyled,pt:t.ptm("pcFilterIconContainer")},{default:L((function(){return[v(t.$slots,"filtericon",{},(function(){return[t.filterIcon?(m(),g("span",I({key:0,class:t.filterIcon},t.ptm("filterIcon")),null,16)):(m(),S(a,F(I({key:1},t.ptm("filterIcon"))),null,16))]}))]})),_:3},8,["unstyled","pt"])]})),_:3},8,["unstyled","pt"]),C("span",I({role:"status","aria-live":"polite",class:"p-hidden-accessible"},t.ptm("hiddenFilterResult"),{"data-p-hidden-accessible":!0}),x(r.filterResultMessageText),17)],16)):$("",!0),C("div",I({class:t.cx("listContainer"),style:{"max-height":r.virtualScrollerDisabled?t.scrollHeight:""}},t.ptm("listContainer")),[V(h,I({ref:r.virtualScrollerRef},t.virtualScrollerOptions,{items:r.visibleOptions,style:{height:t.scrollHeight},tabindex:-1,disabled:r.virtualScrollerDisabled,pt:t.ptm("virtualScroller")}),A({content:L((function(e){var n=e.styleClass,o=e.contentRef,s=e.items,l=e.getItemOptions,a=e.contentStyle,c=e.itemSize;return[C("ul",I({ref:function(t){return r.listRef(t,o)},id:i.id+"_list",class:[t.cx("list"),n],style:a,role:"listbox"},t.ptm("list")),[(m(!0),g(_,null,M(s,(function(e,n){return m(),g(_,{key:r.getOptionRenderKey(e,r.getOptionIndex(n,l))},[r.isOptionGroup(e)?(m(),g("li",I({key:0,id:i.id+"_"+r.getOptionIndex(n,l),style:{height:c?c+"px":void 0},class:t.cx("optionGroup"),role:"option",ref_for:!0},t.ptm("optionGroup")),[v(t.$slots,"optiongroup",{option:e.optionGroup,index:r.getOptionIndex(n,l)},(function(){return[C("span",I({class:t.cx("optionGroupLabel"),ref_for:!0},t.ptm("optionGroupLabel")),x(r.getOptionGroupLabel(e.optionGroup)),17)]}))],16,gn)):O((m(),g("li",I({key:1,id:i.id+"_"+r.getOptionIndex(n,l),class:t.cx("option",{option:e,focusedOption:r.getOptionIndex(n,l)}),style:{height:c?c+"px":void 0},role:"option","aria-label":r.getOptionLabel(e),"aria-selected":r.isSelected(e),"aria-disabled":r.isOptionDisabled(e),"aria-setsize":r.ariaSetSize,"aria-posinset":r.getAriaPosInset(r.getOptionIndex(n,l)),onClick:function(t){return r.onOptionSelect(t,e)},onMousemove:function(t){return r.onOptionMouseMove(t,r.getOptionIndex(n,l))},"data-p-selected":r.isSelected(e),"data-p-focused":i.focusedOptionIndex===r.getOptionIndex(n,l),"data-p-disabled":r.isOptionDisabled(e),ref_for:!0},r.getPTItemOptions(e,l,n,"option")),[t.checkmark?(m(),g(_,{key:0},[r.isSelected(e)?(m(),S(d,I({key:0,class:t.cx("optionCheckIcon"),ref_for:!0},t.ptm("optionCheckIcon")),null,16,["class"])):(m(),S(p,I({key:1,class:t.cx("optionBlankIcon"),ref_for:!0},t.ptm("optionBlankIcon")),null,16,["class"]))],64)):$("",!0),v(t.$slots,"option",{option:e,selected:r.isSelected(e),index:r.getOptionIndex(n,l)},(function(){return[C("span",I({class:t.cx("optionLabel"),ref_for:!0},t.ptm("optionLabel")),x(r.getOptionLabel(e)),17)]}))],16,vn)),[[b]])],64)})),128)),i.filterValue&&(!s||s&&0===s.length)?(m(),g("li",I({key:0,class:t.cx("emptyMessage"),role:"option"},t.ptm("emptyMessage"),{"data-p-hidden-accessible":!0}),[v(t.$slots,"emptyfilter",{},(function(){return[y(x(r.emptyFilterMessageText),1)]}))],16)):!t.options||t.options&&0===t.options.length?(m(),g("li",I({key:1,class:t.cx("emptyMessage"),role:"option"},t.ptm("emptyMessage"),{"data-p-hidden-accessible":!0}),[v(t.$slots,"empty",{},(function(){return[y(x(r.emptyMessageText),1)]}))],16)):$("",!0)],16,mn)]})),_:2},[t.$slots.loader?{name:"loader",fn:L((function(e){var n=e.options;return[v(t.$slots,"loader",{options:n})]})),key:"0"}:void 0]),1040,["items","style","disabled","pt"])],16),v(t.$slots,"footer",{value:t.d_value,options:r.visibleOptions}),!t.options||t.options&&0===t.options.length?(m(),g("span",I({key:1,role:"status","aria-live":"polite",class:"p-hidden-accessible"},t.ptm("hiddenEmptyMessage"),{"data-p-hidden-accessible":!0}),x(r.emptyMessageText),17)):$("",!0),C("span",I({role:"status","aria-live":"polite",class:"p-hidden-accessible"},t.ptm("hiddenSelectedMessage"),{"data-p-hidden-accessible":!0}),x(r.selectedMessageText),17),C("span",I({ref:"lastHiddenFocusableElementOnOverlay",role:"presentation","aria-hidden":"true",class:"p-hidden-accessible p-hidden-focusable",tabindex:0,onFocus:e[8]||(e[8]=function(){return r.onLastHiddenFocus&&r.onLastHiddenFocus.apply(r,arguments)})},t.ptm("hiddenLastFocusableEl"),{"data-p-hidden-accessible":!0,"data-p-hidden-focusable":!0}),null,16)],16)):$("",!0)]})),_:3},16,["onEnter","onAfterEnter","onLeave","onAfterLeave"])]})),_:3},8,["appendTo"])],16,hn)};var yn=St.extend({name:"tooltip-directive",theme:function(t){var e=t.dt;return"\n.p-tooltip {\n position: absolute;\n display: none;\n max-width: ".concat(e("tooltip.max.width"),";\n}\n\n.p-tooltip-right,\n.p-tooltip-left {\n padding: 0 ").concat(e("tooltip.gutter"),";\n}\n\n.p-tooltip-top,\n.p-tooltip-bottom {\n padding: ").concat(e("tooltip.gutter")," 0;\n}\n\n.p-tooltip-text {\n white-space: pre-line;\n word-break: break-word;\n background: ").concat(e("tooltip.background"),";\n color: ").concat(e("tooltip.color"),";\n padding: ").concat(e("tooltip.padding"),";\n box-shadow: ").concat(e("tooltip.shadow"),";\n border-radius: ").concat(e("tooltip.border.radius"),";\n}\n\n.p-tooltip-arrow {\n position: absolute;\n width: 0;\n height: 0;\n border-color: transparent;\n border-style: solid;\n}\n\n.p-tooltip-right .p-tooltip-arrow {\n margin-top: calc(-1 * ").concat(e("tooltip.gutter"),");\n border-width: ").concat(e("tooltip.gutter")," ").concat(e("tooltip.gutter")," ").concat(e("tooltip.gutter")," 0;\n border-right-color: ").concat(e("tooltip.background"),";\n}\n\n.p-tooltip-left .p-tooltip-arrow {\n margin-top: calc(-1 * ").concat(e("tooltip.gutter"),");\n border-width: ").concat(e("tooltip.gutter")," 0 ").concat(e("tooltip.gutter")," ").concat(e("tooltip.gutter"),";\n border-left-color: ").concat(e("tooltip.background"),";\n}\n\n.p-tooltip-top .p-tooltip-arrow {\n margin-left: calc(-1 * ").concat(e("tooltip.gutter"),");\n border-width: ").concat(e("tooltip.gutter")," ").concat(e("tooltip.gutter")," 0 ").concat(e("tooltip.gutter"),";\n border-top-color: ").concat(e("tooltip.background"),";\n border-bottom-color: ").concat(e("tooltip.background"),";\n}\n\n.p-tooltip-bottom .p-tooltip-arrow {\n margin-left: calc(-1 * ").concat(e("tooltip.gutter"),");\n border-width: 0 ").concat(e("tooltip.gutter")," ").concat(e("tooltip.gutter")," ").concat(e("tooltip.gutter"),";\n border-top-color: ").concat(e("tooltip.background"),";\n border-bottom-color: ").concat(e("tooltip.background"),";\n}\n")},classes:{root:"p-tooltip p-component",arrow:"p-tooltip-arrow",text:"p-tooltip-text"}});function xn(t,e){return function(t){if(Array.isArray(t))return t}(t)||function(t,e){var n=null==t?null:"undefined"!=typeof Symbol&&t[Symbol.iterator]||t["@@iterator"];if(null!=n){var o,i,r,s,l=[],a=!0,c=!1;try{if(r=(n=n.call(t)).next,0===e);else for(;!(a=(o=r.call(n)).done)&&(l.push(o.value),l.length!==e);a=!0);}catch(u){c=!0,i=u}finally{try{if(!a&&null!=n.return&&(s=n.return(),Object(s)!==s))return}finally{if(c)throw i}}return l}}(t,e)||function(t,e){if(t){if("string"==typeof t)return In(t,e);var n={}.toString.call(t).slice(8,-1);return"Object"===n&&t.constructor&&(n=t.constructor.name),"Map"===n||"Set"===n?Array.from(t):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?In(t,e):void 0}}(t,e)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function In(t,e){(null==e||e>t.length)&&(e=t.length);for(var n=0,o=Array(e);nl.width||i<0||o<0||o+s>l.height},getTarget:function(t){var e;return vt(t,"p-inputwrapper")&&null!==(e=J(t,"input"))&&void 0!==e?e:t},getModifiers:function(t){return t.modifiers&&Object.keys(t.modifiers).length?t.modifiers:t.arg&&"object"===wn(t.arg)?Object.entries(t.arg).reduce((function(t,e){var n=xn(e,2),o=n[0],i=n[1];return"event"!==o&&"position"!==o||(t[i]=!0),t}),{}):{}}}}),Sn=St.extend({name:"checkbox",theme:function(t){var e=t.dt;return"\n.p-checkbox {\n position: relative;\n display: inline-flex;\n user-select: none;\n vertical-align: bottom;\n width: ".concat(e("checkbox.width"),";\n height: ").concat(e("checkbox.height"),";\n}\n\n.p-checkbox-input {\n cursor: pointer;\n appearance: none;\n position: absolute;\n inset-block-start: 0;\n inset-inline-start: 0;\n width: 100%;\n height: 100%;\n padding: 0;\n margin: 0;\n opacity: 0;\n z-index: 1;\n outline: 0 none;\n border: 1px solid transparent;\n border-radius: ").concat(e("checkbox.border.radius"),";\n}\n\n.p-checkbox-box {\n display: flex;\n justify-content: center;\n align-items: center;\n border-radius: ").concat(e("checkbox.border.radius"),";\n border: 1px solid ").concat(e("checkbox.border.color"),";\n background: ").concat(e("checkbox.background"),";\n width: ").concat(e("checkbox.width"),";\n height: ").concat(e("checkbox.height"),";\n transition: background ").concat(e("checkbox.transition.duration"),", color ").concat(e("checkbox.transition.duration"),", border-color ").concat(e("checkbox.transition.duration"),", box-shadow ").concat(e("checkbox.transition.duration"),", outline-color ").concat(e("checkbox.transition.duration"),";\n outline-color: transparent;\n box-shadow: ").concat(e("checkbox.shadow"),";\n}\n\n.p-checkbox-icon {\n transition-duration: ").concat(e("checkbox.transition.duration"),";\n color: ").concat(e("checkbox.icon.color"),";\n font-size: ").concat(e("checkbox.icon.size"),";\n width: ").concat(e("checkbox.icon.size"),";\n height: ").concat(e("checkbox.icon.size"),";\n}\n\n.p-checkbox:not(.p-disabled):has(.p-checkbox-input:hover) .p-checkbox-box {\n border-color: ").concat(e("checkbox.hover.border.color"),";\n}\n\n.p-checkbox-checked .p-checkbox-box {\n border-color: ").concat(e("checkbox.checked.border.color"),";\n background: ").concat(e("checkbox.checked.background"),";\n}\n\n.p-checkbox-checked .p-checkbox-icon {\n color: ").concat(e("checkbox.icon.checked.color"),";\n}\n\n.p-checkbox-checked:not(.p-disabled):has(.p-checkbox-input:hover) .p-checkbox-box {\n background: ").concat(e("checkbox.checked.hover.background"),";\n border-color: ").concat(e("checkbox.checked.hover.border.color"),";\n}\n\n.p-checkbox-checked:not(.p-disabled):has(.p-checkbox-input:hover) .p-checkbox-icon {\n color: ").concat(e("checkbox.icon.checked.hover.color"),";\n}\n\n.p-checkbox:not(.p-disabled):has(.p-checkbox-input:focus-visible) .p-checkbox-box {\n border-color: ").concat(e("checkbox.focus.border.color"),";\n box-shadow: ").concat(e("checkbox.focus.ring.shadow"),";\n outline: ").concat(e("checkbox.focus.ring.width")," ").concat(e("checkbox.focus.ring.style")," ").concat(e("checkbox.focus.ring.color"),";\n outline-offset: ").concat(e("checkbox.focus.ring.offset"),";\n}\n\n.p-checkbox-checked:not(.p-disabled):has(.p-checkbox-input:focus-visible) .p-checkbox-box {\n border-color: ").concat(e("checkbox.checked.focus.border.color"),";\n}\n\n.p-checkbox.p-invalid > .p-checkbox-box {\n border-color: ").concat(e("checkbox.invalid.border.color"),";\n}\n\n.p-checkbox.p-variant-filled .p-checkbox-box {\n background: ").concat(e("checkbox.filled.background"),";\n}\n\n.p-checkbox-checked.p-variant-filled .p-checkbox-box {\n background: ").concat(e("checkbox.checked.background"),";\n}\n\n.p-checkbox-checked.p-variant-filled:not(.p-disabled):has(.p-checkbox-input:hover) .p-checkbox-box {\n background: ").concat(e("checkbox.checked.hover.background"),";\n}\n\n.p-checkbox.p-disabled {\n opacity: 1;\n}\n\n.p-checkbox.p-disabled .p-checkbox-box {\n background: ").concat(e("checkbox.disabled.background"),";\n border-color: ").concat(e("checkbox.checked.disabled.border.color"),";\n}\n\n.p-checkbox.p-disabled .p-checkbox-box .p-checkbox-icon {\n color: ").concat(e("checkbox.icon.disabled.color"),";\n}\n\n.p-checkbox-sm,\n.p-checkbox-sm .p-checkbox-box {\n width: ").concat(e("checkbox.sm.width"),";\n height: ").concat(e("checkbox.sm.height"),";\n}\n\n.p-checkbox-sm .p-checkbox-icon {\n font-size: ").concat(e("checkbox.icon.sm.size"),";\n width: ").concat(e("checkbox.icon.sm.size"),";\n height: ").concat(e("checkbox.icon.sm.size"),";\n}\n\n.p-checkbox-lg,\n.p-checkbox-lg .p-checkbox-box {\n width: ").concat(e("checkbox.lg.width"),";\n height: ").concat(e("checkbox.lg.height"),";\n}\n\n.p-checkbox-lg .p-checkbox-icon {\n font-size: ").concat(e("checkbox.icon.lg.size"),";\n width: ").concat(e("checkbox.icon.lg.size"),";\n height: ").concat(e("checkbox.icon.lg.size"),";\n}\n")},classes:{root:function(t){var e=t.instance,n=t.props;return["p-checkbox p-component",{"p-checkbox-checked":e.checked,"p-disabled":n.disabled,"p-invalid":e.$pcCheckboxGroup?e.$pcCheckboxGroup.$invalid:e.$invalid,"p-variant-filled":"filled"===e.$variant,"p-checkbox-sm p-inputfield-sm":"small"===n.size,"p-checkbox-lg p-inputfield-lg":"large"===n.size}]},box:"p-checkbox-box",input:"p-checkbox-input",icon:"p-checkbox-icon"}});function Ln(t){return function(t){if(Array.isArray(t))return $n(t)}(t)||function(t){if("undefined"!=typeof Symbol&&null!=t[Symbol.iterator]||null!=t["@@iterator"])return Array.from(t)}(t)||function(t,e){if(t){if("string"==typeof t)return $n(t,e);var n={}.toString.call(t).slice(8,-1);return"Object"===n&&t.constructor&&(n=t.constructor.name),"Map"===n||"Set"===n?Array.from(t):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?$n(t,e):void 0}}(t)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function $n(t,e){(null==e||e>t.length)&&(e=t.length);for(var n=0,o=Array(e);nt.length)&&(e=t.length);for(var n=0,o=Array(e);n2&&void 0!==arguments[2]?arguments[2]:-1,i=arguments.length>3&&void 0!==arguments[3]&&arguments[3];if(!this.disabled&&!this.isOptionDisabled(e)){var r=null;r=this.isSelected(e)?this.d_value.filter((function(t){return!dt(t,n.getOptionValue(e),n.equalityKey)})):[].concat(Bn(this.d_value||[]),[this.getOptionValue(e)]),this.updateModel(t,r),-1!==o&&(this.focusedOptionIndex=o),i&&et(this.$refs.focusInput)}},onOptionMouseMove:function(t,e){this.focusOnHover&&this.changeFocusedOptionIndex(t,e)},onOptionSelectRange:function(t){var e=this,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:-1,o=arguments.length>2&&void 0!==arguments[2]?arguments[2]:-1;if(-1===n&&(n=this.findNearestSelectedOptionIndex(o,!0)),-1===o&&(o=this.findNearestSelectedOptionIndex(n)),-1!==n&&-1!==o){var i=Math.min(n,o),r=Math.max(n,o),s=this.visibleOptions.slice(i,r+1).filter((function(t){return e.isValidOption(t)})).map((function(t){return e.getOptionValue(t)}));this.updateModel(t,s)}},onFilterChange:function(t){var e=t.target.value;this.filterValue=e,this.focusedOptionIndex=-1,this.$emit("filter",{originalEvent:t,value:e}),!this.virtualScrollerDisabled&&this.virtualScroller.scrollToIndex(0)},onFilterKeyDown:function(t){switch(t.code){case"ArrowDown":this.onArrowDownKey(t);break;case"ArrowUp":this.onArrowUpKey(t,!0);break;case"ArrowLeft":case"ArrowRight":this.onArrowLeftKey(t,!0);break;case"Home":this.onHomeKey(t,!0);break;case"End":this.onEndKey(t,!0);break;case"Enter":case"NumpadEnter":this.onEnterKey(t);break;case"Escape":this.onEscapeKey(t);break;case"Tab":this.onTabKey(t,!0)}},onFilterBlur:function(){this.focusedOptionIndex=-1},onFilterUpdated:function(){this.overlayVisible&&this.alignOverlay()},onOverlayClick:function(t){Xe.emit("overlay-click",{originalEvent:t,target:this.$el})},onOverlayKeyDown:function(t){if("Escape"===t.code)this.onEscapeKey(t)},onArrowDownKey:function(t){if(this.overlayVisible){var e=-1!==this.focusedOptionIndex?this.findNextOptionIndex(this.focusedOptionIndex):this.clicked?this.findFirstOptionIndex():this.findFirstFocusedOptionIndex();t.shiftKey&&this.onOptionSelectRange(t,this.startRangeIndex,e),this.changeFocusedOptionIndex(t,e)}else this.show();t.preventDefault()},onArrowUpKey:function(t){var e=arguments.length>1&&void 0!==arguments[1]&&arguments[1];if(t.altKey&&!e)-1!==this.focusedOptionIndex&&this.onOptionSelect(t,this.visibleOptions[this.focusedOptionIndex]),this.overlayVisible&&this.hide(),t.preventDefault();else{var n=-1!==this.focusedOptionIndex?this.findPrevOptionIndex(this.focusedOptionIndex):this.clicked?this.findLastOptionIndex():this.findLastFocusedOptionIndex();t.shiftKey&&this.onOptionSelectRange(t,n,this.startRangeIndex),this.changeFocusedOptionIndex(t,n),!this.overlayVisible&&this.show(),t.preventDefault()}},onArrowLeftKey:function(t){arguments.length>1&&void 0!==arguments[1]&&arguments[1]&&(this.focusedOptionIndex=-1)},onHomeKey:function(t){if(arguments.length>1&&void 0!==arguments[1]&&arguments[1]){var e=t.currentTarget;t.shiftKey?e.setSelectionRange(0,t.target.selectionStart):(e.setSelectionRange(0,0),this.focusedOptionIndex=-1)}else{var n=t.metaKey||t.ctrlKey,o=this.findFirstOptionIndex();t.shiftKey&&n&&this.onOptionSelectRange(t,o,this.startRangeIndex),this.changeFocusedOptionIndex(t,o),!this.overlayVisible&&this.show()}t.preventDefault()},onEndKey:function(t){if(arguments.length>1&&void 0!==arguments[1]&&arguments[1]){var e=t.currentTarget;if(t.shiftKey)e.setSelectionRange(t.target.selectionStart,e.value.length);else{var n=e.value.length;e.setSelectionRange(n,n),this.focusedOptionIndex=-1}}else{var o=t.metaKey||t.ctrlKey,i=this.findLastOptionIndex();t.shiftKey&&o&&this.onOptionSelectRange(t,this.startRangeIndex,i),this.changeFocusedOptionIndex(t,i),!this.overlayVisible&&this.show()}t.preventDefault()},onPageUpKey:function(t){this.scrollInView(0),t.preventDefault()},onPageDownKey:function(t){this.scrollInView(this.visibleOptions.length-1),t.preventDefault()},onEnterKey:function(t){this.overlayVisible?-1!==this.focusedOptionIndex&&(t.shiftKey?this.onOptionSelectRange(t,this.focusedOptionIndex):this.onOptionSelect(t,this.visibleOptions[this.focusedOptionIndex])):(this.focusedOptionIndex=-1,this.onArrowDownKey(t)),t.preventDefault()},onEscapeKey:function(t){this.overlayVisible&&this.hide(!0),t.preventDefault()},onTabKey:function(t){arguments.length>1&&void 0!==arguments[1]&&arguments[1]||(this.overlayVisible&&this.hasFocusableElements()?(et(t.shiftKey?this.$refs.lastHiddenFocusableElementOnOverlay:this.$refs.firstHiddenFocusableElementOnOverlay),t.preventDefault()):(-1!==this.focusedOptionIndex&&this.onOptionSelect(t,this.visibleOptions[this.focusedOptionIndex]),this.overlayVisible&&this.hide(this.filter)))},onShiftKey:function(){this.startRangeIndex=this.focusedOptionIndex},onOverlayEnter:function(t){Q.set("overlay",t,this.$primevue.config.zIndex.overlay),st(t,{position:"absolute",top:"0",left:"0"}),this.alignOverlay(),this.scrollInView(),this.autoFilterFocus&&et(this.$refs.filterInput.$el)},onOverlayAfterEnter:function(){this.bindOutsideClickListener(),this.bindScrollListener(),this.bindResizeListener(),this.$emit("show")},onOverlayLeave:function(){this.unbindOutsideClickListener(),this.unbindScrollListener(),this.unbindResizeListener(),this.$emit("hide"),this.overlay=null},onOverlayAfterLeave:function(t){Q.clear(t)},alignOverlay:function(){"self"===this.appendTo?lt(this.overlay,this.$el):(this.overlay.style.minWidth=G(this.$el)+"px",at(this.overlay,this.$el))},bindOutsideClickListener:function(){var t=this;this.outsideClickListener||(this.outsideClickListener=function(e){t.overlayVisible&&t.isOutsideClicked(e)&&t.hide()},document.addEventListener("click",this.outsideClickListener))},unbindOutsideClickListener:function(){this.outsideClickListener&&(document.removeEventListener("click",this.outsideClickListener),this.outsideClickListener=null)},bindScrollListener:function(){var t=this;this.scrollHandler||(this.scrollHandler=new Tt(this.$refs.container,(function(){t.overlayVisible&&t.hide()}))),this.scrollHandler.bindScrollListener()},unbindScrollListener:function(){this.scrollHandler&&this.scrollHandler.unbindScrollListener()},bindResizeListener:function(){var t=this;this.resizeListener||(this.resizeListener=function(){t.overlayVisible&&!ct()&&t.hide()},window.addEventListener("resize",this.resizeListener))},unbindResizeListener:function(){this.resizeListener&&(window.removeEventListener("resize",this.resizeListener),this.resizeListener=null)},isOutsideClicked:function(t){return!(this.$el.isSameNode(t.target)||this.$el.contains(t.target)||this.overlay&&this.overlay.contains(t.target))},getLabelByValue:function(t){var e=this,n=(this.optionGroupLabel?this.flatOptions(this.options):this.options||[]).find((function(n){return!e.isOptionGroup(n)&&dt(e.getOptionValue(n),t,e.equalityKey)}));return n?this.getOptionLabel(n):null},getSelectedItemsLabel:function(){var t=/{(.*?)}/,e=this.selectedItemsLabel||this.$primevue.config.locale.selectionMessage;return t.test(e)?e.replace(e.match(t)[0],this.d_value.length+""):e},onToggleAll:function(t){var e=this;if(null!==this.selectAll)this.$emit("selectall-change",{originalEvent:t,checked:!this.allSelected});else{var n=this.allSelected?[]:this.visibleOptions.filter((function(t){return e.isValidOption(t)})).map((function(t){return e.getOptionValue(t)}));this.updateModel(t,n)}},removeOption:function(t,e){var n=this;t.stopPropagation();var o=this.d_value.filter((function(t){return!dt(t,e,n.equalityKey)}));this.updateModel(t,o)},clearFilter:function(){this.filterValue=null},hasFocusableElements:function(){return ut(this.overlay,':not([data-p-hidden-focusable="true"])').length>0},isOptionMatched:function(t){var e;return this.isValidOption(t)&&"string"==typeof this.getOptionLabel(t)&&(null===(e=this.getOptionLabel(t))||void 0===e?void 0:e.toLocaleLowerCase(this.filterLocale).startsWith(this.searchValue.toLocaleLowerCase(this.filterLocale)))},isValidOption:function(t){return K(t)&&!(this.isOptionDisabled(t)||this.isOptionGroup(t))},isValidSelectedOption:function(t){return this.isValidOption(t)&&this.isSelected(t)},isEquals:function(t,e){return dt(t,e,this.equalityKey)},isSelected:function(t){var e=this,n=this.getOptionValue(t);return(this.d_value||[]).some((function(t){return e.isEquals(t,n)}))},findFirstOptionIndex:function(){var t=this;return this.visibleOptions.findIndex((function(e){return t.isValidOption(e)}))},findLastOptionIndex:function(){var t=this;return pt(this.visibleOptions,(function(e){return t.isValidOption(e)}))},findNextOptionIndex:function(t){var e=this,n=t-1?n+t+1:t},findPrevOptionIndex:function(t){var e=this,n=t>0?pt(this.visibleOptions.slice(0,t),(function(t){return e.isValidOption(t)})):-1;return n>-1?n:t},findSelectedOptionIndex:function(){var t=this;if(this.$filled)for(var e,n=function(){var e=t.d_value[o],n=t.visibleOptions.findIndex((function(n){return t.isValidSelectedOption(n)&&t.isEquals(e,t.getOptionValue(n))}));if(n>-1)return{v:n}},o=this.d_value.length-1;o>=0;o--)if(e=n())return e.v;return-1},findFirstSelectedOptionIndex:function(){var t=this;return this.$filled?this.visibleOptions.findIndex((function(e){return t.isValidSelectedOption(e)})):-1},findLastSelectedOptionIndex:function(){var t=this;return this.$filled?pt(this.visibleOptions,(function(e){return t.isValidSelectedOption(e)})):-1},findNextSelectedOptionIndex:function(t){var e=this,n=this.$filled&&t-1?n+t+1:-1},findPrevSelectedOptionIndex:function(t){var e=this,n=this.$filled&&t>0?pt(this.visibleOptions.slice(0,t),(function(t){return e.isValidSelectedOption(t)})):-1;return n>-1?n:-1},findNearestSelectedOptionIndex:function(t){var e=arguments.length>1&&void 0!==arguments[1]&&arguments[1],n=-1;return this.$filled&&(n=e?-1===(n=this.findPrevSelectedOptionIndex(t))?this.findNextSelectedOptionIndex(t):n:-1===(n=this.findNextSelectedOptionIndex(t))?this.findPrevSelectedOptionIndex(t):n),n>-1?n:t},findFirstFocusedOptionIndex:function(){var t=this.findSelectedOptionIndex();return t<0?this.findFirstOptionIndex():t},findLastFocusedOptionIndex:function(){var t=this.findSelectedOptionIndex();return t<0?this.findLastOptionIndex():t},searchOptions:function(t){var e=this;this.searchValue=(this.searchValue||"")+t.key;var n=-1;K(this.searchValue)&&(-1===(n=-1!==this.focusedOptionIndex?-1===(n=this.visibleOptions.slice(this.focusedOptionIndex).findIndex((function(t){return e.isOptionMatched(t)})))?this.visibleOptions.slice(0,this.focusedOptionIndex).findIndex((function(t){return e.isOptionMatched(t)})):n+this.focusedOptionIndex:this.visibleOptions.findIndex((function(t){return e.isOptionMatched(t)})))&&-1===this.focusedOptionIndex&&(n=this.findFirstFocusedOptionIndex()),-1!==n&&this.changeFocusedOptionIndex(t,n)),this.searchTimeout&&clearTimeout(this.searchTimeout),this.searchTimeout=setTimeout((function(){e.searchValue="",e.searchTimeout=null}),500)},changeFocusedOptionIndex:function(t,e){this.focusedOptionIndex!==e&&(this.focusedOptionIndex=e,this.scrollInView(),this.selectOnFocus&&this.onOptionSelect(t,this.visibleOptions[e]))},scrollInView:function(){var t=this,e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:-1;this.$nextTick((function(){var n=-1!==e?"".concat(t.id,"_").concat(e):t.focusedOptionId,o=J(t.list,'li[id="'.concat(n,'"]'));o?o.scrollIntoView&&o.scrollIntoView({block:"nearest",inline:"nearest"}):t.virtualScrollerDisabled||t.virtualScroller&&t.virtualScroller.scrollToIndex(-1!==e?e:t.focusedOptionIndex)}))},autoUpdateModel:function(){if(this.selectOnFocus&&this.autoOptionFocus&&!this.$filled){this.focusedOptionIndex=this.findFirstFocusedOptionIndex();var t=this.getOptionValue(this.visibleOptions[this.focusedOptionIndex]);this.updateModel(null,[t])}},updateModel:function(t,e){this.writeValue(e,t),this.$emit("change",{originalEvent:t,value:e})},flatOptions:function(t){var e=this;return(t||[]).reduce((function(t,n,o){t.push({optionGroup:n,group:!0,index:o});var i=e.getOptionGroupChildren(n);return i&&i.forEach((function(e){return t.push(e)})),t}),[])},overlayRef:function(t){this.overlay=t},listRef:function(t,e){this.list=t,e&&e(t)},virtualScrollerRef:function(t){this.virtualScroller=t}},computed:{visibleOptions:function(){var t=this,e=this.optionGroupLabel?this.flatOptions(this.options):this.options||[];if(this.filterValue){var n=_t.filter(e,this.searchFields,this.filterValue,this.filterMatchMode,this.filterLocale);if(this.optionGroupLabel){var o=this.options||[],i=[];return o.forEach((function(e){var o=t.getOptionGroupChildren(e).filter((function(t){return n.includes(t)}));o.length>0&&i.push(Kn(Kn({},e),{},Dn({},"string"==typeof t.optionGroupChildren?t.optionGroupChildren:"items",Bn(o))))})),this.flatOptions(i)}return n}return e},label:function(){var t;if(this.d_value&&this.d_value.length){if(K(this.maxSelectedLabels)&&this.d_value.length>this.maxSelectedLabels)return this.getSelectedItemsLabel();t="";for(var e=0;ethis.maxSelectedLabels},allSelected:function(){var t=this;return null!==this.selectAll?this.selectAll:K(this.visibleOptions)&&this.visibleOptions.every((function(e){return t.isOptionGroup(e)||t.isOptionDisabled(e)||t.isSelected(e)}))},hasSelectedOption:function(){return this.$filled},equalityKey:function(){return this.optionValue?null:this.dataKey},searchFields:function(){return this.filterFields||[this.optionLabel]},maxSelectionLimitReached:function(){return this.selectionLimit&&this.d_value&&this.d_value.length===this.selectionLimit},filterResultMessageText:function(){return K(this.visibleOptions)?this.filterMessageText.replaceAll("{0}",this.visibleOptions.length):this.emptyFilterMessageText},filterMessageText:function(){return this.filterMessage||this.$primevue.config.locale.searchMessage||""},emptyFilterMessageText:function(){return this.emptyFilterMessage||this.$primevue.config.locale.emptySearchMessage||this.$primevue.config.locale.emptyFilterMessage||""},emptyMessageText:function(){return this.emptyMessage||this.$primevue.config.locale.emptyMessage||""},selectionMessageText:function(){return this.selectionMessage||this.$primevue.config.locale.selectionMessage||""},emptySelectionMessageText:function(){return this.emptySelectionMessage||this.$primevue.config.locale.emptySelectionMessage||""},selectedMessageText:function(){return this.$filled?this.selectionMessageText.replaceAll("{0}",this.d_value.length):this.emptySelectionMessageText},focusedOptionId:function(){return-1!==this.focusedOptionIndex?"".concat(this.id,"_").concat(this.focusedOptionIndex):null},ariaSetSize:function(){var t=this;return this.visibleOptions.filter((function(e){return!t.isOptionGroup(e)})).length},toggleAllAriaLabel:function(){return this.$primevue.config.locale.aria?this.$primevue.config.locale.aria[this.allSelected?"selectAll":"unselectAll"]:void 0},listAriaLabel:function(){return this.$primevue.config.locale.aria?this.$primevue.config.locale.aria.listLabel:void 0},virtualScrollerDisabled:function(){return!this.virtualScrollerOptions},hasFluid:function(){return D(this.fluid)?!!this.$pcFluid:this.fluid},isClearIconVisible:function(){return this.showClear&&null!=this.d_value&&K(this.options)}},directives:{ripple:De},components:{InputText:We,Checkbox:Cn,VirtualScroller:nn,Portal:Ye,Chip:_n,IconField:Ue,InputIcon:Ne,TimesIcon:Mt,SearchIcon:Pt,ChevronDownIcon:Vt,SpinnerIcon:Ct,CheckIcon:Ft}};function Rn(t){return(Rn="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}function Gn(t,e,n){return(e=function(t){var e=function(t,e){if("object"!=Rn(t)||!t)return t;var n=t[Symbol.toPrimitive];if(void 0!==n){var o=n.call(t,e||"default");if("object"!=Rn(o))return o;throw new TypeError("@@toPrimitive must return a primitive value.")}return("string"===e?String:Number)(t)}(t,"string");return"symbol"==Rn(e)?e:e+""}(e))in t?Object.defineProperty(t,e,{value:n,enumerable:!0,configurable:!0,writable:!0}):t[e]=n,t}var Un=["id","disabled","placeholder","tabindex","aria-label","aria-labelledby","aria-expanded","aria-controls","aria-activedescendant","aria-invalid"],Nn={key:0},Wn=["id","aria-label"],qn=["id"],Xn=["id","aria-label","aria-selected","aria-disabled","aria-setsize","aria-posinset","onClick","onMousemove","data-p-selected","data-p-focused","data-p-disabled"];jn.render=function(t,e,n,o,i,r){var s=k("Chip"),l=k("SpinnerIcon"),a=k("Checkbox"),c=k("InputText"),u=k("SearchIcon"),d=k("InputIcon"),p=k("IconField"),h=k("VirtualScroller"),f=k("Portal"),b=w("ripple");return m(),g("div",I({ref:"container",class:t.cx("root"),style:t.sx("root"),onClick:e[7]||(e[7]=function(){return r.onContainerClick&&r.onContainerClick.apply(r,arguments)})},t.ptmi("root")),[C("div",I({class:"p-hidden-accessible"},t.ptm("hiddenInputContainer"),{"data-p-hidden-accessible":!0}),[C("input",I({ref:"focusInput",id:t.inputId,type:"text",readonly:"",disabled:t.disabled,placeholder:t.placeholder,tabindex:t.disabled?-1:t.tabindex,role:"combobox","aria-label":t.ariaLabel,"aria-labelledby":t.ariaLabelledby,"aria-haspopup":"listbox","aria-expanded":i.overlayVisible,"aria-controls":i.id+"_list","aria-activedescendant":i.focused?r.focusedOptionId:void 0,"aria-invalid":t.invalid||void 0,onFocus:e[0]||(e[0]=function(){return r.onFocus&&r.onFocus.apply(r,arguments)}),onBlur:e[1]||(e[1]=function(){return r.onBlur&&r.onBlur.apply(r,arguments)}),onKeydown:e[2]||(e[2]=function(){return r.onKeyDown&&r.onKeyDown.apply(r,arguments)})},t.ptm("hiddenInput")),null,16,Un)],16),C("div",I({class:t.cx("labelContainer")},t.ptm("labelContainer")),[C("div",I({class:t.cx("label")},t.ptm("label")),[v(t.$slots,"value",{value:t.d_value,placeholder:t.placeholder},(function(){return["comma"===t.display?(m(),g(_,{key:0},[y(x(r.label||"empty"),1)],64)):"chip"===t.display?(m(),g(_,{key:1},[r.chipSelectedItems?(m(),g("span",Nn,x(r.label),1)):(m(!0),g(_,{key:1},M(t.d_value,(function(e){return m(),g("span",I({key:r.getLabelByValue(e),class:t.cx("chipItem"),ref_for:!0},t.ptm("chipItem")),[v(t.$slots,"chip",{value:e,removeCallback:function(t){return r.removeOption(t,e)}},(function(){return[V(s,{class:z(t.cx("pcChip")),label:r.getLabelByValue(e),removeIcon:t.chipIcon||t.removeTokenIcon,removable:"",unstyled:t.unstyled,onRemove:function(t){return r.removeOption(t,e)},pt:t.ptm("pcChip")},{removeicon:L((function(){return[v(t.$slots,t.$slots.chipicon?"chipicon":"removetokenicon",{class:z(t.cx("chipIcon")),item:e,removeCallback:function(t){return r.removeOption(t,e)}})]})),_:2},1032,["class","label","removeIcon","unstyled","onRemove","pt"])]}))],16)})),128)),t.d_value&&0!==t.d_value.length?$("",!0):(m(),g(_,{key:2},[y(x(t.placeholder||"empty"),1)],64))],64)):$("",!0)]}))],16)],16),r.isClearIconVisible?v(t.$slots,"clearicon",{key:0,class:z(t.cx("clearIcon")),clearCallback:r.onClearClick},(function(){return[(m(),S(E(t.clearIcon?"i":"TimesIcon"),I({ref:"clearIcon",class:[t.cx("clearIcon"),t.clearIcon],onClick:r.onClearClick},t.ptm("clearIcon"),{"data-pc-section":"clearicon"}),null,16,["class","onClick"]))]})):$("",!0),C("div",I({class:t.cx("dropdown")},t.ptm("dropdown")),[t.loading?v(t.$slots,"loadingicon",{key:0,class:z(t.cx("loadingIcon"))},(function(){return[t.loadingIcon?(m(),g("span",I({key:0,class:[t.cx("loadingIcon"),"pi-spin",t.loadingIcon],"aria-hidden":"true"},t.ptm("loadingIcon")),null,16)):(m(),S(l,I({key:1,class:t.cx("loadingIcon"),spin:"","aria-hidden":"true"},t.ptm("loadingIcon")),null,16,["class"]))]})):v(t.$slots,"dropdownicon",{key:1,class:z(t.cx("dropdownIcon"))},(function(){return[(m(),S(E(t.dropdownIcon?"span":"ChevronDownIcon"),I({class:[t.cx("dropdownIcon"),t.dropdownIcon],"aria-hidden":"true"},t.ptm("dropdownIcon")),null,16,["class"]))]}))],16),V(f,{appendTo:t.appendTo},{default:L((function(){return[V(P,I({name:"p-connected-overlay",onEnter:r.onOverlayEnter,onAfterEnter:r.onOverlayAfterEnter,onLeave:r.onOverlayLeave,onAfterLeave:r.onOverlayAfterLeave},t.ptm("transition")),{default:L((function(){return[i.overlayVisible?(m(),g("div",I({key:0,ref:r.overlayRef,style:[t.panelStyle,t.overlayStyle],class:[t.cx("overlay"),t.panelClass,t.overlayClass],onClick:e[5]||(e[5]=function(){return r.onOverlayClick&&r.onOverlayClick.apply(r,arguments)}),onKeydown:e[6]||(e[6]=function(){return r.onOverlayKeyDown&&r.onOverlayKeyDown.apply(r,arguments)})},t.ptm("overlay")),[C("span",I({ref:"firstHiddenFocusableElementOnOverlay",role:"presentation","aria-hidden":"true",class:"p-hidden-accessible p-hidden-focusable",tabindex:0,onFocus:e[3]||(e[3]=function(){return r.onFirstHiddenFocus&&r.onFirstHiddenFocus.apply(r,arguments)})},t.ptm("hiddenFirstFocusableEl"),{"data-p-hidden-accessible":!0,"data-p-hidden-focusable":!0}),null,16),v(t.$slots,"header",{value:t.d_value,options:r.visibleOptions}),t.showToggleAll&&null==t.selectionLimit||t.filter?(m(),g("div",I({key:0,class:t.cx("header")},t.ptm("header")),[t.showToggleAll&&null==t.selectionLimit?(m(),S(a,{key:0,modelValue:r.allSelected,binary:!0,disabled:t.disabled,variant:t.variant,"aria-label":r.toggleAllAriaLabel,onChange:r.onToggleAll,unstyled:t.unstyled,pt:r.getHeaderCheckboxPTOptions("pcHeaderCheckbox")},{icon:L((function(e){return[t.$slots.headercheckboxicon?(m(),S(E(t.$slots.headercheckboxicon),{key:0,checked:e.checked,class:z(e.class)},null,8,["checked","class"])):e.checked?(m(),S(E(t.checkboxIcon?"span":"CheckIcon"),I({key:1,class:[e.class,Gn({},t.checkboxIcon,e.checked)]},r.getHeaderCheckboxPTOptions("pcHeaderCheckbox.icon")),null,16,["class"])):$("",!0)]})),_:1},8,["modelValue","disabled","variant","aria-label","onChange","unstyled","pt"])):$("",!0),t.filter?(m(),S(p,{key:1,class:z(t.cx("pcFilterContainer")),unstyled:t.unstyled,pt:t.ptm("pcFilterContainer")},{default:L((function(){return[V(c,{ref:"filterInput",value:i.filterValue,onVnodeMounted:r.onFilterUpdated,onVnodeUpdated:r.onFilterUpdated,class:z(t.cx("pcFilter")),placeholder:t.filterPlaceholder,disabled:t.disabled,variant:t.variant,unstyled:t.unstyled,role:"searchbox",autocomplete:"off","aria-owns":i.id+"_list","aria-activedescendant":r.focusedOptionId,onKeydown:r.onFilterKeyDown,onBlur:r.onFilterBlur,onInput:r.onFilterChange,pt:t.ptm("pcFilter")},null,8,["value","onVnodeMounted","onVnodeUpdated","class","placeholder","disabled","variant","unstyled","aria-owns","aria-activedescendant","onKeydown","onBlur","onInput","pt"]),V(d,{unstyled:t.unstyled,pt:t.ptm("pcFilterIconContainer")},{default:L((function(){return[v(t.$slots,"filtericon",{},(function(){return[t.filterIcon?(m(),g("span",I({key:0,class:t.filterIcon},t.ptm("filterIcon")),null,16)):(m(),S(u,F(I({key:1},t.ptm("filterIcon"))),null,16))]}))]})),_:3},8,["unstyled","pt"])]})),_:3},8,["class","unstyled","pt"])):$("",!0),t.filter?(m(),g("span",I({key:2,role:"status","aria-live":"polite",class:"p-hidden-accessible"},t.ptm("hiddenFilterResult"),{"data-p-hidden-accessible":!0}),x(r.filterResultMessageText),17)):$("",!0)],16)):$("",!0),C("div",I({class:t.cx("listContainer"),style:{"max-height":r.virtualScrollerDisabled?t.scrollHeight:""}},t.ptm("listContainer")),[V(h,I({ref:r.virtualScrollerRef},t.virtualScrollerOptions,{items:r.visibleOptions,style:{height:t.scrollHeight},tabindex:-1,disabled:r.virtualScrollerDisabled,pt:t.ptm("virtualScroller")}),A({content:L((function(e){var n=e.styleClass,o=e.contentRef,s=e.items,l=e.getItemOptions,c=e.contentStyle,u=e.itemSize;return[C("ul",I({ref:function(t){return r.listRef(t,o)},id:i.id+"_list",class:[t.cx("list"),n],style:c,role:"listbox","aria-multiselectable":"true","aria-label":r.listAriaLabel},t.ptm("list")),[(m(!0),g(_,null,M(s,(function(e,n){return m(),g(_,{key:r.getOptionRenderKey(e,r.getOptionIndex(n,l))},[r.isOptionGroup(e)?(m(),g("li",I({key:0,id:i.id+"_"+r.getOptionIndex(n,l),style:{height:u?u+"px":void 0},class:t.cx("optionGroup"),role:"option",ref_for:!0},t.ptm("optionGroup")),[v(t.$slots,"optiongroup",{option:e.optionGroup,index:r.getOptionIndex(n,l)},(function(){return[y(x(r.getOptionGroupLabel(e.optionGroup)),1)]}))],16,qn)):O((m(),g("li",I({key:1,id:i.id+"_"+r.getOptionIndex(n,l),style:{height:u?u+"px":void 0},class:t.cx("option",{option:e,index:n,getItemOptions:l}),role:"option","aria-label":r.getOptionLabel(e),"aria-selected":r.isSelected(e),"aria-disabled":r.isOptionDisabled(e),"aria-setsize":r.ariaSetSize,"aria-posinset":r.getAriaPosInset(r.getOptionIndex(n,l)),onClick:function(t){return r.onOptionSelect(t,e,r.getOptionIndex(n,l),!0)},onMousemove:function(t){return r.onOptionMouseMove(t,r.getOptionIndex(n,l))},ref_for:!0},r.getCheckboxPTOptions(e,l,n,"option"),{"data-p-selected":r.isSelected(e),"data-p-focused":i.focusedOptionIndex===r.getOptionIndex(n,l),"data-p-disabled":r.isOptionDisabled(e)}),[V(a,{defaultValue:r.isSelected(e),binary:!0,tabindex:-1,variant:t.variant,unstyled:t.unstyled,pt:r.getCheckboxPTOptions(e,l,n,"pcOptionCheckbox")},{icon:L((function(o){return[t.$slots.optioncheckboxicon||t.$slots.itemcheckboxicon?(m(),S(E(t.$slots.optioncheckboxicon||t.$slots.itemcheckboxicon),{key:0,checked:o.checked,class:z(o.class)},null,8,["checked","class"])):o.checked?(m(),S(E(t.checkboxIcon?"span":"CheckIcon"),I({key:1,class:[o.class,Gn({},t.checkboxIcon,o.checked)],ref_for:!0},r.getCheckboxPTOptions(e,l,n,"pcOptionCheckbox.icon")),null,16,["class"])):$("",!0)]})),_:2},1032,["defaultValue","variant","unstyled","pt"]),v(t.$slots,"option",{option:e,selected:r.isSelected(e),index:r.getOptionIndex(n,l)},(function(){return[C("span",I({ref_for:!0},t.ptm("optionLabel")),x(r.getOptionLabel(e)),17)]}))],16,Xn)),[[b]])],64)})),128)),i.filterValue&&(!s||s&&0===s.length)?(m(),g("li",I({key:0,class:t.cx("emptyMessage"),role:"option"},t.ptm("emptyMessage")),[v(t.$slots,"emptyfilter",{},(function(){return[y(x(r.emptyFilterMessageText),1)]}))],16)):!t.options||t.options&&0===t.options.length?(m(),g("li",I({key:1,class:t.cx("emptyMessage"),role:"option"},t.ptm("emptyMessage")),[v(t.$slots,"empty",{},(function(){return[y(x(r.emptyMessageText),1)]}))],16)):$("",!0)],16,Wn)]})),_:2},[t.$slots.loader?{name:"loader",fn:L((function(e){var n=e.options;return[v(t.$slots,"loader",{options:n})]})),key:"0"}:void 0]),1040,["items","style","disabled","pt"])],16),v(t.$slots,"footer",{value:t.d_value,options:r.visibleOptions}),!t.options||t.options&&0===t.options.length?(m(),g("span",I({key:1,role:"status","aria-live":"polite",class:"p-hidden-accessible"},t.ptm("hiddenEmptyMessage"),{"data-p-hidden-accessible":!0}),x(r.emptyMessageText),17)):$("",!0),C("span",I({role:"status","aria-live":"polite",class:"p-hidden-accessible"},t.ptm("hiddenSelectedMessage"),{"data-p-hidden-accessible":!0}),x(r.selectedMessageText),17),C("span",I({ref:"lastHiddenFocusableElementOnOverlay",role:"presentation","aria-hidden":"true",class:"p-hidden-accessible p-hidden-focusable",tabindex:0,onFocus:e[4]||(e[4]=function(){return r.onLastHiddenFocus&&r.onLastHiddenFocus.apply(r,arguments)})},t.ptm("hiddenLastFocusableEl"),{"data-p-hidden-accessible":!0,"data-p-hidden-focusable":!0}),null,16)],16)):$("",!0)]})),_:3},16,["onEnter","onAfterEnter","onLeave","onAfterLeave"])]})),_:3},8,["appendTo"])],16)};var Yn=St.extend({name:"slider",theme:function(t){var e=t.dt;return"\n.p-slider {\n position: relative;\n background: ".concat(e("slider.track.background"),";\n border-radius: ").concat(e("slider.track.border.radius"),";\n}\n\n.p-slider-handle {\n cursor: grab;\n touch-action: none;\n user-select: none;\n display: flex;\n justify-content: center;\n align-items: center;\n height: ").concat(e("slider.handle.height"),";\n width: ").concat(e("slider.handle.width"),";\n background: ").concat(e("slider.handle.background"),";\n border-radius: ").concat(e("slider.handle.border.radius"),";\n transition: background ").concat(e("slider.transition.duration"),", color ").concat(e("slider.transition.duration"),", border-color ").concat(e("slider.transition.duration"),", box-shadow ").concat(e("slider.transition.duration"),", outline-color ").concat(e("slider.transition.duration"),';\n outline-color: transparent;\n}\n\n.p-slider-handle::before {\n content: "";\n width: ').concat(e("slider.handle.content.width"),";\n height: ").concat(e("slider.handle.content.height"),";\n display: block;\n background: ").concat(e("slider.handle.content.background"),";\n border-radius: ").concat(e("slider.handle.content.border.radius"),";\n box-shadow: ").concat(e("slider.handle.content.shadow"),";\n transition: background ").concat(e("slider.transition.duration"),";\n}\n\n.p-slider:not(.p-disabled) .p-slider-handle:hover {\n background: ").concat(e("slider.handle.hover.background"),";\n}\n\n.p-slider:not(.p-disabled) .p-slider-handle:hover::before {\n background: ").concat(e("slider.handle.content.hover.background"),";\n}\n\n.p-slider-handle:focus-visible {\n box-shadow: ").concat(e("slider.handle.focus.ring.shadow"),";\n outline: ").concat(e("slider.handle.focus.ring.width")," ").concat(e("slider.handle.focus.ring.style")," ").concat(e("slider.handle.focus.ring.color"),";\n outline-offset: ").concat(e("slider.handle.focus.ring.offset"),";\n}\n\n.p-slider-range {\n display: block;\n background: ").concat(e("slider.range.background"),";\n border-radius: ").concat(e("slider.border.radius"),";\n}\n\n.p-slider.p-slider-horizontal {\n height: ").concat(e("slider.track.size"),";\n}\n\n.p-slider-horizontal .p-slider-range {\n inset-block-start: 0;\n inset-inline-start: 0;\n height: 100%;\n}\n\n.p-slider-horizontal .p-slider-handle {\n inset-block-start: 50%;\n margin-block-start: calc(-1 * calc(").concat(e("slider.handle.height")," / 2));\n margin-inline-start: calc(-1 * calc(").concat(e("slider.handle.width")," / 2));\n}\n\n.p-slider-vertical {\n min-height: 100px;\n width: ").concat(e("slider.track.size"),";\n}\n\n.p-slider-vertical .p-slider-handle {\n inset-inline-start: 50%;\n margin-inline-start: calc(-1 * calc(").concat(e("slider.handle.width")," / 2));\n margin-block-end: calc(-1 * calc(").concat(e("slider.handle.height")," / 2));\n}\n\n.p-slider-vertical .p-slider-range {\n inset-block-end: 0;\n inset-inline-start: 0;\n width: 100%;\n}\n")},classes:{root:function(t){var e=t.instance,n=t.props;return["p-slider p-component",{"p-disabled":n.disabled,"p-invalid":e.$invalid,"p-slider-horizontal":"horizontal"===n.orientation,"p-slider-vertical":"vertical"===n.orientation}]},range:"p-slider-range",handle:"p-slider-handle"},inlineStyles:{handle:{position:"absolute"},range:{position:"absolute"}}});function Zn(t){return function(t){if(Array.isArray(t))return Jn(t)}(t)||function(t){if("undefined"!=typeof Symbol&&null!=t[Symbol.iterator]||null!=t["@@iterator"])return Array.from(t)}(t)||function(t,e){if(t){if("string"==typeof t)return Jn(t,e);var n={}.toString.call(t).slice(8,-1);return"Object"===n&&t.constructor&&(n=t.constructor.name),"Map"===n||"Set"===n?Array.from(t):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?Jn(t,e):void 0}}(t)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function Jn(t,e){(null==e||e>t.length)&&(e=t.length);for(var n=0,o=Array(e);n0&&(i=r+Math.floor(i/this.step-r/this.step)*this.step)}else i=Math.floor(i);this.updateModel(t,i)},updateModel:function(t,e){var n,o=parseFloat(e.toFixed(10));this.range?(n=this.value?Zn(this.value):[],0==this.handleIndex?(o=this.max&&(o=this.max),n[0]=o):(o>this.max?o=this.max:o<=this.min&&(o=this.min),n[1]=o)):(othis.max&&(o=this.max),n=o),this.writeValue(n,t),this.$emit("change",n)},onDragStart:function(t,e){this.disabled||(this.$el.setAttribute("data-p-sliding",!0),this.dragging=!0,this.updateDomData(),this.range&&this.value[0]===this.max?this.handleIndex=0:this.handleIndex=e,t.currentTarget.focus())},onDrag:function(t){this.dragging&&this.setValue(t)},onDragEnd:function(t){this.dragging&&(this.dragging=!1,this.$el.setAttribute("data-p-sliding",!1),this.$emit("slideend",{originalEvent:t,value:this.value}))},onBarClick:function(t){this.disabled||"handle"!==q(t.target,"data-pc-section")&&(this.updateDomData(),this.setValue(t))},onMouseDown:function(t,e){this.bindDragListeners(),this.onDragStart(t,e)},onKeyDown:function(t,e){switch(this.handleIndex=e,t.code){case"ArrowDown":case"ArrowLeft":this.decrementValue(t,e),t.preventDefault();break;case"ArrowUp":case"ArrowRight":this.incrementValue(t,e),t.preventDefault();break;case"PageDown":this.decrementValue(t,e,!0),t.preventDefault();break;case"PageUp":this.incrementValue(t,e,!0),t.preventDefault();break;case"Home":this.updateModel(t,this.min),t.preventDefault();break;case"End":this.updateModel(t,this.max),t.preventDefault()}},onBlur:function(t,e){var n,o;null===(n=(o=this.formField).onBlur)||void 0===n||n.call(o,t)},decrementValue:function(t,e){var n,o=arguments.length>2&&void 0!==arguments[2]&&arguments[2];n=this.range?this.step?this.value[e]-this.step:this.value[e]-1:this.step?this.value-this.step:!this.step&&o?this.value-10:this.value-1,this.updateModel(t,n),t.preventDefault()},incrementValue:function(t,e){var n,o=arguments.length>2&&void 0!==arguments[2]&&arguments[2];n=this.range?this.step?this.value[e]+this.step:this.value[e]+1:this.step?this.value+this.step:!this.step&&o?this.value+10:this.value+1,this.updateModel(t,n),t.preventDefault()},bindDragListeners:function(){this.dragListener||(this.dragListener=this.onDrag.bind(this),document.addEventListener("mousemove",this.dragListener)),this.dragEndListener||(this.dragEndListener=this.onDragEnd.bind(this),document.addEventListener("mouseup",this.dragEndListener))},unbindDragListeners:function(){this.dragListener&&(document.removeEventListener("mousemove",this.dragListener),this.dragListener=null),this.dragEndListener&&(document.removeEventListener("mouseup",this.dragEndListener),this.dragEndListener=null)},rangeStyle:function(){if(this.range){var t=this.rangeEndPosition>this.rangeStartPosition?this.rangeEndPosition-this.rangeStartPosition:this.rangeStartPosition-this.rangeEndPosition,e=this.rangeEndPosition>this.rangeStartPosition?this.rangeStartPosition:this.rangeEndPosition;return this.horizontal?{"inset-inline-start":e+"%",width:t+"%"}:{bottom:e+"%",height:t+"%"}}return this.horizontal?{width:this.handlePosition+"%"}:{height:this.handlePosition+"%"}},handleStyle:function(){return this.horizontal?{"inset-inline-start":this.handlePosition+"%"}:{bottom:this.handlePosition+"%"}},rangeStartHandleStyle:function(){return this.horizontal?{"inset-inline-start":this.rangeStartPosition+"%"}:{bottom:this.rangeStartPosition+"%"}},rangeEndHandleStyle:function(){return this.horizontal?{"inset-inline-start":this.rangeEndPosition+"%"}:{bottom:this.rangeEndPosition+"%"}}},computed:{value:function(){var t,e,n,o,i;return this.range?[null!==(e=null===(n=this.d_value)||void 0===n?void 0:n[0])&&void 0!==e?e:this.min,null!==(o=null===(i=this.d_value)||void 0===i?void 0:i[1])&&void 0!==o?o:this.max]:null!==(t=this.d_value)&&void 0!==t?t:this.min},horizontal:function(){return"horizontal"===this.orientation},vertical:function(){return"vertical"===this.orientation},handlePosition:function(){return this.valuethis.max?100:100*(this.value-this.min)/(this.max-this.min)},rangeStartPosition:function(){return this.value&&void 0!==this.value[0]?this.value[0]this.max?100:100*(this.value[1]-this.min)/(this.max-this.min):100}}},to=["tabindex","aria-valuemin","aria-valuenow","aria-valuemax","aria-labelledby","aria-label","aria-orientation"],eo=["tabindex","aria-valuemin","aria-valuenow","aria-valuemax","aria-labelledby","aria-label","aria-orientation"],no=["tabindex","aria-valuemin","aria-valuenow","aria-valuemax","aria-labelledby","aria-label","aria-orientation"];Qn.render=function(t,e,n,o,i,r){return m(),g("div",I({class:t.cx("root"),onClick:e[18]||(e[18]=function(){return r.onBarClick&&r.onBarClick.apply(r,arguments)})},t.ptmi("root"),{"data-p-sliding":!1}),[C("span",I({class:t.cx("range"),style:[t.sx("range"),r.rangeStyle()]},t.ptm("range")),null,16),t.range?$("",!0):(m(),g("span",I({key:0,class:t.cx("handle"),style:[t.sx("handle"),r.handleStyle()],onTouchstartPassive:e[0]||(e[0]=function(t){return r.onDragStart(t)}),onTouchmovePassive:e[1]||(e[1]=function(t){return r.onDrag(t)}),onTouchend:e[2]||(e[2]=function(t){return r.onDragEnd(t)}),onMousedown:e[3]||(e[3]=function(t){return r.onMouseDown(t)}),onKeydown:e[4]||(e[4]=function(t){return r.onKeyDown(t)}),onBlur:e[5]||(e[5]=function(t){return r.onBlur(t)}),tabindex:t.tabindex,role:"slider","aria-valuemin":t.min,"aria-valuenow":t.d_value,"aria-valuemax":t.max,"aria-labelledby":t.ariaLabelledby,"aria-label":t.ariaLabel,"aria-orientation":t.orientation},t.ptm("handle")),null,16,to)),t.range?(m(),g("span",I({key:1,class:t.cx("handle"),style:[t.sx("handle"),r.rangeStartHandleStyle()],onTouchstartPassive:e[6]||(e[6]=function(t){return r.onDragStart(t,0)}),onTouchmovePassive:e[7]||(e[7]=function(t){return r.onDrag(t)}),onTouchend:e[8]||(e[8]=function(t){return r.onDragEnd(t)}),onMousedown:e[9]||(e[9]=function(t){return r.onMouseDown(t,0)}),onKeydown:e[10]||(e[10]=function(t){return r.onKeyDown(t,0)}),onBlur:e[11]||(e[11]=function(t){return r.onBlur(t,0)}),tabindex:t.tabindex,role:"slider","aria-valuemin":t.min,"aria-valuenow":t.d_value?t.d_value[0]:null,"aria-valuemax":t.max,"aria-labelledby":t.ariaLabelledby,"aria-label":t.ariaLabel,"aria-orientation":t.orientation},t.ptm("startHandler")),null,16,eo)):$("",!0),t.range?(m(),g("span",I({key:2,class:t.cx("handle"),style:[t.sx("handle"),r.rangeEndHandleStyle()],onTouchstartPassive:e[12]||(e[12]=function(t){return r.onDragStart(t,1)}),onTouchmovePassive:e[13]||(e[13]=function(t){return r.onDrag(t)}),onTouchend:e[14]||(e[14]=function(t){return r.onDragEnd(t)}),onMousedown:e[15]||(e[15]=function(t){return r.onMouseDown(t,1)}),onKeydown:e[16]||(e[16]=function(t){return r.onKeyDown(t,1)}),onBlur:e[17]||(e[17]=function(t){return r.onBlur(t,1)}),tabindex:t.tabindex,role:"slider","aria-valuemin":t.min,"aria-valuenow":t.d_value?t.d_value[1]:null,"aria-valuemax":t.max,"aria-labelledby":t.ariaLabelledby,"aria-label":t.ariaLabel,"aria-orientation":t.orientation},t.ptm("endHandler")),null,16,no)):$("",!0)],16)};var oo={name:"Card",extends:{name:"BaseCard",extends:Lt,style:St.extend({name:"card",theme:function(t){var e=t.dt;return"\n.p-card {\n background: ".concat(e("card.background"),";\n color: ").concat(e("card.color"),";\n box-shadow: ").concat(e("card.shadow"),";\n border-radius: ").concat(e("card.border.radius"),";\n display: flex;\n flex-direction: column;\n}\n\n.p-card-caption {\n display: flex;\n flex-direction: column;\n gap: ").concat(e("card.caption.gap"),";\n}\n\n.p-card-body {\n padding: ").concat(e("card.body.padding"),";\n display: flex;\n flex-direction: column;\n gap: ").concat(e("card.body.gap"),";\n}\n\n.p-card-title {\n font-size: ").concat(e("card.title.font.size"),";\n font-weight: ").concat(e("card.title.font.weight"),";\n}\n\n.p-card-subtitle {\n color: ").concat(e("card.subtitle.color"),";\n}\n")},classes:{root:"p-card p-component",header:"p-card-header",body:"p-card-body",caption:"p-card-caption",title:"p-card-title",subtitle:"p-card-subtitle",content:"p-card-content",footer:"p-card-footer"}}),provide:function(){return{$pcCard:this,$parentInstance:this}}},inheritAttrs:!1};oo.render=function(t,e,n,o,i,r){return m(),g("div",I({class:t.cx("root")},t.ptmi("root")),[t.$slots.header?(m(),g("div",I({key:0,class:t.cx("header")},t.ptm("header")),[v(t.$slots,"header")],16)):$("",!0),C("div",I({class:t.cx("body")},t.ptm("body")),[t.$slots.title||t.$slots.subtitle?(m(),g("div",I({key:0,class:t.cx("caption")},t.ptm("caption")),[t.$slots.title?(m(),g("div",I({key:0,class:t.cx("title")},t.ptm("title")),[v(t.$slots,"title")],16)):$("",!0),t.$slots.subtitle?(m(),g("div",I({key:1,class:t.cx("subtitle")},t.ptm("subtitle")),[v(t.$slots,"subtitle")],16)):$("",!0)],16)):$("",!0),C("div",I({class:t.cx("content")},t.ptm("content")),[v(t.$slots,"content")],16),t.$slots.footer?(m(),g("div",I({key:1,class:t.cx("footer")},t.ptm("footer")),[v(t.$slots,"footer")],16)):$("",!0)],16)],16)};var io=St.extend({name:"contextmenu",theme:function(t){var e=t.dt;return"\n.p-contextmenu {\n background: ".concat(e("contextmenu.background"),";\n color: ").concat(e("contextmenu.color"),";\n border: 1px solid ").concat(e("contextmenu.border.color"),";\n border-radius: ").concat(e("contextmenu.border.radius"),";\n box-shadow: ").concat(e("contextmenu.shadow"),";\n min-width: 12.5rem;\n}\n\n.p-contextmenu-root-list,\n.p-contextmenu-submenu {\n margin: 0;\n padding: ").concat(e("contextmenu.list.padding"),";\n list-style: none;\n outline: 0 none;\n display: flex;\n flex-direction: column;\n gap: ").concat(e("contextmenu.list.gap"),";\n}\n\n.p-contextmenu-submenu {\n position: absolute;\n display: flex;\n flex-direction: column;\n min-width: 100%;\n z-index: 1;\n background: ").concat(e("contextmenu.background"),";\n color: ").concat(e("contextmenu.color"),";\n border: 1px solid ").concat(e("contextmenu.border.color"),";\n border-radius: ").concat(e("contextmenu.border.radius"),";\n box-shadow: ").concat(e("contextmenu.shadow"),";\n}\n\n.p-contextmenu-item {\n position: relative;\n}\n\n.p-contextmenu-item-content {\n transition: background ").concat(e("contextmenu.transition.duration"),", color ").concat(e("contextmenu.transition.duration"),";\n border-radius: ").concat(e("contextmenu.item.border.radius"),";\n color: ").concat(e("contextmenu.item.color"),";\n}\n\n.p-contextmenu-item-link {\n cursor: pointer;\n display: flex;\n align-items: center;\n text-decoration: none;\n overflow: hidden;\n position: relative;\n color: inherit;\n padding: ").concat(e("contextmenu.item.padding"),";\n gap: ").concat(e("contextmenu.item.gap"),";\n user-select: none;\n}\n\n.p-contextmenu-item-label {\n line-height: 1;\n}\n\n.p-contextmenu-item-icon {\n color: ").concat(e("contextmenu.item.icon.color"),";\n}\n\n.p-contextmenu-submenu-icon {\n color: ").concat(e("contextmenu.submenu.icon.color"),";\n margin-left: auto;\n font-size: ").concat(e("contextmenu.submenu.icon.size"),";\n width: ").concat(e("contextmenu.submenu.icon.size"),";\n height: ").concat(e("contextmenu.submenu.icon.size"),";\n}\n\n.p-contextmenu-submenu-icon:dir(rtl) {\n margin-left: 0;\n margin-right: auto;\n}\n\n.p-contextmenu-item.p-focus > .p-contextmenu-item-content {\n color: ").concat(e("contextmenu.item.focus.color"),";\n background: ").concat(e("contextmenu.item.focus.background"),";\n}\n\n.p-contextmenu-item.p-focus > .p-contextmenu-item-content .p-contextmenu-item-icon {\n color: ").concat(e("contextmenu.item.icon.focus.color"),";\n}\n\n.p-contextmenu-item.p-focus > .p-contextmenu-item-content .p-contextmenu-submenu-icon {\n color: ").concat(e("contextmenu.submenu.icon.focus.color"),";\n}\n\n.p-contextmenu-item:not(.p-disabled) > .p-contextmenu-item-content:hover {\n color: ").concat(e("contextmenu.item.focus.color"),";\n background: ").concat(e("contextmenu.item.focus.background"),";\n}\n\n.p-contextmenu-item:not(.p-disabled) > .p-contextmenu-item-content:hover .p-contextmenu-item-icon {\n color: ").concat(e("contextmenu.item.icon.focus.color"),";\n}\n\n.p-contextmenu-item:not(.p-disabled) > .p-contextmenu-item-content:hover .p-contextmenu-submenu-icon {\n color: ").concat(e("contextmenu.submenu.icon.focus.color"),";\n}\n\n.p-contextmenu-item-active > .p-contextmenu-item-content {\n color: ").concat(e("contextmenu.item.active.color"),";\n background: ").concat(e("contextmenu.item.active.background"),";\n}\n\n.p-contextmenu-item-active > .p-contextmenu-item-content .p-contextmenu-item-icon {\n color: ").concat(e("contextmenu.item.icon.active.color"),";\n}\n\n.p-contextmenu-item-active > .p-contextmenu-item-content .p-contextmenu-submenu-icon {\n color: ").concat(e("contextmenu.submenu.icon.active.color"),";\n}\n\n.p-contextmenu-separator {\n border-block-start: 1px solid ").concat(e("contextmenu.separator.border.color"),";\n}\n\n.p-contextmenu-enter-from,\n.p-contextmenu-leave-active {\n opacity: 0;\n}\n\n.p-contextmenu-enter-active {\n transition: opacity 250ms;\n}\n\n.p-contextmenu-mobile .p-contextmenu-submenu {\n position: static;\n box-shadow: none;\n border: 0 none;\n padding-inline-start: ").concat(e("tieredmenu.submenu.mobile.indent"),";\n padding-inline-end: 0;\n}\n\n.p-contextmenu-mobile .p-contextmenu-submenu-icon {\n transition: transform 0.2s;\n transform: rotate(90deg);\n}\n\n.p-contextmenu-mobile .p-contextmenu-item-active > .p-contextmenu-item-content .p-contextmenu-submenu-icon {\n transform: rotate(-90deg);\n}\n")},classes:{root:function(t){return["p-contextmenu p-component",{"p-contextmenu-mobile":t.instance.queryMatches}]},rootList:"p-contextmenu-root-list",item:function(t){var e=t.instance,n=t.processedItem;return["p-contextmenu-item",{"p-contextmenu-item-active":e.isItemActive(n),"p-focus":e.isItemFocused(n),"p-disabled":e.isItemDisabled(n)}]},itemContent:"p-contextmenu-item-content",itemLink:"p-contextmenu-item-link",itemIcon:"p-contextmenu-item-icon",itemLabel:"p-contextmenu-item-label",submenuIcon:"p-contextmenu-submenu-icon",submenu:"p-contextmenu-submenu",separator:"p-contextmenu-separator"}}),ro={name:"BaseContextMenu",extends:Lt,props:{model:{type:Array,default:null},appendTo:{type:[String,Object],default:"body"},autoZIndex:{type:Boolean,default:!0},baseZIndex:{type:Number,default:0},global:{type:Boolean,default:!1},breakpoint:{type:String,default:"960px"},tabindex:{type:Number,default:0},ariaLabelledby:{type:String,default:null},ariaLabel:{type:String,default:null}},style:io,provide:function(){return{$pcContextMenu:this,$parentInstance:this}}},so={name:"ContextMenuSub",hostName:"ContextMenu",extends:Lt,emits:["item-click","item-mouseenter","item-mousemove"],props:{items:{type:Array,default:null},menuId:{type:String,default:null},focusedItemId:{type:String,default:null},root:{type:Boolean,default:!1},visible:{type:Boolean,default:!1},level:{type:Number,default:0},templates:{type:Object,default:null},activeItemPath:{type:Object,default:null},tabindex:{type:Number,default:0}},methods:{getItemId:function(t){return"".concat(this.menuId,"_").concat(t.key)},getItemKey:function(t){return this.getItemId(t)},getItemProp:function(t,e,n){return t&&t.item?It(t.item[e],n):void 0},getItemLabel:function(t){return this.getItemProp(t,"label")},getItemLabelId:function(t){return"".concat(this.menuId,"_").concat(t.key,"_label")},getPTOptions:function(t,e,n){return this.ptm(t,{context:{item:e.item,active:this.isItemActive(e),focused:this.isItemFocused(e),disabled:this.isItemDisabled(e),index:n}})},isItemActive:function(t){return this.activeItemPath.some((function(e){return e.key===t.key}))},isItemVisible:function(t){return!1!==this.getItemProp(t,"visible")},isItemDisabled:function(t){return this.getItemProp(t,"disabled")},isItemFocused:function(t){return this.focusedItemId===this.getItemId(t)},isItemGroup:function(t){return K(t.items)},onItemClick:function(t,e){this.getItemProp(e,"command",{originalEvent:t,item:e.item}),this.$emit("item-click",{originalEvent:t,processedItem:e,isFocus:!0})},onItemMouseEnter:function(t,e){this.$emit("item-mouseenter",{originalEvent:t,processedItem:e})},onItemMouseMove:function(t,e){this.$emit("item-mousemove",{originalEvent:t,processedItem:e,isFocus:!0})},getAriaSetSize:function(){var t=this;return this.items.filter((function(e){return t.isItemVisible(e)&&!t.getItemProp(e,"separator")})).length},getAriaPosInset:function(t){var e=this;return t-this.items.slice(0,t).filter((function(t){return e.isItemVisible(t)&&e.getItemProp(t,"separator")})).length+1},onEnter:function(){Ot(this.$refs.container,this.level)},getMenuItemProps:function(t,e){return{action:I({class:this.cx("itemLink"),tabindex:-1},this.getPTOptions("itemLink",t,e)),icon:I({class:[this.cx("itemIcon"),this.getItemProp(t,"icon")]},this.getPTOptions("itemIcon",t,e)),label:I({class:this.cx("itemLabel")},this.getPTOptions("itemLabel",t,e)),submenuicon:I({class:this.cx("submenuIcon")},this.getPTOptions("submenuicon",t,e))}}},components:{AngleRightIcon:Ht},directives:{ripple:De}},lo=["tabindex"],ao=["id","aria-label","aria-disabled","aria-expanded","aria-haspopup","aria-level","aria-setsize","aria-posinset","data-p-active","data-p-focused","data-p-disabled"],co=["onClick","onMouseenter","onMousemove"],uo=["href","target"],po=["id"],ho=["id"];so.render=function(t,e,n,o,i,r){var s=k("AngleRightIcon"),l=k("ContextMenuSub",!0),a=w("ripple");return m(),S(P,I({name:"p-contextmenusub",onEnter:r.onEnter},t.ptm("menu.transition")),{default:L((function(){return[n.root||n.visible?(m(),g("ul",I({key:0,ref:"container",tabindex:n.tabindex},t.ptm("rootList")),[(m(!0),g(_,null,M(n.items,(function(o,i){return m(),g(_,{key:r.getItemKey(o)},[r.isItemVisible(o)&&!r.getItemProp(o,"separator")?(m(),g("li",I({key:0,id:r.getItemId(o),style:r.getItemProp(o,"style"),class:[t.cx("item",{processedItem:o}),r.getItemProp(o,"class")],role:"menuitem","aria-label":r.getItemLabel(o),"aria-disabled":r.isItemDisabled(o)||void 0,"aria-expanded":r.isItemGroup(o)?r.isItemActive(o):void 0,"aria-haspopup":r.isItemGroup(o)&&!r.getItemProp(o,"to")?"menu":void 0,"aria-level":n.level+1,"aria-setsize":r.getAriaSetSize(),"aria-posinset":r.getAriaPosInset(i),ref_for:!0},r.getPTOptions("item",o,i),{"data-p-active":r.isItemActive(o),"data-p-focused":r.isItemFocused(o),"data-p-disabled":r.isItemDisabled(o)}),[C("div",I({class:t.cx("itemContent"),onClick:function(t){return r.onItemClick(t,o)},onMouseenter:function(t){return r.onItemMouseEnter(t,o)},onMousemove:function(t){return r.onItemMouseMove(t,o)},ref_for:!0},r.getPTOptions("itemContent",o,i)),[n.templates.item?(m(),S(E(n.templates.item),{key:1,item:o.item,hasSubmenu:r.getItemProp(o,"items"),label:r.getItemLabel(o),props:r.getMenuItemProps(o,i)},null,8,["item","hasSubmenu","label","props"])):O((m(),g("a",I({key:0,href:r.getItemProp(o,"url"),class:t.cx("itemLink"),target:r.getItemProp(o,"target"),tabindex:"-1",ref_for:!0},r.getPTOptions("itemLink",o,i)),[n.templates.itemicon?(m(),S(E(n.templates.itemicon),{key:0,item:o.item,class:z(t.cx("itemIcon"))},null,8,["item","class"])):r.getItemProp(o,"icon")?(m(),g("span",I({key:1,class:[t.cx("itemIcon"),r.getItemProp(o,"icon")],ref_for:!0},r.getPTOptions("itemIcon",o,i)),null,16)):$("",!0),C("span",I({id:r.getItemLabelId(o),class:t.cx("itemLabel"),ref_for:!0},r.getPTOptions("itemLabel",o,i)),x(r.getItemLabel(o)),17,po),r.getItemProp(o,"items")?(m(),g(_,{key:2},[n.templates.submenuicon?(m(),S(E(n.templates.submenuicon),{key:0,active:r.isItemActive(o),class:z(t.cx("submenuIcon"))},null,8,["active","class"])):(m(),S(s,I({key:1,class:t.cx("submenuIcon"),ref_for:!0},r.getPTOptions("submenuicon",o,i)),null,16,["class"]))],64)):$("",!0)],16,uo)),[[a]])],16,co),r.isItemVisible(o)&&r.isItemGroup(o)?(m(),S(l,I({key:0,id:r.getItemId(o)+"_list",role:"menu",class:t.cx("submenu"),menuId:n.menuId,focusedItemId:n.focusedItemId,items:o.items,templates:n.templates,activeItemPath:n.activeItemPath,level:n.level+1,visible:r.isItemActive(o)&&r.isItemGroup(o),pt:t.pt,unstyled:t.unstyled,onItemClick:e[0]||(e[0]=function(e){return t.$emit("item-click",e)}),onItemMouseenter:e[1]||(e[1]=function(e){return t.$emit("item-mouseenter",e)}),onItemMousemove:e[2]||(e[2]=function(e){return t.$emit("item-mousemove",e)}),"aria-labelledby":r.getItemLabelId(o),ref_for:!0},t.ptm("submenu")),null,16,["id","class","menuId","focusedItemId","items","templates","activeItemPath","level","visible","pt","unstyled","aria-labelledby"])):$("",!0)],16,ao)):$("",!0),r.isItemVisible(o)&&r.getItemProp(o,"separator")?(m(),g("li",I({key:1,id:r.getItemId(o),style:r.getItemProp(o,"style"),class:[t.cx("separator"),r.getItemProp(o,"class")],role:"separator",ref_for:!0},t.ptm("separator")),null,16,ho)):$("",!0)],64)})),128))],16,lo)):$("",!0)]})),_:1},16,["onEnter"])};var fo={name:"ContextMenu",extends:ro,inheritAttrs:!1,emits:["focus","blur","show","hide","before-show","before-hide"],target:null,outsideClickListener:null,resizeListener:null,documentContextMenuListener:null,matchMediaListener:null,pageX:null,pageY:null,container:null,list:null,data:function(){return{id:this.$attrs.id,focused:!1,focusedItemInfo:{index:-1,level:0,parentKey:""},activeItemPath:[],visible:!1,submenuVisible:!1,query:null,queryMatches:!1}},watch:{"$attrs.id":function(t){this.id=t||Et()},activeItemPath:function(t){K(t)?(this.bindOutsideClickListener(),this.bindResizeListener()):this.visible||(this.unbindOutsideClickListener(),this.unbindResizeListener())}},mounted:function(){this.id=this.id||Et(),this.bindMatchMediaListener(),this.global&&this.bindDocumentContextMenuListener()},beforeUnmount:function(){this.unbindResizeListener(),this.unbindOutsideClickListener(),this.unbindDocumentContextMenuListener(),this.unbindMatchMediaListener(),this.container&&this.autoZIndex&&Q.clear(this.container),this.target=null,this.container=null},methods:{getItemProp:function(t,e){return t?It(t[e]):void 0},getItemLabel:function(t){return this.getItemProp(t,"label")},isItemDisabled:function(t){return this.getItemProp(t,"disabled")},isItemVisible:function(t){return!1!==this.getItemProp(t,"visible")},isItemGroup:function(t){return K(this.getItemProp(t,"items"))},isItemSeparator:function(t){return this.getItemProp(t,"separator")},getProccessedItemLabel:function(t){return t?this.getItemLabel(t.item):void 0},isProccessedItemGroup:function(t){return t&&K(t.items)},toggle:function(t){this.visible?this.hide():this.show(t)},show:function(t){this.$emit("before-show"),this.activeItemPath=[],this.focusedItemInfo={index:-1,level:0,parentKey:""},et(this.list),this.pageX=t.pageX,this.pageY=t.pageY,this.visible?this.position():this.visible=!0,t.stopPropagation(),t.preventDefault()},hide:function(){this.$emit("before-hide"),this.visible=!1,this.activeItemPath=[],this.focusedItemInfo={index:-1,level:0,parentKey:""}},onFocus:function(t){this.focused=!0,this.focusedItemInfo=-1!==this.focusedItemInfo.index?this.focusedItemInfo:{index:-1,level:0,parentKey:""},this.$emit("focus",t)},onBlur:function(t){this.focused=!1,this.focusedItemInfo={index:-1,level:0,parentKey:""},this.searchValue="",this.$emit("blur",t)},onKeyDown:function(t){var e=t.metaKey||t.ctrlKey;switch(t.code){case"ArrowDown":this.onArrowDownKey(t);break;case"ArrowUp":this.onArrowUpKey(t);break;case"ArrowLeft":this.onArrowLeftKey(t);break;case"ArrowRight":this.onArrowRightKey(t);break;case"Home":this.onHomeKey(t);break;case"End":this.onEndKey(t);break;case"Space":this.onSpaceKey(t);break;case"Enter":case"NumpadEnter":this.onEnterKey(t);break;case"Escape":this.onEscapeKey(t);break;case"Tab":this.onTabKey(t);break;case"PageDown":case"PageUp":case"Backspace":case"ShiftLeft":case"ShiftRight":break;default:!e&&ot(t.key)&&this.searchItems(t,t.key)}},onItemChange:function(t,e){var n=t.processedItem,o=t.isFocus;if(!D(n)){var i=n.index,r=n.key,s=n.level,l=n.parentKey,a=n.items,c=K(a),u=this.activeItemPath.filter((function(t){return t.parentKey!==l&&t.parentKey!==r}));c&&(u.push(n),this.submenuVisible=!0),this.focusedItemInfo={index:i,level:s,parentKey:l},o&&et(this.list),"hover"===e&&this.queryMatches||(this.activeItemPath=u)}},onItemClick:function(t){var e=t.processedItem,n=this.isProccessedItemGroup(e);if(this.isSelected(e)){var o=e.index,i=e.key,r=e.level,s=e.parentKey;this.activeItemPath=this.activeItemPath.filter((function(t){return i!==t.key&&i.startsWith(t.key)})),this.focusedItemInfo={index:o,level:r,parentKey:s},et(this.list)}else n?this.onItemChange(t):this.hide()},onItemMouseEnter:function(t){this.onItemChange(t,"hover")},onItemMouseMove:function(t){this.focused&&this.changeFocusedItemIndex(t,t.processedItem.index)},onArrowDownKey:function(t){var e=-1!==this.focusedItemInfo.index?this.findNextItemIndex(this.focusedItemInfo.index):this.findFirstFocusedItemIndex();this.changeFocusedItemIndex(t,e),t.preventDefault()},onArrowUpKey:function(t){if(t.altKey){if(-1!==this.focusedItemInfo.index){var e=this.visibleItems[this.focusedItemInfo.index];!this.isProccessedItemGroup(e)&&this.onItemChange({originalEvent:t,processedItem:e})}this.popup&&this.hide(),t.preventDefault()}else{var n=-1!==this.focusedItemInfo.index?this.findPrevItemIndex(this.focusedItemInfo.index):this.findLastFocusedItemIndex();this.changeFocusedItemIndex(t,n),t.preventDefault()}},onArrowLeftKey:function(t){var e=this,n=this.visibleItems[this.focusedItemInfo.index],o=this.activeItemPath.find((function(t){return t.key===n.parentKey}));D(n.parent)||(this.focusedItemInfo={index:-1,parentKey:o?o.parentKey:""},this.searchValue="",this.onArrowDownKey(t)),this.activeItemPath=this.activeItemPath.filter((function(t){return t.parentKey!==e.focusedItemInfo.parentKey})),t.preventDefault()},onArrowRightKey:function(t){var e=this.visibleItems[this.focusedItemInfo.index];this.isProccessedItemGroup(e)&&(this.onItemChange({originalEvent:t,processedItem:e}),this.focusedItemInfo={index:-1,parentKey:e.key},this.searchValue="",this.onArrowDownKey(t)),t.preventDefault()},onHomeKey:function(t){this.changeFocusedItemIndex(t,this.findFirstItemIndex()),t.preventDefault()},onEndKey:function(t){this.changeFocusedItemIndex(t,this.findLastItemIndex()),t.preventDefault()},onEnterKey:function(t){if(-1!==this.focusedItemInfo.index){var e=J(this.list,'li[id="'.concat("".concat(this.focusedItemIdx),'"]')),n=e&&J(e,'[data-pc-section="itemlink"]');n?n.click():e&&e.click();var o=this.visibleItems[this.focusedItemInfo.index];!this.isProccessedItemGroup(o)&&(this.focusedItemInfo.index=this.findFirstFocusedItemIndex())}t.preventDefault()},onSpaceKey:function(t){this.onEnterKey(t)},onEscapeKey:function(t){this.hide(),!this.popup&&(this.focusedItemInfo.index=this.findFirstFocusedItemIndex()),t.preventDefault()},onTabKey:function(t){if(-1!==this.focusedItemInfo.index){var e=this.visibleItems[this.focusedItemInfo.index];!this.isProccessedItemGroup(e)&&this.onItemChange({originalEvent:t,processedItem:e})}this.hide()},onEnter:function(t){st(t,{position:"absolute"}),this.position(),this.autoZIndex&&Q.set("menu",t,this.baseZIndex+this.$primevue.config.zIndex.menu)},onAfterEnter:function(){this.bindOutsideClickListener(),this.bindResizeListener(),this.$emit("show"),et(this.list)},onLeave:function(){this.$emit("hide"),this.container=null},onAfterLeave:function(t){this.autoZIndex&&Q.clear(t),this.unbindOutsideClickListener(),this.unbindResizeListener()},position:function(){var t=this.pageX+1,e=this.pageY+1,n=this.container.offsetParent?this.container.offsetWidth:kt(this.container),o=this.container.offsetParent?this.container.offsetHeight:wt(this.container),i=gt(),r=window.scrollY||document.documentElement.scrollTop||document.body.scrollTop||0,s=window.scrollX||document.documentElement.scrollLeft||document.body.scrollLeft||0;t+n-s>i.width&&(t-=n),e+o-r>i.height&&(e-=o),t-1?n+t+1:t},findPrevItemIndex:function(t){var e=this,n=t>0?pt(this.visibleItems.slice(0,t),(function(t){return e.isValidItem(t)})):-1;return n>-1?n:t},findSelectedItemIndex:function(){var t=this;return this.visibleItems.findIndex((function(e){return t.isValidSelectedItem(e)}))},findFirstFocusedItemIndex:function(){var t=this.findSelectedItemIndex();return t<0?this.findFirstItemIndex():t},findLastFocusedItemIndex:function(){var t=this.findSelectedItemIndex();return t<0?this.findLastItemIndex():t},searchItems:function(t,e){var n=this;this.searchValue=(this.searchValue||"")+e;var o=-1,i=!1;return-1!==(o=-1!==this.focusedItemInfo.index?-1===(o=this.visibleItems.slice(this.focusedItemInfo.index).findIndex((function(t){return n.isItemMatched(t)})))?this.visibleItems.slice(0,this.focusedItemInfo.index).findIndex((function(t){return n.isItemMatched(t)})):o+this.focusedItemInfo.index:this.visibleItems.findIndex((function(t){return n.isItemMatched(t)})))&&(i=!0),-1===o&&-1===this.focusedItemInfo.index&&(o=this.findFirstFocusedItemIndex()),-1!==o&&this.changeFocusedItemIndex(t,o),this.searchTimeout&&clearTimeout(this.searchTimeout),this.searchTimeout=setTimeout((function(){n.searchValue="",n.searchTimeout=null}),500),i},changeFocusedItemIndex:function(t,e){this.focusedItemInfo.index!==e&&(this.focusedItemInfo.index=e,this.scrollInView())},scrollInView:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:-1,e=-1!==t?"".concat(this.id,"_").concat(t):this.focusedItemIdx,n=J(this.list,'li[id="'.concat(e,'"]'));n&&n.scrollIntoView&&n.scrollIntoView({block:"nearest",inline:"start"})},createProcessedItems:function(t){var e=this,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0,o=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},i=arguments.length>3&&void 0!==arguments[3]?arguments[3]:"",r=[];return t&&t.forEach((function(t,s){var l=(""!==i?i+"_":"")+s,a={item:t,index:s,level:n,key:l,parent:o,parentKey:i};a.items=e.createProcessedItems(t.items,n+1,a,l),r.push(a)})),r},containerRef:function(t){this.container=t},listRef:function(t){this.list=t?t.$el:void 0}},computed:{processedItems:function(){return this.createProcessedItems(this.model||[])},visibleItems:function(){var t=this,e=this.activeItemPath.find((function(e){return e.key===t.focusedItemInfo.parentKey}));return e?e.items:this.processedItems},focusedItemIdx:function(){return-1!==this.focusedItemInfo.index?"".concat(this.id).concat(K(this.focusedItemInfo.parentKey)?"_"+this.focusedItemInfo.parentKey:"","_").concat(this.focusedItemInfo.index):null}},components:{ContextMenuSub:so,Portal:Ye}};fo.render=function(t,e,n,o,i,r){var s=k("ContextMenuSub"),l=k("Portal");return m(),S(l,{appendTo:t.appendTo},{default:L((function(){return[V(P,I({name:"p-contextmenu",onEnter:r.onEnter,onAfterEnter:r.onAfterEnter,onLeave:r.onLeave,onAfterLeave:r.onAfterLeave},t.ptm("transition")),{default:L((function(){return[i.visible?(m(),g("div",I({key:0,ref:r.containerRef,class:t.cx("root")},t.ptmi("root")),[V(s,{ref:r.listRef,id:i.id+"_list",class:z(t.cx("rootList")),role:"menubar",root:!0,tabindex:t.tabindex,"aria-orientation":"vertical","aria-activedescendant":i.focused?r.focusedItemIdx:void 0,menuId:i.id,focusedItemId:i.focused?r.focusedItemIdx:void 0,items:r.processedItems,templates:t.$slots,activeItemPath:i.activeItemPath,"aria-labelledby":t.ariaLabelledby,"aria-label":t.ariaLabel,level:0,visible:i.submenuVisible,pt:t.pt,unstyled:t.unstyled,onFocus:r.onFocus,onBlur:r.onBlur,onKeydown:r.onKeyDown,onItemClick:r.onItemClick,onItemMouseenter:r.onItemMouseEnter,onItemMousemove:r.onItemMouseMove},null,8,["id","class","tabindex","aria-activedescendant","menuId","focusedItemId","items","templates","activeItemPath","aria-labelledby","aria-label","visible","pt","unstyled","onFocus","onBlur","onKeydown","onItemClick","onItemMouseenter","onItemMousemove"])],16)):$("",!0)]})),_:1},16,["onEnter","onAfterEnter","onLeave","onAfterLeave"])]})),_:1},8,["appendTo"])};var bo=St.extend({name:"togglebutton",theme:function(t){var e=t.dt;return"\n.p-togglebutton {\n display: inline-flex;\n cursor: pointer;\n user-select: none;\n align-items: center;\n justify-content: center;\n overflow: hidden;\n position: relative;\n color: ".concat(e("togglebutton.color"),";\n background: ").concat(e("togglebutton.background"),";\n border: 1px solid ").concat(e("togglebutton.border.color"),";\n padding: ").concat(e("togglebutton.padding"),";\n font-size: 1rem;\n font-family: inherit;\n font-feature-settings: inherit;\n transition: background ").concat(e("togglebutton.transition.duration"),", color ").concat(e("togglebutton.transition.duration"),", border-color ").concat(e("togglebutton.transition.duration"),",\n outline-color ").concat(e("togglebutton.transition.duration"),", box-shadow ").concat(e("togglebutton.transition.duration"),";\n border-radius: ").concat(e("togglebutton.border.radius"),";\n outline-color: transparent;\n font-weight: ").concat(e("togglebutton.font.weight"),";\n}\n\n.p-togglebutton-content {\n position: relative;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: ").concat(e("togglebutton.gap"),';\n}\n\n.p-togglebutton-label,\n.p-togglebutton-icon {\n position: relative;\n transition: none;\n}\n\n.p-togglebutton::before {\n content: "";\n background: transparent;\n transition: background ').concat(e("togglebutton.transition.duration"),", color ").concat(e("togglebutton.transition.duration"),", border-color ").concat(e("togglebutton.transition.duration"),",\n outline-color ").concat(e("togglebutton.transition.duration"),", box-shadow ").concat(e("togglebutton.transition.duration"),";\n position: absolute;\n inset-inline-start: ").concat(e("togglebutton.content.left"),";\n inset-block-start: ").concat(e("togglebutton.content.top"),";\n width: calc(100% - calc(2 * ").concat(e("togglebutton.content.left"),"));\n height: calc(100% - calc(2 * ").concat(e("togglebutton.content.top"),"));\n border-radius: ").concat(e("togglebutton.border.radius"),";\n}\n\n.p-togglebutton.p-togglebutton-checked::before {\n background: ").concat(e("togglebutton.content.checked.background"),";\n box-shadow: ").concat(e("togglebutton.content.checked.shadow"),";\n}\n\n.p-togglebutton:not(:disabled):not(.p-togglebutton-checked):hover {\n background: ").concat(e("togglebutton.hover.background"),";\n color: ").concat(e("togglebutton.hover.color"),";\n}\n\n.p-togglebutton.p-togglebutton-checked {\n background: ").concat(e("togglebutton.checked.background"),";\n border-color: ").concat(e("togglebutton.checked.border.color"),";\n color: ").concat(e("togglebutton.checked.color"),";\n}\n\n.p-togglebutton:focus-visible {\n box-shadow: ").concat(e("togglebutton.focus.ring.shadow"),";\n outline: ").concat(e("togglebutton.focus.ring.width")," ").concat(e("togglebutton.focus.ring.style")," ").concat(e("togglebutton.focus.ring.color"),";\n outline-offset: ").concat(e("togglebutton.focus.ring.offset"),";\n}\n\n.p-togglebutton.p-invalid {\n border-color: ").concat(e("togglebutton.invalid.border.color"),";\n}\n\n.p-togglebutton:disabled {\n opacity: 1;\n cursor: default;\n background: ").concat(e("togglebutton.disabled.background"),";\n border-color: ").concat(e("togglebutton.disabled.border.color"),";\n color: ").concat(e("togglebutton.disabled.color"),";\n}\n\n.p-togglebutton-icon {\n color: ").concat(e("togglebutton.icon.color"),";\n}\n\n.p-togglebutton:not(:disabled):not(.p-togglebutton-checked):hover .p-togglebutton-icon {\n color: ").concat(e("togglebutton.icon.hover.color"),";\n}\n\n.p-togglebutton.p-togglebutton-checked .p-togglebutton-icon {\n color: ").concat(e("togglebutton.icon.checked.color"),";\n}\n\n.p-togglebutton:disabled .p-togglebutton-icon {\n color: ").concat(e("togglebutton.icon.disabled.color"),";\n}\n\n.p-togglebutton-sm {\n padding: ").concat(e("togglebutton.sm.padding"),";\n font-size: ").concat(e("togglebutton.sm.font.size"),";\n}\n\n.p-togglebutton-lg {\n padding: ").concat(e("togglebutton.lg.padding"),";\n font-size: ").concat(e("togglebutton.lg.font.size"),";\n}\n")},classes:{root:function(t){var e=t.instance,n=t.props;return["p-togglebutton p-component",{"p-togglebutton-checked":e.active,"p-invalid":e.$invalid,"p-togglebutton-sm p-inputfield-sm":"small"===n.size,"p-togglebutton-lg p-inputfield-lg":"large"===n.size}]},content:"p-togglebutton-content",icon:"p-togglebutton-icon",label:"p-togglebutton-label"}}),mo={name:"ToggleButton",extends:{name:"BaseToggleButton",extends:Bt,props:{onIcon:String,offIcon:String,onLabel:{type:String,default:"Yes"},offLabel:{type:String,default:"No"},iconPos:{type:String,default:"left"},readonly:{type:Boolean,default:!1},tabindex:{type:Number,default:null},ariaLabelledby:{type:String,default:null},ariaLabel:{type:String,default:null},size:{type:String,default:null}},style:bo,provide:function(){return{$pcToggleButton:this,$parentInstance:this}}},inheritAttrs:!1,emits:["change"],methods:{getPTOptions:function(t){return("root"===t?this.ptmi:this.ptm)(t,{context:{active:this.active,disabled:this.disabled}})},onChange:function(t){this.disabled||this.readonly||(this.writeValue(!this.d_value,t),this.$emit("change",t))},onBlur:function(t){var e,n;null===(e=(n=this.formField).onBlur)||void 0===e||e.call(n,t)}},computed:{active:function(){return!0===this.d_value},hasLabel:function(){return K(this.onLabel)&&K(this.offLabel)},label:function(){return this.hasLabel?this.d_value?this.onLabel:this.offLabel:" "}},directives:{ripple:De}},go=["tabindex","disabled","aria-pressed","aria-labelledby","data-p-checked","data-p-disabled"];mo.render=function(t,e,n,o,i,r){var s=w("ripple");return O((m(),g("button",I({type:"button",class:t.cx("root"),tabindex:t.tabindex,disabled:t.disabled,"aria-pressed":t.d_value,onClick:e[0]||(e[0]=function(){return r.onChange&&r.onChange.apply(r,arguments)}),onBlur:e[1]||(e[1]=function(){return r.onBlur&&r.onBlur.apply(r,arguments)})},r.getPTOptions("root"),{"aria-labelledby":t.ariaLabelledby,"data-p-checked":r.active,"data-p-disabled":t.disabled}),[C("span",I({class:t.cx("content")},r.getPTOptions("content")),[v(t.$slots,"default",{},(function(){return[v(t.$slots,"icon",{value:t.d_value,class:z(t.cx("icon"))},(function(){return[t.onIcon||t.offIcon?(m(),g("span",I({key:0,class:[t.cx("icon"),t.d_value?t.onIcon:t.offIcon]},r.getPTOptions("icon")),null,16)):$("",!0)]})),C("span",I({class:t.cx("label")},r.getPTOptions("label")),x(r.label),17)]}))],16)],16,go)),[[s]])};var vo=St.extend({name:"selectbutton",theme:function(t){var e=t.dt;return"\n.p-selectbutton {\n display: inline-flex;\n user-select: none;\n vertical-align: bottom;\n outline-color: transparent;\n border-radius: ".concat(e("selectbutton.border.radius"),";\n}\n\n.p-selectbutton .p-togglebutton {\n border-radius: 0;\n border-width: 1px 1px 1px 0;\n}\n\n.p-selectbutton .p-togglebutton:focus-visible {\n position: relative;\n z-index: 1;\n}\n\n.p-selectbutton .p-togglebutton:first-child {\n border-inline-start-width: 1px;\n border-start-start-radius: ").concat(e("selectbutton.border.radius"),";\n border-end-start-radius: ").concat(e("selectbutton.border.radius"),";\n}\n\n.p-selectbutton .p-togglebutton:last-child {\n border-start-end-radius: ").concat(e("selectbutton.border.radius"),";\n border-end-end-radius: ").concat(e("selectbutton.border.radius"),";\n}\n\n.p-selectbutton.p-invalid {\n outline: 1px solid ").concat(e("selectbutton.invalid.border.color"),";\n outline-offset: 0;\n}\n")},classes:{root:function(t){return["p-selectbutton p-component",{"p-invalid":t.instance.$invalid}]}}});function yo(t){return function(t){if(Array.isArray(t))return Io(t)}(t)||function(t){if("undefined"!=typeof Symbol&&null!=t[Symbol.iterator]||null!=t["@@iterator"])return Array.from(t)}(t)||xo(t)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function xo(t,e){if(t){if("string"==typeof t)return Io(t,e);var n={}.toString.call(t).slice(8,-1);return"Object"===n&&t.constructor&&(n=t.constructor.name),"Map"===n||"Set"===n?Array.from(t):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?Io(t,e):void 0}}function Io(t,e){(null==e||e>t.length)&&(e=t.length);for(var n=0,o=Array(e);n=t.length?{done:!0}:{done:!1,value:t[o++]}},e:function(t){throw t},f:i}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var r,s=!0,l=!1;return{s:function(){n=n.call(t)},n:function(){var t=n.next();return s=t.done,t},e:function(t){l=!0,r=t},f:function(){try{s||null==n.return||n.return()}finally{if(l)throw r}}}}(this.d_value);try{for(i.s();!(o=i.n()).done;){var r=o.value;if(dt(r,n,this.equalityKey)){e=!0;break}}}catch(s){i.e(s)}finally{i.f()}}}else e=dt(this.d_value,n,this.equalityKey);return e}},computed:{equalityKey:function(){return this.optionValue?null:this.dataKey}},directives:{ripple:De},components:{ToggleButton:mo}},wo=["aria-labelledby"];ko.render=function(t,e,n,o,i,r){var s=k("ToggleButton");return m(),g("div",I({class:t.cx("root"),role:"group","aria-labelledby":t.ariaLabelledby},t.ptmi("root")),[(m(!0),g(_,null,M(t.options,(function(e,n){return m(),S(s,{key:r.getOptionRenderKey(e),modelValue:r.isSelected(e),onLabel:r.getOptionLabel(e),offLabel:r.getOptionLabel(e),disabled:t.disabled||r.isOptionDisabled(e),unstyled:t.unstyled,size:t.size,readonly:!t.allowEmpty&&r.isSelected(e),onChange:function(t){return r.onOptionSelect(t,e,n)},pt:t.ptm("pcToggleButton")},A({_:2},[t.$slots.option?{name:"default",fn:L((function(){return[v(t.$slots,"option",{option:e,index:n},(function(){return[C("span",I({ref_for:!0},t.ptm("pcToggleButton").label),x(r.getOptionLabel(e)),17)]}))]})),key:"0"}:void 0]),1032,["modelValue","onLabel","offLabel","disabled","unstyled","size","readonly","onChange","pt"])})),128))],16,wo)};export{On as T,pn as a,jn as b,Qn as c,re as d,se as e,Ne as f,We as g,Ce as h,Ue as i,ko as j,oo as k,fo as l,qt as m,Ge as s}; diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v2/assets/vue-DfGw76ex.js b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v2/assets/vue-DfGw76ex.js new file mode 100644 index 0000000000000000000000000000000000000000..0ad8572b0684e176487d679f6ee13ca1785af1e3 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v2/assets/vue-DfGw76ex.js @@ -0,0 +1,23 @@ +/** +* @vue/shared v3.5.17 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/ +/*! #__NO_SIDE_EFFECTS__ */ +function e(e){const t=Object.create(null);for(const n of e.split(","))t[n]=1;return e=>e in t}const t={},n=[],s=()=>{},r=()=>!1,o=e=>111===e.charCodeAt(0)&&110===e.charCodeAt(1)&&(e.charCodeAt(2)>122||e.charCodeAt(2)<97),i=e=>e.startsWith("onUpdate:"),l=Object.assign,c=(e,t)=>{const n=e.indexOf(t);n>-1&&e.splice(n,1)},a=Object.prototype.hasOwnProperty,u=(e,t)=>a.call(e,t),f=Array.isArray,p=e=>"[object Map]"===x(e),d=e=>"[object Set]"===x(e),h=e=>"[object Date]"===x(e),v=e=>"function"==typeof e,g=e=>"string"==typeof e,m=e=>"symbol"==typeof e,_=e=>null!==e&&"object"==typeof e,y=e=>(_(e)||v(e))&&v(e.then)&&v(e.catch),b=Object.prototype.toString,x=e=>b.call(e),S=e=>x(e).slice(8,-1),C=e=>"[object Object]"===x(e),w=e=>g(e)&&"NaN"!==e&&"-"!==e[0]&&""+parseInt(e,10)===e,k=e(",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted"),A=e=>{const t=Object.create(null);return n=>t[n]||(t[n]=e(n))},E=/-(\w)/g,T=A((e=>e.replace(E,((e,t)=>t?t.toUpperCase():"")))),O=/\B([A-Z])/g,F=A((e=>e.replace(O,"-$1").toLowerCase())),L=A((e=>e.charAt(0).toUpperCase()+e.slice(1))),M=A((e=>e?`on${L(e)}`:"")),j=(e,t)=>!Object.is(e,t),P=(e,...t)=>{for(let n=0;n{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,writable:s,value:n})},V=e=>{const t=parseFloat(e);return isNaN(t)?e:t},R=e=>{const t=g(e)?Number(e):NaN;return isNaN(t)?e:t};let $;const I=()=>$||($="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:"undefined"!=typeof window?window:"undefined"!=typeof global?global:{});function N(e){if(f(e)){const t={};for(let n=0;n{if(e){const n=e.split(B);n.length>1&&(t[n[0].trim()]=n[1].trim())}})),t}function K(e){let t="";if(g(e))t=e;else if(f(e))for(let n=0;nJ(e,t)))}const X=e=>!(!e||!0!==e.__v_isRef),Q=e=>g(e)?e:null==e?"":f(e)||_(e)&&(e.toString===b||!v(e.toString))?X(e)?Q(e.value):JSON.stringify(e,Y,2):String(e),Y=(e,t)=>X(t)?Y(e,t.value):p(t)?{[`Map(${t.size})`]:[...t.entries()].reduce(((e,[t,n],s)=>(e[ee(t,s)+" =>"]=n,e)),{})}:d(t)?{[`Set(${t.size})`]:[...t.values()].map((e=>ee(e)))}:m(t)?ee(t):!_(t)||f(t)||C(t)?t:String(t),ee=(e,t="")=>{var n;return m(e)?`Symbol(${null!=(n=e.description)?n:t})`:e}; +/** +* @vue/reactivity v3.5.17 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/ +let te,ne;class se{constructor(e=!1){this.detached=e,this._active=!0,this._on=0,this.effects=[],this.cleanups=[],this._isPaused=!1,this.parent=te,!e&&te&&(this.index=(te.scopes||(te.scopes=[])).push(this)-1)}get active(){return this._active}pause(){if(this._active){let e,t;if(this._isPaused=!0,this.scopes)for(e=0,t=this.scopes.length;e0&&0==--this._on&&(te=this.prevScope,this.prevScope=void 0)}stop(e){if(this._active){let t,n;for(this._active=!1,t=0,n=this.effects.length;t0)return;if(ue){let e=ue;for(ue=void 0;e;){const t=e.next;e.next=void 0,e.flags&=-9,e=t}}let e;for(;ae;){let n=ae;for(ae=void 0;n;){const s=n.next;if(n.next=void 0,n.flags&=-9,1&n.flags)try{n.trigger()}catch(t){e||(e=t)}n=s}}if(e)throw e}function ve(e){for(let t=e.deps;t;t=t.nextDep)t.version=-1,t.prevActiveLink=t.dep.activeLink,t.dep.activeLink=t}function ge(e){let t,n=e.depsTail,s=n;for(;s;){const e=s.prevDep;-1===s.version?(s===n&&(n=e),ye(s),be(s)):t=s,s.dep.activeLink=s.prevActiveLink,s.prevActiveLink=void 0,s=e}e.deps=t,e.depsTail=n}function me(e){for(let t=e.deps;t;t=t.nextDep)if(t.dep.version!==t.version||t.dep.computed&&(_e(t.dep.computed)||t.dep.version!==t.version))return!0;return!!e._dirty}function _e(e){if(4&e.flags&&!(16&e.flags))return;if(e.flags&=-17,e.globalVersion===Ae)return;if(e.globalVersion=Ae,!e.isSSR&&128&e.flags&&(!e.deps&&!e._dirty||!me(e)))return;e.flags|=2;const t=e.dep,n=ne,s=xe;ne=e,xe=!0;try{ve(e);const n=e.fn(e._value);(0===t.version||j(n,e._value))&&(e.flags|=128,e._value=n,t.version++)}catch(r){throw t.version++,r}finally{ne=n,xe=s,ge(e),e.flags&=-3}}function ye(e,t=!1){const{dep:n,prevSub:s,nextSub:r}=e;if(s&&(s.nextSub=r,e.prevSub=void 0),r&&(r.prevSub=s,e.nextSub=void 0),n.subs===e&&(n.subs=s,!s&&n.computed)){n.computed.flags&=-5;for(let e=n.computed.deps;e;e=e.nextDep)ye(e,!0)}t||--n.sc||!n.map||n.map.delete(n.key)}function be(e){const{prevDep:t,nextDep:n}=e;t&&(t.nextDep=n,e.prevDep=void 0),n&&(n.prevDep=t,e.nextDep=void 0)}let xe=!0;const Se=[];function Ce(){Se.push(xe),xe=!1}function we(){const e=Se.pop();xe=void 0===e||e}function ke(e){const{cleanup:t}=e;if(e.cleanup=void 0,t){const e=ne;ne=void 0;try{t()}finally{ne=e}}}let Ae=0;class Ee{constructor(e,t){this.sub=e,this.dep=t,this.version=t.version,this.nextDep=this.prevDep=this.nextSub=this.prevSub=this.prevActiveLink=void 0}}class Te{constructor(e){this.computed=e,this.version=0,this.activeLink=void 0,this.subs=void 0,this.map=void 0,this.key=void 0,this.sc=0,this.__v_skip=!0}track(e){if(!ne||!xe||ne===this.computed)return;let t=this.activeLink;if(void 0===t||t.sub!==ne)t=this.activeLink=new Ee(ne,this),ne.deps?(t.prevDep=ne.depsTail,ne.depsTail.nextDep=t,ne.depsTail=t):ne.deps=ne.depsTail=t,Oe(t);else if(-1===t.version&&(t.version=this.version,t.nextDep)){const e=t.nextDep;e.prevDep=t.prevDep,t.prevDep&&(t.prevDep.nextDep=e),t.prevDep=ne.depsTail,t.nextDep=void 0,ne.depsTail.nextDep=t,ne.depsTail=t,ne.deps===t&&(ne.deps=e)}return t}trigger(e){this.version++,Ae++,this.notify(e)}notify(e){de();try{0;for(let e=this.subs;e;e=e.prevSub)e.sub.notify()&&e.sub.dep.notify()}finally{he()}}}function Oe(e){if(e.dep.sc++,4&e.sub.flags){const t=e.dep.computed;if(t&&!e.dep.subs){t.flags|=20;for(let e=t.deps;e;e=e.nextDep)Oe(e)}const n=e.dep.subs;n!==e&&(e.prevSub=n,n&&(n.nextSub=e)),e.dep.subs=e}}const Fe=new WeakMap,Le=Symbol(""),Me=Symbol(""),je=Symbol("");function Pe(e,t,n){if(xe&&ne){let t=Fe.get(e);t||Fe.set(e,t=new Map);let s=t.get(n);s||(t.set(n,s=new Te),s.map=t,s.key=n),s.track()}}function De(e,t,n,s,r,o){const i=Fe.get(e);if(!i)return void Ae++;const l=e=>{e&&e.trigger()};if(de(),"clear"===t)i.forEach(l);else{const r=f(e),o=r&&w(n);if(r&&"length"===n){const e=Number(s);i.forEach(((t,n)=>{("length"===n||n===je||!m(n)&&n>=e)&&l(t)}))}else switch((void 0!==n||i.has(void 0))&&l(i.get(n)),o&&l(i.get(je)),t){case"add":r?o&&l(i.get("length")):(l(i.get(Le)),p(e)&&l(i.get(Me)));break;case"delete":r||(l(i.get(Le)),p(e)&&l(i.get(Me)));break;case"set":p(e)&&l(i.get(Le))}}he()}function Ve(e){const t=yt(e);return t===e?t:(Pe(t,0,je),mt(e)?t:t.map(xt))}function Re(e){return Pe(e=yt(e),0,je),e}const $e={__proto__:null,[Symbol.iterator](){return Ie(this,Symbol.iterator,xt)},concat(...e){return Ve(this).concat(...e.map((e=>f(e)?Ve(e):e)))},entries(){return Ie(this,"entries",(e=>(e[1]=xt(e[1]),e)))},every(e,t){return Ue(this,"every",e,t,void 0,arguments)},filter(e,t){return Ue(this,"filter",e,t,(e=>e.map(xt)),arguments)},find(e,t){return Ue(this,"find",e,t,xt,arguments)},findIndex(e,t){return Ue(this,"findIndex",e,t,void 0,arguments)},findLast(e,t){return Ue(this,"findLast",e,t,xt,arguments)},findLastIndex(e,t){return Ue(this,"findLastIndex",e,t,void 0,arguments)},forEach(e,t){return Ue(this,"forEach",e,t,void 0,arguments)},includes(...e){return We(this,"includes",e)},indexOf(...e){return We(this,"indexOf",e)},join(e){return Ve(this).join(e)},lastIndexOf(...e){return We(this,"lastIndexOf",e)},map(e,t){return Ue(this,"map",e,t,void 0,arguments)},pop(){return He(this,"pop")},push(...e){return He(this,"push",e)},reduce(e,...t){return Be(this,"reduce",e,t)},reduceRight(e,...t){return Be(this,"reduceRight",e,t)},shift(){return He(this,"shift")},some(e,t){return Ue(this,"some",e,t,void 0,arguments)},splice(...e){return He(this,"splice",e)},toReversed(){return Ve(this).toReversed()},toSorted(e){return Ve(this).toSorted(e)},toSpliced(...e){return Ve(this).toSpliced(...e)},unshift(...e){return He(this,"unshift",e)},values(){return Ie(this,"values",xt)}};function Ie(e,t,n){const s=Re(e),r=s[t]();return s===e||mt(e)||(r._next=r.next,r.next=()=>{const e=r._next();return e.value&&(e.value=n(e.value)),e}),r}const Ne=Array.prototype;function Ue(e,t,n,s,r,o){const i=Re(e),l=i!==e&&!mt(e),c=i[t];if(c!==Ne[t]){const t=c.apply(e,o);return l?xt(t):t}let a=n;i!==e&&(l?a=function(t,s){return n.call(this,xt(t),s,e)}:n.length>2&&(a=function(t,s){return n.call(this,t,s,e)}));const u=c.call(i,a,s);return l&&r?r(u):u}function Be(e,t,n,s){const r=Re(e);let o=n;return r!==e&&(mt(e)?n.length>3&&(o=function(t,s,r){return n.call(this,t,s,r,e)}):o=function(t,s,r){return n.call(this,t,xt(s),r,e)}),r[t](o,...s)}function We(e,t,n){const s=yt(e);Pe(s,0,je);const r=s[t](...n);return-1!==r&&!1!==r||!_t(n[0])?r:(n[0]=yt(n[0]),s[t](...n))}function He(e,t,n=[]){Ce(),de();const s=yt(e)[t].apply(e,n);return he(),we(),s}const Ke=e("__proto__,__v_isRef,__isVue"),ze=new Set(Object.getOwnPropertyNames(Symbol).filter((e=>"arguments"!==e&&"caller"!==e)).map((e=>Symbol[e])).filter(m));function qe(e){m(e)||(e=String(e));const t=yt(this);return Pe(t,0,e),t.hasOwnProperty(e)}class Ge{constructor(e=!1,t=!1){this._isReadonly=e,this._isShallow=t}get(e,t,n){if("__v_skip"===t)return e.__v_skip;const s=this._isReadonly,r=this._isShallow;if("__v_isReactive"===t)return!s;if("__v_isReadonly"===t)return s;if("__v_isShallow"===t)return r;if("__v_raw"===t)return n===(s?r?ft:ut:r?at:ct).get(e)||Object.getPrototypeOf(e)===Object.getPrototypeOf(n)?e:void 0;const o=f(e);if(!s){let e;if(o&&(e=$e[t]))return e;if("hasOwnProperty"===t)return qe}const i=Reflect.get(e,t,Ct(e)?e:n);return(m(t)?ze.has(t):Ke(t))?i:(s||Pe(e,0,t),r?i:Ct(i)?o&&w(t)?i:i.value:_(i)?s?dt(i):pt(i):i)}}class Je extends Ge{constructor(e=!1){super(!1,e)}set(e,t,n,s){let r=e[t];if(!this._isShallow){const t=gt(r);if(mt(n)||gt(n)||(r=yt(r),n=yt(n)),!f(e)&&Ct(r)&&!Ct(n))return!t&&(r.value=n,!0)}const o=f(e)&&w(t)?Number(t)e,tt=e=>Reflect.getPrototypeOf(e);function nt(e){return function(...t){return"delete"!==e&&("clear"===e?void 0:this)}}function st(e,t){const n={get(n){const s=this.__v_raw,r=yt(s),o=yt(n);e||(j(n,o)&&Pe(r,0,n),Pe(r,0,o));const{has:i}=tt(r),l=t?et:e?St:xt;return i.call(r,n)?l(s.get(n)):i.call(r,o)?l(s.get(o)):void(s!==r&&s.get(n))},get size(){const t=this.__v_raw;return!e&&Pe(yt(t),0,Le),Reflect.get(t,"size",t)},has(t){const n=this.__v_raw,s=yt(n),r=yt(t);return e||(j(t,r)&&Pe(s,0,t),Pe(s,0,r)),t===r?n.has(t):n.has(t)||n.has(r)},forEach(n,s){const r=this,o=r.__v_raw,i=yt(o),l=t?et:e?St:xt;return!e&&Pe(i,0,Le),o.forEach(((e,t)=>n.call(s,l(e),l(t),r)))}};l(n,e?{add:nt("add"),set:nt("set"),delete:nt("delete"),clear:nt("clear")}:{add(e){t||mt(e)||gt(e)||(e=yt(e));const n=yt(this);return tt(n).has.call(n,e)||(n.add(e),De(n,"add",e,e)),this},set(e,n){t||mt(n)||gt(n)||(n=yt(n));const s=yt(this),{has:r,get:o}=tt(s);let i=r.call(s,e);i||(e=yt(e),i=r.call(s,e));const l=o.call(s,e);return s.set(e,n),i?j(n,l)&&De(s,"set",e,n):De(s,"add",e,n),this},delete(e){const t=yt(this),{has:n,get:s}=tt(t);let r=n.call(t,e);r||(e=yt(e),r=n.call(t,e)),s&&s.call(t,e);const o=t.delete(e);return r&&De(t,"delete",e,void 0),o},clear(){const e=yt(this),t=0!==e.size,n=e.clear();return t&&De(e,"clear",void 0,void 0),n}});return["keys","values","entries",Symbol.iterator].forEach((s=>{n[s]=function(e,t,n){return function(...s){const r=this.__v_raw,o=yt(r),i=p(o),l="entries"===e||e===Symbol.iterator&&i,c="keys"===e&&i,a=r[e](...s),u=n?et:t?St:xt;return!t&&Pe(o,0,c?Me:Le),{next(){const{value:e,done:t}=a.next();return t?{value:e,done:t}:{value:l?[u(e[0]),u(e[1])]:u(e),done:t}},[Symbol.iterator](){return this}}}}(s,e,t)})),n}function rt(e,t){const n=st(e,t);return(t,s,r)=>"__v_isReactive"===s?!e:"__v_isReadonly"===s?e:"__v_raw"===s?t:Reflect.get(u(n,s)&&s in t?n:t,s,r)}const ot={get:rt(!1,!1)},it={get:rt(!1,!0)},lt={get:rt(!0,!1)},ct=new WeakMap,at=new WeakMap,ut=new WeakMap,ft=new WeakMap;function pt(e){return gt(e)?e:ht(e,!1,Xe,ot,ct)}function dt(e){return ht(e,!0,Qe,lt,ut)}function ht(e,t,n,s,r){if(!_(e))return e;if(e.__v_raw&&(!t||!e.__v_isReactive))return e;const o=(i=e).__v_skip||!Object.isExtensible(i)?0:function(e){switch(e){case"Object":case"Array":return 1;case"Map":case"Set":case"WeakMap":case"WeakSet":return 2;default:return 0}}(S(i));var i;if(0===o)return e;const l=r.get(e);if(l)return l;const c=new Proxy(e,2===o?s:n);return r.set(e,c),c}function vt(e){return gt(e)?vt(e.__v_raw):!(!e||!e.__v_isReactive)}function gt(e){return!(!e||!e.__v_isReadonly)}function mt(e){return!(!e||!e.__v_isShallow)}function _t(e){return!!e&&!!e.__v_raw}function yt(e){const t=e&&e.__v_raw;return t?yt(t):e}function bt(e){return!u(e,"__v_skip")&&Object.isExtensible(e)&&D(e,"__v_skip",!0),e}const xt=e=>_(e)?pt(e):e,St=e=>_(e)?dt(e):e;function Ct(e){return!!e&&!0===e.__v_isRef}function wt(e){return At(e,!1)}function kt(e){return At(e,!0)}function At(e,t){return Ct(e)?e:new Et(e,t)}class Et{constructor(e,t){this.dep=new Te,this.__v_isRef=!0,this.__v_isShallow=!1,this._rawValue=t?e:yt(e),this._value=t?e:xt(e),this.__v_isShallow=t}get value(){return this.dep.track(),this._value}set value(e){const t=this._rawValue,n=this.__v_isShallow||mt(e)||gt(e);e=n?e:yt(e),j(e,t)&&(this._rawValue=e,this._value=n?e:xt(e),this.dep.trigger())}}function Tt(e){return Ct(e)?e.value:e}function Ot(e){return v(e)?e():Tt(e)}const Ft={get:(e,t,n)=>"__v_raw"===t?e:Tt(Reflect.get(e,t,n)),set:(e,t,n,s)=>{const r=e[t];return Ct(r)&&!Ct(n)?(r.value=n,!0):Reflect.set(e,t,n,s)}};function Lt(e){return vt(e)?e:new Proxy(e,Ft)}function Mt(e){const t=f(e)?new Array(e.length):{};for(const n in e)t[n]=Vt(e,n);return t}class jt{constructor(e,t,n){this._object=e,this._key=t,this._defaultValue=n,this.__v_isRef=!0,this._value=void 0}get value(){const e=this._object[this._key];return this._value=void 0===e?this._defaultValue:e}set value(e){this._object[this._key]=e}get dep(){return function(e,t){const n=Fe.get(e);return n&&n.get(t)}(yt(this._object),this._key)}}class Pt{constructor(e){this._getter=e,this.__v_isRef=!0,this.__v_isReadonly=!0,this._value=void 0}get value(){return this._value=this._getter()}}function Dt(e,t,n){return Ct(e)?e:v(e)?new Pt(e):_(e)&&arguments.length>1?Vt(e,t,n):wt(e)}function Vt(e,t,n){const s=e[t];return Ct(s)?s:new jt(e,t,n)}class Rt{constructor(e,t,n){this.fn=e,this.setter=t,this._value=void 0,this.dep=new Te(this),this.__v_isRef=!0,this.deps=void 0,this.depsTail=void 0,this.flags=16,this.globalVersion=Ae-1,this.next=void 0,this.effect=this,this.__v_isReadonly=!t,this.isSSR=n}notify(){if(this.flags|=16,!(8&this.flags)&&ne!==this)return pe(this,!0),!0}get value(){const e=this.dep.track();return _e(this),e&&(e.version=this.dep.version),this._value}set value(e){this.setter&&this.setter(e)}}const $t={},It=new WeakMap;let Nt;function Ut(e,n,r=t){const{immediate:o,deep:i,once:l,scheduler:a,augmentJob:u,call:p}=r,d=e=>i?e:mt(e)||!1===i||0===i?Bt(e,1):Bt(e);let h,g,m,_,y=!1,b=!1;if(Ct(e)?(g=()=>e.value,y=mt(e)):vt(e)?(g=()=>d(e),y=!0):f(e)?(b=!0,y=e.some((e=>vt(e)||mt(e))),g=()=>e.map((e=>Ct(e)?e.value:vt(e)?d(e):v(e)?p?p(e,2):e():void 0))):g=v(e)?n?p?()=>p(e,2):e:()=>{if(m){Ce();try{m()}finally{we()}}const t=Nt;Nt=h;try{return p?p(e,3,[_]):e(_)}finally{Nt=t}}:s,n&&i){const e=g,t=!0===i?1/0:i;g=()=>Bt(e(),t)}const x=oe(),S=()=>{h.stop(),x&&x.active&&c(x.effects,h)};if(l&&n){const e=n;n=(...t)=>{e(...t),S()}}let C=b?new Array(e.length).fill($t):$t;const w=e=>{if(1&h.flags&&(h.dirty||e))if(n){const e=h.run();if(i||y||(b?e.some(((e,t)=>j(e,C[t]))):j(e,C))){m&&m();const t=Nt;Nt=h;try{const t=[e,C===$t?void 0:b&&C[0]===$t?[]:C,_];C=e,p?p(n,3,t):n(...t)}finally{Nt=t}}}else h.run()};return u&&u(w),h=new ce(g),h.scheduler=a?()=>a(w,!1):w,_=e=>function(e,t=!1,n=Nt){if(n){let t=It.get(n);t||It.set(n,t=[]),t.push(e)}}(e,!1,h),m=h.onStop=()=>{const e=It.get(h);if(e){if(p)p(e,4);else for(const t of e)t();It.delete(h)}},n?o?w(!0):C=h.run():a?a(w.bind(null,!0),!0):h.run(),S.pause=h.pause.bind(h),S.resume=h.resume.bind(h),S.stop=S,S}function Bt(e,t=1/0,n){if(t<=0||!_(e)||e.__v_skip)return e;if((n=n||new Set).has(e))return e;if(n.add(e),t--,Ct(e))Bt(e.value,t,n);else if(f(e))for(let s=0;s{Bt(e,t,n)}));else if(C(e)){for(const s in e)Bt(e[s],t,n);for(const s of Object.getOwnPropertySymbols(e))Object.prototype.propertyIsEnumerable.call(e,s)&&Bt(e[s],t,n)}return e} +/** +* @vue/runtime-core v3.5.17 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/function Wt(e,t,n,s){try{return s?e(...s):e()}catch(r){Kt(r,t,n)}}function Ht(e,t,n,s){if(v(e)){const r=Wt(e,t,n,s);return r&&y(r)&&r.catch((e=>{Kt(e,t,n)})),r}if(f(e)){const r=[];for(let o=0;o=rn(n)?zt.push(e):zt.splice(function(e){let t=qt+1,n=zt.length;for(;t>>1,r=zt[s],o=rn(r);orn(e)-rn(t)));if(Gt.length=0,Jt)return void Jt.push(...e);for(Jt=e,Zt=0;Ztnull==e.id?2&e.flags?-1:1/0:e.id;function on(e){try{for(qt=0;qt{s._d&&Mr(-1);const r=an(t);let o;try{o=e(...n)}finally{an(r),s._d&&Mr(1)}return o};return s._n=!0,s._c=!0,s._d=!0,s}function fn(e,n){if(null===ln)return e;const s=uo(ln),r=e.dirs||(e.dirs=[]);for(let o=0;oe.__isTeleport,vn=e=>e&&(e.disabled||""===e.disabled),gn=e=>e&&(e.defer||""===e.defer),mn=e=>"undefined"!=typeof SVGElement&&e instanceof SVGElement,_n=e=>"function"==typeof MathMLElement&&e instanceof MathMLElement,yn=(e,t)=>{const n=e&&e.to;if(g(n)){if(t){return t(n)}return null}return n},bn={name:"Teleport",__isTeleport:!0,process(e,t,n,s,r,o,i,l,c,a){const{mc:u,pc:f,pbc:p,o:{insert:d,querySelector:h,createText:v,createComment:g}}=a,m=vn(t.props);let{shapeFlag:_,children:y,dynamicChildren:b}=t;if(null==e){const e=t.el=v(""),a=t.anchor=v("");d(e,n,s),d(a,n,s);const f=(e,t)=>{16&_&&(r&&r.isCE&&(r.ce._teleportTarget=e),u(y,e,t,r,o,i,l,c))},p=()=>{const e=t.target=yn(t.props,h),n=wn(e,t,v,d);e&&("svg"!==i&&mn(e)?i="svg":"mathml"!==i&&_n(e)&&(i="mathml"),m||(f(e,n),Cn(t,!1)))};m&&(f(n,a),Cn(t,!0)),gn(t.props)?(t.el.__isMounted=!1,tr((()=>{p(),delete t.el.__isMounted}),o)):p()}else{if(gn(t.props)&&!1===e.el.__isMounted)return void tr((()=>{bn.process(e,t,n,s,r,o,i,l,c,a)}),o);t.el=e.el,t.targetStart=e.targetStart;const u=t.anchor=e.anchor,d=t.target=e.target,v=t.targetAnchor=e.targetAnchor,g=vn(e.props),_=g?n:d,y=g?u:v;if("svg"===i||mn(d)?i="svg":("mathml"===i||_n(d))&&(i="mathml"),b?(p(e.dynamicChildren,b,_,r,o,i,l),or(e,t,!0)):c||f(e,t,_,y,r,o,i,l,!1),m)g?t.props&&e.props&&t.props.to!==e.props.to&&(t.props.to=e.props.to):xn(t,n,u,a,1);else if((t.props&&t.props.to)!==(e.props&&e.props.to)){const e=t.target=yn(t.props,h);e&&xn(t,e,null,a,0)}else g&&xn(t,d,v,a,1);Cn(t,m)}},remove(e,t,n,{um:s,o:{remove:r}},o){const{shapeFlag:i,children:l,anchor:c,targetStart:a,targetAnchor:u,target:f,props:p}=e;if(f&&(r(a),r(u)),o&&r(c),16&i){const e=o||!vn(p);for(let r=0;r{const t=e.subTree;return t.component?On(t.component):t};function Fn(e){let t=e[0];if(e.length>1)for(const n of e)if(n.type!==Ar){t=n;break}return t}const Ln={name:"BaseTransition",props:Tn,setup(e,{slots:t}){const n=Yr(),s=function(){const e={isMounted:!1,isLeaving:!1,isUnmounting:!1,leavingVNodes:new Map};return Jn((()=>{e.isMounted=!0})),Qn((()=>{e.isUnmounting=!0})),e}();return()=>{const r=t.default&&Rn(t.default(),!0);if(!r||!r.length)return;const o=Fn(r),i=yt(e),{mode:l}=i;if(s.isLeaving)return Pn(o);const c=Dn(o);if(!c)return Pn(o);let a=jn(c,i,s,n,(e=>a=e));c.type!==Ar&&Vn(c,a);let u=n.subTree&&Dn(n.subTree);if(u&&u.type!==Ar&&!Rr(c,u)&&On(n).type!==Ar){let e=jn(u,i,s,n);if(Vn(u,e),"out-in"===l&&c.type!==Ar)return s.isLeaving=!0,e.afterLeave=()=>{s.isLeaving=!1,8&n.job.flags||n.update(),delete e.afterLeave,u=void 0},Pn(o);"in-out"===l&&c.type!==Ar?e.delayLeave=(e,t,n)=>{Mn(s,u)[String(u.key)]=u,e[kn]=()=>{t(),e[kn]=void 0,delete a.delayedLeave,u=void 0},a.delayedLeave=()=>{n(),delete a.delayedLeave,u=void 0}}:u=void 0}else u&&(u=void 0);return o}}};function Mn(e,t){const{leavingVNodes:n}=e;let s=n.get(t.type);return s||(s=Object.create(null),n.set(t.type,s)),s}function jn(e,t,n,s,r){const{appear:o,mode:i,persisted:l=!1,onBeforeEnter:c,onEnter:a,onAfterEnter:u,onEnterCancelled:p,onBeforeLeave:d,onLeave:h,onAfterLeave:v,onLeaveCancelled:g,onBeforeAppear:m,onAppear:_,onAfterAppear:y,onAppearCancelled:b}=t,x=String(e.key),S=Mn(n,e),C=(e,t)=>{e&&Ht(e,s,9,t)},w=(e,t)=>{const n=t[1];C(e,t),f(e)?e.every((e=>e.length<=1))&&n():e.length<=1&&n()},k={mode:i,persisted:l,beforeEnter(t){let s=c;if(!n.isMounted){if(!o)return;s=m||c}t[kn]&&t[kn](!0);const r=S[x];r&&Rr(e,r)&&r.el[kn]&&r.el[kn](),C(s,[t])},enter(e){let t=a,s=u,r=p;if(!n.isMounted){if(!o)return;t=_||a,s=y||u,r=b||p}let i=!1;const l=e[An]=t=>{i||(i=!0,C(t?r:s,[e]),k.delayedLeave&&k.delayedLeave(),e[An]=void 0)};t?w(t,[e,l]):l()},leave(t,s){const r=String(e.key);if(t[An]&&t[An](!0),n.isUnmounting)return s();C(d,[t]);let o=!1;const i=t[kn]=n=>{o||(o=!0,s(),C(n?g:v,[t]),t[kn]=void 0,S[r]===e&&delete S[r])};S[r]=e,h?w(h,[t,i]):i()},clone(e){const o=jn(e,t,n,s,r);return r&&r(o),o}};return k}function Pn(e){if(Un(e))return(e=Br(e)).children=null,e}function Dn(e){if(!Un(e))return hn(e.type)&&e.children?Fn(e.children):e;if(e.component)return e.component.subTree;const{shapeFlag:t,children:n}=e;if(n){if(16&t)return n[0];if(32&t&&v(n.default))return n.default()}}function Vn(e,t){6&e.shapeFlag&&e.component?(e.transition=t,Vn(e.component.subTree,t)):128&e.shapeFlag?(e.ssContent.transition=t.clone(e.ssContent),e.ssFallback.transition=t.clone(e.ssFallback)):e.transition=t}function Rn(e,t=!1,n){let s=[],r=0;for(let o=0;o1)for(let o=0;oIn(e,n&&(f(n)?n[t]:n),s,r,o)));if(Nn(r)&&!o)return void(512&r.shapeFlag&&r.type.__asyncResolved&&r.component.subTree.component&&In(e,n,s,r.component.subTree));const i=4&r.shapeFlag?uo(r.component):r.el,l=o?null:i,{i:a,r:p}=e,d=n&&n.r,h=a.refs===t?a.refs={}:a.refs,m=a.setupState,_=yt(m),y=m===t?()=>!1:e=>u(_,e);if(null!=d&&d!==p&&(g(d)?(h[d]=null,y(d)&&(m[d]=null)):Ct(d)&&(d.value=null)),v(p))Wt(p,a,12,[l,h]);else{const t=g(p),n=Ct(p);if(t||n){const r=()=>{if(e.f){const n=t?y(p)?m[p]:h[p]:p.value;o?f(n)&&c(n,i):f(n)?n.includes(i)||n.push(i):t?(h[p]=[i],y(p)&&(m[p]=h[p])):(p.value=[i],e.k&&(h[e.k]=p.value))}else t?(h[p]=l,y(p)&&(m[p]=l)):n&&(p.value=l,e.k&&(h[e.k]=l))};l?(r.id=-1,tr(r,s)):r()}}}I().requestIdleCallback,I().cancelIdleCallback;const Nn=e=>!!e.type.__asyncLoader,Un=e=>e.type.__isKeepAlive;function Bn(e,t){Hn(e,"a",t)}function Wn(e,t){Hn(e,"da",t)}function Hn(e,t,n=Qr){const s=e.__wdc||(e.__wdc=()=>{let t=n;for(;t;){if(t.isDeactivated)return;t=t.parent}return e()});if(zn(t,s,n),n){let e=n.parent;for(;e&&e.parent;)Un(e.parent.vnode)&&Kn(s,t,n,e),e=e.parent}}function Kn(e,t,n,s){const r=zn(t,e,s,!0);Yn((()=>{c(s[t],r)}),n)}function zn(e,t,n=Qr,s=!1){if(n){const r=n[e]||(n[e]=[]),o=t.__weh||(t.__weh=(...s)=>{Ce();const r=no(n),o=Ht(t,n,e,s);return r(),we(),o});return s?r.unshift(o):r.push(o),o}}const qn=e=>(t,n=Qr)=>{io&&"sp"!==e||zn(e,((...e)=>t(...e)),n)},Gn=qn("bm"),Jn=qn("m"),Zn=qn("bu"),Xn=qn("u"),Qn=qn("bum"),Yn=qn("um"),es=qn("sp"),ts=qn("rtg"),ns=qn("rtc");function ss(e,t=Qr){zn("ec",e,t)}const rs="components";function os(e,t){return as(rs,e,!0,t)||e}const is=Symbol.for("v-ndc");function ls(e){return g(e)?as(rs,e,!1)||e:e||is}function cs(e){return as("directives",e)}function as(e,t,n=!0,s=!1){const r=ln||Qr;if(r){const n=r.type;if(e===rs){const e=fo(n,!1);if(e&&(e===t||e===T(t)||e===L(T(t))))return n}const o=us(r[e]||n[e],t)||us(r.appContext[e],t);return!o&&s?n:o}}function us(e,t){return e&&(e[t]||e[T(t)]||e[L(T(t))])}function fs(e,t,n,s){let r;const o=n,i=f(e);if(i||g(e)){let n=!1,s=!1;i&&vt(e)&&(n=!mt(e),s=gt(e),e=Re(e)),r=new Array(e.length);for(let i=0,l=e.length;it(e,n,void 0,o)));else{const n=Object.keys(e);r=new Array(n.length);for(let s=0,i=n.length;s{const t=s.fn(...e);return t&&(t.key=s.key),t}:s.fn)}return e}function ds(e,t,n={},s,r){if(ln.ce||ln.parent&&Nn(ln.parent)&&ln.parent.ce)return"default"!==t&&(n.name=t),Fr(),Dr(wr,null,[Ur("slot",n,s&&s())],64);let o=e[t];o&&o._c&&(o._d=!1),Fr();const i=o&&hs(o(n)),l=n.key||i&&i.key,c=Dr(wr,{key:(l&&!m(l)?l:`_${t}`)+(!i&&s?"_fb":"")},i||(s?s():[]),i&&1===e._?64:-2);return!r&&c.scopeId&&(c.slotScopeIds=[c.scopeId+"-s"]),o&&o._c&&(o._d=!0),c}function hs(e){return e.some((e=>!Vr(e)||e.type!==Ar&&!(e.type===wr&&!hs(e.children))))?e:null}const vs=e=>e?ro(e)?uo(e):vs(e.parent):null,gs=l(Object.create(null),{$:e=>e,$el:e=>e.vnode.el,$data:e=>e.data,$props:e=>e.props,$attrs:e=>e.attrs,$slots:e=>e.slots,$refs:e=>e.refs,$parent:e=>vs(e.parent),$root:e=>vs(e.root),$host:e=>e.ce,$emit:e=>e.emit,$options:e=>ws(e),$forceUpdate:e=>e.f||(e.f=()=>{en(e.update)}),$nextTick:e=>e.n||(e.n=Yt.bind(e.proxy)),$watch:e=>dr.bind(e)}),ms=(e,n)=>e!==t&&!e.__isScriptSetup&&u(e,n),_s={get({_:e},n){if("__v_skip"===n)return!0;const{ctx:s,setupState:r,data:o,props:i,accessCache:l,type:c,appContext:a}=e;let f;if("$"!==n[0]){const c=l[n];if(void 0!==c)switch(c){case 1:return r[n];case 2:return o[n];case 4:return s[n];case 3:return i[n]}else{if(ms(r,n))return l[n]=1,r[n];if(o!==t&&u(o,n))return l[n]=2,o[n];if((f=e.propsOptions[0])&&u(f,n))return l[n]=3,i[n];if(s!==t&&u(s,n))return l[n]=4,s[n];bs&&(l[n]=0)}}const p=gs[n];let d,h;return p?("$attrs"===n&&Pe(e.attrs,0,""),p(e)):(d=c.__cssModules)&&(d=d[n])?d:s!==t&&u(s,n)?(l[n]=4,s[n]):(h=a.config.globalProperties,u(h,n)?h[n]:void 0)},set({_:e},n,s){const{data:r,setupState:o,ctx:i}=e;return ms(o,n)?(o[n]=s,!0):r!==t&&u(r,n)?(r[n]=s,!0):!u(e.props,n)&&(("$"!==n[0]||!(n.slice(1)in e))&&(i[n]=s,!0))},has({_:{data:e,setupState:n,accessCache:s,ctx:r,appContext:o,propsOptions:i}},l){let c;return!!s[l]||e!==t&&u(e,l)||ms(n,l)||(c=i[0])&&u(c,l)||u(r,l)||u(gs,l)||u(o.config.globalProperties,l)},defineProperty(e,t,n){return null!=n.get?e._.accessCache[t]=0:u(n,"value")&&this.set(e,t,n.value,null),Reflect.defineProperty(e,t,n)}};function ys(e){return f(e)?e.reduce(((e,t)=>(e[t]=null,e)),{}):e}let bs=!0;function xs(e){const t=ws(e),n=e.proxy,r=e.ctx;bs=!1,t.beforeCreate&&Ss(t.beforeCreate,e,"bc");const{data:o,computed:i,methods:l,watch:c,provide:a,inject:u,created:p,beforeMount:d,mounted:h,beforeUpdate:g,updated:m,activated:y,deactivated:b,beforeDestroy:x,beforeUnmount:S,destroyed:C,unmounted:w,render:k,renderTracked:A,renderTriggered:E,errorCaptured:T,serverPrefetch:O,expose:F,inheritAttrs:L,components:M,directives:j,filters:P}=t;if(u&&function(e,t){f(e)&&(e=Ts(e));for(const n in e){const s=e[n];let r;r=_(s)?"default"in s?Vs(s.from||n,s.default,!0):Vs(s.from||n):Vs(s),Ct(r)?Object.defineProperty(t,n,{enumerable:!0,configurable:!0,get:()=>r.value,set:e=>r.value=e}):t[n]=r}}(u,r,null),l)for(const s in l){const e=l[s];v(e)&&(r[s]=e.bind(n))}if(o){const t=o.call(n,n);_(t)&&(e.data=pt(t))}if(bs=!0,i)for(const f in i){const e=i[f],t=v(e)?e.bind(n,n):v(e.get)?e.get.bind(n,n):s,o=!v(e)&&v(e.set)?e.set.bind(n):s,l=po({get:t,set:o});Object.defineProperty(r,f,{enumerable:!0,configurable:!0,get:()=>l.value,set:e=>l.value=e})}if(c)for(const s in c)Cs(c[s],r,n,s);if(a){const e=v(a)?a.call(n):a;Reflect.ownKeys(e).forEach((t=>{!function(e,t){if(Qr){let n=Qr.provides;const s=Qr.parent&&Qr.parent.provides;s===n&&(n=Qr.provides=Object.create(s)),n[e]=t}else;}(t,e[t])}))}function D(e,t){f(t)?t.forEach((t=>e(t.bind(n)))):t&&e(t.bind(n))}if(p&&Ss(p,e,"c"),D(Gn,d),D(Jn,h),D(Zn,g),D(Xn,m),D(Bn,y),D(Wn,b),D(ss,T),D(ns,A),D(ts,E),D(Qn,S),D(Yn,w),D(es,O),f(F))if(F.length){const t=e.exposed||(e.exposed={});F.forEach((e=>{Object.defineProperty(t,e,{get:()=>n[e],set:t=>n[e]=t})}))}else e.exposed||(e.exposed={});k&&e.render===s&&(e.render=k),null!=L&&(e.inheritAttrs=L),M&&(e.components=M),j&&(e.directives=j),O&&$n(e)}function Ss(e,t,n){Ht(f(e)?e.map((e=>e.bind(t.proxy))):e.bind(t.proxy),t,n)}function Cs(e,t,n,s){let r=s.includes(".")?hr(n,s):()=>n[s];if(g(e)){const n=t[e];v(n)&&fr(r,n)}else if(v(e))fr(r,e.bind(n));else if(_(e))if(f(e))e.forEach((e=>Cs(e,t,n,s)));else{const s=v(e.handler)?e.handler.bind(n):t[e.handler];v(s)&&fr(r,s,e)}}function ws(e){const t=e.type,{mixins:n,extends:s}=t,{mixins:r,optionsCache:o,config:{optionMergeStrategies:i}}=e.appContext,l=o.get(t);let c;return l?c=l:r.length||n||s?(c={},r.length&&r.forEach((e=>ks(c,e,i,!0))),ks(c,t,i)):c=t,_(t)&&o.set(t,c),c}function ks(e,t,n,s=!1){const{mixins:r,extends:o}=t;o&&ks(e,o,n,!0),r&&r.forEach((t=>ks(e,t,n,!0)));for(const i in t)if(s&&"expose"===i);else{const s=As[i]||n&&n[i];e[i]=s?s(e[i],t[i]):t[i]}return e}const As={data:Es,props:Ls,emits:Ls,methods:Fs,computed:Fs,beforeCreate:Os,created:Os,beforeMount:Os,mounted:Os,beforeUpdate:Os,updated:Os,beforeDestroy:Os,beforeUnmount:Os,destroyed:Os,unmounted:Os,activated:Os,deactivated:Os,errorCaptured:Os,serverPrefetch:Os,components:Fs,directives:Fs,watch:function(e,t){if(!e)return t;if(!t)return e;const n=l(Object.create(null),e);for(const s in t)n[s]=Os(e[s],t[s]);return n},provide:Es,inject:function(e,t){return Fs(Ts(e),Ts(t))}};function Es(e,t){return t?e?function(){return l(v(e)?e.call(this,this):e,v(t)?t.call(this,this):t)}:t:e}function Ts(e){if(f(e)){const t={};for(let n=0;n(o.has(e)||(e&&v(e.install)?(o.add(e),e.install(a,...t)):v(e)&&(o.add(e),e(a,...t))),a),mixin:e=>(r.mixins.includes(e)||r.mixins.push(e),a),component:(e,t)=>t?(r.components[e]=t,a):r.components[e],directive:(e,t)=>t?(r.directives[e]=t,a):r.directives[e],mount(o,i,l){if(!c){const u=a._ceVNode||Ur(n,s);return u.appContext=r,!0===l?l="svg":!1===l&&(l=void 0),i&&t?t(u,o):e(u,o,l),c=!0,a._container=o,o.__vue_app__=a,uo(u.component)}},onUnmount(e){i.push(e)},unmount(){c&&(Ht(i,a._instance,16),e(null,a._container),delete a._container.__vue_app__)},provide:(e,t)=>(r.provides[e]=t,a),runWithContext(e){const t=Ds;Ds=a;try{return e()}finally{Ds=t}}};return a}}let Ds=null;function Vs(e,t,n=!1){const s=Qr||ln;if(s||Ds){let r=Ds?Ds._context.provides:s?null==s.parent||s.ce?s.vnode.appContext&&s.vnode.appContext.provides:s.parent.provides:void 0;if(r&&e in r)return r[e];if(arguments.length>1)return n&&v(t)?t.call(s&&s.proxy):t}}function Rs(){return!!(Qr||ln||Ds)}const $s={},Is=()=>Object.create($s),Ns=e=>Object.getPrototypeOf(e)===$s;function Us(e,t,n,s=!1){const r={},o=Is();e.propsDefaults=Object.create(null),Bs(e,t,r,o);for(const i in e.propsOptions[0])i in r||(r[i]=void 0);n?e.props=s?r:ht(r,!1,Ye,it,at):e.type.props?e.props=r:e.props=o,e.attrs=o}function Bs(e,n,s,r){const[o,i]=e.propsOptions;let l,c=!1;if(n)for(let t in n){if(k(t))continue;const a=n[t];let f;o&&u(o,f=T(t))?i&&i.includes(f)?(l||(l={}))[f]=a:s[f]=a:_r(e.emitsOptions,t)||t in r&&a===r[t]||(r[t]=a,c=!0)}if(i){const n=yt(s),r=l||t;for(let t=0;t{d=!0;const[t,n]=Ks(e,s,!0);l(a,t),n&&p.push(...n)};!r&&s.mixins.length&&s.mixins.forEach(t),e.extends&&t(e.extends),e.mixins&&e.mixins.forEach(t)}if(!c&&!d)return _(e)&&o.set(e,n),n;if(f(c))for(let n=0;n"_"===e[0]||"$stable"===e,Gs=e=>f(e)?e.map(Kr):[Kr(e)],Js=(e,t,n)=>{if(t._n)return t;const s=un(((...e)=>Gs(t(...e))),n);return s._c=!1,s},Zs=(e,t,n)=>{const s=e._ctx;for(const r in e){if(qs(r))continue;const n=e[r];if(v(n))t[r]=Js(0,n,s);else if(null!=n){const e=Gs(n);t[r]=()=>e}}},Xs=(e,t)=>{const n=Gs(t);e.slots.default=()=>n},Qs=(e,t,n)=>{for(const s in t)!n&&qs(s)||(e[s]=t[s])},Ys=(e,t,n)=>{const s=e.slots=Is();if(32&e.vnode.shapeFlag){const e=t.__;e&&D(s,"__",e,!0);const r=t._;r?(Qs(s,t,n),n&&D(s,"_",r,!0)):Zs(t,s)}else t&&Xs(e,t)},er=(e,n,s)=>{const{vnode:r,slots:o}=e;let i=!0,l=t;if(32&r.shapeFlag){const e=n._;e?s&&1===e?i=!1:Qs(o,n,s):(i=!n.$stable,Zs(n,o)),l=n}else n&&(Xs(e,n),l={default:1});if(i)for(const t in o)qs(t)||null!=l[t]||delete o[t]},tr=function(e,t){t&&t.pendingBranch?f(e)?t.effects.push(...e):t.effects.push(e):(f(n=e)?Gt.push(...n):Jt&&-1===n.id?Jt.splice(Zt+1,0,n):1&n.flags||(Gt.push(n),n.flags|=1),tn());var n};function nr(e){return function(e){I().__VUE__=!0;const{insert:r,remove:o,patchProp:i,createElement:l,createText:c,createComment:a,setText:p,setElementText:d,parentNode:h,nextSibling:v,setScopeId:g=s,insertStaticContent:m}=e,_=(e,t,n,s=null,r=null,o=null,i=void 0,l=null,c=!!t.dynamicChildren)=>{if(e===t)return;e&&!Rr(e,t)&&(s=Q(e),q(e,r,o,!0),e=null),-2===t.patchFlag&&(c=!1,t.dynamicChildren=null);const{type:a,ref:u,shapeFlag:f}=t;switch(a){case kr:b(e,t,n,s);break;case Ar:x(e,t,n,s);break;case Er:null==e&&S(t,n,s,i);break;case wr:V(e,t,n,s,r,o,i,l,c);break;default:1&f?A(e,t,n,s,r,o,i,l,c):6&f?R(e,t,n,s,r,o,i,l,c):(64&f||128&f)&&a.process(e,t,n,s,r,o,i,l,c,te)}null!=u&&r?In(u,e&&e.ref,o,t||e,!t):null==u&&e&&null!=e.ref&&In(e.ref,null,o,e,!0)},b=(e,t,n,s)=>{if(null==e)r(t.el=c(t.children),n,s);else{const n=t.el=e.el;t.children!==e.children&&p(n,t.children)}},x=(e,t,n,s)=>{null==e?r(t.el=a(t.children||""),n,s):t.el=e.el},S=(e,t,n,s)=>{[e.el,e.anchor]=m(e.children,t,n,s,e.el,e.anchor)},C=({el:e,anchor:t},n,s)=>{let o;for(;e&&e!==t;)o=v(e),r(e,n,s),e=o;r(t,n,s)},w=({el:e,anchor:t})=>{let n;for(;e&&e!==t;)n=v(e),o(e),e=n;o(t)},A=(e,t,n,s,r,o,i,l,c)=>{"svg"===t.type?i="svg":"math"===t.type&&(i="mathml"),null==e?E(t,n,s,r,o,i,l,c):M(e,t,r,o,i,l,c)},E=(e,t,n,s,o,c,a,u)=>{let f,p;const{props:h,shapeFlag:v,transition:g,dirs:m}=e;if(f=e.el=l(e.type,c,h&&h.is,h),8&v?d(f,e.children):16&v&&L(e.children,f,null,s,o,sr(e,c),a,u),m&&pn(e,null,s,"created"),O(f,e,e.scopeId,a,s),h){for(const e in h)"value"===e||k(e)||i(f,e,null,h[e],c,s);"value"in h&&i(f,"value",null,h.value,c),(p=h.onVnodeBeforeMount)&&Jr(p,s,e)}m&&pn(e,null,s,"beforeMount");const _=function(e,t){return(!e||e&&!e.pendingBranch)&&t&&!t.persisted}(o,g);_&&g.beforeEnter(f),r(f,t,n),((p=h&&h.onVnodeMounted)||_||m)&&tr((()=>{p&&Jr(p,s,e),_&&g.enter(f),m&&pn(e,null,s,"mounted")}),o)},O=(e,t,n,s,r)=>{if(n&&g(e,n),s)for(let o=0;o{for(let a=c;a{const a=n.el=e.el;let{patchFlag:u,dynamicChildren:f,dirs:p}=n;u|=16&e.patchFlag;const h=e.props||t,v=n.props||t;let g;if(s&&rr(s,!1),(g=v.onVnodeBeforeUpdate)&&Jr(g,s,n,e),p&&pn(n,e,s,"beforeUpdate"),s&&rr(s,!0),(h.innerHTML&&null==v.innerHTML||h.textContent&&null==v.textContent)&&d(a,""),f?j(e.dynamicChildren,f,a,s,r,sr(n,o),l):c||W(e,n,a,null,s,r,sr(n,o),l,!1),u>0){if(16&u)D(a,h,v,s,o);else if(2&u&&h.class!==v.class&&i(a,"class",null,v.class,o),4&u&&i(a,"style",h.style,v.style,o),8&u){const e=n.dynamicProps;for(let t=0;t{g&&Jr(g,s,n,e),p&&pn(n,e,s,"updated")}),r)},j=(e,t,n,s,r,o,i)=>{for(let l=0;l{if(n!==s){if(n!==t)for(const t in n)k(t)||t in s||i(e,t,n[t],null,o,r);for(const t in s){if(k(t))continue;const l=s[t],c=n[t];l!==c&&"value"!==t&&i(e,t,c,l,o,r)}"value"in s&&i(e,"value",n.value,s.value,o)}},V=(e,t,n,s,o,i,l,a,u)=>{const f=t.el=e?e.el:c(""),p=t.anchor=e?e.anchor:c("");let{patchFlag:d,dynamicChildren:h,slotScopeIds:v}=t;v&&(a=a?a.concat(v):v),null==e?(r(f,n,s),r(p,n,s),L(t.children||[],n,p,o,i,l,a,u)):d>0&&64&d&&h&&e.dynamicChildren?(j(e.dynamicChildren,h,n,o,i,l,a),(null!=t.key||o&&t===o.subTree)&&or(e,t,!0)):W(e,t,n,p,o,i,l,a,u)},R=(e,t,n,s,r,o,i,l,c)=>{t.slotScopeIds=l,null==e?512&t.shapeFlag?r.ctx.activate(t,n,s,i,c):$(t,n,s,r,o,i,c):N(e,t,c)},$=(e,n,s,r,o,i,l)=>{const c=e.component=function(e,n,s){const r=e.type,o=(n?n.appContext:e.appContext)||Zr,i={uid:Xr++,vnode:e,type:r,parent:n,appContext:o,root:null,next:null,subTree:null,effect:null,update:null,job:null,scope:new se(!0),render:null,proxy:null,exposed:null,exposeProxy:null,withProxy:null,provides:n?n.provides:Object.create(o.provides),ids:n?n.ids:["",0,0],accessCache:null,renderCache:[],components:null,directives:null,propsOptions:Ks(r,o),emitsOptions:mr(r,o),emit:null,emitted:null,propsDefaults:t,inheritAttrs:r.inheritAttrs,ctx:t,data:t,props:t,attrs:t,slots:t,refs:t,setupState:t,setupContext:null,suspense:s,suspenseId:s?s.pendingId:0,asyncDep:null,asyncResolved:!1,isMounted:!1,isUnmounted:!1,isDeactivated:!1,bc:null,c:null,bm:null,m:null,bu:null,u:null,um:null,bum:null,da:null,a:null,rtg:null,rtc:null,ec:null,sp:null};i.ctx={_:i},i.root=n?n.root:i,i.emit=gr.bind(null,i),e.ce&&e.ce(i);return i}(e,r,o);if(Un(e)&&(c.ctx.renderer=te),function(e,t=!1,n=!1){t&&to(t);const{props:s,children:r}=e.vnode,o=ro(e);Us(e,s,o,t),Ys(e,r,n||t);const i=o?function(e,t){const n=e.type;e.accessCache=Object.create(null),e.proxy=new Proxy(e.ctx,_s);const{setup:s}=n;if(s){Ce();const n=e.setupContext=s.length>1?function(e){const t=t=>{e.exposed=t||{}};return{attrs:new Proxy(e.attrs,ao),slots:e.slots,emit:e.emit,expose:t}}(e):null,r=no(e),o=Wt(s,e,0,[e.props,n]),i=y(o);if(we(),r(),!i&&!e.sp||Nn(e)||$n(e),i){if(o.then(so,so),t)return o.then((n=>{lo(e,n,t)})).catch((t=>{Kt(t,e,0)}));e.asyncDep=o}else lo(e,o,t)}else co(e,t)}(e,t):void 0;t&&to(!1)}(c,!1,l),c.asyncDep){if(o&&o.registerDep(c,U,l),!e.el){const e=c.subTree=Ur(Ar);x(null,e,n,s)}}else U(c,e,n,s,o,i,l)},N=(e,t,n)=>{const s=t.component=e.component;if(function(e,t,n){const{props:s,children:r,component:o}=e,{props:i,children:l,patchFlag:c}=t,a=o.emitsOptions;if(t.dirs||t.transition)return!0;if(!(n&&c>=0))return!(!r&&!l||l&&l.$stable)||s!==i&&(s?!i||Sr(s,i,a):!!i);if(1024&c)return!0;if(16&c)return s?Sr(s,i,a):!!i;if(8&c){const e=t.dynamicProps;for(let t=0;t{const l=()=>{if(e.isMounted){let{next:t,bu:n,u:s,parent:c,vnode:a}=e;{const n=ir(e);if(n)return t&&(t.el=a.el,B(e,t,i)),void n.asyncDep.then((()=>{e.isUnmounted||l()}))}let u,f=t;rr(e,!1),t?(t.el=a.el,B(e,t,i)):t=a,n&&P(n),(u=t.props&&t.props.onVnodeBeforeUpdate)&&Jr(u,c,t,a),rr(e,!0);const p=yr(e),d=e.subTree;e.subTree=p,_(d,p,h(d.el),Q(d),e,r,o),t.el=p.el,null===f&&function({vnode:e,parent:t},n){for(;t;){const s=t.subTree;if(s.suspense&&s.suspense.activeBranch===e&&(s.el=e.el),s!==e)break;(e=t.vnode).el=n,t=t.parent}}(e,p.el),s&&tr(s,r),(u=t.props&&t.props.onVnodeUpdated)&&tr((()=>Jr(u,c,t,a)),r)}else{let i;const{el:l,props:c}=t,{bm:a,m:u,parent:f,root:p,type:d}=e,h=Nn(t);if(rr(e,!1),a&&P(a),!h&&(i=c&&c.onVnodeBeforeMount)&&Jr(i,f,t),rr(e,!0),l&&re){const t=()=>{e.subTree=yr(e),re(l,e.subTree,e,r,null)};h&&d.__asyncHydrate?d.__asyncHydrate(l,e,t):t()}else{p.ce&&!1!==p.ce._def.shadowRoot&&p.ce._injectChildStyle(d);const i=e.subTree=yr(e);_(null,i,n,s,e,r,o),t.el=i.el}if(u&&tr(u,r),!h&&(i=c&&c.onVnodeMounted)){const e=t;tr((()=>Jr(i,f,e)),r)}(256&t.shapeFlag||f&&Nn(f.vnode)&&256&f.vnode.shapeFlag)&&e.a&&tr(e.a,r),e.isMounted=!0,t=n=s=null}};e.scope.on();const c=e.effect=new ce(l);e.scope.off();const a=e.update=c.run.bind(c),u=e.job=c.runIfDirty.bind(c);u.i=e,u.id=e.uid,c.scheduler=()=>en(u),rr(e,!0),a()},B=(e,t,n)=>{t.component=e;const s=e.vnode.props;e.vnode=t,e.next=null,function(e,t,n,s){const{props:r,attrs:o,vnode:{patchFlag:i}}=e,l=yt(r),[c]=e.propsOptions;let a=!1;if(!(s||i>0)||16&i){let s;Bs(e,t,r,o)&&(a=!0);for(const o in l)t&&(u(t,o)||(s=F(o))!==o&&u(t,s))||(c?!n||void 0===n[o]&&void 0===n[s]||(r[o]=Ws(c,l,o,void 0,e,!0)):delete r[o]);if(o!==l)for(const e in o)t&&u(t,e)||(delete o[e],a=!0)}else if(8&i){const n=e.vnode.dynamicProps;for(let s=0;s{const a=e&&e.children,u=e?e.shapeFlag:0,f=t.children,{patchFlag:p,shapeFlag:h}=t;if(p>0){if(128&p)return void K(a,f,n,s,r,o,i,l,c);if(256&p)return void H(a,f,n,s,r,o,i,l,c)}8&h?(16&u&&X(a,r,o),f!==a&&d(n,f)):16&u?16&h?K(a,f,n,s,r,o,i,l,c):X(a,r,o,!0):(8&u&&d(n,""),16&h&&L(f,n,s,r,o,i,l,c))},H=(e,t,s,r,o,i,l,c,a)=>{t=t||n;const u=(e=e||n).length,f=t.length,p=Math.min(u,f);let d;for(d=0;df?X(e,o,i,!0,!1,p):L(t,s,r,o,i,l,c,a,p)},K=(e,t,s,r,o,i,l,c,a)=>{let u=0;const f=t.length;let p=e.length-1,d=f-1;for(;u<=p&&u<=d;){const n=e[u],r=t[u]=a?zr(t[u]):Kr(t[u]);if(!Rr(n,r))break;_(n,r,s,null,o,i,l,c,a),u++}for(;u<=p&&u<=d;){const n=e[p],r=t[d]=a?zr(t[d]):Kr(t[d]);if(!Rr(n,r))break;_(n,r,s,null,o,i,l,c,a),p--,d--}if(u>p){if(u<=d){const e=d+1,n=ed)for(;u<=p;)q(e[u],o,i,!0),u++;else{const h=u,v=u,g=new Map;for(u=v;u<=d;u++){const e=t[u]=a?zr(t[u]):Kr(t[u]);null!=e.key&&g.set(e.key,u)}let m,y=0;const b=d-v+1;let x=!1,S=0;const C=new Array(b);for(u=0;u=b){q(n,o,i,!0);continue}let r;if(null!=n.key)r=g.get(n.key);else for(m=v;m<=d;m++)if(0===C[m-v]&&Rr(n,t[m])){r=m;break}void 0===r?q(n,o,i,!0):(C[r-v]=u+1,r>=S?S=r:x=!0,_(n,t[r],s,null,o,i,l,c,a),y++)}const w=x?function(e){const t=e.slice(),n=[0];let s,r,o,i,l;const c=e.length;for(s=0;s>1,e[n[l]]0&&(t[s]=n[o-1]),n[o]=s)}}o=n.length,i=n[o-1];for(;o-- >0;)n[o]=i,i=t[i];return n}(C):n;for(m=w.length-1,u=b-1;u>=0;u--){const e=v+u,n=t[e],p=e+1{const{el:l,type:c,transition:a,children:u,shapeFlag:f}=e;if(6&f)return void z(e.component.subTree,t,n,s);if(128&f)return void e.suspense.move(t,n,s);if(64&f)return void c.move(e,t,n,te);if(c===wr){r(l,t,n);for(let e=0;ea.enter(l)),i);else{const{leave:s,delayLeave:i,afterLeave:c}=a,u=()=>{e.ctx.isUnmounted?o(l):r(l,t,n)},f=()=>{s(l,(()=>{u(),c&&c()}))};i?i(l,u,f):f()}else r(l,t,n)},q=(e,t,n,s=!1,r=!1)=>{const{type:o,props:i,ref:l,children:c,dynamicChildren:a,shapeFlag:u,patchFlag:f,dirs:p,cacheIndex:d}=e;if(-2===f&&(r=!1),null!=l&&(Ce(),In(l,null,n,e,!0),we()),null!=d&&(t.renderCache[d]=void 0),256&u)return void t.ctx.deactivate(e);const h=1&u&&p,v=!Nn(e);let g;if(v&&(g=i&&i.onVnodeBeforeUnmount)&&Jr(g,t,e),6&u)Z(e.component,n,s);else{if(128&u)return void e.suspense.unmount(n,s);h&&pn(e,null,t,"beforeUnmount"),64&u?e.type.remove(e,t,n,te,s):a&&!a.hasOnce&&(o!==wr||f>0&&64&f)?X(a,t,n,!1,!0):(o===wr&&384&f||!r&&16&u)&&X(c,t,n),s&&G(e)}(v&&(g=i&&i.onVnodeUnmounted)||h)&&tr((()=>{g&&Jr(g,t,e),h&&pn(e,null,t,"unmounted")}),n)},G=e=>{const{type:t,el:n,anchor:s,transition:r}=e;if(t===wr)return void J(n,s);if(t===Er)return void w(e);const i=()=>{o(n),r&&!r.persisted&&r.afterLeave&&r.afterLeave()};if(1&e.shapeFlag&&r&&!r.persisted){const{leave:t,delayLeave:s}=r,o=()=>t(n,i);s?s(e.el,i,o):o()}else i()},J=(e,t)=>{let n;for(;e!==t;)n=v(e),o(e),e=n;o(t)},Z=(e,t,n)=>{const{bum:s,scope:r,job:o,subTree:i,um:l,m:c,a:a,parent:u,slots:{__:p}}=e;lr(c),lr(a),s&&P(s),u&&f(p)&&p.forEach((e=>{u.renderCache[e]=void 0})),r.stop(),o&&(o.flags|=8,q(i,e,t,n)),l&&tr(l,t),tr((()=>{e.isUnmounted=!0}),t),t&&t.pendingBranch&&!t.isUnmounted&&e.asyncDep&&!e.asyncResolved&&e.suspenseId===t.pendingId&&(t.deps--,0===t.deps&&t.resolve())},X=(e,t,n,s=!1,r=!1,o=0)=>{for(let i=o;i{if(6&e.shapeFlag)return Q(e.component.subTree);if(128&e.shapeFlag)return e.suspense.next();const t=v(e.anchor||e.el),n=t&&t[dn];return n?v(n):t};let Y=!1;const ee=(e,t,n)=>{null==e?t._vnode&&q(t._vnode,null,null,!0):_(t._vnode||null,e,t,null,null,null,n),t._vnode=e,Y||(Y=!0,nn(),sn(),Y=!1)},te={p:_,um:q,m:z,r:G,mt:$,mc:L,pc:W,pbc:j,n:Q,o:e};let ne,re;return{render:ee,hydrate:ne,createApp:Ps(ee,ne)}}(e)}function sr({type:e,props:t},n){return"svg"===n&&"foreignObject"===e||"mathml"===n&&"annotation-xml"===e&&t&&t.encoding&&t.encoding.includes("html")?void 0:n}function rr({effect:e,job:t},n){n?(e.flags|=32,t.flags|=4):(e.flags&=-33,t.flags&=-5)}function or(e,t,n=!1){const s=e.children,r=t.children;if(f(s)&&f(r))for(let o=0;oVs(cr);function ur(e,t){return pr(e,null,t)}function fr(e,t,n){return pr(e,t,n)}function pr(e,n,r=t){const{immediate:o,deep:i,flush:c,once:a}=r,u=l({},r),f=n&&o||!n&&"post"!==c;let p;if(io)if("sync"===c){const e=ar();p=e.__watcherHandles||(e.__watcherHandles=[])}else if(!f){const e=()=>{};return e.stop=s,e.resume=s,e.pause=s,e}const d=Qr;u.call=(e,t,n)=>Ht(e,d,t,n);let h=!1;"post"===c?u.scheduler=e=>{tr(e,d&&d.suspense)}:"sync"!==c&&(h=!0,u.scheduler=(e,t)=>{t?e():en(e)}),u.augmentJob=e=>{n&&(e.flags|=4),h&&(e.flags|=2,d&&(e.id=d.uid,e.i=d))};const v=Ut(e,n,u);return io&&(p?p.push(v):f&&v()),v}function dr(e,t,n){const s=this.proxy,r=g(e)?e.includes(".")?hr(s,e):()=>s[e]:e.bind(s,s);let o;v(t)?o=t:(o=t.handler,n=t);const i=no(this),l=pr(r,o.bind(s),n);return i(),l}function hr(e,t){const n=t.split(".");return()=>{let t=e;for(let e=0;e"modelValue"===t||"model-value"===t?e.modelModifiers:e[`${t}Modifiers`]||e[`${T(t)}Modifiers`]||e[`${F(t)}Modifiers`];function gr(e,n,...s){if(e.isUnmounted)return;const r=e.vnode.props||t;let o=s;const i=n.startsWith("update:"),l=i&&vr(r,n.slice(7));let c;l&&(l.trim&&(o=s.map((e=>g(e)?e.trim():e))),l.number&&(o=s.map(V)));let a=r[c=M(n)]||r[c=M(T(n))];!a&&i&&(a=r[c=M(F(n))]),a&&Ht(a,e,6,o);const u=r[c+"Once"];if(u){if(e.emitted){if(e.emitted[c])return}else e.emitted={};e.emitted[c]=!0,Ht(u,e,6,o)}}function mr(e,t,n=!1){const s=t.emitsCache,r=s.get(e);if(void 0!==r)return r;const o=e.emits;let i={},c=!1;if(!v(e)){const s=e=>{const n=mr(e,t,!0);n&&(c=!0,l(i,n))};!n&&t.mixins.length&&t.mixins.forEach(s),e.extends&&s(e.extends),e.mixins&&e.mixins.forEach(s)}return o||c?(f(o)?o.forEach((e=>i[e]=null)):l(i,o),_(e)&&s.set(e,i),i):(_(e)&&s.set(e,null),null)}function _r(e,t){return!(!e||!o(t))&&(t=t.slice(2).replace(/Once$/,""),u(e,t[0].toLowerCase()+t.slice(1))||u(e,F(t))||u(e,t))}function yr(e){const{type:t,vnode:n,proxy:s,withProxy:r,propsOptions:[o],slots:l,attrs:c,emit:a,render:u,renderCache:f,props:p,data:d,setupState:h,ctx:v,inheritAttrs:g}=e,m=an(e);let _,y;try{if(4&n.shapeFlag){const e=r||s,t=e;_=Kr(u.call(t,e,f,p,h,d,v)),y=c}else{const e=t;0,_=Kr(e.length>1?e(p,{attrs:c,slots:l,emit:a}):e(p,null)),y=t.props?c:br(c)}}catch(x){Tr.length=0,Kt(x,e,1),_=Ur(Ar)}let b=_;if(y&&!1!==g){const e=Object.keys(y),{shapeFlag:t}=b;e.length&&7&t&&(o&&e.some(i)&&(y=xr(y,o)),b=Br(b,y,!1,!0))}return n.dirs&&(b=Br(b,null,!1,!0),b.dirs=b.dirs?b.dirs.concat(n.dirs):n.dirs),n.transition&&Vn(b,n.transition),_=b,an(m),_}const br=e=>{let t;for(const n in e)("class"===n||"style"===n||o(n))&&((t||(t={}))[n]=e[n]);return t},xr=(e,t)=>{const n={};for(const s in e)i(s)&&s.slice(9)in t||(n[s]=e[s]);return n};function Sr(e,t,n){const s=Object.keys(t);if(s.length!==Object.keys(e).length)return!0;for(let r=0;re.__isSuspense;const wr=Symbol.for("v-fgt"),kr=Symbol.for("v-txt"),Ar=Symbol.for("v-cmt"),Er=Symbol.for("v-stc"),Tr=[];let Or=null;function Fr(e=!1){Tr.push(Or=e?null:[])}let Lr=1;function Mr(e,t=!1){Lr+=e,e<0&&Or&&t&&(Or.hasOnce=!0)}function jr(e){return e.dynamicChildren=Lr>0?Or||n:null,Tr.pop(),Or=Tr[Tr.length-1]||null,Lr>0&&Or&&Or.push(e),e}function Pr(e,t,n,s,r,o){return jr(Nr(e,t,n,s,r,o,!0))}function Dr(e,t,n,s,r){return jr(Ur(e,t,n,s,r,!0))}function Vr(e){return!!e&&!0===e.__v_isVNode}function Rr(e,t){return e.type===t.type&&e.key===t.key}const $r=({key:e})=>null!=e?e:null,Ir=({ref:e,ref_key:t,ref_for:n})=>("number"==typeof e&&(e=""+e),null!=e?g(e)||Ct(e)||v(e)?{i:ln,r:e,k:t,f:!!n}:e:null);function Nr(e,t=null,n=null,s=0,r=null,o=(e===wr?0:1),i=!1,l=!1){const c={__v_isVNode:!0,__v_skip:!0,type:e,props:t,key:t&&$r(t),ref:t&&Ir(t),scopeId:cn,slotScopeIds:null,children:n,component:null,suspense:null,ssContent:null,ssFallback:null,dirs:null,transition:null,el:null,anchor:null,target:null,targetStart:null,targetAnchor:null,staticCount:0,shapeFlag:o,patchFlag:s,dynamicProps:r,dynamicChildren:null,appContext:null,ctx:ln};return l?(qr(c,n),128&o&&e.normalize(c)):n&&(c.shapeFlag|=g(n)?8:16),Lr>0&&!i&&Or&&(c.patchFlag>0||6&o)&&32!==c.patchFlag&&Or.push(c),c}const Ur=function(e,t=null,n=null,s=0,r=null,o=!1){e&&e!==is||(e=Ar);if(Vr(e)){const s=Br(e,t,!0);return n&&qr(s,n),Lr>0&&!o&&Or&&(6&s.shapeFlag?Or[Or.indexOf(e)]=s:Or.push(s)),s.patchFlag=-2,s}i=e,v(i)&&"__vccOpts"in i&&(e=e.__vccOpts);var i;if(t){t=function(e){return e?_t(e)||Ns(e)?l({},e):e:null}(t);let{class:e,style:n}=t;e&&!g(e)&&(t.class=K(e)),_(n)&&(_t(n)&&!f(n)&&(n=l({},n)),t.style=N(n))}const c=g(e)?1:Cr(e)?128:hn(e)?64:_(e)?4:v(e)?2:0;return Nr(e,t,n,s,r,c,o,!0)};function Br(e,t,n=!1,s=!1){const{props:r,ref:o,patchFlag:i,children:l,transition:c}=e,a=t?Gr(r||{},t):r,u={__v_isVNode:!0,__v_skip:!0,type:e.type,props:a,key:a&&$r(a),ref:t&&t.ref?n&&o?f(o)?o.concat(Ir(t)):[o,Ir(t)]:Ir(t):o,scopeId:e.scopeId,slotScopeIds:e.slotScopeIds,children:l,target:e.target,targetStart:e.targetStart,targetAnchor:e.targetAnchor,staticCount:e.staticCount,shapeFlag:e.shapeFlag,patchFlag:t&&e.type!==wr?-1===i?16:16|i:i,dynamicProps:e.dynamicProps,dynamicChildren:e.dynamicChildren,appContext:e.appContext,dirs:e.dirs,transition:c,component:e.component,suspense:e.suspense,ssContent:e.ssContent&&Br(e.ssContent),ssFallback:e.ssFallback&&Br(e.ssFallback),el:e.el,anchor:e.anchor,ctx:e.ctx,ce:e.ce};return c&&s&&Vn(u,c.clone(u)),u}function Wr(e=" ",t=0){return Ur(kr,null,e,t)}function Hr(e="",t=!1){return t?(Fr(),Dr(Ar,null,e)):Ur(Ar,null,e)}function Kr(e){return null==e||"boolean"==typeof e?Ur(Ar):f(e)?Ur(wr,null,e.slice()):Vr(e)?zr(e):Ur(kr,null,String(e))}function zr(e){return null===e.el&&-1!==e.patchFlag||e.memo?e:Br(e)}function qr(e,t){let n=0;const{shapeFlag:s}=e;if(null==t)t=null;else if(f(t))n=16;else if("object"==typeof t){if(65&s){const n=t.default;return void(n&&(n._c&&(n._d=!1),qr(e,n()),n._c&&(n._d=!0)))}{n=32;const s=t._;s||Ns(t)?3===s&&ln&&(1===ln.slots._?t._=1:(t._=2,e.patchFlag|=1024)):t._ctx=ln}}else v(t)?(t={default:t,_ctx:ln},n=32):(t=String(t),64&s?(n=16,t=[Wr(t)]):n=8);e.children=t,e.shapeFlag|=n}function Gr(...e){const t={};for(let n=0;nQr||ln;let eo,to;{const e=I(),t=(t,n)=>{let s;return(s=e[t])||(s=e[t]=[]),s.push(n),e=>{s.length>1?s.forEach((t=>t(e))):s[0](e)}};eo=t("__VUE_INSTANCE_SETTERS__",(e=>Qr=e)),to=t("__VUE_SSR_SETTERS__",(e=>io=e))}const no=e=>{const t=Qr;return eo(e),e.scope.on(),()=>{e.scope.off(),eo(t)}},so=()=>{Qr&&Qr.scope.off(),eo(null)};function ro(e){return 4&e.vnode.shapeFlag}let oo,io=!1;function lo(e,t,n){v(t)?e.type.__ssrInlineRender?e.ssrRender=t:e.render=t:_(t)&&(e.setupState=Lt(t)),co(e,n)}function co(e,t,n){const r=e.type;if(!e.render){if(!t&&oo&&!r.render){const t=r.template||ws(e).template;if(t){const{isCustomElement:n,compilerOptions:s}=e.appContext.config,{delimiters:o,compilerOptions:i}=r,c=l(l({isCustomElement:n,delimiters:o},s),i);r.render=oo(t,c)}}e.render=r.render||s}{const t=no(e);Ce();try{xs(e)}finally{we(),t()}}}const ao={get:(e,t)=>(Pe(e,0,""),e[t])};function uo(e){return e.exposed?e.exposeProxy||(e.exposeProxy=new Proxy(Lt(bt(e.exposed)),{get:(t,n)=>n in t?t[n]:n in gs?gs[n](e):void 0,has:(e,t)=>t in e||t in gs})):e.proxy}function fo(e,t=!0){return v(e)?e.displayName||e.name:e.name||t&&e.__name}const po=(e,t)=>{const n=function(e,t,n=!1){let s,r;return v(e)?s=e:(s=e.get,r=e.set),new Rt(s,r,n)}(e,0,io);return n};function ho(e,t,n){const s=arguments.length;return 2===s?_(t)&&!f(t)?Vr(t)?Ur(e,null,[t]):Ur(e,t):Ur(e,null,t):(s>3?n=Array.prototype.slice.call(arguments,2):3===s&&Vr(n)&&(n=[n]),Ur(e,t,n))}const vo="3.5.17"; +/** +* @vue/runtime-dom v3.5.17 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/let go;const mo="undefined"!=typeof window&&window.trustedTypes;if(mo)try{go=mo.createPolicy("vue",{createHTML:e=>e})}catch(Si){}const _o=go?e=>go.createHTML(e):e=>e,yo="undefined"!=typeof document?document:null,bo=yo&&yo.createElement("template"),xo={insert:(e,t,n)=>{t.insertBefore(e,n||null)},remove:e=>{const t=e.parentNode;t&&t.removeChild(e)},createElement:(e,t,n,s)=>{const r="svg"===t?yo.createElementNS("http://www.w3.org/2000/svg",e):"mathml"===t?yo.createElementNS("http://www.w3.org/1998/Math/MathML",e):n?yo.createElement(e,{is:n}):yo.createElement(e);return"select"===e&&s&&null!=s.multiple&&r.setAttribute("multiple",s.multiple),r},createText:e=>yo.createTextNode(e),createComment:e=>yo.createComment(e),setText:(e,t)=>{e.nodeValue=t},setElementText:(e,t)=>{e.textContent=t},parentNode:e=>e.parentNode,nextSibling:e=>e.nextSibling,querySelector:e=>yo.querySelector(e),setScopeId(e,t){e.setAttribute(t,"")},insertStaticContent(e,t,n,s,r,o){const i=n?n.previousSibling:t.lastChild;if(r&&(r===o||r.nextSibling))for(;t.insertBefore(r.cloneNode(!0),n),r!==o&&(r=r.nextSibling););else{bo.innerHTML=_o("svg"===s?`${e}`:"mathml"===s?`${e}`:e);const r=bo.content;if("svg"===s||"mathml"===s){const e=r.firstChild;for(;e.firstChild;)r.appendChild(e.firstChild);r.removeChild(e)}t.insertBefore(r,n)}return[i?i.nextSibling:t.firstChild,n?n.previousSibling:t.lastChild]}},So="transition",Co="animation",wo=Symbol("_vtc"),ko={name:String,type:String,css:{type:Boolean,default:!0},duration:[String,Number,Object],enterFromClass:String,enterActiveClass:String,enterToClass:String,appearFromClass:String,appearActiveClass:String,appearToClass:String,leaveFromClass:String,leaveActiveClass:String,leaveToClass:String},Ao=l({},Tn,ko),Eo=(e=>(e.displayName="Transition",e.props=Ao,e))(((e,{slots:t})=>ho(Ln,function(e){const t={};for(const l in e)l in ko||(t[l]=e[l]);if(!1===e.css)return t;const{name:n="v",type:s,duration:r,enterFromClass:o=`${n}-enter-from`,enterActiveClass:i=`${n}-enter-active`,enterToClass:c=`${n}-enter-to`,appearFromClass:a=o,appearActiveClass:u=i,appearToClass:f=c,leaveFromClass:p=`${n}-leave-from`,leaveActiveClass:d=`${n}-leave-active`,leaveToClass:h=`${n}-leave-to`}=e,v=function(e){if(null==e)return null;if(_(e))return[Fo(e.enter),Fo(e.leave)];{const t=Fo(e);return[t,t]}}(r),g=v&&v[0],m=v&&v[1],{onBeforeEnter:y,onEnter:b,onEnterCancelled:x,onLeave:S,onLeaveCancelled:C,onBeforeAppear:w=y,onAppear:k=b,onAppearCancelled:A=x}=t,E=(e,t,n,s)=>{e._enterCancelled=s,Mo(e,t?f:c),Mo(e,t?u:i),n&&n()},T=(e,t)=>{e._isLeaving=!1,Mo(e,p),Mo(e,h),Mo(e,d),t&&t()},O=e=>(t,n)=>{const r=e?k:b,i=()=>E(t,e,n);To(r,[t,i]),jo((()=>{Mo(t,e?a:o),Lo(t,e?f:c),Oo(r)||Do(t,s,g,i)}))};return l(t,{onBeforeEnter(e){To(y,[e]),Lo(e,o),Lo(e,i)},onBeforeAppear(e){To(w,[e]),Lo(e,a),Lo(e,u)},onEnter:O(!1),onAppear:O(!0),onLeave(e,t){e._isLeaving=!0;const n=()=>T(e,t);Lo(e,p),e._enterCancelled?(Lo(e,d),$o()):($o(),Lo(e,d)),jo((()=>{e._isLeaving&&(Mo(e,p),Lo(e,h),Oo(S)||Do(e,s,m,n))})),To(S,[e,n])},onEnterCancelled(e){E(e,!1,void 0,!0),To(x,[e])},onAppearCancelled(e){E(e,!0,void 0,!0),To(A,[e])},onLeaveCancelled(e){T(e),To(C,[e])}})}(e),t))),To=(e,t=[])=>{f(e)?e.forEach((e=>e(...t))):e&&e(...t)},Oo=e=>!!e&&(f(e)?e.some((e=>e.length>1)):e.length>1);function Fo(e){return R(e)}function Lo(e,t){t.split(/\s+/).forEach((t=>t&&e.classList.add(t))),(e[wo]||(e[wo]=new Set)).add(t)}function Mo(e,t){t.split(/\s+/).forEach((t=>t&&e.classList.remove(t)));const n=e[wo];n&&(n.delete(t),n.size||(e[wo]=void 0))}function jo(e){requestAnimationFrame((()=>{requestAnimationFrame(e)}))}let Po=0;function Do(e,t,n,s){const r=e._endId=++Po,o=()=>{r===e._endId&&s()};if(null!=n)return setTimeout(o,n);const{type:i,timeout:l,propCount:c}=function(e,t){const n=window.getComputedStyle(e),s=e=>(n[e]||"").split(", "),r=s(`${So}Delay`),o=s(`${So}Duration`),i=Vo(r,o),l=s(`${Co}Delay`),c=s(`${Co}Duration`),a=Vo(l,c);let u=null,f=0,p=0;t===So?i>0&&(u=So,f=i,p=o.length):t===Co?a>0&&(u=Co,f=a,p=c.length):(f=Math.max(i,a),u=f>0?i>a?So:Co:null,p=u?u===So?o.length:c.length:0);const d=u===So&&/\b(transform|all)(,|$)/.test(s(`${So}Property`).toString());return{type:u,timeout:f,propCount:p,hasTransform:d}}(e,t);if(!i)return s();const a=i+"end";let u=0;const f=()=>{e.removeEventListener(a,p),o()},p=t=>{t.target===e&&++u>=c&&f()};setTimeout((()=>{uRo(t)+Ro(e[n]))))}function Ro(e){return"auto"===e?0:1e3*Number(e.slice(0,-1).replace(",","."))}function $o(){return document.body.offsetHeight}const Io=Symbol("_vod"),No=Symbol("_vsh"),Uo=Symbol(""),Bo=/(^|;)\s*display\s*:/;const Wo=/\s*!important$/;function Ho(e,t,n){if(f(n))n.forEach((n=>Ho(e,t,n)));else if(null==n&&(n=""),t.startsWith("--"))e.setProperty(t,n);else{const s=function(e,t){const n=zo[t];if(n)return n;let s=T(t);if("filter"!==s&&s in e)return zo[t]=s;s=L(s);for(let r=0;r{if(e._vts){if(e._vts<=n.attached)return}else e._vts=Date.now();Ht(function(e,t){if(f(t)){const n=e.stopImmediatePropagation;return e.stopImmediatePropagation=()=>{n.call(e),e._stopped=!0},t.map((e=>t=>!t._stopped&&e&&e(t)))}return t}(e,n.value),t,5,[e])};return n.value=e,n.attached=ni(),n}(s,r);Zo(e,n,i,l)}else i&&(!function(e,t,n,s){e.removeEventListener(t,n,s)}(e,n,i,l),o[t]=void 0)}}const Yo=/(?:Once|Passive|Capture)$/;let ei=0;const ti=Promise.resolve(),ni=()=>ei||(ti.then((()=>ei=0)),ei=Date.now());const si=e=>111===e.charCodeAt(0)&&110===e.charCodeAt(1)&&e.charCodeAt(2)>96&&e.charCodeAt(2)<123;const ri=e=>{const t=e.props["onUpdate:modelValue"]||!1;return f(t)?e=>P(t,e):t};function oi(e){e.target.composing=!0}function ii(e){const t=e.target;t.composing&&(t.composing=!1,t.dispatchEvent(new Event("input")))}const li=Symbol("_assign"),ci={created(e,{modifiers:{lazy:t,trim:n,number:s}},r){e[li]=ri(r);const o=s||r.props&&"number"===r.props.type;Zo(e,t?"change":"input",(t=>{if(t.target.composing)return;let s=e.value;n&&(s=s.trim()),o&&(s=V(s)),e[li](s)})),n&&Zo(e,"change",(()=>{e.value=e.value.trim()})),t||(Zo(e,"compositionstart",oi),Zo(e,"compositionend",ii),Zo(e,"change",ii))},mounted(e,{value:t}){e.value=null==t?"":t},beforeUpdate(e,{value:t,oldValue:n,modifiers:{lazy:s,trim:r,number:o}},i){if(e[li]=ri(i),e.composing)return;const l=null==t?"":t;if((!o&&"number"!==e.type||/^0\d/.test(e.value)?e.value:V(e.value))!==l){if(document.activeElement===e&&"range"!==e.type){if(s&&t===n)return;if(r&&e.value.trim()===l)return}e.value=l}}},ai={deep:!0,created(e,t,n){e[li]=ri(n),Zo(e,"change",(()=>{const t=e._modelValue,n=function(e){return"_value"in e?e._value:e.value}(e),s=e.checked,r=e[li];if(f(t)){const e=Z(t,n),o=-1!==e;if(s&&!o)r(t.concat(n));else if(!s&&o){const n=[...t];n.splice(e,1),r(n)}}else if(d(t)){const e=new Set(t);s?e.add(n):e.delete(n),r(e)}else r(fi(e,s))}))},mounted:ui,beforeUpdate(e,t,n){e[li]=ri(n),ui(e,t,n)}};function ui(e,{value:t,oldValue:n},s){let r;if(e._modelValue=t,f(t))r=Z(t,s.props.value)>-1;else if(d(t))r=t.has(s.props.value);else{if(t===n)return;r=J(t,fi(e,!0))}e.checked!==r&&(e.checked=r)}function fi(e,t){const n=t?"_trueValue":"_falseValue";return n in e?e[n]:t}const pi=["ctrl","shift","alt","meta"],di={stop:e=>e.stopPropagation(),prevent:e=>e.preventDefault(),self:e=>e.target!==e.currentTarget,ctrl:e=>!e.ctrlKey,shift:e=>!e.shiftKey,alt:e=>!e.altKey,meta:e=>!e.metaKey,left:e=>"button"in e&&0!==e.button,middle:e=>"button"in e&&1!==e.button,right:e=>"button"in e&&2!==e.button,exact:(e,t)=>pi.some((n=>e[`${n}Key`]&&!t.includes(n)))},hi=(e,t)=>{const n=e._withMods||(e._withMods={}),s=t.join(".");return n[s]||(n[s]=(n,...s)=>{for(let e=0;e{const n=e._withKeys||(e._withKeys={}),s=t.join(".");return n[s]||(n[s]=n=>{if(!("key"in n))return;const s=F(n.key);return t.some((e=>e===s||vi[e]===s))?e(n):void 0})},mi=l({patchProp:(e,t,n,s,r,l)=>{const c="svg"===r;"class"===t?function(e,t,n){const s=e[wo];s&&(t=(t?[t,...s]:[...s]).join(" ")),null==t?e.removeAttribute("class"):n?e.setAttribute("class",t):e.className=t}(e,s,c):"style"===t?function(e,t,n){const s=e.style,r=g(n);let o=!1;if(n&&!r){if(t)if(g(t))for(const e of t.split(";")){const t=e.slice(0,e.indexOf(":")).trim();null==n[t]&&Ho(s,t,"")}else for(const e in t)null==n[e]&&Ho(s,e,"");for(const e in n)"display"===e&&(o=!0),Ho(s,e,n[e])}else if(r){if(t!==n){const e=s[Uo];e&&(n+=";"+e),s.cssText=n,o=Bo.test(n)}}else t&&e.removeAttribute("style");Io in e&&(e[Io]=o?s.display:"",e[No]&&(s.display="none"))}(e,n,s):o(t)?i(t)||Qo(e,t,0,s,l):("."===t[0]?(t=t.slice(1),1):"^"===t[0]?(t=t.slice(1),0):function(e,t,n,s){if(s)return"innerHTML"===t||"textContent"===t||!!(t in e&&si(t)&&v(n));if("spellcheck"===t||"draggable"===t||"translate"===t||"autocorrect"===t)return!1;if("form"===t)return!1;if("list"===t&&"INPUT"===e.tagName)return!1;if("type"===t&&"TEXTAREA"===e.tagName)return!1;if("width"===t||"height"===t){const t=e.tagName;if("IMG"===t||"VIDEO"===t||"CANVAS"===t||"SOURCE"===t)return!1}if(si(t)&&g(n))return!1;return t in e}(e,t,s,c))?(Jo(e,t,s),e.tagName.includes("-")||"value"!==t&&"checked"!==t&&"selected"!==t||Go(e,t,s,c,0,"value"!==t)):!e._isVueCE||!/[A-Z]/.test(t)&&g(s)?("true-value"===t?e._trueValue=s:"false-value"===t&&(e._falseValue=s),Go(e,t,s,c)):Jo(e,T(t),s,0,t)}},xo);let _i;function yi(){return _i||(_i=nr(mi))}const bi=(...e)=>{yi().render(...e)},xi=(...e)=>{const t=yi().createApp(...e),{mount:n}=t;return t.mount=e=>{const s=function(e){if(g(e)){return document.querySelector(e)}return e}(e);if(!s)return;const r=t._component;v(r)||r.render||r.template||(r.template=s.innerHTML),1===s.nodeType&&(s.textContent="");const o=n(s,!1,function(e){if(e instanceof SVGElement)return"svg";if("function"==typeof MathMLElement&&e instanceof MathMLElement)return"mathml"}(s));return s instanceof Element&&(s.removeAttribute("v-cloak"),s.setAttribute("data-v-app","")),o},t};export{ho as $,un as A,xi as B,dt as C,Yr as D,Gr as E,wr as F,re as G,Ct as H,vt as I,yt as J,Dt as K,oe as L,ie as M,Vs as N,Mt as O,Rs as P,os as Q,cs as R,ls as S,Sn as T,Eo as U,z as V,ps as W,kt as X,Ot as Y,ur as Z,bi as _,Fr as a,Nr as b,Pr as c,Ur as d,fn as e,Dr as f,Qn as g,po as h,fs as i,hi as j,Hr as k,N as l,bt as m,K as n,Jn as o,pt as p,Wr as q,wt as r,ci as s,Q as t,Tt as u,ai as v,fr as w,Yt as x,gi as y,ds as z}; diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v2/assets/vueuse-Bf7Wy5dB.js b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v2/assets/vueuse-Bf7Wy5dB.js new file mode 100644 index 0000000000000000000000000000000000000000..f1cbb6f42bd98a40126cf727a285a3289d622965 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v2/assets/vueuse-Bf7Wy5dB.js @@ -0,0 +1 @@ +import{L as e,M as t,o as n,x as o,w as s,D as l,X as u,h as r,Y as i,Z as a,u as c}from"./vue-DfGw76ex.js";function v(n){return!!e()&&(t(n),!0)}const d="undefined"!=typeof window&&"undefined"!=typeof document;"undefined"!=typeof WorkerGlobalScope&&(globalThis,WorkerGlobalScope);const f=e=>null!=e,p=Object.prototype.toString,m=e=>"[object Object]"===p.call(e),h=()=>{};function g(e){return Array.isArray(e)?e:[e]}function w(e,t=!0,s){l()?n(e,s):t?e():o(e)}const y=d?window:void 0;function b(e){var t;const n=i(e);return null!=(t=null==n?void 0:n.$el)?t:n}function O(...e){const t=[],n=()=>{t.forEach((e=>e())),t.length=0},o=r((()=>{const t=g(i(e[0])).filter((e=>null!=e));return t.every((e=>"string"!=typeof e))?t:void 0})),l=(u=([e,o,s,l])=>{if(n(),!(null==e?void 0:e.length)||!(null==o?void 0:o.length)||!(null==s?void 0:s.length))return;const u=m(l)?{...l}:l;t.push(...e.flatMap((e=>o.flatMap((t=>s.map((n=>((e,t,n,o)=>(e.addEventListener(t,n,o),()=>e.removeEventListener(t,n,o)))(e,t,n,u))))))))},a={flush:"post"},s((()=>{var t,n;return[null!=(n=null==(t=o.value)?void 0:t.map((e=>b(e))))?n:[y].filter((e=>null!=e)),g(i(o.value?e[1]:e[0])),g(c(o.value?e[2]:e[1])),i(o.value?e[3]:e[2])]}),u,{...a,immediate:!0}));var u,a;return v(n),()=>{l(),n()}}function E(e){const t=function(){const e=u(!1),t=l();return t&&n((()=>{e.value=!0}),t),e}();return r((()=>(t.value,Boolean(e()))))}function X(e,t,n={}){const{window:o=y,...l}=n;let u;const a=E((()=>o&&"MutationObserver"in o)),c=()=>{u&&(u.disconnect(),u=void 0)},d=r((()=>{const t=g(i(e)).map(b).filter(f);return new Set(t)})),p=s((()=>d.value),(e=>{c(),a.value&&e.size&&(u=new MutationObserver(t),e.forEach((e=>u.observe(e,l))))}),{immediate:!0,flush:"post"}),m=()=>{p(),c()};return v(m),{isSupported:a,stop:m,takeRecords:()=>null==u?void 0:u.takeRecords()}}function Y(e,t={}){const{delayEnter:n=0,delayLeave:o=0,triggerOnRemoval:s=!1,window:l=y}=t,i=u(!1);let c;const d=e=>{const t=e?n:o;c&&(clearTimeout(c),c=void 0),t?c=setTimeout((()=>i.value=e),t):i.value=e};return l?(O(e,"mouseenter",(()=>d(!0)),{passive:!0}),O(e,"mouseleave",(()=>d(!1)),{passive:!0}),s&&function(e,t,n={}){const{window:o=y,document:s=(null==o?void 0:o.document),flush:l="sync"}=n;if(!o||!s)return h;let u;const r=e=>{null==u||u(),u=e},i=a((()=>{const n=b(e);if(n){const{stop:e}=X(s,(e=>{e.map((e=>[...e.removedNodes])).flat().some((e=>e===n||e.contains(n)))&&t(e)}),{window:o,childList:!0,subtree:!0});r(e)}}),{flush:l}),c=()=>{i(),r()};v(c)}(r((()=>b(e))),(()=>d(!1))),i):i}function x(e,t,n={}){const{root:o,rootMargin:l="0px",threshold:a=0,window:c=y,immediate:d=!0}=n,p=E((()=>c&&"IntersectionObserver"in c)),m=r((()=>g(i(e)).map(b).filter(f)));let w=h;const O=u(d),X=p.value?s((()=>[m.value,b(o),O.value]),(([e,n])=>{if(w(),!O.value)return;if(!e.length)return;const o=new IntersectionObserver(t,{root:b(n),rootMargin:l,threshold:a});e.forEach((e=>e&&o.observe(e))),w=()=>{o.disconnect(),w=h}}),{immediate:d,flush:"post"}):h,Y=()=>{w(),X(),O.value=!1};return v(Y),{isSupported:p,isActive:O,pause(){w(),O.value=!1},resume(){O.value=!0},stop:Y}}const M={page:e=>[e.pageX,e.pageY],client:e=>[e.clientX,e.clientY],screen:e=>[e.screenX,e.screenY],movement:e=>e instanceof MouseEvent?[e.movementX,e.movementY]:null};function S(e,t={}){const{windowResize:n=!0,windowScroll:o=!0,handleOutside:l=!0,window:a=y}=t,c=t.type||"page",{x:d,y:f,sourceType:p}=function(e={}){const{type:t="page",touch:n=!0,resetOnTouchEnds:o=!1,initialValue:s={x:0,y:0},window:l=y,target:r=l,scroll:i=!0,eventFilter:a}=e;let c=null,v=0,d=0;const f=u(s.x),p=u(s.y),m=u(null),h="function"==typeof t?t:M[t],g=e=>{const t=h(e);c=e,t&&([f.value,p.value]=t,m.value="mouse"),l&&(v=l.scrollX,d=l.scrollY)},w=e=>{if(e.touches.length>0){const t=h(e.touches[0]);t&&([f.value,p.value]=t,m.value="touch")}},b=()=>{if(!c||!l)return;const e=h(c);c instanceof MouseEvent&&e&&(f.value=e[0]+l.scrollX-v,p.value=e[1]+l.scrollY-d)},E=()=>{f.value=s.x,p.value=s.y},X=a?e=>a((()=>g(e)),{}):e=>g(e),Y=a?e=>a((()=>w(e)),{}):e=>w(e),x=a?()=>a((()=>b()),{}):()=>b();if(r){const e={passive:!0};O(r,["mousemove","dragover"],X,e),n&&"movement"!==t&&(O(r,["touchstart","touchmove"],Y,e),o&&O(r,"touchend",E,e)),i&&"page"===t&&O(l,"scroll",x,e)}return{x:f,y:p,sourceType:m}}(t),m=u(null!=e?e:null==a?void 0:a.document.body),h=u(0),g=u(0),Y=u(0),x=u(0),S=u(0),R=u(0),T=u(!0);function z(){if(!a)return;const e=b(m);if(!(e&&e instanceof Element))return;const{left:t,top:n,width:o,height:s}=e.getBoundingClientRect();Y.value=t+("page"===c?a.pageXOffset:0),x.value=n+("page"===c?a.pageYOffset:0),S.value=s,R.value=o;const u=d.value-Y.value,r=f.value-x.value;T.value=0===o||0===s||u<0||r<0||u>o||r>s,l&&(h.value=u,g.value=r)}const A=[];if(w((()=>{z()})),a){const{stop:e}=function(e,t,n={}){const{window:o=y,...l}=n;let u;const a=E((()=>o&&"ResizeObserver"in o)),c=()=>{u&&(u.disconnect(),u=void 0)},d=r((()=>{const t=i(e);return Array.isArray(t)?t.map((e=>b(e))):[b(t)]})),f=s(d,(e=>{if(c(),a.value&&o){u=new ResizeObserver(t);for(const t of e)t&&u.observe(t,l)}}),{immediate:!0,flush:"post"}),p=()=>{c(),f()};return v(p),{isSupported:a,stop:p}}(m,z),{stop:t}=X(m,z,{attributeFilter:["style","class"]}),l=s([m,d,f],z);A.push(e,t,l),O(document,"mouseleave",(()=>T.value=!0),{passive:!0}),o&&A.push(O("scroll",z,{capture:!0,passive:!0})),n&&A.push(O("resize",z,{passive:!0}))}return{x:d,y:f,sourceType:p,elementX:h,elementY:g,elementPositionX:Y,elementPositionY:x,elementHeight:S,elementWidth:R,isOutside:T,stop:function(){A.forEach((e=>e())),A.length=0}}}export{x as a,S as b,Y as c,O as u}; diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v2/easyuse.js b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v2/easyuse.js new file mode 100644 index 0000000000000000000000000000000000000000..189790a143c674207476f5de8a103cc5fc655584 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/web_version/v2/easyuse.js @@ -0,0 +1,2 @@ +!function(){"use strict";try{if("undefined"!=typeof document){var e=document.createElement("style");e.appendChild(document.createTextNode('@charset "UTF-8";.easyuse-prompt-await-select .p-select-label{font-size:12px!important}.easyuse-prompt-await-select .p-select-dropdown{width:1.5rem!important}.easyuse-prompt-await-select .p-select-dropdown svg{width:12px!important;height:12px!important}.easyuse-multiselect{font-size:12px;padding:0 15px;--p-chip-remove-icon-size: .75rem;--p-chip-remove-icon-focus-ring-width: var(--p-focus-ring-width);--p-chip-remove-icon-focus-ring-style: var(--p-focus-ring-style);--p-chip-remove-icon-focus-ring-color: var(--p-focus-ring-color);--p-chip-remove-icon-focus-ring-offset: var(--p-focus-ring-offset);--p-chip-remove-icon-focus-ring-shadow: var(--p-form-field-focus-ring-shadow);--p-chip-icon-size: .75rem;--p-chip-image-width: 2rem;--p-chip-image-height: 2rem;--p-chip-border-radius: 16px;--p-chip-padding-x: .25rem;--p-chip-padding-y: .5rem;--p-chip-gap: .25rem;--p-chip-transition-duration: var(--p-transition-duration);--p-chip-remove-icon-color: var(--input-text);--p-chip-icon-color: var(--input-text);--p-chip-background: var(--content-bg);--p-chip-color: var(--input-text);--p-checkbox-icon-size: .875rem;--p-checkbox-icon-color: var(--p-form-field-color);--p-checkbox-icon-checked-color: var(--p-primary-contrast-color);--p-checkbox-icon-checked-hover-color: var(--p-primary-contrast-color);--p-checkbox-icon-disabled-color: var(--p-form-field-disabled-color);--p-checkbox-icon-lg-size: 1rem;--p-checkbox-icon-sm-size: .75rem;--p-checkbox-border-radius: var(--p-border-radius-sm);--p-checkbox-width: 1rem;--p-checkbox-height: 1rem;--p-checkbox-background: var(--p-form-field-background);--p-checkbox-checked-background: var(--p-primary-color);--p-checkbox-checked-hover-background: var(--p-primary-hover-color);--p-checkbox-disabled-background: var(--p-form-field-disabled-background);--p-checkbox-filled-background: var(--p-form-field-filled-background);--p-checkbox-border-color: var(--p-form-field-border-color);--p-checkbox-hover-border-color: var(--p-form-field-hover-border-color);--p-checkbox-focus-border-color: var(--p-form-field-border-color);--p-checkbox-checked-border-color: var(--p-primary-color);--p-checkbox-checked-hover-border-color: var(--p-primary-hover-color);--p-checkbox-checked-focus-border-color: var(--p-primary-color);--p-checkbox-checked-disabled-border-color: var(--p-form-field-border-color);--p-checkbox-invalid-border-color: var(--p-form-field-invalid-border-color);--p-checkbox-shadow: var(--p-form-field-shadow);--p-checkbox-transition-duration: var(--p-form-field-transition-duration);--p-checkbox-lg-width: 1.5rem;--p-checkbox-lg-height: 1.5rem;--p-checkbox-sm-width: 1rem;--p-checkbox-sm-height: 1rem;--p-checkbox-focus-ring-width: var(--p-focus-ring-width);--p-checkbox-focus-ring-style: var(--p-focus-ring-style);--p-checkbox-focus-ring-color: var(--p-focus-ring-color);--p-checkbox-focus-ring-offset: var(--p-focus-ring-offset);--p-checkbox-focus-ring-shadow: var(--p-focus-ring-shadow);--p-multiselect-empty-message-padding: var(--p-list-option-padding);--p-multiselect-clear-icon-color: var(--p-form-field-icon-color);--p-multiselect-chip-border-radius: var(--p-border-radius-sm);--p-multiselect-option-group-background: var(--p-list-option-group-background);--p-multiselect-option-group-color: var(--p-list-option-group-color);--p-multiselect-option-group-font-weight: var(--p-list-option-group-font-weight);--p-multiselect-option-group-padding: var(--p-list-option-group-padding);--p-multiselect-option-focus-background: var(--p-list-option-focus-background);--p-multiselect-option-selected-background: var(--p-list-option-selected-background);--p-multiselect-option-selected-focus-background: var(--p-list-option-selected-focus-background);--p-multiselect-option-color: var(--p-list-option-color);--p-multiselect-option-focus-color: var(--p-list-option-focus-color);--p-multiselect-option-selected-color: var(--p-list-option-selected-color);--p-multiselect-option-selected-focus-color: var(--p-list-option-selected-focus-color);--p-multiselect-option-padding: .25rem;--p-multiselect-option-border-radius: var(--p-list-option-border-radius);--p-multiselect-option-gap: .5rem;--p-multiselect-list-padding: var(--p-list-padding);--p-multiselect-list-gap: var(--p-list-gap);--p-multiselect-list-header-padding: .25rem;--p-multiselect-overlay-background: var(--p-overlay-select-background);--p-multiselect-overlay-border-color: var(--p-overlay-select-border-color);--p-multiselect-overlay-border-radius: var(--p-overlay-select-border-radius);--p-multiselect-overlay-color: var(--p-overlay-select-color);--p-multiselect-overlay-shadow: var(--p-overlay-select-shadow);--p-multiselect-dropdown-width: 1.25rem;--p-multiselect-dropdown-color: var(--input-text);--p-multiselect-background: var(--comfy-input-bg);--p-multiselect-disabled-background: var(--p-form-field-disabled-background);--p-multiselect-filled-background: var(--p-form-field-filled-background);--p-multiselect-filled-hover-background: var(--p-form-field-filled-hover-background);--p-multiselect-filled-focus-background: var(--p-form-field-filled-focus-background);--p-multiselect-border-color: var(--border-color);--p-multiselect-hover-border-color: var(--border-color);--p-multiselect-focus-border-color: var(--border-color);--p-multiselect-invalid-border-color: var(--p-form-field-invalid-border-color);--p-multiselect-color: var(--p-form-field-color);--p-multiselect-disabled-color: var(--p-form-field-disabled-color);--p-multiselect-placeholder-color:var(--descrip-text);--p-multiselect-invalid-placeholder-color: var(--p-form-field-invalid-placeholder-color);--p-multiselect-shadow: var(--p-form-field-shadow);--p-multiselect-padding-x: 8px;--p-multiselect-padding-y: 2px;--p-multiselect-border-radius: 10px;--p-multiselect-transition-duration: var(--p-form-field-transition-duration);--p-multiselect-lg-font-size: var(--p-form-field-lg-font-size);--p-multiselect-lg-padding-x: var(--p-form-field-lg-padding-x);--p-multiselect-lg-padding-y: var(--p-form-field-lg-padding-y);--p-multiselect-sm-font-size: var(--p-form-field-sm-font-size);--p-multiselect-sm-padding-x: var(--p-form-field-sm-padding-x);--p-multiselect-sm-padding-y: var(--p-form-field-sm-padding-y);--p-multiselect-focus-ring-width: var(--p-form-field-focus-ring-width);--p-multiselect-focus-ring-style: var(--p-form-field-focus-ring-style);--p-multiselect-focus-ring-color: var(--p-form-field-focus-ring-color);--p-multiselect-focus-ring-offset: var(--p-form-field-focus-ring-offset);--p-multiselect-focus-ring-shadow: var(--p-form-field-focus-ring-shadow);--p-icon-size: .7rem;--p-inputtext-padding-y: .15rem;--p-inputtext-padding-x: .25rem}.easyuse-multiselect .p-multiselect-list{flex-wrap:wrap;flex-direction:row}.comfyui-easyuse .easyuse-multiselect{font-size:11px;--p-multiselect-padding-y: 2px;padding:0 10px;--p-multiselect-border-radius: var(--p-form-field-border-radius);--p-multiselect-placeholder-color: #d4d4d8;--p-multiselect-dropdown-color: var(--input-text);--p-multiselect-border-color: var(--p-form-field-border-color);--p-multiselect-hover-border-color: var(--p-form-field-hover-border-color);--p-multiselect-focus-border-color: var(--p-form-field-focus-border-color)}.easyuse-multiangle-widget{font-size:12px;user-select:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none}.easyuse-multiangle-tabs{overflow-y:auto;margin-bottom:-1px}.tab-item{display:flex;align-items:center;gap:4px;padding:6px 10px;border-radius:6px 6px 0 0;background:transparent;border:1px solid transparent;cursor:pointer;transition:all .2s;font-size:11px;color:var(--p-text-muted-color);margin-bottom:-1px}.tab-item:hover{color:var(--p-primary-color)}.tab-item.active{background:#000;color:var(--p-primary-color);border:1px solid var(--p-content-border-color);border-bottom-color:#000;z-index:2;font-weight:600}.tab-number{font-weight:inherit;font-size:11px}.easyuse-multiangle-content{border:1px solid var(--p-content-border-color);background:var(--p-content-background);border-radius:0 6px 6px;overflow:hidden}.tab-close{font-size:10px;opacity:.7;transition:opacity .2s}.tab-close:hover{opacity:1}.tab-add-btn{display:flex;align-items:center;justify-content:center;width:24px;height:24px;border-radius:4px;background:transparent;border:none;cursor:pointer;transition:all .2s;color:var(--p-text-secondary-color)}.tab-add-btn:hover{border-color:var(--p-primary-color);color:var(--p-primary-color);background:transparent}.tab-add-btn i{font-size:10px}.easyuse-multiangle-widget{font-size:12px;--p-slider-handle-width: 16px;--p-slider-handle-height: 16px;--p-slider-handle-border-radius: 50%;--p-slider-handle-background: var(--p-content-border-color);--p-slider-handle-hover-background: var(--p-content-border-color);--p-slider-handle-focus-ring-width: var(--p-focus-ring-width);--p-slider-handle-focus-ring-style: var(--p-focus-ring-style);--p-slider-handle-focus-ring-color: var(--p-focus-ring-color);--p-slider-handle-focus-ring-offset: var(--p-focus-ring-offset);--p-slider-handle-focus-ring-shadow: var(--p-focus-ring-shadow);--p-slider-handle-content-border-radius: 50%;--p-slider-handle-content-hover-background: var(--p-content-background);--p-slider-handle-content-width: 8px;--p-slider-handle-content-height: 8px;--p-slider-handle-content-shadow: 0px .5px 0px 0px rgba(0, 0, 0, .08), 0px 1px 1px 0px rgba(0, 0, 0, .14);--p-slider-range-background: var(--p-primary-color);--p-slider-track-background: var(--p-content-border-color);--p-slider-track-border-radius: var(--p-content-border-radius);--p-slider-track-size: 3px;--p-slider-transition-duration: var(--p-transition-duration);--p-slider-handle-content-background: var(--p-surface-0)}.easyuse-multiangle-cube-face{border:4px solid var(--p-content-border-color);color:var(--p-text-muted-color);background:rgba(from var(--p-content-color) r g b / .1)}.easyuse-multiangle-cube-face:hover{border-color:var(--p-primary-color);box-shadow:0 0 10px var(--p-primary-color)}.easyuse-multiangle-cube{position:relative;overflow:hidden;cursor:grab;perspective:800px;height:200px;border-radius:0 4px 8px 8px}.easyuse-multiangle-cube:active{cursor:grabbing}.easyuse-multiangle-cube .settings-icon{position:absolute;top:6px;left:6px;color:#ddd;cursor:pointer;z-index:20}.easyuse-multiangle-cube .settings-icon i{font-size:12px}.easyuse-multiangle-cube .settings-icon:hover{color:var(--p-primary-color)}.easyuse-multiangle-cube .settings-dropdown{position:absolute;top:100%;left:0;background:var(--p-content-background);border:1px solid var(--p-content-border-color);border-radius:4px;padding:4px;display:flex;flex-direction:column;gap:4px;min-width:100px;box-shadow:0 2px 10px #0003}.settings-item{display:flex;align-items:center;gap:6px;font-size:10px;color:var(--p-text-color)}.settings-item label{cursor:pointer}.easyuse-multiangle-cube:not(.is-hollow) .easyuse-multiangle-cube-face{background:var(--p-content-background)}.easyuse-cube-face-label{width:24px;height:24px;border-radius:50%;background:#000000e6;display:flex;align-items:center;justify-content:center;font-size:10px;color:#ffffffe6;pointer-events:none;line-height:1}.easyuse-multiangle-cube .reset-icon{position:absolute;top:6px;right:6px;color:#ddd;cursor:pointer}.easyuse-multiangle-cube .reset-icon i{font-size:12px!important}.easyuse-multiangle-cube .reset-icon:hover{color:var(--p-primary-color)}.easyuse-mulitangle-slider{font-size:10px;padding-bottom:14px}.no-scrollbar::-webkit-scrollbar{display:none}.no-scrollbar{-ms-overflow-style:none;scrollbar-width:none}.table-editor-widget[data-v-7e0ef36e]{display:flex;flex-direction:column;width:100%;height:100%;font-size:12px;color:var(--fg-color, #e0e0e0);background:var(--comfy-menu-bg, #1e1e1e);border-radius:6px;overflow:hidden}.tew-toolbar[data-v-7e0ef36e]{display:flex;align-items:center;justify-content:space-between;padding:6px 8px;flex-shrink:0;gap:8px}.tew-mode-tabs[data-v-7e0ef36e]{display:flex;gap:4px;background:var(--comfy-menu-bg, #1a1a1a);padding:2px;border-radius:6px;border:1px solid var(--border-color, #333)}.tew-tab[data-v-7e0ef36e]{display:flex;align-items:center;gap:6px;padding:4px 10px;border:none;border-radius:4px;background:transparent;color:var(--descrip-text, #999);cursor:pointer;font-size:11px;font-weight:500;transition:all .2s ease}.tew-tab[data-v-7e0ef36e]:hover{color:var(--fg-color, #fff)}.tew-tab.active[data-v-7e0ef36e]{background:var(--comfy-input-bg, #333);color:var(--p-primary-color, #4a9eff);box-shadow:0 1px 2px #0003}.tew-actions[data-v-7e0ef36e]{display:flex;gap:6px}.tew-btn[data-v-7e0ef36e]{display:flex;align-items:center;gap:4px;padding:4px 8px;border:1px solid var(--border-color, #444);border-radius:4px;background:var(--comfy-menu-bg, #2a2a2a);color:var(--fg-color, #ccc);cursor:pointer;font-size:11px;transition:all .2s ease}.tew-btn[data-v-7e0ef36e]:hover{background:var(--comfy-input-bg, #3a3a3a);border-color:var(--p-primary-color, #4a9eff);color:var(--p-primary-color, #4a9eff)}.tew-table-wrapper[data-v-7e0ef36e]{flex:1;overflow:auto;padding:8px;background:var(--comfy-menu-bg, #1e1e1e)}.tew-table[data-v-7e0ef36e]{border-collapse:separate;border-spacing:0;table-layout:fixed;width:auto;border:1px solid var(--border-color, #333);border-radius:4px;overflow:hidden}.tew-row-handle[data-v-7e0ef36e]{width:24px;padding:0;text-align:center;background:var(--comfy-input-bg, #252525);border-bottom:1px solid var(--border-color, #333);border-right:1px solid var(--border-color, #333)}.tew-header-cell[data-v-7e0ef36e]{padding:4px 6px;border-bottom:1px solid var(--border-color, #333);border-right:1px solid var(--border-color, #333);background:var(--comfy-input-bg, #252525)}.tew-header-cell[data-v-7e0ef36e]:last-child{border-right:none}.tew-cell-inner[data-v-7e0ef36e]{display:flex;align-items:center;gap:4px}.tew-body-cell[data-v-7e0ef36e]{padding:0;border-bottom:1px solid var(--border-color, #333);border-right:1px solid var(--border-color, #333);background:var(--comfy-menu-bg, #1e1e1e);transition:background .2s}.tew-body-cell[data-v-7e0ef36e]:last-child{vertical-align:top;border-right:none}.tew-table tbody tr:last-child .tew-body-cell[data-v-7e0ef36e],.tew-table tbody tr:last-child .tew-row-handle[data-v-7e0ef36e]{border-bottom:none}.tew-table tbody tr:hover .tew-body-cell[data-v-7e0ef36e]{background:var(--comfy-input-bg, #2a2a2a)}.tew-cell-input[data-v-7e0ef36e]{width:100%;background:transparent;border:1px solid transparent;outline:none;color:var(--fg-color, #e0e0e0);font-size:12px;padding:6px 8px;box-sizing:border-box;transition:all .2s}.tew-cell-input[data-v-7e0ef36e]:focus{background:var(--comfy-input-bg, #252535);border-color:var(--p-primary-color, #4a9eff)}.tew-cell-textarea[data-v-7e0ef36e]{resize:none;min-height:28px;font-family:inherit;line-height:1.4;vertical-align:top;overflow:hidden;box-sizing:border-box;white-space:pre-wrap;word-break:break-word;overflow-wrap:break-word}.tew-header-input[data-v-7e0ef36e]{font-weight:600;color:var(--p-primary-color, #6ab0ff);flex:1;padding:4px 6px}.tew-col-del[data-v-7e0ef36e],.tew-row-del[data-v-7e0ef36e]{display:flex;align-items:center;justify-content:center;width:18px;height:18px;border-radius:3px;background:transparent;border:none;cursor:pointer;color:var(--error-text, #ff6b6b);font-size:14px;line-height:1;opacity:0;transition:all .2s;flex-shrink:0}.tew-header-cell:hover .tew-col-del[data-v-7e0ef36e],.tew-table tbody tr:hover .tew-row-del[data-v-7e0ef36e]{opacity:.6}.tew-col-del[data-v-7e0ef36e]:hover,.tew-row-del[data-v-7e0ef36e]:hover{opacity:1!important;background:#ff6b6b26}.tew-md-wrapper[data-v-7e0ef36e]{flex:1;display:flex;flex-direction:column;padding:8px;overflow:hidden;background:var(--comfy-menu-bg, #1e1e1e)}.tew-md-textarea[data-v-7e0ef36e]{flex:1;width:100%;background:var(--comfy-input-bg, #151515);border:1px solid var(--border-color, #333);border-radius:4px;color:var(--fg-color, #d4d4d4);font-family:JetBrains Mono,Consolas,Monaco,Courier New,monospace;font-size:12px;line-height:1.6;background:var(--comfy-input-bg, #252525);border-top:1px solid var(--border-color, #3a3a3a);flex-shrink:0}.tew-output-label[data-v-7e0ef36e]{color:var(--p-primary-color, #4a9eff);font-weight:500;opacity:.8}img[data-v-604a4248]{transition:transform 1s cubic-bezier(.2,0,.4,1)}.lazy-image-container[data-v-4bfc03bf]{position:relative;width:100%;height:100%}.lazy-image-placeholder[data-v-4bfc03bf],.lazy-image-error[data-v-4bfc03bf]{transition:background-color .2s ease}img[data-v-4bfc03bf]{transition:opacity .3s ease}.selector-card-wrapper[data-v-88af28b0]{width:100%}.card-item[data-v-88af28b0]{border:1px solid var(--border-color);border-radius:var(--border-radius);background:var(--comfy-menu-bg);border-radius:8px;overflow:hidden;transition:all .2s ease;box-shadow:0 1px 3px #0000001a}.dark .card-item[data-v-88af28b0]{background-color:var(--surface-900);border-color:var(--surface-700)}.card-item[data-v-88af28b0]:hover{box-shadow:0 4px 12px #00000026;border:1px solid var(--input-text)}.card-item.clickable[data-v-88af28b0]{cursor:pointer}.card-item.selected[data-v-88af28b0]{border:1px solid var(--success-color, #4CAF50)}.card-image-container[data-v-88af28b0]{position:relative}.card-image[data-v-88af28b0]{width:100%;height:80px;object-fit:cover;transition:transform .2s ease;background:var(--bg-color)}.card-item:hover .card-image[data-v-88af28b0]{transform:scale(1.05)}.selection-badge[data-v-88af28b0]{position:absolute;top:4px;right:4px;width:10px;height:10px;background-color:var(--success-color, #4CAF50);border-radius:50%;display:flex;align-items:center;justify-content:center;box-shadow:0 2px 4px #0000004d;z-index:10;animation:scaleIn-88af28b0 .2s ease-out}.selection-badge i[data-v-88af28b0]{font-size:6px;font-weight:700;color:#fff}@keyframes scaleIn-88af28b0{0%{transform:scale(0);opacity:0}to{transform:scale(1);opacity:1}}.card-actions[data-v-88af28b0]{position:absolute;top:8px;left:8px;display:flex;gap:4px;opacity:0;transition:opacity .2s ease}.card-item:hover .card-actions[data-v-88af28b0]{opacity:1}.card-content[data-v-88af28b0]{padding:2px 4px}.card-title[data-v-88af28b0]{color:var(--input-text);font-size:8px;font-weight:400;display:-webkit-box;-webkit-line-clamp:2;line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;line-height:1.25;transition:color .2s ease}.dark .card-title[data-v-88af28b0]{color:var(--surface-0)}.card-item.selected .card-title[data-v-88af28b0]{color:var(--primary-600);font-weight:500}.dark .card-item.selected .card-title[data-v-88af28b0]{color:var(--primary-300)}.list-item[data-v-c93b1734]{display:inline-block;position:relative}.list-item__tag[data-v-c93b1734]{display:inline-block;vertical-align:middle;margin-top:8px;margin-right:8px;padding:4px;color:var(--input-text);background-color:var(--bg-color);border-radius:var(--border-radius);border:1px solid var(--border-color);font-size:11px;cursor:pointer;overflow:hidden;position:relative;transition:all .2s ease}.list-item__tag[data-v-c93b1734]:hover{filter:brightness(1.2)}.list-item__tag input[type=checkbox][data-v-c93b1734]{--ring-color: transparent;position:relative;box-shadow:none;background:linear-gradient(135deg,var(--comfy-menu-bg) 0%,var(--comfy-input-bg) 60%);display:inline-block;flex-shrink:0;vertical-align:middle;-webkit-appearance:none;-moz-appearance:none;appearance:none;border:1px solid var(--border-color);background-origin:border-box;padding:0;width:1rem;height:1rem;border-radius:4px;color:var(--theme-color);-webkit-user-select:none;user-select:none;margin-right:4px}.list-item__tag input[type=checkbox][data-v-c93b1734]:checked{border:1px solid var(--theme-color);background-color:var(--theme-color);background-image:url("data:image/svg+xml,%3csvg viewBox=\'0 0 16 16\' fill=\'white\' xmlns=\'http://www.w3.org/2000/svg\'%3e%3cpath d=\'M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z\'/%3e%3c/svg%3e")}.list-item-content[data-v-c93b1734]{margin:0 4px;vertical-align:middle;display:inline-block}.list-title[data-v-c93b1734]{color:inherit;font-size:10px;font-weight:500;margin:0;line-height:1.4;word-break:break-word;display:block}.list-description[data-v-c93b1734]{color:var(--input-text);font-size:10px;line-height:1.4;margin:2px 0 0;opacity:.8;display:-webkit-box;-webkit-line-clamp:2;line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}@media (max-width: 768px){.list-item__tag[data-v-c93b1734]{padding:6px;margin-right:6px;margin-top:6px}}@media (max-width: 480px){.list-item__tag[data-v-c93b1734]{padding:4px;margin-right:4px;margin-top:4px}.list-title[data-v-c93b1734]{font-size:11px}.list-description[data-v-c93b1734]{font-size:9px}}.easyuse-styles-selector{position:relative;height:calc(100% - 10px);--p-inputtext-padding-y: 4px;--p-form-field-padding-x: 6px;--p-button-sm-padding-y: 0px;--p-icon-size: 12px;--p-button-icon-only-width: 24px;--p-button-sm-font-size: 12px;--p-togglebutton-content-left: 2px;--p-togglebutton-content-top: 2px}.easyuse-styles-selector .p-inputtext{font-size:11px}.easyuse-styles-selector .p-togglebutton{padding:0 6px}.easyuse-styles-selector-content{list-style:none;padding:0;margin:0;min-height:150px;height:calc(100% - 12px);overflow-y:auto;overflow-x:hidden}.easyuse-styles-selector-content .grid-container{display:grid;grid-template-columns:repeat(auto-fill,minmax(80px,1fr));padding:4px 0;gap:8px}.easyuse-styles-selector-content .list-container{list-style:none;padding:6px 0;margin:0;display:flex;flex-wrap:wrap}.easyuse-styles-selector-previewer{position:absolute;top:0;left:-150px;width:130px;padding:8px;z-index:2;border-radius:var(--border-radius);background:var(--comfy-menu-bg);overflow:hidden;box-shadow:0 4px 12px #00000026}.easyuse-styles-selector-previewer__text{color:var(--input-text);word-wrap:break-word}.easyuse-styles-selector-previewer__text .title{line-height:1;font-size:10px}.easyuse-styles-selector-previewer__text h6{line-height:1;font-size:8px;margin:8px 0;white-space:wrap}.easyuse-styles-selector-previewer__text h6 .comfyui-easyuse-success{color:#22c55e;font-weight:600}.easyuse-styles-selector-previewer__text h6 .comfyui-easyuse-error{color:#ef4444;font-weight:600}.easyuse-styles-selector-previewer__text h6 span:last-child{color:var(--input-text);font-weight:400;margin-left:4px}\n@charset "UTF-8";.easyuse-model-info{color:#fff;max-width:90vw;font-family:var(--font-family)}.easyuse-model-content{display:flex;flex-direction:column;overflow:hidden}.easyuse-model-header{margin:0 0 15px}.easyuse-model-header-remark{display:flex;align-items:center;margin-top:5px}.easyuse-model-info h2{text-align:left;margin:0}.easyuse-model-info h5{text-align:left;margin:0 15px 0 0;font-weight:400;color:var(--descrip-text)}.easyuse-model-info p{margin:5px 0}.easyuse-model-info a{color:var(--theme-color-light)}.easyuse-model-info a:hover{text-decoration:underline}.easyuse-model-tags-list{display:flex;flex-wrap:wrap;list-style:none;gap:10px;max-height:200px;overflow:auto;margin:10px 0;padding:0}.easyuse-model-tag{background-color:var(--comfy-input-bg);border:2px solid var(--border-color);color:var(--input-text);display:flex;align-items:center;gap:5px;border-radius:5px;padding:2px 5px;cursor:pointer}.easyuse-model-tag--selected span:before{content:"✅";position:absolute;background-color:var(--theme-color-light);left:0;top:0;right:0;bottom:0;text-align:center}.easyuse-model-tag:hover{border:2px solid var(--theme-color-light)}.easyuse-model-tag p{margin:0}.easyuse-model-tag span{text-align:center;border-radius:5px;background-color:var(--theme-color-light);padding:2px;position:relative;min-width:20px;overflow:hidden;color:#fff}.easyuse-model-metadata .comfy-modal-content{max-width:100%}.easyuse-model-metadata label{margin-right:1ch;color:#ccc}.easyuse-model-metadata span{color:var(--theme-color-light)}.easyuse-preview{max-width:660px;margin-right:15px;position:relative}.easyuse-preview-group{position:relative;overflow:hidden;border-radius:.5rem;width:660px}.easyuse-preview-list{display:flex;flex-wrap:nowrap;width:100%;transition:all .5s ease-in-out}.easyuse-preview-list.no-transition{transition:none}.easyuse-preview-slide{display:flex;flex-basis:calc(50% - 5px);flex-grow:0;flex-shrink:0;position:relative;justify-content:center;align-items:center;padding-right:5px;padding-left:0}.easyuse-preview-slide:nth-child(2n){padding-left:5px;padding-right:0}.easyuse-preview-slide-content{position:relative;min-height:150px;width:100%}.easyuse-preview-slide-content .save{position:absolute;right:6px;z-index:12;bottom:6px;display:flex;align-items:center;height:26px;padding:0 9px;color:var(--input-text);font-size:12px;line-height:26px;background:#00000080;border-radius:13px;cursor:pointer;min-width:80px;text-align:center}.easyuse-preview-slide-content .save:hover{filter:brightness(120%);will-change:auto}.easyuse-preview-slide-content img{border-radius:14px;object-position:center center;max-width:100%;max-height:700px;border-style:none;vertical-align:middle}.easyuse-preview button{position:absolute;z-index:10;top:50%;display:flex;align-items:center;justify-content:center;width:30px;height:30px;border-radius:15px;border:1px solid rgba(66,63,78,.15);background-color:#423f4e80;color:#fffc;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transform:translateY(-50%)}.easyuse-preview button.left{left:10px}.easyuse-preview button.right{right:10px}.easyuse-model-detail{margin-top:16px;overflow:hidden;border:1px solid var(--border-color);border-radius:8px;width:300px}.easyuse-model-detail-head{height:40px;padding:0 10px;font-weight:500;font-size:14px;font-style:normal;line-height:40px}.easyuse-model-detail-body{box-sizing:border-box;font-size:12px}.easyuse-model-detail-item{display:flex;justify-content:flex-start;border-top:1px solid var(--border-color)}.easyuse-model-detail-item-label{flex-shrink:0;width:88px;padding-top:5px;padding-bottom:5px;padding-left:10px;border-right:1px solid var(--border-color);color:var(--input-text);font-weight:400}.easyuse-model-detail-item-value{display:flex;flex-wrap:wrap;padding:5px 10px;color:var(--input-text)}.easyuse-model-detail-textarea{border-top:1px solid var(--border-color);padding:10px;height:100px;overflow-y:auto;font-size:12px}.easyuse-model-detail-textarea textarea{width:100%;height:100%;border:0;background-color:transparent;color:var(--input-text)}.easyuse-model-detail-textarea textarea::placeholder{color:var(--descrip-text)}.easyuse-model-detail-textarea.empty{display:flex;justify-content:center;align-items:center;color:var(--descrip-text)}.easyuse-model-notes{background-color:#00000040;padding:5px;margin-top:5px}.easyuse-model-notes:empty{display:none}.easyuse-account-user{font-size:10px;color:var(--descrip-text);text-align:center}.easyuse-account-user-info{display:flex;justify-content:space-between;align-items:center;padding-bottom:10px;cursor:pointer}.easyuse-account-user-info .user{display:flex;align-items:center}.easyuse-account-user-info .edit{padding:5px 10px;background:var(--comfy-menu-bg);border-radius:4px}.easyuse-account-user-info:hover{filter:brightness(110%)}.easyuse-account-user-info h5{margin:0;font-size:10px;text-align:left}.easyuse-account-user-info h6{margin:0;font-size:8px;text-align:left;font-weight:300}.easyuse-account-user-info .remark{margin-top:4px}.easyuse-account-user-info .avatar{width:36px;height:36px;background:var(--comfy-input-bg);border-radius:50%;margin-right:5px;display:flex;justify-content:center;align-items:center;font-size:16px;overflow:hidden}.easyuse-account-user-info .avatar img{width:100%;height:100%}.easyuse-account-dialog{width:600px}.easyuse-account-dialog-main a,.easyuse-account-dialog-main a:visited{font-weight:400;color:var(--theme-color-light)}.easyuse-account-dialog-item{display:flex;justify-content:flex-start;align-items:center;padding:10px 0;border-bottom:1px solid var(--border-color)}.easyuse-account-dialog-item input{padding:5px;margin-right:5px}.easyuse-account-dialog-item input.key{flex:1}.easyuse-account-dialog-item button{cursor:pointer;margin-left:5px!important;padding:5px!important;font-size:16px!important}.easyuse-account-dialog-item button:hover{filter:brightness(120%)}.easyuse-account-dialog-item button.choose{background:var(--theme-color)}.easyuse-account-dialog-item button.delete{background:var(--error-color)}.easy-dropdown,.easy-nested-dropdown{position:relative;box-sizing:border-box;background-color:#171717;box-shadow:0 4px 4px #ffffff40;padding:0;margin:0;list-style:none;z-index:1000;overflow:visible;max-height:fit-content;max-width:fit-content}.easy-dropdown{position:absolute;border-radius:0}.easy-dropdown li.item,.easy-nested-dropdown li.item{font-weight:400;min-width:max-content}.easy-dropdown li.folder,.easy-nested-dropdown li.folder{cursor:default;position:relative;border-right:3px solid cyan}.easy-dropdown li.folder:after,.easy-nested-dropdown li.folder:after{content:">";position:absolute;right:2px;font-weight:400}.easy-dropdown li,.easy-nested-dropdown li{padding:4px 10px;cursor:pointer;font-family:system-ui;font-size:.7rem;position:relative}.easy-nested-dropdown{position:absolute;top:0;left:100%;margin:0;border:none;display:none}.easy-dropdown li.selected>.easy-nested-dropdown,.easy-nested-dropdown li.selected>.easy-nested-dropdown{display:block;border:none}.easy-dropdown li.selected,.easy-nested-dropdown li.selected{background-color:#e5e5e5;border:none}#crystools-root-easyuse{flex-direction:row;justify-content:center;flex-shrink:1;width:min-content;min-width:max-content;height:100%;margin:0 auto}#crystools-root-easyuse .crystools-monitor-container{width:100%;cursor:crosshair;display:flex;flex-wrap:wrap;flex-direction:row;justify-content:flex-end;gap:5px;height:100%}#crystools-root-easyuse .crystools-monitor-container .crystools-monitor{background-color:var(--comfy-input-bg);position:relative;align-items:center;flex-direction:row;width:40px;height:100%}#crystools-root-easyuse .crystools-monitor-container .crystools-monitor .crystools-text{font-size:10px;text-align:right;margin-left:3px;position:absolute;font-weight:100;bottom:2px;z-index:10}#crystools-root-easyuse .crystools-monitor-container .crystools-content{position:relative;height:100%}#crystools-root-easyuse .crystools-monitor-container .crystools-slider{position:absolute;height:100%;width:0;box-shadow:inset 2px 2px 10px #0003}#crystools-root-easyuse .crystools-monitor-container .crystools-label{position:relative;width:100%;color:var(--input-text);font-weight:500;font-size:11px;left:2px;top:4px;text-align:right}:root{--theme-color:var(--p-primary-color, #60a5fa);--theme-color-light: var(--p-primary-hover-color, #93c5fd);--success-color: #52c41a;--error-color: #ff4d4f;--warning-color: #faad14;--font-family: Inter, -apple-system, BlinkMacSystemFont, Helvetica Neue, sans-serif;--p-inputtext-background: var(--p-form-field-background);--p-inputtext-disabled-background: var(--p-form-field-disabled-background);--p-inputtext-filled-background: var(--p-form-field-filled-background);--p-inputtext-filled-focus-background: var(--p-form-field-filled-focus-background);--p-inputtext-border-color: var(--p-form-field-border-color);--p-inputtext-hover-border-color: var(--p-form-field-hover-border-color);--p-inputtext-focus-border-color: var(--p-form-field-focus-border-color);--p-inputtext-invalid-border-color: var(--p-form-field-invalid-border-color);--p-inputtext-color: var(--p-form-field-color);--p-inputtext-disabled-color: var(--p-form-field-disabled-color);--p-inputtext-placeholder-color: var(--p-form-field-placeholder-color);--p-inputtext-shadow: var(--p-form-field-shadow);--p-inputtext-padding-x: var(--p-form-field-padding-x);--p-inputtext-padding-y: var(--p-form-field-padding-y);--p-inputtext-border-radius: var(--p-form-field-border-radius);--p-inputtext-focus-ring-width: var(--p-form-field-focus-ring-width);--p-inputtext-focus-ring-style: var(--p-form-field-focus-ring-style);--p-inputtext-focus-ring-color: var(--p-form-field-focus-ring-color);--p-inputtext-focus-ring-offset: var(--p-form-field-focus-ring-offset);--p-inputtext-focus-ring-shadow: var(--p-form-field-focus-ring-shadow);--p-inputtext-transition-duration: var(--p-form-field-transition-duration);--p-inputtext-sm-font-size: .875rem;--p-inputtext-sm-padding-x: .625rem;--p-inputtext-sm-padding-y: .375rem;--p-inputtext-lg-font-size: 1.125rem;--p-inputtext-lg-padding-x: .875rem;--p-inputtext-lg-padding-y: .625rem;--p-tooltip-max-width: 12.5rem;--p-tooltip-gutter: .25rem;--p-tooltip-shadow: var(--p-overlay-popover-shadow);--p-tooltip-padding: .5rem .75rem;--p-tooltip-border-radius: var(--p-overlay-popover-border-radius);--p-tooltip-background: var(--p-surface-700);--p-tooltip-color: var(--p-surface-0)}.comfyui-easyuse-theme,.comfyui-easyuse-primary{color:var(--theme-color-light)}.comfyui-easyuse-theme.point:hover,.comfyui-easyuse-primary.point:hover{opacity:.8}.comfyui-easyuse-success{color:var(--success-color)}.comfyui-easyuse-success.point:hover{opacity:.8}.comfyui-easyuse-error{color:var(--error-color)}.comfyui-easyuse-error.point:hover{opacity:.8}.comfyui-easyuse-warning,.comfyui-easyuse--warn{color:var(--warning-color)}.comfyui-easyuse-warning.point:hover,.comfyui-easyuse--warn.point:hover{opacity:.8}.flex-row{flex-direction:row}.flex-row-reverse{flex-direction:row-reverse}.flex-col{flex-direction:column}.flex-col-reverse{flex-direction:column-reverse}.flex-wrap{flex-wrap:wrap}.flex-nowrap{flex-wrap:nowrap}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.items-stretch{align-items:stretch}.justify-start{justify-content:flex-start}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.grid{display:grid}.gap-0\\.5{gap:.125rem}.gap-1{gap:.25rem}.gap-10{gap:2.5rem}.gap-12{gap:3rem}.gap-2{gap:.5rem}.gap-2\\.5{gap:.625rem}.gap-20{gap:5rem}.gap-3{gap:.75rem}.gap-3\\.5{gap:.875rem}.gap-4{gap:1rem}.gap-5{gap:1.25rem}.gap-6{gap:1.5rem}.gap-7{gap:1.75rem}.gap-8{gap:2rem}.gap-\\[0\\.28rem\\]{gap:.28rem}.gap-x-2{-moz-column-gap:.5rem;column-gap:.5rem}.gap-x-4{-moz-column-gap:1rem;column-gap:1rem}.gap-y-1{row-gap:.25rem}.gap-y-6{row-gap:1.5rem}@media (min-width: 576px){.sm\\:col-span-4{grid-column:span 4/span 4}.sm\\:col-span-6{grid-column:span 6/span 6}.sm\\:ml-8{margin-left:2rem}.sm\\:mt-0{margin-top:0}.sm\\:flex{display:flex}.sm\\:h-60{height:15rem}.sm\\:\\!w-64{width:16rem!important}.sm\\:w-40{width:10rem}.sm\\:w-44{width:11rem}.sm\\:w-56{width:14rem}.sm\\:w-60{width:15rem}.sm\\:w-64{width:16rem}.sm\\:w-80{width:20rem}.sm\\:w-96{width:24rem}.sm\\:w-\\[30rem\\]{width:30rem}.sm\\:w-auto{width:auto}.sm\\:min-w-\\[30rem\\]{min-width:30rem}.sm\\:flex-row{flex-direction:row}.sm\\:flex-col{flex-direction:column}.sm\\:flex-nowrap{flex-wrap:nowrap}.sm\\:items-start{align-items:flex-start}.sm\\:items-end{align-items:flex-end}.sm\\:items-center{align-items:center}.sm\\:justify-center{justify-content:center}.sm\\:justify-between{justify-content:space-between}.sm\\:gap-2{gap:.5rem}.sm\\:p-20{padding:5rem}.sm\\:px-10{padding-left:2.5rem;padding-right:2.5rem}.sm\\:py-10{padding-bottom:2.5rem;padding-top:2.5rem}.sm\\:py-5{padding-bottom:1.25rem;padding-top:1.25rem}.sm\\:pt-32{padding-top:8rem}.sm\\:text-left{text-align:left}.sm\\:text-sm{font-size:.875rem;line-height:1.25rem}}@media (min-width: 768px){.md\\:-bottom-12{bottom:-3rem}.md\\:-bottom-28{bottom:-7rem}.md\\:-bottom-8{bottom:-2rem}.md\\:-bottom-\\[26rem\\]{bottom:-26rem}.md\\:-left-12{left:-3rem}.md\\:-left-28{left:-7rem}.md\\:-left-32{left:-8rem}.md\\:-left-4{left:-1rem}.md\\:-left-48{left:-12rem}.md\\:-left-\\[22rem\\]{left:-22rem}.md\\:bottom-0{bottom:0}.md\\:left-10{left:2.5rem}.md\\:left-\\[32rem\\]{left:32rem}.md\\:left-\\[42rem\\]{left:42rem}.md\\:top-1\\/2{top:50%}.md\\:top-32{top:8rem}.md\\:top-8{top:2rem}.md\\:col-span-2{grid-column:span 2/span 2}.md\\:col-span-4{grid-column:span 4/span 4}.md\\:col-span-6{grid-column:span 6/span 6}.md\\:ml-auto{margin-left:auto}.md\\:block{display:block}.md\\:flex{display:flex}.md\\:hidden{display:none}.md\\:h-\\[20rem\\]{height:20rem}.md\\:h-\\[32rem\\]{height:32rem}.md\\:\\!w-80{width:20rem!important}.md\\:w-2\\/12{width:16.666667%}.md\\:w-40{width:10rem}.md\\:w-5\\/12{width:41.666667%}.md\\:w-56{width:14rem}.md\\:w-6\\/12{width:50%}.md\\:w-60{width:15rem}.md\\:w-8\\/12{width:66.666667%}.md\\:w-80{width:20rem}.md\\:w-\\[100rem\\]{width:100rem}.md\\:w-\\[26rem\\]{width:26rem}.md\\:w-\\[30rem\\]{width:30rem}.md\\:w-\\[50rem\\]{width:50rem}.md\\:w-\\[52rem\\]{width:52rem}.md\\:w-\\[60rem\\]{width:60rem}.md\\:w-\\[95rem\\]{width:95rem}.md\\:w-screen{width:100vw}.md\\:flex-initial{flex:0 1 auto}.md\\:-translate-y-1\\/2{--tw-translate-y: -50% }.md\\:-translate-y-1\\/2,.md\\:translate-x-0{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.md\\:translate-x-0{--tw-translate-x: 0px }.md\\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\\:flex-row{flex-direction:row}.md\\:flex-col{flex-direction:column}.md\\:items-end{align-items:flex-end}.md\\:items-center{align-items:center}.md\\:justify-start{justify-content:flex-start}.md\\:justify-center{justify-content:center}.md\\:gap-20{gap:5rem}.md\\:gap-4{gap:1rem}.md\\:p-5{padding:1.25rem}.md\\:p-8{padding:2rem}}@media (min-width: 992px){.lg\\:left-20{left:5rem}.lg\\:left-\\[36rem\\]{left:36rem}.lg\\:left-\\[50rem\\]{left:50rem}.lg\\:col-span-1{grid-column:span 1/span 1}.lg\\:col-span-2{grid-column:span 2/span 2}.lg\\:col-span-4{grid-column:span 4/span 4}.lg\\:col-span-6{grid-column:span 6/span 6}.lg\\:mb-0{margin-bottom:0}.lg\\:mt-0{margin-top:0}.lg\\:mt-20{margin-top:5rem}.lg\\:flex{display:flex}.lg\\:hidden{display:none}.lg\\:h-10{height:2.5rem}.lg\\:h-32{height:8rem}.lg\\:h-\\[28rem\\]{height:28rem}.lg\\:\\!w-\\[30rem\\]{width:30rem!important}.lg\\:w-3\\/12{width:25%}.lg\\:w-32{width:8rem}.lg\\:w-\\[28rem\\]{width:28rem}.lg\\:w-\\[64rem\\]{width:64rem}.lg\\:w-fit{width:-moz-fit-content;width:fit-content}.lg\\:max-w-6xl{max-width:72rem}.lg\\:flex-row{flex-direction:row}.lg\\:gap-0{gap:0}.lg\\:rounded-2xl{border-radius:1rem}.lg\\:rounded-3xl{border-radius:1.5rem}.lg\\:rounded-xl{border-radius:.75rem}.lg\\:p-7{padding:1.75rem}.lg\\:px-2{padding-left:.5rem;padding-right:.5rem}.lg\\:px-20{padding-left:5rem;padding-right:5rem}.lg\\:px-56{padding-left:14rem;padding-right:14rem}.lg\\:px-8{padding-left:2rem;padding-right:2rem}.lg\\:px-9{padding-left:2.25rem;padding-right:2.25rem}.lg\\:py-20{padding-bottom:5rem;padding-top:5rem}.lg\\:py-7{padding-bottom:1.75rem;padding-top:1.75rem}.lg\\:pt-0{padding-top:0}.lg\\:text-2xl{font-size:1.5rem;line-height:2rem}.lg\\:text-5xl{font-size:3rem;line-height:1}.lg\\:text-base{font-size:1rem;line-height:1.5rem}.lg\\:text-xl{font-size:1.25rem;line-height:1.75rem}}@media (min-width: 1200px){.xl\\:-left-12{left:-3rem}.xl\\:-left-28{left:-7rem}.xl\\:bottom-0{bottom:0}.xl\\:left-36{left:9rem}.xl\\:left-\\[42rem\\]{left:42rem}.xl\\:left-\\[60rem\\]{left:60rem}.xl\\:col-span-3{grid-column:span 3/span 3}.xl\\:col-span-4{grid-column:span 4/span 4}.xl\\:col-span-6{grid-column:span 6/span 6}.xl\\:block{display:block}.xl\\:flex{display:flex}.xl\\:hidden{display:none}.xl\\:h-\\[36\\.25rem\\]{height:36.25rem}.xl\\:\\!w-40{width:10rem!important}.xl\\:w-3\\/12{width:25%}.xl\\:w-6\\/12{width:50%}.xl\\:w-96{width:24rem}.xl\\:w-\\[29rem\\]{width:29rem}.xl\\:max-w-36{max-width:9rem}.xl\\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.xl\\:flex-row{flex-direction:row}.xl\\:items-start{align-items:flex-start}.xl\\:items-center{align-items:center}.xl\\:gap-1{gap:.25rem}.xl\\:gap-6{gap:1.5rem}.xl\\:text-left{text-align:left}}.p-blockui-mask.p-overlay-mask-leave{display:none!important}.comfyui-easyuse-toast{position:fixed;z-index:99999;top:0;left:0;height:0;width:100%;display:flex;flex-direction:column;align-items:center;justify-content:start}.comfyui-easyuse-toast-container{position:relative;height:fit-content;padding:4px;margin-top:-100px;opacity:0;z-index:3;-webkit-transition:all .3s ease-in-out;-khtml-transition:all .3s ease-in-out;-moz-transition:all .3s ease-in-out;-ms-transition:all .3s ease-in-out;-o-transition:all .3s ease-in-out;transition:all .3s ease-in-out}.comfyui-easyuse-toast-container:last-child{z-index:1}.comfyui-easyuse-toast-container.show{opacity:1;margin-top:0!important;transform:translateY(0)}.comfyui-easyuse-toast-container:not(.show){z-index:1}.comfyui-easyuse-toast-container>div{position:relative;background:var(--comfy-menu-bg);color:var(--input-text);height:fit-content;box-shadow:0 0 10px #000000e0;padding:9px 12px;border-radius:var(--border-radius);font-size:14px;pointer-events:all;margin-top:40px;display:-webkit-box;display:-moz-box;display:-ms-flexbox;display:-webkit-flex;display:flex;-webkit-justify-content:center;-khtml-justify-content:center;-moz-justify-content:center;-ms-justify-content:center;-o-justify-content:center;justify-content:center;-webkit-align-items:center;-khtml-align-items:center;-moz-align-items:center;-ms-align-items:center;-o-align-items:center;align-items:center}.comfyui-easyuse-toast-container>div>span{display:-webkit-box;display:-moz-box;display:-ms-flexbox;display:-webkit-flex;display:flex;-webkit-justify-content:center;-khtml-justify-content:center;-moz-justify-content:center;-ms-justify-content:center;-o-justify-content:center;justify-content:center;-webkit-align-items:center;-khtml-align-items:center;-moz-align-items:center;-ms-align-items:center;-o-align-items:center;align-items:center}.comfyui-easyuse-toast-container>div>span i{font-size:16px;margin-right:8px}.comfyui-easyuse-toast-container>div>span i.loading{animation:loading-rotate 1s linear infinite}.comfyui-easyuse-toast-container a{cursor:pointer;text-decoration:underline;color:var(--theme-color-light);margin-left:4px;display:inline-block;line-height:1}.comfyui-easyuse-toast-container a:hover{color:var(--theme-color-light);text-decoration:none}@keyframes loading-rotate{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.comfyui-easyuse-chooser-dialog{max-width:600px}.comfyui-easyuse-chooser-dialog-title{font-size:18px;font-weight:700;text-align:center;color:var(--input-text);margin:0}.comfyui-easyuse-chooser-dialog-images{margin-top:10px;display:flex;flex-wrap:wrap;width:100%;box-sizing:border-box}.comfyui-easyuse-chooser-dialog-images img{width:50%;height:auto;cursor:pointer;box-sizing:border-box;filter:brightness(80%)}.comfyui-easyuse-chooser-dialog-images img:hover{filter:brightness(100%)}.comfyui-easyuse-chooser-dialog-images img.selected{border:4px solid var(--success-color)}.comfyui-easyuse-chooser-dialog-hidden{display:none;height:0}.comfyui-easyuse-contextmenu{--height: 26px;--padding: 8px;font-family:var(--font-family);position:fixed;top:0;left:0;width:100%;max-width:200px;min-width:100px;min-height:100px;padding:var(--padding) 0;box-shadow:0 0 10px #00000040;background-color:var(--tr-odd-bg-color);border-radius:var(--border-radius);z-index:10;will-change:transform}.comfyui-easyuse-contextmenu-item-divider{height:1px;width:100%;background-color:var(--border-color);margin:var(--padding) 0}.comfyui-easyuse-contextmenu-item-content{height:var(--height);padding:0 12px;cursor:pointer;display:-webkit-box;display:-moz-box;display:-ms-flexbox;display:-webkit-flex;display:flex;-webkit-justify-content:space-between;-khtml-justify-content:space-between;-moz-justify-content:space-between;-ms-justify-content:space-between;-o-justify-content:space-between;justify-content:space-between;-webkit-align-items:center;-khtml-align-items:center;-moz-align-items:center;-ms-align-items:center;-o-align-items:center;align-items:center}.comfyui-easyuse-contextmenu-item-content span{font-size:11px;color:var(--input-text);display:-webkit-box;-webkit-line-clamp:1;overflow:hidden;word-break:break-all;text-overflow:ellipsis;-webkit-box-orient:vertical;-webkit-box-flex:1;-ms-flex:1;-webkit-flex:1;flex:1}.comfyui-easyuse-contextmenu-item-content i{color:var(--input-text);margin-left:4px;display:block;width:0;height:0;border-width:4px 4px 0;border-style:solid;border-color:var(--descrip-text) transparent transparent;-webkit-transform:scaleY(.8) rotate(-90deg);-khtml-transform:scaleY(.8) rotate(-90deg);-moz-transform:scaleY(.8) rotate(-90deg);-ms-transform:scaleY(.8) rotate(-90deg);-o-transform:scaleY(.8) rotate(-90deg);transform:scaleY(.8) rotate(-90deg)}.comfyui-easyuse-contextmenu-item-content:hover{background:var(--theme-color)}.comfyui-easyuse-contextmenu-item.disabled .comfyui-easyuse-contextmenu-item-content span{color:var(--border-color);cursor:default}.comfyui-easyuse-contextmenu-item.disabled .comfyui-easyuse-contextmenu-item-content:hover{background:transparent}.comfyui-easyuse-contextmenu-submenu{font-family:var(--font-family);position:absolute;top:0;left:200px;max-width:200px;width:200px;min-width:100px;min-height:--height;padding:var(--padding) 0;box-shadow:0 0 10px #00000040;background-color:var(--tr-odd-bg-color);border-radius:var(--border-radius);z-index:10;will-change:transform}.comfyui-easyuse-contextmenu-model{position:relative}#easyuse-model-thumbnail{display:none;position:fixed;z-index:9999;width:200px;height:auto;opacity:0;-webkit-transition:all .5s cubic-bezier(.55,0,.1,1);-khtml-transition:all .5s cubic-bezier(.55,0,.1,1);-moz-transition:all .5s cubic-bezier(.55,0,.1,1);-ms-transition:all .5s cubic-bezier(.55,0,.1,1);-o-transition:all .5s cubic-bezier(.55,0,.1,1);transition:all .5s cubic-bezier(.55,0,.1,1)}.comfyui-easyuse-slider{width:100%;height:100%;display:-webkit-box;display:-moz-box;display:-ms-flexbox;display:-webkit-flex;display:flex;-webkit-justify-content:space-between;-khtml-justify-content:space-between;-moz-justify-content:space-between;-ms-justify-content:space-between;-o-justify-content:space-between;justify-content:space-between;-webkit-align-items:center;-khtml-align-items:center;-moz-align-items:center;-ms-align-items:center;-o-align-items:center;align-items:center}.comfyui-easyuse-slider-item{height:inherit;min-width:25px;display:-webkit-box;display:-moz-box;display:-ms-flexbox;display:-webkit-flex;display:flex;-webkit-justify-content:center;-khtml-justify-content:center;-moz-justify-content:center;-ms-justify-content:center;-o-justify-content:center;justify-content:center;-webkit-align-items:center;-khtml-align-items:center;-moz-align-items:center;-ms-align-items:center;-o-align-items:center;align-items:center;-webkit-flex-direction:column;-khtml-flex-direction:column;-moz-flex-direction:column;-ms-flex-direction:column;-o-flex-direction:column;flex-direction:column}.comfyui-easyuse-slider-item-input{height:15px;font-size:10px;color:var(--input-text)}.comfyui-easyuse-slider-item-label{height:15px;border:none;color:var(--descrip-text);font-size:8px}.comfyui-easyuse-slider-item-scroll{width:5px;height:calc(100% - 30px);background:var(--comfy-input-bg);border-radius:10px;position:relative}.comfyui-easyuse-slider-item-bar{width:10px;height:10px;background:linear-gradient(to bottom,var(--input-text),var(--descrip-text));border-radius:100%;box-shadow:0 2px 10px var(--bg-color);position:absolute;top:0;left:-2.5px;cursor:pointer;z-index:1}.comfyui-easyuse-slider-item-area{width:100%;border-radius:20px;position:absolute;bottom:0;background:var(--input-text);z-index:0}.comfyui-easyuse-slider-item.positive .comfyui-easyuse-slider-item-label{color:var(--success-color)}.comfyui-easyuse-slider-item.positive .comfyui-easyuse-slider-item-area{background:var(--success-color)}.comfyui-easyuse-slider-item.negative .comfyui-easyuse-slider-item-label{color:var(--error-color)}.comfyui-easyuse-slider-item.negative .comfyui-easyuse-slider-item-area{background:var(--error-color)}.comfyui-easyuse-map{height:100%;background:var(--comfy-menu-bg)}.comfyui-easyuse-map .p-splitter-gutter-handle{height:1px!important}.comfyui-easyuse-map-nodes{height:100%;position:relative}.comfyui-easyuse-map-nodes__header{position:absolute;z-index:2;top:0;left:0;width:100%;padding:.25rem 0 .25rem .5rem;height:2.8rem;background:var(--comfy-menu-bg);border-bottom:1px solid var(--border-color);display:-webkit-box;display:-moz-box;display:-ms-flexbox;display:-webkit-flex;display:flex;-webkit-justify-content:space-between;-khtml-justify-content:space-between;-moz-justify-content:space-between;-ms-justify-content:space-between;-o-justify-content:space-between;justify-content:space-between;-webkit-align-items:center;-khtml-align-items:center;-moz-align-items:center;-ms-align-items:center;-o-align-items:center;align-items:center;-webkit-backdrop-filter:saturate(180%) blur(40px);-khtml-backdrop-filter:saturate(180%) blur(40px);-moz-backdrop-filter:saturate(180%) blur(40px);-ms-backdrop-filter:saturate(180%) blur(40px);-o-backdrop-filter:saturate(180%) blur(40px);backdrop-filter:saturate(180%) blur(40px)}.comfyui-easyuse-map-nodes__header .title{font-size:13px;color:var(--input-text);font-weight:400;line-height:1.5;-webkit-user-select:none;user-select:none}.comfyui-easyuse-map-nodes__header .toolbar{display:-webkit-box;display:-moz-box;display:-ms-flexbox;display:-webkit-flex;display:flex;-webkit-align-items:center;-khtml-align-items:center;-moz-align-items:center;-ms-align-items:center;-o-align-items:center;align-items:center}.comfyui-easyuse-map-nodes__header .toolbar .icon{font-size:.85rem;margin-left:.25rem;cursor:pointer}.comfyui-easyuse-map-nodes__content{position:relative;padding:2.8rem 0 0;height:100%;overflow:auto}.comfyui-easyuse-map-nodes__content-searchbox{padding:1rem}.comfyui-easyuse-map-nodes__content dl .label{padding-left:1rem}.comfyui-easyuse-map-nodes__content ul,.comfyui-easyuse-map-nodes__content ol,.comfyui-easyuse-map-nodes__content dl{list-style-type:none;padding:0;margin:0}.comfyui-easyuse-map-nodes__content ul .toolbar span,.comfyui-easyuse-map-nodes__content ol .toolbar span,.comfyui-easyuse-map-nodes__content dl .toolbar span{font-size:13px}.comfyui-easyuse-map-nodes__content ul .toolbar span.pi-eye,.comfyui-easyuse-map-nodes__content ol .toolbar span.pi-eye,.comfyui-easyuse-map-nodes__content dl .toolbar span.pi-eye{color:var(--input-text)}.comfyui-easyuse-map-nodes__content ul .toolbar span.pi-eye-slash,.comfyui-easyuse-map-nodes__content ol .toolbar span.pi-eye-slash,.comfyui-easyuse-map-nodes__content dl .toolbar span.pi-eye-slash{color:var(--descrip-text)}.comfyui-easyuse-map-nodes__content ul .toolbar span.pi-eye-slash.never,.comfyui-easyuse-map-nodes__content ol .toolbar span.pi-eye-slash.never,.comfyui-easyuse-map-nodes__content dl .toolbar span.pi-eye-slash.never{opacity:.5}.comfyui-easyuse-map-nodes__content ol{padding:0;padding-block-start:2px;padding-inline-start:1rem}.comfyui-easyuse-map-nodes__content .no_result{display:-webkit-box;display:-moz-box;display:-ms-flexbox;display:-webkit-flex;display:flex;-webkit-justify-content:center;-khtml-justify-content:center;-moz-justify-content:center;-ms-justify-content:center;-o-justify-content:center;justify-content:center;-webkit-align-items:center;-khtml-align-items:center;-moz-align-items:center;-ms-align-items:center;-o-align-items:center;align-items:center;text-align:center}.comfyui-easyuse-map-nodes .p-contextmenu{display:block!important;position:fixed!important;z-index:1000!important}.comfyui-easyuse-map-nodes-group{position:relative;overflow:hidden;width:100%;height:2rem;cursor:default;display:-webkit-box;display:-moz-box;display:-ms-flexbox;display:-webkit-flex;display:flex;-webkit-justify-content:space-between;-khtml-justify-content:space-between;-moz-justify-content:space-between;-ms-justify-content:space-between;-o-justify-content:space-between;justify-content:space-between;-webkit-align-items:center;-khtml-align-items:center;-moz-align-items:center;-ms-align-items:center;-o-align-items:center;align-items:center;pointer-events:auto}.comfyui-easyuse-map-nodes-group .left,.comfyui-easyuse-map-nodes-group .right{height:100%;display:-webkit-box;display:-moz-box;display:-ms-flexbox;display:-webkit-flex;display:flex;-webkit-align-items:center;-khtml-align-items:center;-moz-align-items:center;-ms-align-items:center;-o-align-items:center;align-items:center}.comfyui-easyuse-map-nodes-group .left{padding-left:.5rem;margin-right:.25rem;-webkit-box-flex:1;-ms-flex:1;-webkit-flex:1;flex:1}.comfyui-easyuse-map-nodes-group .icon{font-size:.85rem;margin-right:.25rem}.comfyui-easyuse-map-nodes-group .label{display:-webkit-box;display:-moz-box;display:-ms-flexbox;display:-webkit-flex;display:flex;-webkit-align-items:center;-khtml-align-items:center;-moz-align-items:center;-ms-align-items:center;-o-align-items:center;align-items:center;height:100%;width:100%;min-width:80px}.comfyui-easyuse-map-nodes-group .label span{font-size:14px;color:var(--input-text);font-weight:400;line-height:1.5;display:-webkit-box;-webkit-line-clamp:1;overflow:hidden;word-break:break-all;text-overflow:ellipsis;-webkit-box-orient:vertical;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;-o-user-select:none;user-select:none}.comfyui-easyuse-map-nodes-group:hover{background:var(--content-hover-bg)!important}.comfyui-easyuse-map-nodes-group.active{background:var(--theme-color)!important}.comfyui-easyuse-map-nodes-group.active .label{color:#fff;cursor:default}.comfyui-easyuse-map-nodes-group.never .label{color:var(--descrip-text);opacity:.4}.comfyui-easyuse-map-nodes-group.bypass .label{color:var(--descrip-text)}.comfyui-easyuse-map-nodes-node{height:2rem;cursor:default;display:-webkit-box;display:-moz-box;display:-ms-flexbox;display:-webkit-flex;display:flex;-webkit-justify-content:space-between;-khtml-justify-content:space-between;-moz-justify-content:space-between;-ms-justify-content:space-between;-o-justify-content:space-between;justify-content:space-between;-webkit-align-items:center;-khtml-align-items:center;-moz-align-items:center;-ms-align-items:center;-o-align-items:center;align-items:center;position:relative;overflow:hidden;pointer-events:auto}.comfyui-easyuse-map-nodes-node .node_id{background:#0b2000;padding:.25rem;margin:0 0 0 .25rem;border-radius:.25rem;text-align:center;color:#fff;font-size:12px}.comfyui-easyuse-map-nodes-node .left{height:100%;display:-webkit-box;display:-moz-box;display:-ms-flexbox;display:-webkit-flex;display:flex;-webkit-align-items:center;-khtml-align-items:center;-moz-align-items:center;-ms-align-items:center;-o-align-items:center;align-items:center}.comfyui-easyuse-map-nodes-node .label{text-indent:.5rem;font-size:14px;color:var(--input-text);font-weight:400;line-height:1.5;-webkit-box-flex:1;-ms-flex:1;-webkit-flex:1;flex:1;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;-o-user-select:none;user-select:none;margin-right:.25rem;height:2rem;line-height:2rem;width:100%;display:-webkit-box;-webkit-line-clamp:1;overflow:hidden;word-break:break-all;text-overflow:ellipsis;-webkit-box-orient:vertical}.comfyui-easyuse-map-nodes-node .label.error{color:var(--error-color)}.comfyui-easyuse-map-nodes-node:hover{background:var(--content-hover-bg)!important}.comfyui-easyuse-map-nodes-node.never .label{color:var(--descrip-text);opacity:.5}.comfyui-easyuse-map-nodes-node.bypass .label{color:#f0f;opacity:.5}.comfyui-easyuse-toolbar{border-radius:0 12px 12px 0;min-width:50px;height:24px;position:fixed;bottom:85px;left:0;display:flex;align-items:center;z-index:1000;background-color:var(--comfy-menu-bg);border:1px solid var(--bg-color);-webkit-backdrop-filter:saturate(180%) blur(40px);-khtml-backdrop-filter:saturate(180%) blur(40px);-moz-backdrop-filter:saturate(180%) blur(40px);-ms-backdrop-filter:saturate(180%) blur(40px);-o-backdrop-filter:saturate(180%) blur(40px);backdrop-filter:saturate(180%) blur(40px)}.comfyui-easyuse-toolbar-icon{height:100%;padding:0 4px;display:flex;justify-content:center;align-items:center;font-size:12px;color:var(--input-text);transition:all .3s ease-in-out;cursor:pointer}.comfyui-easyuse-toolbar-icon svg{width:14px;height:14px}.comfyui-easyuse-toolbar-icon:hover.group{color:var(--warning-color)}.comfyui-easyuse-toolbar-icon:hover.rocket{color:var(--theme-color)}.comfyui-easyuse-toolbar-nodes-map{position:absolute;top:50px;left:10px;width:200px;border-radius:12px;min-height:100px;max-height:600px;color:var(--descrip-text);background-color:var(--comfy-menu-bg);border:1px solid var(--bg-color);-webkit-backdrop-filter:saturate(180%) blur(40px);-khtml-backdrop-filter:saturate(180%) blur(40px);-moz-backdrop-filter:saturate(180%) blur(40px);-ms-backdrop-filter:saturate(180%) blur(40px);-o-backdrop-filter:saturate(180%) blur(40px);backdrop-filter:saturate(180%) blur(40px);z-index:399;padding-top:0;overflow:hidden}.comfyui-easyuse-toolbar-nodes-map .no-result-placeholder-content{-webkit-transform:scale(.8);-khtml-transform:scale(.8);-moz-transform:scale(.8);-ms-transform:scale(.8);-o-transform:scale(.8);transform:scale(.8)}.comfyui-easyuse-toolbar-nodes-map .comfyui-easyuse-map-nodes{min-height:100px;max-height:600px}.comfyui-easyuse-toolbar-nodes-map .comfyui-easyuse-map-nodes__header:before{content:"…";position:absolute;left:.25rem;top:2.75rem;transform:translateY(-2rem) rotate(90deg);width:.5rem;height:.5rem;display:inline-block;overflow:hidden;line-height:5px;padding:3px 4px;cursor:move;vertical-align:middle;font-size:12px;font-family:sans-serif;letter-spacing:2px;color:var(--drag-text);z-index:3;text-shadow:1px 0 1px black}.comfyui-easyuse-toolbar-nodes-map .comfyui-easyuse-map-nodes__header .title{cursor:move;padding-left:.25rem}.comfyui-easyuse-toolbar-nodes-map .comfyui-easyuse-map-nodes__content{max-height:calc(600px - 2.7rem)}.no-result-placeholder{display:flex;justify-content:center;align-items:center;height:100%}.no-result-placeholder-content{text-align:center;display:-webkit-box;display:-moz-box;display:-ms-flexbox;display:-webkit-flex;display:flex;-webkit-flex-direction:column;-khtml-flex-direction:column;-moz-flex-direction:column;-ms-flex-direction:column;-o-flex-direction:column;flex-direction:column;-webkit-justify-content:space-between;-khtml-justify-content:space-between;-moz-justify-content:space-between;-ms-justify-content:space-between;-o-justify-content:space-between;justify-content:space-between}.no-result-placeholder .p-card{background-color:transparent!important;box-shadow:none;text-align:center}.no-result-placeholder h3{color:var(--input-text);margin-bottom:.5rem}.no-result-placeholder p{color:var(--descrip-text);margin-bottom:1rem;margin-top:0}#comfyui-easyuse-components{position:absolute;top:0;left:0;z-index:3}.comfyui-easyuse{--primary-bg:var(--theme-color)!important;--primary-hover-bg: var(--theme-color-light)!important;--p-datatable-header-cell-padding: .15rem 1rem;--p-datatable-body-cell-padding: .15rem 1rem;--border-color-solid: var(--border-color);--border-radius: 8px}.comfyui-easyuse.dark-theme{--fg-color: #fff;--bg-color: #090909;--content-bg:#18181b;--content-fg:#fff;--content-hover-bg: #27272a;--comfy-menu-bg: rgba(24,24,27,.9);--comfy-input-bg: #242427;--input-text: #ffffff;--descrip-text: #71717a;--drag-text: #ccc;--error-text: #ff4444;--border-color: #3f3f46;--border-color-solid: #2a2a2e;--tr-even-bg-color: rgba(28,28,28,.9);--tr-odd-bg-color: rgba(19,19,19,.9)}.comfyui-easyuse ::-webkit-scrollbar{width:0em}.comfyui-easyuse ::-webkit-scrollbar-track{background-color:transparent}.comfyui-easyuse ::-webkit-scrollbar-thumb{background-color:transparent;border-radius:2px}.comfyui-easyuse ::-webkit-scrollbar-thumb:hover{background-color:transparent}.comfyui-easyuse body{font-family:var(--font-family)!important;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.comfyui-easyuse textarea{font-family:var(--font-family)!important}.comfyui-easyuse hr{border:1px solid var(--border-color)}.comfyui-easyuse .comfy-multiline-input{background-color:transparent;border:1px solid var(--border-color-solid);border-radius:var(--border-radius);padding:8px;font-size:12px}.comfyui-easyuse #comfy-settings-dialog{border:1px solid var(--border-color);background:transparent;-webkit-backdrop-filter:blur(8px) brightness(120%);backdrop-filter:blur(8px) brightness(120%);box-shadow:none}.comfyui-easyuse .comfy-modal{border:1px solid var(--border-color);box-shadow:none;-webkit-backdrop-filter:blur(8px) brightness(120%);backdrop-filter:blur(8px) brightness(120%)}.comfyui-easyuse .cm-title{background-color:transparent!important}.comfyui-easyuse .cm-notice-board{border-radius:10px!important;border:1px solid var(--border-color)!important}.comfyui-easyuse .cm-menu-container{margin-bottom:50px!important}.comfyui-easyuse .cn-manager-custom_milk_white .tg-column-name,.comfyui-easyuse .cmm-manager-custom_milk_white .tg-column-name{color:var(--input-text)}.comfyui-easyuse .cn-manager-custom_milk_white .tg-body-message,.comfyui-easyuse .cmm-manager-custom_milk_white .tg-body-message{text-align:center;color:var(--descrip-text)!important}.comfyui-easyuse .cn-manager-custom_milk_white .tg-body-frame .tg-cell,.comfyui-easyuse .cmm-manager-custom_milk_white .tg-body-frame .tg-cell{color:var(--input-text)}.comfyui-easyuse .cn-manager-custom_milk_white .tg-body-frame .cn-node-name a,.comfyui-easyuse .cn-manager-custom_milk_white .tg-body-frame .cmm-node-name a,.comfyui-easyuse .cmm-manager-custom_milk_white .tg-body-frame .cn-node-name a,.comfyui-easyuse .cmm-manager-custom_milk_white .tg-body-frame .cmm-node-name a{color:var(--theme-color)!important}.comfyui-easyuse .comfy-menu{border-radius:16px;box-shadow:0 0 1px var(--descrip-text);-webkit-backdrop-filter:blur(8px) brightness(120%);backdrop-filter:blur(8px) brightness(120%)}.comfyui-easyuse .comfy-menu button.comfy-settings-btn{font-size:12px}.comfyui-easyuse .comfy-menu-btns{margin-bottom:4px}.comfyui-easyuse .comfy-menu button,.comfyui-easyuse .comfy-modal button{font-size:14px;padding:4px 0;margin-bottom:4px}.comfyui-easyuse .comfy-menu-btns button,.comfyui-easyuse .comfy-list-actions button{font-size:10px}.comfyui-easyuse .comfy-menu>button,.comfyui-easyuse .comfy-menu-btns button,.comfyui-easyuse .comfy-menu .comfy-list button,.comfyui-easyuse .comfy-modal button{border-width:1px}.comfyui-easyuse #comfy-dev-save-api-button{justify-content:center}.comfyui-easyuse #queue-button{position:relative;overflow:hidden;min-height:30px;z-index:1}.comfyui-easyuse #queue-button:after{clear:both;content:attr(data-attr);background:green;color:#fff;width:var(--process-bar-width);height:100%;position:absolute;top:0;left:0;z-index:0;text-align:center;display:flex;justify-content:center;align-items:center}.comfyui-easyuse #shareButton{background:linear-gradient(to left,var(--theme-color),var(--theme-color-light))!important;color:#fff!important}.comfyui-easyuse .litegraph.litecontextmenu{--height: 24px;--padding: 6px;font-family:var(--font-family);padding:var(--padding) 0;border-radius:var(--border-radius);-webkit-backdrop-filter:blur(8px) brightness(120%);backdrop-filter:blur(8px) brightness(120%)}.comfyui-easyuse .litegraph.litecontextmenu .litemenu-title{padding:var(--padding)}.comfyui-easyuse .litegraph.litecontextmenu>div:first-child.litemenu-title{margin-top:-6px}.comfyui-easyuse .litegraph.litecontextmenu .submenu{height:var(--height);line-height:var(--height);padding:0 18px 0 12px;margin:0;background:transparent!important}.comfyui-easyuse .litegraph.litecontextmenu .submenu.has_submenu{border-right:none;position:relative}.comfyui-easyuse .litegraph.litecontextmenu .submenu.has_submenu:after{content:"";display:block;width:0;height:0;border-width:4px 4px 0;border-style:solid;border-color:var(--input-text) transparent transparent;transform:translateY(-50%) rotate(-90deg);top:50%;position:absolute}.comfyui-easyuse .litegraph.litecontextmenu .submenu.separator{height:1px;width:100%;background-color:var(--border-color)!important;margin:var(--padding) 0;border:none}.comfyui-easyuse .litegraph.litecontextmenu .submenu:last-child.separator{display:none}.comfyui-easyuse .litegraph.litecontextmenu .submenu:hover:not(.separator){background:var(--theme-color)!important}.comfyui-easyuse .litegraph.lite-search-item{background-color:var(--comfy-input-bg)!important;filter:brightness(100%)}.comfyui-easyuse .litegraph.lite-search-item:hover{filter:brightness(120%);color:var(--input-text)}.comfyui-easyuse .graphdialog{-webkit-backdrop-filter:blur(8px) brightness(120%);backdrop-filter:blur(8px) brightness(120%)}.comfyui-easyuse .graphdialog button{display:-webkit-box;display:-moz-box;display:-ms-flexbox;display:-webkit-flex;display:flex;-webkit-justify-content:center;-khtml-justify-content:center;-moz-justify-content:center;-ms-justify-content:center;-o-justify-content:center;justify-content:center;-webkit-align-items:center;-khtml-align-items:center;-moz-align-items:center;-ms-align-items:center;-o-align-items:center;align-items:center}.comfyui-easyuse .comfyui-menu{background:var(--comfy-menu-bg);border-bottom:1px solid var(--bg-color)}.comfyui-easyuse .side-tool-bar-container:not(.bg-transparent){background:var(--comfy-menu-bg);border-right:1px solid var(--bg-color)}.comfyui-easyuse .comfy-modal-content{width:100%}.comfyui-easyuse-poseEditor{display:-webkit-box;display:-moz-box;display:-ms-flexbox;display:-webkit-flex;display:flex;-webkit-justify-content:center;-khtml-justify-content:center;-moz-justify-content:center;-ms-justify-content:center;-o-justify-content:center;justify-content:center;-webkit-align-items:center;-khtml-align-items:center;-moz-align-items:center;-ms-align-items:center;-o-align-items:center;align-items:center;text-align:center;font-size:18px;line-height:1.5}.comfyui-easyuse .dom-widget{padding:0 4px}.comfyui-easyuse .pi.pi-circle-fill[data-testid=noColor]{color:#000!important}.comfyui-easyuse .pi.pi-circle-fill[data-testid=red]{color:#af3535!important}.comfyui-easyuse .pi.pi-circle-fill[data-testid=brown]{color:#38291f!important}.comfyui-easyuse .pi.pi-circle-fill[data-testid=green]{color:#346434!important}.comfyui-easyuse .pi.pi-circle-fill[data-testid=blue]{color:#1f1f48!important}.comfyui-easyuse .pi.pi-circle-fill[data-testid=pale_blue]{color:#006691!important}.comfyui-easyuse .pi.pi-circle-fill[data-testid=cyan]{color:#008181!important}.comfyui-easyuse .pi.pi-circle-fill[data-testid=purple]{color:#422342!important}.comfyui-easyuse .pi.pi-circle-fill[data-testid=yellow]{color:#c09430!important}.comfyui-easyuse .pi.pi-circle-fill[data-testid=black]{color:#000c!important}')),document.head.appendChild(e)}}catch(t){console.error("vite-plugin-css-injected-by-js",t)}}(); +var e;import{u as o,a as s,s as n,g as t,b as a,c as l,j as i,N as d,$ as u,t as r,d as p,e as c,f as m,h as v}from"./assets/extensions-nX1P6R6o.js";import{w as h,r as g,o as f,a as y,c as _,F as w,i as b,n as x,b as M,t as k,l as C,k as S,f as N,u as D,d as $,A as E,h as A,y as V,j as H,z as G,Q as z,e as T,_ as U,$ as L,B as j}from"./assets/vue-DfGw76ex.js";import{e as O,s as R,k as Y,g as B,T as P,f as I,i as W,l as F,m as q}from"./assets/vendor-DrOEdrAG.js";import{c as K}from"./assets/lodash-DkvwM0Z9.js";import{P as X}from"./assets/primevue-keNb8gkp.js";import"./assets/vueuse-Bf7Wy5dB.js";import"./assets/primeuix-CNMLIuxO.js";const J=["data-id"],Q=["onMousedown","onDblclick"],Z="comfyui-easyuse-slider",ee="ipadapter layer weights",oe={__name:"sliderControl",props:{id:{type:String|Number,default:""},mode:{type:String,default:""},type:{type:String,default:""},values:{type:Array,default:[]},show:{type:Boolean,default:!1}},emits:["changeValues","showSlider"],setup(e,{emit:o}){const s=e,n=o,t=(e,o,s)=>(e-o)/(s-o)*100,a=(e,o,n=void 0)=>{if(s.mode===ee){let s={3:2.5,6:1}[o]||0;return{default:12==e?s:0,min:-1,max:3,step:.05,value:void 0!==n?n:12==e?s:0,top:void 0!==n?100-t(n,-1,3)+"%":null,height:void 0!==n?t(n,-1,3)+"%":null}}};h((e=>s.mode),(async(e,o)=>{var t;if(e!==o&&e===ee)if(!o&&(null==(t=s.values)?void 0:t.length)>0){const e=s.values.map((e=>{const o=e.split(":");return a(s.values.length,o[0],parseFloat(o[1]))}));await n("changeValues",e)}else{let e="sd1"==s.type?16:12,o=Array.from({length:e},((o,s)=>a(e,s)));await n("changeValues",o)}n("showSlider")}),{immediate:!0}),h((e=>s.type),((e,o)=>{if(e!=o&&s.mode==ee){let e="sd1"==s.type?16:12,o=Array.from({length:e},((o,s)=>a(e,s)));n("changeValues",o)}}));const l=g(null),i=g(null);return f((()=>{document.onmouseup=e=>document.onmousemove=null})),(o,a)=>{var d;return y(),_("div",{class:x(Z),"data-id":e.id},[(null==(d=e.values)?void 0:d.length)>0&&e.show?(y(!0),_(w,{key:0},b(e.values,((o,a)=>(y(),_("div",{class:x([Z+"-item",{positive:3==a&&"sdxl"==e.type&&e.mode==ee},{negative:6==a&&"sdxl"==e.type&&e.mode==ee}]),key:a},[M("div",{class:x(Z+"-item-input")},k(o.value),3),M("div",{class:x(Z+"-item-scroll"),ref_for:!0,ref_key:"scroll",ref:l},[M("div",{class:x(Z+"-item-bar"),ref_for:!0,ref_key:"bar",ref:i,style:C({top:o.top||100-t(o.default,o.min,o.max)+"%"}),onMousedown:e=>((e,o,t)=>{let a=e||window.event,d=l.value[t],u=i.value[t],r=K(s.values),p=a.clientY-u.offsetTop;document.onmousemove=e=>{let s=(e||window.event).clientY-p;s<0?s=0:s>d.offsetHeight-u.offsetHeight&&(s=d.offsetHeight-u.offsetHeight);let a=(o.max-o.min)/o.step,l=(d.offsetHeight-u.offsetHeight)/a;s=Math.round(s/l)*l;const i=Math.floor(s/(d.offsetHeight-u.offsetHeight)*100)+"%",c=Math.floor((d.offsetHeight-u.offsetHeight-s)/(d.offsetHeight-u.offsetHeight)*100)+"%",m=parseFloat(parseFloat(o.max-(o.max-o.min)*(s/(d.offsetHeight-u.offsetHeight))).toFixed(2));r[t]={...r[t],top:i,height:c,value:m},n("changeValues",r),window.getSelection?window.getSelection().removeAllRanges():document.selection.empty()}})(e,o,a),onDblclick:e=>((e,o,t)=>{let a=K(s.values);a[t]={...a[t],top:null,height:null,value:o.default},n("changeValues",a)})(0,o,a)},null,46,Q),M("div",{class:x(Z+"-item-area"),style:C({height:o.height||t(o.default,o.min,o.max)+"%"})},null,6)],2),M("div",{class:x(Z+"-item-label")},[M("span",null,k(o.label),1)],2)],2)))),128)):S("",!0)],8,J)}}},se={__name:"index",setup(e){const a=o(),{slider_controls:l}=O(a),i=g({}),d=async e=>{var o,s,d,u,r;await n(1);const p=t(e,"mode"),c=t(e,"model_type"),m=(null==(o=e.properties.values)?void 0:o.length)>0?e.properties.values:[];(null==(s=e.flags)?void 0:s.collapsed)&&e.collapse();let v=K(l.value);v.push({id:e.id,type:c.value,mode:p.value,value:m,show:!1});const h=v.length-1;await a.setSliderControls(v);let g=null==(r=null==(u=null==(d=i.value[e.id])?void 0:d._)?void 0:u.vnode)?void 0:r.el;if(!g)return;e.addDOMWidget("values","btn",g,{setValue(){},getValue(){var o;const s=(null==(o=l.value)?void 0:o[h].value)||[];return e.properties.values=s.map(((e,o)=>`${o}:${e.value}`)),e.properties.values.join(",")}}),e.properties.values||e.setProperty("values",[]),e.setSize("sdxl"==c.value?[375,320]:[455,320]),c.callback=o=>{v=K(l.value),v[h].type!=o&&(e.setSize("sdxl"==o?[375,320]:[455,320]),v[h].value=[],v[h].type=o,a.setSliderControls(v))};const f=e.onRemoved;e.onRemoved=function(){if(f&&(null==f||f.apply(this,arguments)),void 0!==l.value.findIndex((o=>o.id==e.id))){let e=K(l.value);e.splice(h,1),a.setSliderControls(e)}return f}};return f((e=>{s.registerExtension({name:"Comfy.EasyUse.Components",async beforeRegisterNodeDef(e,o){const s=e.prototype.onNodeCreated;"easy sliderControl"==o.name&&(e.prototype.onNodeCreated=async function(){return s&&(null==s||s.apply(this,arguments)),await d(this),s}),"easy poseEditor"==o.name&&(e.prototype.onNodeCreated=async function(){s&&(null==s||s.apply(this,arguments));const e=document.createElement("div");return e.className="comfyui-easyuse-poseEditor",e.innerHTML='
This node is about to be removed, you can use ComfyUI_Custom_Nodes_AlekPet to replace it.
',this.addDOMWidget("editor","btn",e),s})}})})),(e,o)=>(y(!0),_(w,null,b(D(l),((e,o)=>(y(),N(oe,{ref_for:!0,ref:o=>{o&&(i.value[e.id]=o)},type:e.type,key:o,id:e.id,show:e.show,mode:e.mode,values:e.value,onChangeValues:e=>((e,o)=>{let s=K(l.value);s[o].value=e,a.setSliderControls(s)})(e,o),onShowSlider:e=>(e=>{let o=K(l.value);o[e].show=!0,a.setSliderControls(o)})(o)},null,8,["type","id","show","mode","values","onChangeValues","onShowSlider"])))),128))}},ne={class:"no-result-placeholder"},te={class:"no-result-placeholder-content"},ae={key:0},le={__name:"noResultsPlaceholder",props:{icon:{type:String,default:"",required:!1},iconSize:{type:String,default:"3rem",required:!1},title:{type:String,required:!0},message:{type:String,required:!1},buttonLabel:{type:String,default:"",required:!1}},emits:["action"],setup:e=>(o,s)=>(y(),_("div",ne,[$(D(Y),null,{content:E((()=>[M("div",te,[M("i",{class:x(e.icon),style:C({"font-size":e.iconSize,"margin-bottom":".5rem"})},null,6),M("h3",null,k(e.title),1),e.message?(y(),_("p",ae,k(e.message),1)):S("",!0),e.buttonLabel?(y(),N(D(R),{key:1,label:e.buttonLabel,onClick:s[0]||(s[0]=e=>o.$emit("action")),class:"p-button-text"},null,8,["label"])):S("",!0)])])),_:1})]))},ie={class:"left flex-1"},de={key:0,class:"node_id"},ue={key:1,class:"edit"},re={class:"right toolbar"},pe={__name:"node",props:{node:{type:Object,default:{}}},emits:["mousedown","mouseup","changeMode"],setup(e){const o=a(),n=A((e=>l("EasyUse.NodesMap.DisplayNodeID"))),t=e,u=g(!1),r=g(null),p=g("");h((e=>t.node.is_edit),(e=>{e&&(p.value=t.node.title)}));const c=async()=>{let e=s.canvas.graph._nodes.find((e=>e.id==t.node.id));e?(e.is_edit=!1,e.title=p.value,await o.setNodes(s.canvas.graph._nodes),u.value=!1):u.value=!1};return(o,s)=>{var t;return y(),_("div",{draggable:!1,class:x(["comfyui-easyuse-map-nodes-node",{never:void 0!==e.node.mode&&e.node.mode==D(d).NEVER},{bypass:void 0!==e.node.mode&&e.node.mode==D(d).BYPASS}])},[M("div",ie,[n.value?(y(),_("span",de,k(e.node.id),1)):S("",!0),(null==(t=e.node)?void 0:t.is_edit)?(y(),_("div",ue,[$(D(B),{ref_key:"modifyRef",ref:r,modelValue:p.value,"onUpdate:modelValue":s[0]||(s[0]=e=>p.value=e),variant:"outline",size:"small",type:"text",onBlur:c,onKeydown:[V(c,["enter"]),V(c,["esc"])],style:{width:"100%"}},null,8,["modelValue"])])):void 0!==e.node.title?(y(),_("span",{key:2,class:"label",onDblclick:s[1]||(s[1]=H((o=>D(i)(e.node.id)),["stop"]))},k(e.node.title),33)):(y(),_("span",{key:3,class:"label error",onDblclick:s[2]||(s[2]=H((o=>D(i)(e.node.id)),["stop"]))},k(e.node.type),33))]),M("div",re,[$(D(R),{size:"small",icon:e.node.mode==D(d).ALWAYS?"pi pi-eye":"pi pi-eye-slash",text:"",rounded:"",severity:"secondary",onClick:s[3]||(s[3]=H((e=>o.$emit("changeMode")),["stop"])),onMousedown:s[4]||(s[4]=H((e=>o.$emit("mousedown")),["stop"])),onMouseup:s[5]||(s[5]=H((e=>o.$emit("mouseup")),["stop"]))},null,8,["icon"])])],2)}}},ce={class:"left flex-1"},me={key:1,class:"edit"},ve={key:2,class:"label"},he={class:"right toolbar"},ge={key:0,class:"child"},fe={__name:"group",props:{item:{type:Object,default:{}}},emits:["mousedown","mouseup","changeMode"],setup(e){const o=e,n=a(),t=g(!1),l=g(null),i=g("");h((e=>o.item.info.is_edit),(e=>{e&&(i.value=o.item.info.title)}));const u=e=>{var t,a;let l=o.item;if(!(null==(t=l.info)?void 0:t.is_edit)&&(null==(a=l.children)?void 0:a.length)>0){let e=s.canvas.graph._groups.find((e=>e.pos[0]==l.info.pos[0]&&e.pos[1]==l.info.pos[1]));e&&(e.show_nodes=!e.show_nodes,n.setGroups(s.canvas.graph._groups))}},r=async()=>{let e=o.item,a=s.canvas.graph._groups.find((o=>o.pos[0]==e.info.pos[0]&&o.pos[1]==e.info.pos[1]));a?(a.is_edit=!1,a.title=i.value,await n.setGroups(s.canvas.graph._groups),t.value=!1):t.value=!1};return(o,s)=>{var n,t,a;return y(),_(w,null,[M("div",{class:x("comfyui-easyuse-map-nodes-group"),onClick:u},[M("div",ce,[e.item.children?(y(),_("i",{key:0,class:x(["icon",e.item.info.show_nodes?"pi pi-folder-open":"pi pi-folder"]),style:C({color:e.item.info.color})},null,6)):S("",!0),(null==(n=e.item.info)?void 0:n.is_edit)?(y(),_("div",me,[$(D(B),{ref_key:"modifyRef",ref:l,modelValue:i.value,"onUpdate:modelValue":s[0]||(s[0]=e=>i.value=e),variant:"outline",size:"small",type:"text",onBlur:r,onKeydown:[V(r,["enter"]),V(r,["esc"])],style:{width:"100%"}},null,8,["modelValue"])])):(y(),_("div",ve,[M("span",null,k(e.item.info.title),1)]))]),M("div",he,[(null==(t=e.item.children)?void 0:t.length)>0?(y(),N(D(R),{key:0,size:"small",icon:e.item.children.find((e=>e.mode==D(d).ALWAYS))?"pi pi-eye":"pi pi-eye-slash",text:"",rounded:"",severity:"secondary",onClick:s[1]||(s[1]=H((e=>o.$emit("changeMode")),["stop"])),onMousedown:s[2]||(s[2]=H((e=>o.$emit("mousedown")),["stop"])),onMouseup:s[3]||(s[3]=H((e=>o.$emit("mouseup")),["stop"]))},null,8,["icon"])):S("",!0)])]),(null==(a=e.item.children)?void 0:a.length)>0&&e.item.info.show_nodes?(y(),_("div",ge,[G(o.$slots,"default")])):S("",!0)],64)}}},ye={__name:"tree",props:{item:Object,index:Number,showGroupOnly:Boolean},emits:["dragstart","dragend","dragover","contextmenu","changeGroupMode","changeNodeMode","mousedown","mouseup"],setup:e=>(o,s)=>{var n,t,a,l;const i=z("Tree",!0);return y(),_("li",{onDragstart:s[10]||(s[10]=s=>o.$emit("dragstart",s,e.index)),onDragend:s[11]||(s[11]=s=>o.$emit("dragend",s,e.index)),onDragover:s[12]||(s[12]=s=>o.$emit("dragover",s,e.index)),draggable:!0,onContextmenu:s[13]||(s[13]=H((s=>o.$emit("contextmenu",s,e.item)),["stop"]))},[(null==(n=e.item.children)?void 0:n.length)||(null==(a=null==(t=e.item)?void 0:t.info)?void 0:a.sub_groups)?(y(),N(fe,{key:0,item:e.item,onChangeMode:s[4]||(s[4]=s=>o.$emit("changeGroupMode",e.item)),onMousedown:s[5]||(s[5]=s=>o.$emit("mousedown",e.item,"group")),onMouseup:s[6]||(s[6]=e=>o.$emit("mouseup"))},{default:E((()=>[M("ol",null,[(y(!0),_(w,null,b(e.item.children,((n,t)=>(y(),N(i,{key:t,item:n,index:t,showGroupOnly:e.showGroupOnly,onDragstart:e=>o.$emit("dragstart",e,t),onDragend:e=>o.$emit("dragend",e,t),onDragover:e=>o.$emit("dragover",e,t),onContextmenu:e=>o.$emit("contextmenu",e,n),onChangeGroupMode:s[0]||(s[0]=e=>o.$emit("changeGroupMode",e)),onChangeNodeMode:s[1]||(s[1]=e=>o.$emit("changeNodeMode",e)),onMousedown:s[2]||(s[2]=e=>o.$emit("mousedown",e[0],e[1])),onMouseup:s[3]||(s[3]=e=>o.$emit("mouseup"))},null,8,["item","index","showGroupOnly","onDragstart","onDragend","onDragover","onContextmenu"])))),128))])])),_:1},8,["item"])):e.showGroupOnly?S("",!0):(y(),N(pe,{key:1,node:(null==(l=e.item)?void 0:l.info)||e.item,onChangeMode:s[7]||(s[7]=s=>{var n;return o.$emit("changeNodeMode",(null==(n=e.item)?void 0:n.info)||e.item)}),onMousedown:s[8]||(s[8]=s=>{var n;return o.$emit("mousedown",(null==(n=e.item)?void 0:n.info)||e.item,"node")}),onMouseup:s[9]||(s[9]=e=>o.$emit("mouseup"))},null,8,["node"]))],32)}},_e={class:"title"},we={class:"toolbar"},be={key:0},xe={key:1},Me={key:2,class:"no_result",style:{height:"100%"}},ke="comfyui-easyuse-map-nodes",Ce={__name:"nodesMap",emits:["handleHeader"],setup(e){const o=a(),{groups_nodes:n,groups:t,nodes:p}=O(o),c=A((e=>l("EasyUse.NodesMap.DisplayGroupOnly"))),m=g(!1),v=e=>{m.value=!m.value,s.canvas.graph._groups.forEach((e=>{e.show_nodes=m.value})),o.setGroups(s.canvas.graph._groups)},h=g(""),f=A((e=>(null==p?void 0:p.value.filter((e=>{var o,s,n,t;return(null==(s=null==(o=e.id)?void 0:o.toString())?void 0:s.includes(h.value))||(null==(n=e.type)?void 0:n.includes(h.value))||(null==(t=e.title)?void 0:t.includes(h.value))})))||[])),C=g(null),V=g(!1),z=A((()=>V.value?[{label:u("Jump to this node"),icon:"pi pi-arrow-circle-right",visible:!0,command:()=>j(U.value)},{label:u("Rename node"),icon:"pi pi-file-edit",visible:!0,command:()=>Y(U.value)},{label:u("Delete node"),icon:"pi pi-trash",command:()=>q(U.value),visible:!0}]:[{label:u("Rename group"),icon:"pi pi-file-edit",visible:!0,command:()=>K(U.value)},{label:u("Delete group"),icon:"pi pi-trash",command:()=>X(U.value),visible:!0}])),U=g(null),L=(e,o)=>{U.value=o,V.value=!o.children,C.value.show(e)},j=e=>{i((null==e?void 0:e.info)?e.info.id:e.id)},Y=e=>{e.info?e.info.is_edit=!0:e.is_edit=!0},q=e=>{const n=e.info?e.info.id:e.id;s.canvas.graph.beforeChange(),s.canvas.graph._nodes.splice(s.canvas.graph._nodes.findIndex((e=>e.id==n)),1),s.canvas.graph.afterChange(),s.canvas.setDirty(!0,!0),o.setNodes(s.canvas.graph._nodes)},K=e=>{e.info.is_edit=!0},X=e=>{s.canvas.graph.beforeChange(),s.canvas.graph._groups.splice(s.canvas.graph._groups.findIndex((o=>o.id==e.info.id)),1),s.canvas.graph.afterChange(),s.canvas.setDirty(!0,!0),o.setGroups(s.canvas.graph._groups)};let J,Q=0,Z=0,ee=!1;const oe=(e,n=!1)=>{if(ee)return void(ee=!1);const t=e.children.find((e=>e.mode==d.ALWAYS)),a=e.children.map((e=>e.id));s.canvas.graph._nodes.forEach((e=>{a.includes(e.id)&&(e.mode=t?n?d.NEVER:d.BYPASS:d.ALWAYS,s.graph.change())})),o.setNodes(s.canvas.graph._nodes)},se=(e,n=!1)=>{if(ee)return void(ee=!1);const t=e.mode==d.ALWAYS,a=s.canvas.graph._nodes.find((o=>o.id==e.id));a&&(a.mode=t?n?d.NEVER:d.BYPASS:d.ALWAYS,s.graph.change(),o.setNodes(s.canvas.graph._nodes))},ne=(e,o="group")=>{Q=(new Date).getTime(),clearTimeout(J),J=setTimeout((s=>{"group"==o?oe(e,!0):se(e,!0)}),500)},te=e=>{Z=(new Date).getTime(),Z-Q>500&&(ee=!0),clearTimeout(J)};let ae=g(null),ie=g(null);g(!1);const de=(e,o)=>{ae.value=o,e.currentTarget.style.opacity="0.6",e.currentTarget.style.border="1px dashed yellow",e.dataTransfer.effectAllowed="move"},ue=(e,n)=>{e.target.style.opacity="1",e.currentTarget.style.border="1px dashed transparent";if("Manual drag&drop sorting"!==l("EasyUse.NodesMap.Sorting"))return void r.warn(u("For drag and drop sorting, please find Nodes map sorting mode in Settings->EasyUse and change it to manual"));let t=s.canvas.graph._groups,a=t[ae.value],i=t[ie.value];s.canvas.graph._groups[ae.value]=i,s.canvas.graph._groups[ie.value]=a,o.setGroups(s.canvas.graph._groups)},re=(e,o)=>{e.preventDefault(),e.currentIndex!=ae.value&&(ie.value=o)};return(e,o)=>{var s,a,l;return y(),_("div",{class:x(ke)},[M("div",{class:x(ke+"__header"),onMousedown:o[0]||(o[0]=o=>e.$emit("handleHeader",o))},[M("div",_e,k(D(u)("NODES MAP")),1),M("div",we,[(null==(s=D(t))?void 0:s.length)>0?T((y(),N(D(R),{key:0,icon:m.value?"pi pi-angle-double-down":"pi pi-angle-double-up",text:"",rounded:"",severity:"secondary",onClick:H(v,["stop"]),size:"small"},null,8,["icon"])),[[D(P),m.value?D(u)("Collapse All"):D(u)("Expand All"),void 0,{top:!0}]]):S("",!0),G(e.$slots,"icon")])],34),M("div",{class:x(ke+"__content")},[M("div",{class:x(ke+"__content-searchbox")},[$(D(W),null,{default:E((()=>[$(D(B),{class:"search-box-input",modelValue:h.value,"onUpdate:modelValue":o[1]||(o[1]=e=>h.value=e),placeholder:D(u)("Search by Node ID/Name..."),variant:"outlined",style:{width:"100%"}},null,8,["modelValue","placeholder"]),h.value?(y(),N(D(I),{key:1,class:"pi pi-times",onClick:o[2]||(o[2]=e=>h.value="")})):(y(),N(D(I),{key:0,class:"pi pi-search"}))])),_:1})],2),(null==(a=f.value)?void 0:a.length)>0&&h.value?(y(),_("ul",be,[(y(!0),_(w,null,b(f.value,((e,o)=>(y(),_("li",{key:o},[$(pe,{node:e,onChangeMode:o=>se(e),onMousedown:o=>ne(e,"node"),onMouseup:te},null,8,["node","onChangeMode","onMousedown"])])))),128))])):(null==(l=D(n))?void 0:l.length)>0&&!h.value?(y(),_("ul",xe,[(y(!0),_(w,null,b(D(n),((e,o)=>(y(),N(ye,{key:o,item:e,index:o,showGroupOnly:c.value,onDragstart:de,onDragend:ue,onDragover:re,onContextmenu:L,onChangeGroupMode:oe,onChangeNodeMode:se,onMousedown:ne,onMouseup:te},null,8,["item","index","showGroupOnly"])))),128))])):(y(),_("div",Me,[$(le,{icon:"pi pi-sitemap",title:D(u)("No Nodes",!0),message:h.value?D(u)("No nodes found in the search"):D(u)("No nodes found in the map",!0)},null,8,["title","message"])]))],2),$(D(F),{ref_key:"menuRef",ref:C,model:z.value,autoZIndex:!1,appendTo:"self"},null,8,["model"])])}}},Se="comfyui-easyuse-toolbar",Ne={__name:"index",setup(e){const o=a(),s=g(!1);h((e=>s.value),(e=>{e?o.watchGraph(!0):o.unwatchGraph()}));const n=g(null),t=e=>{const o=n.value;var s=e.clientX||0,t=e.clientY||0,a=o.offsetLeft,l=o.offsetTop;function i(e){var n=e.clientX,i=e.clientY,d=n-s,u=i-t;o.style.left=a+d+"px",o.style.top=l+u+"px"}document.addEventListener("mousemove",i),document.addEventListener("mouseup",(function e(){document.removeEventListener("mousemove",i),document.removeEventListener("mouseup",e)}))};return(e,o)=>(y(),_(w,null,[M("div",{class:x(["flex-c",Se])},[M("div",{class:x(["group flex-c",Se+"-icon"]),onClick:o[0]||(o[0]=e=>s.value=!s.value)},o[3]||(o[3]=[M("svg",{class:"icon",t:"1714565543756",viewBox:"0 0 1024 1024",version:"1.1",xmlns:"http://www.w3.org/2000/svg","p-id":"22538",width:"200",height:"200"},[M("path",{d:"M871.616 64H152.384c-31.488 0-60.416 25.28-60.416 58.24v779.52c0 32.896 26.24 58.24 60.352 58.24h719.232c34.112 0 60.352-25.344 60.352-58.24V122.24c0.128-32.96-28.8-58.24-60.288-58.24zM286.272 512c-23.616 0-44.672-20.224-44.672-43.008 0-22.784 20.992-43.008 44.608-43.008 23.616 0 44.608 20.224 44.608 43.008A43.328 43.328 0 0 1 286.272 512z m0-202.496c-23.616 0-44.608-20.224-44.608-43.008 0-22.784 20.992-43.008 44.608-43.008 23.616 0 44.608 20.224 44.608 43.008a43.456 43.456 0 0 1-44.608 43.008zM737.728 512H435.904c-23.68 0-44.672-20.224-44.672-43.008 0-22.784 20.992-43.008 44.608-43.008h299.264c23.616 0 44.608 20.224 44.608 43.008a42.752 42.752 0 0 1-41.984 43.008z m0-202.496H435.904c-23.616 0-44.608-20.224-44.608-43.008 0-22.784 20.992-43.008 44.608-43.008h299.264c23.616 0 44.608 20.224 44.608 43.008a42.88 42.88 0 0 1-42.048 43.008z","p-id":"22539",fill:"currentColor"})],-1)]),2),M("div",{class:x(["rocket flex-c",Se+"-icon"]),onClick:o[1]||(o[1]=(...e)=>D(p)&&D(p)(...e))},o[4]||(o[4]=[M("svg",{class:"icon",t:"1714565020764",viewBox:"0 0 1024 1024",version:"1.1",xmlns:"http://www.w3.org/2000/svg","p-id":"7999",width:"200",height:"200"},[M("path",{d:"M810.438503 379.664884l-71.187166-12.777183C737.426025 180.705882 542.117647 14.602496 532.991087 7.301248c-12.777184-10.951872-32.855615-10.951872-47.45811 0-9.12656 7.301248-204.434938 175.229947-206.26025 359.586453l-67.536542 10.951871c-18.253119 3.650624-31.030303 18.253119-31.030303 36.506239v189.832442c0 10.951872 5.475936 21.903743 12.777184 27.379679 7.301248 5.475936 14.602496 9.12656 23.729055 9.12656h5.475936l133.247772-23.729055c40.156863 47.458111 91.265597 73.012478 151.500891 73.012477 60.235294 0 111.344029-27.379679 151.500891-74.837789l136.898396 23.729055h5.475936c9.12656 0 16.427807-3.650624 23.729055-9.12656 9.12656-7.301248 12.777184-16.427807 12.777184-27.379679V412.520499c1.825312-14.602496-10.951872-29.204991-27.379679-32.855615zM620.606061 766.631016H401.568627c-20.078431 0-36.506239 16.427807-36.506238 36.506239v109.518716c0 14.602496 9.12656 29.204991 23.729055 34.680927 14.602496 5.475936 31.030303 1.825312 40.156863-9.126559l16.427807-18.25312 32.855615 80.313726c5.475936 14.602496 18.253119 23.729055 34.680927 23.729055 16.427807 0 27.379679-9.12656 34.680927-23.729055l32.855615-80.313726 16.427807 18.25312c10.951872 10.951872 25.554367 14.602496 40.156863 9.126559 14.602496-5.475936 23.729055-18.253119 23.729055-34.680927v-109.518716c-3.650624-20.078431-20.078431-36.506239-40.156862-36.506239z",fill:"currentColor","p-id":"8000"})],-1)]),2)]),s.value?(y(),_("div",{key:0,ref_key:"nodesMapRef",ref:n,class:x(Se+"-nodes-map")},[$(Ce,{onHandleHeader:t},{icon:E((()=>[T($(D(R),{icon:"pi pi-times",text:"",rounded:"",severity:"secondary",onClick:o[2]||(o[2]=e=>s.value=!1),size:"small"},null,512),[[D(P),D(u)("Close"),void 0,{top:!0}]])])),_:1})],2)):S("",!0)],64))}},De={__name:"index",setup(e){const o=a();return f((e=>{o.watchGraph()})),(e,o)=>(y(),_("div",{class:x("comfyui-easyuse-map")},[$(Ce)]))}},$e="Comfy.UseNewMenu",Ee={__name:"App",setup(e){const o=g(null);return f((e=>{try{(async()=>{l("EasyUse.NodesMap.Enable",null)&&s.extensionManager.registerSidebarTab({id:c,icon:"pi pi-sitemap",title:u("NodesMap",!0),tooltip:u("NodesMap",!0),type:"custom",render:e=>{e.style.height="100%",U(L(De,{}),e)}});const e=await m.fetchApi("/easyuse/version");if(200===e.status){let o=await e.json();o.version&&s.registerExtension({name:"EasyUse Version",aboutPageBadges:[{label:"EasyUse v"+o.version,url:"https://github.com/yolain/ComfyUI-Easy-Use",icon:"pi pi-github"}]})}o.value=l($e),v($e,(e=>{o.value=e}))})()}catch(n){}})),(e,s)=>(y(),_(w,null,[$(se),s[0]||(s[0]=M("img",{id:"easyuse-model-thumbnail"},null,-1)),"Disabled"==o.value?(y(),N(Ne,{key:0})):S("",!0)],64))}},Ae=null==(e=document.getElementsByClassName("graph-canvas-container"))?void 0:e[0],Ve=document.createElement("div");Ve.id="comfyui-easyuse-components",Ae?Ae.append(Ve):document.body.append(Ve);const He=j(Ee);He.use(X),He.use(q()),He.mount("#"+Ve.id); diff --git a/ComfyUI/custom_nodes/ComfyUI-Easy-Use/wildcards/example.txt b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/wildcards/example.txt new file mode 100644 index 0000000000000000000000000000000000000000..59ec397273da1a3828cb76fe43e3d7851ab5ddbd --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-Easy-Use/wildcards/example.txt @@ -0,0 +1,10 @@ +blue +red +yellow +green +brown +pink +purple +orange +black +white \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-GGUF/.gitignore b/ComfyUI/custom_nodes/ComfyUI-GGUF/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..44fe82fdbad306e07e27f784d6c906f0708956e2 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-GGUF/.gitignore @@ -0,0 +1,167 @@ +*.bin +*.gguf +*.safetensors +tools/llama.cpp* + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ diff --git a/ComfyUI/custom_nodes/ComfyUI-GGUF/LICENSE b/ComfyUI/custom_nodes/ComfyUI-GGUF/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..261eeb9e9f8b2b4b0d119366dda99c6fd7d35c64 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-GGUF/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/ComfyUI/custom_nodes/ComfyUI-GGUF/README.md b/ComfyUI/custom_nodes/ComfyUI-GGUF/README.md new file mode 100644 index 0000000000000000000000000000000000000000..6915927e1b61cc3c1113732e4e581e2af627a791 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-GGUF/README.md @@ -0,0 +1,49 @@ +# ComfyUI-GGUF +GGUF Quantization support for native ComfyUI models + +This is currently very much WIP. These custom nodes provide support for model files stored in the GGUF format popularized by [llama.cpp](https://github.com/ggerganov/llama.cpp). + +While quantization wasn't feasible for regular UNET models (conv2d), transformer/DiT models such as flux seem less affected by quantization. This allows running it in much lower bits per weight variable bitrate quants on low-end GPUs. For further VRAM savings, a node to load a quantized version of the T5 text encoder is also included. + +![Comfy_Flux1_dev_Q4_0_GGUF_1024](https://github.com/user-attachments/assets/70d16d97-c522-4ef4-9435-633f128644c8) + +Note: The "Force/Set CLIP Device" is **NOT** part of this node pack. Do not install it if you only have one GPU. Do not set it to cuda:0 then complain about OOM errors if you do not undestand what it is for. There is not need to copy the workflow above, just use your own workflow and replace the stock "Load Diffusion Model" with the "Unet Loader (GGUF)" node. + +## Installation + +> [!IMPORTANT] +> Make sure your ComfyUI is on a recent-enough version to support custom ops when loading the UNET-only. + +To install the custom node normally, git clone this repository into your custom nodes folder (`ComfyUI/custom_nodes`) and install the only dependency for inference (`pip install --upgrade gguf`) + +``` +git clone https://github.com/city96/ComfyUI-GGUF +``` + +To install the custom node on a standalone ComfyUI release, open a CMD inside the "ComfyUI_windows_portable" folder (where your `run_nvidia_gpu.bat` file is) and use the following commands: + +``` +git clone https://github.com/city96/ComfyUI-GGUF ComfyUI/custom_nodes/ComfyUI-GGUF +.\python_embeded\python.exe -s -m pip install -r .\ComfyUI\custom_nodes\ComfyUI-GGUF\requirements.txt +``` + +On MacOS sequoia, torch 2.4.1 seems to be required, as 2.6.X nightly versions cause a "M1 buffer is not large enough" error. See [this issue](https://github.com/city96/ComfyUI-GGUF/issues/107) for more information/workarounds. + +## Usage + +Simply use the GGUF Unet loader found under the `bootleg` category. Place the .gguf model files in your `ComfyUI/models/unet` folder. + +LoRA loading is experimental but it should work with just the built-in LoRA loader node(s). + +Pre-quantized models: + +- [flux1-dev GGUF](https://huggingface.co/city96/FLUX.1-dev-gguf) +- [flux1-schnell GGUF](https://huggingface.co/city96/FLUX.1-schnell-gguf) +- [stable-diffusion-3.5-large GGUF](https://huggingface.co/city96/stable-diffusion-3.5-large-gguf) +- [stable-diffusion-3.5-large-turbo GGUF](https://huggingface.co/city96/stable-diffusion-3.5-large-turbo-gguf) + +Initial support for quantizing T5 has also been added recently, these can be used using the various `*CLIPLoader (gguf)` nodes which can be used inplace of the regular ones. For the CLIP model, use whatever model you were using before for CLIP. The loader can handle both types of files - `gguf` and regular `safetensors`/`bin`. + +- [t5_v1.1-xxl GGUF](https://huggingface.co/city96/t5-v1_1-xxl-encoder-gguf) + +See the instructions in the [tools](https://github.com/city96/ComfyUI-GGUF/tree/main/tools) folder for how to create your own quants. diff --git a/ComfyUI/custom_nodes/ComfyUI-GGUF/__init__.py b/ComfyUI/custom_nodes/ComfyUI-GGUF/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a03726e3b0a08957ded67cdd21beb9544a3f6e4d --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-GGUF/__init__.py @@ -0,0 +1,9 @@ +# only import if running as a custom node +try: + import comfy.utils +except ImportError: + pass +else: + from .nodes import NODE_CLASS_MAPPINGS + NODE_DISPLAY_NAME_MAPPINGS = {k:v.TITLE for k,v in NODE_CLASS_MAPPINGS.items()} + __all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS'] diff --git a/ComfyUI/custom_nodes/ComfyUI-GGUF/__pycache__/__init__.cpython-312.pyc b/ComfyUI/custom_nodes/ComfyUI-GGUF/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4f66b8f2667a41ab254bb1bb7a85b742ee09a386 Binary files /dev/null and b/ComfyUI/custom_nodes/ComfyUI-GGUF/__pycache__/__init__.cpython-312.pyc differ diff --git a/ComfyUI/custom_nodes/ComfyUI-GGUF/__pycache__/dequant.cpython-312.pyc b/ComfyUI/custom_nodes/ComfyUI-GGUF/__pycache__/dequant.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..23124589683a4e55f5e220d6ea9a2ea69849a376 Binary files /dev/null and b/ComfyUI/custom_nodes/ComfyUI-GGUF/__pycache__/dequant.cpython-312.pyc differ diff --git a/ComfyUI/custom_nodes/ComfyUI-GGUF/__pycache__/loader.cpython-312.pyc b/ComfyUI/custom_nodes/ComfyUI-GGUF/__pycache__/loader.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..266532582d068dd36a9fcd0eba91222325b7b2d3 Binary files /dev/null and b/ComfyUI/custom_nodes/ComfyUI-GGUF/__pycache__/loader.cpython-312.pyc differ diff --git a/ComfyUI/custom_nodes/ComfyUI-GGUF/__pycache__/nodes.cpython-312.pyc b/ComfyUI/custom_nodes/ComfyUI-GGUF/__pycache__/nodes.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c94ceea82e8df520ede625e39da2b67bb8aff27f Binary files /dev/null and b/ComfyUI/custom_nodes/ComfyUI-GGUF/__pycache__/nodes.cpython-312.pyc differ diff --git a/ComfyUI/custom_nodes/ComfyUI-GGUF/__pycache__/ops.cpython-312.pyc b/ComfyUI/custom_nodes/ComfyUI-GGUF/__pycache__/ops.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d00b08a82721579859fca2b7fa137f55528ce181 Binary files /dev/null and b/ComfyUI/custom_nodes/ComfyUI-GGUF/__pycache__/ops.cpython-312.pyc differ diff --git a/ComfyUI/custom_nodes/ComfyUI-GGUF/dequant.py b/ComfyUI/custom_nodes/ComfyUI-GGUF/dequant.py new file mode 100644 index 0000000000000000000000000000000000000000..78f5f266aaffd1a7f2502e0bf924a8260e7cf9b4 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-GGUF/dequant.py @@ -0,0 +1,301 @@ +# (c) City96 || Apache-2.0 (apache.org/licenses/LICENSE-2.0) +import gguf +import torch +from tqdm import tqdm + + +TORCH_COMPATIBLE_QTYPES = (None, gguf.GGMLQuantizationType.F32, gguf.GGMLQuantizationType.F16) + +def is_torch_compatible(tensor): + return tensor is None or getattr(tensor, "tensor_type", None) in TORCH_COMPATIBLE_QTYPES + +def is_quantized(tensor): + return not is_torch_compatible(tensor) + +def dequantize_tensor(tensor, dtype=None, dequant_dtype=None): + qtype = getattr(tensor, "tensor_type", None) + oshape = getattr(tensor, "tensor_shape", tensor.shape) + + if qtype in TORCH_COMPATIBLE_QTYPES: + return tensor.to(dtype) + elif qtype in dequantize_functions: + dequant_dtype = dtype if dequant_dtype == "target" else dequant_dtype + return dequantize(tensor.data, qtype, oshape, dtype=dequant_dtype).to(dtype) + else: + # this is incredibly slow + tqdm.write(f"Falling back to numpy dequant for qtype: {getattr(qtype, 'name', repr(qtype))}") + new = gguf.quants.dequantize(tensor.cpu().numpy(), qtype) + return torch.from_numpy(new).to(tensor.device, dtype=dtype) + +def dequantize(data, qtype, oshape, dtype=None): + """ + Dequantize tensor back to usable shape/dtype + """ + block_size, type_size = gguf.GGML_QUANT_SIZES[qtype] + dequantize_blocks = dequantize_functions[qtype] + + rows = data.reshape( + (-1, data.shape[-1]) + ).view(torch.uint8) + + n_blocks = rows.numel() // type_size + blocks = rows.reshape((n_blocks, type_size)) + blocks = dequantize_blocks(blocks, block_size, type_size, dtype) + return blocks.reshape(oshape) + +def to_uint32(x): + # no uint32 :( + x = x.view(torch.uint8).to(torch.int32) + return (x[:, 0] | x[:, 1] << 8 | x[:, 2] << 16 | x[:, 3] << 24).unsqueeze(1) + +def to_uint16(x): + x = x.view(torch.uint8).to(torch.int32) + return (x[:, 0] | x[:, 1] << 8).unsqueeze(1) + +def split_block_dims(blocks, *args): + n_max = blocks.shape[1] + dims = list(args) + [n_max - sum(args)] + return torch.split(blocks, dims, dim=1) + +# Full weights # +def dequantize_blocks_BF16(blocks, block_size, type_size, dtype=None): + return (blocks.view(torch.int16).to(torch.int32) << 16).view(torch.float32) + +# Legacy Quants # +def dequantize_blocks_Q8_0(blocks, block_size, type_size, dtype=None): + d, x = split_block_dims(blocks, 2) + d = d.view(torch.float16).to(dtype) + x = x.view(torch.int8) + return (d * x) + +def dequantize_blocks_Q5_1(blocks, block_size, type_size, dtype=None): + n_blocks = blocks.shape[0] + + d, m, qh, qs = split_block_dims(blocks, 2, 2, 4) + d = d.view(torch.float16).to(dtype) + m = m.view(torch.float16).to(dtype) + qh = to_uint32(qh) + + qh = qh.reshape((n_blocks, 1)) >> torch.arange(32, device=d.device, dtype=torch.int32).reshape(1, 32) + ql = qs.reshape((n_blocks, -1, 1, block_size // 2)) >> torch.tensor([0, 4], device=d.device, dtype=torch.uint8).reshape(1, 1, 2, 1) + qh = (qh & 1).to(torch.uint8) + ql = (ql & 0x0F).reshape((n_blocks, -1)) + + qs = (ql | (qh << 4)) + return (d * qs) + m + +def dequantize_blocks_Q5_0(blocks, block_size, type_size, dtype=None): + n_blocks = blocks.shape[0] + + d, qh, qs = split_block_dims(blocks, 2, 4) + d = d.view(torch.float16).to(dtype) + qh = to_uint32(qh) + + qh = qh.reshape(n_blocks, 1) >> torch.arange(32, device=d.device, dtype=torch.int32).reshape(1, 32) + ql = qs.reshape(n_blocks, -1, 1, block_size // 2) >> torch.tensor([0, 4], device=d.device, dtype=torch.uint8).reshape(1, 1, 2, 1) + + qh = (qh & 1).to(torch.uint8) + ql = (ql & 0x0F).reshape(n_blocks, -1) + + qs = (ql | (qh << 4)).to(torch.int8) - 16 + return (d * qs) + +def dequantize_blocks_Q4_1(blocks, block_size, type_size, dtype=None): + n_blocks = blocks.shape[0] + + d, m, qs = split_block_dims(blocks, 2, 2) + d = d.view(torch.float16).to(dtype) + m = m.view(torch.float16).to(dtype) + + qs = qs.reshape((n_blocks, -1, 1, block_size // 2)) >> torch.tensor([0, 4], device=d.device, dtype=torch.uint8).reshape(1, 1, 2, 1) + qs = (qs & 0x0F).reshape(n_blocks, -1) + + return (d * qs) + m + +def dequantize_blocks_Q4_0(blocks, block_size, type_size, dtype=None): + n_blocks = blocks.shape[0] + + d, qs = split_block_dims(blocks, 2) + d = d.view(torch.float16).to(dtype) + + qs = qs.reshape((n_blocks, -1, 1, block_size // 2)) >> torch.tensor([0, 4], device=d.device, dtype=torch.uint8).reshape((1, 1, 2, 1)) + qs = (qs & 0x0F).reshape((n_blocks, -1)).to(torch.int8) - 8 + return (d * qs) + +# K Quants # +QK_K = 256 +K_SCALE_SIZE = 12 + +def get_scale_min(scales): + n_blocks = scales.shape[0] + scales = scales.view(torch.uint8) + scales = scales.reshape((n_blocks, 3, 4)) + + d, m, m_d = torch.split(scales, scales.shape[-2] // 3, dim=-2) + + sc = torch.cat([d & 0x3F, (m_d & 0x0F) | ((d >> 2) & 0x30)], dim=-1) + min = torch.cat([m & 0x3F, (m_d >> 4) | ((m >> 2) & 0x30)], dim=-1) + + return (sc.reshape((n_blocks, 8)), min.reshape((n_blocks, 8))) + +def dequantize_blocks_Q6_K(blocks, block_size, type_size, dtype=None): + n_blocks = blocks.shape[0] + + ql, qh, scales, d, = split_block_dims(blocks, QK_K // 2, QK_K // 4, QK_K // 16) + + scales = scales.view(torch.int8).to(dtype) + d = d.view(torch.float16).to(dtype) + d = (d * scales).reshape((n_blocks, QK_K // 16, 1)) + + ql = ql.reshape((n_blocks, -1, 1, 64)) >> torch.tensor([0, 4], device=d.device, dtype=torch.uint8).reshape((1, 1, 2, 1)) + ql = (ql & 0x0F).reshape((n_blocks, -1, 32)) + qh = qh.reshape((n_blocks, -1, 1, 32)) >> torch.tensor([0, 2, 4, 6], device=d.device, dtype=torch.uint8).reshape((1, 1, 4, 1)) + qh = (qh & 0x03).reshape((n_blocks, -1, 32)) + q = (ql | (qh << 4)).to(torch.int8) - 32 + q = q.reshape((n_blocks, QK_K // 16, -1)) + + return (d * q).reshape((n_blocks, QK_K)) + +def dequantize_blocks_Q5_K(blocks, block_size, type_size, dtype=None): + n_blocks = blocks.shape[0] + + d, dmin, scales, qh, qs = split_block_dims(blocks, 2, 2, K_SCALE_SIZE, QK_K // 8) + + d = d.view(torch.float16).to(dtype) + dmin = dmin.view(torch.float16).to(dtype) + + sc, m = get_scale_min(scales) + + d = (d * sc).reshape((n_blocks, -1, 1)) + dm = (dmin * m).reshape((n_blocks, -1, 1)) + + ql = qs.reshape((n_blocks, -1, 1, 32)) >> torch.tensor([0, 4], device=d.device, dtype=torch.uint8).reshape((1, 1, 2, 1)) + qh = qh.reshape((n_blocks, -1, 1, 32)) >> torch.tensor([i for i in range(8)], device=d.device, dtype=torch.uint8).reshape((1, 1, 8, 1)) + ql = (ql & 0x0F).reshape((n_blocks, -1, 32)) + qh = (qh & 0x01).reshape((n_blocks, -1, 32)) + q = (ql | (qh << 4)) + + return (d * q - dm).reshape((n_blocks, QK_K)) + +def dequantize_blocks_Q4_K(blocks, block_size, type_size, dtype=None): + n_blocks = blocks.shape[0] + + d, dmin, scales, qs = split_block_dims(blocks, 2, 2, K_SCALE_SIZE) + d = d.view(torch.float16).to(dtype) + dmin = dmin.view(torch.float16).to(dtype) + + sc, m = get_scale_min(scales) + + d = (d * sc).reshape((n_blocks, -1, 1)) + dm = (dmin * m).reshape((n_blocks, -1, 1)) + + qs = qs.reshape((n_blocks, -1, 1, 32)) >> torch.tensor([0, 4], device=d.device, dtype=torch.uint8).reshape((1, 1, 2, 1)) + qs = (qs & 0x0F).reshape((n_blocks, -1, 32)) + + return (d * qs - dm).reshape((n_blocks, QK_K)) + +def dequantize_blocks_Q3_K(blocks, block_size, type_size, dtype=None): + n_blocks = blocks.shape[0] + + hmask, qs, scales, d = split_block_dims(blocks, QK_K // 8, QK_K // 4, 12) + d = d.view(torch.float16).to(dtype) + + lscales, hscales = scales[:, :8], scales[:, 8:] + lscales = lscales.reshape((n_blocks, 1, 8)) >> torch.tensor([0, 4], device=d.device, dtype=torch.uint8).reshape((1, 2, 1)) + lscales = lscales.reshape((n_blocks, 16)) + hscales = hscales.reshape((n_blocks, 1, 4)) >> torch.tensor([0, 2, 4, 6], device=d.device, dtype=torch.uint8).reshape((1, 4, 1)) + hscales = hscales.reshape((n_blocks, 16)) + scales = (lscales & 0x0F) | ((hscales & 0x03) << 4) + scales = (scales.to(torch.int8) - 32) + + dl = (d * scales).reshape((n_blocks, 16, 1)) + + ql = qs.reshape((n_blocks, -1, 1, 32)) >> torch.tensor([0, 2, 4, 6], device=d.device, dtype=torch.uint8).reshape((1, 1, 4, 1)) + qh = hmask.reshape(n_blocks, -1, 1, 32) >> torch.tensor([i for i in range(8)], device=d.device, dtype=torch.uint8).reshape((1, 1, 8, 1)) + ql = ql.reshape((n_blocks, 16, QK_K // 16)) & 3 + qh = (qh.reshape((n_blocks, 16, QK_K // 16)) & 1) ^ 1 + q = (ql.to(torch.int8) - (qh << 2).to(torch.int8)) + + return (dl * q).reshape((n_blocks, QK_K)) + +def dequantize_blocks_Q2_K(blocks, block_size, type_size, dtype=None): + n_blocks = blocks.shape[0] + + scales, qs, d, dmin = split_block_dims(blocks, QK_K // 16, QK_K // 4, 2) + d = d.view(torch.float16).to(dtype) + dmin = dmin.view(torch.float16).to(dtype) + + # (n_blocks, 16, 1) + dl = (d * (scales & 0xF)).reshape((n_blocks, QK_K // 16, 1)) + ml = (dmin * (scales >> 4)).reshape((n_blocks, QK_K // 16, 1)) + + shift = torch.tensor([0, 2, 4, 6], device=d.device, dtype=torch.uint8).reshape((1, 1, 4, 1)) + + qs = (qs.reshape((n_blocks, -1, 1, 32)) >> shift) & 3 + qs = qs.reshape((n_blocks, QK_K // 16, 16)) + qs = dl * qs - ml + + return qs.reshape((n_blocks, -1)) + +# IQ quants +KVALUES = torch.tensor([-127, -104, -83, -65, -49, -35, -22, -10, 1, 13, 25, 38, 53, 69, 89, 113], dtype=torch.int8) + +def dequantize_blocks_IQ4_NL(blocks, block_size, type_size, dtype=None): + n_blocks = blocks.shape[0] + + d, qs = split_block_dims(blocks, 2) + d = d.view(torch.float16).to(dtype) + + qs = qs.reshape((n_blocks, -1, 1, block_size//2)) >> torch.tensor([0, 4], device=d.device, dtype=torch.uint8).reshape((1, 1, 2, 1)) + qs = (qs & 0x0F).reshape((n_blocks, -1, 1)).to(torch.int64) + + kvalues = KVALUES.to(qs.device).expand(*qs.shape[:-1], 16) + qs = torch.gather(kvalues, dim=-1, index=qs).reshape((n_blocks, -1)) + del kvalues # should still be view, but just to be safe + + return (d * qs) + +def dequantize_blocks_IQ4_XS(blocks, block_size, type_size, dtype=None): + n_blocks = blocks.shape[0] + d, scales_h, scales_l, qs = split_block_dims(blocks, 2, 2, QK_K // 64) + d = d.view(torch.float16).to(dtype) + scales_h = to_uint16(scales_h) + + shift_a = torch.tensor([0, 4], device=d.device, dtype=torch.uint8).reshape((1, 1, 2)) + shift_b = torch.tensor([2 * i for i in range(QK_K // 32)], device=d.device, dtype=torch.uint8).reshape((1, -1, 1)) + + scales_l = scales_l.reshape((n_blocks, -1, 1)) >> shift_a.reshape((1, 1, 2)) + scales_h = scales_h.reshape((n_blocks, -1, 1)) >> shift_b.reshape((1, -1, 1)) + + scales_l = scales_l.reshape((n_blocks, -1)) & 0x0F + scales_h = scales_h.reshape((n_blocks, -1)).to(torch.uint8) & 0x03 + + scales = (scales_l | (scales_h << 4)).to(torch.int8) - 32 + dl = (d * scales.to(dtype)).reshape((n_blocks, -1, 1)) + + qs = qs.reshape((n_blocks, -1, 1, 16)) >> shift_a.reshape((1, 1, 2, 1)) + qs = qs.reshape((n_blocks, -1, 32, 1)) & 0x0F + + kvalues = KVALUES.to(qs.device).expand(*qs.shape[:-1], 16) + qs = torch.gather(kvalues, dim=-1, index=qs.to(torch.int64)).reshape((n_blocks, -1, 32)) + del kvalues # see IQ4_NL + del shift_a + del shift_b + + return (dl * qs).reshape((n_blocks, -1)) + +dequantize_functions = { + gguf.GGMLQuantizationType.BF16: dequantize_blocks_BF16, + gguf.GGMLQuantizationType.Q8_0: dequantize_blocks_Q8_0, + gguf.GGMLQuantizationType.Q5_1: dequantize_blocks_Q5_1, + gguf.GGMLQuantizationType.Q5_0: dequantize_blocks_Q5_0, + gguf.GGMLQuantizationType.Q4_1: dequantize_blocks_Q4_1, + gguf.GGMLQuantizationType.Q4_0: dequantize_blocks_Q4_0, + gguf.GGMLQuantizationType.Q6_K: dequantize_blocks_Q6_K, + gguf.GGMLQuantizationType.Q5_K: dequantize_blocks_Q5_K, + gguf.GGMLQuantizationType.Q4_K: dequantize_blocks_Q4_K, + gguf.GGMLQuantizationType.Q3_K: dequantize_blocks_Q3_K, + gguf.GGMLQuantizationType.Q2_K: dequantize_blocks_Q2_K, + gguf.GGMLQuantizationType.IQ4_NL: dequantize_blocks_IQ4_NL, + gguf.GGMLQuantizationType.IQ4_XS: dequantize_blocks_IQ4_XS, +} diff --git a/ComfyUI/custom_nodes/ComfyUI-GGUF/loader.py b/ComfyUI/custom_nodes/ComfyUI-GGUF/loader.py new file mode 100644 index 0000000000000000000000000000000000000000..7cefb113bb7f9d85585f6478ebbece9f5f3e9919 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-GGUF/loader.py @@ -0,0 +1,506 @@ +# (c) City96 || Apache-2.0 (apache.org/licenses/LICENSE-2.0) +import warnings +import logging +import torch +import gguf +import re +import os + +from .ops import GGMLTensor +from .dequant import is_quantized, dequantize_tensor + +IMG_ARCH_LIST = {"flux", "sd1", "sdxl", "sd3", "aura", "hidream", "cosmos", "ltxv", "hyvid", "wan", "lumina2", "qwen_image"} +TXT_ARCH_LIST = {"t5", "t5encoder", "llama", "qwen2vl", "qwen3", "qwen3vl", "gemma3"} +VIS_TYPE_LIST = {"clip-vision", "mmproj"} + +def get_orig_shape(reader, tensor_name): + field_key = f"comfy.gguf.orig_shape.{tensor_name}" + field = reader.get_field(field_key) + if field is None: + return None + # Has original shape metadata, so we try to decode it. + if len(field.types) != 2 or field.types[0] != gguf.GGUFValueType.ARRAY or field.types[1] != gguf.GGUFValueType.INT32: + raise TypeError(f"Bad original shape metadata for {field_key}: Expected ARRAY of INT32, got {field.types}") + return torch.Size(tuple(int(field.parts[part_idx][0]) for part_idx in field.data)) + +def get_field(reader, field_name, field_type): + field = reader.get_field(field_name) + if field is None: + return None + elif field_type == str: + # extra check here as this is used for checking arch string + if len(field.types) != 1 or field.types[0] != gguf.GGUFValueType.STRING: + raise TypeError(f"Bad type for GGUF {field_name} key: expected string, got {field.types!r}") + return str(field.parts[field.data[-1]], encoding="utf-8") + elif field_type in [int, float, bool]: + return field_type(field.parts[field.data[-1]].item()) + else: + raise TypeError(f"Unknown field type {field_type}") + +def get_list_field(reader, field_name, field_type): + field = reader.get_field(field_name) + if field is None: + return None + elif field_type == str: + return tuple(str(field.parts[part_idx], encoding="utf-8") for part_idx in field.data) + elif field_type in [int, float, bool]: + return tuple(field_type(field.parts[part_idx][0]) for part_idx in field.data) + else: + raise TypeError(f"Unknown field type {field_type}") + +def get_gguf_metadata(reader): + """Extract all simple metadata fields like safetensors""" + metadata = {} + for field_name in reader.fields: + try: + field = reader.get_field(field_name) + if len(field.types) == 1: # Simple scalar fields only + if field.types[0] == gguf.GGUFValueType.STRING: + metadata[field_name] = str(field.parts[field.data[-1]], "utf-8") + elif field.types[0] == gguf.GGUFValueType.INT32: + metadata[field_name] = int(field.parts[field.data[-1]]) + elif field.types[0] == gguf.GGUFValueType.F32: + metadata[field_name] = float(field.parts[field.data[-1]]) + elif field.types[0] == gguf.GGUFValueType.BOOL: + metadata[field_name] = bool(field.parts[field.data[-1]]) + except: + continue + return metadata + +def gguf_sd_loader(path, handle_prefix="model.diffusion_model.", is_text_model=False): + """ + Read state dict as fake tensors + """ + reader = gguf.GGUFReader(path) + + # filter and strip prefix + has_prefix = False + if handle_prefix is not None: + prefix_len = len(handle_prefix) + tensor_names = set(tensor.name for tensor in reader.tensors) + has_prefix = any(s.startswith(handle_prefix) for s in tensor_names) + + tensors = [] + for tensor in reader.tensors: + sd_key = tensor_name = tensor.name + if has_prefix: + if not tensor_name.startswith(handle_prefix): + continue + sd_key = tensor_name[prefix_len:] + tensors.append((sd_key, tensor)) + + # detect and verify architecture + compat = None + arch_str = get_field(reader, "general.architecture", str) + type_str = get_field(reader, "general.type", str) + if arch_str in [None, "pig", "cow"]: + if is_text_model: + raise ValueError(f"This gguf file is incompatible with llama.cpp!\nConsider using safetensors or a compatible gguf file\n({path})") + compat = "sd.cpp" if arch_str is None else arch_str + # import here to avoid changes to convert.py breaking regular models + from .tools.convert import detect_arch + try: + arch_str = detect_arch(set(val[0] for val in tensors)).arch + except Exception as e: + raise ValueError(f"This model is not currently supported - ({e})") + elif arch_str not in TXT_ARCH_LIST and is_text_model: + if type_str not in VIS_TYPE_LIST: + raise ValueError(f"Unexpected text model architecture type in GGUF file: {arch_str!r}") + elif arch_str not in IMG_ARCH_LIST and not is_text_model: + raise ValueError(f"Unexpected architecture type in GGUF file: {arch_str!r}") + + if compat: + logging.warning(f"Warning: This gguf model file is loaded in compatibility mode '{compat}' [arch:{arch_str}]") + + # main loading loop + state_dict = {} + qtype_dict = {} + for sd_key, tensor in tensors: + tensor_name = tensor.name + # torch_tensor = torch.from_numpy(tensor.data) # mmap + + # NOTE: line above replaced with this block to avoid persistent numpy warning about mmap + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", message="The given NumPy array is not writable") + torch_tensor = torch.from_numpy(tensor.data) # mmap + + shape = get_orig_shape(reader, tensor_name) + if shape is None: + shape = torch.Size(tuple(int(v) for v in reversed(tensor.shape))) + # Workaround for stable-diffusion.cpp SDXL detection. + if compat == "sd.cpp" and arch_str == "sdxl": + if any([tensor_name.endswith(x) for x in (".proj_in.weight", ".proj_out.weight")]): + while len(shape) > 2 and shape[-1] == 1: + shape = shape[:-1] + + # add to state dict + if tensor.tensor_type in {gguf.GGMLQuantizationType.F32, gguf.GGMLQuantizationType.F16}: + torch_tensor = torch_tensor.view(*shape) + state_dict[sd_key] = GGMLTensor(torch_tensor, tensor_type=tensor.tensor_type, tensor_shape=shape) + + # 1D tensors shouldn't be quantized, this is a fix for BF16 + if len(shape) <= 1 and tensor.tensor_type == gguf.GGMLQuantizationType.BF16: + state_dict[sd_key] = dequantize_tensor(state_dict[sd_key], dtype=torch.float32) + + # keep track of loaded tensor types + tensor_type_str = getattr(tensor.tensor_type, "name", repr(tensor.tensor_type)) + qtype_dict[tensor_type_str] = qtype_dict.get(tensor_type_str, 0) + 1 + + # print loaded tensor type counts + logging.info("gguf qtypes: " + ", ".join(f"{k} ({v})" for k, v in qtype_dict.items())) + + # mark largest tensor for vram estimation + qsd = {k:v for k,v in state_dict.items() if is_quantized(v)} + if len(qsd) > 0: + max_key = max(qsd.keys(), key=lambda k: qsd[k].numel()) + state_dict[max_key].is_largest_weight = True + + # extra info to return + extra = { + "arch_str": arch_str, + "metadata": get_gguf_metadata(reader) + } + return (state_dict, extra) + +# for remapping llama.cpp -> original key names +T5_SD_MAP = { + "enc.": "encoder.", + ".blk.": ".block.", + "token_embd": "shared", + "output_norm": "final_layer_norm", + "attn_q": "layer.0.SelfAttention.q", + "attn_k": "layer.0.SelfAttention.k", + "attn_v": "layer.0.SelfAttention.v", + "attn_o": "layer.0.SelfAttention.o", + "attn_norm": "layer.0.layer_norm", + "attn_rel_b": "layer.0.SelfAttention.relative_attention_bias", + "ffn_up": "layer.1.DenseReluDense.wi_1", + "ffn_down": "layer.1.DenseReluDense.wo", + "ffn_gate": "layer.1.DenseReluDense.wi_0", + "ffn_norm": "layer.1.layer_norm", +} + +LLAMA_SD_MAP = { + "blk.": "model.layers.", + "attn_norm": "input_layernorm", + "attn_q_norm.": "self_attn.q_norm.", + "attn_k_norm.": "self_attn.k_norm.", + "attn_v_norm.": "self_attn.v_norm.", + "attn_q": "self_attn.q_proj", + "attn_k": "self_attn.k_proj", + "attn_v": "self_attn.v_proj", + "attn_output": "self_attn.o_proj", + "ffn_up": "mlp.up_proj", + "ffn_down": "mlp.down_proj", + "ffn_gate": "mlp.gate_proj", + "ffn_norm": "post_attention_layernorm", + "token_embd": "model.embed_tokens", + "output_norm": "model.norm", + "output.weight": "lm_head.weight", +} + +GEMMA3_SD_MAP = LLAMA_SD_MAP.copy() +GEMMA3_SD_MAP.update({ + "ffn_norm": "pre_feedforward_layernorm", + "post_ffw_norm": "post_feedforward_layernorm", + "post_attention_norm": "post_attention_layernorm", +}) + +CLIP_VISION_SD_MAP = { + "mm.": "visual.merger.mlp.", + "v.post_ln.": "visual.merger.ln_q.", + "v.patch_embd": "visual.patch_embed.proj", + "v.blk.": "visual.blocks.", + "ffn_up": "mlp.up_proj", + "ffn_down": "mlp.down_proj", + "ffn_gate": "mlp.gate_proj", + "attn_out.": "attn.proj.", + "ln1.": "norm1.", + "ln2.": "norm2.", +} + +def sd_map_replace(raw_sd, key_map): + sd = {} + for k,v in raw_sd.items(): + for s,d in key_map.items(): + k = k.replace(s,d) + sd[k] = v + return sd + +def llama_permute(raw_sd, n_head, n_head_kv): + # Reverse version of LlamaModel.permute in llama.cpp convert script + sd = {} + permute = lambda x,h: x.reshape(h, x.shape[0] // h // 2, 2, *x.shape[1:]).swapaxes(1, 2).reshape(x.shape) + for k,v in raw_sd.items(): + if k.endswith(("q_proj.weight", "q_proj.bias")): + v.data = permute(v.data, n_head) + if k.endswith(("k_proj.weight", "k_proj.bias")): + v.data = permute(v.data, n_head_kv) + sd[k] = v + return sd + +def gemma3_norm_corrections(sd): + # Reverse change from Gemma3Model modify_tensors in llama.cpp convert script + norm_patterns = [ + "input_layernorm.weight", + "post_attention_layernorm.weight", + "pre_feedforward_layernorm.weight", + "post_feedforward_layernorm.weight", + "self_attn.q_norm.weight", + "self_attn.k_norm.weight", + "model.norm.weight" + ] + corrected = 0 + for key in list(sd.keys()): + if any(p in key for p in norm_patterns): + if is_quantized(sd[key]): + sd[key] = dequantize_tensor(sd[key], dtype=torch.float32) - 1.0 + else: + sd[key] = sd[key].float() - 1.0 + corrected += 1 + #logging.info(f"Gemma3: Applied -1 norm correction to {corrected} tensors") + return sd + +def strip_quant_suffix(name): + pattern = r"[-_]?(?:ud-)?i?q[0-9]_[a-z0-9_\-]{1,8}$" + match = re.search(pattern, name, re.IGNORECASE) + if match: + name = name[:match.start()] + return name + +def gguf_mmproj_loader(path): + # Reverse version of Qwen2VLVisionModel.modify_tensors + logging.info("Attenpting to find mmproj file for text encoder...") + + # get name to match w/o quant suffix + tenc_fname = os.path.basename(path) + tenc = os.path.splitext(tenc_fname)[0].lower() + tenc = strip_quant_suffix(tenc) + + # try and find matching mmproj + target = [] + root = os.path.dirname(path) + for fname in os.listdir(root): + name, ext = os.path.splitext(fname) + if ext.lower() != ".gguf": + continue + if "mmproj" not in name.lower(): + continue + if tenc in name.lower(): + target.append(fname) + + if len(target) == 0: + logging.error(f"Error: Can't find mmproj file for '{tenc_fname}' (matching:'{tenc}')! Qwen-Image-Edit will be broken!") + return {} + if len(target) > 1: + logging.error(f"Ambiguous mmproj for text encoder '{tenc_fname}', will use first match.") + + logging.info(f"Using mmproj '{target[0]}' for text encoder '{tenc_fname}'.") + target = os.path.join(root, target[0]) + vsd, _ = gguf_sd_loader(target, is_text_model=True) + + # concat 4D to 5D + if "v.patch_embd.weight.1" in vsd: + w1 = dequantize_tensor(vsd.pop("v.patch_embd.weight"), dtype=torch.float32) + w2 = dequantize_tensor(vsd.pop("v.patch_embd.weight.1"), dtype=torch.float32) + vsd["v.patch_embd.weight"] = torch.stack([w1, w2], dim=2) + + # run main replacement + vsd = sd_map_replace(vsd, CLIP_VISION_SD_MAP) + + # handle split Q/K/V + if "visual.blocks.0.attn_q.weight" in vsd: + attns = {} + # filter out attentions + group + for k,v in vsd.items(): + if any(x in k for x in ["attn_q", "attn_k", "attn_v"]): + k_attn, k_name = k.rsplit(".attn_", 1) + k_attn += ".attn.qkv." + k_name.split(".")[-1] + if k_attn not in attns: + attns[k_attn] = {} + attns[k_attn][k_name] = dequantize_tensor( + v, dtype=(torch.bfloat16 if is_quantized(v) else torch.float16) + ) + + # recombine + for k,v in attns.items(): + suffix = k.split(".")[-1] + vsd[k] = torch.cat([ + v[f"q.{suffix}"], + v[f"k.{suffix}"], + v[f"v.{suffix}"], + ], dim=0) + del attns + + return vsd + +def gguf_tokenizer_loader(path, temb_shape): + # convert gguf tokenizer to spiece + logging.info("Attempting to recreate sentencepiece tokenizer from GGUF file metadata...") + try: + from sentencepiece import sentencepiece_model_pb2 as model + except ImportError: + raise ImportError("Please make sure sentencepiece and protobuf are installed.\npip install sentencepiece protobuf") + spm = model.ModelProto() + + reader = gguf.GGUFReader(path) + + if get_field(reader, "tokenizer.ggml.model", str) == "t5": + if temb_shape == (256384, 4096): # probably UMT5 + spm.trainer_spec.model_type == 1 # Unigram (do we have a T5 w/ BPE?) + else: + raise NotImplementedError("Unknown model, can't set tokenizer!") + else: + raise NotImplementedError("Unknown model, can't set tokenizer!") + + spm.normalizer_spec.add_dummy_prefix = get_field(reader, "tokenizer.ggml.add_space_prefix", bool) + spm.normalizer_spec.remove_extra_whitespaces = get_field(reader, "tokenizer.ggml.remove_extra_whitespaces", bool) + + tokens = get_list_field(reader, "tokenizer.ggml.tokens", str) + scores = get_list_field(reader, "tokenizer.ggml.scores", float) + toktypes = get_list_field(reader, "tokenizer.ggml.token_type", int) + + for idx, (token, score, toktype) in enumerate(zip(tokens, scores, toktypes)): + # # These aren't present in the original? + # if toktype == 5 and idx >= temb_shape[0]%1000): + # continue + + piece = spm.SentencePiece() + piece.piece = token + piece.score = score + piece.type = toktype + spm.pieces.append(piece) + + # unsure if any of these are correct + spm.trainer_spec.byte_fallback = True + spm.trainer_spec.vocab_size = len(tokens) # split off unused? + spm.trainer_spec.max_sentence_length = 4096 + spm.trainer_spec.eos_id = get_field(reader, "tokenizer.ggml.eos_token_id", int) + spm.trainer_spec.pad_id = get_field(reader, "tokenizer.ggml.padding_token_id", int) + + logging.info(f"Created tokenizer with vocab size of {len(spm.pieces)}") + del reader + return torch.ByteTensor(list(spm.SerializeToString())) + +def gguf_tekken_tokenizer_loader(path, temb_shape): + # convert ggml (hf) tokenizer metadata to tekken/comfy data + logging.info("Attempting to recreate tekken tokenizer from GGUF file metadata...") + import json + import base64 + from transformers.convert_slow_tokenizer import bytes_to_unicode + + reader = gguf.GGUFReader(path) + + model_str = get_field(reader, "tokenizer.ggml.model", str) + if model_str == "gpt2": + if temb_shape == (131072, 5120): # probably Mistral + data = { + "config": {"num_vocab_tokens": 150000, "default_vocab_size": 131072}, + "vocab": [], + "special_tokens": [], + } + else: + raise NotImplementedError("Unknown model, can't set tokenizer!") + else: + raise NotImplementedError("Unknown model, can't set tokenizer!") + + tokens = get_list_field(reader, "tokenizer.ggml.tokens", str) + toktypes = get_list_field(reader, "tokenizer.ggml.token_type", int) + + decoder = {v: k for k, v in bytes_to_unicode().items()} + for idx, (token, toktype) in enumerate(zip(tokens, toktypes)): + if toktype == 3: + data["special_tokens"].append( + {'rank': idx, 'token_str': token, 'is_control': True} + ) + else: + tok = bytes([decoder[char] for char in token]) + data["vocab"].append({ + "rank": len(data["vocab"]), + "token_bytes": base64.b64encode(tok).decode("ascii"), + "token_str": tok.decode("utf-8", errors="replace") # ? + }) + + logging.info(f"Created tekken tokenizer with vocab size of {len(data['vocab'])} (+{len(data['special_tokens'])})") + del reader + return torch.ByteTensor(list(json.dumps(data).encode('utf-8'))) + +def gguf_gemma3_tokenizer_loader(path): + #TODO: merge into gguf_tokenizer_loader + logging.info("Attempting to recreate sentencepiece tokenizer from GGUF file metadata...") + try: + from sentencepiece import sentencepiece_model_pb2 as model + except ImportError: + raise ImportError("Please install sentencepiece and protobuf.\npip install sentencepiece protobuf") + spm = model.ModelProto() + reader = gguf.GGUFReader(path) + + spm.normalizer_spec.name = "identity" + spm.normalizer_spec.add_dummy_prefix = False + spm.trainer_spec.model_type = 2 + spm.trainer_spec.input_format = "tsv" + spm.trainer_spec.byte_fallback = True + spm.trainer_spec.max_sentence_length = 4192 + spm.trainer_spec.bos_piece = "" + + tokens = get_list_field(reader, "tokenizer.ggml.tokens", str) + scores = get_list_field(reader, "tokenizer.ggml.scores", float) + toktype = get_list_field(reader, "tokenizer.ggml.token_type", int) + + if not tokens or not scores or not toktype: + raise ValueError("Missing tokenizer metadata") + + for idx in range(len(tokens)): + piece = spm.SentencePiece() + piece.piece = tokens[idx] + if idx == 3: # UNK position + piece.type = 2 # UNK Token + piece.score = 0.0 # UNK Score + else: + piece.type = toktype[idx] + piece.score = scores[idx] + spm.pieces.append(piece) + + spm.trainer_spec.vocab_size = len(spm.pieces) + logging.info(f"Created tokenizer with vocab size of {len(spm.pieces)}") + + del reader + return torch.ByteTensor(list(spm.SerializeToString())) + +def gguf_clip_loader(path): + sd, extra = gguf_sd_loader(path, is_text_model=True) + arch = extra.get("arch_str", None) + if arch in {"t5", "t5encoder"}: + temb_key = "token_embd.weight" + if temb_key in sd and sd[temb_key].shape == (256384, 4096): + # non-standard Comfy-Org tokenizer + sd["spiece_model"] = gguf_tokenizer_loader(path, sd[temb_key].shape) + # TODO: dequantizing token embed here is janky but otherwise we OOM due to tensor being massive. + logging.warning(f"Dequantizing {temb_key} to prevent runtime OOM.") + sd[temb_key] = dequantize_tensor(sd[temb_key], dtype=torch.float16) + sd = sd_map_replace(sd, T5_SD_MAP) + elif arch in {"llama", "qwen2vl", "qwen3", "qwen3vl", "gemma3"}: + # TODO: pass model_options["vocab_size"] to loader somehow + temb_key = "token_embd.weight" + if temb_key in sd and sd[temb_key].shape[0] >= (64 * 1024): + if arch == "llama" and sd[temb_key].shape == (131072, 5120): + # non-standard Comfy-Org tokenizer + sd["tekken_model"] = gguf_tekken_tokenizer_loader(path, sd[temb_key].shape) + elif arch == "gemma3": + sd["spiece_model"] = gguf_gemma3_tokenizer_loader(path) + # See note above for T5. + logging.warning(f"Dequantizing {temb_key} to prevent runtime OOM.") + sd[temb_key] = dequantize_tensor(sd[temb_key], dtype=torch.float16) + if arch == "gemma3": + sd = sd_map_replace(sd, GEMMA3_SD_MAP) + sd = gemma3_norm_corrections(sd) + else: + sd = sd_map_replace(sd, LLAMA_SD_MAP) + if arch == "llama": + sd = llama_permute(sd, 32, 8) # L3 / Mistral + if arch == "qwen2vl": + vsd = gguf_mmproj_loader(path) + sd.update(vsd) + else: + pass + return sd diff --git a/ComfyUI/custom_nodes/ComfyUI-GGUF/nodes.py b/ComfyUI/custom_nodes/ComfyUI-GGUF/nodes.py new file mode 100644 index 0000000000000000000000000000000000000000..468351467cf0edb3070f2c1d48319631fcc39bb4 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-GGUF/nodes.py @@ -0,0 +1,329 @@ +# (c) City96 || Apache-2.0 (apache.org/licenses/LICENSE-2.0) +import torch +import logging +import inspect +import collections + +import nodes +import comfy.sd +import comfy.lora +import comfy.float +import comfy.utils +import comfy.model_patcher +import comfy.model_management +import folder_paths + +from .ops import GGMLOps, move_patch_to_device +from .loader import gguf_sd_loader, gguf_clip_loader +from .dequant import is_quantized, is_torch_compatible + +def update_folder_names_and_paths(key, targets=[]): + # check for existing key + base = folder_paths.folder_names_and_paths.get(key, ([], {})) + base = base[0] if isinstance(base[0], (list, set, tuple)) else [] + # find base key & add w/ fallback, sanity check + warning + target = next((x for x in targets if x in folder_paths.folder_names_and_paths), targets[0]) + orig, _ = folder_paths.folder_names_and_paths.get(target, ([], {})) + folder_paths.folder_names_and_paths[key] = (orig or base, {".gguf"}) + if base and base != orig: + logging.warning(f"Unknown file list already present on key {key}: {base}") + +# Add a custom keys for files ending in .gguf +update_folder_names_and_paths("unet_gguf", ["diffusion_models", "unet"]) +update_folder_names_and_paths("clip_gguf", ["text_encoders", "clip"]) + +class GGUFModelPatcher(comfy.model_patcher.ModelPatcher): + patch_on_device = False + + def patch_weight_to_device(self, key, device_to=None, inplace_update=False): + if key not in self.patches: + return + weight = comfy.utils.get_attr(self.model, key) + + patches = self.patches[key] + if is_quantized(weight): + out_weight = weight.to(device_to) + patches = move_patch_to_device(patches, self.load_device if self.patch_on_device else self.offload_device) + # TODO: do we ever have legitimate duplicate patches? (i.e. patch on top of patched weight) + out_weight.patches = [(patches, key)] + else: + inplace_update = self.weight_inplace_update or inplace_update + if key not in self.backup: + self.backup[key] = collections.namedtuple('Dimension', ['weight', 'inplace_update'])( + weight.to(device=self.offload_device, copy=inplace_update), inplace_update + ) + + if device_to is not None: + temp_weight = comfy.model_management.cast_to_device(weight, device_to, torch.float32, copy=True) + else: + temp_weight = weight.to(torch.float32, copy=True) + + out_weight = comfy.lora.calculate_weight(patches, temp_weight, key) + out_weight = comfy.float.stochastic_rounding(out_weight, weight.dtype) + + if inplace_update: + comfy.utils.copy_to_param(self.model, key, out_weight) + else: + comfy.utils.set_attr_param(self.model, key, out_weight) + + def unpatch_model(self, device_to=None, unpatch_weights=True): + if unpatch_weights: + for p in self.model.parameters(): + if is_torch_compatible(p): + continue + patches = getattr(p, "patches", []) + if len(patches) > 0: + p.patches = [] + # TODO: Find another way to not unload after patches + return super().unpatch_model(device_to=device_to, unpatch_weights=unpatch_weights) + + + def pin_weight_to_device(self, key): + op_key = key.rsplit('.', 1)[0] + if not self.mmap_released and op_key in self.named_modules_to_munmap: + # TODO: possible to OOM, find better way to detach + self.named_modules_to_munmap[op_key].to(self.load_device).to(self.offload_device) + del self.named_modules_to_munmap[op_key] + super().pin_weight_to_device(key) + + mmap_released = False + named_modules_to_munmap = {} + + def load(self, *args, force_patch_weights=False, **kwargs): + if not self.mmap_released: + self.named_modules_to_munmap = dict(self.model.named_modules()) + + # always call `patch_weight_to_device` even for lowvram + super().load(*args, force_patch_weights=True, **kwargs) + + # make sure nothing stays linked to mmap after first load + if not self.mmap_released: + linked = [] + if kwargs.get("lowvram_model_memory", 0) > 0: + for n, m in self.named_modules_to_munmap.items(): + if hasattr(m, "weight"): + device = getattr(m.weight, "device", None) + if device == self.offload_device: + linked.append((n, m)) + continue + if hasattr(m, "bias"): + device = getattr(m.bias, "device", None) + if device == self.offload_device: + linked.append((n, m)) + continue + if linked and self.load_device != self.offload_device: + logging.info(f"Attempting to release mmap ({len(linked)})") + for n, m in linked: + # TODO: possible to OOM, find better way to detach + m.to(self.load_device).to(self.offload_device) + self.mmap_released = True + self.named_modules_to_munmap = {} + + def clone(self, *args, **kwargs): + src_cls = self.__class__ + self.__class__ = GGUFModelPatcher + n = super().clone(*args, **kwargs) + n.__class__ = GGUFModelPatcher + self.__class__ = src_cls + # GGUF specific clone values below + n.patch_on_device = getattr(self, "patch_on_device", False) + n.mmap_released = getattr(self, "mmap_released", False) + if src_cls != GGUFModelPatcher: + n.size = 0 # force recalc + return n + +class UnetLoaderGGUF: + @classmethod + def INPUT_TYPES(s): + unet_names = [x for x in folder_paths.get_filename_list("unet_gguf")] + return { + "required": { + "unet_name": (unet_names,), + } + } + + RETURN_TYPES = ("MODEL",) + FUNCTION = "load_unet" + CATEGORY = "bootleg" + TITLE = "Unet Loader (GGUF)" + + def load_unet(self, unet_name, dequant_dtype=None, patch_dtype=None, patch_on_device=None): + ops = GGMLOps() + + if dequant_dtype in ("default", None): + ops.Linear.dequant_dtype = None + elif dequant_dtype in ["target"]: + ops.Linear.dequant_dtype = dequant_dtype + else: + ops.Linear.dequant_dtype = getattr(torch, dequant_dtype) + + if patch_dtype in ("default", None): + ops.Linear.patch_dtype = None + elif patch_dtype in ["target"]: + ops.Linear.patch_dtype = patch_dtype + else: + ops.Linear.patch_dtype = getattr(torch, patch_dtype) + + # init model + unet_path = folder_paths.get_full_path("unet", unet_name) + sd, extra = gguf_sd_loader(unet_path) + + kwargs = {} + valid_params = inspect.signature(comfy.sd.load_diffusion_model_state_dict).parameters + if "metadata" in valid_params: + kwargs["metadata"] = extra.get("metadata", {}) + + model = comfy.sd.load_diffusion_model_state_dict( + sd, model_options={"custom_operations": ops}, **kwargs, + ) + if model is None: + logging.error("ERROR UNSUPPORTED UNET {}".format(unet_path)) + raise RuntimeError("ERROR: Could not detect model type of: {}".format(unet_path)) + model = GGUFModelPatcher.clone(model) + model.patch_on_device = patch_on_device + return (model,) + +class UnetLoaderGGUFAdvanced(UnetLoaderGGUF): + @classmethod + def INPUT_TYPES(s): + unet_names = [x for x in folder_paths.get_filename_list("unet_gguf")] + return { + "required": { + "unet_name": (unet_names,), + "dequant_dtype": (["default", "target", "float32", "float16", "bfloat16"], {"default": "default"}), + "patch_dtype": (["default", "target", "float32", "float16", "bfloat16"], {"default": "default"}), + "patch_on_device": ("BOOLEAN", {"default": False}), + } + } + TITLE = "Unet Loader (GGUF/Advanced)" + +class CLIPLoaderGGUF: + @classmethod + def INPUT_TYPES(s): + base = nodes.CLIPLoader.INPUT_TYPES() + return { + "required": { + "clip_name": (s.get_filename_list(),), + "type": base["required"]["type"], + } + } + + RETURN_TYPES = ("CLIP",) + FUNCTION = "load_clip" + CATEGORY = "bootleg" + TITLE = "CLIPLoader (GGUF)" + + @classmethod + def get_filename_list(s): + files = [] + files += folder_paths.get_filename_list("clip") + files += folder_paths.get_filename_list("clip_gguf") + return sorted(files) + + def load_data(self, ckpt_paths): + clip_data = [] + for p in ckpt_paths: + if p.endswith(".gguf"): + sd = gguf_clip_loader(p) + else: + sd = comfy.utils.load_torch_file(p, safe_load=True) + if "scaled_fp8" in sd: # NOTE: Scaled FP8 would require different custom ops, but only one can be active + raise NotImplementedError(f"Mixing scaled FP8 with GGUF is not supported! Use regular CLIP loader or switch model(s)\n({p})") + clip_data.append(sd) + return clip_data + + def load_patcher(self, clip_paths, clip_type, clip_data): + clip = comfy.sd.load_text_encoder_state_dicts( + clip_type = clip_type, + state_dicts = clip_data, + model_options = { + "custom_operations": GGMLOps, + "initial_device": comfy.model_management.text_encoder_offload_device() + }, + embedding_directory = folder_paths.get_folder_paths("embeddings"), + ) + clip.patcher = GGUFModelPatcher.clone(clip.patcher) + return clip + + def load_clip(self, clip_name, type="stable_diffusion"): + clip_path = folder_paths.get_full_path("clip", clip_name) + clip_type = getattr(comfy.sd.CLIPType, type.upper(), comfy.sd.CLIPType.STABLE_DIFFUSION) + return (self.load_patcher([clip_path], clip_type, self.load_data([clip_path])),) + +class DualCLIPLoaderGGUF(CLIPLoaderGGUF): + @classmethod + def INPUT_TYPES(s): + base = nodes.DualCLIPLoader.INPUT_TYPES() + file_options = (s.get_filename_list(), ) + return { + "required": { + "clip_name1": file_options, + "clip_name2": file_options, + "type": base["required"]["type"], + } + } + + TITLE = "DualCLIPLoader (GGUF)" + + def load_clip(self, clip_name1, clip_name2, type): + clip_path1 = folder_paths.get_full_path("clip", clip_name1) + clip_path2 = folder_paths.get_full_path("clip", clip_name2) + clip_paths = (clip_path1, clip_path2) + clip_type = getattr(comfy.sd.CLIPType, type.upper(), comfy.sd.CLIPType.STABLE_DIFFUSION) + return (self.load_patcher(clip_paths, clip_type, self.load_data(clip_paths)),) + +class TripleCLIPLoaderGGUF(CLIPLoaderGGUF): + @classmethod + def INPUT_TYPES(s): + file_options = (s.get_filename_list(), ) + return { + "required": { + "clip_name1": file_options, + "clip_name2": file_options, + "clip_name3": file_options, + } + } + + TITLE = "TripleCLIPLoader (GGUF)" + + def load_clip(self, clip_name1, clip_name2, clip_name3, type="sd3"): + clip_path1 = folder_paths.get_full_path("clip", clip_name1) + clip_path2 = folder_paths.get_full_path("clip", clip_name2) + clip_path3 = folder_paths.get_full_path("clip", clip_name3) + clip_paths = (clip_path1, clip_path2, clip_path3) + clip_type = getattr(comfy.sd.CLIPType, type.upper(), comfy.sd.CLIPType.STABLE_DIFFUSION) + return (self.load_patcher(clip_paths, clip_type, self.load_data(clip_paths)),) + +class QuadrupleCLIPLoaderGGUF(CLIPLoaderGGUF): + @classmethod + def INPUT_TYPES(s): + file_options = (s.get_filename_list(), ) + return { + "required": { + "clip_name1": file_options, + "clip_name2": file_options, + "clip_name3": file_options, + "clip_name4": file_options, + } + } + + TITLE = "QuadrupleCLIPLoader (GGUF)" + + def load_clip(self, clip_name1, clip_name2, clip_name3, clip_name4, type="stable_diffusion"): + clip_path1 = folder_paths.get_full_path("clip", clip_name1) + clip_path2 = folder_paths.get_full_path("clip", clip_name2) + clip_path3 = folder_paths.get_full_path("clip", clip_name3) + clip_path4 = folder_paths.get_full_path("clip", clip_name4) + clip_paths = (clip_path1, clip_path2, clip_path3, clip_path4) + clip_type = getattr(comfy.sd.CLIPType, type.upper(), comfy.sd.CLIPType.STABLE_DIFFUSION) + return (self.load_patcher(clip_paths, clip_type, self.load_data(clip_paths)),) + +NODE_CLASS_MAPPINGS = { + "UnetLoaderGGUF": UnetLoaderGGUF, + "CLIPLoaderGGUF": CLIPLoaderGGUF, + "DualCLIPLoaderGGUF": DualCLIPLoaderGGUF, + "TripleCLIPLoaderGGUF": TripleCLIPLoaderGGUF, + "QuadrupleCLIPLoaderGGUF": QuadrupleCLIPLoaderGGUF, + "UnetLoaderGGUFAdvanced": UnetLoaderGGUFAdvanced, +} + diff --git a/ComfyUI/custom_nodes/ComfyUI-GGUF/ops.py b/ComfyUI/custom_nodes/ComfyUI-GGUF/ops.py new file mode 100644 index 0000000000000000000000000000000000000000..88a352e2abc78055a7a6e3933fc46b77b8a5c4ac --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-GGUF/ops.py @@ -0,0 +1,281 @@ +# (c) City96 || Apache-2.0 (apache.org/licenses/LICENSE-2.0) +import gguf +import torch +import logging + +import comfy.ops +import comfy.lora +import comfy.model_management +from .dequant import dequantize_tensor, is_quantized + +def chained_hasattr(obj, chained_attr): + probe = obj + for attr in chained_attr.split('.'): + if hasattr(probe, attr): + probe = getattr(probe, attr) + else: + return False + return True + +# A bakcward and forward compatible way to get `torch.compiler.disable`. +def get_torch_compiler_disable_decorator(): + def dummy_decorator(*args, **kwargs): + def noop(x): + return x + return noop + + from packaging import version + + if not chained_hasattr(torch, "compiler.disable"): + logging.info("ComfyUI-GGUF: Torch too old for torch.compile - bypassing") + return dummy_decorator # torch too old + elif version.parse(torch.__version__) >= version.parse("2.8"): + logging.info("ComfyUI-GGUF: Allowing full torch compile") + return dummy_decorator # torch compile works + if chained_hasattr(torch, "_dynamo.config.nontraceable_tensor_subclasses"): + logging.info("ComfyUI-GGUF: Allowing full torch compile (nightly)") + return dummy_decorator # torch compile works, nightly before 2.8 release + else: + logging.info("ComfyUI-GGUF: Partial torch compile only, consider updating pytorch") + return torch.compiler.disable + +torch_compiler_disable = get_torch_compiler_disable_decorator() + +class GGMLTensor(torch.Tensor): + """ + Main tensor-like class for storing quantized weights + """ + def __init__(self, *args, tensor_type, tensor_shape, patches=[], **kwargs): + super().__init__() + self.tensor_type = tensor_type + self.tensor_shape = tensor_shape + self.patches = patches + + def __new__(cls, *args, tensor_type, tensor_shape, patches=[], **kwargs): + return super().__new__(cls, *args, **kwargs) + + def to(self, *args, **kwargs): + new = super().to(*args, **kwargs) + new.tensor_type = getattr(self, "tensor_type", None) + new.tensor_shape = getattr(self, "tensor_shape", new.data.shape) + new.patches = getattr(self, "patches", []).copy() + return new + + def clone(self, *args, **kwargs): + return self + + def detach(self, *args, **kwargs): + return self + + def copy_(self, *args, **kwargs): + # fixes .weight.copy_ in comfy/clip_model/CLIPTextModel + try: + return super().copy_(*args, **kwargs) + except Exception as e: + logging.warning(f"ignoring 'copy_' on tensor: {e}") + + def new_empty(self, size, *args, **kwargs): + # Intel Arc fix, ref#50 + new_tensor = super().new_empty(size, *args, **kwargs) + return GGMLTensor( + new_tensor, + tensor_type = getattr(self, "tensor_type", None), + tensor_shape = size, + patches = getattr(self, "patches", []).copy() + ) + + @property + def shape(self): + if not hasattr(self, "tensor_shape"): + self.tensor_shape = self.size() + return self.tensor_shape + +class GGMLLayer(torch.nn.Module): + """ + This (should) be responsible for de-quantizing on the fly + """ + comfy_cast_weights = True + dequant_dtype = None + patch_dtype = None + largest_layer = False + torch_compatible_tensor_types = {None, gguf.GGMLQuantizationType.F32, gguf.GGMLQuantizationType.F16} + + def is_ggml_quantized(self, *, weight=None, bias=None): + if weight is None: + weight = self.weight + if bias is None: + bias = self.bias + return is_quantized(weight) or is_quantized(bias) + + def _load_from_state_dict(self, state_dict, prefix, *args, **kwargs): + weight, bias = state_dict.get(f"{prefix}weight"), state_dict.get(f"{prefix}bias") + # NOTE: using modified load for linear due to not initializing on creation, see GGMLOps todo + if self.is_ggml_quantized(weight=weight, bias=bias) or isinstance(self, torch.nn.Linear): + return self.ggml_load_from_state_dict(state_dict, prefix, *args, **kwargs) + # Not strictly required, but fixes embedding shape mismatch. Threshold set in loader.py + if isinstance(self, torch.nn.Embedding) and self.weight.shape[0] >= (64 * 1024): + return self.ggml_load_from_state_dict(state_dict, prefix, *args, **kwargs) + return super()._load_from_state_dict(state_dict, prefix, *args, **kwargs) + + def ggml_load_from_state_dict(self, state_dict, prefix, local_metadata, strict, missing_keys, unexpected_keys, error_msgs): + prefix_len = len(prefix) + for k,v in state_dict.items(): + if k[prefix_len:] == "weight": + self.weight = torch.nn.Parameter(v, requires_grad=False) + elif k[prefix_len:] == "bias" and v is not None: + self.bias = torch.nn.Parameter(v, requires_grad=False) + else: + unexpected_keys.append(k) + + # For Linear layer with missing weight + if self.weight is None and isinstance(self, torch.nn.Linear): + v = torch.zeros(self.in_features, self.out_features) + self.weight = torch.nn.Parameter(v, requires_grad=False) + missing_keys.append(prefix+"weight") + + # for vram estimation (TODO: less fragile logic?) + if getattr(self.weight, "is_largest_weight", False): + self.largest_layer = True + + def _save_to_state_dict(self, *args, **kwargs): + if self.is_ggml_quantized(): + return self.ggml_save_to_state_dict(*args, **kwargs) + return super()._save_to_state_dict(*args, **kwargs) + + def ggml_save_to_state_dict(self, destination, prefix, keep_vars): + # This is a fake state dict for vram estimation + weight = torch.zeros_like(self.weight, device=torch.device("meta")) + destination[prefix + "weight"] = weight + if self.bias is not None: + bias = torch.zeros_like(self.bias, device=torch.device("meta")) + destination[prefix + "bias"] = bias + + # Take into account space required for dequantizing the largest tensor + if self.largest_layer: + shape = getattr(self.weight, "tensor_shape", self.weight.shape) + dtype = self.dequant_dtype if self.dequant_dtype and self.dequant_dtype != "target" else torch.float16 + temp = torch.empty(*shape, device=torch.device("meta"), dtype=dtype) + destination[prefix + "temp.weight"] = temp + + return + # This would return the dequantized state dict + destination[prefix + "weight"] = self.get_weight(self.weight) + if bias is not None: + destination[prefix + "bias"] = self.get_weight(self.bias) + + def get_weight(self, tensor, dtype): + if tensor is None: + return + + # consolidate and load patches to GPU in async + patch_list = [] + device = tensor.device + for patches, key in getattr(tensor, "patches", []): + patch_list += move_patch_to_device(patches, device) + + # dequantize tensor while patches load + weight = dequantize_tensor(tensor, dtype, self.dequant_dtype) + + # prevent propagating custom tensor class + if isinstance(weight, GGMLTensor): + weight = torch.Tensor(weight) + + # apply patches + if len(patch_list) > 0: + if self.patch_dtype is None: + weight = comfy.lora.calculate_weight(patch_list, weight, key) + else: + # for testing, may degrade image quality + patch_dtype = dtype if self.patch_dtype == "target" else self.patch_dtype + weight = comfy.lora.calculate_weight(patch_list, weight, key, patch_dtype) + return weight + + @torch_compiler_disable() + def cast_bias_weight(s, input=None, dtype=None, device=None, bias_dtype=None): + if input is not None: + if dtype is None: + dtype = getattr(input, "dtype", torch.float32) + if bias_dtype is None: + bias_dtype = dtype + if device is None: + device = input.device + + bias = None + non_blocking = comfy.model_management.device_supports_non_blocking(device) + if s.bias is not None: + bias = s.get_weight(s.bias.to(device), dtype) + bias = comfy.ops.cast_to(bias, bias_dtype, device, non_blocking=non_blocking, copy=False) + + weight = s.get_weight(s.weight.to(device), dtype) + weight = comfy.ops.cast_to(weight, dtype, device, non_blocking=non_blocking, copy=False) + return weight, bias + + def forward_comfy_cast_weights(self, input, *args, **kwargs): + if self.is_ggml_quantized(): + out = self.forward_ggml_cast_weights(input, *args, **kwargs) + else: + out = super().forward_comfy_cast_weights(input, *args, **kwargs) + + # non-ggml forward might still propagate custom tensor class + if isinstance(out, GGMLTensor): + out = torch.Tensor(out) + return out + + def forward_ggml_cast_weights(self, input): + raise NotImplementedError + +class GGMLOps(comfy.ops.manual_cast): + """ + Dequantize weights on the fly before doing the compute + """ + class Linear(GGMLLayer, comfy.ops.manual_cast.Linear): + def __init__(self, in_features, out_features, bias=True, device=None, dtype=None): + torch.nn.Module.__init__(self) + # TODO: better workaround for reserved memory spike on windows + # Issue is with `torch.empty` still reserving the full memory for the layer + # Windows doesn't over-commit memory so without this 24GB+ of pagefile is used + self.in_features = in_features + self.out_features = out_features + self.weight = None + self.bias = None + + def forward_ggml_cast_weights(self, input): + weight, bias = self.cast_bias_weight(input) + return torch.nn.functional.linear(input, weight, bias) + + class Conv2d(GGMLLayer, comfy.ops.manual_cast.Conv2d): + def forward_ggml_cast_weights(self, input): + weight, bias = self.cast_bias_weight(input) + return self._conv_forward(input, weight, bias) + + class Embedding(GGMLLayer, comfy.ops.manual_cast.Embedding): + def forward_ggml_cast_weights(self, input, out_dtype=None): + output_dtype = out_dtype + if self.weight.dtype == torch.float16 or self.weight.dtype == torch.bfloat16: + out_dtype = None + weight, _bias = self.cast_bias_weight(self, device=input.device, dtype=out_dtype) + return torch.nn.functional.embedding( + input, weight, self.padding_idx, self.max_norm, self.norm_type, self.scale_grad_by_freq, self.sparse + ).to(dtype=output_dtype) + + class LayerNorm(GGMLLayer, comfy.ops.manual_cast.LayerNorm): + def forward_ggml_cast_weights(self, input): + if self.weight is None: + return super().forward_comfy_cast_weights(input) + weight, bias = self.cast_bias_weight(input) + return torch.nn.functional.layer_norm(input, self.normalized_shape, weight, bias, self.eps) + + class GroupNorm(GGMLLayer, comfy.ops.manual_cast.GroupNorm): + def forward_ggml_cast_weights(self, input): + weight, bias = self.cast_bias_weight(input) + return torch.nn.functional.group_norm(input, self.num_groups, weight, bias, self.eps) + +def move_patch_to_device(item, device): + if isinstance(item, torch.Tensor): + return item.to(device, non_blocking=True) + elif isinstance(item, tuple): + return tuple(move_patch_to_device(x, device) for x in item) + elif isinstance(item, list): + return [move_patch_to_device(x, device) for x in item] + else: + return item diff --git a/ComfyUI/custom_nodes/ComfyUI-GGUF/pyproject.toml b/ComfyUI/custom_nodes/ComfyUI-GGUF/pyproject.toml new file mode 100644 index 0000000000000000000000000000000000000000..8b8cf53b0c1e5cbb2ee37c7dfc7326e2cdd48313 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-GGUF/pyproject.toml @@ -0,0 +1,14 @@ +[project] +name = "comfyui-gguf" +description = "GGUF Quantization support for native ComfyUI models." +version = "2.0.0" # 2.0.0 = GitHub main, 1.X.X = ComfyUI Registry +license = { file = "LICENSE" } +dependencies = ["gguf>=0.13.0", "sentencepiece", "protobuf"] + +[project.urls] +Repository = "https://github.com/city96/ComfyUI-GGUF" + +[tool.comfy] +PublisherId = "city96" +DisplayName = "ComfyUI-GGUF" +Icon = "" diff --git a/ComfyUI/custom_nodes/ComfyUI-GGUF/requirements.txt b/ComfyUI/custom_nodes/ComfyUI-GGUF/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..f49905c79e1ea579a8e4fde0fbec966dcff2f2f8 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-GGUF/requirements.txt @@ -0,0 +1,5 @@ +# main +gguf>=0.13.0 +# optional - tokenizer +sentencepiece +protobuf diff --git a/ComfyUI/custom_nodes/ComfyUI-GGUF/tools/README.md b/ComfyUI/custom_nodes/ComfyUI-GGUF/tools/README.md new file mode 100644 index 0000000000000000000000000000000000000000..228bc220593d7878ca854032638bdc2682c76a71 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-GGUF/tools/README.md @@ -0,0 +1,93 @@ +## Converting initial model + +To convert your initial safetensors/ckpt model to FP16/BF16 GGUF, run the following command: + +``` +python convert.py --src E:\models\unet\flux1-dev.safetensors +``` +Make sure `gguf>=0.13.0` is installed for this step. Optionally, specify the output gguf file with the `--dst` arg. + +> [!NOTE] +> Do not use the diffusers UNET format for flux, it won't work, use the default/reference checkpoint key format. This is due to q/k/v being merged into one qkv key. +> You can convert it by loading it in ComfyUI and saving it using the built-in "ModelSave" node. + +> [!WARNING] +> For hunyuan video/wan 2.1, you will see a warning about 5D tensors. This means the script will save a **non functional** model to disk first, that you can quantize. I recommend saving these in a separate `raw` folder to avoid confusion. +> +> After quantization, you will have to run `fix_5d_tensor.py` manually to add back the missing key that was saved by the conversion code. + +## Quantizing using custom llama.cpp + +Depending on your git settings, you may need to run the following script first in order to make sure the patch file is valid. It will convert Windows (CRLF) line endings to Unix (LF) ones. + +``` +python fix_lines_ending.py +``` + +Git clone llama.cpp into the current folder: + +``` +git clone https://github.com/ggerganov/llama.cpp +``` + +Check out the correct branch, then apply the custom patch needed to add image model support to the repo you just cloned. + +``` +cd llama.cpp +git checkout tags/b3962 +git apply ..\lcpp.patch +``` + +Compile the llama-quantize binary. This example uses cmake, on linux you can just use make. + +### Visual Studio 2019, Linux, etc... + +``` +mkdir build +cmake -B build +cmake --build build --config Debug -j10 --target llama-quantize +cd .. +``` + +### Visual Studio 2022 + +``` +mkdir build +cmake -B build -DCMAKE_CXX_STANDARD=17 -DCMAKE_CXX_STANDARD_REQUIRED=ON -DCMAKE_CXX_FLAGS="-std=c++17" +``` + +Edit the `llama.cpp\common\log.cpp` file, inserts two lines after the existing first line: + +``` +#include "log.h" + +#define _SILENCE_CXX23_CHRONO_DEPRECATION_WARNING +#include +``` + +Then you can build the project: +``` +cmake --build build --config Debug -j10 --target llama-quantize +cd .. +``` + +### Quantize your model + + +Now you can use the newly build binary to quantize your model to the desired format: +``` +llama.cpp\build\bin\Debug\llama-quantize.exe E:\models\unet\flux1-dev-BF16.gguf E:\models\unet\flux1-dev-Q4_K_S.gguf Q4_K_S +``` + +You can extract the patch again with `git diff src\llama.cpp > lcpp.patch` if you wish to change something and contribute back. + +> [!WARNING] +> For hunyuan video/wan 2.1, you will have to run `fix_5d_tensor.py` after the quantization step is done. +> +> Example usage: `fix_5d_tensors.py --src E:\models\video\raw\wan2.1-t2v-1.3b-Q8_0.gguf --dst E:\models\video\wan2.1-t2v-1.3b-Q8_0.gguf` +> +> By default, this also saves a `fix_5d_tensors_[arch].safetensors` file in the `ComfyUI-GGUF/tools` folder, it's recommended to delete this after all models have been converted. + +> [!NOTE] +> Do not quantize SDXL / SD1 / other Conv2D heavy models. If you do, make sure to **extract the UNET model first**. +>This should be obvious, but also don't use the resulting llama-quantize binary with LLMs. diff --git a/ComfyUI/custom_nodes/ComfyUI-GGUF/tools/convert.py b/ComfyUI/custom_nodes/ComfyUI-GGUF/tools/convert.py new file mode 100644 index 0000000000000000000000000000000000000000..5029c874277358559f8855d3f1032963437a3e91 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-GGUF/tools/convert.py @@ -0,0 +1,365 @@ +# (c) City96 || Apache-2.0 (apache.org/licenses/LICENSE-2.0) +import os +import gguf +import torch +import logging +import argparse +from tqdm import tqdm +from safetensors.torch import load_file, save_file + +QUANTIZATION_THRESHOLD = 1024 +REARRANGE_THRESHOLD = 512 +MAX_TENSOR_NAME_LENGTH = 127 +MAX_TENSOR_DIMS = 4 + +class ModelTemplate: + arch = "invalid" # string describing architecture + shape_fix = False # whether to reshape tensors + keys_detect = [] # list of lists to match in state dict + keys_banned = [] # list of keys that should mark model as invalid for conversion + keys_hiprec = [] # list of keys that need to be kept in fp32 for some reason + keys_ignore = [] # list of strings to ignore keys by when found + + def handle_nd_tensor(self, key, data): + raise NotImplementedError(f"Tensor detected that exceeds dims supported by C++ code! ({key} @ {data.shape})") + +class ModelFlux(ModelTemplate): + arch = "flux" + keys_detect = [ + ("transformer_blocks.0.attn.norm_added_k.weight",), + ("double_blocks.0.img_attn.proj.weight",), + ] + keys_banned = ["transformer_blocks.0.attn.norm_added_k.weight",] + +class ModelSD3(ModelTemplate): + arch = "sd3" + keys_detect = [ + ("transformer_blocks.0.attn.add_q_proj.weight",), + ("joint_blocks.0.x_block.attn.qkv.weight",), + ] + keys_banned = ["transformer_blocks.0.attn.add_q_proj.weight",] + +class ModelAura(ModelTemplate): + arch = "aura" + keys_detect = [ + ("double_layers.3.modX.1.weight",), + ("joint_transformer_blocks.3.ff_context.out_projection.weight",), + ] + keys_banned = ["joint_transformer_blocks.3.ff_context.out_projection.weight",] + +class ModelHiDream(ModelTemplate): + arch = "hidream" + keys_detect = [ + ( + "caption_projection.0.linear.weight", + "double_stream_blocks.0.block.ff_i.shared_experts.w3.weight" + ) + ] + keys_hiprec = [ + # nn.parameter, can't load from BF16 ver + ".ff_i.gate.weight", + "img_emb.emb_pos" + ] + +class CosmosPredict2(ModelTemplate): + arch = "cosmos" + keys_detect = [ + ( + "blocks.0.mlp.layer1.weight", + "blocks.0.adaln_modulation_cross_attn.1.weight", + ) + ] + keys_hiprec = ["pos_embedder"] + keys_ignore = ["_extra_state", "accum_"] + +class ModelHyVid(ModelTemplate): + arch = "hyvid" + keys_detect = [ + ( + "double_blocks.0.img_attn_proj.weight", + "txt_in.individual_token_refiner.blocks.1.self_attn_qkv.weight", + ) + ] + + def handle_nd_tensor(self, key, data): + # hacky but don't have any better ideas + path = f"./fix_5d_tensors_{self.arch}.safetensors" # TODO: somehow get a path here?? + if os.path.isfile(path): + raise RuntimeError(f"5D tensor fix file already exists! {path}") + fsd = {key: torch.from_numpy(data)} + tqdm.write(f"5D key found in state dict! Manual fix required! - {key} {data.shape}") + save_file(fsd, path) + +class ModelWan(ModelHyVid): + arch = "wan" + keys_detect = [ + ( + "blocks.0.self_attn.norm_q.weight", + "text_embedding.2.weight", + "head.modulation", + ) + ] + keys_hiprec = [ + ".modulation" # nn.parameter, can't load from BF16 ver + ] + +class ModelLTXV(ModelTemplate): + arch = "ltxv" + keys_detect = [ + ( + "adaln_single.emb.timestep_embedder.linear_2.weight", + "transformer_blocks.27.scale_shift_table", + "caption_projection.linear_2.weight", + ) + ] + keys_hiprec = [ + "scale_shift_table" # nn.parameter, can't load from BF16 base quant + ] + +class ModelSDXL(ModelTemplate): + arch = "sdxl" + shape_fix = True + keys_detect = [ + ("down_blocks.0.downsamplers.0.conv.weight", "add_embedding.linear_1.weight",), + ( + "input_blocks.3.0.op.weight", "input_blocks.6.0.op.weight", + "output_blocks.2.2.conv.weight", "output_blocks.5.2.conv.weight", + ), # Non-diffusers + ("label_emb.0.0.weight",), + ] + +class ModelSD1(ModelTemplate): + arch = "sd1" + shape_fix = True + keys_detect = [ + ("down_blocks.0.downsamplers.0.conv.weight",), + ( + "input_blocks.3.0.op.weight", "input_blocks.6.0.op.weight", "input_blocks.9.0.op.weight", + "output_blocks.2.1.conv.weight", "output_blocks.5.2.conv.weight", "output_blocks.8.2.conv.weight" + ), # Non-diffusers + ] + +class ModelLumina2(ModelTemplate): + arch = "lumina2" + keys_detect = [ + ("cap_embedder.1.weight", "context_refiner.0.attention.qkv.weight") + ] + +arch_list = [ModelFlux, ModelSD3, ModelAura, ModelHiDream, CosmosPredict2, + ModelLTXV, ModelHyVid, ModelWan, ModelSDXL, ModelSD1, ModelLumina2] + +def is_model_arch(model, state_dict): + # check if model is correct + matched = False + invalid = False + for match_list in model.keys_detect: + if all(key in state_dict for key in match_list): + matched = True + invalid = any(key in state_dict for key in model.keys_banned) + break + assert not invalid, "Model architecture not allowed for conversion! (i.e. reference VS diffusers format)" + return matched + +def detect_arch(state_dict): + model_arch = None + for arch in arch_list: + if is_model_arch(arch, state_dict): + model_arch = arch() + break + assert model_arch is not None, "Unknown model architecture!" + return model_arch + +def parse_args(): + parser = argparse.ArgumentParser(description="Generate F16 GGUF files from single UNET") + parser.add_argument("--src", required=True, help="Source model ckpt file.") + parser.add_argument("--dst", help="Output unet gguf file.") + args = parser.parse_args() + + if not os.path.isfile(args.src): + parser.error("No input provided!") + + return args + +def strip_prefix(state_dict): + # prefix for mixed state dict + prefix = None + for pfx in ["model.diffusion_model.", "model."]: + if any([x.startswith(pfx) for x in state_dict.keys()]): + prefix = pfx + break + + # prefix for uniform state dict + if prefix is None: + for pfx in ["net."]: + if all([x.startswith(pfx) for x in state_dict.keys()]): + prefix = pfx + break + + # strip prefix if found + if prefix is not None: + logging.info(f"State dict prefix found: '{prefix}'") + sd = {} + for k, v in state_dict.items(): + if prefix not in k: + continue + k = k.replace(prefix, "") + sd[k] = v + else: + logging.debug("State dict has no prefix") + sd = state_dict + + return sd + +def load_state_dict(path): + if any(path.endswith(x) for x in [".ckpt", ".pt", ".bin", ".pth"]): + state_dict = torch.load(path, map_location="cpu", weights_only=True) + for subkey in ["model", "module"]: + if subkey in state_dict: + state_dict = state_dict[subkey] + break + if len(state_dict) < 20: + raise RuntimeError(f"pt subkey load failed: {state_dict.keys()}") + else: + state_dict = load_file(path) + + return strip_prefix(state_dict) + +def handle_tensors(writer, state_dict, model_arch): + name_lengths = tuple(sorted( + ((key, len(key)) for key in state_dict.keys()), + key=lambda item: item[1], + reverse=True, + )) + if not name_lengths: + return + max_name_len = name_lengths[0][1] + if max_name_len > MAX_TENSOR_NAME_LENGTH: + bad_list = ", ".join(f"{key!r} ({namelen})" for key, namelen in name_lengths if namelen > MAX_TENSOR_NAME_LENGTH) + raise ValueError(f"Can only handle tensor names up to {MAX_TENSOR_NAME_LENGTH} characters. Tensors exceeding the limit: {bad_list}") + for key, data in tqdm(state_dict.items()): + old_dtype = data.dtype + + if any(x in key for x in model_arch.keys_ignore): + tqdm.write(f"Filtering ignored key: '{key}'") + continue + + if data.dtype == torch.bfloat16: + data = data.to(torch.float32).numpy() + # this is so we don't break torch 2.0.X + elif data.dtype in [getattr(torch, "float8_e4m3fn", "_invalid"), getattr(torch, "float8_e5m2", "_invalid")]: + data = data.to(torch.float16).numpy() + else: + data = data.numpy() + + n_dims = len(data.shape) + data_shape = data.shape + if old_dtype == torch.bfloat16: + data_qtype = gguf.GGMLQuantizationType.BF16 + # elif old_dtype == torch.float32: + # data_qtype = gguf.GGMLQuantizationType.F32 + else: + data_qtype = gguf.GGMLQuantizationType.F16 + + # The max no. of dimensions that can be handled by the quantization code is 4 + if len(data.shape) > MAX_TENSOR_DIMS: + model_arch.handle_nd_tensor(key, data) + continue # needs to be added back later + + # get number of parameters (AKA elements) in this tensor + n_params = 1 + for dim_size in data_shape: + n_params *= dim_size + + if old_dtype in (torch.float32, torch.bfloat16): + if n_dims == 1: + # one-dimensional tensors should be kept in F32 + # also speeds up inference due to not dequantizing + data_qtype = gguf.GGMLQuantizationType.F32 + + elif n_params <= QUANTIZATION_THRESHOLD: + # very small tensors + data_qtype = gguf.GGMLQuantizationType.F32 + + elif any(x in key for x in model_arch.keys_hiprec): + # tensors that require max precision + data_qtype = gguf.GGMLQuantizationType.F32 + + if (model_arch.shape_fix # NEVER reshape for models such as flux + and n_dims > 1 # Skip one-dimensional tensors + and n_params >= REARRANGE_THRESHOLD # Only rearrange tensors meeting the size requirement + and (n_params / 256).is_integer() # Rearranging only makes sense if total elements is divisible by 256 + and not (data.shape[-1] / 256).is_integer() # Only need to rearrange if the last dimension is not divisible by 256 + ): + orig_shape = data.shape + data = data.reshape(n_params // 256, 256) + writer.add_array(f"comfy.gguf.orig_shape.{key}", tuple(int(dim) for dim in orig_shape)) + + try: + data = gguf.quants.quantize(data, data_qtype) + except (AttributeError, gguf.QuantError) as e: + tqdm.write(f"falling back to F16: {e}") + data_qtype = gguf.GGMLQuantizationType.F16 + data = gguf.quants.quantize(data, data_qtype) + + new_name = key # do we need to rename? + + shape_str = f"{{{', '.join(str(n) for n in reversed(data.shape))}}}" + tqdm.write(f"{f'%-{max_name_len + 4}s' % f'{new_name}'} {old_dtype} --> {data_qtype.name}, shape = {shape_str}") + + writer.add_tensor(new_name, data, raw_dtype=data_qtype) + +def convert_file(path, dst_path=None, interact=True, overwrite=False): + # load & run model detection logic + state_dict = load_state_dict(path) + model_arch = detect_arch(state_dict) + logging.info(f"* Architecture detected from input: {model_arch.arch}") + + # detect & set dtype for output file + dtypes = [x.dtype for x in state_dict.values()] + dtypes = {x:dtypes.count(x) for x in set(dtypes)} + main_dtype = max(dtypes, key=dtypes.get) + + if main_dtype == torch.bfloat16: + ftype_name = "BF16" + ftype_gguf = gguf.LlamaFileType.MOSTLY_BF16 + # elif main_dtype == torch.float32: + # ftype_name = "F32" + # ftype_gguf = None + else: + ftype_name = "F16" + ftype_gguf = gguf.LlamaFileType.MOSTLY_F16 + + if dst_path is None: + dst_path = f"{os.path.splitext(path)[0]}-{ftype_name}.gguf" + elif "{ftype}" in dst_path: # lcpp logic + dst_path = dst_path.replace("{ftype}", ftype_name) + + if os.path.isfile(dst_path) and not overwrite: + if interact: + input("Output exists enter to continue or ctrl+c to abort!") + else: + raise OSError("Output exists and overwriting is disabled!") + + # handle actual file + writer = gguf.GGUFWriter(path=None, arch=model_arch.arch) + writer.add_quantization_version(gguf.GGML_QUANT_VERSION) + if ftype_gguf is not None: + writer.add_file_type(ftype_gguf) + + handle_tensors(writer, state_dict, model_arch) + writer.write_header_to_file(path=dst_path) + writer.write_kv_data_to_file() + writer.write_tensors_to_file(progress=True) + writer.close() + + fix = f"./fix_5d_tensors_{model_arch.arch}.safetensors" + if os.path.isfile(fix): + logging.warning(f"\n### Warning! Fix file found at '{fix}'") + logging.warning(" you most likely need to run 'fix_5d_tensors.py' after quantization.") + + return dst_path, model_arch + +if __name__ == "__main__": + args = parse_args() + convert_file(args.src, args.dst) + diff --git a/ComfyUI/custom_nodes/ComfyUI-GGUF/tools/fix_5d_tensors.py b/ComfyUI/custom_nodes/ComfyUI-GGUF/tools/fix_5d_tensors.py new file mode 100644 index 0000000000000000000000000000000000000000..0e61d1c2e5f2572c3d9fa12eba38c84f13689a53 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-GGUF/tools/fix_5d_tensors.py @@ -0,0 +1,82 @@ +# (c) City96 || Apache-2.0 (apache.org/licenses/LICENSE-2.0) +import os +import gguf +import torch +import argparse +from tqdm import tqdm +from safetensors.torch import load_file + +def get_args(): + parser = argparse.ArgumentParser() + parser.add_argument("--src", required=True) + parser.add_argument("--dst", required=True) + parser.add_argument("--fix", required=False, help="Defaults to ./fix_5d_tensors_[arch].pt") + parser.add_argument("--overwrite", action="store_true") + args = parser.parse_args() + + if not os.path.isfile(args.src): + parser.error(f"Invalid source file '{args.src}'") + if not args.overwrite and os.path.exists(args.dst): + parser.error(f"Output exists, use '--overwrite' ({args.dst})") + + return args + +def get_arch_str(reader): + field = reader.get_field("general.architecture") + return str(field.parts[field.data[-1]], encoding="utf-8") + +def get_file_type(reader): + field = reader.get_field("general.file_type") + ft = int(field.parts[field.data[-1]]) + return gguf.LlamaFileType(ft) + +if __name__ == "__main__": + args = get_args() + + # read existing + reader = gguf.GGUFReader(args.src) + arch = get_arch_str(reader) + file_type = get_file_type(reader) + print(f"Detected arch: '{arch}' (ftype: {str(file_type)})") + + # prep fix + if args.fix is None: + args.fix = f"./fix_5d_tensors_{arch}.safetensors" + + if not os.path.isfile(args.fix): + raise OSError(f"No 5D tensor fix file: {args.fix}") + + sd5d = load_file(args.fix) + sd5d = {k:v.numpy() for k,v in sd5d.items()} + print("5D tensors:", sd5d.keys()) + + # prep output + writer = gguf.GGUFWriter(path=None, arch=arch) + writer.add_quantization_version(gguf.GGML_QUANT_VERSION) + writer.add_file_type(file_type) + + added = [] + def add_extra_key(writer, key, data): + global added + data_qtype = gguf.GGMLQuantizationType.F32 + data = gguf.quants.quantize(data, data_qtype) + tqdm.write(f"Adding key {key} ({data.shape})") + writer.add_tensor(key, data, raw_dtype=data_qtype) + added.append(key) + + # main loop to add missing 5D tensor(s) + for tensor in tqdm(reader.tensors): + writer.add_tensor(tensor.name, tensor.data, raw_dtype=tensor.tensor_type) + key5d = tensor.name.replace(".bias", ".weight") + if key5d in sd5d.keys(): + add_extra_key(writer, key5d, sd5d[key5d]) + + # brute force for any missed + for key, data in sd5d.items(): + if key not in added: + add_extra_key(writer, key, data) + + writer.write_header_to_file(path=args.dst) + writer.write_kv_data_to_file() + writer.write_tensors_to_file(progress=True) + writer.close() diff --git a/ComfyUI/custom_nodes/ComfyUI-GGUF/tools/fix_lines_ending.py b/ComfyUI/custom_nodes/ComfyUI-GGUF/tools/fix_lines_ending.py new file mode 100644 index 0000000000000000000000000000000000000000..346e3501fb7682fa3754f175965aa750241fe4ac --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-GGUF/tools/fix_lines_ending.py @@ -0,0 +1,31 @@ +import os + +files = ["lcpp.patch", "lcpp_sd3.patch"] + +def has_unix_line_endings(file_path): + try: + with open(file_path, 'rb') as file: + content = file.read() + return b'\r\n' not in content + except Exception as e: + print(f"Error checking '{file_path}': {e}") + return False + +def convert_to_linux_format(file_path): + try: + with open(file_path, 'rb') as file: + content = file.read().replace(b'\r\n', b'\n') + with open(file_path, 'wb') as file: + file.write(content) + print(f"'{file_path}' converted to Linux line endings (LF).") + except Exception as e: + print(f"Error processing '{file_path}': {e}") + +for file in files: + if os.path.exists(file): + if has_unix_line_endings(file): + print(f"'{file}' already has Unix line endings (LF). No conversion needed.") + else: + convert_to_linux_format(file) + else: + print(f"File '{file}' does not exist.") diff --git a/ComfyUI/custom_nodes/ComfyUI-GGUF/tools/lcpp.patch b/ComfyUI/custom_nodes/ComfyUI-GGUF/tools/lcpp.patch new file mode 100644 index 0000000000000000000000000000000000000000..92396e17b4ecf281265f07603232d23111ee9baa --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-GGUF/tools/lcpp.patch @@ -0,0 +1,451 @@ +diff --git a/ggml/include/ggml.h b/ggml/include/ggml.h +index de3c706f..0267c1fa 100644 +--- a/ggml/include/ggml.h ++++ b/ggml/include/ggml.h +@@ -223,7 +223,7 @@ + #define GGML_MAX_OP_PARAMS 64 + + #ifndef GGML_MAX_NAME +-# define GGML_MAX_NAME 64 ++# define GGML_MAX_NAME 128 + #endif + + #define GGML_DEFAULT_N_THREADS 4 +@@ -2449,6 +2449,7 @@ extern "C" { + + // manage tensor info + GGML_API void gguf_add_tensor(struct gguf_context * ctx, const struct ggml_tensor * tensor); ++ GGML_API void gguf_set_tensor_ndim(struct gguf_context * ctx, const char * name, int n_dim); + GGML_API void gguf_set_tensor_type(struct gguf_context * ctx, const char * name, enum ggml_type type); + GGML_API void gguf_set_tensor_data(struct gguf_context * ctx, const char * name, const void * data, size_t size); + +diff --git a/ggml/src/ggml.c b/ggml/src/ggml.c +index b16c462f..6d1568f1 100644 +--- a/ggml/src/ggml.c ++++ b/ggml/src/ggml.c +@@ -22960,6 +22960,14 @@ void gguf_add_tensor( + ctx->header.n_tensors++; + } + ++void gguf_set_tensor_ndim(struct gguf_context * ctx, const char * name, const int n_dim) { ++ const int idx = gguf_find_tensor(ctx, name); ++ if (idx < 0) { ++ GGML_ABORT("tensor not found"); ++ } ++ ctx->infos[idx].n_dims = n_dim; ++} ++ + void gguf_set_tensor_type(struct gguf_context * ctx, const char * name, enum ggml_type type) { + const int idx = gguf_find_tensor(ctx, name); + if (idx < 0) { +diff --git a/src/llama.cpp b/src/llama.cpp +index 24e1f1f0..25db4c69 100644 +--- a/src/llama.cpp ++++ b/src/llama.cpp +@@ -205,6 +205,17 @@ enum llm_arch { + LLM_ARCH_GRANITE, + LLM_ARCH_GRANITE_MOE, + LLM_ARCH_CHAMELEON, ++ LLM_ARCH_FLUX, ++ LLM_ARCH_SD1, ++ LLM_ARCH_SDXL, ++ LLM_ARCH_SD3, ++ LLM_ARCH_AURA, ++ LLM_ARCH_LTXV, ++ LLM_ARCH_HYVID, ++ LLM_ARCH_WAN, ++ LLM_ARCH_HIDREAM, ++ LLM_ARCH_COSMOS, ++ LLM_ARCH_LUMINA2, + LLM_ARCH_UNKNOWN, + }; + +@@ -258,6 +269,17 @@ static const std::map LLM_ARCH_NAMES = { + { LLM_ARCH_GRANITE, "granite" }, + { LLM_ARCH_GRANITE_MOE, "granitemoe" }, + { LLM_ARCH_CHAMELEON, "chameleon" }, ++ { LLM_ARCH_FLUX, "flux" }, ++ { LLM_ARCH_SD1, "sd1" }, ++ { LLM_ARCH_SDXL, "sdxl" }, ++ { LLM_ARCH_SD3, "sd3" }, ++ { LLM_ARCH_AURA, "aura" }, ++ { LLM_ARCH_LTXV, "ltxv" }, ++ { LLM_ARCH_HYVID, "hyvid" }, ++ { LLM_ARCH_WAN, "wan" }, ++ { LLM_ARCH_HIDREAM, "hidream" }, ++ { LLM_ARCH_COSMOS, "cosmos" }, ++ { LLM_ARCH_LUMINA2, "lumina2" }, + { LLM_ARCH_UNKNOWN, "(unknown)" }, + }; + +@@ -1531,6 +1553,17 @@ static const std::map> LLM_TENSOR_N + { LLM_TENSOR_ATTN_K_NORM, "blk.%d.attn_k_norm" }, + }, + }, ++ { LLM_ARCH_FLUX, {}}, ++ { LLM_ARCH_SD1, {}}, ++ { LLM_ARCH_SDXL, {}}, ++ { LLM_ARCH_SD3, {}}, ++ { LLM_ARCH_AURA, {}}, ++ { LLM_ARCH_LTXV, {}}, ++ { LLM_ARCH_HYVID, {}}, ++ { LLM_ARCH_WAN, {}}, ++ { LLM_ARCH_HIDREAM, {}}, ++ { LLM_ARCH_COSMOS, {}}, ++ { LLM_ARCH_LUMINA2, {}}, + { + LLM_ARCH_UNKNOWN, + { +@@ -5403,6 +5436,25 @@ static void llm_load_hparams( + // get general kv + ml.get_key(LLM_KV_GENERAL_NAME, model.name, false); + ++ // Disable LLM metadata for image models ++ switch (model.arch) { ++ case LLM_ARCH_FLUX: ++ case LLM_ARCH_SD1: ++ case LLM_ARCH_SDXL: ++ case LLM_ARCH_SD3: ++ case LLM_ARCH_AURA: ++ case LLM_ARCH_LTXV: ++ case LLM_ARCH_HYVID: ++ case LLM_ARCH_WAN: ++ case LLM_ARCH_HIDREAM: ++ case LLM_ARCH_COSMOS: ++ case LLM_ARCH_LUMINA2: ++ model.ftype = ml.ftype; ++ return; ++ default: ++ break; ++ } ++ + // get hparams kv + ml.get_key(LLM_KV_VOCAB_SIZE, hparams.n_vocab, false) || ml.get_arr_n(LLM_KV_TOKENIZER_LIST, hparams.n_vocab); + +@@ -18016,6 +18068,134 @@ static void llama_tensor_dequantize_internal( + workers.clear(); + } + ++static ggml_type img_tensor_get_type(quantize_state_internal & qs, ggml_type new_type, const ggml_tensor * tensor, llama_ftype ftype) { ++ // Special function for quantizing image model tensors ++ const std::string name = ggml_get_name(tensor); ++ const llm_arch arch = qs.model.arch; ++ ++ // Sanity check ++ if ( ++ (name.find("model.diffusion_model.") != std::string::npos) || ++ (name.find("first_stage_model.") != std::string::npos) || ++ (name.find("single_transformer_blocks.") != std::string::npos) || ++ (name.find("joint_transformer_blocks.") != std::string::npos) ++ ) { ++ throw std::runtime_error("Invalid input GGUF file. This is not a supported UNET model"); ++ } ++ ++ // Unsupported quant types - exclude all IQ quants for now ++ if (ftype == LLAMA_FTYPE_MOSTLY_IQ2_XXS || ftype == LLAMA_FTYPE_MOSTLY_IQ2_XS || ++ ftype == LLAMA_FTYPE_MOSTLY_IQ2_S || ftype == LLAMA_FTYPE_MOSTLY_IQ2_M || ++ ftype == LLAMA_FTYPE_MOSTLY_IQ3_XXS || ftype == LLAMA_FTYPE_MOSTLY_IQ1_S || ++ ftype == LLAMA_FTYPE_MOSTLY_IQ1_M || ftype == LLAMA_FTYPE_MOSTLY_IQ4_NL || ++ ftype == LLAMA_FTYPE_MOSTLY_IQ4_XS || ftype == LLAMA_FTYPE_MOSTLY_IQ3_S || ++ ftype == LLAMA_FTYPE_MOSTLY_IQ3_M || ftype == LLAMA_FTYPE_MOSTLY_Q4_0_4_4 || ++ ftype == LLAMA_FTYPE_MOSTLY_Q4_0_4_8 || ftype == LLAMA_FTYPE_MOSTLY_Q4_0_8_8) { ++ throw std::runtime_error("Invalid quantization type for image model (Not supported)"); ++ } ++ ++ if ( // Rules for to_v attention ++ (name.find("attn_v.weight") != std::string::npos) || ++ (name.find(".to_v.weight") != std::string::npos) || ++ (name.find(".v.weight") != std::string::npos) || ++ (name.find(".attn.w1v.weight") != std::string::npos) || ++ (name.find(".attn.w2v.weight") != std::string::npos) || ++ (name.find("_attn.v_proj.weight") != std::string::npos) ++ ){ ++ if (ftype == LLAMA_FTYPE_MOSTLY_Q2_K) { ++ new_type = GGML_TYPE_Q3_K; ++ } ++ else if (ftype == LLAMA_FTYPE_MOSTLY_Q3_K_M) { ++ new_type = qs.i_attention_wv < 2 ? GGML_TYPE_Q5_K : GGML_TYPE_Q4_K; ++ } ++ else if (ftype == LLAMA_FTYPE_MOSTLY_Q3_K_L) { ++ new_type = GGML_TYPE_Q5_K; ++ } ++ else if (ftype == LLAMA_FTYPE_MOSTLY_Q4_K_M || ftype == LLAMA_FTYPE_MOSTLY_Q5_K_M) { ++ new_type = GGML_TYPE_Q6_K; ++ } ++ else if (ftype == LLAMA_FTYPE_MOSTLY_Q4_K_S && qs.i_attention_wv < 4) { ++ new_type = GGML_TYPE_Q5_K; ++ } ++ ++qs.i_attention_wv; ++ } else if ( // Rules for fused qkv attention ++ (name.find("attn_qkv.weight") != std::string::npos) || ++ (name.find("attn.qkv.weight") != std::string::npos) || ++ (name.find("attention.qkv.weight") != std::string::npos) ++ ) { ++ if (ftype == LLAMA_FTYPE_MOSTLY_Q3_K_M || ftype == LLAMA_FTYPE_MOSTLY_Q3_K_L) { ++ new_type = GGML_TYPE_Q4_K; ++ } ++ else if (ftype == LLAMA_FTYPE_MOSTLY_Q4_K_M) { ++ new_type = GGML_TYPE_Q5_K; ++ } ++ else if (ftype == LLAMA_FTYPE_MOSTLY_Q5_K_M) { ++ new_type = GGML_TYPE_Q6_K; ++ } ++ } else if ( // Rules for ffn ++ (name.find("ffn_down") != std::string::npos) || ++ ((name.find("experts.") != std::string::npos) && (name.find(".w2.weight") != std::string::npos)) || ++ (name.find(".ffn.2.weight") != std::string::npos) || // is this even the right way around? ++ (name.find(".ff.net.2.weight") != std::string::npos) || ++ (name.find(".mlp.layer2.weight") != std::string::npos) || ++ (name.find(".adaln_modulation_mlp.2.weight") != std::string::npos) || ++ (name.find(".feed_forward.w2.weight") != std::string::npos) ++ ) { ++ // TODO: add back `layer_info` with some model specific logic + logic further down ++ if (ftype == LLAMA_FTYPE_MOSTLY_Q3_K_M) { ++ new_type = GGML_TYPE_Q4_K; ++ } ++ else if (ftype == LLAMA_FTYPE_MOSTLY_Q3_K_L) { ++ new_type = GGML_TYPE_Q5_K; ++ } ++ else if (ftype == LLAMA_FTYPE_MOSTLY_Q4_K_S) { ++ new_type = GGML_TYPE_Q5_K; ++ } ++ else if (ftype == LLAMA_FTYPE_MOSTLY_Q4_K_M) { ++ new_type = GGML_TYPE_Q6_K; ++ } ++ else if (ftype == LLAMA_FTYPE_MOSTLY_Q5_K_M) { ++ new_type = GGML_TYPE_Q6_K; ++ } ++ else if (ftype == LLAMA_FTYPE_MOSTLY_Q4_0) { ++ new_type = GGML_TYPE_Q4_1; ++ } ++ else if (ftype == LLAMA_FTYPE_MOSTLY_Q5_0) { ++ new_type = GGML_TYPE_Q5_1; ++ } ++ ++qs.i_ffn_down; ++ } ++ ++ // Sanity check for row shape ++ bool convert_incompatible_tensor = false; ++ if (new_type == GGML_TYPE_Q2_K || new_type == GGML_TYPE_Q3_K || new_type == GGML_TYPE_Q4_K || ++ new_type == GGML_TYPE_Q5_K || new_type == GGML_TYPE_Q6_K) { ++ int nx = tensor->ne[0]; ++ int ny = tensor->ne[1]; ++ if (nx % QK_K != 0) { ++ LLAMA_LOG_WARN("\n\n%s : tensor cols %d x %d are not divisible by %d, required for %s", __func__, nx, ny, QK_K, ggml_type_name(new_type)); ++ convert_incompatible_tensor = true; ++ } else { ++ ++qs.n_k_quantized; ++ } ++ } ++ if (convert_incompatible_tensor) { ++ // TODO: Possibly reenable this in the future ++ // switch (new_type) { ++ // case GGML_TYPE_Q2_K: ++ // case GGML_TYPE_Q3_K: ++ // case GGML_TYPE_Q4_K: new_type = GGML_TYPE_Q5_0; break; ++ // case GGML_TYPE_Q5_K: new_type = GGML_TYPE_Q5_1; break; ++ // case GGML_TYPE_Q6_K: new_type = GGML_TYPE_Q8_0; break; ++ // default: throw std::runtime_error("\nUnsupported tensor size encountered\n"); ++ // } ++ new_type = GGML_TYPE_F16; ++ LLAMA_LOG_WARN(" - using fallback quantization %s\n", ggml_type_name(new_type)); ++ ++qs.n_fallback; ++ } ++ return new_type; ++} ++ + static ggml_type llama_tensor_get_type(quantize_state_internal & qs, ggml_type new_type, const ggml_tensor * tensor, llama_ftype ftype) { + const std::string name = ggml_get_name(tensor); + +@@ -18513,7 +18693,9 @@ static void llama_model_quantize_internal(const std::string & fname_inp, const s + if (llama_model_has_encoder(&model)) { + n_attn_layer *= 3; + } +- GGML_ASSERT((qs.n_attention_wv == n_attn_layer) && "n_attention_wv is unexpected"); ++ if (model.arch != LLM_ARCH_HYVID) { // TODO: Check why this fails ++ GGML_ASSERT((qs.n_attention_wv == n_attn_layer) && "n_attention_wv is unexpected"); ++ } + } + + size_t total_size_org = 0; +@@ -18547,6 +18729,51 @@ static void llama_model_quantize_internal(const std::string & fname_inp, const s + ctx_outs[i_split] = gguf_init_empty(); + } + gguf_add_tensor(ctx_outs[i_split], tensor); ++ // SD3 pos_embed needs special fix as first dim is 1, which gets truncated here ++ if (model.arch == LLM_ARCH_SD3) { ++ const std::string name = ggml_get_name(tensor); ++ if (name == "pos_embed" && tensor->ne[2] == 1) { ++ const int n_dim = 3; ++ gguf_set_tensor_ndim(ctx_outs[i_split], "pos_embed", n_dim); ++ LLAMA_LOG_INFO("\n%s: Correcting pos_embed shape for SD3: [key:%s]\n", __func__, tensor->name); ++ } ++ } ++ // same goes for auraflow ++ if (model.arch == LLM_ARCH_AURA) { ++ const std::string name = ggml_get_name(tensor); ++ if (name == "positional_encoding" && tensor->ne[2] == 1) { ++ const int n_dim = 3; ++ gguf_set_tensor_ndim(ctx_outs[i_split], "positional_encoding", n_dim); ++ LLAMA_LOG_INFO("\n%s: Correcting positional_encoding shape for AuraFlow: [key:%s]\n", __func__, tensor->name); ++ } ++ if (name == "register_tokens" && tensor->ne[2] == 1) { ++ const int n_dim = 3; ++ gguf_set_tensor_ndim(ctx_outs[i_split], "register_tokens", n_dim); ++ LLAMA_LOG_INFO("\n%s: Correcting register_tokens shape for AuraFlow: [key:%s]\n", __func__, tensor->name); ++ } ++ } ++ // conv3d fails due to max dims - unsure what to do here as we never even reach this check ++ if (model.arch == LLM_ARCH_HYVID) { ++ const std::string name = ggml_get_name(tensor); ++ if (name == "img_in.proj.weight" && tensor->ne[5] != 1 ) { ++ throw std::runtime_error("img_in.proj.weight size failed for HyVid"); ++ } ++ } ++ // All the modulation layers also have dim1, and I think conv3d fails here too but we segfaul way before that... ++ if (model.arch == LLM_ARCH_WAN) { ++ const std::string name = ggml_get_name(tensor); ++ if (name.find(".modulation") != std::string::npos && tensor->ne[2] == 1) { ++ const int n_dim = 3; ++ gguf_set_tensor_ndim(ctx_outs[i_split], tensor->name, n_dim); ++ LLAMA_LOG_INFO("\n%s: Correcting shape for Wan: [key:%s]\n", __func__, tensor->name); ++ } ++ // FLF2V model only ++ if (name == "img_emb.emb_pos") { ++ const int n_dim = 3; ++ gguf_set_tensor_ndim(ctx_outs[i_split], tensor->name, n_dim); ++ LLAMA_LOG_INFO("\n%s: Correcting shape for Wan FLF2V: [key:%s]\n", __func__, tensor->name); ++ } ++ } + } + + // Set split info if needed +@@ -18647,6 +18874,110 @@ static void llama_model_quantize_internal(const std::string & fname_inp, const s + // do not quantize relative position bias (T5) + quantize &= name.find("attn_rel_b.weight") == std::string::npos; + ++ // rules for image models ++ bool image_model = false; ++ if (model.arch == LLM_ARCH_FLUX) { ++ image_model = true; ++ quantize &= name.find("txt_in.") == std::string::npos; ++ quantize &= name.find("img_in.") == std::string::npos; ++ quantize &= name.find("time_in.") == std::string::npos; ++ quantize &= name.find("vector_in.") == std::string::npos; ++ quantize &= name.find("guidance_in.") == std::string::npos; ++ quantize &= name.find("final_layer.") == std::string::npos; ++ } ++ if (model.arch == LLM_ARCH_SD1 || model.arch == LLM_ARCH_SDXL) { ++ image_model = true; ++ quantize &= name.find("class_embedding.") == std::string::npos; ++ quantize &= name.find("time_embedding.") == std::string::npos; ++ quantize &= name.find("add_embedding.") == std::string::npos; ++ quantize &= name.find("time_embed.") == std::string::npos; ++ quantize &= name.find("label_emb.") == std::string::npos; ++ quantize &= name.find("conv_in.") == std::string::npos; ++ quantize &= name.find("conv_out.") == std::string::npos; ++ quantize &= name != "input_blocks.0.0.weight"; ++ quantize &= name != "out.2.weight"; ++ } ++ if (model.arch == LLM_ARCH_SD3) { ++ image_model = true; ++ quantize &= name.find("final_layer.") == std::string::npos; ++ quantize &= name.find("time_text_embed.") == std::string::npos; ++ quantize &= name.find("context_embedder.") == std::string::npos; ++ quantize &= name.find("t_embedder.") == std::string::npos; ++ quantize &= name.find("y_embedder.") == std::string::npos; ++ quantize &= name.find("x_embedder.") == std::string::npos; ++ quantize &= name != "proj_out.weight"; ++ quantize &= name != "pos_embed"; ++ } ++ if (model.arch == LLM_ARCH_AURA) { ++ image_model = true; ++ quantize &= name.find("t_embedder.") == std::string::npos; ++ quantize &= name.find("init_x_linear.") == std::string::npos; ++ quantize &= name != "modF.1.weight"; ++ quantize &= name != "cond_seq_linear.weight"; ++ quantize &= name != "final_linear.weight"; ++ quantize &= name != "final_linear.weight"; ++ quantize &= name != "positional_encoding"; ++ quantize &= name != "register_tokens"; ++ } ++ if (model.arch == LLM_ARCH_LTXV) { ++ image_model = true; ++ quantize &= name.find("adaln_single.") == std::string::npos; ++ quantize &= name.find("caption_projection.") == std::string::npos; ++ quantize &= name.find("patchify_proj.") == std::string::npos; ++ quantize &= name.find("proj_out.") == std::string::npos; ++ quantize &= name.find("scale_shift_table") == std::string::npos; // last block too ++ } ++ if (model.arch == LLM_ARCH_HYVID) { ++ image_model = true; ++ quantize &= name.find("txt_in.") == std::string::npos; ++ quantize &= name.find("img_in.") == std::string::npos; ++ quantize &= name.find("time_in.") == std::string::npos; ++ quantize &= name.find("vector_in.") == std::string::npos; ++ quantize &= name.find("guidance_in.") == std::string::npos; ++ quantize &= name.find("final_layer.") == std::string::npos; ++ } ++ if (model.arch == LLM_ARCH_WAN) { ++ image_model = true; ++ quantize &= name.find("modulation.") == std::string::npos; ++ quantize &= name.find("patch_embedding.") == std::string::npos; ++ quantize &= name.find("text_embedding.") == std::string::npos; ++ quantize &= name.find("time_projection.") == std::string::npos; ++ quantize &= name.find("time_embedding.") == std::string::npos; ++ quantize &= name.find("img_emb.") == std::string::npos; ++ quantize &= name.find("head.") == std::string::npos; ++ } ++ if (model.arch == LLM_ARCH_HIDREAM) { ++ image_model = true; ++ quantize &= name.find("p_embedder.") == std::string::npos; ++ quantize &= name.find("t_embedder.") == std::string::npos; ++ quantize &= name.find("x_embedder.") == std::string::npos; ++ quantize &= name.find("final_layer.") == std::string::npos; ++ quantize &= name.find(".ff_i.gate.weight") == std::string::npos; ++ quantize &= name.find("caption_projection.") == std::string::npos; ++ } ++ if (model.arch == LLM_ARCH_COSMOS) { ++ image_model = true; ++ quantize &= name.find("p_embedder.") == std::string::npos; ++ quantize &= name.find("t_embedder.") == std::string::npos; ++ quantize &= name.find("t_embedding_norm.") == std::string::npos; ++ quantize &= name.find("x_embedder.") == std::string::npos; ++ quantize &= name.find("pos_embedder.") == std::string::npos; ++ quantize &= name.find("final_layer.") == std::string::npos; ++ } ++ if (model.arch == LLM_ARCH_LUMINA2) { ++ image_model = true; ++ quantize &= name.find("t_embedder.") == std::string::npos; ++ quantize &= name.find("x_embedder.") == std::string::npos; ++ quantize &= name.find("final_layer.") == std::string::npos; ++ quantize &= name.find("cap_embedder.") == std::string::npos; ++ quantize &= name.find("context_refiner.") == std::string::npos; ++ quantize &= name.find("noise_refiner.") == std::string::npos; ++ } ++ // ignore 3D/4D tensors for image models as the code was never meant to handle these ++ if (image_model) { ++ quantize &= ggml_n_dims(tensor) == 2; ++ } ++ + enum ggml_type new_type; + void * new_data; + size_t new_size; +@@ -18655,6 +18986,9 @@ static void llama_model_quantize_internal(const std::string & fname_inp, const s + new_type = default_type; + + // get more optimal quantization type based on the tensor shape, layer, etc. ++ if (image_model) { ++ new_type = img_tensor_get_type(qs, new_type, tensor, ftype); ++ } else { + if (!params->pure && ggml_is_quantized(default_type)) { + new_type = llama_tensor_get_type(qs, new_type, tensor, ftype); + } +@@ -18664,6 +18998,7 @@ static void llama_model_quantize_internal(const std::string & fname_inp, const s + if (params->output_tensor_type < GGML_TYPE_COUNT && strcmp(tensor->name, "output.weight") == 0) { + new_type = params->output_tensor_type; + } ++ } + + // If we've decided to quantize to the same type the tensor is already + // in then there's nothing to do. diff --git a/ComfyUI/custom_nodes/ComfyUI-GGUF/tools/read_tensors.py b/ComfyUI/custom_nodes/ComfyUI-GGUF/tools/read_tensors.py new file mode 100644 index 0000000000000000000000000000000000000000..1bdff028a787c09b38e5616ef75a2f070c672445 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-GGUF/tools/read_tensors.py @@ -0,0 +1,21 @@ +#!/usr/bin/python3 +import os +import sys +import gguf + +def read_tensors(path): + reader = gguf.GGUFReader(path) + for tensor in reader.tensors: + if tensor.tensor_type == gguf.GGMLQuantizationType.F32: + continue + print(f"{str(tensor.tensor_type):32}: {tensor.name}") + +try: + path = sys.argv[1] + assert os.path.isfile(path), "Invalid path" + print(f"input: {path}") +except Exception as e: + input(f"failed: {e}") +else: + read_tensors(path) + input() diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/.github/workflows/publish.yml b/ComfyUI/custom_nodes/ComfyUI-KJNodes/.github/workflows/publish.yml new file mode 100644 index 0000000000000000000000000000000000000000..e155f5f40e46fa83942dee5a9460e8093f3b4208 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-KJNodes/.github/workflows/publish.yml @@ -0,0 +1,25 @@ +name: Publish to Comfy registry +on: + workflow_dispatch: + push: + branches: + - main + paths: + - "pyproject.toml" + +permissions: + issues: write + +jobs: + publish-node: + name: Publish Custom Node to registry + runs-on: ubuntu-latest + if: ${{ github.repository_owner == 'kijai' }} + steps: + - name: Check out code + uses: actions/checkout@v4 + - name: Publish Custom Node + uses: Comfy-Org/publish-node-action@v1 + with: + ## Add your own personal access token to your Github Repository secrets and reference it here. + personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }} diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/.gitignore b/ComfyUI/custom_nodes/ComfyUI-KJNodes/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..6f1bfa9b027ca5507d6edf093373ac1688ff9c19 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-KJNodes/.gitignore @@ -0,0 +1,13 @@ +__pycache__ +/venv +*.code-workspace +.history +.vscode +*.ckpt +*.pth +types +models +jsconfig.json +custom_dimensions.json +.claude +.tests diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/LICENSE b/ComfyUI/custom_nodes/ComfyUI-KJNodes/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..f288702d2fa16d3cdf0035b15a9fcbc552cd88e7 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-KJNodes/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/README.md b/ComfyUI/custom_nodes/ComfyUI-KJNodes/README.md new file mode 100644 index 0000000000000000000000000000000000000000..77e2a46ef0cb1240613555880077bcead86ab260 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-KJNodes/README.md @@ -0,0 +1,84 @@ + +# KJNodes for ComfyUI + +At this point pretty random collection of utility, model optimization and QoL nodes, while keeping dependencies at minimum. + +Documentation is mostly in the node descriptions and tooltips. + +# Installation +1. Clone this repo into `custom_nodes` folder. +2. Install dependencies: `pip install -r requirements.txt` + or if you use the portable install, run this in ComfyUI_windows_portable -folder: + + `python_embeded\python.exe -m pip install -r ComfyUI\custom_nodes\ComfyUI-KJNodes\requirements.txt` + +# JS extensions: + +Node insert (default hotkey D, configurable) and shake to disconnect (default disabled, found in settings to enable): + +https://github.com/user-attachments/assets/e81f02e6-9cb0-4cb3-bcdd-67e7bd5bf6fb + +Node swap (default hotkey S, configurable): + +https://github.com/user-attachments/assets/735d973e-1b8b-4936-a1fb-29736b9c063a + + +### Set/Get + +# UPDATE March 19th 2026 + +Complete (backwards compatible as far as I'm aware currently) rewrite of Set/Get nodes. + +# Changelog March 19th 2026 + +## New Features + +- **Nodes 2.0 support** - Works with nodes 2.0 as well as legacy. + +- **Subgraph support** — Set/Get now works across subgraph boundaries. A Set in a parent graph is visible to all child subgraphs, and Get nodes search upward through ancestors. Cross-graph connections are resolved during prompt execution. + +- **Convert link to Set/Get** — Right-click any link midpoint to convert it into a Set/Get pair. + +- **Convert to links** — Right-click a Set or Get node to convert the pair back to direct links. + +- **Convert outputs to Set/Get** — Batch operation to convert all outputs of selected nodes into Set/Get pairs at once. + +- **Canvas right-click menu** — "Convert outputs on all selected nodes to Set/Get" and "Convert selected Set/Get to links". + +- **Keyboard shortcuts** — Ctrl+Shift+S (add Set to selected / at cursor), Ctrl+Shift+G (add Get at cursor), Ctrl+Shift+L (force-show all connections toggle). + +- **"Convert ALL Set/Get to links" setting** — One-click button in settings to replace every Set/Get pair with direct links, including across subgraph boundaries. + +- **Show links setting** — Combo setting (never / selected / always) to control when virtual links between Set/Get pairs are drawn. "Selected" mode shows links only for currently selected Set/Get nodes. Ctrl+Shift+L temporarily forces all links visible. + +- **Add Set/Get from connection menu** — When dragging from a slot, "Add SetNode" and "Add GetNode" entries appear next to "Add Reroute" in the connection menu. + +- **Shift+middle-click creates Set/Get** — Shift+middle-click on an output slot creates a connected SetNode; on an input slot creates a connected GetNode. Optional setting to override normal middle-click to also create Set/Get instead of Reroute. + +- **Add paired GetNode** — Right-click a SetNode to add a paired GetNode pre-configured with the same name. + +- **Double-click Get to jump to Set** — Double-click a GetNode to center and select its paired SetNode. + +- **Default widget value setting** — Controls the initial Constant value when a Set node is first connected: empty (default), slot name, slot name (lowercase), or slot name (UPPERCASE). + +- **Type inference from output** — If a SetNode's input is unconnected but its output is connected to a typed input, the Set adopts that type and recolors accordingly. + +- **Getters submenu navigates into subgraphs** — SetNode's getter list now includes cross-subgraph GetNodes and can navigate into subgraphs to focus them. + +## Fixes + +- **Paste rename coordination** — When pasting Set+Get pairs, GetNodes now correctly follow their SetNode's rename (e.g., `MODEL` -> `MODEL_0`). + +- **Type/color reset on disconnect** — Disconnecting input or output now correctly resets type and color, but only when neither side is still connected. + +- **Suffix stripping only on paste** — `validateName` no longer strips intentional `_N` suffixes from user-typed names. Only strips during paste to avoid `FOO_0_1_2` accumulation. + +- **Connection drawing moved to canvas level** — Visualization links remain visible even when nodes are off-screen or collapsed. + +## Settings + +- Settings are now organized under **KJNodes > Set & Get** and **KJNodes > General** categories in the settings panel. + + +--- + diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/__init__.py b/ComfyUI/custom_nodes/ComfyUI-KJNodes/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..79fe5df0faaad8777b84aef6067276479803f510 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-KJNodes/__init__.py @@ -0,0 +1,407 @@ +from .nodes.nodes import ( + BOOLConstant, INTConstant, FloatConstant, StringConstant, StringConstantMultiline, + ScaleBatchPromptSchedule, GetLatentsFromBatchIndexed, ConditioningMultiCombine, + AppendStringsToList, JoinStrings, JoinStringMulti, CondPassThrough, ModelPassThrough, + ConditioningSetMaskAndCombine, ConditioningSetMaskAndCombine3, + ConditioningSetMaskAndCombine4, ConditioningSetMaskAndCombine5, + VRAM_Debug, SomethingToString, Sleep, EmptyLatentImagePresets, + EmptyLatentImageCustomPresets, WidgetToString, DummyOut, FlipSigmasAdjusted, + CustomSigmas, StringToFloatList, InjectNoiseToLatent, SoundReactive, GenerateNoise, + StableZero123_BatchSchedule, SV3D_BatchSchedule, Superprompt, CameraPoseVisualizer, + CheckpointPerturbWeights, DifferentialDiffusionAdvanced, FluxBlockLoraSelect, + HunyuanVideoBlockLoraSelect, Wan21BlockLoraSelect, LTX2BlockLoraSelect, + DiTBlockLoraLoader, CustomControlNetWeightsFluxFromList, + SetShakkerLabsUnionControlNetType, ModelSaveKJ, StyleModelApplyAdvanced, + AudioConcatenate, LeapfusionHunyuanI2V, ImageNoiseAugmentation, VAELoaderKJ, VAEMergeKJ, + ScheduledCFGGuidance, ApplyRifleXRoPE_WanVideo, ApplyRifleXRoPE_HunuyanVideo, + TimerNodeKJ, HunyuanVideoEncodeKeyframesToCond, LazySwitchKJ, LatentInpaintTTM, + SimpleCalculatorKJ, GetTrackRange, AddNoiseToTrackPath, VAEDecodeLoopKJ, + WanImageToVideoSVIPro, DeprecatedCompileNodeKJ, VisualizeSigmasKJ, + PreviewLatentNoiseMask, PlaySoundKJ, +) +from .nodes.curve_nodes import ( + PlotCoordinates, SplineEditor, CreateShapeMaskOnPath, CreateShapeImageOnPath, + CreateTextOnPath, CreateGradientFromCoords, GradientToFloat, MaskOrImageToWeight, + WeightScheduleConvert, FloatToMask, WeightScheduleExtend, FloatToSigmas, SigmasToFloat, + GLIGENTextBoxApplyBatchCoords, CreateInstanceDiffusionTracking, + AppendInstanceDiffusionTracking, InterpolateCoords, DrawInstanceDiffusionTracking, + PointsEditor, CutAndDragOnPath, +) +from .nodes.batchcrop_nodes import ( + BatchCropFromMask, BatchUncrop, BatchCropFromMaskAdvanced, + FilterZeroMasksAndCorrespondingImages, InsertImageBatchByIndexes, BatchUncropAdvanced, + SplitBboxes, BboxToInt, BboxVisualize, +) +from .nodes.audioscheduler_nodes import ( + NormalizedAmplitudeToMask, NormalizedAmplitudeToFloatList, + OffsetMaskByNormalizedAmplitude, ImageTransformByNormalizedAmplitude, +) +from .nodes.image_nodes import ( + ImagePass, ColorMatch, ColorMatchV2, SaveImageWithAlpha, ImageConcanate, + ImageConcatFromBatch, ImageGridComposite2x2, ImageGridComposite3x3, + ImageBatchTestPattern, ImageGrabPIL, Screencap_mss, ScreencapStream, WebcamCaptureCV2, + AddLabel, GetImageSizeAndCount, GetLatentSizeAndCount, ImageBatchRepeatInterleaving, + ImageUpscaleWithModelBatched, ImageNormalize_Neg1_To_1, RemapImageRange, + SplitImageChannels, MergeImageChannels, ImagePadForOutpaintMasked, + ImagePadForOutpaintTargetSize, ImagePrepForICLora, ImageAndMaskPreview, + CrossFadeImages, CrossFadeImagesMulti, TransitionImagesMulti, TransitionImagesInBatch, + ImageBatchJoinWithTransition, ShuffleImageBatch, GetImageRangeFromBatch, + RandomImageFromBatch, ImageBatchExtendWithOverlap, GetLatentRangeFromBatch, + InsertLatentToIndex, ImageBatchFilter, GetImagesFromBatchIndexed, + InsertImagesToBatchIndexed, PadImageBatchInterleaved, ReplaceImagesInBatch, + ReverseImageBatch, ImageBatchMulti, ImageTensorList, ImageAddMulti, ImageConcatMulti, + PreviewAnimation, ImageResizeKJ, ImageResizeKJv2, LoadAndResizeImage, + LoadImagesFromFolderKJ, ImageGridtoBatch, SaveImageKJ, SaveStringKJ, FastPreview, + FastPreviewBatch, ImageCropByMaskAndResize, ImageCropByMask, ImageUncropByMask, + ImageCropByMaskBatch, ImagePadKJ, LoadVideosFromFolder, EncodeVideoComponents, + DecodeAndSaveVideo, PreviewImageOrMask, +) + +from .nodes.mask_nodes import ( + BatchCLIPSeg, DownloadAndLoadCLIPSeg, CreateTextMask, ColorToMask, CreateFluidMask, + CreateAudioMask, CreateGradientMask, CreateFadeMask, CreateFadeMaskAdvanced, + CreateMagicMask, CreateShapeMask, CreateVoronoiMask, GetMaskSizeAndCount, + GrowMaskWithBlur, MaskBatchMulti, OffsetMask, RoundMask, ResizeMask, RemapMaskRange, + SeparateMasks, ConsolidateMasksKJ, DrawMaskOnImage, BlockifyMask, +) +from .nodes.model_optimization_nodes import ( + PathchSageAttentionKJ, PatchFlashAttentionKJ, CheckpointLoaderKJ, DiffusionModelSelector, + DiffusionModelLoaderKJ, ModelPatchTorchSettings, PatchModelPatcherOrder, + TorchCompileModelFluxAdvancedV2, TorchCompileModelWanVideoV2, + TorchCompileModelAdvanced, TorchCompileVAE, TorchCompileControlNet, + WanVideoTeaCacheKJ, WanVideoEnhanceAVideoKJ, LTXVEnhanceAVideoKJ, WanVideoNAG, Krea2PromptWeight, + SkipLayerGuidanceWanVideo, CFGZeroStarAndInit, GGUFLoaderKJ, NABLA_AttentionKJ, + StartRecordCUDAMemoryHistory, EndRecordCUDAMemoryHistory, VisualizeCUDAMemoryHistory, + ModelMemoryUseReportPatch, ModelMemoryUsageFactorOverride, WanChunkFeedForward, + SamplerSelfRefineVideo, PiDColorBiasCorrection, Ideogram4OptimizationsKJ, +) +from .nodes.lora_nodes import LoraExtractKJ, LoraReduceRank +from .nodes.image_transform_node import ImageTransformKJ, BBOXToBoundingBoxKJ +from .nodes.sharpen_nodes import ImageSharpenKJ +from .nodes.hdr_preview_node import HDRPreviewKJ +from .nodes.preview_override_node import ModelPreviewOverrideKJ, GetPreviewOverrideFramesKJ +from .nodes.context_windows_visualizer import ContextWindowsVisualizerKJ +from .nodes.ideogram4_nodes import Ideogram4PromptBuilderKJ + +import logging + +NODE_CONFIG = { + #constants + "BOOLConstant": {"class": BOOLConstant, "name": "BOOL Constant"}, + "INTConstant": {"class": INTConstant, "name": "INT Constant"}, + "FloatConstant": {"class": FloatConstant, "name": "Float Constant"}, + "StringConstant": {"class": StringConstant, "name": "String Constant"}, + "StringConstantMultiline": {"class": StringConstantMultiline, "name": "String Constant Multiline"}, + #conditioning + "ConditioningMultiCombine": {"class": ConditioningMultiCombine, "name": "Conditioning Multi Combine"}, + "ConditioningSetMaskAndCombine": {"class": ConditioningSetMaskAndCombine, "name": "ConditioningSetMaskAndCombine"}, + "ConditioningSetMaskAndCombine3": {"class": ConditioningSetMaskAndCombine3, "name": "ConditioningSetMaskAndCombine3"}, + "ConditioningSetMaskAndCombine4": {"class": ConditioningSetMaskAndCombine4, "name": "ConditioningSetMaskAndCombine4"}, + "ConditioningSetMaskAndCombine5": {"class": ConditioningSetMaskAndCombine5, "name": "ConditioningSetMaskAndCombine5"}, + "CondPassThrough": {"class": CondPassThrough}, + "WanImageToVideoSVIPro": {"class": WanImageToVideoSVIPro, "name": "Wan Image To Video SVIPro"}, + #masking + "DrawMaskOnImage": {"class": DrawMaskOnImage, "name": "Draw Mask On Image"}, + "DownloadAndLoadCLIPSeg": {"class": DownloadAndLoadCLIPSeg, "name": "(Down)load CLIPSeg"}, + "BatchCLIPSeg": {"class": BatchCLIPSeg, "name": "Batch CLIPSeg"}, + "BlockifyMask": {"class": BlockifyMask, "name": "Blockify Mask"}, + "ColorToMask": {"class": ColorToMask, "name": "Color To Mask"}, + "CreateGradientMask": {"class": CreateGradientMask, "name": "Create Gradient Mask"}, + "CreateTextMask": {"class": CreateTextMask, "name": "Create Text Mask"}, + "CreateAudioMask": {"class": CreateAudioMask, "name": "Create Audio Mask"}, + "CreateFadeMask": {"class": CreateFadeMask, "name": "Create Fade Mask"}, + "CreateFadeMaskAdvanced": {"class": CreateFadeMaskAdvanced, "name": "Create Fade Mask Advanced"}, + "CreateFluidMask": {"class": CreateFluidMask, "name": "Create Fluid Mask"}, + "CreateShapeMask": {"class": CreateShapeMask, "name": "Create Shape Mask"}, + "CreateVoronoiMask": {"class": CreateVoronoiMask, "name": "Create Voronoi Mask"}, + "CreateMagicMask": {"class": CreateMagicMask, "name": "Create Magic Mask"}, + "GetMaskSizeAndCount": {"class": GetMaskSizeAndCount, "name": "Get Mask Size & Count"}, + "GrowMaskWithBlur": {"class": GrowMaskWithBlur, "name": "Grow Mask With Blur"}, + "MaskBatchMulti": {"class": MaskBatchMulti, "name": "Mask Batch Multi"}, + "OffsetMask": {"class": OffsetMask, "name": "Offset Mask"}, + "RemapMaskRange": {"class": RemapMaskRange, "name": "Remap Mask Range"}, + "ResizeMask": {"class": ResizeMask, "name": "Resize Mask"}, + "RoundMask": {"class": RoundMask, "name": "Round Mask"}, + "SeparateMasks": {"class": SeparateMasks, "name": "Separate Masks"}, + "ConsolidateMasksKJ": {"class": ConsolidateMasksKJ, "name": "Consolidate Masks"}, + #images + "AddLabel": {"class": AddLabel, "name": "Add Label"}, + "ColorMatch": {"class": ColorMatch, "name": "Color Match"}, + "ColorMatchV2": {"class": ColorMatchV2, "name": "Color Match V2"}, + "ImageTensorList": {"class": ImageTensorList, "name": "Image Tensor List"}, + "CrossFadeImages": {"class": CrossFadeImages, "name": "Cross Fade Images"}, + "CrossFadeImagesMulti": {"class": CrossFadeImagesMulti, "name": "Cross Fade Images Multi"}, + "GetImagesFromBatchIndexed": {"class": GetImagesFromBatchIndexed, "name": "Get Images From Batch Indexed"}, + "GetImageRangeFromBatch": {"class": GetImageRangeFromBatch, "name": "Get Image or Mask Range From Batch"}, + "RandomImageFromBatch": {"class": RandomImageFromBatch, "name": "Random Image From Batch"}, + "GetLatentRangeFromBatch": {"class": GetLatentRangeFromBatch, "name": "Get Latent Range From Batch"}, + "GetLatentSizeAndCount": {"class": GetLatentSizeAndCount, "name": "Get Latent Size & Count"}, + "GetImageSizeAndCount": {"class": GetImageSizeAndCount, "name": "Get Image Size & Count"}, + "FastPreview": {"class": FastPreview, "name": "Fast Preview"}, + "FastPreviewBatch": {"class": FastPreviewBatch, "name": "Fast Preview Batch"}, + "ImageBatchFilter": {"class": ImageBatchFilter, "name": "Image Batch Filter"}, + "ImageAndMaskPreview": {"class": ImageAndMaskPreview}, + "ImageAddMulti": {"class": ImageAddMulti, "name": "Image Add Multi"}, + "ImageBatchJoinWithTransition": {"class": ImageBatchJoinWithTransition, "name": "Image Batch Join With Transition"}, + "ImageBatchMulti": {"class": ImageBatchMulti, "name": "Image Batch Multi"}, + "ImageBatchRepeatInterleaving": {"class": ImageBatchRepeatInterleaving}, + "ImageBatchTestPattern": {"class": ImageBatchTestPattern, "name": "Image Batch Test Pattern"}, + "ImageConcanate": {"class": ImageConcanate, "name": "Image Concatenate"}, + "ImageConcatFromBatch": {"class": ImageConcatFromBatch, "name": "Image Concatenate From Batch"}, + "ImageConcatMulti": {"class": ImageConcatMulti, "name": "Image Concatenate Multi"}, + "ImageCropByMask": {"class": ImageCropByMask, "name": "Image Crop By Mask"}, + "ImageCropByMaskAndResize": {"class": ImageCropByMaskAndResize, "name": "Image Crop By Mask And Resize"}, + "ImageCropByMaskBatch": {"class": ImageCropByMaskBatch, "name": "Image Crop By Mask Batch"}, + "ImageUncropByMask": {"class": ImageUncropByMask, "name": "Image Uncrop By Mask"}, + "ImageBatchExtendWithOverlap": {"class": ImageBatchExtendWithOverlap, "name": "Image Batch Extend With Overlap"}, + "ImageGrabPIL": {"class": ImageGrabPIL, "name": "Image Grab PIL"}, + "ImageGridComposite2x2": {"class": ImageGridComposite2x2, "name": "Image Grid Composite 2x2"}, + "ImageGridComposite3x3": {"class": ImageGridComposite3x3, "name": "Image Grid Composite 3x3"}, + "ImageGridtoBatch": {"class": ImageGridtoBatch, "name": "Image Grid To Batch"}, + "ImageNoiseAugmentation": {"class": ImageNoiseAugmentation, "name": "Image Noise Augmentation"}, + "ImageNormalize_Neg1_To_1": {"class": ImageNormalize_Neg1_To_1, "name": "Image Normalize -1 to 1"}, + "ImagePass": {"class": ImagePass}, + "ImagePadKJ": {"class": ImagePadKJ, "name": "ImagePad KJ"}, + "ImagePadForOutpaintMasked": {"class": ImagePadForOutpaintMasked, "name": "Image Pad For Outpaint Masked"}, + "ImagePadForOutpaintTargetSize": {"class": ImagePadForOutpaintTargetSize, "name": "Image Pad For Outpaint Target Size"}, + "ImagePrepForICLora": {"class": ImagePrepForICLora, "name": "Image Prep For ICLora"}, + "ImageResizeKJ": {"class": ImageResizeKJ, "name": "Resize Image (deprecated)"}, + "ImageResizeKJv2": {"class": ImageResizeKJv2, "name": "Resize Image v2"}, + "ImageUpscaleWithModelBatched": {"class": ImageUpscaleWithModelBatched, "name": "Image Upscale With Model Batched"}, + "InsertImagesToBatchIndexed": {"class": InsertImagesToBatchIndexed, "name": "Insert Images To Batch Indexed"}, + "InsertLatentToIndexed": {"class": InsertLatentToIndex, "name": "Insert Latent To Index"}, + "LoadAndResizeImage": {"class": LoadAndResizeImage, "name": "Load & Resize Image"}, + "LoadImagesFromFolderKJ": {"class": LoadImagesFromFolderKJ, "name": "Load Images From Folder (KJ)"}, + "LoadVideosFromFolder": {"class": LoadVideosFromFolder, "name": "Load Videos From Folder"}, + "MergeImageChannels": {"class": MergeImageChannels, "name": "Merge Image Channels"}, + "PadImageBatchInterleaved": {"class": PadImageBatchInterleaved, "name": "Pad Image Batch Interleaved"}, + "PreviewAnimation": {"class": PreviewAnimation, "name": "Preview Animation"}, + "RemapImageRange": {"class": RemapImageRange, "name": "Remap Image Range"}, + "ReverseImageBatch": {"class": ReverseImageBatch, "name": "Reverse Image Batch"}, + "ReplaceImagesInBatch": {"class": ReplaceImagesInBatch, "name": "Replace Images In Batch"}, + "SaveImageWithAlpha": {"class": SaveImageWithAlpha, "name": "Save Image With Alpha"}, + "SaveImageKJ": {"class": SaveImageKJ, "name": "Save Image KJ"}, + "ShuffleImageBatch": {"class": ShuffleImageBatch, "name": "Shuffle Image Batch"}, + "SplitImageChannels": {"class": SplitImageChannels, "name": "Split Image Channels"}, + "TransitionImagesMulti": {"class": TransitionImagesMulti, "name": "Transition Images Multi"}, + "TransitionImagesInBatch": {"class": TransitionImagesInBatch, "name": "Transition Images In Batch"}, + "EncodeVideoComponents": {"class": EncodeVideoComponents, "name": "Encode Video Components"}, + "DecodeAndSaveVideo": {"class": DecodeAndSaveVideo, "name": "Decode And Save Video"}, + "ImageTransformKJ": {"class": ImageTransformKJ, "name": "Image Transform KJ"}, + "BBOXToBoundingBoxKJ": {"class": BBOXToBoundingBoxKJ, "name": "BBOX to Bounding Box KJ"}, + "Ideogram4PromptBuilderKJ": {"class": Ideogram4PromptBuilderKJ, "name": "Ideogram 4 Prompt Builder KJ"}, + "HDRPreviewKJ": {"class": HDRPreviewKJ, "name": "HDR Preview KJ"}, + "ModelPreviewOverrideKJ": {"class": ModelPreviewOverrideKJ, "name": "Model Preview Override KJ"}, + "GetPreviewOverrideFramesKJ": {"class": GetPreviewOverrideFramesKJ, "name": "Get Preview Override Frames KJ"}, + "PreviewImageOrMask": {"class": PreviewImageOrMask, "name": "Preview Image Or Mask"}, + "ImageSharpenKJ": {"class": ImageSharpenKJ, "name": "Image Sharpen KJ"}, + #batch cropping + "BatchCropFromMask": {"class": BatchCropFromMask, "name": "Batch Crop From Mask"}, + "BatchCropFromMaskAdvanced": {"class": BatchCropFromMaskAdvanced, "name": "Batch Crop From Mask Advanced"}, + "FilterZeroMasksAndCorrespondingImages": {"class": FilterZeroMasksAndCorrespondingImages}, + "InsertImageBatchByIndexes": {"class": InsertImageBatchByIndexes, "name": "Insert Image Batch By Indexes"}, + "BatchUncrop": {"class": BatchUncrop, "name": "Batch Uncrop"}, + "BatchUncropAdvanced": {"class": BatchUncropAdvanced, "name": "Batch Uncrop Advanced"}, + "SplitBboxes": {"class": SplitBboxes, "name": "Split Bboxes"}, + "BboxToInt": {"class": BboxToInt, "name": "Bbox To Int"}, + "BboxVisualize": {"class": BboxVisualize, "name": "Bbox Visualize"}, + #noise + "GenerateNoise": {"class": GenerateNoise, "name": "Generate Noise"}, + "FlipSigmasAdjusted": {"class": FlipSigmasAdjusted, "name": "Flip Sigmas Adjusted"}, + "InjectNoiseToLatent": {"class": InjectNoiseToLatent, "name": "Inject Noise To Latent"}, + "CustomSigmas": {"class": CustomSigmas, "name": "Custom Sigmas"}, + #utility + "StringToFloatList": {"class": StringToFloatList, "name": "String to Float List"}, + "WidgetToString": {"class": WidgetToString, "name": "Widget To String"}, + "SaveStringKJ": {"class": SaveStringKJ, "name": "Save String KJ"}, + "DummyOut": {"class": DummyOut, "name": "Dummy Out"}, + "GetLatentsFromBatchIndexed": {"class": GetLatentsFromBatchIndexed, "name": "Get Latents From Batch Indexed"}, + "ScaleBatchPromptSchedule": {"class": ScaleBatchPromptSchedule, "name": "Scale Batch Prompt Schedule"}, + "CameraPoseVisualizer": {"class": CameraPoseVisualizer, "name": "Camera Pose Visualizer"}, + "AppendStringsToList": {"class": AppendStringsToList, "name": "Append Strings To List"}, + "JoinStrings": {"class": JoinStrings, "name": "Join Strings"}, + "JoinStringMulti": {"class": JoinStringMulti, "name": "Join String Multi"}, + "SimpleCalculatorKJ": {"class": SimpleCalculatorKJ, "name": "Simple Calculator KJ"}, + "SomethingToString": {"class": SomethingToString, "name": "Something To String"}, + "Sleep": {"class": Sleep, "name": "Sleep"}, + "VRAM_Debug": {"class": VRAM_Debug, "name": "VRAM Debug"}, + "EmptyLatentImagePresets": {"class": EmptyLatentImagePresets, "name": "Empty Latent Image Presets"}, + "EmptyLatentImageCustomPresets": {"class": EmptyLatentImageCustomPresets, "name": "Empty Latent Image Custom Presets"}, + "ModelPassThrough": {"class": ModelPassThrough, "name": "ModelPass"}, + "ModelSaveKJ": {"class": ModelSaveKJ, "name": "Model Save KJ"}, + "SetShakkerLabsUnionControlNetType": {"class": SetShakkerLabsUnionControlNetType, "name": "Set Shakker Labs Union ControlNet Type"}, + "StyleModelApplyAdvanced": {"class": StyleModelApplyAdvanced, "name": "Style Model Apply Advanced"}, + "DiffusionModelSelector": {"class": DiffusionModelSelector, "name": "Diffusion Model Selector"}, + "LazySwitchKJ": {"class": LazySwitchKJ, "name": "Lazy Switch KJ"}, + "VisualizeSigmasKJ": {"class": VisualizeSigmasKJ, "name": "Visualize Sigmas KJ"}, + #audioscheduler stuff + "NormalizedAmplitudeToMask": {"class": NormalizedAmplitudeToMask}, + "NormalizedAmplitudeToFloatList": {"class": NormalizedAmplitudeToFloatList}, + "OffsetMaskByNormalizedAmplitude": {"class": OffsetMaskByNormalizedAmplitude}, + "ImageTransformByNormalizedAmplitude": {"class": ImageTransformByNormalizedAmplitude}, + "AudioConcatenate": {"class": AudioConcatenate}, + #curve nodes + "SplineEditor": {"class": SplineEditor, "name": "Spline Editor"}, + "CreateShapeImageOnPath": {"class": CreateShapeImageOnPath, "name": "Create Shape Image On Path"}, + "CreateShapeMaskOnPath": {"class": CreateShapeMaskOnPath, "name": "Create Shape Mask On Path"}, + "CreateTextOnPath": {"class": CreateTextOnPath, "name": "Create Text On Path"}, + "CreateGradientFromCoords": {"class": CreateGradientFromCoords, "name": "Create Gradient From Coords"}, + "CutAndDragOnPath": {"class": CutAndDragOnPath, "name": "Cut And Drag On Path"}, + "GradientToFloat": {"class": GradientToFloat, "name": "Gradient To Float"}, + "WeightScheduleExtend": {"class": WeightScheduleExtend, "name": "Weight Schedule Extend"}, + "MaskOrImageToWeight": {"class": MaskOrImageToWeight, "name": "Mask Or Image To Weight"}, + "WeightScheduleConvert": {"class": WeightScheduleConvert, "name": "Weight Schedule Convert"}, + "FloatToMask": {"class": FloatToMask, "name": "Float To Mask"}, + "FloatToSigmas": {"class": FloatToSigmas, "name": "Float To Sigmas"}, + "SigmasToFloat": {"class": SigmasToFloat, "name": "Sigmas To Float"}, + "PlotCoordinates": {"class": PlotCoordinates, "name": "Plot Coordinates"}, + "InterpolateCoords": {"class": InterpolateCoords, "name": "Interpolate Coords"}, + "PointsEditor": {"class": PointsEditor, "name": "Points Editor"}, + #experimental + "SoundReactive": {"class": SoundReactive, "name": "Sound Reactive"}, + "StableZero123_BatchSchedule": {"class": StableZero123_BatchSchedule, "name": "Stable Zero123 Batch Schedule"}, + "SV3D_BatchSchedule": {"class": SV3D_BatchSchedule, "name": "SV3D Batch Schedule"}, + "Superprompt": {"class": Superprompt, "name": "Superprompt"}, + "GLIGENTextBoxApplyBatchCoords": {"class": GLIGENTextBoxApplyBatchCoords}, + "CheckpointPerturbWeights": {"class": CheckpointPerturbWeights, "name": "CheckpointPerturbWeights"}, + "Screencap_mss": {"class": Screencap_mss, "name": "Screencap mss"}, + "ScreencapStream": {"class": ScreencapStream, "name": "Screencap Stream"}, + "WebcamCaptureCV2": {"class": WebcamCaptureCV2, "name": "Webcam Capture CV2"}, + "DifferentialDiffusionAdvanced": {"class": DifferentialDiffusionAdvanced, "name": "Differential Diffusion Advanced"}, + "DiTBlockLoraLoader": {"class": DiTBlockLoraLoader, "name": "DiT Block Lora Loader"}, + "FluxBlockLoraSelect": {"class": FluxBlockLoraSelect, "name": "Flux Block Lora Select"}, + "HunyuanVideoBlockLoraSelect": {"class": HunyuanVideoBlockLoraSelect, "name": "Hunyuan Video Block Lora Select"}, + "Wan21BlockLoraSelect": {"class": Wan21BlockLoraSelect, "name": "Wan21 Block Lora Select"}, + "LTX2BlockLoraSelect": {"class": LTX2BlockLoraSelect, "name": "LTX2 Block Lora Select"}, + "CustomControlNetWeightsFluxFromList": {"class": CustomControlNetWeightsFluxFromList, "name": "Custom ControlNet Weights Flux From List"}, + "CheckpointLoaderKJ": {"class": CheckpointLoaderKJ, "name": "CheckpointLoaderKJ"}, + "DiffusionModelLoaderKJ": {"class": DiffusionModelLoaderKJ, "name": "Diffusion Model Loader KJ"}, + "TorchCompileModelFluxAdvancedV2": {"class": TorchCompileModelFluxAdvancedV2, "name": "TorchCompileModelFluxAdvancedV2"}, + "TorchCompileVAE": {"class": TorchCompileVAE, "name": "TorchCompileVAE"}, + "TorchCompileControlNet": {"class": TorchCompileControlNet, "name": "TorchCompileControlNet"}, + "TorchCompileModelWanVideoV2": {"class": TorchCompileModelWanVideoV2, "name": "TorchCompileModelWanVideoV2"}, + "PathchSageAttentionKJ": {"class": PathchSageAttentionKJ, "name": "Patch Sage Attention KJ"}, + "PatchFlashAttentionKJ": {"class": PatchFlashAttentionKJ, "name": "Patch Flash Attention KJ"}, + "LeapfusionHunyuanI2VPatcher": {"class": LeapfusionHunyuanI2V, "name": "Leapfusion Hunyuan I2V Patcher"}, + "VAELoaderKJ": {"class": VAELoaderKJ, "name": "VAELoader KJ"}, + "VAEMergeKJ": {"class": VAEMergeKJ, "name": "VAE Merge KJ"}, + "VAEDecodeLoopKJ": {"class": VAEDecodeLoopKJ, "name": "VAE Decode Loop KJ"}, + "ScheduledCFGGuidance": {"class": ScheduledCFGGuidance, "name": "Scheduled CFG Guidance"}, + "ApplyRifleXRoPE_HunuyanVideo": {"class": ApplyRifleXRoPE_HunuyanVideo, "name": "Apply RifleXRoPE HunuyanVideo"}, + "ApplyRifleXRoPE_WanVideo": {"class": ApplyRifleXRoPE_WanVideo, "name": "Apply RifleXRoPE WanVideo"}, + "WanVideoTeaCacheKJ": {"class": WanVideoTeaCacheKJ, "name": "WanVideo Tea Cache (native)"}, + "WanVideoEnhanceAVideoKJ": {"class": WanVideoEnhanceAVideoKJ, "name": "WanVideo Enhance A Video (native)"}, + "SkipLayerGuidanceWanVideo": {"class": SkipLayerGuidanceWanVideo, "name": "Skip Layer Guidance WanVideo"}, + "TimerNodeKJ": {"class": TimerNodeKJ, "name": "Timer Node KJ"}, + "HunyuanVideoEncodeKeyframesToCond": {"class": HunyuanVideoEncodeKeyframesToCond, "name": "HunyuanVideo Encode Keyframes To Cond"}, + "CFGZeroStarAndInit": {"class": CFGZeroStarAndInit, "name": "CFG Zero Star/Init"}, + "PiDColorBiasCorrection": {"class": PiDColorBiasCorrection, "name": "PiD Color Bias Correction"}, + "ModelPatchTorchSettings": {"class": ModelPatchTorchSettings, "name": "Model Patch Torch Settings"}, + "WanVideoNAG": {"class": WanVideoNAG, "name": "WanVideoNAG"}, + "Krea2PromptWeight": {"class": Krea2PromptWeight, "name": "Krea2 Prompt Weight"}, + "GGUFLoaderKJ": {"class": GGUFLoaderKJ, "name": "GGUF Loader KJ"}, + "LatentInpaintTTM": {"class": LatentInpaintTTM, "name": "Latent Inpaint TTM"}, + "NABLA_AttentionKJ": {"class": NABLA_AttentionKJ, "name": "NABLA Attention KJ"}, + "TorchCompileModelAdvanced": {"class": TorchCompileModelAdvanced, "name": "TorchCompileModelAdvanced"}, + "StartRecordCUDAMemoryHistory": {"class": StartRecordCUDAMemoryHistory, "name": "Start Recording CUDAMemory History"}, + "EndRecordCUDAMemoryHistory": {"class": EndRecordCUDAMemoryHistory, "name": "End Recording CUDAMemory History"}, + "VisualizeCUDAMemoryHistory": {"class": VisualizeCUDAMemoryHistory, "name": "Visualize CUDAMemory History"}, + "PreviewLatentNoiseMask": {"class": PreviewLatentNoiseMask, "name": "Preview Latent Noise Mask"}, + "ModelMemoryUseReportPatch": {"class": ModelMemoryUseReportPatch, "name": "Model Memory Use Report Patch"}, + "ModelMemoryUsageFactorOverride": {"class": ModelMemoryUsageFactorOverride, "name": "Model Memory Usage Factor Override"}, + "WanChunkFeedForward": {"class": WanChunkFeedForward, "name": "Wan ChunkFeedForward"}, + "Ideogram4OptimizationsKJ": {"class": Ideogram4OptimizationsKJ, "name": "Ideogram4 Optimizations KJ"}, + "SamplerSelfRefineVideo": {"class": SamplerSelfRefineVideo, "name": "Sampler SelfRefineVideo"}, + "PlaySoundKJ": {"class": PlaySoundKJ, "name": "Play Sound KJ"}, + + #instance diffusion + "CreateInstanceDiffusionTracking": {"class": CreateInstanceDiffusionTracking}, + "AppendInstanceDiffusionTracking": {"class": AppendInstanceDiffusionTracking}, + "DrawInstanceDiffusionTracking": {"class": DrawInstanceDiffusionTracking}, + + #lora + "LoraExtractKJ": {"class": LoraExtractKJ, "name": "LoraExtractKJ"}, + "LoraReduceRankKJ": {"class": LoraReduceRank, "name": "LoraReduceRank"}, + + #tracks + "GetTrackRange": {"class": GetTrackRange, "name": "Get Track Range"}, + "AddNoiseToTrackPath": {"class": AddNoiseToTrackPath, "name": "Add Noise To Track"}, + + #context windows + "ContextWindowsVisualizerKJ": {"class": ContextWindowsVisualizerKJ, "name": "Context Windows Visualizer (KJ)"}, + # deprecated + "PatchModelPatcherOrder": {"class": PatchModelPatcherOrder, "name": "Patch Model Patcher Order"}, + "TorchCompileModelFluxAdvanced": {"class": DeprecatedCompileNodeKJ, "name": "TorchCompileModelFluxAdvanced"}, + "TorchCompileLTXModel": {"class": DeprecatedCompileNodeKJ, "name": "TorchCompileLTXModel"}, + "TorchCompileCosmosModel": {"class": DeprecatedCompileNodeKJ, "name": "TorchCompileCosmosModel"}, + "TorchCompileModelHyVideo": {"class": DeprecatedCompileNodeKJ, "name": "TorchCompileModelHyVideo"}, + "TorchCompileModelQwenImage": {"class": DeprecatedCompileNodeKJ, "name": "TorchCompileModelQwenImage"}, + "TorchCompileModelWanVideo": {"class": DeprecatedCompileNodeKJ, "name": "TorchCompileModelWanVideo"}, +} + +#ltxv +try: + from .nodes.ltxv_nodes import ( + LTXVAddGuideMulti, LTXVAddGuidesFromBatch, LTXVAudioVideoMask, LTX2_NAG, + LTXVChunkFeedForward, LTX2SamplingPreviewOverride, + LTX2AudioLatentNormalizingSampling, LTXVImgToVideoInplaceKJ, + LTX2AttentionTunerPatch, LTX2MemoryEfficientSageAttentionPatch, + LTX2LoraLoaderAdvanced, WanVideoMemoryEfficientSageAttentionPatch, + MiniMaxH3MemoryEfficientSageAttentionPatch, + ) + NODE_CONFIG.update({ + "LTXVEnhanceAVideoKJ": {"class": LTXVEnhanceAVideoKJ, "name": "LTXV Enhance A Video KJ"}, + "LTXVAddGuideMulti": {"class": LTXVAddGuideMulti, "name": "LTXV Add Guide Multi"}, + "LTXVAddGuidesFromBatch": {"class": LTXVAddGuidesFromBatch, "name": "LTXV Add Guides From Batch"}, + "LTXVAudioVideoMask": {"class": LTXVAudioVideoMask, "name": "LTXV Audio Video Mask"}, + "LTX2_NAG": {"class": LTX2_NAG, "name": "LTX2 NAG"}, + "LTXVChunkFeedForward": {"class": LTXVChunkFeedForward, "name": "LTXV Chunk Feed Forward"}, + "LTX2SamplingPreviewOverride": {"class": LTX2SamplingPreviewOverride, "name": "LTX2 Sampling Preview Override"}, + "LTX2AudioLatentNormalizingSampling": {"class": LTX2AudioLatentNormalizingSampling, "name": "LTX2 Audio Latent Normalizing Sampling"}, + "LTXVImgToVideoInplaceKJ": {"class": LTXVImgToVideoInplaceKJ, "name": "LTXV Img To Video Inplace KJ"}, + "LTX2AttentionTunerPatch": {"class": LTX2AttentionTunerPatch, "name": "LTX2 Attention Tuner Patch"}, + "LTX2MemoryEfficientSageAttentionPatch": {"class": LTX2MemoryEfficientSageAttentionPatch, "name": "LTX2 Memory Efficient Sage Attention Patch"}, + "LTX2LoraLoaderAdvanced": {"class": LTX2LoraLoaderAdvanced, "name": "LTX2 Lora Loader Advanced"}, + "WanVideoMemoryEfficientSageAttentionPatch": {"class": WanVideoMemoryEfficientSageAttentionPatch, "name": "WanVideo Memory Efficient Sage Attention Patch"}, + "MiniMaxH3MemoryEfficientSageAttentionPatch": {"class": MiniMaxH3MemoryEfficientSageAttentionPatch, "name": "MiniMax H3 Memory Efficient Sage Attention Patch"}, + }) +except Exception as e: + logging.warning(f"KJNodes: LTXV nodes could not be imported. LTXV nodes will be unavailable. Error: {e}", exc_info=True) + +try: + from .nodes.triton_vae import PatchTritonVAE + NODE_CONFIG.update({ + "PatchTritonVAE": {"class": PatchTritonVAE, "name": "Patch Triton VAE"}, + }) +except Exception: + logging.warning("KJNodes: PatchTritonVAE node could not be imported. PatchTritonVAE will be unavailable.", exc_info=True) + +def generate_node_mappings(node_config): + node_class_mappings = {} + node_display_name_mappings = {} + + for node_name, node_info in node_config.items(): + node_class_mappings[node_name] = node_info["class"] + node_display_name_mappings[node_name] = node_info.get("name", node_info["class"].__name__) + + return node_class_mappings, node_display_name_mappings + +NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS = generate_node_mappings(NODE_CONFIG) + +__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"] + +WEB_DIRECTORY = "./web" + +from aiohttp import web +from server import PromptServer +from pathlib import Path + +if hasattr(PromptServer, "instance"): + # NOTE: we add an extra static path to avoid comfy mechanism that loads every script in web. + if not PromptServer.instance.app.router.frozen: + try: + PromptServer.instance.app.add_routes( + [web.static("/kjweb_async", (Path(__file__).parent.absolute() / "kjweb_async").as_posix())] + ) + except Exception: + logging.exception("KJNodes: failed to register /kjweb_async static route") diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/__pycache__/__init__.cpython-312.pyc b/ComfyUI/custom_nodes/ComfyUI-KJNodes/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cdfad667ffa190699a529d9e4c1bb3a6cc4f3e07 Binary files /dev/null and b/ComfyUI/custom_nodes/ComfyUI-KJNodes/__pycache__/__init__.cpython-312.pyc differ diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/custom_dimensions_example.json b/ComfyUI/custom_nodes/ComfyUI-KJNodes/custom_dimensions_example.json new file mode 100644 index 0000000000000000000000000000000000000000..5c4814d377d44916a14b4d4a83b7cba72ae2958b --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-KJNodes/custom_dimensions_example.json @@ -0,0 +1,22 @@ +[ + { + "label": "SD", + "value": "512x512" + }, + { + "label": "HD", + "value": "768x768" + }, + { + "label": "Full HD", + "value": "1024x1024" + }, + { + "label": "4k", + "value": "2048x2048" + }, + { + "label": "SVD", + "value": "1024x576" + } +] diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/docs/images/2024-04-03_20_49_29-ComfyUI.png b/ComfyUI/custom_nodes/ComfyUI-KJNodes/docs/images/2024-04-03_20_49_29-ComfyUI.png new file mode 100644 index 0000000000000000000000000000000000000000..b42cfe56374c6db142326761a2a3d96211519664 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-KJNodes/docs/images/2024-04-03_20_49_29-ComfyUI.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:85805d3c7ca8f5d281886ea0ad61f9a78edad755ef8014b3870f91b871807ac9 +size 176158 diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/docs/images/319121566-05f66385-7568-4b1f-8bbc-11053660b02f.png b/ComfyUI/custom_nodes/ComfyUI-KJNodes/docs/images/319121566-05f66385-7568-4b1f-8bbc-11053660b02f.png new file mode 100644 index 0000000000000000000000000000000000000000..e749239c1c4ffd5ab29b51695dd8d8b51ed3597f Binary files /dev/null and b/ComfyUI/custom_nodes/ComfyUI-KJNodes/docs/images/319121566-05f66385-7568-4b1f-8bbc-11053660b02f.png differ diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/docs/images/319121636-706b5081-9120-4a29-bd76-901691ada688.png b/ComfyUI/custom_nodes/ComfyUI-KJNodes/docs/images/319121636-706b5081-9120-4a29-bd76-901691ada688.png new file mode 100644 index 0000000000000000000000000000000000000000..b53ad666ff060d87971f3962e74101f0cb2a5c3f Binary files /dev/null and b/ComfyUI/custom_nodes/ComfyUI-KJNodes/docs/images/319121636-706b5081-9120-4a29-bd76-901691ada688.png differ diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/example_workflows/leapfusion_hunyuuanvideo_i2v_native_testing.json b/ComfyUI/custom_nodes/ComfyUI-KJNodes/example_workflows/leapfusion_hunyuuanvideo_i2v_native_testing.json new file mode 100644 index 0000000000000000000000000000000000000000..134a83788815d04b5574a807db67eb6e45bf9263 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-KJNodes/example_workflows/leapfusion_hunyuuanvideo_i2v_native_testing.json @@ -0,0 +1,1188 @@ +{ + "last_node_id": 86, + "last_link_id": 144, + "nodes": [ + { + "id": 62, + "type": "FluxGuidance", + "pos": [ + -630, + -170 + ], + "size": [ + 317.4000244140625, + 58 + ], + "flags": {}, + "order": 13, + "mode": 0, + "inputs": [ + { + "name": "conditioning", + "type": "CONDITIONING", + "link": 82 + } + ], + "outputs": [ + { + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 83 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "FluxGuidance" + }, + "widgets_values": [ + 6 + ] + }, + { + "id": 51, + "type": "KSamplerSelect", + "pos": [ + -610, + -480 + ], + "size": [ + 315, + 58 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "SAMPLER", + "type": "SAMPLER", + "links": [ + 61 + ] + } + ], + "properties": { + "Node name for S&R": "KSamplerSelect" + }, + "widgets_values": [ + "euler" + ] + }, + { + "id": 57, + "type": "VAEDecodeTiled", + "pos": [ + -200, + 90 + ], + "size": [ + 315, + 150 + ], + "flags": {}, + "order": 20, + "mode": 0, + "inputs": [ + { + "name": "samples", + "type": "LATENT", + "link": 142 + }, + { + "name": "vae", + "type": "VAE", + "link": 74 + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 105 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "VAEDecodeTiled" + }, + "widgets_values": [ + 128, + 64, + 64, + 8 + ] + }, + { + "id": 65, + "type": "LoadImage", + "pos": [ + -2212.498779296875, + -632.4085083007812 + ], + "size": [ + 315, + 314 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 86 + ], + "slot_index": 0 + }, + { + "name": "MASK", + "type": "MASK", + "links": null + } + ], + "properties": { + "Node name for S&R": "LoadImage" + }, + "widgets_values": [ + "Mona-Lisa-oil-wood-panel-Leonardo-da.webp", + "image" + ] + }, + { + "id": 64, + "type": "VAEEncode", + "pos": [ + -1336.7884521484375, + -492.5806884765625 + ], + "size": [ + 210, + 46 + ], + "flags": {}, + "order": 14, + "mode": 0, + "inputs": [ + { + "name": "pixels", + "type": "IMAGE", + "link": 144 + }, + { + "name": "vae", + "type": "VAE", + "link": 88 + } + ], + "outputs": [ + { + "name": "LATENT", + "type": "LATENT", + "links": [ + 137 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "VAEEncode" + }, + "widgets_values": [] + }, + { + "id": 44, + "type": "UNETLoader", + "pos": [ + -2373.55029296875, + -193.91510009765625 + ], + "size": [ + 459.56060791015625, + 82 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "MODEL", + "type": "MODEL", + "links": [ + 135 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "UNETLoader" + }, + "widgets_values": [ + "hyvideo\\hunyuan_video_720_fp8_e4m3fn.safetensors", + "fp8_e4m3fn_fast" + ] + }, + { + "id": 49, + "type": "VAELoader", + "pos": [ + -1876.39306640625, + -35.19633865356445 + ], + "size": [ + 433.7603454589844, + 58.71116256713867 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "VAE", + "type": "VAE", + "links": [ + 74, + 88 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "VAELoader" + }, + "widgets_values": [ + "hyvid\\hunyuan_video_vae_bf16.safetensors" + ] + }, + { + "id": 47, + "type": "DualCLIPLoader", + "pos": [ + -2284.893798828125, + 150.4042205810547 + ], + "size": [ + 343.3958435058594, + 106.86042785644531 + ], + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "CLIP", + "type": "CLIP", + "links": [ + 56 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "DualCLIPLoader" + }, + "widgets_values": [ + "clip_l.safetensors", + "llava_llama3_fp16.safetensors", + "hunyuan_video", + "default" + ] + }, + { + "id": 45, + "type": "CLIPTextEncode", + "pos": [ + -1839.1649169921875, + 143.5203094482422 + ], + "size": [ + 400, + 200 + ], + "flags": {}, + "order": 8, + "mode": 0, + "inputs": [ + { + "name": "clip", + "type": "CLIP", + "link": 56 + } + ], + "outputs": [ + { + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 69, + 82 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "CLIPTextEncode" + }, + "widgets_values": [ + "woman puts on sunglasses" + ] + }, + { + "id": 53, + "type": "EmptyHunyuanLatentVideo", + "pos": [ + -1120, + 90 + ], + "size": [ + 315, + 130 + ], + "flags": {}, + "order": 10, + "mode": 0, + "inputs": [ + { + "name": "width", + "type": "INT", + "link": 89, + "widget": { + "name": "width" + } + }, + { + "name": "height", + "type": "INT", + "link": 90, + "widget": { + "name": "height" + } + } + ], + "outputs": [ + { + "name": "LATENT", + "type": "LATENT", + "links": [ + 119 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "EmptyHunyuanLatentVideo" + }, + "widgets_values": [ + 960, + 544, + 65, + 1 + ] + }, + { + "id": 55, + "type": "ConditioningZeroOut", + "pos": [ + -910, + 300 + ], + "size": [ + 251.14309692382812, + 26 + ], + "flags": { + "collapsed": true + }, + "order": 12, + "mode": 0, + "inputs": [ + { + "name": "conditioning", + "type": "CONDITIONING", + "link": 69 + } + ], + "outputs": [ + { + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 70 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "ConditioningZeroOut" + }, + "widgets_values": [] + }, + { + "id": 52, + "type": "BasicScheduler", + "pos": [ + -600, + -350 + ], + "size": [ + 315, + 106 + ], + "flags": {}, + "order": 17, + "mode": 0, + "inputs": [ + { + "name": "model", + "type": "MODEL", + "link": 78 + } + ], + "outputs": [ + { + "name": "SIGMAS", + "type": "SIGMAS", + "links": [ + 62 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "BasicScheduler" + }, + "widgets_values": [ + "simple", + 20, + 1 + ] + }, + { + "id": 42, + "type": "SamplerCustom", + "pos": [ + -640, + 10 + ], + "size": [ + 355.20001220703125, + 467.4666748046875 + ], + "flags": {}, + "order": 18, + "mode": 0, + "inputs": [ + { + "name": "model", + "type": "MODEL", + "link": 77 + }, + { + "name": "positive", + "type": "CONDITIONING", + "link": 83 + }, + { + "name": "negative", + "type": "CONDITIONING", + "link": 70 + }, + { + "name": "sampler", + "type": "SAMPLER", + "link": 61 + }, + { + "name": "sigmas", + "type": "SIGMAS", + "link": 62 + }, + { + "name": "latent_image", + "type": "LATENT", + "link": 119 + } + ], + "outputs": [ + { + "name": "output", + "type": "LATENT", + "links": null + }, + { + "name": "denoised_output", + "type": "LATENT", + "links": [ + 141 + ], + "slot_index": 1 + } + ], + "properties": { + "Node name for S&R": "SamplerCustom" + }, + "widgets_values": [ + true, + 6, + "fixed", + 1, + null + ] + }, + { + "id": 84, + "type": "GetLatentRangeFromBatch", + "pos": [ + -240, + -100 + ], + "size": [ + 340.20001220703125, + 82 + ], + "flags": {}, + "order": 19, + "mode": 0, + "inputs": [ + { + "name": "latents", + "type": "LATENT", + "link": 141 + } + ], + "outputs": [ + { + "name": "LATENT", + "type": "LATENT", + "links": [ + 142 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "GetLatentRangeFromBatch" + }, + "widgets_values": [ + 1, + -1 + ] + }, + { + "id": 50, + "type": "VHS_VideoCombine", + "pos": [ + 165.77645874023438, + -619.0606079101562 + ], + "size": [ + 1112.6898193359375, + 1076.4598388671875 + ], + "flags": {}, + "order": 21, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 105 + }, + { + "name": "audio", + "type": "AUDIO", + "link": null, + "shape": 7 + }, + { + "name": "meta_batch", + "type": "VHS_BatchManager", + "link": null, + "shape": 7 + }, + { + "name": "vae", + "type": "VAE", + "link": null, + "shape": 7 + } + ], + "outputs": [ + { + "name": "Filenames", + "type": "VHS_FILENAMES", + "links": null + } + ], + "properties": { + "Node name for S&R": "VHS_VideoCombine" + }, + "widgets_values": { + "frame_rate": 24, + "loop_count": 0, + "filename_prefix": "hyvidcomfy", + "format": "video/h264-mp4", + "pix_fmt": "yuv420p", + "crf": 19, + "save_metadata": true, + "trim_to_audio": false, + "pingpong": false, + "save_output": false, + "videopreview": { + "hidden": false, + "paused": false, + "params": { + "filename": "hyvidcomfy_00001.mp4", + "subfolder": "", + "type": "temp", + "format": "video/h264-mp4", + "frame_rate": 24, + "workflow": "hyvidcomfy_00001.png", + "fullpath": "N:\\AI\\ComfyUI\\temp\\hyvidcomfy_00001.mp4" + }, + "muted": false + } + } + }, + { + "id": 54, + "type": "ModelSamplingSD3", + "pos": [ + -1079.9112548828125, + -146.69448852539062 + ], + "size": [ + 315, + 58 + ], + "flags": {}, + "order": 16, + "mode": 0, + "inputs": [ + { + "name": "model", + "type": "MODEL", + "link": 117 + } + ], + "outputs": [ + { + "name": "MODEL", + "type": "MODEL", + "links": [ + 77, + 78 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "ModelSamplingSD3" + }, + "widgets_values": [ + 9 + ] + }, + { + "id": 80, + "type": "PathchSageAttentionKJ", + "pos": [ + -2273.926513671875, + -36.720542907714844 + ], + "size": [ + 315, + 58 + ], + "flags": {}, + "order": 7, + "mode": 4, + "inputs": [ + { + "name": "model", + "type": "MODEL", + "link": 135 + } + ], + "outputs": [ + { + "name": "MODEL", + "type": "MODEL", + "links": [ + 136 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "PathchSageAttentionKJ" + }, + "widgets_values": [ + "auto" + ] + }, + { + "id": 85, + "type": "Note", + "pos": [ + -1838.572265625, + -302.1575927734375 + ], + "size": [ + 408.4594421386719, + 58 + ], + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [], + "outputs": [], + "properties": {}, + "widgets_values": [ + "https://huggingface.co/Kijai/Leapfusion-image2vid-comfy/blob/main/leapfusion_img2vid544p_comfy.safetensors" + ], + "color": "#432", + "bgcolor": "#653" + }, + { + "id": 74, + "type": "LeapfusionHunyuanI2VPatcher", + "pos": [ + -1059.552978515625, + -459.34674072265625 + ], + "size": [ + 277.3238525390625, + 150 + ], + "flags": {}, + "order": 15, + "mode": 0, + "inputs": [ + { + "name": "model", + "type": "MODEL", + "link": 123 + }, + { + "name": "latent", + "type": "LATENT", + "link": 137 + } + ], + "outputs": [ + { + "name": "MODEL", + "type": "MODEL", + "links": [ + 117 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "LeapfusionHunyuanI2VPatcher" + }, + "widgets_values": [ + 0, + 0, + 1, + 0.8 + ] + }, + { + "id": 59, + "type": "LoraLoaderModelOnly", + "pos": [ + -1870.3748779296875, + -194.6091766357422 + ], + "size": [ + 442.8438720703125, + 82 + ], + "flags": {}, + "order": 11, + "mode": 0, + "inputs": [ + { + "name": "model", + "type": "MODEL", + "link": 136 + } + ], + "outputs": [ + { + "name": "MODEL", + "type": "MODEL", + "links": [ + 123 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "LoraLoaderModelOnly" + }, + "widgets_values": [ + "hyvid\\musubi-tuner\\img2vid544p.safetensors", + 1 + ] + }, + { + "id": 66, + "type": "ImageResizeKJ", + "pos": [ + -1821.1531982421875, + -632.925048828125 + ], + "size": [ + 315, + 266 + ], + "flags": {}, + "order": 6, + "mode": 0, + "inputs": [ + { + "name": "image", + "type": "IMAGE", + "link": 86 + }, + { + "name": "get_image_size", + "type": "IMAGE", + "link": null, + "shape": 7 + }, + { + "name": "width_input", + "type": "INT", + "link": null, + "widget": { + "name": "width_input" + }, + "shape": 7 + }, + { + "name": "height_input", + "type": "INT", + "link": null, + "widget": { + "name": "height_input" + }, + "shape": 7 + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 143 + ], + "slot_index": 0 + }, + { + "name": "width", + "type": "INT", + "links": [ + 89 + ], + "slot_index": 1 + }, + { + "name": "height", + "type": "INT", + "links": [ + 90 + ], + "slot_index": 2 + } + ], + "properties": { + "Node name for S&R": "ImageResizeKJ" + }, + "widgets_values": [ + 960, + 640, + "lanczos", + false, + 2, + 0, + 0, + "center" + ] + }, + { + "id": 86, + "type": "ImageNoiseAugmentation", + "pos": [ + -1361.111572265625, + -667.0104370117188 + ], + "size": [ + 315, + 106 + ], + "flags": {}, + "order": 9, + "mode": 0, + "inputs": [ + { + "name": "image", + "type": "IMAGE", + "link": 143 + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 144 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "ImageNoiseAugmentation" + }, + "widgets_values": [ + 0.05, + 123, + "fixed" + ] + } + ], + "links": [ + [ + 56, + 47, + 0, + 45, + 0, + "CLIP" + ], + [ + 61, + 51, + 0, + 42, + 3, + "SAMPLER" + ], + [ + 62, + 52, + 0, + 42, + 4, + "SIGMAS" + ], + [ + 69, + 45, + 0, + 55, + 0, + "CONDITIONING" + ], + [ + 70, + 55, + 0, + 42, + 2, + "CONDITIONING" + ], + [ + 74, + 49, + 0, + 57, + 1, + "VAE" + ], + [ + 77, + 54, + 0, + 42, + 0, + "MODEL" + ], + [ + 78, + 54, + 0, + 52, + 0, + "MODEL" + ], + [ + 82, + 45, + 0, + 62, + 0, + "CONDITIONING" + ], + [ + 83, + 62, + 0, + 42, + 1, + "CONDITIONING" + ], + [ + 86, + 65, + 0, + 66, + 0, + "IMAGE" + ], + [ + 88, + 49, + 0, + 64, + 1, + "VAE" + ], + [ + 89, + 66, + 1, + 53, + 0, + "INT" + ], + [ + 90, + 66, + 2, + 53, + 1, + "INT" + ], + [ + 105, + 57, + 0, + 50, + 0, + "IMAGE" + ], + [ + 117, + 74, + 0, + 54, + 0, + "MODEL" + ], + [ + 119, + 53, + 0, + 42, + 5, + "LATENT" + ], + [ + 123, + 59, + 0, + 74, + 0, + "MODEL" + ], + [ + 135, + 44, + 0, + 80, + 0, + "MODEL" + ], + [ + 136, + 80, + 0, + 59, + 0, + "MODEL" + ], + [ + 137, + 64, + 0, + 74, + 1, + "LATENT" + ], + [ + 141, + 42, + 1, + 84, + 0, + "LATENT" + ], + [ + 142, + 84, + 0, + 57, + 0, + "LATENT" + ], + [ + 143, + 66, + 0, + 86, + 0, + "IMAGE" + ], + [ + 144, + 86, + 0, + 64, + 0, + "IMAGE" + ] + ], + "groups": [], + "config": {}, + "extra": { + "ds": { + "scale": 0.740024994425854, + "offset": [ + 2525.036093151529, + 802.59123935694 + ] + }, + "node_versions": { + "comfy-core": "0.3.13", + "ComfyUI-KJNodes": "a8aeef670b3f288303f956bf94385cb87978ea93", + "ComfyUI-VideoHelperSuite": "c47b10ca1798b4925ff5a5f07d80c51ca80a837d" + }, + "VHS_latentpreview": true, + "VHS_latentpreviewrate": 0 + }, + "version": 0.4 +} \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/fonts/FreeMono.ttf b/ComfyUI/custom_nodes/ComfyUI-KJNodes/fonts/FreeMono.ttf new file mode 100644 index 0000000000000000000000000000000000000000..1e35d08261b9a61f87b6f1e7393d5c1221828ed1 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-KJNodes/fonts/FreeMono.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a7c692ad545c308b7b8fc2db770760c4a5d15ca50f12addf58c8f5360370e831 +size 343980 diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/fonts/FreeMonoBoldOblique.otf b/ComfyUI/custom_nodes/ComfyUI-KJNodes/fonts/FreeMonoBoldOblique.otf new file mode 100644 index 0000000000000000000000000000000000000000..3aafa30ff9d9f31df006993f3c3d5c2eb5953f4f --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-KJNodes/fonts/FreeMonoBoldOblique.otf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:96187651ee033d0d9791dc2beeebfba5d1f070ab410fce1a5c16483ca249c588 +size 237600 diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/fonts/TTNorms-Black.otf b/ComfyUI/custom_nodes/ComfyUI-KJNodes/fonts/TTNorms-Black.otf new file mode 100644 index 0000000000000000000000000000000000000000..2cd91a4fc9637af834c5e2793ab6487c5067063d --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-KJNodes/fonts/TTNorms-Black.otf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:710977e683bf0db6416d6d41b427e0363c914e6c503a5291fcb330f30b8448ea +size 152736 diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/kjweb_async/marked.min.js b/ComfyUI/custom_nodes/ComfyUI-KJNodes/kjweb_async/marked.min.js new file mode 100644 index 0000000000000000000000000000000000000000..2e66c369c388c135cc68d399861a737f4c5e68cd --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-KJNodes/kjweb_async/marked.min.js @@ -0,0 +1,6 @@ +/** + * marked v12.0.1 - a markdown parser + * Copyright (c) 2011-2024, Christopher Jeffrey. (MIT Licensed) + * https://github.com/markedjs/marked + */ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).marked={})}(this,(function(e){"use strict";function t(){return{async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null}}function n(t){e.defaults=t}e.defaults={async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null};const s=/[&<>"']/,r=new RegExp(s.source,"g"),i=/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/,l=new RegExp(i.source,"g"),o={"&":"&","<":"<",">":">",'"':""","'":"'"},a=e=>o[e];function c(e,t){if(t){if(s.test(e))return e.replace(r,a)}else if(i.test(e))return e.replace(l,a);return e}const h=/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/gi;function p(e){return e.replace(h,((e,t)=>"colon"===(t=t.toLowerCase())?":":"#"===t.charAt(0)?"x"===t.charAt(1)?String.fromCharCode(parseInt(t.substring(2),16)):String.fromCharCode(+t.substring(1)):""))}const u=/(^|[^\[])\^/g;function k(e,t){let n="string"==typeof e?e:e.source;t=t||"";const s={replace:(e,t)=>{let r="string"==typeof t?t:t.source;return r=r.replace(u,"$1"),n=n.replace(e,r),s},getRegex:()=>new RegExp(n,t)};return s}function g(e){try{e=encodeURI(e).replace(/%25/g,"%")}catch(e){return null}return e}const f={exec:()=>null};function d(e,t){const n=e.replace(/\|/g,((e,t,n)=>{let s=!1,r=t;for(;--r>=0&&"\\"===n[r];)s=!s;return s?"|":" |"})).split(/ \|/);let s=0;if(n[0].trim()||n.shift(),n.length>0&&!n[n.length-1].trim()&&n.pop(),t)if(n.length>t)n.splice(t);else for(;n.length0)return{type:"space",raw:t[0]}}code(e){const t=this.rules.block.code.exec(e);if(t){const e=t[0].replace(/^ {1,4}/gm,"");return{type:"code",raw:t[0],codeBlockStyle:"indented",text:this.options.pedantic?e:x(e,"\n")}}}fences(e){const t=this.rules.block.fences.exec(e);if(t){const e=t[0],n=function(e,t){const n=e.match(/^(\s+)(?:```)/);if(null===n)return t;const s=n[1];return t.split("\n").map((e=>{const t=e.match(/^\s+/);if(null===t)return e;const[n]=t;return n.length>=s.length?e.slice(s.length):e})).join("\n")}(e,t[3]||"");return{type:"code",raw:e,lang:t[2]?t[2].trim().replace(this.rules.inline.anyPunctuation,"$1"):t[2],text:n}}}heading(e){const t=this.rules.block.heading.exec(e);if(t){let e=t[2].trim();if(/#$/.test(e)){const t=x(e,"#");this.options.pedantic?e=t.trim():t&&!/ $/.test(t)||(e=t.trim())}return{type:"heading",raw:t[0],depth:t[1].length,text:e,tokens:this.lexer.inline(e)}}}hr(e){const t=this.rules.block.hr.exec(e);if(t)return{type:"hr",raw:t[0]}}blockquote(e){const t=this.rules.block.blockquote.exec(e);if(t){const e=x(t[0].replace(/^ *>[ \t]?/gm,""),"\n"),n=this.lexer.state.top;this.lexer.state.top=!0;const s=this.lexer.blockTokens(e);return this.lexer.state.top=n,{type:"blockquote",raw:t[0],tokens:s,text:e}}}list(e){let t=this.rules.block.list.exec(e);if(t){let n=t[1].trim();const s=n.length>1,r={type:"list",raw:"",ordered:s,start:s?+n.slice(0,-1):"",loose:!1,items:[]};n=s?`\\d{1,9}\\${n.slice(-1)}`:`\\${n}`,this.options.pedantic&&(n=s?n:"[*+-]");const i=new RegExp(`^( {0,3}${n})((?:[\t ][^\\n]*)?(?:\\n|$))`);let l="",o="",a=!1;for(;e;){let n=!1;if(!(t=i.exec(e)))break;if(this.rules.block.hr.test(e))break;l=t[0],e=e.substring(l.length);let s=t[2].split("\n",1)[0].replace(/^\t+/,(e=>" ".repeat(3*e.length))),c=e.split("\n",1)[0],h=0;this.options.pedantic?(h=2,o=s.trimStart()):(h=t[2].search(/[^ ]/),h=h>4?1:h,o=s.slice(h),h+=t[1].length);let p=!1;if(!s&&/^ *$/.test(c)&&(l+=c+"\n",e=e.substring(c.length+1),n=!0),!n){const t=new RegExp(`^ {0,${Math.min(3,h-1)}}(?:[*+-]|\\d{1,9}[.)])((?:[ \t][^\\n]*)?(?:\\n|$))`),n=new RegExp(`^ {0,${Math.min(3,h-1)}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`),r=new RegExp(`^ {0,${Math.min(3,h-1)}}(?:\`\`\`|~~~)`),i=new RegExp(`^ {0,${Math.min(3,h-1)}}#`);for(;e;){const a=e.split("\n",1)[0];if(c=a,this.options.pedantic&&(c=c.replace(/^ {1,4}(?=( {4})*[^ ])/g," ")),r.test(c))break;if(i.test(c))break;if(t.test(c))break;if(n.test(e))break;if(c.search(/[^ ]/)>=h||!c.trim())o+="\n"+c.slice(h);else{if(p)break;if(s.search(/[^ ]/)>=4)break;if(r.test(s))break;if(i.test(s))break;if(n.test(s))break;o+="\n"+c}p||c.trim()||(p=!0),l+=a+"\n",e=e.substring(a.length+1),s=c.slice(h)}}r.loose||(a?r.loose=!0:/\n *\n *$/.test(l)&&(a=!0));let u,k=null;this.options.gfm&&(k=/^\[[ xX]\] /.exec(o),k&&(u="[ ] "!==k[0],o=o.replace(/^\[[ xX]\] +/,""))),r.items.push({type:"list_item",raw:l,task:!!k,checked:u,loose:!1,text:o,tokens:[]}),r.raw+=l}r.items[r.items.length-1].raw=l.trimEnd(),r.items[r.items.length-1].text=o.trimEnd(),r.raw=r.raw.trimEnd();for(let e=0;e"space"===e.type)),n=t.length>0&&t.some((e=>/\n.*\n/.test(e.raw)));r.loose=n}if(r.loose)for(let e=0;e$/,"$1").replace(this.rules.inline.anyPunctuation,"$1"):"",s=t[3]?t[3].substring(1,t[3].length-1).replace(this.rules.inline.anyPunctuation,"$1"):t[3];return{type:"def",tag:e,raw:t[0],href:n,title:s}}}table(e){const t=this.rules.block.table.exec(e);if(!t)return;if(!/[:|]/.test(t[2]))return;const n=d(t[1]),s=t[2].replace(/^\||\| *$/g,"").split("|"),r=t[3]&&t[3].trim()?t[3].replace(/\n[ \t]*$/,"").split("\n"):[],i={type:"table",raw:t[0],header:[],align:[],rows:[]};if(n.length===s.length){for(const e of s)/^ *-+: *$/.test(e)?i.align.push("right"):/^ *:-+: *$/.test(e)?i.align.push("center"):/^ *:-+ *$/.test(e)?i.align.push("left"):i.align.push(null);for(const e of n)i.header.push({text:e,tokens:this.lexer.inline(e)});for(const e of r)i.rows.push(d(e,i.header.length).map((e=>({text:e,tokens:this.lexer.inline(e)}))));return i}}lheading(e){const t=this.rules.block.lheading.exec(e);if(t)return{type:"heading",raw:t[0],depth:"="===t[2].charAt(0)?1:2,text:t[1],tokens:this.lexer.inline(t[1])}}paragraph(e){const t=this.rules.block.paragraph.exec(e);if(t){const e="\n"===t[1].charAt(t[1].length-1)?t[1].slice(0,-1):t[1];return{type:"paragraph",raw:t[0],text:e,tokens:this.lexer.inline(e)}}}text(e){const t=this.rules.block.text.exec(e);if(t)return{type:"text",raw:t[0],text:t[0],tokens:this.lexer.inline(t[0])}}escape(e){const t=this.rules.inline.escape.exec(e);if(t)return{type:"escape",raw:t[0],text:c(t[1])}}tag(e){const t=this.rules.inline.tag.exec(e);if(t)return!this.lexer.state.inLink&&/^/i.test(t[0])&&(this.lexer.state.inLink=!1),!this.lexer.state.inRawBlock&&/^<(pre|code|kbd|script)(\s|>)/i.test(t[0])?this.lexer.state.inRawBlock=!0:this.lexer.state.inRawBlock&&/^<\/(pre|code|kbd|script)(\s|>)/i.test(t[0])&&(this.lexer.state.inRawBlock=!1),{type:"html",raw:t[0],inLink:this.lexer.state.inLink,inRawBlock:this.lexer.state.inRawBlock,block:!1,text:t[0]}}link(e){const t=this.rules.inline.link.exec(e);if(t){const e=t[2].trim();if(!this.options.pedantic&&/^$/.test(e))return;const t=x(e.slice(0,-1),"\\");if((e.length-t.length)%2==0)return}else{const e=function(e,t){if(-1===e.indexOf(t[1]))return-1;let n=0;for(let s=0;s-1){const n=(0===t[0].indexOf("!")?5:4)+t[1].length+e;t[2]=t[2].substring(0,e),t[0]=t[0].substring(0,n).trim(),t[3]=""}}let n=t[2],s="";if(this.options.pedantic){const e=/^([^'"]*[^\s])\s+(['"])(.*)\2/.exec(n);e&&(n=e[1],s=e[3])}else s=t[3]?t[3].slice(1,-1):"";return n=n.trim(),/^$/.test(e)?n.slice(1):n.slice(1,-1)),b(t,{href:n?n.replace(this.rules.inline.anyPunctuation,"$1"):n,title:s?s.replace(this.rules.inline.anyPunctuation,"$1"):s},t[0],this.lexer)}}reflink(e,t){let n;if((n=this.rules.inline.reflink.exec(e))||(n=this.rules.inline.nolink.exec(e))){const e=t[(n[2]||n[1]).replace(/\s+/g," ").toLowerCase()];if(!e){const e=n[0].charAt(0);return{type:"text",raw:e,text:e}}return b(n,e,n[0],this.lexer)}}emStrong(e,t,n=""){let s=this.rules.inline.emStrongLDelim.exec(e);if(!s)return;if(s[3]&&n.match(/[\p{L}\p{N}]/u))return;if(!(s[1]||s[2]||"")||!n||this.rules.inline.punctuation.exec(n)){const n=[...s[0]].length-1;let r,i,l=n,o=0;const a="*"===s[0][0]?this.rules.inline.emStrongRDelimAst:this.rules.inline.emStrongRDelimUnd;for(a.lastIndex=0,t=t.slice(-1*e.length+n);null!=(s=a.exec(t));){if(r=s[1]||s[2]||s[3]||s[4]||s[5]||s[6],!r)continue;if(i=[...r].length,s[3]||s[4]){l+=i;continue}if((s[5]||s[6])&&n%3&&!((n+i)%3)){o+=i;continue}if(l-=i,l>0)continue;i=Math.min(i,i+l+o);const t=[...s[0]][0].length,a=e.slice(0,n+s.index+t+i);if(Math.min(n,i)%2){const e=a.slice(1,-1);return{type:"em",raw:a,text:e,tokens:this.lexer.inlineTokens(e)}}const c=a.slice(2,-2);return{type:"strong",raw:a,text:c,tokens:this.lexer.inlineTokens(c)}}}}codespan(e){const t=this.rules.inline.code.exec(e);if(t){let e=t[2].replace(/\n/g," ");const n=/[^ ]/.test(e),s=/^ /.test(e)&&/ $/.test(e);return n&&s&&(e=e.substring(1,e.length-1)),e=c(e,!0),{type:"codespan",raw:t[0],text:e}}}br(e){const t=this.rules.inline.br.exec(e);if(t)return{type:"br",raw:t[0]}}del(e){const t=this.rules.inline.del.exec(e);if(t)return{type:"del",raw:t[0],text:t[2],tokens:this.lexer.inlineTokens(t[2])}}autolink(e){const t=this.rules.inline.autolink.exec(e);if(t){let e,n;return"@"===t[2]?(e=c(t[1]),n="mailto:"+e):(e=c(t[1]),n=e),{type:"link",raw:t[0],text:e,href:n,tokens:[{type:"text",raw:e,text:e}]}}}url(e){let t;if(t=this.rules.inline.url.exec(e)){let e,n;if("@"===t[2])e=c(t[0]),n="mailto:"+e;else{let s;do{s=t[0],t[0]=this.rules.inline._backpedal.exec(t[0])?.[0]??""}while(s!==t[0]);e=c(t[0]),n="www."===t[1]?"http://"+t[0]:t[0]}return{type:"link",raw:t[0],text:e,href:n,tokens:[{type:"text",raw:e,text:e}]}}}inlineText(e){const t=this.rules.inline.text.exec(e);if(t){let e;return e=this.lexer.state.inRawBlock?t[0]:c(t[0]),{type:"text",raw:t[0],text:e}}}}const m=/^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/,y=/(?:[*+-]|\d{1,9}[.)])/,$=k(/^(?!bull |blockCode|fences|blockquote|heading|html)((?:.|\n(?!\s*?\n|bull |blockCode|fences|blockquote|heading|html))+?)\n {0,3}(=+|-+) *(?:\n+|$)/).replace(/bull/g,y).replace(/blockCode/g,/ {4}/).replace(/fences/g,/ {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g,/ {0,3}>/).replace(/heading/g,/ {0,3}#{1,6}/).replace(/html/g,/ {0,3}<[^\n>]+>\n/).getRegex(),z=/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/,T=/(?!\s*\])(?:\\.|[^\[\]\\])+/,R=k(/^ {0,3}\[(label)\]: *(?:\n *)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n *)?| *\n *)(title))? *(?:\n+|$)/).replace("label",T).replace("title",/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/).getRegex(),_=k(/^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/).replace(/bull/g,y).getRegex(),A="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",S=/|$))/,I=k("^ {0,3}(?:<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|\\n*|$)|\\n*|$)|)[\\s\\S]*?(?:(?:\\n *)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$)|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$))","i").replace("comment",S).replace("tag",A).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),E=k(z).replace("hr",m).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("|table","").replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",A).getRegex(),q={blockquote:k(/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/).replace("paragraph",E).getRegex(),code:/^( {4}[^\n]+(?:\n(?: *(?:\n|$))*)?)+/,def:R,fences:/^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/,heading:/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,hr:m,html:I,lheading:$,list:_,newline:/^(?: *(?:\n|$))+/,paragraph:E,table:f,text:/^[^\n]+/},Z=k("^ *([^\\n ].*)\\n {0,3}((?:\\| *)?:?-+:? *(?:\\| *:?-+:? *)*(?:\\| *)?)(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)").replace("hr",m).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("blockquote"," {0,3}>").replace("code"," {4}[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",A).getRegex(),L={...q,table:Z,paragraph:k(z).replace("hr",m).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("table",Z).replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",A).getRegex()},P={...q,html:k("^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))").replace("comment",S).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/,heading:/^(#{1,6})(.*)(?:\n+|$)/,fences:f,lheading:/^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/,paragraph:k(z).replace("hr",m).replace("heading"," *#{1,6} *[^\n]").replace("lheading",$).replace("|table","").replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").replace("|tag","").getRegex()},Q=/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,v=/^( {2,}|\\)\n(?!\s*$)/,B="\\p{P}\\p{S}",C=k(/^((?![*_])[\spunctuation])/,"u").replace(/punctuation/g,B).getRegex(),M=k(/^(?:\*+(?:((?!\*)[punct])|[^\s*]))|^_+(?:((?!_)[punct])|([^\s_]))/,"u").replace(/punct/g,B).getRegex(),O=k("^[^_*]*?__[^_*]*?\\*[^_*]*?(?=__)|[^*]+(?=[^*])|(?!\\*)[punct](\\*+)(?=[\\s]|$)|[^punct\\s](\\*+)(?!\\*)(?=[punct\\s]|$)|(?!\\*)[punct\\s](\\*+)(?=[^punct\\s])|[\\s](\\*+)(?!\\*)(?=[punct])|(?!\\*)[punct](\\*+)(?!\\*)(?=[punct])|[^punct\\s](\\*+)(?=[^punct\\s])","gu").replace(/punct/g,B).getRegex(),D=k("^[^_*]*?\\*\\*[^_*]*?_[^_*]*?(?=\\*\\*)|[^_]+(?=[^_])|(?!_)[punct](_+)(?=[\\s]|$)|[^punct\\s](_+)(?!_)(?=[punct\\s]|$)|(?!_)[punct\\s](_+)(?=[^punct\\s])|[\\s](_+)(?!_)(?=[punct])|(?!_)[punct](_+)(?!_)(?=[punct])","gu").replace(/punct/g,B).getRegex(),j=k(/\\([punct])/,"gu").replace(/punct/g,B).getRegex(),H=k(/^<(scheme:[^\s\x00-\x1f<>]*|email)>/).replace("scheme",/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/).replace("email",/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/).getRegex(),U=k(S).replace("(?:--\x3e|$)","--\x3e").getRegex(),X=k("^comment|^|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^|^").replace("comment",U).replace("attribute",/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/).getRegex(),F=/(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/,N=k(/^!?\[(label)\]\(\s*(href)(?:\s+(title))?\s*\)/).replace("label",F).replace("href",/<(?:\\.|[^\n<>\\])+>|[^\s\x00-\x1f]*/).replace("title",/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/).getRegex(),G=k(/^!?\[(label)\]\[(ref)\]/).replace("label",F).replace("ref",T).getRegex(),J=k(/^!?\[(ref)\](?:\[\])?/).replace("ref",T).getRegex(),K={_backpedal:f,anyPunctuation:j,autolink:H,blockSkip:/\[[^[\]]*?\]\([^\(\)]*?\)|`[^`]*?`|<[^<>]*?>/g,br:v,code:/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,del:f,emStrongLDelim:M,emStrongRDelimAst:O,emStrongRDelimUnd:D,escape:Q,link:N,nolink:J,punctuation:C,reflink:G,reflinkSearch:k("reflink|nolink(?!\\()","g").replace("reflink",G).replace("nolink",J).getRegex(),tag:X,text:/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\t+" ".repeat(n.length)));e;)if(!(this.options.extensions&&this.options.extensions.block&&this.options.extensions.block.some((s=>!!(n=s.call({lexer:this},e,t))&&(e=e.substring(n.raw.length),t.push(n),!0)))))if(n=this.tokenizer.space(e))e=e.substring(n.raw.length),1===n.raw.length&&t.length>0?t[t.length-1].raw+="\n":t.push(n);else if(n=this.tokenizer.code(e))e=e.substring(n.raw.length),s=t[t.length-1],!s||"paragraph"!==s.type&&"text"!==s.type?t.push(n):(s.raw+="\n"+n.raw,s.text+="\n"+n.text,this.inlineQueue[this.inlineQueue.length-1].src=s.text);else if(n=this.tokenizer.fences(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.heading(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.hr(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.blockquote(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.list(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.html(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.def(e))e=e.substring(n.raw.length),s=t[t.length-1],!s||"paragraph"!==s.type&&"text"!==s.type?this.tokens.links[n.tag]||(this.tokens.links[n.tag]={href:n.href,title:n.title}):(s.raw+="\n"+n.raw,s.text+="\n"+n.raw,this.inlineQueue[this.inlineQueue.length-1].src=s.text);else if(n=this.tokenizer.table(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.lheading(e))e=e.substring(n.raw.length),t.push(n);else{if(r=e,this.options.extensions&&this.options.extensions.startBlock){let t=1/0;const n=e.slice(1);let s;this.options.extensions.startBlock.forEach((e=>{s=e.call({lexer:this},n),"number"==typeof s&&s>=0&&(t=Math.min(t,s))})),t<1/0&&t>=0&&(r=e.substring(0,t+1))}if(this.state.top&&(n=this.tokenizer.paragraph(r)))s=t[t.length-1],i&&"paragraph"===s.type?(s.raw+="\n"+n.raw,s.text+="\n"+n.text,this.inlineQueue.pop(),this.inlineQueue[this.inlineQueue.length-1].src=s.text):t.push(n),i=r.length!==e.length,e=e.substring(n.raw.length);else if(n=this.tokenizer.text(e))e=e.substring(n.raw.length),s=t[t.length-1],s&&"text"===s.type?(s.raw+="\n"+n.raw,s.text+="\n"+n.text,this.inlineQueue.pop(),this.inlineQueue[this.inlineQueue.length-1].src=s.text):t.push(n);else if(e){const t="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(t);break}throw new Error(t)}}return this.state.top=!0,t}inline(e,t=[]){return this.inlineQueue.push({src:e,tokens:t}),t}inlineTokens(e,t=[]){let n,s,r,i,l,o,a=e;if(this.tokens.links){const e=Object.keys(this.tokens.links);if(e.length>0)for(;null!=(i=this.tokenizer.rules.inline.reflinkSearch.exec(a));)e.includes(i[0].slice(i[0].lastIndexOf("[")+1,-1))&&(a=a.slice(0,i.index)+"["+"a".repeat(i[0].length-2)+"]"+a.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex))}for(;null!=(i=this.tokenizer.rules.inline.blockSkip.exec(a));)a=a.slice(0,i.index)+"["+"a".repeat(i[0].length-2)+"]"+a.slice(this.tokenizer.rules.inline.blockSkip.lastIndex);for(;null!=(i=this.tokenizer.rules.inline.anyPunctuation.exec(a));)a=a.slice(0,i.index)+"++"+a.slice(this.tokenizer.rules.inline.anyPunctuation.lastIndex);for(;e;)if(l||(o=""),l=!1,!(this.options.extensions&&this.options.extensions.inline&&this.options.extensions.inline.some((s=>!!(n=s.call({lexer:this},e,t))&&(e=e.substring(n.raw.length),t.push(n),!0)))))if(n=this.tokenizer.escape(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.tag(e))e=e.substring(n.raw.length),s=t[t.length-1],s&&"text"===n.type&&"text"===s.type?(s.raw+=n.raw,s.text+=n.text):t.push(n);else if(n=this.tokenizer.link(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.reflink(e,this.tokens.links))e=e.substring(n.raw.length),s=t[t.length-1],s&&"text"===n.type&&"text"===s.type?(s.raw+=n.raw,s.text+=n.text):t.push(n);else if(n=this.tokenizer.emStrong(e,a,o))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.codespan(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.br(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.del(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.autolink(e))e=e.substring(n.raw.length),t.push(n);else if(this.state.inLink||!(n=this.tokenizer.url(e))){if(r=e,this.options.extensions&&this.options.extensions.startInline){let t=1/0;const n=e.slice(1);let s;this.options.extensions.startInline.forEach((e=>{s=e.call({lexer:this},n),"number"==typeof s&&s>=0&&(t=Math.min(t,s))})),t<1/0&&t>=0&&(r=e.substring(0,t+1))}if(n=this.tokenizer.inlineText(r))e=e.substring(n.raw.length),"_"!==n.raw.slice(-1)&&(o=n.raw.slice(-1)),l=!0,s=t[t.length-1],s&&"text"===s.type?(s.raw+=n.raw,s.text+=n.text):t.push(n);else if(e){const t="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(t);break}throw new Error(t)}}else e=e.substring(n.raw.length),t.push(n);return t}}class se{options;constructor(t){this.options=t||e.defaults}code(e,t,n){const s=(t||"").match(/^\S*/)?.[0];return e=e.replace(/\n$/,"")+"\n",s?'
'+(n?e:c(e,!0))+"
\n":"
"+(n?e:c(e,!0))+"
\n"}blockquote(e){return`
\n${e}
\n`}html(e,t){return e}heading(e,t,n){return`${e}\n`}hr(){return"
\n"}list(e,t,n){const s=t?"ol":"ul";return"<"+s+(t&&1!==n?' start="'+n+'"':"")+">\n"+e+"\n"}listitem(e,t,n){return`
  • ${e}
  • \n`}checkbox(e){return"'}paragraph(e){return`

    ${e}

    \n`}table(e,t){return t&&(t=`${t}`),"\n\n"+e+"\n"+t+"
    \n"}tablerow(e){return`\n${e}\n`}tablecell(e,t){const n=t.header?"th":"td";return(t.align?`<${n} align="${t.align}">`:`<${n}>`)+e+`\n`}strong(e){return`${e}`}em(e){return`${e}`}codespan(e){return`${e}`}br(){return"
    "}del(e){return`${e}`}link(e,t,n){const s=g(e);if(null===s)return n;let r='
    ",r}image(e,t,n){const s=g(e);if(null===s)return n;let r=`${n}0&&"paragraph"===n.tokens[0].type?(n.tokens[0].text=e+" "+n.tokens[0].text,n.tokens[0].tokens&&n.tokens[0].tokens.length>0&&"text"===n.tokens[0].tokens[0].type&&(n.tokens[0].tokens[0].text=e+" "+n.tokens[0].tokens[0].text)):n.tokens.unshift({type:"text",text:e+" "}):o+=e+" "}o+=this.parse(n.tokens,i),l+=this.renderer.listitem(o,r,!!s)}n+=this.renderer.list(l,t,s);continue}case"html":{const e=r;n+=this.renderer.html(e.text,e.block);continue}case"paragraph":{const e=r;n+=this.renderer.paragraph(this.parseInline(e.tokens));continue}case"text":{let i=r,l=i.tokens?this.parseInline(i.tokens):i.text;for(;s+1{const r=e[s].flat(1/0);n=n.concat(this.walkTokens(r,t))})):e.tokens&&(n=n.concat(this.walkTokens(e.tokens,t)))}}return n}use(...e){const t=this.defaults.extensions||{renderers:{},childTokens:{}};return e.forEach((e=>{const n={...e};if(n.async=this.defaults.async||n.async||!1,e.extensions&&(e.extensions.forEach((e=>{if(!e.name)throw new Error("extension name required");if("renderer"in e){const n=t.renderers[e.name];t.renderers[e.name]=n?function(...t){let s=e.renderer.apply(this,t);return!1===s&&(s=n.apply(this,t)),s}:e.renderer}if("tokenizer"in e){if(!e.level||"block"!==e.level&&"inline"!==e.level)throw new Error("extension level must be 'block' or 'inline'");const n=t[e.level];n?n.unshift(e.tokenizer):t[e.level]=[e.tokenizer],e.start&&("block"===e.level?t.startBlock?t.startBlock.push(e.start):t.startBlock=[e.start]:"inline"===e.level&&(t.startInline?t.startInline.push(e.start):t.startInline=[e.start]))}"childTokens"in e&&e.childTokens&&(t.childTokens[e.name]=e.childTokens)})),n.extensions=t),e.renderer){const t=this.defaults.renderer||new se(this.defaults);for(const n in e.renderer){if(!(n in t))throw new Error(`renderer '${n}' does not exist`);if("options"===n)continue;const s=n,r=e.renderer[s],i=t[s];t[s]=(...e)=>{let n=r.apply(t,e);return!1===n&&(n=i.apply(t,e)),n||""}}n.renderer=t}if(e.tokenizer){const t=this.defaults.tokenizer||new w(this.defaults);for(const n in e.tokenizer){if(!(n in t))throw new Error(`tokenizer '${n}' does not exist`);if(["options","rules","lexer"].includes(n))continue;const s=n,r=e.tokenizer[s],i=t[s];t[s]=(...e)=>{let n=r.apply(t,e);return!1===n&&(n=i.apply(t,e)),n}}n.tokenizer=t}if(e.hooks){const t=this.defaults.hooks||new le;for(const n in e.hooks){if(!(n in t))throw new Error(`hook '${n}' does not exist`);if("options"===n)continue;const s=n,r=e.hooks[s],i=t[s];le.passThroughHooks.has(n)?t[s]=e=>{if(this.defaults.async)return Promise.resolve(r.call(t,e)).then((e=>i.call(t,e)));const n=r.call(t,e);return i.call(t,n)}:t[s]=(...e)=>{let n=r.apply(t,e);return!1===n&&(n=i.apply(t,e)),n}}n.hooks=t}if(e.walkTokens){const t=this.defaults.walkTokens,s=e.walkTokens;n.walkTokens=function(e){let n=[];return n.push(s.call(this,e)),t&&(n=n.concat(t.call(this,e))),n}}this.defaults={...this.defaults,...n}})),this}setOptions(e){return this.defaults={...this.defaults,...e},this}lexer(e,t){return ne.lex(e,t??this.defaults)}parser(e,t){return ie.parse(e,t??this.defaults)}#e(e,t){return(n,s)=>{const r={...s},i={...this.defaults,...r};!0===this.defaults.async&&!1===r.async&&(i.silent||console.warn("marked(): The async option was set to true by an extension. The async: false option sent to parse will be ignored."),i.async=!0);const l=this.#t(!!i.silent,!!i.async);if(null==n)return l(new Error("marked(): input parameter is undefined or null"));if("string"!=typeof n)return l(new Error("marked(): input parameter is of type "+Object.prototype.toString.call(n)+", string expected"));if(i.hooks&&(i.hooks.options=i),i.async)return Promise.resolve(i.hooks?i.hooks.preprocess(n):n).then((t=>e(t,i))).then((e=>i.hooks?i.hooks.processAllTokens(e):e)).then((e=>i.walkTokens?Promise.all(this.walkTokens(e,i.walkTokens)).then((()=>e)):e)).then((e=>t(e,i))).then((e=>i.hooks?i.hooks.postprocess(e):e)).catch(l);try{i.hooks&&(n=i.hooks.preprocess(n));let s=e(n,i);i.hooks&&(s=i.hooks.processAllTokens(s)),i.walkTokens&&this.walkTokens(s,i.walkTokens);let r=t(s,i);return i.hooks&&(r=i.hooks.postprocess(r)),r}catch(e){return l(e)}}}#t(e,t){return n=>{if(n.message+="\nPlease report this to https://github.com/markedjs/marked.",e){const e="

    An error occurred:

    "+c(n.message+"",!0)+"
    ";return t?Promise.resolve(e):e}if(t)return Promise.reject(n);throw n}}}const ae=new oe;function ce(e,t){return ae.parse(e,t)}ce.options=ce.setOptions=function(e){return ae.setOptions(e),ce.defaults=ae.defaults,n(ce.defaults),ce},ce.getDefaults=t,ce.defaults=e.defaults,ce.use=function(...e){return ae.use(...e),ce.defaults=ae.defaults,n(ce.defaults),ce},ce.walkTokens=function(e,t){return ae.walkTokens(e,t)},ce.parseInline=ae.parseInline,ce.Parser=ie,ce.parser=ie.parse,ce.Renderer=se,ce.TextRenderer=re,ce.Lexer=ne,ce.lexer=ne.lex,ce.Tokenizer=w,ce.Hooks=le,ce.parse=ce;const he=ce.options,pe=ce.setOptions,ue=ce.use,ke=ce.walkTokens,ge=ce.parseInline,fe=ce,de=ie.parse,xe=ne.lex;e.Hooks=le,e.Lexer=ne,e.Marked=oe,e.Parser=ie,e.Renderer=se,e.TextRenderer=re,e.Tokenizer=w,e.getDefaults=t,e.lexer=xe,e.marked=ce,e.options=he,e.parse=fe,e.parseInline=ge,e.parser=de,e.setOptions=pe,e.use=ue,e.walkTokens=ke})); diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/kjweb_async/purify.min.js b/ComfyUI/custom_nodes/ComfyUI-KJNodes/kjweb_async/purify.min.js new file mode 100644 index 0000000000000000000000000000000000000000..c2f5164618eebcc44b0186f594ccb8092639c670 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-KJNodes/kjweb_async/purify.min.js @@ -0,0 +1,3 @@ +/*! @license DOMPurify 3.0.11 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.0.11/LICENSE */ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).DOMPurify=t()}(this,(function(){"use strict";const{entries:e,setPrototypeOf:t,isFrozen:n,getPrototypeOf:o,getOwnPropertyDescriptor:r}=Object;let{freeze:i,seal:a,create:l}=Object,{apply:c,construct:s}="undefined"!=typeof Reflect&&Reflect;i||(i=function(e){return e}),a||(a=function(e){return e}),c||(c=function(e,t,n){return e.apply(t,n)}),s||(s=function(e,t){return new e(...t)});const u=b(Array.prototype.forEach),m=b(Array.prototype.pop),p=b(Array.prototype.push),f=b(String.prototype.toLowerCase),d=b(String.prototype.toString),h=b(String.prototype.match),g=b(String.prototype.replace),T=b(String.prototype.indexOf),y=b(String.prototype.trim),E=b(Object.prototype.hasOwnProperty),A=b(RegExp.prototype.test),_=(N=TypeError,function(){for(var e=arguments.length,t=new Array(e),n=0;n1?n-1:0),r=1;r2&&void 0!==arguments[2]?arguments[2]:f;t&&t(e,null);let i=o.length;for(;i--;){let t=o[i];if("string"==typeof t){const e=r(t);e!==t&&(n(o)||(o[i]=e),t=e)}e[t]=!0}return e}function R(e){for(let t=0;t/gm),B=a(/\${[\w\W]*}/gm),W=a(/^data-[\-\w.\u00B7-\uFFFF]/),G=a(/^aria-[\-\w]+$/),Y=a(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),j=a(/^(?:\w+script|data):/i),X=a(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),q=a(/^html$/i),$=a(/^[a-z][.\w]*(-[.\w]+)+$/i);var K=Object.freeze({__proto__:null,MUSTACHE_EXPR:H,ERB_EXPR:z,TMPLIT_EXPR:B,DATA_ATTR:W,ARIA_ATTR:G,IS_ALLOWED_URI:Y,IS_SCRIPT_OR_DATA:j,ATTR_WHITESPACE:X,DOCTYPE_NAME:q,CUSTOM_ELEMENT:$});const V=function(){return"undefined"==typeof window?null:window},Z=function(e,t){if("object"!=typeof e||"function"!=typeof e.createPolicy)return null;let n=null;const o="data-tt-policy-suffix";t&&t.hasAttribute(o)&&(n=t.getAttribute(o));const r="dompurify"+(n?"#"+n:"");try{return e.createPolicy(r,{createHTML:e=>e,createScriptURL:e=>e})}catch(e){return console.warn("TrustedTypes policy "+r+" could not be created."),null}};var J=function t(){let n=arguments.length>0&&void 0!==arguments[0]?arguments[0]:V();const o=e=>t(e);if(o.version="3.0.11",o.removed=[],!n||!n.document||9!==n.document.nodeType)return o.isSupported=!1,o;let{document:r}=n;const a=r,c=a.currentScript,{DocumentFragment:s,HTMLTemplateElement:N,Node:b,Element:R,NodeFilter:H,NamedNodeMap:z=n.NamedNodeMap||n.MozNamedAttrMap,HTMLFormElement:B,DOMParser:W,trustedTypes:G}=n,j=R.prototype,X=L(j,"cloneNode"),$=L(j,"nextSibling"),J=L(j,"childNodes"),Q=L(j,"parentNode");if("function"==typeof N){const e=r.createElement("template");e.content&&e.content.ownerDocument&&(r=e.content.ownerDocument)}let ee,te="";const{implementation:ne,createNodeIterator:oe,createDocumentFragment:re,getElementsByTagName:ie}=r,{importNode:ae}=a;let le={};o.isSupported="function"==typeof e&&"function"==typeof Q&&ne&&void 0!==ne.createHTMLDocument;const{MUSTACHE_EXPR:ce,ERB_EXPR:se,TMPLIT_EXPR:ue,DATA_ATTR:me,ARIA_ATTR:pe,IS_SCRIPT_OR_DATA:fe,ATTR_WHITESPACE:de,CUSTOM_ELEMENT:he}=K;let{IS_ALLOWED_URI:ge}=K,Te=null;const ye=S({},[...D,...C,...O,...v,...M]);let Ee=null;const Ae=S({},[...I,...U,...P,...F]);let _e=Object.seal(l(null,{tagNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},allowCustomizedBuiltInElements:{writable:!0,configurable:!1,enumerable:!0,value:!1}})),Ne=null,be=null,Se=!0,Re=!0,we=!1,Le=!0,De=!1,Ce=!0,Oe=!1,xe=!1,ve=!1,ke=!1,Me=!1,Ie=!1,Ue=!0,Pe=!1;const Fe="user-content-";let He=!0,ze=!1,Be={},We=null;const Ge=S({},["annotation-xml","audio","colgroup","desc","foreignobject","head","iframe","math","mi","mn","mo","ms","mtext","noembed","noframes","noscript","plaintext","script","style","svg","template","thead","title","video","xmp"]);let Ye=null;const je=S({},["audio","video","img","source","image","track"]);let Xe=null;const qe=S({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),$e="http://www.w3.org/1998/Math/MathML",Ke="http://www.w3.org/2000/svg",Ve="http://www.w3.org/1999/xhtml";let Ze=Ve,Je=!1,Qe=null;const et=S({},[$e,Ke,Ve],d);let tt=null;const nt=["application/xhtml+xml","text/html"],ot="text/html";let rt=null,it=null;const at=r.createElement("form"),lt=function(e){return e instanceof RegExp||e instanceof Function},ct=function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};if(!it||it!==e){if(e&&"object"==typeof e||(e={}),e=w(e),tt=-1===nt.indexOf(e.PARSER_MEDIA_TYPE)?ot:e.PARSER_MEDIA_TYPE,rt="application/xhtml+xml"===tt?d:f,Te=E(e,"ALLOWED_TAGS")?S({},e.ALLOWED_TAGS,rt):ye,Ee=E(e,"ALLOWED_ATTR")?S({},e.ALLOWED_ATTR,rt):Ae,Qe=E(e,"ALLOWED_NAMESPACES")?S({},e.ALLOWED_NAMESPACES,d):et,Xe=E(e,"ADD_URI_SAFE_ATTR")?S(w(qe),e.ADD_URI_SAFE_ATTR,rt):qe,Ye=E(e,"ADD_DATA_URI_TAGS")?S(w(je),e.ADD_DATA_URI_TAGS,rt):je,We=E(e,"FORBID_CONTENTS")?S({},e.FORBID_CONTENTS,rt):Ge,Ne=E(e,"FORBID_TAGS")?S({},e.FORBID_TAGS,rt):{},be=E(e,"FORBID_ATTR")?S({},e.FORBID_ATTR,rt):{},Be=!!E(e,"USE_PROFILES")&&e.USE_PROFILES,Se=!1!==e.ALLOW_ARIA_ATTR,Re=!1!==e.ALLOW_DATA_ATTR,we=e.ALLOW_UNKNOWN_PROTOCOLS||!1,Le=!1!==e.ALLOW_SELF_CLOSE_IN_ATTR,De=e.SAFE_FOR_TEMPLATES||!1,Ce=!1!==e.SAFE_FOR_XML,Oe=e.WHOLE_DOCUMENT||!1,ke=e.RETURN_DOM||!1,Me=e.RETURN_DOM_FRAGMENT||!1,Ie=e.RETURN_TRUSTED_TYPE||!1,ve=e.FORCE_BODY||!1,Ue=!1!==e.SANITIZE_DOM,Pe=e.SANITIZE_NAMED_PROPS||!1,He=!1!==e.KEEP_CONTENT,ze=e.IN_PLACE||!1,ge=e.ALLOWED_URI_REGEXP||Y,Ze=e.NAMESPACE||Ve,_e=e.CUSTOM_ELEMENT_HANDLING||{},e.CUSTOM_ELEMENT_HANDLING&<(e.CUSTOM_ELEMENT_HANDLING.tagNameCheck)&&(_e.tagNameCheck=e.CUSTOM_ELEMENT_HANDLING.tagNameCheck),e.CUSTOM_ELEMENT_HANDLING&<(e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)&&(_e.attributeNameCheck=e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck),e.CUSTOM_ELEMENT_HANDLING&&"boolean"==typeof e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements&&(_e.allowCustomizedBuiltInElements=e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements),De&&(Re=!1),Me&&(ke=!0),Be&&(Te=S({},M),Ee=[],!0===Be.html&&(S(Te,D),S(Ee,I)),!0===Be.svg&&(S(Te,C),S(Ee,U),S(Ee,F)),!0===Be.svgFilters&&(S(Te,O),S(Ee,U),S(Ee,F)),!0===Be.mathMl&&(S(Te,v),S(Ee,P),S(Ee,F))),e.ADD_TAGS&&(Te===ye&&(Te=w(Te)),S(Te,e.ADD_TAGS,rt)),e.ADD_ATTR&&(Ee===Ae&&(Ee=w(Ee)),S(Ee,e.ADD_ATTR,rt)),e.ADD_URI_SAFE_ATTR&&S(Xe,e.ADD_URI_SAFE_ATTR,rt),e.FORBID_CONTENTS&&(We===Ge&&(We=w(We)),S(We,e.FORBID_CONTENTS,rt)),He&&(Te["#text"]=!0),Oe&&S(Te,["html","head","body"]),Te.table&&(S(Te,["tbody"]),delete Ne.tbody),e.TRUSTED_TYPES_POLICY){if("function"!=typeof e.TRUSTED_TYPES_POLICY.createHTML)throw _('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.');if("function"!=typeof e.TRUSTED_TYPES_POLICY.createScriptURL)throw _('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.');ee=e.TRUSTED_TYPES_POLICY,te=ee.createHTML("")}else void 0===ee&&(ee=Z(G,c)),null!==ee&&"string"==typeof te&&(te=ee.createHTML(""));i&&i(e),it=e}},st=S({},["mi","mo","mn","ms","mtext"]),ut=S({},["foreignobject","desc","title","annotation-xml"]),mt=S({},["title","style","font","a","script"]),pt=S({},[...C,...O,...x]),ft=S({},[...v,...k]),dt=function(e){let t=Q(e);t&&t.tagName||(t={namespaceURI:Ze,tagName:"template"});const n=f(e.tagName),o=f(t.tagName);return!!Qe[e.namespaceURI]&&(e.namespaceURI===Ke?t.namespaceURI===Ve?"svg"===n:t.namespaceURI===$e?"svg"===n&&("annotation-xml"===o||st[o]):Boolean(pt[n]):e.namespaceURI===$e?t.namespaceURI===Ve?"math"===n:t.namespaceURI===Ke?"math"===n&&ut[o]:Boolean(ft[n]):e.namespaceURI===Ve?!(t.namespaceURI===Ke&&!ut[o])&&(!(t.namespaceURI===$e&&!st[o])&&(!ft[n]&&(mt[n]||!pt[n]))):!("application/xhtml+xml"!==tt||!Qe[e.namespaceURI]))},ht=function(e){p(o.removed,{element:e});try{e.parentNode.removeChild(e)}catch(t){e.remove()}},gt=function(e,t){try{p(o.removed,{attribute:t.getAttributeNode(e),from:t})}catch(e){p(o.removed,{attribute:null,from:t})}if(t.removeAttribute(e),"is"===e&&!Ee[e])if(ke||Me)try{ht(t)}catch(e){}else try{t.setAttribute(e,"")}catch(e){}},Tt=function(e){let t=null,n=null;if(ve)e=""+e;else{const t=h(e,/^[\r\n\t ]+/);n=t&&t[0]}"application/xhtml+xml"===tt&&Ze===Ve&&(e=''+e+"");const o=ee?ee.createHTML(e):e;if(Ze===Ve)try{t=(new W).parseFromString(o,tt)}catch(e){}if(!t||!t.documentElement){t=ne.createDocument(Ze,"template",null);try{t.documentElement.innerHTML=Je?te:o}catch(e){}}const i=t.body||t.documentElement;return e&&n&&i.insertBefore(r.createTextNode(n),i.childNodes[0]||null),Ze===Ve?ie.call(t,Oe?"html":"body")[0]:Oe?t.documentElement:i},yt=function(e){return oe.call(e.ownerDocument||e,e,H.SHOW_ELEMENT|H.SHOW_COMMENT|H.SHOW_TEXT|H.SHOW_PROCESSING_INSTRUCTION|H.SHOW_CDATA_SECTION,null)},Et=function(e){return e instanceof B&&("string"!=typeof e.nodeName||"string"!=typeof e.textContent||"function"!=typeof e.removeChild||!(e.attributes instanceof z)||"function"!=typeof e.removeAttribute||"function"!=typeof e.setAttribute||"string"!=typeof e.namespaceURI||"function"!=typeof e.insertBefore||"function"!=typeof e.hasChildNodes)},At=function(e){return"function"==typeof b&&e instanceof b},_t=function(e,t,n){le[e]&&u(le[e],(e=>{e.call(o,t,n,it)}))},Nt=function(e){let t=null;if(_t("beforeSanitizeElements",e,null),Et(e))return ht(e),!0;const n=rt(e.nodeName);if(_t("uponSanitizeElement",e,{tagName:n,allowedTags:Te}),e.hasChildNodes()&&!At(e.firstElementChild)&&A(/<[/\w]/g,e.innerHTML)&&A(/<[/\w]/g,e.textContent))return ht(e),!0;if(7===e.nodeType)return ht(e),!0;if(Ce&&8===e.nodeType&&A(/<[/\w]/g,e.data))return ht(e),!0;if(!Te[n]||Ne[n]){if(!Ne[n]&&St(n)){if(_e.tagNameCheck instanceof RegExp&&A(_e.tagNameCheck,n))return!1;if(_e.tagNameCheck instanceof Function&&_e.tagNameCheck(n))return!1}if(He&&!We[n]){const t=Q(e)||e.parentNode,n=J(e)||e.childNodes;if(n&&t){for(let o=n.length-1;o>=0;--o)t.insertBefore(X(n[o],!0),$(e))}}return ht(e),!0}return e instanceof R&&!dt(e)?(ht(e),!0):"noscript"!==n&&"noembed"!==n&&"noframes"!==n||!A(/<\/no(script|embed|frames)/i,e.innerHTML)?(De&&3===e.nodeType&&(t=e.textContent,u([ce,se,ue],(e=>{t=g(t,e," ")})),e.textContent!==t&&(p(o.removed,{element:e.cloneNode()}),e.textContent=t)),_t("afterSanitizeElements",e,null),!1):(ht(e),!0)},bt=function(e,t,n){if(Ue&&("id"===t||"name"===t)&&(n in r||n in at))return!1;if(Re&&!be[t]&&A(me,t));else if(Se&&A(pe,t));else if(!Ee[t]||be[t]){if(!(St(e)&&(_e.tagNameCheck instanceof RegExp&&A(_e.tagNameCheck,e)||_e.tagNameCheck instanceof Function&&_e.tagNameCheck(e))&&(_e.attributeNameCheck instanceof RegExp&&A(_e.attributeNameCheck,t)||_e.attributeNameCheck instanceof Function&&_e.attributeNameCheck(t))||"is"===t&&_e.allowCustomizedBuiltInElements&&(_e.tagNameCheck instanceof RegExp&&A(_e.tagNameCheck,n)||_e.tagNameCheck instanceof Function&&_e.tagNameCheck(n))))return!1}else if(Xe[t]);else if(A(ge,g(n,de,"")));else if("src"!==t&&"xlink:href"!==t&&"href"!==t||"script"===e||0!==T(n,"data:")||!Ye[e]){if(we&&!A(fe,g(n,de,"")));else if(n)return!1}else;return!0},St=function(e){return"annotation-xml"!==e&&h(e,he)},Rt=function(e){_t("beforeSanitizeAttributes",e,null);const{attributes:t}=e;if(!t)return;const n={attrName:"",attrValue:"",keepAttr:!0,allowedAttributes:Ee};let r=t.length;for(;r--;){const i=t[r],{name:a,namespaceURI:l,value:c}=i,s=rt(a);let p="value"===a?c:y(c);if(n.attrName=s,n.attrValue=p,n.keepAttr=!0,n.forceKeepAttr=void 0,_t("uponSanitizeAttribute",e,n),p=n.attrValue,n.forceKeepAttr)continue;if(gt(a,e),!n.keepAttr)continue;if(!Le&&A(/\/>/i,p)){gt(a,e);continue}De&&u([ce,se,ue],(e=>{p=g(p,e," ")}));const f=rt(e.nodeName);if(bt(f,s,p)){if(!Pe||"id"!==s&&"name"!==s||(gt(a,e),p=Fe+p),ee&&"object"==typeof G&&"function"==typeof G.getAttributeType)if(l);else switch(G.getAttributeType(f,s)){case"TrustedHTML":p=ee.createHTML(p);break;case"TrustedScriptURL":p=ee.createScriptURL(p)}try{l?e.setAttributeNS(l,a,p):e.setAttribute(a,p),m(o.removed)}catch(e){}}}_t("afterSanitizeAttributes",e,null)},wt=function e(t){let n=null;const o=yt(t);for(_t("beforeSanitizeShadowDOM",t,null);n=o.nextNode();)_t("uponSanitizeShadowNode",n,null),Nt(n)||(n.content instanceof s&&e(n.content),Rt(n));_t("afterSanitizeShadowDOM",t,null)};return o.sanitize=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=null,r=null,i=null,l=null;if(Je=!e,Je&&(e="\x3c!--\x3e"),"string"!=typeof e&&!At(e)){if("function"!=typeof e.toString)throw _("toString is not a function");if("string"!=typeof(e=e.toString()))throw _("dirty is not a string, aborting")}if(!o.isSupported)return e;if(xe||ct(t),o.removed=[],"string"==typeof e&&(ze=!1),ze){if(e.nodeName){const t=rt(e.nodeName);if(!Te[t]||Ne[t])throw _("root node is forbidden and cannot be sanitized in-place")}}else if(e instanceof b)n=Tt("\x3c!----\x3e"),r=n.ownerDocument.importNode(e,!0),1===r.nodeType&&"BODY"===r.nodeName||"HTML"===r.nodeName?n=r:n.appendChild(r);else{if(!ke&&!De&&!Oe&&-1===e.indexOf("<"))return ee&&Ie?ee.createHTML(e):e;if(n=Tt(e),!n)return ke?null:Ie?te:""}n&&ve&&ht(n.firstChild);const c=yt(ze?e:n);for(;i=c.nextNode();)Nt(i)||(i.content instanceof s&&wt(i.content),Rt(i));if(ze)return e;if(ke){if(Me)for(l=re.call(n.ownerDocument);n.firstChild;)l.appendChild(n.firstChild);else l=n;return(Ee.shadowroot||Ee.shadowrootmode)&&(l=ae.call(a,l,!0)),l}let m=Oe?n.outerHTML:n.innerHTML;return Oe&&Te["!doctype"]&&n.ownerDocument&&n.ownerDocument.doctype&&n.ownerDocument.doctype.name&&A(q,n.ownerDocument.doctype.name)&&(m="\n"+m),De&&u([ce,se,ue],(e=>{m=g(m,e," ")})),ee&&Ie?ee.createHTML(m):m},o.setConfig=function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};ct(e),xe=!0},o.clearConfig=function(){it=null,xe=!1},o.isValidAttribute=function(e,t,n){it||ct({});const o=rt(e),r=rt(t);return bt(o,r,n)},o.addHook=function(e,t){"function"==typeof t&&(le[e]=le[e]||[],p(le[e],t))},o.removeHook=function(e){if(le[e])return m(le[e])},o.removeHooks=function(e){le[e]&&(le[e]=[])},o.removeAllHooks=function(){le={}},o}();return J})); +//# sourceMappingURL=purify.min.js.map diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/__pycache__/audioscheduler_nodes.cpython-312.pyc b/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/__pycache__/audioscheduler_nodes.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bb666ee57c203a1d47114e1f224e19b644f27c6e Binary files /dev/null and b/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/__pycache__/audioscheduler_nodes.cpython-312.pyc differ diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/__pycache__/batchcrop_nodes.cpython-312.pyc b/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/__pycache__/batchcrop_nodes.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..20c79653f14a2217bf35f548b28628798f29bc8d Binary files /dev/null and b/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/__pycache__/batchcrop_nodes.cpython-312.pyc differ diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/__pycache__/context_windows_visualizer.cpython-312.pyc b/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/__pycache__/context_windows_visualizer.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..86d9ea3e9846354177cc53b66f8da56c9f71710b Binary files /dev/null and b/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/__pycache__/context_windows_visualizer.cpython-312.pyc differ diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/__pycache__/curve_nodes.cpython-312.pyc b/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/__pycache__/curve_nodes.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c1a7add43f86e6658408e9d49f5a0b643bf5ca15 Binary files /dev/null and b/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/__pycache__/curve_nodes.cpython-312.pyc differ diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/__pycache__/hdr_preview_node.cpython-312.pyc b/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/__pycache__/hdr_preview_node.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a92b19b7289409893d7eed114d464f604a251d52 Binary files /dev/null and b/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/__pycache__/hdr_preview_node.cpython-312.pyc differ diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/__pycache__/ideogram4_nodes.cpython-312.pyc b/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/__pycache__/ideogram4_nodes.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..beed9631fb7e2f9d336bb9e9783ee06586ecffe7 Binary files /dev/null and b/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/__pycache__/ideogram4_nodes.cpython-312.pyc differ diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/__pycache__/image_nodes.cpython-312.pyc b/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/__pycache__/image_nodes.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..19a5a320015e416dd60a669982aeb18d76983632 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/__pycache__/image_nodes.cpython-312.pyc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ddcf1df5aecf7c9c08d560aa43a10b3ae69455199bf0928866fe413415d99b61 +size 246822 diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/__pycache__/image_transform_node.cpython-312.pyc b/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/__pycache__/image_transform_node.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e2c94563edd92dc6fd673a331b8ceff038111d38 Binary files /dev/null and b/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/__pycache__/image_transform_node.cpython-312.pyc differ diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/__pycache__/lora_nodes.cpython-312.pyc b/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/__pycache__/lora_nodes.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..302cc1993aaa6210b9a2bcb207e37813a2cf96b8 Binary files /dev/null and b/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/__pycache__/lora_nodes.cpython-312.pyc differ diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/__pycache__/ltxv_nodes.cpython-312.pyc b/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/__pycache__/ltxv_nodes.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f908ae523367beac04e2657f0c6a740f9211c1c6 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/__pycache__/ltxv_nodes.cpython-312.pyc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:170324aba040fe8db68d5883c016f2ee19b12317c9474dcaff2a63b6d41e86e0 +size 138919 diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/__pycache__/mask_nodes.cpython-312.pyc b/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/__pycache__/mask_nodes.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9399d4e57508cd1f6d23999ddefab1ba8500d888 Binary files /dev/null and b/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/__pycache__/mask_nodes.cpython-312.pyc differ diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/__pycache__/model_optimization_nodes.cpython-312.pyc b/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/__pycache__/model_optimization_nodes.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3c522e274eb8337087a20ea83d85d5869d3d5126 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/__pycache__/model_optimization_nodes.cpython-312.pyc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c5962d4da5934f5beeb983426234c8980005a371c0ccd91fcc433aa7165b6bd3 +size 136367 diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/__pycache__/nodes.cpython-312.pyc b/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/__pycache__/nodes.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e8e96b2772d5b8324f236d4765ef6eea52a087f7 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/__pycache__/nodes.cpython-312.pyc @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:22d703377c4c7a4b0a8d5fcf158732f6861f739ab5a38459f9c4974c0e18e12e +size 163575 diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/__pycache__/preview_override_node.cpython-312.pyc b/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/__pycache__/preview_override_node.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d92d87e1d1e15d5f3b009a4e6e598b83080c9842 Binary files /dev/null and b/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/__pycache__/preview_override_node.cpython-312.pyc differ diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/__pycache__/sharpen_nodes.cpython-312.pyc b/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/__pycache__/sharpen_nodes.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4f9ae3c3dde9bf9cd487c6793ba540a46dec9cfa Binary files /dev/null and b/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/__pycache__/sharpen_nodes.cpython-312.pyc differ diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/__pycache__/tiny_vae.cpython-312.pyc b/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/__pycache__/tiny_vae.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4deb3514ef0ae73be83800011c0f72cf3168b1f4 Binary files /dev/null and b/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/__pycache__/tiny_vae.cpython-312.pyc differ diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/__pycache__/triton_vae.cpython-312.pyc b/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/__pycache__/triton_vae.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2e4e325c0fba0916c0b6f392c9bd58573f1d8da8 Binary files /dev/null and b/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/__pycache__/triton_vae.cpython-312.pyc differ diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/audioscheduler_nodes.py b/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/audioscheduler_nodes.py new file mode 100644 index 0000000000000000000000000000000000000000..69d0422e7da875298f87fe60a7f6d1494530dca2 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/audioscheduler_nodes.py @@ -0,0 +1,251 @@ +# to be used with https://github.com/a1lazydog/ComfyUI-AudioScheduler +import torch +from torchvision.transforms import functional as TF +from PIL import Image, ImageDraw +import numpy as np +from ..utility.utility import pil2tensor +from nodes import MAX_RESOLUTION + +class NormalizedAmplitudeToMask: + @classmethod + def INPUT_TYPES(s): + return {"required": { + "normalized_amp": ("NORMALIZED_AMPLITUDE",), + "width": ("INT", {"default": 512,"min": 16, "max": 4096, "step": 1}), + "height": ("INT", {"default": 512,"min": 16, "max": 4096, "step": 1}), + "frame_offset": ("INT", {"default": 0,"min": -255, "max": 255, "step": 1}), + "location_x": ("INT", {"default": 256,"min": 0, "max": 4096, "step": 1}), + "location_y": ("INT", {"default": 256,"min": 0, "max": 4096, "step": 1}), + "size": ("INT", {"default": 128,"min": 8, "max": 4096, "step": 1}), + "shape": ( + [ + 'none', + 'circle', + 'square', + 'triangle', + ], + { + "default": 'none' + }), + "color": ( + [ + 'white', + 'amplitude', + ], + { + "default": 'amplitude' + }), + },} + + CATEGORY = "KJNodes/audio" + RETURN_TYPES = ("MASK",) + FUNCTION = "convert" + DESCRIPTION = """ +Works as a bridge to the AudioScheduler -nodes: +https://github.com/a1lazydog/ComfyUI-AudioScheduler +Creates masks based on the normalized amplitude. +""" + + def convert(self, normalized_amp, width, height, frame_offset, shape, location_x, location_y, size, color): + # Ensure normalized_amp is an array and within the range [0, 1] + normalized_amp = np.clip(normalized_amp, 0.0, 1.0) + + # Offset the amplitude values by rolling the array + normalized_amp = np.roll(normalized_amp, frame_offset) + + # Initialize an empty list to hold the image tensors + out = [] + # Iterate over each amplitude value to create an image + for amp in normalized_amp: + # Scale the amplitude value to cover the full range of grayscale values + if color == 'amplitude': + grayscale_value = int(amp * 255) + elif color == 'white': + grayscale_value = 255 + # Convert the grayscale value to an RGB format + gray_color = (grayscale_value, grayscale_value, grayscale_value) + finalsize = size * amp + + if shape == 'none': + shapeimage = Image.new("RGB", (width, height), gray_color) + else: + shapeimage = Image.new("RGB", (width, height), "black") + + draw = ImageDraw.Draw(shapeimage) + if shape == 'circle' or shape == 'square': + # Define the bounding box for the shape + left_up_point = (location_x - finalsize, location_y - finalsize) + right_down_point = (location_x + finalsize,location_y + finalsize) + two_points = [left_up_point, right_down_point] + + if shape == 'circle': + draw.ellipse(two_points, fill=gray_color) + elif shape == 'square': + draw.rectangle(two_points, fill=gray_color) + + elif shape == 'triangle': + # Define the points for the triangle + left_up_point = (location_x - finalsize, location_y + finalsize) # bottom left + right_down_point = (location_x + finalsize, location_y + finalsize) # bottom right + top_point = (location_x, location_y) # top point + draw.polygon([top_point, left_up_point, right_down_point], fill=gray_color) + + shapeimage = pil2tensor(shapeimage) + mask = shapeimage[:, :, :, 0] + out.append(mask) + + return (torch.cat(out, dim=0),) + +class NormalizedAmplitudeToFloatList: + @classmethod + def INPUT_TYPES(s): + return {"required": { + "normalized_amp": ("NORMALIZED_AMPLITUDE",), + },} + + CATEGORY = "KJNodes/audio" + RETURN_TYPES = ("FLOAT",) + FUNCTION = "convert" + DESCRIPTION = """ +Works as a bridge to the AudioScheduler -nodes: +https://github.com/a1lazydog/ComfyUI-AudioScheduler +Creates a list of floats from the normalized amplitude. +""" + + def convert(self, normalized_amp): + # Ensure normalized_amp is an array and within the range [0, 1] + normalized_amp = np.clip(normalized_amp, 0.0, 1.0) + return (normalized_amp.tolist(),) + +class OffsetMaskByNormalizedAmplitude: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "normalized_amp": ("NORMALIZED_AMPLITUDE",), + "mask": ("MASK",), + "x": ("INT", { "default": 0, "min": -4096, "max": MAX_RESOLUTION, "step": 1, "display": "number" }), + "y": ("INT", { "default": 0, "min": -4096, "max": MAX_RESOLUTION, "step": 1, "display": "number" }), + "rotate": ("BOOLEAN", { "default": False }), + "angle_multiplier": ("FLOAT", { "default": 0.0, "min": -1.0, "max": 1.0, "step": 0.001, "display": "number" }), + } + } + + RETURN_TYPES = ("MASK",) + RETURN_NAMES = ("mask",) + FUNCTION = "offset" + CATEGORY = "KJNodes/audio" + DESCRIPTION = """ +Works as a bridge to the AudioScheduler -nodes: +https://github.com/a1lazydog/ComfyUI-AudioScheduler +Offsets masks based on the normalized amplitude. +""" + + def offset(self, mask, x, y, angle_multiplier, rotate, normalized_amp): + + # Ensure normalized_amp is an array and within the range [0, 1] + offsetmask = mask.clone() + normalized_amp = np.clip(normalized_amp, 0.0, 1.0) + + batch_size, height, width = mask.shape + + if rotate: + for i in range(batch_size): + rotation_amp = int(normalized_amp[i] * (360 * angle_multiplier)) + rotation_angle = rotation_amp + offsetmask[i] = TF.rotate(offsetmask[i].unsqueeze(0), rotation_angle).squeeze(0) + if x != 0 or y != 0: + for i in range(batch_size): + offset_amp = normalized_amp[i] * 10 + shift_x = min(x*offset_amp, width-1) + shift_y = min(y*offset_amp, height-1) + if shift_x != 0: + offsetmask[i] = torch.roll(offsetmask[i], shifts=int(shift_x), dims=1) + if shift_y != 0: + offsetmask[i] = torch.roll(offsetmask[i], shifts=int(shift_y), dims=0) + + return offsetmask, + +class ImageTransformByNormalizedAmplitude: + @classmethod + def INPUT_TYPES(s): + return {"required": { + "normalized_amp": ("NORMALIZED_AMPLITUDE",), + "zoom_scale": ("FLOAT", { "default": 0.0, "min": -1.0, "max": 1.0, "step": 0.001, "display": "number" }), + "x_offset": ("INT", { "default": 0, "min": (1 -MAX_RESOLUTION), "max": MAX_RESOLUTION, "step": 1, "display": "number" }), + "y_offset": ("INT", { "default": 0, "min": (1 -MAX_RESOLUTION), "max": MAX_RESOLUTION, "step": 1, "display": "number" }), + "cumulative": ("BOOLEAN", { "default": False }), + "image": ("IMAGE",), + }} + + RETURN_TYPES = ("IMAGE",) + FUNCTION = "amptransform" + CATEGORY = "KJNodes/audio" + DESCRIPTION = """ +Works as a bridge to the AudioScheduler -nodes: +https://github.com/a1lazydog/ComfyUI-AudioScheduler +Transforms image based on the normalized amplitude. +""" + + def amptransform(self, image, normalized_amp, zoom_scale, cumulative, x_offset, y_offset): + # Ensure normalized_amp is an array and within the range [0, 1] + normalized_amp = np.clip(normalized_amp, 0.0, 1.0) + transformed_images = [] + + # Initialize the cumulative zoom factor + prev_amp = 0.0 + + for i in range(image.shape[0]): + img = image[i] # Get the i-th image in the batch + amp = normalized_amp[i] # Get the corresponding amplitude value + + # Incrementally increase the cumulative zoom factor + if cumulative: + prev_amp += amp + amp += prev_amp + + # Convert the image tensor from BxHxWxC to CxHxW format expected by torchvision + img = img.permute(2, 0, 1) + + # Convert PyTorch tensor to PIL Image for processing + pil_img = TF.to_pil_image(img) + + # Calculate the crop size based on the amplitude + width, height = pil_img.size + crop_size = int(min(width, height) * (1 - amp * zoom_scale)) + crop_size = max(crop_size, 1) + + # Calculate the crop box coordinates (centered crop) + left = (width - crop_size) // 2 + top = (height - crop_size) // 2 + right = (width + crop_size) // 2 + bottom = (height + crop_size) // 2 + + # Crop and resize back to original size + cropped_img = TF.crop(pil_img, top, left, crop_size, crop_size) + resized_img = TF.resize(cropped_img, (height, width)) + + # Convert back to tensor in CxHxW format + tensor_img = TF.to_tensor(resized_img) + + # Convert the tensor back to BxHxWxC format + tensor_img = tensor_img.permute(1, 2, 0) + + # Offset the image based on the amplitude + offset_amp = amp * 10 # Calculate the offset magnitude based on the amplitude + shift_x = min(x_offset * offset_amp, img.shape[1] - 1) # Calculate the shift in x direction + shift_y = min(y_offset * offset_amp, img.shape[0] - 1) # Calculate the shift in y direction + + # Apply the offset to the image tensor + if shift_x != 0: + tensor_img = torch.roll(tensor_img, shifts=int(shift_x), dims=1) + if shift_y != 0: + tensor_img = torch.roll(tensor_img, shifts=int(shift_y), dims=0) + + # Add to the list + transformed_images.append(tensor_img) + + # Stack all transformed images into a batch + transformed_batch = torch.stack(transformed_images) + + return (transformed_batch,) \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/batchcrop_nodes.py b/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/batchcrop_nodes.py new file mode 100644 index 0000000000000000000000000000000000000000..dda22b3145555a19c6f8664113a730afb3ae4268 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/batchcrop_nodes.py @@ -0,0 +1,775 @@ +from ..utility.utility import tensor2pil, pil2tensor, normalize_bboxes, BBOX_TYPES +from PIL import Image, ImageDraw, ImageFilter +import numpy as np +import torch +from torchvision.transforms import Resize, CenterCrop, InterpolationMode +import math + +#based on nodes from mtb https://github.com/melMass/comfy_mtb + +def bbox_to_region(bbox, target_size=None): + bbox = bbox_check(bbox, target_size) + return (bbox[0], bbox[1], bbox[0] + bbox[2], bbox[1] + bbox[3]) + +def bbox_check(bbox, target_size=None): + if not target_size: + return bbox + + new_bbox = ( + bbox[0], + bbox[1], + min(target_size[0] - bbox[0], bbox[2]), + min(target_size[1] - bbox[1], bbox[3]), + ) + return new_bbox + +class BatchCropFromMask: + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "original_images": ("IMAGE",), + "masks": ("MASK",), + "crop_size_mult": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.001}), + "bbox_smooth_alpha": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}), + }, + } + + RETURN_TYPES = ( + "IMAGE", + "IMAGE", + "BBOX", + "INT", + "INT", + ) + RETURN_NAMES = ( + "original_images", + "cropped_images", + "bboxes", + "width", + "height", + ) + FUNCTION = "crop" + CATEGORY = "KJNodes/masking" + + def smooth_bbox_size(self, prev_bbox_size, curr_bbox_size, alpha): + if alpha == 0: + return prev_bbox_size + return round(alpha * curr_bbox_size + (1 - alpha) * prev_bbox_size) + + def smooth_center(self, prev_center, curr_center, alpha=0.5): + if alpha == 0: + return prev_center + return ( + round(alpha * curr_center[0] + (1 - alpha) * prev_center[0]), + round(alpha * curr_center[1] + (1 - alpha) * prev_center[1]) + ) + + def crop(self, masks, original_images, crop_size_mult, bbox_smooth_alpha): + + bounding_boxes = [] + cropped_images = [] + + self.max_bbox_width = 0 + self.max_bbox_height = 0 + + # First, calculate the maximum bounding box size across all masks + curr_max_bbox_width = 0 + curr_max_bbox_height = 0 + for mask in masks: + _mask = tensor2pil(mask)[0] + non_zero_indices = np.nonzero(np.array(_mask)) + min_x, max_x = np.min(non_zero_indices[1]), np.max(non_zero_indices[1]) + min_y, max_y = np.min(non_zero_indices[0]), np.max(non_zero_indices[0]) + width = max_x - min_x + height = max_y - min_y + curr_max_bbox_width = max(curr_max_bbox_width, width) + curr_max_bbox_height = max(curr_max_bbox_height, height) + + # Smooth the changes in the bounding box size + self.max_bbox_width = self.smooth_bbox_size(self.max_bbox_width, curr_max_bbox_width, bbox_smooth_alpha) + self.max_bbox_height = self.smooth_bbox_size(self.max_bbox_height, curr_max_bbox_height, bbox_smooth_alpha) + + # Apply the crop size multiplier + self.max_bbox_width = round(self.max_bbox_width * crop_size_mult) + self.max_bbox_height = round(self.max_bbox_height * crop_size_mult) + bbox_aspect_ratio = self.max_bbox_width / self.max_bbox_height + + # Then, for each mask and corresponding image... + for i, (mask, img) in enumerate(zip(masks, original_images)): + _mask = tensor2pil(mask)[0] + non_zero_indices = np.nonzero(np.array(_mask)) + min_x, max_x = np.min(non_zero_indices[1]), np.max(non_zero_indices[1]) + min_y, max_y = np.min(non_zero_indices[0]), np.max(non_zero_indices[0]) + + # Calculate center of bounding box + center_x = np.mean(non_zero_indices[1]) + center_y = np.mean(non_zero_indices[0]) + curr_center = (round(center_x), round(center_y)) + + # If this is the first frame, initialize prev_center with curr_center + if not hasattr(self, 'prev_center'): + self.prev_center = curr_center + + # Smooth the changes in the center coordinates from the second frame onwards + if i > 0: + center = self.smooth_center(self.prev_center, curr_center, bbox_smooth_alpha) + else: + center = curr_center + + # Update prev_center for the next frame + self.prev_center = center + + # Create bounding box using max_bbox_width and max_bbox_height + half_box_width = round(self.max_bbox_width / 2) + half_box_height = round(self.max_bbox_height / 2) + min_x = max(0, center[0] - half_box_width) + max_x = min(img.shape[1], center[0] + half_box_width) + min_y = max(0, center[1] - half_box_height) + max_y = min(img.shape[0], center[1] + half_box_height) + + # Append bounding box coordinates + bounding_boxes.append((min_x, min_y, max_x - min_x, max_y - min_y)) + + # Crop the image from the bounding box + cropped_img = img[min_y:max_y, min_x:max_x, :] + + # Calculate the new dimensions while maintaining the aspect ratio + new_height = min(cropped_img.shape[0], self.max_bbox_height) + new_width = round(new_height * bbox_aspect_ratio) + + # Resize the image + resize_transform = Resize((new_height, new_width)) + resized_img = resize_transform(cropped_img.permute(2, 0, 1)) + + # Perform the center crop to the desired size + crop_transform = CenterCrop((self.max_bbox_height, self.max_bbox_width)) # swap the order here if necessary + cropped_resized_img = crop_transform(resized_img) + + cropped_images.append(cropped_resized_img.permute(1, 2, 0)) + + cropped_out = torch.stack(cropped_images, dim=0) + + return (original_images, cropped_out, bounding_boxes, self.max_bbox_width, self.max_bbox_height, ) + +class BatchUncrop: + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "original_images": ("IMAGE",), + "cropped_images": ("IMAGE",), + "bboxes": (BBOX_TYPES,), + "border_blending": ("FLOAT", {"default": 0.25, "min": 0.0, "max": 1.0, "step": 0.01}, ), + "crop_rescale": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}), + "border_top": ("BOOLEAN", {"default": True}), + "border_bottom": ("BOOLEAN", {"default": True}), + "border_left": ("BOOLEAN", {"default": True}), + "border_right": ("BOOLEAN", {"default": True}), + } + } + + RETURN_TYPES = ("IMAGE",) + FUNCTION = "uncrop" + + CATEGORY = "KJNodes/masking" + + def uncrop(self, original_images, cropped_images, bboxes, border_blending, crop_rescale, border_top, border_bottom, border_left, border_right): + def inset_border(image, border_width, border_color, border_top, border_bottom, border_left, border_right): + draw = ImageDraw.Draw(image) + width, height = image.size + if border_top: + draw.rectangle((0, 0, width, border_width), fill=border_color) + if border_bottom: + draw.rectangle((0, height - border_width, width, height), fill=border_color) + if border_left: + draw.rectangle((0, 0, border_width, height), fill=border_color) + if border_right: + draw.rectangle((width - border_width, 0, width, height), fill=border_color) + return image + + if len(original_images) != len(cropped_images): + raise ValueError(f"The number of original_images ({len(original_images)}) and cropped_images ({len(cropped_images)}) should be the same") + + bboxes = normalize_bboxes(bboxes) + + # Ensure there are enough bboxes, but drop the excess if there are more bboxes than images + if len(bboxes) > len(original_images): + print(f"Warning: Dropping excess bounding boxes. Expected {len(original_images)}, but got {len(bboxes)}") + bboxes = bboxes[:len(original_images)] + elif len(bboxes) < len(original_images): + raise ValueError("There should be at least as many bboxes as there are original and cropped images") + + input_images = tensor2pil(original_images) + crop_imgs = tensor2pil(cropped_images) + + out_images = [] + for i in range(len(input_images)): + img = input_images[i] + crop = crop_imgs[i] + bbox = bboxes[i] + + # uncrop the image based on the bounding box + bb_x, bb_y, bb_width, bb_height = bbox + + paste_region = bbox_to_region((bb_x, bb_y, bb_width, bb_height), img.size) + + # scale factors + scale_x = crop_rescale + scale_y = crop_rescale + + # scaled paste_region + paste_region = (round(paste_region[0]*scale_x), round(paste_region[1]*scale_y), round(paste_region[2]*scale_x), round(paste_region[3]*scale_y)) + + # rescale the crop image to fit the paste_region + crop = crop.resize((round(paste_region[2]-paste_region[0]), round(paste_region[3]-paste_region[1]))) + crop_img = crop.convert("RGB") + + if border_blending > 1.0: + border_blending = 1.0 + elif border_blending < 0.0: + border_blending = 0.0 + + blend_ratio = (max(crop_img.size) / 2) * float(border_blending) + + blend = img.convert("RGBA") + mask = Image.new("L", img.size, 0) + + mask_block = Image.new("L", (paste_region[2]-paste_region[0], paste_region[3]-paste_region[1]), 255) + mask_block = inset_border(mask_block, round(blend_ratio / 2), (0), border_top, border_bottom, border_left, border_right) + + mask.paste(mask_block, paste_region) + blend.paste(crop_img, paste_region) + + mask = mask.filter(ImageFilter.BoxBlur(radius=blend_ratio / 4)) + mask = mask.filter(ImageFilter.GaussianBlur(radius=blend_ratio / 4)) + + blend.putalpha(mask) + img = Image.alpha_composite(img.convert("RGBA"), blend) + out_images.append(img.convert("RGB")) + + return (pil2tensor(out_images),) + +class BatchCropFromMaskAdvanced: + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "original_images": ("IMAGE",), + "masks": ("MASK",), + "crop_size_mult": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}), + "bbox_smooth_alpha": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}), + }, + } + + RETURN_TYPES = ( + "IMAGE", + "IMAGE", + "MASK", + "IMAGE", + "MASK", + "BBOX", + "BBOX", + "INT", + "INT", + ) + RETURN_NAMES = ( + "original_images", + "cropped_images", + "cropped_masks", + "combined_crop_image", + "combined_crop_masks", + "bboxes", + "combined_bounding_box", + "bbox_width", + "bbox_height", + ) + FUNCTION = "crop" + CATEGORY = "KJNodes/masking" + + def smooth_bbox_size(self, prev_bbox_size, curr_bbox_size, alpha): + return round(alpha * curr_bbox_size + (1 - alpha) * prev_bbox_size) + + def smooth_center(self, prev_center, curr_center, alpha=0.5): + return (round(alpha * curr_center[0] + (1 - alpha) * prev_center[0]), + round(alpha * curr_center[1] + (1 - alpha) * prev_center[1])) + + def crop(self, masks, original_images, crop_size_mult, bbox_smooth_alpha): + bounding_boxes = [] + combined_bounding_box = [] + cropped_images = [] + cropped_masks = [] + cropped_masks_out = [] + combined_crop_out = [] + combined_cropped_images = [] + combined_cropped_masks = [] + + def calculate_bbox(mask): + non_zero_indices = np.nonzero(np.array(mask)) + + # handle empty masks + min_x, max_x, min_y, max_y = 0, 0, 0, 0 + if len(non_zero_indices[1]) > 0 and len(non_zero_indices[0]) > 0: + min_x, max_x = np.min(non_zero_indices[1]), np.max(non_zero_indices[1]) + min_y, max_y = np.min(non_zero_indices[0]), np.max(non_zero_indices[0]) + + width = max_x - min_x + height = max_y - min_y + bbox_size = max(width, height) + return min_x, max_x, min_y, max_y, bbox_size + + combined_mask = torch.max(masks, dim=0)[0] + _mask = tensor2pil(combined_mask)[0] + new_min_x, new_max_x, new_min_y, new_max_y, combined_bbox_size = calculate_bbox(_mask) + center_x = (new_min_x + new_max_x) / 2 + center_y = (new_min_y + new_max_y) / 2 + half_box_size = round(combined_bbox_size // 2) + new_min_x = max(0, round(center_x - half_box_size)) + new_max_x = min(original_images[0].shape[1], round(center_x + half_box_size)) + new_min_y = max(0, round(center_y - half_box_size)) + new_max_y = min(original_images[0].shape[0], round(center_y + half_box_size)) + + combined_bounding_box.append((new_min_x, new_min_y, new_max_x - new_min_x, new_max_y - new_min_y)) + + self.max_bbox_size = 0 + + # First, calculate the maximum bounding box size across all masks + curr_max_bbox_size = max(calculate_bbox(tensor2pil(mask)[0])[-1] for mask in masks) + # Smooth the changes in the bounding box size + self.max_bbox_size = self.smooth_bbox_size(self.max_bbox_size, curr_max_bbox_size, bbox_smooth_alpha) + # Apply the crop size multiplier + self.max_bbox_size = round(self.max_bbox_size * crop_size_mult) + # Make sure max_bbox_size is divisible by 16, if not, round it upwards so it is + self.max_bbox_size = math.ceil(self.max_bbox_size / 16) * 16 + + if self.max_bbox_size > original_images[0].shape[0] or self.max_bbox_size > original_images[0].shape[1]: + # max_bbox_size can only be as big as our input's width or height, and it has to be even + self.max_bbox_size = math.floor(min(original_images[0].shape[0], original_images[0].shape[1]) / 2) * 2 + + # Then, for each mask and corresponding image... + for i, (mask, img) in enumerate(zip(masks, original_images)): + _mask = tensor2pil(mask)[0] + non_zero_indices = np.nonzero(np.array(_mask)) + + # check for empty masks + if len(non_zero_indices[0]) > 0 and len(non_zero_indices[1]) > 0: + min_x, max_x = np.min(non_zero_indices[1]), np.max(non_zero_indices[1]) + min_y, max_y = np.min(non_zero_indices[0]), np.max(non_zero_indices[0]) + + # Calculate center of bounding box + center_x = np.mean(non_zero_indices[1]) + center_y = np.mean(non_zero_indices[0]) + curr_center = (round(center_x), round(center_y)) + + # If this is the first frame, initialize prev_center with curr_center + if not hasattr(self, 'prev_center'): + self.prev_center = curr_center + + # Smooth the changes in the center coordinates from the second frame onwards + if i > 0: + center = self.smooth_center(self.prev_center, curr_center, bbox_smooth_alpha) + else: + center = curr_center + + # Update prev_center for the next frame + self.prev_center = center + + # Create bounding box using max_bbox_size + half_box_size = self.max_bbox_size // 2 + min_x = max(0, center[0] - half_box_size) + max_x = min(img.shape[1], center[0] + half_box_size) + min_y = max(0, center[1] - half_box_size) + max_y = min(img.shape[0], center[1] + half_box_size) + + # Append bounding box coordinates + bounding_boxes.append((min_x, min_y, max_x - min_x, max_y - min_y)) + + # Crop the image from the bounding box + cropped_img = img[min_y:max_y, min_x:max_x, :] + cropped_mask = mask[min_y:max_y, min_x:max_x] + + # Resize the cropped image to a fixed size + new_size = max(cropped_img.shape[0], cropped_img.shape[1]) + resize_transform = Resize(new_size, interpolation=InterpolationMode.NEAREST, max_size=max(img.shape[0], img.shape[1])) + resized_mask = resize_transform(cropped_mask.unsqueeze(0).unsqueeze(0)).squeeze(0).squeeze(0) + resized_img = resize_transform(cropped_img.permute(2, 0, 1)) + # Perform the center crop to the desired size + # Constrain the crop to the smaller of our bbox or our image so we don't expand past the image dimensions. + crop_transform = CenterCrop((min(self.max_bbox_size, resized_img.shape[1]), min(self.max_bbox_size, resized_img.shape[2]))) + + cropped_resized_img = crop_transform(resized_img) + cropped_images.append(cropped_resized_img.permute(1, 2, 0)) + + cropped_resized_mask = crop_transform(resized_mask) + cropped_masks.append(cropped_resized_mask) + + combined_cropped_img = original_images[i][new_min_y:new_max_y, new_min_x:new_max_x, :] + combined_cropped_images.append(combined_cropped_img) + + combined_cropped_mask = masks[i][new_min_y:new_max_y, new_min_x:new_max_x] + combined_cropped_masks.append(combined_cropped_mask) + else: + bounding_boxes.append((0, 0, img.shape[1], img.shape[0])) + cropped_images.append(img) + cropped_masks.append(mask) + combined_cropped_images.append(img) + combined_cropped_masks.append(mask) + + cropped_out = torch.stack(cropped_images, dim=0) + combined_crop_out = torch.stack(combined_cropped_images, dim=0) + cropped_masks_out = torch.stack(cropped_masks, dim=0) + combined_crop_mask_out = torch.stack(combined_cropped_masks, dim=0) + + return (original_images, cropped_out, cropped_masks_out, combined_crop_out, combined_crop_mask_out, bounding_boxes, combined_bounding_box, self.max_bbox_size, self.max_bbox_size) + +class FilterZeroMasksAndCorrespondingImages: + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "masks": ("MASK",), + }, + "optional": { + "original_images": ("IMAGE",), + }, + } + + RETURN_TYPES = ("MASK", "IMAGE", "IMAGE", "INDEXES",) + RETURN_NAMES = ("non_zero_masks_out", "non_zero_mask_images_out", "zero_mask_images_out", "zero_mask_images_out_indexes",) + FUNCTION = "filter" + CATEGORY = "KJNodes/masking" + DESCRIPTION = """ +Filter out all the empty (i.e. all zero) mask in masks +Also filter out all the corresponding images in original_images by indexes if provide + +original_images (optional): If provided, need have same length as masks. +""" + + def filter(self, masks, original_images=None): + non_zero_masks = [] + non_zero_mask_images = [] + zero_mask_images = [] + zero_mask_images_indexes = [] + + masks_num = len(masks) + also_process_images = False + if original_images is not None: + imgs_num = len(original_images) + if len(original_images) == masks_num: + also_process_images = True + else: + print(f"[WARNING] ignore input: original_images, due to number of original_images ({imgs_num}) is not equal to number of masks ({masks_num})") + + for i in range(masks_num): + non_zero_num = np.count_nonzero(np.array(masks[i])) + if non_zero_num > 0: + non_zero_masks.append(masks[i]) + if also_process_images: + non_zero_mask_images.append(original_images[i]) + else: + zero_mask_images.append(original_images[i]) + zero_mask_images_indexes.append(i) + + non_zero_masks_out = torch.stack(non_zero_masks, dim=0) + non_zero_mask_images_out = zero_mask_images_out = zero_mask_images_out_indexes = None + + if also_process_images: + non_zero_mask_images_out = torch.stack(non_zero_mask_images, dim=0) + if len(zero_mask_images) > 0: + zero_mask_images_out = torch.stack(zero_mask_images, dim=0) + zero_mask_images_out_indexes = zero_mask_images_indexes + + return (non_zero_masks_out, non_zero_mask_images_out, zero_mask_images_out, zero_mask_images_out_indexes) + +class InsertImageBatchByIndexes: + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "images": ("IMAGE",), + "images_to_insert": ("IMAGE",), + "insert_indexes": ("INDEXES",), + }, + } + + RETURN_TYPES = ("IMAGE", ) + RETURN_NAMES = ("images_after_insert", ) + FUNCTION = "insert" + CATEGORY = "KJNodes/image" + DESCRIPTION = """ +This node is designed to be use with node FilterZeroMasksAndCorrespondingImages +It inserts the images_to_insert into images according to insert_indexes + +Returns: + images_after_insert: updated original images with origonal sequence order +""" + + def insert(self, images, images_to_insert, insert_indexes): + images_after_insert = images + + if images_to_insert is not None and insert_indexes is not None: + images_to_insert_num = len(images_to_insert) + insert_indexes_num = len(insert_indexes) + if images_to_insert_num == insert_indexes_num: + images_after_insert = [] + + i_images = 0 + for i in range(len(images) + images_to_insert_num): + if i in insert_indexes: + images_after_insert.append(images_to_insert[insert_indexes.index(i)]) + else: + images_after_insert.append(images[i_images]) + i_images += 1 + + images_after_insert = torch.stack(images_after_insert, dim=0) + + else: + print(f"[WARNING] skip this node, due to number of images_to_insert ({images_to_insert_num}) is not equal to number of insert_indexes ({insert_indexes_num})") + + + return (images_after_insert, ) + +class BatchUncropAdvanced: + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "original_images": ("IMAGE",), + "cropped_images": ("IMAGE",), + "cropped_masks": ("MASK",), + "combined_crop_mask": ("MASK",), + "bboxes": (BBOX_TYPES,), + "border_blending": ("FLOAT", {"default": 0.25, "min": 0.0, "max": 1.0, "step": 0.01}, ), + "crop_rescale": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}), + "use_combined_mask": ("BOOLEAN", {"default": False}), + "use_square_mask": ("BOOLEAN", {"default": True}), + }, + "optional": { + "combined_bounding_box": (BBOX_TYPES, {"default": None}), + }, + } + + RETURN_TYPES = ("IMAGE",) + FUNCTION = "uncrop" + CATEGORY = "KJNodes/masking" + + + def uncrop(self, original_images, cropped_images, cropped_masks, combined_crop_mask, bboxes, border_blending, crop_rescale, use_combined_mask, use_square_mask, combined_bounding_box = None): + + def inset_border(image, border_width=20, border_color=(0)): + width, height = image.size + bordered_image = Image.new(image.mode, (width, height), border_color) + bordered_image.paste(image, (0, 0)) + draw = ImageDraw.Draw(bordered_image) + draw.rectangle((0, 0, width - 1, height - 1), outline=border_color, width=border_width) + return bordered_image + + if len(original_images) != len(cropped_images): + raise ValueError(f"The number of original_images ({len(original_images)}) and cropped_images ({len(cropped_images)}) should be the same") + + bboxes = normalize_bboxes(bboxes) + if combined_bounding_box is not None: + combined_bounding_box = normalize_bboxes(combined_bounding_box) + + # Ensure there are enough bboxes, but drop the excess if there are more bboxes than images + if len(bboxes) > len(original_images): + print(f"Warning: Dropping excess bounding boxes. Expected {len(original_images)}, but got {len(bboxes)}") + bboxes = bboxes[:len(original_images)] + elif len(bboxes) < len(original_images): + raise ValueError("There should be at least as many bboxes as there are original and cropped images") + + crop_imgs = tensor2pil(cropped_images) + input_images = tensor2pil(original_images) + out_images = [] + + for i in range(len(input_images)): + img = input_images[i] + crop = crop_imgs[i] + bbox = bboxes[i] + + if use_combined_mask: + bb_x, bb_y, bb_width, bb_height = combined_bounding_box[0] + paste_region = bbox_to_region((bb_x, bb_y, bb_width, bb_height), img.size) + mask = combined_crop_mask[i] + else: + bb_x, bb_y, bb_width, bb_height = bbox + paste_region = bbox_to_region((bb_x, bb_y, bb_width, bb_height), img.size) + mask = cropped_masks[i] + + # scale paste_region + scale_x = scale_y = crop_rescale + paste_region = (round(paste_region[0]*scale_x), round(paste_region[1]*scale_y), round(paste_region[2]*scale_x), round(paste_region[3]*scale_y)) + + # rescale the crop image to fit the paste_region + crop = crop.resize((round(paste_region[2]-paste_region[0]), round(paste_region[3]-paste_region[1]))) + crop_img = crop.convert("RGB") + + #border blending + if border_blending > 1.0: + border_blending = 1.0 + elif border_blending < 0.0: + border_blending = 0.0 + + blend_ratio = (max(crop_img.size) / 2) * float(border_blending) + blend = img.convert("RGBA") + + if use_square_mask: + mask = Image.new("L", img.size, 0) + mask_block = Image.new("L", (paste_region[2]-paste_region[0], paste_region[3]-paste_region[1]), 255) + mask_block = inset_border(mask_block, round(blend_ratio / 2), (0)) + mask.paste(mask_block, paste_region) + else: + original_mask = tensor2pil(mask)[0] + original_mask = original_mask.resize((paste_region[2]-paste_region[0], paste_region[3]-paste_region[1])) + mask = Image.new("L", img.size, 0) + mask.paste(original_mask, paste_region) + + mask = mask.filter(ImageFilter.BoxBlur(radius=blend_ratio / 4)) + mask = mask.filter(ImageFilter.GaussianBlur(radius=blend_ratio / 4)) + + blend.paste(crop_img, paste_region) + blend.putalpha(mask) + + img = Image.alpha_composite(img.convert("RGBA"), blend) + out_images.append(img.convert("RGB")) + + return (pil2tensor(out_images),) + +class SplitBboxes: + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "bboxes": (BBOX_TYPES,), + "index": ("INT", {"default": 0,"min": 0, "max": 99999999, "step": 1}), + }, + } + + RETURN_TYPES = ("BBOX","BBOX",) + RETURN_NAMES = ("bboxes_a","bboxes_b",) + FUNCTION = "splitbbox" + CATEGORY = "KJNodes/masking" + DESCRIPTION = """ +Splits the specified bbox list at the given index into two lists. +""" + + def splitbbox(self, bboxes, index): + bboxes = normalize_bboxes(bboxes) + bboxes_a = bboxes[:index] # Sub-list from the start of bboxes up to (but not including) the index + bboxes_b = bboxes[index:] # Sub-list from the index to the end of bboxes + + return (bboxes_a, bboxes_b,) + +class BboxToInt: + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "bboxes": (BBOX_TYPES,), + "index": ("INT", {"default": 0,"min": 0, "max": 99999999, "step": 1}), + }, + } + + RETURN_TYPES = ("INT","INT","INT","INT","INT","INT",) + RETURN_NAMES = ("x_min","y_min","width","height", "center_x","center_y",) + FUNCTION = "bboxtoint" + CATEGORY = "KJNodes/masking" + DESCRIPTION = """ +Returns selected index from bounding box list as integers. +""" + def bboxtoint(self, bboxes, index): + x_min, y_min, width, height = normalize_bboxes(bboxes)[index] + center_x = int(x_min + width / 2) + center_y = int(y_min + height / 2) + + return (x_min, y_min, width, height, center_x, center_y,) + +class BboxVisualize: + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "images": ("IMAGE",), + "bboxes": (BBOX_TYPES,), + "line_width": ("INT", {"default": 1,"min": 1, "max": 10, "step": 1}), + "bbox_format": (["xywh", "xyxy"], {"default": "xywh"}), + }, + } + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("images",) + FUNCTION = "visualizebbox" + DESCRIPTION = """ +Visualizes the specified bbox on the image. +""" + + CATEGORY = "KJNodes/masking" + + def visualizebbox(self, bboxes, images, line_width, bbox_format): + image_list = [] + for image, bbox in zip(images, normalize_bboxes(bboxes, bbox_format)): + # Ensure bbox is a sequence of 4 values + if isinstance(bbox, (list, tuple, np.ndarray)) and len(bbox) == 4: + if bbox_format == "xywh": + x_min, y_min, width, height = bbox + elif bbox_format == "xyxy": + x_min, y_min, x_max, y_max = bbox + width = x_max - x_min + height = y_max - y_min + else: + raise ValueError(f"Unknown bbox_format: {bbox_format}") + else: + print("Invalid bbox:", bbox) + continue + + # Ensure bbox coordinates are integers + x_min = int(x_min) + y_min = int(y_min) + width = int(width) + height = int(height) + + # Permute the image dimensions + image = image.permute(2, 0, 1) + + # Clone the image to draw bounding boxes + img_with_bbox = image.clone() + + # Define the color for the bbox, e.g., red + color = torch.tensor([1, 0, 0], dtype=torch.float32) + + # Ensure color tensor matches the image channels + if color.shape[0] != img_with_bbox.shape[0]: + color = color.unsqueeze(1).expand(-1, line_width) + + # Draw lines for each side of the bbox with the specified line width + for lw in range(line_width): + # Top horizontal line + if y_min + lw < img_with_bbox.shape[1]: + img_with_bbox[:, y_min + lw, x_min:x_min + width] = color[:, None] + + # Bottom horizontal line + if y_min + height - lw < img_with_bbox.shape[1]: + img_with_bbox[:, y_min + height - lw, x_min:x_min + width] = color[:, None] + + # Left vertical line + if x_min + lw < img_with_bbox.shape[2]: + img_with_bbox[:, y_min:y_min + height, x_min + lw] = color[:, None] + + # Right vertical line + if x_min + width - lw < img_with_bbox.shape[2]: + img_with_bbox[:, y_min:y_min + height, x_min + width - lw] = color[:, None] + + # Permute the image dimensions back + img_with_bbox = img_with_bbox.permute(1, 2, 0).unsqueeze(0) + image_list.append(img_with_bbox) + + return (torch.cat(image_list, dim=0),) \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/context_windows_visualizer.py b/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/context_windows_visualizer.py new file mode 100644 index 0000000000000000000000000000000000000000..1f3f2be17e1e07796c58f6d64d28d5aec7964887 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/context_windows_visualizer.py @@ -0,0 +1,162 @@ +import inspect +import math + +from comfy_api.latest import io + +try: + import comfy.context_windows as context_windows +except ImportError: + context_windows = None + +SCHEDULE_OPTIONS = ["standard_static", "standard_static_balanced", "standard_uniform", "looped_uniform", "batched", "batched_shifted"] +FUSE_OPTIONS = ["pyramid", "relative", "flat", "overlap-linear", "hann", "gaussian"] + + +def _ordered_halving(val): + # low-discrepancy fraction in [0, 1) from bit-reversal of the step index (core's ordered_halving) + return int(f"{int(val):064b}"[::-1], 2) / (1 << 64) + + +def create_windows_batched_shifted(num_frames, handler, model_options=None): + # zero-overlap partition whose seams roll each step (no fixed boundary error); ~1.0x compute + F = int(num_frames) + L = int(handler.context_length) + if F <= L: + return [list(range(F))] + offset = round(F * _ordered_halving(handler._step)) % L # 0 at step 0 (plain batched) + windows = [] + start = 0 + if offset > 0: + windows.append(list(range(0, offset))) # leading partial + start = offset + while start < F: + end = min(start + L, F) + windows.append(list(range(start, end))) # trailing partial when unaligned + start = end + return windows + + +def create_windows_static_balanced(num_frames, handler, model_options=None): + # standard_static window count, but spread evenly so every seam is uniform (±1 frame) + F = int(num_frames) + L = int(handler.context_length) + overlap = int(handler.context_overlap) + if F <= L: + return [list(range(F))] + stride_target = max(1, L - overlap) + N = -(-(F - L) // stride_target) + 1 # ceil((F-L)/stride_target)+1; stride <= L, no gaps + windows = [] + for i in range(N): + start = math.floor(i * (F - L) / (N - 1) + 0.5) + windows.append(list(range(start, start + L))) + return windows + + +def create_weights_hann(length, full_length=None, idxs=None, handler=None, **kwargs): + # smooth overlap-linear: flat interior, raised-cosine ramps in the overlaps (sum to 1) + weights = [1.0] * length + overlap = min(max(int(getattr(handler, "context_overlap", 0) or 0), 0), length) + if overlap >= 1 and idxs: + denom = max(overlap - 1, 1) + if min(idxs) > 0: # left ramp, skipped on first window + for j in range(overlap): + weights[j] = max(0.5 * (1 - math.cos(math.pi * j / denom)), 1e-37) + if full_length is not None and max(idxs) < full_length - 1: # right ramp, skipped on last + for j in range(overlap): + weights[length - overlap + j] = max(0.5 * (1 - math.cos(math.pi * (denom - j) / denom)), 1e-37) + return weights + + +def create_weights_gaussian(length, **kwargs): + # smooth bell over the window (smooth twin of pyramid) + if length <= 1: + return [1.0] * length + c = (length - 1) / 2.0 + std = max(length / 4.0, 1e-6) + return [math.exp(-0.5 * ((i - c) / std) ** 2) for i in range(length)] + + +LOCAL_FUSE = {"hann": create_weights_hann, "gaussian": create_weights_gaussian} +LOCAL_SCHEDULES = { + "standard_static_balanced": create_windows_static_balanced, + "batched_shifted": create_windows_batched_shifted, +} + + +class ContextWindowsVisualizerKJ(io.ComfyNode): + @classmethod + def define_schema(cls): + return io.Schema( + node_id="ContextWindowsVisualizerKJ", + display_name="Context Windows Visualizer (KJ)", + category="KJNodes/misc", + description="Interactive visualization of ComfyUI context windows: window placement, overlap, " + "fuse weights and per-frame blending, updated live. Optionally applies the settings " + "to a model like the core Context Windows (Manual) node.", + is_experimental=True, + inputs=[ + io.Model.Input("model", optional=True, tooltip="Optional model to apply the context windows to during sampling."), + io.Combo.Input("frame_units", options=["pixel", "latent"], default="pixel", + tooltip="Units for the frame counts below. 'pixel' converts to latent via ((n - 1) // temporal_downscale) + 1."), + io.Int.Input("dim", default=2, min=0, max=5, tooltip="Latent temporal dimension: 2 for WAN-like, 0 for AnimateDiff-style. Model path only.", advanced=True), + io.Int.Input("temporal_downscale", default=4, min=1, max=16, + tooltip="VAE temporal compression for pixel->latent. WAN/Hunyuan/CogVideoX=4, Mochi=6, LTXV=8."), + io.Int.Input("num_frames", default=161, min=1, max=100000, tooltip="Total frames to visualize. Ignored on the model path (the real latent length is used)."), + io.Int.Input("context_length", default=81, min=1, max=100000, tooltip="Window length."), + io.Int.Input("context_overlap", default=30, min=0, max=100000, tooltip="Overlap between windows."), + io.Combo.Input("context_schedule", options=SCHEDULE_OPTIONS, tooltip="Window placement. standard_static_balanced: static with windows spread evenly (uniform seams, any frame count). batched_shifted: zero-overlap windows whose seams roll each step (~1.0x compute, pairs with freenoise). Uniform schedules also shift per step."), + io.Int.Input("context_stride", default=1, min=1, max=32, tooltip="Max stride power for uniform schedules (adds dilated windows: 1, 2, 4...)."), + io.Boolean.Input("closed_loop", default=False, tooltip="Wrap looped-schedule windows around the end back to frame 0."), + io.Combo.Input("fuse_method", options=FUSE_OPTIONS, default="pyramid", tooltip="How overlapping windows are blended."), + io.Boolean.Input("causal_window_fix", default=False, tooltip="Prepend an anchor frame to non-0 windows, stripped after the model call.", advanced=True), + io.Boolean.Input("freenoise", default=True, tooltip="FreeNoise noise shuffling for smoother blending. Model path only.", advanced=True), + io.String.Input("cond_retain_index_list", default="", tooltip="Comma-separated window-relative indices that keep their original conditioning, e.g. '0' reuses the start frame's cond per window. No viz effect; model path only.", advanced=True), + ], + outputs=[ + io.Model.Output(display_name="model", tooltip="Model with context windows applied; only valid when a model is connected."), + ], + ) + + @classmethod + def execute(cls, frame_units, temporal_downscale, num_frames, context_length, context_overlap, context_schedule, context_stride, + closed_loop, fuse_method, dim, causal_window_fix, freenoise, cond_retain_index_list="", model=None) -> io.NodeOutput: + if model is None: + return io.NodeOutput(None) + if context_windows is None: + raise Exception("comfy.context_windows not available in this ComfyUI version.") + if frame_units == "pixel": + f = max(int(temporal_downscale), 1) + context_length = max(((context_length - 1) // f) + 1, 1) + context_overlap = max(((context_overlap - 1) // f) + 1, 0) + # build local schedules/fuse methods (not in core) as Context* objects + if context_schedule in LOCAL_SCHEDULES: + schedule = context_windows.ContextSchedule(context_schedule, LOCAL_SCHEDULES[context_schedule]) + else: + schedule = context_windows.get_matching_context_schedule(context_schedule) + if fuse_method in LOCAL_FUSE: + fuse = context_windows.ContextFuseMethod(fuse_method, LOCAL_FUSE[fuse_method]) + else: + fuse = context_windows.get_matching_fuse_method(fuse_method) + model = model.clone() + handler_kwargs = dict( + context_schedule=schedule, + fuse_method=fuse, + context_length=context_length, + context_overlap=context_overlap, + context_stride=context_stride, + closed_loop=closed_loop, + dim=dim, + freenoise=freenoise, + causal_window_fix=causal_window_fix, + cond_retain_index_list=cond_retain_index_list, + ) + # drop kwargs this ComfyUI version's handler doesn't accept + sig = inspect.signature(context_windows.IndexListContextHandler.__init__) + handler_kwargs = {k: v for k, v in handler_kwargs.items() if k in sig.parameters} + handler = context_windows.IndexListContextHandler(**handler_kwargs) + model.model_options["context_handler"] = handler + # scope the vram estimate to the context window length + context_windows.create_prepare_sampling_wrapper(model) + if freenoise and hasattr(context_windows, "create_sampler_sample_wrapper"): + context_windows.create_sampler_sample_wrapper(model) + return io.NodeOutput(model) diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/curve_nodes.py b/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/curve_nodes.py new file mode 100644 index 0000000000000000000000000000000000000000..35dc3558790ca2f5e7860c04827e47a012bdebc7 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/curve_nodes.py @@ -0,0 +1,1642 @@ +import torch +from torchvision import transforms +import json +from PIL import Image, ImageDraw, ImageFont, ImageFilter +import numpy as np +from ..utility.utility import pil2tensor, tensor2pil +import folder_paths +import base64 +from io import BytesIO + + +def parse_color(color): + if isinstance(color, str) and ',' in color: + return tuple(int(c.strip()) for c in color.split(',')) + return color + +def parse_json_tracks(tracks): + tracks_data = [] + try: + # If tracks is a string, try to parse it as JSON + if isinstance(tracks, str): + parsed = json.loads(tracks.replace("'", '"')) + tracks_data.extend(parsed) + else: + # If tracks is a list of strings, parse each one + for track_str in tracks: + parsed = json.loads(track_str.replace("'", '"')) + tracks_data.append(parsed) + + # Check if we have a single track (dict with x,y) or a list of tracks + if tracks_data and isinstance(tracks_data[0], dict) and 'x' in tracks_data[0]: + # Single track detected, wrap it in a list + tracks_data = [tracks_data] + elif tracks_data and isinstance(tracks_data[0], list) and tracks_data[0] and isinstance(tracks_data[0][0], dict) and 'x' in tracks_data[0][0]: + # Already a list of tracks, nothing to do + pass + else: + # Unexpected format + print(f"Warning: Unexpected track format: {type(tracks_data[0])}") + + except json.JSONDecodeError as e: + print(f"Error parsing tracks JSON: {e}") + tracks_data = [] + + return tracks_data + +def plot_coordinates_to_tensor(coordinates, height, width, bbox_height, bbox_width, size_multiplier, prompt): + import matplotlib + matplotlib.use('Agg') + from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas + text_color = '#999999' + bg_color = '#353535' + matplotlib.pyplot.rcParams['text.color'] = text_color + fig, ax = matplotlib.pyplot.subplots(figsize=(width/100, height/100), dpi=100) + fig.patch.set_facecolor(bg_color) + ax.set_facecolor(bg_color) + ax.grid(color=text_color, linestyle='-', linewidth=0.5) + ax.set_xlabel('x', color=text_color) + ax.set_ylabel('y', color=text_color) + for text in ax.get_xticklabels() + ax.get_yticklabels(): + text.set_color(text_color) + ax.set_title('position for: ' + prompt) + ax.set_xlabel('X Coordinate') + ax.set_ylabel('Y Coordinate') + #ax.legend().remove() + ax.set_xlim(0, width) # Set the x-axis to match the input latent width + ax.set_ylim(height, 0) # Set the y-axis to match the input latent height, with (0,0) at top-left + # Adjust the margins of the subplot + matplotlib.pyplot.subplots_adjust(left=0.08, right=0.95, bottom=0.05, top=0.95, wspace=0.2, hspace=0.2) + + cmap = matplotlib.pyplot.get_cmap('rainbow') + image_batch = [] + canvas = FigureCanvas(fig) + width, height = fig.get_size_inches() * fig.get_dpi() + # Draw a box at each coordinate + for i, ((x, y), size) in enumerate(zip(coordinates, size_multiplier)): + color_index = i / (len(coordinates) - 1) + color = cmap(color_index) + draw_height = bbox_height * size + draw_width = bbox_width * size + rect = matplotlib.patches.Rectangle((x - draw_width/2, y - draw_height/2), draw_width, draw_height, + linewidth=1, edgecolor=color, facecolor='none', alpha=0.5) + ax.add_patch(rect) + + # Check if there is a next coordinate to draw an arrow to + if i < len(coordinates) - 1: + x1, y1 = coordinates[i] + x2, y2 = coordinates[i + 1] + ax.annotate("", xy=(x2, y2), xytext=(x1, y1), + arrowprops=dict(arrowstyle="->", + linestyle="-", + lw=1, + color=color, + mutation_scale=20)) + canvas.draw() + try: + image_np = np.frombuffer(canvas.tostring_rgb(), dtype='uint8').reshape(int(height), int(width), 3).copy() + except AttributeError: + image_np = np.frombuffer(canvas.tostring_argb(), dtype='uint8').reshape(int(height), int(width), 4) + image_np = image_np[:, :, [1, 2, 3]] # Convert ARGB to RGB + image_tensor = torch.from_numpy(image_np).float() / 255.0 + image_tensor = image_tensor.unsqueeze(0) + image_batch.append(image_tensor) + + matplotlib.pyplot.close(fig) + image_batch_tensor = torch.cat(image_batch, dim=0) + + return image_batch_tensor + +class PlotCoordinates: + @classmethod + def INPUT_TYPES(s): + return {"required": { + "coordinates": ("STRING", {"forceInput": True}), + "text": ("STRING", {"default": 'title', "multiline": False}), + "width": ("INT", {"default": 512, "min": 8, "max": 4096, "step": 8}), + "height": ("INT", {"default": 512, "min": 8, "max": 4096, "step": 8}), + "bbox_width": ("INT", {"default": 128, "min": 8, "max": 4096, "step": 8}), + "bbox_height": ("INT", {"default": 128, "min": 8, "max": 4096, "step": 8}), + }, + "optional": {"size_multiplier": ("FLOAT", {"default": [1.0], "forceInput": True})}, + } + RETURN_TYPES = ("IMAGE", "INT", "INT", "INT", "INT",) + RETURN_NAMES = ("images", "width", "height", "bbox_width", "bbox_height",) + FUNCTION = "append" + CATEGORY = "KJNodes/experimental" + DESCRIPTION = """ +Plots coordinates to sequence of images using Matplotlib. + +""" + + def append(self, coordinates, text, width, height, bbox_width, bbox_height, size_multiplier=[1.0]): + coordinates = json.loads(coordinates.replace("'", '"')) + coordinates = [(coord['x'], coord['y']) for coord in coordinates] + batch_size = len(coordinates) + if not size_multiplier or len(size_multiplier) != batch_size: + size_multiplier = [0] * batch_size + else: + size_multiplier = size_multiplier * (batch_size // len(size_multiplier)) + size_multiplier[:batch_size % len(size_multiplier)] + + plot_image_tensor = plot_coordinates_to_tensor(coordinates, height, width, bbox_height, bbox_width, size_multiplier, text) + + return (plot_image_tensor, width, height, bbox_width, bbox_height) + +class SplineEditor: + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "points_store": ("STRING", {"multiline": False, "advanced": True}), + "coordinates": ("STRING", {"multiline": False, "advanced": True}), + "mask_width": ("INT", {"default": 512, "min": 8, "max": 4096, "step": 8}), + "mask_height": ("INT", {"default": 512, "min": 8, "max": 4096, "step": 8}), + "points_to_sample": ("INT", {"default": 16, "min": 2, "max": 1000, "step": 1}), + "sampling_method": ( + [ + 'path', + 'time', + 'controlpoints', + 'speed' + ], + { + "default": 'time' + }), + "interpolation": ( + [ + 'cardinal', + 'monotone', + 'basis', + 'linear', + 'step-before', + 'step-after', + 'polar', + 'polar-reverse', + 'bezier', + ], + { + "default": 'cardinal' + }), + "tension": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}), + "repeat_output": ("INT", {"default": 1, "min": 1, "max": 4096, "step": 1}), + "float_output_type": ( + [ + 'list', + 'pandas series', + 'tensor', + ], + { + "default": 'list' + }), + }, + "optional": { + "min_value": ("FLOAT", {"default": 0.0, "min": -10000.0, "max": 10000.0, "step": 0.01}), + "max_value": ("FLOAT", {"default": 1.0, "min": -10000.0, "max": 10000.0, "step": 0.01}), + "bg_image": ("IMAGE", ), + } + } + + RETURN_TYPES = ("MASK", "STRING", "FLOAT", "INT", "STRING",) + RETURN_NAMES = ("mask", "coord_str", "float", "count", "normalized_str",) + FUNCTION = "splinedata" + CATEGORY = "KJNodes/weights" + DESCRIPTION = """ +# WORK IN PROGRESS +Do not count on this as part of your workflow yet, +probably contains lots of bugs and stability is not +guaranteed!! + +## Graphical editor to create values for various +## schedules and/or mask batches. + +**Shift + click** to add control point at end. +**Ctrl + click** to add control point (subdivide) between two points. +**Right click on a point** to delete it. +Note that you can't delete from start/end. + +Right click on canvas for context menu: +NEW!: +- Add new spline + - Creates a new spline on same canvas, currently these paths are only outputed + as coordinates. +- Add single point + - Creates a single point that only returns it's current position coords +- Delete spline + - Deletes the currently selected spline, you can select a spline by clicking on + it's path, or cycle through them with the 'Next spline' -option. + +These are purely visual options, doesn't affect the output: + - Toggle handles visibility + - Display sample points: display the points to be returned. + +**points_to_sample** value sets the number of samples +returned from the **drawn spline itself**, this is independent from the +actual control points, so the interpolation type matters. +sampling_method: + - time: samples along the time axis, used for schedules + - path: samples along the path itself, useful for coordinates + - controlpoints: samples only the control points themselves + +output types: + - mask batch + example compatible nodes: anything that takes masks + - list of floats + example compatible nodes: IPAdapter weights + - pandas series + example compatible nodes: anything that takes Fizz' + nodes Batch Value Schedule + - torch tensor + example compatible nodes: unknown +""" + + def splinedata(self, mask_width, mask_height, coordinates, float_output_type, interpolation, + points_to_sample, sampling_method, points_store, tension, repeat_output, + min_value=0.0, max_value=1.0, bg_image=None): + + coordinates = json.loads(coordinates) + + # Handle nested list structure if present + all_normalized = [] + all_normalized_y_values = [] + + # Check if we have a nested list structure + if isinstance(coordinates, list) and len(coordinates) > 0 and isinstance(coordinates[0], list): + # Process each list of coordinates in the nested structure + coordinate_sets = coordinates + else: + # If not nested, treat as a single list of coordinates + coordinate_sets = [coordinates] + + first_spline = coordinate_sets[0] if coordinate_sets else [] + + # Process each set of coordinates + for coord_set in coordinate_sets: + normalized = [] + normalized_y_values = [] + + for coord in coord_set: + coord['x'] = int(round(coord['x'])) + coord['y'] = int(round(coord['y'])) + norm_x = (1.0 - (coord['x'] / mask_height) - 0.0) * (max_value - min_value) + min_value + norm_y = (1.0 - (coord['y'] / mask_height) - 0.0) * (max_value - min_value) + min_value + normalized_y_values.append(norm_y) + normalized.append({'x':norm_x, 'y':norm_y}) + + all_normalized.extend(normalized) + all_normalized_y_values.extend(normalized_y_values) + + # Use the combined normalized values for output + if float_output_type == 'list': + out_floats = all_normalized_y_values * repeat_output + elif float_output_type == 'pandas series': + try: + import pandas as pd + except ImportError as e: + raise ImportError("MaskOrImageToWeight: pandas is not installed. Please install pandas to use this output_type") from e + out_floats = pd.Series(all_normalized_y_values * repeat_output), + elif float_output_type == 'tensor': + out_floats = torch.tensor(all_normalized_y_values * repeat_output, dtype=torch.float32) + + color_map = lambda y: torch.full((mask_height, mask_width, 3), y, dtype=torch.float32) + y_values_for_masks = normalized_y_values if normalized_y_values else [min_value] + mask_tensors = [color_map(y) for y in y_values_for_masks] + masks_out = torch.stack(mask_tensors) + masks_out = masks_out.repeat(repeat_output, 1, 1, 1) + masks_out = masks_out.mean(dim=-1) + + single_spline_count = len(first_spline) + + if bg_image is None: + return (masks_out, json.dumps(coordinates if len(coordinates) > 1 else coordinates[0]), out_floats, single_spline_count, json.dumps(all_normalized)) + else: + transform = transforms.ToPILImage() + image = transform(bg_image[0].permute(2, 0, 1)) + buffered = BytesIO() + image.save(buffered, format="JPEG", quality=75) + + # Encode the image bytes to a Base64 string + img_bytes = buffered.getvalue() + img_base64 = base64.b64encode(img_bytes).decode('utf-8') + + return { + "ui": {"bg_image": [img_base64]}, + "result": (masks_out, json.dumps(coordinates if len(coordinates) > 1 else coordinates[0]), out_floats, single_spline_count, json.dumps(all_normalized)) + } + + +class CreateShapeMaskOnPath: + + RETURN_TYPES = ("MASK", "MASK",) + RETURN_NAMES = ("mask", "mask_inverted",) + FUNCTION = "createshapemask" + CATEGORY = "KJNodes/masking/generate" + DESCRIPTION = """ +Creates a mask or batch of masks with the specified shape. +Locations are center locations. +""" + DEPRECATED = True + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "shape": ( + [ 'circle', + 'square', + 'triangle', + ], + { + "default": 'circle' + }), + "coordinates": ("STRING", {"forceInput": True}), + "frame_width": ("INT", {"default": 512,"min": 16, "max": 4096, "step": 1}), + "frame_height": ("INT", {"default": 512,"min": 16, "max": 4096, "step": 1}), + "shape_width": ("INT", {"default": 128,"min": 8, "max": 4096, "step": 1}), + "shape_height": ("INT", {"default": 128,"min": 8, "max": 4096, "step": 1}), + }, + "optional": { + "size_multiplier": ("FLOAT", {"default": [1.0], "forceInput": True}), + } + } + + def createshapemask(self, coordinates, frame_width, frame_height, shape_width, shape_height, shape, size_multiplier=[1.0]): + # Define the number of images in the batch + coordinates = coordinates.replace("'", '"') + coordinates = json.loads(coordinates) + + batch_size = len(coordinates) + out = [] + color = "white" + if not size_multiplier or len(size_multiplier) != batch_size: + size_multiplier = [0] * batch_size + else: + size_multiplier = size_multiplier * (batch_size // len(size_multiplier)) + size_multiplier[:batch_size % len(size_multiplier)] + for i, coord in enumerate(coordinates): + image = Image.new("RGB", (frame_width, frame_height), "black") + draw = ImageDraw.Draw(image) + + # Calculate the size for this frame and ensure it's not less than 0 + current_width = max(0, shape_width + i * size_multiplier[i]) + current_height = max(0, shape_height + i * size_multiplier[i]) + + location_x = coord['x'] + location_y = coord['y'] + + if shape == 'circle' or shape == 'square': + # Define the bounding box for the shape + left_up_point = (location_x - current_width // 2, location_y - current_height // 2) + right_down_point = (location_x + current_width // 2, location_y + current_height // 2) + two_points = [left_up_point, right_down_point] + + if shape == 'circle': + draw.ellipse(two_points, fill=color) + elif shape == 'square': + draw.rectangle(two_points, fill=color) + + elif shape == 'triangle': + # Define the points for the triangle + left_up_point = (location_x - current_width // 2, location_y + current_height // 2) # bottom left + right_down_point = (location_x + current_width // 2, location_y + current_height // 2) # bottom right + top_point = (location_x, location_y - current_height // 2) # top point + draw.polygon([top_point, left_up_point, right_down_point], fill=color) + + image = pil2tensor(image) + mask = image[:, :, :, 0] + out.append(mask) + outstack = torch.cat(out, dim=0) + return (outstack, 1.0 - outstack,) + + + +class CreateShapeImageOnPath: + + RETURN_TYPES = ("IMAGE", "MASK",) + RETURN_NAMES = ("image","mask", ) + FUNCTION = "createshapemask" + CATEGORY = "KJNodes/image" + DESCRIPTION = """ +Creates an image or batch of images with the specified shape. +Locations are center locations. +""" + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "shape": ( + [ 'circle', + 'square', + 'triangle', + ], + { + "default": 'circle' + }), + "coordinates": ("STRING", {"forceInput": True}), + "frame_width": ("INT", {"default": 512,"min": 16, "max": 4096, "step": 1}), + "frame_height": ("INT", {"default": 512,"min": 16, "max": 4096, "step": 1}), + "shape_width": ("INT", {"default": 128,"min": 2, "max": 4096, "step": 1}), + "shape_height": ("INT", {"default": 128,"min": 2, "max": 4096, "step": 1}), + "shape_color": ("STRING", {"default": 'white'}), + "bg_color": ("STRING", {"default": 'black'}), + "blur_radius": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 100, "step": 0.1}), + "intensity": ("FLOAT", {"default": 1.0, "min": 0.01, "max": 100.0, "step": 0.01}), + }, + "optional": { + "size_multiplier": ("FLOAT", {"default": [1.0], "forceInput": True}), + "trailing": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}), + "border_width": ("INT", {"default": 0, "min": 0, "max": 100, "step": 1}), + "border_color": ("STRING", {"default": 'black'}), + } + } + + def createshapemask(self, coordinates, frame_width, frame_height, shape_width, shape_height, shape_color, + bg_color, blur_radius, shape, intensity, size_multiplier=[1.0], trailing=1.0, border_width=0, border_color='black'): + + shape_color = parse_color(shape_color) + border_color = parse_color(border_color) + bg_color = parse_color(bg_color) + coords_list = parse_json_tracks(coordinates) + + batch_size = len(coords_list[0]) + images_list = [] + masks_list = [] + + if not size_multiplier or len(size_multiplier) != batch_size: + size_multiplier = [1] * batch_size + else: + size_multiplier = size_multiplier * (batch_size // len(size_multiplier)) + size_multiplier[:batch_size % len(size_multiplier)] + + previous_output = None + + for i in range(batch_size): + image = Image.new("RGB", (frame_width, frame_height), bg_color) + draw = ImageDraw.Draw(image) + + # Calculate the size for this frame and ensure it's not less than 0 + current_width = shape_width * size_multiplier[i] + current_height = shape_height * size_multiplier[i] + + for coords in coords_list: + location_x = coords[i]['x'] + location_y = coords[i]['y'] + + if shape == 'circle' or shape == 'square': + # Define the bounding box for the shape + left_up_point = (location_x - current_width // 2, location_y - current_height // 2) + right_down_point = (location_x + current_width // 2, location_y + current_height // 2) + two_points = [left_up_point, right_down_point] + + if shape == 'circle': + if border_width > 0: + draw.ellipse(two_points, fill=shape_color, outline=border_color, width=border_width) + else: + draw.ellipse(two_points, fill=shape_color) + elif shape == 'square': + if border_width > 0: + draw.rectangle(two_points, fill=shape_color, outline=border_color, width=border_width) + else: + draw.rectangle(two_points, fill=shape_color) + + elif shape == 'triangle': + # Define the points for the triangle + left_up_point = (location_x - current_width // 2, location_y + current_height // 2) # bottom left + right_down_point = (location_x + current_width // 2, location_y + current_height // 2) # bottom right + top_point = (location_x, location_y - current_height // 2) # top point + + if border_width > 0: + draw.polygon([top_point, left_up_point, right_down_point], fill=shape_color, outline=border_color, width=border_width) + else: + draw.polygon([top_point, left_up_point, right_down_point], fill=shape_color) + + if blur_radius != 0: + image = image.filter(ImageFilter.GaussianBlur(blur_radius)) + # Blend the current image with the accumulated image + + image = pil2tensor(image) + if trailing != 1.0 and previous_output is not None: + # Add the decayed previous output to the current frame + image += trailing * previous_output + image = image / image.max() + previous_output = image + image = image * intensity + mask = image[:, :, :, 0] + masks_list.append(mask) + images_list.append(image) + out_images = torch.cat(images_list, dim=0).cpu().float() + out_masks = torch.cat(masks_list, dim=0) + return (out_images, out_masks) + +class CreateTextOnPath: + + RETURN_TYPES = ("IMAGE", "MASK", "MASK",) + RETURN_NAMES = ("image", "mask", "mask_inverted",) + FUNCTION = "createtextmask" + CATEGORY = "KJNodes/masking/generate" + DESCRIPTION = """ +Creates a mask or batch of masks with the specified text. +Locations are center locations. +""" + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "coordinates": ("STRING", {"forceInput": True}), + "text": ("STRING", {"default": 'text', "multiline": True}), + "frame_width": ("INT", {"default": 512,"min": 16, "max": 4096, "step": 1}), + "frame_height": ("INT", {"default": 512,"min": 16, "max": 4096, "step": 1}), + "font": (folder_paths.get_filename_list("kjnodes_fonts"), ), + "font_size": ("INT", {"default": 42}), + "alignment": ( + [ 'left', + 'center', + 'right' + ], + {"default": 'center'} + ), + "text_color": ("STRING", {"default": 'white'}), + }, + "optional": { + "size_multiplier": ("FLOAT", {"default": [1.0], "forceInput": True}), + } + } + + def createtextmask(self, coordinates, frame_width, frame_height, font, font_size, text, text_color, alignment, size_multiplier=[1.0]): + coordinates = coordinates.replace("'", '"') + coordinates = json.loads(coordinates) + + batch_size = len(coordinates) + mask_list = [] + image_list = [] + color = parse_color(text_color) + font_path = folder_paths.get_full_path("kjnodes_fonts", font) + + if len(size_multiplier) != batch_size: + size_multiplier = size_multiplier * (batch_size // len(size_multiplier)) + size_multiplier[:batch_size % len(size_multiplier)] + + for i, coord in enumerate(coordinates): + image = Image.new("RGB", (frame_width, frame_height), "black") + draw = ImageDraw.Draw(image) + lines = text.split('\n') # Split the text into lines + # Apply the size multiplier to the font size for this iteration + current_font_size = int(font_size * size_multiplier[i]) + current_font = ImageFont.truetype(font_path, current_font_size) + line_heights = [current_font.getbbox(line)[3] for line in lines] # List of line heights + total_text_height = sum(line_heights) # Total height of text block + + # Calculate the starting Y position to center the block of text + start_y = coord['y'] - total_text_height // 2 + for j, line in enumerate(lines): + text_width, text_height = current_font.getbbox(line)[2], line_heights[j] + if alignment == 'left': + location_x = coord['x'] + elif alignment == 'center': + location_x = int(coord['x'] - text_width // 2) + elif alignment == 'right': + location_x = int(coord['x'] - text_width) + + location_y = int(start_y + sum(line_heights[:j])) + text_position = (location_x, location_y) + # Draw the text + try: + draw.text(text_position, line, fill=color, font=current_font, features=['-liga']) + except Exception: + draw.text(text_position, line, fill=color, font=current_font) + + image = pil2tensor(image) + non_black_pixels = (image > 0).any(dim=-1) + mask = non_black_pixels.to(image.dtype) + mask_list.append(mask) + image_list.append(image) + + out_images = torch.cat(image_list, dim=0).cpu().float() + out_masks = torch.cat(mask_list, dim=0) + return (out_images, out_masks, 1.0 - out_masks,) + +class CreateGradientFromCoords: + + RETURN_TYPES = ("IMAGE", ) + RETURN_NAMES = ("image", ) + FUNCTION = "generate" + CATEGORY = "KJNodes/image" + DESCRIPTION = """ +Creates a gradient image from coordinates. +""" + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "coordinates": ("STRING", {"forceInput": True}), + "frame_width": ("INT", {"default": 512,"min": 16, "max": 4096, "step": 1}), + "frame_height": ("INT", {"default": 512,"min": 16, "max": 4096, "step": 1}), + "start_color": ("STRING", {"default": 'white'}), + "end_color": ("STRING", {"default": 'black'}), + "multiplier": ("FLOAT", {"default": 1.0, "min": 0.01, "max": 100.0, "step": 0.01}), + }, + } + + def generate(self, coordinates, frame_width, frame_height, start_color, end_color, multiplier): + # Parse the coordinates + coordinates = json.loads(coordinates.replace("'", '"')) + + # Create an image + image = Image.new("RGB", (frame_width, frame_height)) + draw = ImageDraw.Draw(image) + + # Extract start and end points for the gradient + start_coord = coordinates[0] + end_coord = coordinates[1] + + start_color = parse_color(start_color) + end_color = parse_color(end_color) + + # Calculate the gradient direction (vector) + gradient_direction = (end_coord['x'] - start_coord['x'], end_coord['y'] - start_coord['y']) + gradient_length = (gradient_direction[0] ** 2 + gradient_direction[1] ** 2) ** 0.5 + + # Iterate over each pixel in the image + for y in range(frame_height): + for x in range(frame_width): + # Calculate the projection of the point on the gradient line + point_vector = (x - start_coord['x'], y - start_coord['y']) + projection = (point_vector[0] * gradient_direction[0] + point_vector[1] * gradient_direction[1]) / gradient_length + projection = max(min(projection, gradient_length), 0) # Clamp the projection value + + # Calculate the blend factor for the current pixel + blend = projection * multiplier / gradient_length + + # Determine the color of the current pixel + color = ( + int(start_color[0] + (end_color[0] - start_color[0]) * blend), + int(start_color[1] + (end_color[1] - start_color[1]) * blend), + int(start_color[2] + (end_color[2] - start_color[2]) * blend) + ) + + # Set the pixel color + draw.point((x, y), fill=color) + + # Convert the PIL image to a tensor (assuming such a function exists in your context) + image_tensor = pil2tensor(image) + + return (image_tensor,) + +class GradientToFloat: + + RETURN_TYPES = ("FLOAT", "FLOAT",) + RETURN_NAMES = ("float_x", "float_y", ) + FUNCTION = "sample" + CATEGORY = "KJNodes/image" + DESCRIPTION = """ +Calculates list of floats from image. +""" + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "image": ("IMAGE", ), + "steps": ("INT", {"default": 10, "min": 2, "max": 10000, "step": 1}), + }, + } + + def sample(self, image, steps): + # Assuming image is a tensor with shape [B, H, W, C] + B, H, W, C = image.shape + + # Sample along the width axis (W) + w_intervals = torch.linspace(0, W - 1, steps=steps, dtype=torch.int64) + # Assuming we're sampling from the first batch and the first channel + w_sampled = image[0, :, w_intervals, 0] + + # Sample along the height axis (H) + h_intervals = torch.linspace(0, H - 1, steps=steps, dtype=torch.int64) + # Assuming we're sampling from the first batch and the first channel + h_sampled = image[0, h_intervals, :, 0] + + # Taking the mean across the height for width sampling, and across the width for height sampling + w_values = w_sampled.mean(dim=0).tolist() + h_values = h_sampled.mean(dim=1).tolist() + + return (w_values, h_values) + +class MaskOrImageToWeight: + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "output_type": ( + [ + 'list', + 'pandas series', + 'tensor', + 'string' + ], + { + "default": 'list' + }), + }, + "optional": { + "images": ("IMAGE",), + "masks": ("MASK",), + }, + + } + RETURN_TYPES = ("FLOAT", "STRING",) + FUNCTION = "execute" + CATEGORY = "KJNodes/weights" + DESCRIPTION = """ +Gets the mean values from mask or image batch +and returns that as the selected output type. +""" + + def execute(self, output_type, images=None, masks=None): + mean_values = [] + if masks is not None and images is None: + for mask in masks: + mean_values.append(mask.mean().item()) + elif masks is None and images is not None: + for image in images: + mean_values.append(image.mean().item()) + elif masks is not None and images is not None: + raise ValueError("MaskOrImageToWeight: Use either mask or image input only.") + + # Convert mean_values to the specified output_type + if output_type == 'list': + out = mean_values + elif output_type == 'pandas series': + try: + import pandas as pd + except ImportError as e: + raise ImportError("MaskOrImageToWeight: pandas is not installed. Please install pandas to use this output_type") from e + out = pd.Series(mean_values), + elif output_type == 'tensor': + out = torch.tensor(mean_values, dtype=torch.float32), + return (out, [str(value) for value in mean_values],) + +class WeightScheduleConvert: + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "input_values": ("FLOAT", {"default": 0.0, "forceInput": True}), + "output_type": ( + [ + 'match_input', + 'list', + 'pandas series', + 'tensor', + ], + { + "default": 'list' + }), + "invert": ("BOOLEAN", {"default": False}), + "repeat": ("INT", {"default": 1,"min": 1, "max": 255, "step": 1}), + }, + "optional": { + "remap_to_frames": ("INT", {"default": 0}), + "interpolation_curve": ("FLOAT", {"forceInput": True}), + "remap_values": ("BOOLEAN", {"default": False}), + "remap_min": ("FLOAT", {"default": 0.0, "min": -100000, "max": 100000.0, "step": 0.01}), + "remap_max": ("FLOAT", {"default": 1.0, "min": -100000, "max": 100000.0, "step": 0.01}), + }, + + } + RETURN_TYPES = ("FLOAT", "STRING", "INT",) + FUNCTION = "execute" + CATEGORY = "KJNodes/weights" + DESCRIPTION = """ +Converts different value lists/series to another type. +""" + + def detect_input_type(self, input_values): + import pandas as pd + if isinstance(input_values, list): + return 'list' + elif isinstance(input_values, pd.Series): + return 'pandas series' + elif isinstance(input_values, torch.Tensor): + return 'tensor' + else: + raise ValueError("Unsupported input type") + + def execute(self, input_values, output_type, invert, repeat, remap_to_frames=0, interpolation_curve=None, remap_min=0.0, remap_max=1.0, remap_values=False): + import pandas as pd + input_type = self.detect_input_type(input_values) + + if input_type == 'pandas series': + float_values = input_values.tolist() + elif input_type == 'tensor': + float_values = input_values + else: + float_values = input_values + + if invert: + float_values = [1 - value for value in float_values] + + if interpolation_curve is not None: + interpolated_pattern = [] + orig_float_values = float_values + for value in interpolation_curve: + min_val = min(orig_float_values) + max_val = max(orig_float_values) + # Normalize the values to [0, 1] + normalized_values = [(value - min_val) / (max_val - min_val) for value in orig_float_values] + # Interpolate the normalized values to the new frame count + remapped_float_values = np.interp(np.linspace(0, 1, int(remap_to_frames * value)), np.linspace(0, 1, len(normalized_values)), normalized_values).tolist() + interpolated_pattern.extend(remapped_float_values) + float_values = interpolated_pattern + else: + # Remap float_values to match target_frame_amount + if remap_to_frames > 0 and remap_to_frames != len(float_values): + min_val = min(float_values) + max_val = max(float_values) + # Normalize the values to [0, 1] + normalized_values = [(value - min_val) / (max_val - min_val) for value in float_values] + # Interpolate the normalized values to the new frame count + float_values = np.interp(np.linspace(0, 1, remap_to_frames), np.linspace(0, 1, len(normalized_values)), normalized_values).tolist() + + float_values = float_values * repeat + if remap_values: + float_values = self.remap_values(float_values, remap_min, remap_max) + + if output_type == 'list': + out = float_values, + elif output_type == 'pandas series': + out = pd.Series(float_values), + elif output_type == 'tensor': + if input_type == 'pandas series': + out = torch.tensor(float_values.values, dtype=torch.float32), + else: + out = torch.tensor(float_values, dtype=torch.float32), + elif output_type == 'match_input': + out = float_values, + return (out, [str(value) for value in float_values], [int(value) for value in float_values]) + + def remap_values(self, values, target_min, target_max): + # Determine the current range + current_min = min(values) + current_max = max(values) + current_range = current_max - current_min + + # Determine the target range + target_range = target_max - target_min + + # Perform the linear interpolation for each value + remapped_values = [(value - current_min) / current_range * target_range + target_min for value in values] + + return remapped_values + + +class FloatToMask: + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "input_values": ("FLOAT", {"forceInput": True, "default": 0}), + "width": ("INT", {"default": 100, "min": 1}), + "height": ("INT", {"default": 100, "min": 1}), + }, + } + RETURN_TYPES = ("MASK",) + FUNCTION = "execute" + CATEGORY = "KJNodes/masking/generate" + DESCRIPTION = """ +Generates a batch of masks based on the input float values. +The batch size is determined by the length of the input float values. +Each mask is generated with the specified width and height. +""" + + def execute(self, input_values, width, height): + import pandas as pd + # Ensure input_values is a list + if isinstance(input_values, (float, int)): + input_values = [input_values] + elif isinstance(input_values, pd.Series): + input_values = input_values.tolist() + elif isinstance(input_values, list) and all(isinstance(item, list) for item in input_values): + input_values = [item for sublist in input_values for item in sublist] + + # Generate a batch of masks based on the input_values + masks = [] + for value in input_values: + # Assuming value is a float between 0 and 1 representing the mask's intensity + mask = torch.ones((height, width), dtype=torch.float32) * value + masks.append(mask) + masks_out = torch.stack(masks, dim=0) + + return(masks_out,) +class WeightScheduleExtend: + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "input_values_1": ("FLOAT", {"default": 0.0, "forceInput": True}), + "input_values_2": ("FLOAT", {"default": 0.0, "forceInput": True}), + "output_type": ( + [ + 'match_input', + 'list', + 'pandas series', + 'tensor', + ], + { + "default": 'match_input' + }), + }, + + } + RETURN_TYPES = ("FLOAT",) + FUNCTION = "execute" + CATEGORY = "KJNodes/weights" + DESCRIPTION = """ +Extends, and converts if needed, different value lists/series +""" + + def detect_input_type(self, input_values): + import pandas as pd + if isinstance(input_values, list): + return 'list' + elif isinstance(input_values, pd.Series): + return 'pandas series' + elif isinstance(input_values, torch.Tensor): + return 'tensor' + else: + raise ValueError("Unsupported input type") + + def execute(self, input_values_1, input_values_2, output_type): + import pandas as pd + input_type_1 = self.detect_input_type(input_values_1) + input_type_2 = self.detect_input_type(input_values_2) + # Convert input_values_2 to the same format as input_values_1 if they do not match + if not input_type_1 == input_type_2: + print("Converting input_values_2 to the same format as input_values_1") + if input_type_1 == 'pandas series': + # Convert input_values_2 to a pandas Series + float_values_2 = pd.Series(input_values_2) + elif input_type_1 == 'tensor': + # Convert input_values_2 to a tensor + float_values_2 = torch.tensor(input_values_2, dtype=torch.float32) + else: + print("Input types match, no conversion needed") + # If the types match, no conversion is needed + float_values_2 = input_values_2 + + float_values = input_values_1 + float_values_2 + + if output_type == 'list': + return float_values, + elif output_type == 'pandas series': + return pd.Series(float_values), + elif output_type == 'tensor': + if input_type_1 == 'pandas series': + return torch.tensor(float_values.values, dtype=torch.float32), + else: + return torch.tensor(float_values, dtype=torch.float32), + elif output_type == 'match_input': + return float_values, + else: + raise ValueError(f"Unsupported output_type: {output_type}") + +class FloatToSigmas: + @classmethod + def INPUT_TYPES(s): + return {"required": + { + "float_list": ("FLOAT", {"default": 0.0, "forceInput": True}), + } + } + RETURN_TYPES = ("SIGMAS",) + RETURN_NAMES = ("SIGMAS",) + CATEGORY = "KJNodes/noise" + FUNCTION = "customsigmas" + DESCRIPTION = """ +Creates a sigmas tensor from list of float values. + +""" + def customsigmas(self, float_list): + return torch.tensor(float_list, dtype=torch.float32), + +class SigmasToFloat: + @classmethod + def INPUT_TYPES(s): + return {"required": + { + "sigmas": ("SIGMAS",), + } + } + RETURN_TYPES = ("FLOAT",) + RETURN_NAMES = ("float",) + CATEGORY = "KJNodes/noise" + FUNCTION = "customsigmas" + DESCRIPTION = """ +Creates a float list from sigmas tensors. + +""" + def customsigmas(self, sigmas): + return sigmas.tolist(), + +class GLIGENTextBoxApplyBatchCoords: + @classmethod + def INPUT_TYPES(s): + return {"required": {"conditioning_to": ("CONDITIONING", ), + "latents": ("LATENT", ), + "clip": ("CLIP", ), + "gligen_textbox_model": ("GLIGEN", ), + "coordinates": ("STRING", {"forceInput": True}), + "text": ("STRING", {"multiline": True}), + "width": ("INT", {"default": 128, "min": 8, "max": 4096, "step": 8}), + "height": ("INT", {"default": 128, "min": 8, "max": 4096, "step": 8}), + }, + "optional": {"size_multiplier": ("FLOAT", {"default": [1.0], "forceInput": True})}, + } + RETURN_TYPES = ("CONDITIONING", "IMAGE", ) + RETURN_NAMES = ("conditioning", "coord_preview", ) + FUNCTION = "append" + CATEGORY = "KJNodes/experimental" + DESCRIPTION = """ +This node allows scheduling GLIGEN text box positions in a batch, +to be used with AnimateDiff-Evolved. Intended to pair with the +Spline Editor -node. + +GLIGEN model can be downloaded through the Manage's "Install Models" menu. +Or directly from here: +https://huggingface.co/comfyanonymous/GLIGEN_pruned_safetensors/tree/main + +Inputs: +- **latents** input is used to calculate batch size +- **clip** is your standard text encoder, use same as for the main prompt +- **gligen_textbox_model** connects to GLIGEN Loader +- **coordinates** takes a json string of points, directly compatible +with the spline editor node. +- **text** is the part of the prompt to set position for +- **width** and **height** are the size of the GLIGEN bounding box + +Outputs: +- **conditioning** goes between to clip text encode and the sampler +- **coord_preview** is an optional preview of the coordinates and +bounding boxes. + +""" + + def append(self, latents, coordinates, conditioning_to, clip, gligen_textbox_model, text, width, height, size_multiplier=[1.0]): + coordinates = json.loads(coordinates.replace("'", '"')) + coordinates = [(coord['x'], coord['y']) for coord in coordinates] + + batch_size = sum(tensor.size(0) for tensor in latents.values()) + if len(coordinates) != batch_size: + print("GLIGENTextBoxApplyBatchCoords WARNING: The number of coordinates does not match the number of latents") + + c = [] + _, cond_pooled = clip.encode_from_tokens(clip.tokenize(text), return_pooled=True) + + for t in conditioning_to: + n = [t[0], t[1].copy()] + + position_params_batch = [[] for _ in range(batch_size)] # Initialize a list of empty lists for each batch item + if len(size_multiplier) != batch_size: + size_multiplier = size_multiplier * (batch_size // len(size_multiplier)) + size_multiplier[:batch_size % len(size_multiplier)] + + for i in range(batch_size): + x_position, y_position = coordinates[i] + position_param = (cond_pooled, int((height // 8) * size_multiplier[i]), int((width // 8) * size_multiplier[i]), (y_position - height // 2) // 8, (x_position - width // 2) // 8) + position_params_batch[i].append(position_param) # Append position_param to the correct sublist + + prev = [] + if "gligen" in n[1]: + prev = n[1]['gligen'][2] + else: + prev = [[] for _ in range(batch_size)] + # Concatenate prev and position_params_batch, ensuring both are lists of lists + # and each sublist corresponds to a batch item + combined_position_params = [prev_item + batch_item for prev_item, batch_item in zip(prev, position_params_batch)] + n[1]['gligen'] = ("position_batched", gligen_textbox_model, combined_position_params) + c.append(n) + + image_height = latents['samples'].shape[-2] * 8 + image_width = latents['samples'].shape[-1] * 8 + plot_image_tensor = plot_coordinates_to_tensor(coordinates, image_height, image_width, height, width, size_multiplier, text) + + return (c, plot_image_tensor,) + +class CreateInstanceDiffusionTracking: + + RETURN_TYPES = ("TRACKING", "STRING", "INT", "INT", "INT", "INT",) + RETURN_NAMES = ("tracking", "prompt", "width", "height", "bbox_width", "bbox_height",) + FUNCTION = "tracking" + CATEGORY = "KJNodes/InstanceDiffusion" + DESCRIPTION = """ +Creates tracking data to be used with InstanceDiffusion: +https://github.com/logtd/ComfyUI-InstanceDiffusion + +InstanceDiffusion prompt format: +"class_id.class_name": "prompt", +for example: +"1.head": "((head))", +""" + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "coordinates": ("STRING", {"forceInput": True}), + "width": ("INT", {"default": 512,"min": 16, "max": 4096, "step": 1}), + "height": ("INT", {"default": 512,"min": 16, "max": 4096, "step": 1}), + "bbox_width": ("INT", {"default": 512,"min": 16, "max": 4096, "step": 1}), + "bbox_height": ("INT", {"default": 512,"min": 16, "max": 4096, "step": 1}), + "class_name": ("STRING", {"default": "class_name"}), + "class_id": ("INT", {"default": 0,"min": 0, "max": 255, "step": 1}), + "prompt": ("STRING", {"default": "prompt", "multiline": True}), + }, + "optional": { + "size_multiplier": ("FLOAT", {"default": [1.0], "forceInput": True}), + "fit_in_frame": ("BOOLEAN", {"default": True}), + } + } + + def tracking(self, coordinates, class_name, class_id, width, height, bbox_width, bbox_height, prompt, size_multiplier=[1.0], fit_in_frame=True): + # Define the number of images in the batch + coordinates = coordinates.replace("'", '"') + coordinates = json.loads(coordinates) + + tracked = {} + tracked[class_name] = {} + batch_size = len(coordinates) + # Initialize a list to hold the coordinates for the current ID + id_coordinates = [] + if not size_multiplier or len(size_multiplier) != batch_size: + size_multiplier = [0] * batch_size + else: + size_multiplier = size_multiplier * (batch_size // len(size_multiplier)) + size_multiplier[:batch_size % len(size_multiplier)] + for i, coord in enumerate(coordinates): + x = coord['x'] + y = coord['y'] + adjusted_bbox_width = bbox_width * size_multiplier[i] + adjusted_bbox_height = bbox_height * size_multiplier[i] + # Calculate the top left and bottom right coordinates + top_left_x = x - adjusted_bbox_width // 2 + top_left_y = y - adjusted_bbox_height // 2 + bottom_right_x = x + adjusted_bbox_width // 2 + bottom_right_y = y + adjusted_bbox_height // 2 + + if fit_in_frame: + # Clip the coordinates to the frame boundaries + top_left_x = max(0, top_left_x) + top_left_y = max(0, top_left_y) + bottom_right_x = min(width, bottom_right_x) + bottom_right_y = min(height, bottom_right_y) + # Ensure width and height are positive + adjusted_bbox_width = max(1, bottom_right_x - top_left_x) + adjusted_bbox_height = max(1, bottom_right_y - top_left_y) + + # Update the coordinates with the new width and height + bottom_right_x = top_left_x + adjusted_bbox_width + bottom_right_y = top_left_y + adjusted_bbox_height + + # Append the top left and bottom right coordinates to the list for the current ID + id_coordinates.append([top_left_x, top_left_y, bottom_right_x, bottom_right_y, width, height]) + + class_id = int(class_id) + # Assign the list of coordinates to the specified ID within the class_id dictionary + tracked[class_name][class_id] = id_coordinates + + prompt_string = "" + for class_name, class_data in tracked.items(): + for class_id in class_data.keys(): + class_id_str = str(class_id) + # Use the incoming prompt for each class name and ID + prompt_string += f'"{class_id_str}.{class_name}": "({prompt})",\n' + + # Remove the last comma and newline + prompt_string = prompt_string.rstrip(",\n") + + return (tracked, prompt_string, width, height, bbox_width, bbox_height) + +class AppendInstanceDiffusionTracking: + + RETURN_TYPES = ("TRACKING", "STRING",) + RETURN_NAMES = ("tracking", "prompt",) + FUNCTION = "append" + CATEGORY = "KJNodes/InstanceDiffusion" + DESCRIPTION = """ +Appends tracking data to be used with InstanceDiffusion: +https://github.com/logtd/ComfyUI-InstanceDiffusion + +""" + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "tracking_1": ("TRACKING", {"forceInput": True}), + "tracking_2": ("TRACKING", {"forceInput": True}), + }, + "optional": { + "prompt_1": ("STRING", {"default": "", "forceInput": True}), + "prompt_2": ("STRING", {"default": "", "forceInput": True}), + } + } + + def append(self, tracking_1, tracking_2, prompt_1="", prompt_2=""): + tracking_copy = tracking_1.copy() + # Check for existing class names and class IDs, and raise an error if they exist + for class_name, class_data in tracking_2.items(): + if class_name not in tracking_copy: + tracking_copy[class_name] = class_data + else: + # If the class name exists, merge the class data from tracking_2 into tracking_copy + # This will add new class IDs under the same class name without raising an error + tracking_copy[class_name].update(class_data) + prompt_string = prompt_1 + "," + prompt_2 + return (tracking_copy, prompt_string) + +class InterpolateCoords: + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("coordinates",) + FUNCTION = "interpolate" + CATEGORY = "KJNodes/experimental" + DESCRIPTION = """ +Interpolates coordinates based on a curve. +""" + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "coordinates": ("STRING", {"forceInput": True}), + "interpolation_curve": ("FLOAT", {"forceInput": True}), + + }, + } + + def interpolate(self, coordinates, interpolation_curve): + # Parse the JSON string to get the list of coordinates + coordinates = json.loads(coordinates.replace("'", '"')) + + # Convert the list of dictionaries to a list of (x, y) tuples for easier processing + coordinates = [(coord['x'], coord['y']) for coord in coordinates] + + # Calculate the total length of the original path + path_length = sum(np.linalg.norm(np.array(coordinates[i]) - np.array(coordinates[i-1])) + for i in range(1, len(coordinates))) + + # Initialize variables for interpolation + interpolated_coords = [] + current_length = 0 + current_index = 0 + + # Iterate over the normalized curve + for normalized_length in interpolation_curve: + target_length = normalized_length * path_length # Convert to the original scale + while current_index < len(coordinates) - 1: + segment_start, segment_end = np.array(coordinates[current_index]), np.array(coordinates[current_index + 1]) + segment_length = np.linalg.norm(segment_end - segment_start) + if current_length + segment_length >= target_length: + break + current_length += segment_length + current_index += 1 + + # Interpolate between the last two points + if current_index < len(coordinates) - 1: + p1, p2 = np.array(coordinates[current_index]), np.array(coordinates[current_index + 1]) + segment_length = np.linalg.norm(p2 - p1) + if segment_length > 0: + t = (target_length - current_length) / segment_length + interpolated_point = p1 + t * (p2 - p1) + interpolated_coords.append(interpolated_point.tolist()) + else: + interpolated_coords.append(p1.tolist()) + else: + # If the target_length is at or beyond the end of the path, add the last coordinate + interpolated_coords.append(coordinates[-1]) + + # Convert back to string format if necessary + interpolated_coords_str = "[" + ", ".join([f"{{'x': {round(coord[0])}, 'y': {round(coord[1])}}}" for coord in interpolated_coords]) + "]" + print(interpolated_coords_str) + + return (interpolated_coords_str,) + +class DrawInstanceDiffusionTracking: + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("image", ) + FUNCTION = "draw" + CATEGORY = "KJNodes/InstanceDiffusion" + DESCRIPTION = """ +Draws the tracking data from +CreateInstanceDiffusionTracking -node. + +""" + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "image": ("IMAGE", ), + "tracking": ("TRACKING", {"forceInput": True}), + "box_line_width": ("INT", {"default": 2, "min": 1, "max": 10, "step": 1}), + "draw_text": ("BOOLEAN", {"default": True}), + "font": (folder_paths.get_filename_list("kjnodes_fonts"), ), + "font_size": ("INT", {"default": 20}), + }, + } + + def draw(self, image, tracking, box_line_width, draw_text, font, font_size): + import matplotlib.cm as cm + + modified_images = [] + + colormap = cm.get_cmap('rainbow', len(tracking)) + if draw_text: + font_path = folder_paths.get_full_path("kjnodes_fonts", font) + font = ImageFont.truetype(font_path, font_size) + + # Iterate over each image in the batch + for i in range(image.shape[0]): + # Extract the current image and convert it to a PIL image + current_image = image[i, :, :, :].permute(2, 0, 1) + pil_image = transforms.ToPILImage()(current_image) + + draw = ImageDraw.Draw(pil_image) + + # Iterate over the bounding boxes for the current image + for j, (class_name, class_data) in enumerate(tracking.items()): + for class_id, bbox_list in class_data.items(): + # Check if the current index is within the bounds of the bbox_list + if i < len(bbox_list): + bbox = bbox_list[i] + # Ensure bbox is a list or tuple before unpacking + if isinstance(bbox, (list, tuple)): + x1, y1, x2, y2, _, _ = bbox + # Convert coordinates to integers + x1, y1, x2, y2 = int(x1), int(y1), int(x2), int(y2) + # Generate a color from the rainbow colormap + color = tuple(int(255 * x) for x in colormap(j / len(tracking)))[:3] + # Draw the bounding box on the image with the generated color + draw.rectangle([x1, y1, x2, y2], outline=color, width=box_line_width) + if draw_text: + # Draw the class name and ID as text above the box with the generated color + text = f"{class_id}.{class_name}" + # Calculate the width and height of the text + _, _, text_width, text_height = draw.textbbox((0, 0), text=text, font=font) + # Position the text above the top-left corner of the box + text_position = (x1, y1 - text_height) + draw.text(text_position, text, fill=color, font=font) + else: + print(f"Unexpected data type for bbox: {type(bbox)}") + + # Convert the drawn image back to a torch tensor and adjust back to (H, W, C) + modified_image_tensor = transforms.ToTensor()(pil_image).permute(1, 2, 0) + modified_images.append(modified_image_tensor) + + # Stack the modified images back into a batch + image_tensor_batch = torch.stack(modified_images).cpu().float() + + return image_tensor_batch, + +class PointsEditor: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "points_store": ("STRING", {"multiline": False, "advanced": True}), + "coordinates": ("STRING", {"multiline": False, "socketless": True, "advanced": True}), + "neg_coordinates": ("STRING", {"multiline": False, "socketless": True, "advanced": True}), + "bbox_store": ("STRING", {"multiline": False, "advanced": True}), + "bboxes": ("STRING", {"multiline": False, "socketless": True, "advanced": True}), + "bbox_format": ( + [ + 'xyxy', + 'xywh', + ], + ), + "width": ("INT", {"default": 512, "min": 8, "max": 4096, "step": 8}), + "height": ("INT", {"default": 512, "min": 8, "max": 4096, "step": 8}), + "normalize": ("BOOLEAN", {"default": False}), + }, + "optional": { + "bg_image": ("IMAGE", ), + }, + } + + RETURN_TYPES = ("STRING", "STRING", "BBOX", "MASK", "IMAGE") + RETURN_NAMES = ("positive_coords", "negative_coords", "bbox", "bbox_mask", "cropped_image") + FUNCTION = "pointdata" + CATEGORY = "KJNodes/experimental" + DESCRIPTION = """ +# WORK IN PROGRESS +Do not count on this as part of your workflow yet, +probably contains lots of bugs and stability is not +guaranteed!! + +## Graphical editor to create coordinates + +**Shift + click** to add a positive (green) point. +**Shift + right click** to add a negative (red) point. +**Right click on a point** to delete it. +**Ctrl + click** to draw a bounding box. +**Drag bbox corners** to resize, **drag inside** to move. +**Right click on bbox** to delete it. + +To add an image select the node and copy/paste or drag in the image. +Or from the bg_image input on queue (first frame of the batch). + +**THE IMAGE IS SAVED TO THE NODE AND WORKFLOW METADATA** +you can clear the image from the context menu by right clicking on the canvas + +""" + + def pointdata(self, points_store, bbox_store, width, height, coordinates, neg_coordinates, normalize, bboxes, bbox_format="xyxy", bg_image=None): + coordinates = json.loads(coordinates) + if not coordinates: + raise ValueError("No points on the canvas. Use Shift+click to add positive points or Shift+right-click to add negative points before executing.") + pos_coordinates = [] + for coord in coordinates: + coord['x'] = int(round(coord['x'])) + coord['y'] = int(round(coord['y'])) + if normalize: + norm_x = coord['x'] / width + norm_y = coord['y'] / height + pos_coordinates.append({'x': norm_x, 'y': norm_y}) + else: + pos_coordinates.append({'x': coord['x'], 'y': coord['y']}) + + if neg_coordinates: + coordinates = json.loads(neg_coordinates) + neg_coordinates = [] + for coord in coordinates: + coord['x'] = int(round(coord['x'])) + coord['y'] = int(round(coord['y'])) + if normalize: + norm_x = coord['x'] / width + norm_y = coord['y'] / height + neg_coordinates.append({'x': norm_x, 'y': norm_y}) + else: + neg_coordinates.append({'x': coord['x'], 'y': coord['y']}) + + # Create a blank mask + mask = np.zeros((height, width), dtype=np.uint8) + bboxes = json.loads(bboxes) + valid_bboxes = [] + for bbox in bboxes: + if (bbox.get("startX") is None or + bbox.get("startY") is None or + bbox.get("endX") is None or + bbox.get("endY") is None): + continue # Skip this bounding box if any value is None + else: + # Ensure that endX and endY are greater than startX and startY + x_min = min(int(bbox["startX"]), int(bbox["endX"])) + y_min = min(int(bbox["startY"]), int(bbox["endY"])) + x_max = max(int(bbox["startX"]), int(bbox["endX"])) + y_max = max(int(bbox["startY"]), int(bbox["endY"])) + + valid_bboxes.append((x_min, y_min, x_max, y_max)) + + bboxes_xyxy = [] + for bbox in valid_bboxes: + x_min, y_min, x_max, y_max = bbox + bboxes_xyxy.append((x_min, y_min, x_max, y_max)) + mask[y_min:y_max, x_min:x_max] = 1 # Fill the bounding box area with 1s + + if bbox_format == "xywh": + bboxes_xywh = [] + for bbox in valid_bboxes: + x_min, y_min, x_max, y_max = bbox + width = x_max - x_min + height = y_max - y_min + bboxes_xywh.append((x_min, y_min, width, height)) + bboxes = bboxes_xywh + else: + bboxes = bboxes_xyxy + + mask_tensor = torch.from_numpy(mask) + mask_tensor = mask_tensor.unsqueeze(0).float().cpu() + + if bg_image is not None and len(valid_bboxes) > 0: + x_min, y_min, x_max, y_max = bboxes[0] + cropped_image = bg_image[:, y_min:y_max, x_min:x_max, :] + elif bg_image is not None: + cropped_image = bg_image + else: + cropped_image = torch.zeros(1, height, width, 3, dtype=torch.float32) + + if bg_image is None: + return (json.dumps(pos_coordinates), json.dumps(neg_coordinates), bboxes, mask_tensor, cropped_image) + else: + transform = transforms.ToPILImage() + image = transform(bg_image[0].permute(2, 0, 1)) + buffered = BytesIO() + image.save(buffered, format="JPEG", quality=75) + img_base64 = base64.b64encode(buffered.getvalue()).decode('utf-8') + + return { + "ui": {"bg_image": [img_base64]}, + "result": (json.dumps(pos_coordinates), json.dumps(neg_coordinates), bboxes, mask_tensor, cropped_image) + } + +class CutAndDragOnPath: + RETURN_TYPES = ("IMAGE", "MASK",) + RETURN_NAMES = ("image","mask", ) + FUNCTION = "cutanddrag" + CATEGORY = "KJNodes/image" + DESCRIPTION = """ +Cuts the masked area from the image, and drags it along the path. If inpaint is enabled, and no bg_image is provided, the cut area is filled using cv2 TELEA algorithm. +""" + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "image": ("IMAGE",), + "coordinates": ("STRING", {"forceInput": True}), + "mask": ("MASK",), + "frame_width": ("INT", {"default": 512,"min": 16, "max": 4096, "step": 1}), + "frame_height": ("INT", {"default": 512,"min": 16, "max": 4096, "step": 1}), + "inpaint": ("BOOLEAN", {"default": True}), + }, + "optional": { + "bg_image": ("IMAGE",), + } + } + + def cutanddrag(self, image, coordinates, mask, frame_width, frame_height, inpaint, bg_image=None): + # Parse coordinates + coords_list = parse_json_tracks(coordinates) + + batch_size = len(coords_list[0]) + images_list = [] + masks_list = [] + + # Convert input image and mask to PIL + input_image = tensor2pil(image)[0] + input_mask = tensor2pil(mask)[0] + + # Find masked region bounds + mask_array = np.array(input_mask) + y_indices, x_indices = np.where(mask_array > 0) + if len(x_indices) == 0 or len(y_indices) == 0: + return (image, mask) + + x_min, x_max = x_indices.min(), x_indices.max() + y_min, y_max = y_indices.min(), y_indices.max() + + # Cut out the masked region + cut_width = x_max - x_min + cut_height = y_max - y_min + cut_image = input_image.crop((x_min, y_min, x_max, y_max)) + cut_mask = input_mask.crop((x_min, y_min, x_max, y_max)) + + # Create inpainted background + if bg_image is None: + background = input_image.copy() + # Inpaint the cut area + if inpaint: + import cv2 + border = 5 # Create small border around cut area for better inpainting + fill_mask = Image.new("L", background.size, 0) + draw = ImageDraw.Draw(fill_mask) + draw.rectangle([x_min-border, y_min-border, x_max+border, y_max+border], fill=255) + background = cv2.inpaint( + np.array(background), + np.array(fill_mask), + inpaintRadius=3, + flags=cv2.INPAINT_TELEA + ) + background = Image.fromarray(background) + else: + background = tensor2pil(bg_image)[0] + + # Create batch of images with cut region at different positions + for i in range(batch_size): + # Create new image + new_image = background.copy() + new_mask = Image.new("L", (frame_width, frame_height), 0) + + # Get target position from coordinates + for coords in coords_list: + target_x = int(coords[i]['x'] - cut_width/2) + target_y = int(coords[i]['y'] - cut_height/2) + + # Paste cut region at new position + new_image.paste(cut_image, (target_x, target_y), cut_mask) + new_mask.paste(cut_mask, (target_x, target_y)) + + # Convert to tensor and append + image_tensor = pil2tensor(new_image) + mask_tensor = pil2tensor(new_mask) + + images_list.append(image_tensor) + masks_list.append(mask_tensor) + + # Stack tensors into batches + out_images = torch.cat(images_list, dim=0).cpu().float() + out_masks = torch.cat(masks_list, dim=0) + + return (out_images, out_masks) \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/hdr_preview_node.py b/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/hdr_preview_node.py new file mode 100644 index 0000000000000000000000000000000000000000..82d2f585a963de873815c4dacf1a4e1419bc13ed --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/hdr_preview_node.py @@ -0,0 +1,166 @@ +import os +import random + +import torch +from PIL import Image +from comfy_api.latest import io +import comfy.model_management as mm +import folder_paths + + +# LogC3 constants (ARRI LogC3, EI 800). Kept in sync with the WebGL shader in web/js/hdr_preview.js. +LC_A = 5.555556 +LC_B = 0.052272 +LC_C = 0.247190 +LC_D = 0.385537 +LC_E = 5.367655 +LC_F = 0.092809 +LC_CUT = 0.010591 +LC_CUT_LOG = LC_E * LC_CUT + LC_F # ~0.14966 + +def _logc3_decompress(logc: torch.Tensor) -> torch.Tensor: + logc = logc.clamp(0.0, 1.0) + lin_from_log = (10.0 ** ((logc - LC_D) / LC_C) - LC_B) / LC_A + lin_from_lin = (logc - LC_F) / LC_E + return torch.where(logc >= LC_CUT_LOG, lin_from_log, lin_from_lin) + + +def _linear_to_srgb(x: torch.Tensor) -> torch.Tensor: + cutoff = 0.0031308 + return torch.where( + x <= cutoff, + 12.92 * x, + 1.055 * torch.pow(x.clamp(min=cutoff), 1.0 / 2.4) - 0.055, + ).clamp_(0.0, 1.0) + + +def _srgb_to_linear(x: torch.Tensor) -> torch.Tensor: + cutoff = 0.04045 + return torch.where( + x <= cutoff, + x / 12.92, + ((x.clamp(min=0.0) + 0.055) / 1.055) ** 2.4, + ) + + +class HDRPreviewKJ(io.ComfyNode): + @classmethod + def define_schema(cls): + return io.Schema( + node_id="HDRPreviewKJ", + display_name="HDR Preview KJ", + category="KJNodes/image", + is_output_node=True, + is_experimental=True, + description=( + "Realtime-exposure preview for HDR-compressed images.\n\n" + "Input: LogC3-compressed [0,1] image/video batch (e.g. the VAE-decoded output " + "of an HDR IC-LoRA workflow, prior to HDR decompression).\n\n" + "Decompression + exposure + saturation + Reinhard tonemap + sRGB runs in a WebGL " + "fragment shader in the browser for realtime slider feedback, and the same math " + "runs server-side to produce the baked sRGB IMAGE output. Slider changes update " + "the preview immediately; the IMAGE output only updates when the workflow is re-queued." + ), + inputs=[ + io.Image.Input("image", + tooltip="LogC3-compressed HDR image/video in [0,1], or linear HDR if 'input_space' is 'linear'."), + io.Float.Input("exposure", default=0.0, min=-10.0, max=10.0, step=0.01, + tooltip="Exposure in EV stops. 0 = no change, +1 = 2x brighter."), + io.Float.Input("saturation", default=1.0, min=0.0, max=2.0, step=0.01, + tooltip="Saturation multiplier. 0 = grayscale, 1 = unchanged, 2 = 2x."), + io.Float.Input("fps", default=24.0, min=1.0, max=120.0, step=0.1, optional=True, + tooltip="Playback frame rate for video (batch) inputs."), + io.Combo.Input("input_space", options=["logc3", "linear", "srgb"], default="logc3", optional=True, + tooltip="Color space of input. 'logc3' = ARRI LogC3 compressed HDR; 'linear' = linear HDR directly; 'srgb' = already-graded sRGB image (skips Reinhard tonemap)."), + ], + outputs=[ + io.Image.Output(display_name="image", + tooltip="Tonemapped sRGB image, ready for preview/save."), + ], + ) + + @classmethod + def execute(cls, image: torch.Tensor, exposure: float = 0.0, saturation: float = 1.0, fps: float = 24.0, input_space: str = "logc3") -> io.NodeOutput: + temp_dir = folder_paths.get_temp_directory() + os.makedirs(temp_dir, exist_ok=True) + prefix = f"hdrprv_{random.randint(0, 0xFFFFFF):06x}" + + B, H, W, _ = image.shape + device = mm.get_torch_device() + exposure_mul = 2.0 ** exposure + luma_weights = torch.tensor([0.2126, 0.7152, 0.0722], device=device) + + bytes_per_frame = H * W * 3 * 4 + chunk_size = max(1, min(B, int(1_000_000_000 // max(bytes_per_frame * 10, 1)))) + + # For linear input we need the global max across all frames to normalize previews. + norm_scale = 1.0 + if input_space == "linear": + max_val = float(image[..., :3].max().item()) + norm_scale = max_val if max_val > 1.0 else 1.0 + + filenames = [] + srgb_chunks = [] + + for start in range(0, B, chunk_size): + end = min(start + chunk_size, B) + image_rgb = image[start:end, ..., :3].float().to(device, non_blocking=True) + + # --- Preview frames (8-bit PNG, always in the "raw" pre-exposure space) --- + if input_space == "linear": + preview = (image_rgb / norm_scale).clamp_(0.0, 1.0) + else: + preview = image_rgb.clamp(0.0, 1.0) + + preview_np = preview.mul_(255.0).add_(0.5).clamp_(0.0, 255.0).to(torch.uint8).cpu().numpy() + del preview + + for i in range(end - start): + fname = f"{prefix}_{start + i:05d}.png" + Image.fromarray(preview_np[i], mode="RGB").save( + os.path.join(temp_dir, fname), + format="PNG", + compress_level=1, + ) + filenames.append(fname) + del preview_np + + # --- Baked sRGB output (same math as the shader in hdr_preview.js) --- + if input_space == "logc3": + hdr = _logc3_decompress(image_rgb).clamp_(min=0.0) + elif input_space == "srgb": + hdr = _srgb_to_linear(image_rgb).clamp_(min=0.0) + else: + hdr = image_rgb.clamp(min=0.0) + del image_rgb + + exposed = hdr.mul_(exposure_mul) + luma = (exposed * luma_weights.to(exposed.dtype)).sum(dim=-1, keepdim=True) + saturated = (luma + (exposed - luma) * saturation).clamp_(min=0.0) + del luma, exposed + + if input_space == "srgb": + # Already display-ready linear; skip Reinhard, just clip over-exposed highlights. + tonemapped = saturated.clamp_(0.0, 1.0) + else: + tonemapped = saturated / (1.0 + saturated) + del saturated + + srgb_chunks.append(_linear_to_srgb(tonemapped).cpu()) + del tonemapped + + srgb = torch.cat(srgb_chunks, dim=0) + del srgb_chunks + + data = { + "frames": [{"filename": f, "type": "temp"} for f in filenames], + "width": int(W), + "height": int(H), + "fps": float(fps), + "input_space": input_space, + "linear_scale": float(norm_scale), + "frame_count": int(B), + "exposure": float(exposure), + "saturation": float(saturation), + } + return io.NodeOutput(srgb, ui={"hdr_preview_data": [data]}) diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/ideogram4_nodes.py b/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/ideogram4_nodes.py new file mode 100644 index 0000000000000000000000000000000000000000..f00c12d6e7c4c7a4eeced984630440b812217686 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/ideogram4_nodes.py @@ -0,0 +1,445 @@ +"""Ideogram 4 prompt builder. + +A single self-contained node with a visual bbox editor: draw regions on a blank +canvas, set each region's type/desc/text/color palette, and assemble the Ideogram 4 JSON caption prompt. +""" + +import json +import os +import re +import logging + +import numpy as np +import torch +from PIL import Image, ImageDraw, ImageFont, ImageEnhance + +from comfy_api.latest import io + + +_FONT_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), "fonts", "FreeMono.ttf") + + +def _hex_rgb(h): + h = h.lstrip("#") + return (int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16)) if len(h) == 6 else (255, 255, 255) + + +def _readable(rgb): + # Lighten toward white if too dark, so box-colored text stays legible on the dark canvas. + r, g, b = rgb + lum = 0.299 * r + 0.587 * g + 0.114 * b + if lum < 130: + t = (130 - lum) / (255 - lum) + r, g, b = round(r + (255 - r) * t), round(g + (255 - g) * t), round(b + (255 - b) * t) + return (r, g, b) + + +def _font(size): + try: + return ImageFont.truetype(_FONT_PATH, size) + except Exception: + try: + return ImageFont.load_default(size) + except Exception: + return ImageFont.load_default() + + +def _wrap(draw, text, font, max_w): + lines = [] + for para in text.split("\n"): + line = "" + for word in para.split(): + test = word if not line else line + " " + word + if line and draw.textlength(test, font=font) > max_w: + lines.append(line) + line = word + else: + line = test + lines.append(line) + return lines + + +def _render_preview(boxes, width, height, bg=None, brightness=50): + # Render the regions + prompts over the reference image (or a black canvas). + if bg is not None: + iw, ih = bg.size + long_edge = max(iw, ih) + scale = min(1.0, 1024 / long_edge) if long_edge > 0 else 1.0 + rw, rh = max(1, round(iw * scale)), max(1, round(ih * scale)) + base = bg.convert("RGB").resize((rw, rh), Image.LANCZOS) + if brightness < 100: # dim to match the editor's brightness slider + base = ImageEnhance.Brightness(base).enhance(max(0.0, brightness / 100.0)) + img = base.convert("RGBA") + else: + long_edge = max(width, height) + scale = min(1.0, 1024 / long_edge) if long_edge > 0 else 1.0 + rw = max(1, round(width * scale)) + rh = max(1, round(height * scale)) + img = Image.new("RGBA", (rw, rh), (0, 0, 0, 255)) # black so the overlay composites cleanly + overlay = Image.new("RGBA", (rw, rh), (0, 0, 0, 0)) + draw = ImageDraw.Draw(overlay) + fs = max(10, round(rh / 64)) + font = _font(fs) + tag_font = _font(max(9, fs - 2)) + lh = fs + 2 + + for i, box in enumerate(boxes): + if not isinstance(box, dict) or box.get("nobbox"): + continue # skip unplaced elements (no real location) + palette = [c for c in (box.get("palette") or []) if c] + r, g, b = _hex_rgb(palette[0]) if palette else (140, 140, 140) # box = first palette color, else grey + x1 = max(0, min(rw, round(box.get("x", 0) * rw))) + y1 = max(0, min(rh, round(box.get("y", 0) * rh))) + x2 = max(0, min(rw, round((box.get("x", 0) + box.get("w", 0)) * rw))) + y2 = max(0, min(rh, round((box.get("y", 0) + box.get("h", 0)) * rh))) + if x2 < x1: + x1, x2 = x2, x1 + if y2 < y1: + y1, y2 = y2, y1 + + draw.rectangle([x1, y1, x2, y2], outline=(r, g, b, 255), width=2) + + pal5 = palette[:5] # palette shown as a strip along the top edge + if pal5 and (x2 - x1) > 2: + sh = max(5, fs // 2) + seg = (x2 - x1) / len(pal5) + for p, hexc in enumerate(pal5): + sx = x1 + round(p * seg) + draw.rectangle([sx, y1, x1 + round((p + 1) * seg), y1 + sh], fill=_hex_rgb(hexc)) + + etype = "text" if box.get("type") == "text" else "obj" + tag = str(i + 1).zfill(2) + tw = draw.textlength(tag, font=tag_font) + draw.rectangle([x1, y1, x1 + tw + 6, y1 + fs + 2], fill=(r, g, b, 255)) # tag chip = box color + tagfill = (0, 0, 0, 255) if (0.299 * r + 0.587 * g + 0.114 * b) > 140 else (255, 255, 255, 255) + draw.text((x1 + 3, y1 + 1), tag, fill=tagfill, font=tag_font) + + body = box.get("desc", "") or "" + if etype == "text" and box.get("text"): + body = '"%s"%s' % (box["text"], " — " + body if body else "") + if body and (x2 - x1) > 8: + ty = y1 + fs + 5 + for line in _wrap(draw, body, font, x2 - x1 - 8): + if ty > y2: + break + draw.text((x1 + 4, ty), line, fill=_readable((r, g, b)) + (255,), font=font) + ty += lh + + img = Image.alpha_composite(img, overlay).convert("RGB") + arr = np.asarray(img, dtype=np.float32) / 255.0 + return torch.from_numpy(arr).unsqueeze(0) + + +def _norm_bbox(box, sx=1000, sy=1000, order="yx"): + # {x, y, w, h} fractions (0-1) -> bbox; x scaled by sx, y by sy. + # Default sx=sy=1000 is the Ideogram-standard 0-1000 grid; pass (width, height) for absolute pixels. + # order="yx" -> [ymin, xmin, ymax, xmax] (Ideogram); "xy" -> [xmin, ymin, xmax, ymax] (Qwen/standard). + def cx(v): + return max(0, min(sx, round(v * sx))) + def cy(v): + return max(0, min(sy, round(v * sy))) + x, y, w, h = box.get("x", 0.0), box.get("y", 0.0), box.get("w", 0.0), box.get("h", 0.0) + ymin, xmin, ymax, xmax = cy(y), cx(x), cy(y + h), cx(x + w) + if ymin > ymax: + ymin, ymax = ymax, ymin + if xmin > xmax: + xmin, xmax = xmax, xmin + return [xmin, ymin, xmax, ymax] if order == "xy" else [ymin, xmin, ymax, xmax] + + +def _palette(colors): + # ["#rrggbb", ...] (or autogrow dict) -> ["#RRGGBB", ...] in order, dropping empties. + if isinstance(colors, dict): + colors = colors.values() + return [c.upper() for c in colors if c] + + +def _dumps(v, lvl=0): + # Like json.dumps(ensure_ascii=False, indent=4), but scalar arrays stay on one line. + pad, end = " " * (lvl + 1), " " * lvl + if isinstance(v, str): + return json.dumps(v, ensure_ascii=False) + if isinstance(v, list): + if not v: + return "[]" + if all(not isinstance(x, (dict, list)) for x in v): + return "[" + ", ".join(_dumps(x, lvl) for x in v) + "]" + return "[\n" + ",\n".join(pad + _dumps(x, lvl + 1) for x in v) + "\n" + end + "]" + if isinstance(v, dict): + if not v: + return "{}" + items = [pad + json.dumps(k, ensure_ascii=False) + ": " + _dumps(val, lvl + 1) for k, val in v.items()] + return "{\n" + ",\n".join(items) + "\n" + end + "}" + return json.dumps(v, ensure_ascii=False) + + +def _parse_json_list(s): + if s: + try: + v = json.loads(s) + if isinstance(v, list): + return v + except json.JSONDecodeError: + pass + return [] + + +def _repair_json(s): + # Slice out the outermost {...} (drops ``` fences / prose), then strip trailing commas before + # } or ] — the leading "(...)" alt matches whole strings first, so quoted commas are untouched. + i, j = s.find("{"), s.rfind("}") + t = s[i:j + 1] if (i != -1 and j > i) else s + return re.sub(r'("(?:[^"\\]|\\.)*")|,(\s*[}\]])', lambda m: m.group(1) or m.group(2), t) + + +def _loads_caption(s): + # Parse a caption dict; on failure retry once with the lenient repair. Returns dict or None. + for cand in ((s, _repair_json(s)) if s and s.strip() else ()): + try: + v = json.loads(cand) + if isinstance(v, dict): + if cand is not s: + logging.warning("[Ideogram4PromptBuilderKJ] import_json had errors; recovered with lenient parse") + return v + except (json.JSONDecodeError, TypeError): + continue + return None + + +def _caption_to_boxes(cap): + # Caption dict -> editor box list ({x,y,w,h, type, text, desc, palette}) for preview/bboxes. + cd = cap.get("compositional_deconstruction") or {} + boxes = [] + for el in (cd.get("elements") or []): + if not isinstance(el, dict): + continue + box = {"type": "text" if el.get("type") == "text" else "obj", + "text": el.get("text", "") or "", "desc": el.get("desc", "") or "", + "palette": list(el.get("color_palette") or [])} + bb = el.get("bbox") + if isinstance(bb, (list, tuple)) and len(bb) == 4: + ymin, xmin, ymax, xmax = bb + box.update(x=xmin / 1000.0, y=ymin / 1000.0, + w=(xmax - xmin) / 1000.0, h=(ymax - ymin) / 1000.0) + else: # no bbox: unplaced placeholder + box.update(x=0.03, y=0.03, w=0.22, h=0.14, nobbox=True) + boxes.append(box) + return boxes + + +class Ideogram4PromptBuilderKJ(io.ComfyNode): + @classmethod + def define_schema(cls): + return io.Schema( + node_id="Ideogram4PromptBuilderKJ", + display_name="Ideogram 4 Prompt Builder KJ", + category="KJNodes/text", + search_aliases=["ideogram", "caption", "bbox", "prompt builder", "json prompt"], + is_experimental=True, + description=""" +Visual prompt builder for Ideogram 4's structured JSON caption format. + +Drag on the canvas to draw regions; select a region to set its type (obj/text), +description, text, and color palette. Set the background and optional style fields +as widgets. Outputs the assembled caption JSON string. + +bbox is normalized to a 0-1000 grid as [ymin, xmin, ymax, xmax]; width/height set +the canvas aspect ratio. + +Canvas controls: +- Drag: draw a new region +- Ctrl/Cmd-drag: force-draw a new region even on top of an existing one +- Click: select a region · Alt-click: cycle overlapping regions +- Double-click: edit the description inline +- Right-click: region list (select / delete / duplicate / reorder, top = front) +- Del / Backspace: remove the selected region +- Ctrl/Cmd + C / V / D: copy / paste / duplicate the selected region +- bbox fields (px / out) next to obj/text are editable + +Color swatches: +- Click: edit · Drag: reorder · Right-click: remove +- Hover + Ctrl/Cmd + C / V: copy / paste the hex +- "+": add a color (uses the clipboard color if it is one) + +Toolbar: +- Live: use the live sampling preview as the background (and grab the final result) +- Grab BG / Clear BG: use the last generated image as the background +- brightness slider, token estimate, and Copy / Paste / Clear all""", + inputs=[ + io.Int.Input("width", default=1024, min=64, max=16384, step=16, + tooltip="Canvas aspect width (also the pixel grid the bbox is measured in). Ideogram 4 needs multiples of 16."), + io.Int.Input("height", default=1024, min=64, max=16384, step=16, + tooltip="Canvas aspect height (also the pixel grid the bbox is measured in). Ideogram 4 needs multiples of 16."), + io.String.Input("high_level_description", multiline=True, default="", + tooltip="Optional one-line overview of the whole image (blank = omitted)."), + io.String.Input("background", multiline=True, default="", + tooltip="Required scene background description."), + io.DynamicCombo.Input("style", options=[ + io.DynamicCombo.Option("none", []), + io.DynamicCombo.Option("photo", [ + io.String.Input("photo", default=""), + ]), + io.DynamicCombo.Option("art_style", [ + io.String.Input("art_style", default=""), + ]), + ]), + io.String.Input("aesthetics", default="", tooltip="Style descriptor (blank = omitted)."), + io.String.Input("lighting", default="", tooltip="Style descriptor (blank = omitted)."), + io.String.Input("medium", default="", tooltip="Style descriptor (blank = omitted)."), + io.Image.Input("image", optional=True, + tooltip="Optional reference image shown as the editor background (and behind the preview)."), + io.String.Input("import_json", default="", optional=True, force_input=True, + tooltip="Optional: a full caption JSON. When connected, it loads into the editor " + "and drives the output per 'import_mode'."), + io.String.Input("style_palette_data", default="", socketless=True, advanced=True, + tooltip="Serialized style color palette from the editor (managed by the node UI)."), + io.String.Input("elements_data", default="", socketless=True, advanced=True, + tooltip="Serialized regions from the editor (managed by the node UI)."), + io.Int.Input("bg_brightness", default=25, min=0, max=100, socketless=True, advanced=True, + tooltip="Background image brightness % (managed by the node UI slider)."), + io.Combo.Input("import_mode", options=["when empty", "always"], default="when empty", + tooltip="How a wired import_json is used: 'when empty' only seeds the editor while " + "it has no regions (then the editor wins, so you can edit); 'always' makes " + "the wired JSON authoritative so its changes always propagate to the output."), + io.String.Input("output_format", default="compact", socketless=True, advanced=True, + tooltip="Output JSON formatting (set via the editor toolbar): 'compact' (default, what " + "Ideogram 4 expects) or 'pretty' (indented, for readability)."), + io.String.Input("coord_mode", default="normalized", socketless=True, advanced=True, + tooltip="bbox coordinate space (set via the editor toolbar): 'normalized' (default, the " + "0-1000 grid Ideogram 4 expects) or 'absolute' (pixels, scaled by width/height " + "— NON-STANDARD for Ideogram, for other tools that want pixel coords)."), + io.String.Input("bbox_order", default="yx", socketless=True, advanced=True, + tooltip="bbox axis order (set via the editor toolbar): 'yx' (default, Ideogram's " + "[ymin,xmin,ymax,xmax]) or 'xy' ([xmin,ymin,xmax,ymax], the standard x1,y1,x2,y2 " + "that Qwen-VL and most detectors use)."), + io.BoundingBox.Input("bboxes", optional=True, force_input=True, + tooltip="Optional pixel-space boxes ({x, y, width, height}) used to seed the " + "editor's regions when it has none. Ignored once regions exist."), + ], + outputs=[ + io.String.Output(display_name="prompt"), + io.Image.Output(display_name="preview"), + io.BoundingBox.Output(display_name="bboxes"), + io.Int.Output(display_name="width"), + io.Int.Output(display_name="height"), + ], + ) + + + @classmethod + def execute(cls, width, height, background, style, + high_level_description="", aesthetics="", lighting="", medium="", + style_palette_data="", elements_data="", import_json="", import_mode="when empty", + output_format="pretty", coord_mode="normalized", bbox_order="yx", bboxes=None, image=None, bg_brightness=25) -> io.NodeOutput: + if import_mode not in ("when empty", "always"): # old workflows saved before this widget existed + import_mode = "when empty" + dump = _dumps if output_format == "pretty" else (lambda v: json.dumps(v, ensure_ascii=False, separators=(",", ":"))) + # bbox scale: 0-1000 grid (Ideogram-standard) or absolute pixels (width/height) when coord_mode=="absolute". + # bbox axis order: "yx" (Ideogram) or "xy" (Qwen/standard x1,y1,x2,y2). + bsx, bsy = (width, height) if coord_mode == "absolute" else (1000, 1000) + border = "xy" if bbox_order == "xy" else "yx" + boxes = _parse_json_list(elements_data) + boxes_seeded = False + if not boxes and bboxes: + if isinstance(bboxes, dict): # a single BoundingBox is a bare {x,y,width,height} dict + frame = [bboxes] + elif bboxes and isinstance(bboxes[0], (list, tuple)): + frame = bboxes[0] # per-frame nesting: [[box, ...], ...] + else: + frame = bboxes # flat list of boxes + for bb in frame: + if not isinstance(bb, dict): + continue + boxes.append({"x": bb.get("x", 0) / width, "y": bb.get("y", 0) / height, + "w": bb.get("width", 0) / width, "h": bb.get("height", 0) / height, + "type": "obj", "text": "", "desc": "", "palette": []}) + boxes_seeded = bool(boxes) + + imported = _loads_caption(import_json) # strict parse, then a lenient repair fallback + + kind = style["style"] # "none" | "photo" | "art_style" + + # Use the wired import_json directly per import_mode: "always" -> authoritative (its changes + # always propagate); "when empty" -> only seed the editor while it has no regions, then the + # editor wins so manual edits stick. The editor mirrors it via ui when used. + used_import = imported is not None and (import_mode == "always" or not boxes) + + if used_import: + caption = imported + boxes = _caption_to_boxes(imported) + else: + caption = {} + if high_level_description.strip(): + caption["high_level_description"] = high_level_description + + if kind != "none": + # The verifier requires every style key present (in order) once a style is + # chosen; only color_palette is conditional. Emit blanks rather than omit. + sd = {"aesthetics": aesthetics, "lighting": lighting} + # photo: ...photo, medium... | art_style: ...medium, art_style... (key order) + if kind == "photo": + sd["photo"] = style.get("photo", "") + sd["medium"] = medium + else: + sd["medium"] = medium + sd["art_style"] = style.get("art_style", "") + palette = _palette(_parse_json_list(style_palette_data)) + if palette: + sd["color_palette"] = palette + caption["style_description"] = sd + + elements = [] + for box in boxes: + if not isinstance(box, dict): + continue + etype = "text" if box.get("type") == "text" else "obj" + elem = {"type": etype} # key order matters + if not box.get("nobbox"): # unplaced elements omit bbox + elem["bbox"] = _norm_bbox(box, bsx, bsy, border) + if etype == "text": + elem["text"] = box.get("text", "") + elem["desc"] = box.get("desc", "") + palette = _palette(box.get("palette", [])) + if palette: + elem["color_palette"] = palette[:5] + elements.append(elem) + + caption["compositional_deconstruction"] = { + "background": background, + "elements": elements, + } + bg = None + if image is not None: # composite over the input image, else black + try: + bg = Image.fromarray((image[0].detach().cpu().numpy() * 255).clip(0, 255).astype(np.uint8)) + except Exception: + bg = None + preview = _render_preview(boxes, width, height, bg, bg_brightness) + + # Pixel-space bboxes ({x, y, width, height}) for SAM3 / BoundingBox consumers. + bbox_dicts = [] + for box in boxes: + if not isinstance(box, dict) or box.get("nobbox"): + continue + x, y = box.get("x", 0.0), box.get("y", 0.0) + bw, bh = box.get("w", 0.0), box.get("h", 0.0) + if bw < 0: + x += bw + bw = -bw + if bh < 0: + y += bh + bh = -bh + bbox_dicts.append({"x": round(x * width), "y": round(y * height), + "width": round(bw * width), "height": round(bh * height)}) + # Per-frame nesting (list[list[dict]]) — the canonical BoundingBox shape that + # SAM3 / crop nodes expect (bboxes[frame] -> list of boxes). + bboxes_out = [bbox_dicts] if bbox_dicts else [] + + # ui: send the resolved width/height so the editor canvas can follow connected + # inputs; import_json (if wired) loads into the editor (output reflects editor only). + ui = {"dims": [width, height]} + if boxes_seeded: + ui["boxes"] = [json.dumps(boxes)] + if used_import: # mirror the import in the editor (only when used) + ui["caption"] = [_dumps(imported)] + return io.NodeOutput(dump(caption), preview, bboxes_out, width, height, ui=ui) diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/image_nodes.py b/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/image_nodes.py new file mode 100644 index 0000000000000000000000000000000000000000..e62fc7d68392bb96798188b92e5f798dbcd63c65 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/image_nodes.py @@ -0,0 +1,5164 @@ +import numpy as np +import time +import torch +import torch.nn.functional as F +import torchvision.transforms as T +import base64 +import random +import math +import os +import re +import json +import importlib +import hashlib +import pathlib +import logging +from io import BytesIO + +try: + import cv2 + HAS_CV2 = True +except ImportError: + logging.warning("OpenCV not installed") + HAS_CV2 = False + +from PIL import ImageGrab, ImageDraw, ImageFont, Image, ImageOps, ImageSequence, ImageStat +from PIL.PngImagePlugin import PngInfo + +from nodes import MAX_RESOLUTION, SaveImage +from comfy_extras.nodes_mask import composite +from comfy.cli_args import args +from comfy.utils import ProgressBar, common_upscale, tiled_scale_multidim +from comfy import model_management +from comfy_api.latest import io, InputImpl, Types, ui +from fractions import Fraction +import node_helpers +import folder_paths + +from ..utility.utility import string_to_color, normalize_bboxes, BBOX_TYPES + +try: + from server import PromptServer, BinaryEventTypes +except ImportError: + PromptServer = None + BinaryEventTypes = None +from concurrent.futures import ThreadPoolExecutor + +script_directory = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + +class ImagePass: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + }, + "optional": { + "image": ("IMAGE",), + }, + } + RETURN_TYPES = ("IMAGE",) + FUNCTION = "passthrough" + CATEGORY = "KJNodes/image" + DESCRIPTION = """ +Passes the image through without modifying it. +""" + + def passthrough(self, image=None): + return image, + +class ColorMatch: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "image_ref": ("IMAGE",), + "image_target": ("IMAGE",), + "method": (['mkl','hm', 'reinhard', 'mvgd', 'hm-mvgd-hm', 'hm-mkl-hm'], { + "default": 'mkl' + }), + }, + "optional": { + "strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}), + "multithread": ("BOOLEAN", {"default": True}), + } + } + + CATEGORY = "KJNodes/image" + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("image",) + FUNCTION = "colormatch" + DEPRECATED = True + DESCRIPTION = """ +color-matcher enables color transfer across images which comes in handy for automatic +color-grading of photographs, paintings and film sequences as well as light-field +and stopmotion corrections. + +The methods behind the mappings are based on the approach from Reinhard et al., +the Monge-Kantorovich Linearization (MKL) as proposed by Pitie et al. and our analytical solution +to a Multi-Variate Gaussian Distribution (MVGD) transfer in conjunction with classical histogram +matching. As shown below our HM-MVGD-HM compound outperforms existing methods. +https://github.com/hahnec/color-matcher/ + +""" + + def colormatch(self, image_ref, image_target, method, strength=1.0, multithread=True): + # Skip unnecessary processing + if strength == 0: + return (image_target,) + + try: + from color_matcher import ColorMatcher + except ImportError as e: + raise ImportError("Can't import color-matcher, did you install requirements.txt? Manual install: pip install color-matcher") from e + + image_ref = image_ref.cpu() + image_target = image_target.cpu() + batch_size = image_target.size(0) + + images_target = image_target.squeeze() + images_ref = image_ref.squeeze() + + image_ref_np = images_ref.numpy() + images_target_np = images_target.numpy() + + def process(i): + cm = ColorMatcher() + image_target_np_i = images_target_np if batch_size == 1 else images_target[i].numpy() + image_ref_np_i = image_ref_np if image_ref.size(0) == 1 else images_ref[i].numpy() + try: + image_result = cm.transfer(src=image_target_np_i, ref=image_ref_np_i, method=method) # Avoid potential blur when only the fully color-matched image is used + if strength != 1: + image_result = image_target_np_i + strength * (image_result - image_target_np_i) + + return torch.from_numpy(image_result) + + except Exception as e: + logging.warning(f"Thread {i} error: {e}") + return torch.from_numpy(image_target_np_i) # fallback + + if multithread and batch_size > 1: + max_threads = min(os.cpu_count() or 1, batch_size) + with ThreadPoolExecutor(max_workers=max_threads) as executor: + out = list(executor.map(process, range(batch_size))) + else: + out = [process(i) for i in range(batch_size)] + + out = torch.stack(out, dim=0).to(torch.float32) + out.clamp_(0, 1) + return (out,) + +class ColorMatchV2(io.ComfyNode): + @classmethod + def define_schema(cls): + return io.Schema( + node_id="ColorMatchV2", + category="KJNodes/image", + description=""" +color-matcher enables color transfer across images which comes in handy for automatic +color-grading of photographs, paintings and film sequences as well as light-field +and stopmotion corrections. + +The methods behind the mappings are based on the approach from Reinhard et al., +the Monge-Kantorovich Linearization (MKL) as proposed by Pitie et al. and our analytical solution +to a Multi-Variate Gaussian Distribution (MVGD) transfer in conjunction with classical histogram +matching. As shown below our HM-MVGD-HM compound outperforms existing methods. +https://github.com/hahnec/color-matcher/ + +'reinhard_lab_gpu' method uses Kornia for GPU-accelerated color transfer in Lab color space. +""", + inputs=[ + io.Image.Input("image_target"), + io.Image.Input("image_ref"), + io.Combo.Input("method", + options=['mkl', 'hm', 'reinhard', 'mvgd', 'hm-mvgd-hm', 'hm-mkl-hm', 'reinhard_lab_gpu'], + default='mkl'), + io.Float.Input("strength", default=1.0, min=0.0, max=10.0, step=0.01), + io.Boolean.Input("multithread", default=True), + ], + outputs=[ + io.Image.Output(display_name="image"), + ], + ) + + @classmethod + def execute(cls, image_target, image_ref, method, strength=1.0, multithread=True) -> io.NodeOutput: + # Skip unnecessary processing + if strength == 0: + return io.NodeOutput(image_target) + + if method == "reinhard_lab_gpu": + import kornia + device = model_management.get_torch_device() + + B, H, W, C = image_target.shape + + src_bchw = image_target.to(device).permute(0, 3, 1, 2).contiguous() # (B, H, W, C) -> (B, C, H, W) + ref_bchw = image_ref.to(device).permute(0, 3, 1, 2).contiguous() + # RGB->Lab + src_lab = kornia.color.rgb_to_lab(src_bchw) + ref_lab = kornia.color.rgb_to_lab(ref_bchw) + + src_lab_flat = src_lab.view(B, C, -1) # (B, C, HW) + ref_lab_flat = ref_lab.view(ref_lab.shape[0], C, -1) # (B or 1, C, HW) + + src_std, src_mean = torch.std_mean(src_lab_flat, dim=-1, keepdim=True, unbiased=False) + ref_std, ref_mean = torch.std_mean(ref_lab_flat, dim=-1, keepdim=True, unbiased=False) + src_std = src_std.clamp_min_(1e-6) + + if ref_lab.shape[0] == 1 and B > 1: + ref_mean = ref_mean.expand(B, -1, -1) + ref_std = ref_std.expand(B, -1, -1) + + corrected_lab_flat = (src_lab_flat - src_mean) * (ref_std / src_std) + ref_mean + corrected_lab = corrected_lab_flat.view(B, C, H, W) + + # Lab->RGB + corrected_rgb_01 = kornia.color.lab_to_rgb(corrected_lab) + out = (1.0 - strength) * src_bchw + strength * corrected_rgb_01 + out = out.permute(0, 2, 3, 1).contiguous() # (B, C, H, W) -> (B, H, W, C) + + return io.NodeOutput(out.cpu().float().clamp_(0, 1)) + + try: + from color_matcher import ColorMatcher + except ImportError as e: + raise ImportError("Can't import color-matcher, did you install requirements.txt? Manual install: pip install color-matcher") from e + + batch_size = image_target.size(0) + ref_batch_size = image_ref.size(0) + + def process(i): + cm = ColorMatcher() + image_target_np = image_target[i].cpu().numpy() + image_ref_np = image_ref[min(i, ref_batch_size - 1)].cpu().numpy() + try: + image_result = cm.transfer(src=image_target_np, ref=image_ref_np, method=method) + if strength != 1: + image_result = image_target_np + strength * (image_result - image_target_np) + + return torch.from_numpy(image_result) + + except Exception as e: + logging.error(f"Thread {i} error: {e}") + return torch.from_numpy(image_target_np) # fallback + if multithread and batch_size > 1: + max_threads = min(os.cpu_count() or 1, batch_size) + with ThreadPoolExecutor(max_workers=max_threads) as executor: + out = list(executor.map(process, range(batch_size))) + else: + out = [process(i) for i in range(batch_size)] + + out = torch.stack(out, dim=0).to(torch.float32).clamp_(0, 1) + + return io.NodeOutput(out) + +class SaveImageWithAlpha: + def __init__(self): + self.output_dir = folder_paths.get_output_directory() + self.type = "output" + self.prefix_append = "" + + @classmethod + def INPUT_TYPES(s): + return {"required": + {"images": ("IMAGE", ), + "mask": ("MASK", ), + "filename_prefix": ("STRING", {"default": "ComfyUI"})}, + "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"}, + } + + RETURN_TYPES = () + FUNCTION = "save_images_alpha" + OUTPUT_NODE = True + CATEGORY = "KJNodes/image" + DESCRIPTION = """ +Saves an image and mask as .PNG with the mask as the alpha channel. +""" + + def save_images_alpha(self, images, mask, filename_prefix="ComfyUI_image_with_alpha", prompt=None, extra_pnginfo=None): + filename_prefix += self.prefix_append + full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(filename_prefix, self.output_dir, images[0].shape[1], images[0].shape[0]) + results = list() + def file_counter(): + max_counter = 0 + # Loop through the existing files + for existing_file in sorted(os.listdir(full_output_folder)): + # Check if the file matches the expected format + match = re.fullmatch(fr"{filename}_(\d+)_?\.[a-zA-Z0-9]+", existing_file) + if match: + # Extract the numeric portion of the filename + file_counter = int(match.group(1)) + # Update the maximum counter value if necessary + if file_counter > max_counter: + max_counter = file_counter + return max_counter + + for image, alpha in zip(images, mask): + i = 255. * image.cpu().numpy() + a = 255. * (1.0 - alpha.cpu().float()).numpy() + img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8)) + + # Resize the mask to match the image size + a_resized = Image.fromarray(a).resize(img.size, Image.LANCZOS) + a_resized = np.clip(a_resized, 0, 255).astype(np.uint8) + img.putalpha(Image.fromarray(a_resized, mode='L')) + metadata = None + if not args.disable_metadata: + metadata = PngInfo() + if prompt is not None: + metadata.add_text("prompt", json.dumps(prompt)) + if extra_pnginfo is not None: + for x in extra_pnginfo: + metadata.add_text(x, json.dumps(extra_pnginfo[x])) + + # Increment the counter by 1 to get the next available value + counter = file_counter() + 1 + file = f"{filename}_{counter:05}.png" + img.save(os.path.join(full_output_folder, file), pnginfo=metadata, compress_level=4) + results.append({ + "filename": file, + "subfolder": subfolder, + "type": self.type + }) + + return { "ui": { "images": results } } + + +class ImageConcanate(io.ComfyNode): + @classmethod + def define_schema(cls): + # image1 drives the output type; image2 can independently be IMAGE or MASK and gets + # converted to image1's type inside concatenate(). + type_template = io.MatchType.Template("image_or_mask", allowed_types=[io.Image, io.Mask]) + return io.Schema( + node_id="ImageConcanate", + category="KJNodes/image", + description=( + "Concatenates image2 to image1 in the specified direction.\n" + "Both inputs accept IMAGE or MASK; the output type follows image1.\n" + "If image2 is a different type than image1 it's converted (RGB mean for image→mask,\n" + "channel-replicate for mask→image).\n" + "When match_image_size is False and dimensions don't match along the shared axis,\n" + "the smaller image is centered and zero-padded instead of erroring." + ), + inputs=[ + io.MatchType.Input("image1", template=type_template), + io.MultiType.Input("image2", types=[io.Image, io.Mask]), + io.Combo.Input("direction", options=['right', 'down', 'left', 'up'], default='right'), + io.Boolean.Input("match_image_size", default=True), + ], + outputs=[ + io.MatchType.Output(template=type_template, display_name="output"), + ], + ) + + @classmethod + def execute(cls, image1, image2, direction, match_image_size) -> io.NodeOutput: + return io.NodeOutput(cls.concatenate(image1, image2, direction, match_image_size)) + + @staticmethod + def concatenate(image1, image2, direction, match_image_size, first_image_shape=None): + # IMAGE is BHWC, MASK is BHW. Output type follows image1; convert image2 to match, + # then unsqueeze any masks to BHW1 so the rest of the function can stay BHWC-only. + output_is_mask = image1.dim() == 3 + if output_is_mask and image2.dim() == 4: + ch = min(3, image2.shape[-1]) + image2 = image2[..., :ch].mean(dim=-1) + elif not output_is_mask and image2.dim() == 3: + image2 = image2.unsqueeze(-1).expand(-1, -1, -1, image1.shape[-1]) + if output_is_mask: + image1 = image1.unsqueeze(-1) + image2 = image2.unsqueeze(-1) + + bs1 = image1.shape[0] + bs2 = image2.shape[0] + B = max(bs1, bs2) + + H1, W1 = image1.shape[1], image1.shape[2] + C1, C2 = image1.shape[-1], image2.shape[-1] + out_C = max(C1, C2) + + if match_image_size: + target_shape = first_image_shape if first_image_shape is not None else image1.shape + orig_aspect = image2.shape[2] / image2.shape[1] + if direction in ('left', 'right'): + H2 = target_shape[1] + W2 = int(H2 * orig_aspect) + else: + W2 = target_shape[2] + H2 = int(W2 / orig_aspect) + else: + H2, W2 = image2.shape[1], image2.shape[2] + + if direction in ('right', 'left'): + out_H, out_W = max(H1, H2), W1 + W2 + else: + out_H, out_W = H1 + H2, max(W1, W2) + + if direction == 'right': + i1_y, i1_x, i2_y, i2_x = (out_H - H1) // 2, 0, (out_H - H2) // 2, W1 + elif direction == 'left': + i1_y, i1_x, i2_y, i2_x = (out_H - H1) // 2, W2, (out_H - H2) // 2, 0 + elif direction == 'down': + i1_y, i1_x, i2_y, i2_x = 0, (out_W - W1) // 2, H1, (out_W - W2) // 2 + else: # 'up' + i1_y, i1_x, i2_y, i2_x = H2, (out_W - W1) // 2, 0, (out_W - W2) // 2 + + output = torch.zeros( + (B, out_H, out_W, out_C), + dtype=model_management.intermediate_dtype(), + device=model_management.intermediate_device(), + ) + + def write(dst, src, src_C): + if dst.shape[-1] == src_C: + dst.copy_(src) + else: + dst[..., :src_C].copy_(src) + dst[..., src_C:].fill_(1.0) + + slot1 = output[:, i1_y:i1_y + H1, i1_x:i1_x + W1, :] + if bs1 == B: + write(slot1, image1, C1) + else: + write(slot1[:bs1], image1, C1) + write(slot1[bs1:], image1[-1:].expand(B - bs1, -1, -1, -1), C1) + del slot1 + + slot2 = output[:, i2_y:i2_y + H2, i2_x:i2_x + W2, :] + if match_image_size: + pbar = ProgressBar(B) + device = model_management.get_torch_device() + for i in range(B): + src_i = min(i, bs2 - 1) + frame = image2[src_i:src_i + 1].to(device, non_blocking=True).permute(0, 3, 1, 2) + resized = F.interpolate(frame, size=(H2, W2), mode='bicubic', antialias=True).permute(0, 2, 3, 1) + write(slot2[i:i + 1], resized, C2) + del frame, resized + pbar.update(1) + else: + if bs2 == B: + write(slot2, image2, C2) + else: + write(slot2[:bs2], image2, C2) + write(slot2[bs2:], image2[-1:].expand(B - bs2, -1, -1, -1), C2) + del slot2 + + if output_is_mask: + return output.squeeze(-1) + return output + + +class ImageConcatFromBatch: + @classmethod + def INPUT_TYPES(s): + return {"required": { + "images": ("IMAGE",), + "num_columns": ("INT", {"default": 3, "min": 1, "max": 255, "step": 1}), + "match_image_size": ("BOOLEAN", {"default": False}), + "max_resolution": ("INT", {"default": 4096}), + }, + } + + RETURN_TYPES = ("IMAGE",) + FUNCTION = "concat" + CATEGORY = "KJNodes/image" + DESCRIPTION = """ + Concatenates images from a batch into a grid with a specified number of columns. + """ + + def concat(self, images, num_columns, match_image_size, max_resolution): + # Assuming images is a batch of images (B, H, W, C) + batch_size, height, width, channels = images.shape + num_rows = (batch_size + num_columns - 1) // num_columns # Calculate number of rows + + logging.info(f"Initial dimensions: batch_size={batch_size}, height={height}, width={width}, channels={channels}") + logging.info(f"num_rows={num_rows}, num_columns={num_columns}") + + if match_image_size: + target_shape = images[0].shape + + resized_images = [] + for image in images: + original_height = image.shape[0] + original_width = image.shape[1] + original_aspect_ratio = original_width / original_height + + if original_aspect_ratio > 1: + target_height = target_shape[0] + target_width = int(target_height * original_aspect_ratio) + else: + target_width = target_shape[1] + target_height = int(target_width / original_aspect_ratio) + + logging.info(f"Resizing image from ({original_height}, {original_width}) to ({target_height}, {target_width})") + + # Resize the image to match the target size while preserving aspect ratio + resized_image = common_upscale(image.movedim(-1, 0), target_width, target_height, "lanczos", "disabled") + resized_image = resized_image.movedim(0, -1) # Move channels back to the last dimension + resized_images.append(resized_image) + + # Convert the list of resized images back to a tensor + images = torch.stack(resized_images) + + height, width = target_shape[:2] # Update height and width + + # Initialize an empty grid + grid_height = num_rows * height + grid_width = num_columns * width + + logging.info(f"Grid dimensions before scaling: grid_height={grid_height}, grid_width={grid_width}") + + # Original scale factor calculation remains unchanged + scale_factor = min(max_resolution / grid_height, max_resolution / grid_width, 1.0) + + # Apply scale factor to height and width + scaled_height = height * scale_factor + scaled_width = width * scale_factor + + # Round scaled dimensions to the nearest number divisible by 8 + height = max(1, int(round(scaled_height / 8) * 8)) + width = max(1, int(round(scaled_width / 8) * 8)) + + if abs(scaled_height - height) > 4: + height = max(1, int(round((scaled_height + 4) / 8) * 8)) + if abs(scaled_width - width) > 4: + width = max(1, int(round((scaled_width + 4) / 8) * 8)) + + # Recalculate grid dimensions with adjusted height and width + grid_height = num_rows * height + grid_width = num_columns * width + logging.info(f"Grid dimensions after scaling: grid_height={grid_height}, grid_width={grid_width}") + logging.info(f"Final image dimensions: height={height}, width={width}") + + grid = torch.zeros((grid_height, grid_width, channels), dtype=images.dtype) + + for idx, image in enumerate(images): + resized_image = torch.nn.functional.interpolate(image.unsqueeze(0).permute(0, 3, 1, 2), size=(height, width), mode="bilinear").squeeze().permute(1, 2, 0) + row = idx // num_columns + col = idx % num_columns + grid[row*height:(row+1)*height, col*width:(col+1)*width, :] = resized_image + + return grid.unsqueeze(0), + + +class ImageGridComposite2x2: + @classmethod + def INPUT_TYPES(s): + return {"required": { + "image1": ("IMAGE",), + "image2": ("IMAGE",), + "image3": ("IMAGE",), + "image4": ("IMAGE",), + }} + + RETURN_TYPES = ("IMAGE",) + FUNCTION = "compositegrid" + CATEGORY = "KJNodes/image" + DESCRIPTION = """ +Concatenates the 4 input images into a 2x2 grid. +""" + + def compositegrid(self, image1, image2, image3, image4): + top_row = torch.cat((image1, image2), dim=2) + bottom_row = torch.cat((image3, image4), dim=2) + grid = torch.cat((top_row, bottom_row), dim=1) + return (grid,) + +class ImageGridComposite3x3: + @classmethod + def INPUT_TYPES(s): + return {"required": { + "image1": ("IMAGE",), + "image2": ("IMAGE",), + "image3": ("IMAGE",), + "image4": ("IMAGE",), + "image5": ("IMAGE",), + "image6": ("IMAGE",), + "image7": ("IMAGE",), + "image8": ("IMAGE",), + "image9": ("IMAGE",), + }} + + RETURN_TYPES = ("IMAGE",) + FUNCTION = "compositegrid" + CATEGORY = "KJNodes/image" + DESCRIPTION = """ +Concatenates the 9 input images into a 3x3 grid. +""" + + def compositegrid(self, image1, image2, image3, image4, image5, image6, image7, image8, image9): + top_row = torch.cat((image1, image2, image3), dim=2) + mid_row = torch.cat((image4, image5, image6), dim=2) + bottom_row = torch.cat((image7, image8, image9), dim=2) + grid = torch.cat((top_row, mid_row, bottom_row), dim=1) + return (grid,) + + +class ImageBatchTestPattern(io.ComfyNode): + @classmethod + def define_schema(cls): + return io.Schema( + node_id="ImageBatchTestPattern", + category="KJNodes/text", + description="Generate a batch of images with sequential numbers rendered in a chosen font.", + inputs=[ + io.Int.Input("batch_size", default=1, min=1, max=4096, step=1), + io.Int.Input("start_from", default=0, min=0, max=4096, step=1), + io.Int.Input("text_x", default=256, min=0, max=4096, step=1), + io.Int.Input("text_y", default=256, min=0, max=4096, step=1), + io.Int.Input("width", default=512, min=16, max=4096, step=1), + io.Int.Input("height", default=512, min=16, max=4096, step=1), + io.Combo.Input("font", options=folder_paths.get_filename_list("kjnodes_fonts")), + io.Int.Input("font_size", default=255, min=8, max=4096, step=1), + ], + outputs=[ + io.Image.Output(display_name="image"), + ], + ) + + @classmethod + def execute(cls, batch_size, font, font_size, start_from, width, height, text_x, text_y) -> io.NodeOutput: + font_path = folder_paths.get_full_path("kjnodes_fonts", font) + pil_font = ImageFont.truetype(font_path, font_size) + + # Probe once whether the '-liga' feature is supported by this PIL build/font + use_liga = True + try: + ImageDraw.Draw(Image.new("RGB", (1, 1))).text( + (0, 0), "0", font=pil_font, fill=(0, 0, 0), features=['-liga'] + ) + except Exception: + use_liga = False + + image = Image.new("RGB", (width, height), color='black') + draw = ImageDraw.Draw(image) + + out_buf = np.empty((batch_size, height, width, 3), dtype=np.uint8) + pbar = ProgressBar(batch_size) + + for i in range(batch_size): + # Reset canvas to black instead of allocating a new PIL image + draw.rectangle((0, 0, width, height), fill='black') + + font_color = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)) + text = str(start_from + i) + + if use_liga: + draw.text((text_x, text_y), text, font=pil_font, fill=font_color, features=['-liga']) + else: + draw.text((text_x, text_y), text, font=pil_font, fill=font_color) + + out_buf[i] = np.asarray(image) + pbar.update(1) + + out_tensor = torch.from_numpy(out_buf).to( + device=model_management.intermediate_device(), + dtype=model_management.intermediate_dtype(), + ).div_(255.0) + return io.NodeOutput(out_tensor) + +class ImageGrabPIL: + + @classmethod + def IS_CHANGED(cls): + + return + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("image",) + FUNCTION = "screencap" + CATEGORY = "KJNodes/image" + DESCRIPTION = """ +Captures an area specified by screen coordinates. +Can be used for realtime diffusion with autoqueue. +""" + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "x": ("INT", {"default": 0,"min": 0, "max": 4096, "step": 1}), + "y": ("INT", {"default": 0,"min": 0, "max": 4096, "step": 1}), + "width": ("INT", {"default": 512,"min": 0, "max": 4096, "step": 1}), + "height": ("INT", {"default": 512,"min": 0, "max": 4096, "step": 1}), + "num_frames": ("INT", {"default": 1,"min": 1, "max": 255, "step": 1}), + "delay": ("FLOAT", {"default": 0.1,"min": 0.0, "max": 10.0, "step": 0.01}), + }, + } + + def screencap(self, x, y, width, height, num_frames, delay): + start_time = time.time() + captures = [] + bbox = (x, y, x + width, y + height) + + for _ in range(num_frames): + # Capture screen + screen_capture = ImageGrab.grab(bbox=bbox) + screen_capture_torch = torch.from_numpy(np.array(screen_capture, dtype=np.float32) / 255.0).unsqueeze(0) + captures.append(screen_capture_torch) + + # Wait for a short delay if more than one frame is to be captured + if num_frames > 1: + time.sleep(delay) + + elapsed_time = time.time() - start_time + logging.info(f"screengrab took {elapsed_time} seconds.") + + return (torch.cat(captures, dim=0),) + +class Screencap_mss: + + @classmethod + def IS_CHANGED(s, **kwargs): + return float("NaN") + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("image",) + FUNCTION = "screencap" + CATEGORY = "KJNodes/image" + DESCRIPTION = """ +Captures an area specified by screen coordinates. +Can be used for realtime diffusion with autoqueue. +""" + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "x": ("INT", {"default": 0,"min": 0, "max": 10000, "step": 1}), + "y": ("INT", {"default": 0,"min": 0, "max": 10000, "step": 1}), + "width": ("INT", {"default": 512,"min": 0, "max": 10000, "step": 1}), + "height": ("INT", {"default": 512,"min": 0, "max": 10000, "step": 1}), + "num_frames": ("INT", {"default": 1,"min": 1, "max": 255, "step": 1}), + "delay": ("FLOAT", {"default": 0.1,"min": 0.0, "max": 10.0, "step": 0.01}), + }, + } + + def screencap(self, x, y, width, height, num_frames, delay): + from mss import mss + captures = [] + with mss() as sct: + bbox = {'top': y, 'left': x, 'width': width, 'height': height} + + for _ in range(num_frames): + sct_img = sct.grab(bbox) + img_np = np.array(sct_img) + img_torch = torch.from_numpy(img_np[..., [2, 1, 0]]).float() / 255.0 + captures.append(img_torch) + + if num_frames > 1: + time.sleep(delay) + + return (torch.stack(captures, 0),) + +class ScreencapStream: + + @classmethod + def IS_CHANGED(s, **kwargs): + return float("NaN") + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("image",) + FUNCTION = "capture" + CATEGORY = "KJNodes/image" + DESCRIPTION = """ +Captures a frame from a browser screen/window share stream. +Click 'Start capture' to select a screen or window to share. +Live preview is shown in the node. Works with auto-queue. + +Crop controls: +- Drag on preview to draw a crop box +- Drag inside the box to move it +- Drag edges or corners to resize +- Shift+drag to lock aspect ratio +- Right-click or double-click to clear crop +""" + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "frame_data": ("STRING", {"default": "", "multiline": False}), + "crop_width": ("INT", {"default": 1, "min": 1, "max": MAX_RESOLUTION, "step": 1}), + "crop_height": ("INT", {"default": 1, "min": 1, "max": MAX_RESOLUTION, "step": 1}), + }, + } + + MAX_FRAME_BYTES = 50 * 1024 * 1024 # 50MB base64 limit (PNG is larger than JPEG) + + def capture(self, crop_width, crop_height, frame_data): + if not frame_data: + w = crop_width if crop_width > 0 else 512 + h = crop_height if crop_height > 0 else 512 + return (torch.zeros(1, h, w, 3),) + if len(frame_data) > self.MAX_FRAME_BYTES: + raise ValueError(f"Frame data exceeds {self.MAX_FRAME_BYTES // (1024*1024)}MB limit") + try: + img_bytes = base64.b64decode(frame_data.split(",", 1)[-1]) + except Exception: + raise ValueError("Invalid frame data encoding") + img = Image.open(BytesIO(img_bytes)).convert("RGB") + img_np = np.array(img).astype(np.float32) / 255.0 + img_tensor = torch.from_numpy(img_np).unsqueeze(0) + return (img_tensor,) + +class WebcamCaptureCV2: + + @classmethod + def IS_CHANGED(cls): + return + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("image",) + FUNCTION = "capture" + CATEGORY = "KJNodes/experimental" + DESCRIPTION = """ +Captures a frame from a webcam using CV2. +Can be used for realtime diffusion with autoqueue. +""" + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "x": ("INT", {"default": 0,"min": 0, "max": 4096, "step": 1}), + "y": ("INT", {"default": 0,"min": 0, "max": 4096, "step": 1}), + "width": ("INT", {"default": 512,"min": 0, "max": 4096, "step": 1}), + "height": ("INT", {"default": 512,"min": 0, "max": 4096, "step": 1}), + "cam_index": ("INT", {"default": 0,"min": 0, "max": 255, "step": 1}), + "release": ("BOOLEAN", {"default": False}), + }, + } + + def capture(self, x, y, cam_index, width, height, release): + # Check if the camera index has changed or the capture object doesn't exist + if not hasattr(self, "cap") or self.cap is None or self.current_cam_index != cam_index: + if hasattr(self, "cap") and self.cap is not None: + self.cap.release() + self.current_cam_index = cam_index + self.cap = cv2.VideoCapture(cam_index) + try: + self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, width) + self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, height) + except cv2.error: + pass + if not self.cap.isOpened(): + raise RuntimeError("Could not open webcam") + + ret, frame = self.cap.read() + if not ret: + raise RuntimeError("Failed to capture image from webcam") + + # Crop the frame to the specified bbox + frame = frame[y:y+height, x:x+width] + img_torch = torch.from_numpy(frame[..., [2, 1, 0]]).float() / 255.0 + + if release: + self.cap.release() + self.cap = None + + return (img_torch.unsqueeze(0),) + +class AddLabel: + @classmethod + def INPUT_TYPES(s): + return {"required": { + "image":("IMAGE",), + "text_x": ("INT", {"default": 10, "min": 0, "max": 4096, "step": 1}), + "text_y": ("INT", {"default": 2, "min": 0, "max": 4096, "step": 1}), + "height": ("INT", {"default": 48, "min": -1, "max": 4096, "step": 1}), + "font_size": ("INT", {"default": 32, "min": 0, "max": 4096, "step": 1}), + "font_color": ("STRING", {"default": "white"}), + "label_color": ("STRING", {"default": "black"}), + "font": (folder_paths.get_filename_list("kjnodes_fonts"), ), + "text": ("STRING", {"default": "Text"}), + "direction": ( + [ 'up', + 'down', + 'left', + 'right', + 'overlay' + ], + { + "default": 'up' + }), + }, + "optional":{ + "caption": ("STRING", {"default": "", "forceInput": True}), + } + } + RETURN_TYPES = ("IMAGE",) + FUNCTION = "addlabel" + CATEGORY = "KJNodes/text" + DESCRIPTION = """ +Creates a new with the given text, and concatenates it to +either above or below the input image. +Note that this changes the input image's height! +Fonts are loaded from this folder: +ComfyUI/custom_nodes/ComfyUI-KJNodes/fonts +""" + + def addlabel(self, image, text_x, text_y, text, height, font_size, font_color, label_color, font, direction, caption=""): + batch_size = image.shape[0] + width = image.shape[2] + channels = image.shape[3] + # match the label to the input, so alpha images stay 4 channel + pil_mode = "RGBA" if channels == 4 else "RGB" + + font_path = os.path.join(script_directory, "fonts", "TTNorms-Black.otf") if font == "TTNorms-Black.otf" else folder_paths.get_full_path("kjnodes_fonts", font) + + # Parse colors using helper function + font_color_rgb = string_to_color(font_color) + label_color_rgb = string_to_color(label_color) + + # Convert to tuples for PIL + font_color_tuple = tuple(font_color_rgb[:3]) # RGB only + label_color_tuple = tuple(label_color_rgb[:3]) # RGB only + if pil_mode == "RGBA": + font_color_tuple += (font_color_rgb[3] if len(font_color_rgb) > 3 else 255,) + label_color_tuple += (label_color_rgb[3] if len(label_color_rgb) > 3 else 255,) + + def process_image(input_image, caption_text): + font = ImageFont.truetype(font_path, font_size) + lines = [] + for text_line in caption_text.split('\n'): + if text_line.strip() == "": + # Preserve empty lines for multiple newlines + lines.append("") + continue + words = text_line.split() + current_line = [] + for word in words: + if current_line: + test_line = " ".join(current_line + [word]) + else: + test_line = word + try: + test_line_width = font.getbbox(test_line)[2] + except Exception: + test_line_width = font.getsize(test_line)[0] + if test_line_width <= width - 2 * text_x: + current_line.append(word) + else: + lines.append(" ".join(current_line)) + current_line = [word] + if current_line: + lines.append(" ".join(current_line)) + + if direction == 'overlay': + pil_image = Image.fromarray((input_image.cpu().numpy() * 255).astype(np.uint8)) + else: + if height == -1: + # Adjust the image height automatically + margin = 8 + required_height = (text_y + len(lines) * font_size) + margin # Calculate required height + pil_image = Image.new(pil_mode, (width, required_height), label_color_tuple) + else: + # Initialize with a minimal height + label_image = Image.new(pil_mode, (width, height), label_color_tuple) + pil_image = label_image + + draw = ImageDraw.Draw(pil_image) + + + y_offset = text_y + for line in lines: + try: + draw.text((text_x, y_offset), line, font=font, fill=font_color_tuple, features=['-liga']) + except Exception: + draw.text((text_x, y_offset), line, font=font, fill=font_color_tuple) + y_offset += font_size + + processed_image = torch.from_numpy(np.array(pil_image).astype(np.float32) / 255.0).unsqueeze(0) + return processed_image + + if caption == "": + processed_images = [process_image(img, text) for img in image] + else: + assert len(caption) == batch_size, f"Number of captions {(len(caption))} does not match number of images" + processed_images = [process_image(img, cap) for img, cap in zip(image, caption)] + processed_batch = torch.cat(processed_images, dim=0) + + # Combine images based on direction + if direction == 'down': + combined_images = torch.cat((image, processed_batch), dim=1) + elif direction == 'up': + combined_images = torch.cat((processed_batch, image), dim=1) + elif direction == 'left': + processed_batch = torch.rot90(processed_batch, 3, (2, 3)).permute(0, 3, 1, 2) + combined_images = torch.cat((processed_batch, image), dim=2) + elif direction == 'right': + processed_batch = torch.rot90(processed_batch, 3, (2, 3)).permute(0, 3, 1, 2) + combined_images = torch.cat((image, processed_batch), dim=2) + else: + combined_images = processed_batch + + return (combined_images,) + +class GetImageSizeAndCount: + @classmethod + def INPUT_TYPES(s): + return {"required": { + "image": ("IMAGE",), + }} + + RETURN_TYPES = ("IMAGE","INT", "INT", "INT",) + RETURN_NAMES = ("image", "width", "height", "count",) + FUNCTION = "getsize" + CATEGORY = "KJNodes/image" + DESCRIPTION = """ +Returns width, height and batch size of the image, +and passes it through unchanged. + +""" + + def getsize(self, image): + width = image.shape[2] + height = image.shape[1] + count = image.shape[0] + return {"ui": { + "text": [f"{count}x{width}x{height}"]}, + "result": (image, width, height, count) + } + +class GetLatentSizeAndCount: + @classmethod + def INPUT_TYPES(s): + return {"required": { + "latent": ("LATENT",), + }} + + RETURN_TYPES = ("LATENT","INT", "INT", "INT", "INT", "INT") + RETURN_NAMES = ("latent", "batch_size", "channels", "frames", "height", "width") + FUNCTION = "getsize" + CATEGORY = "KJNodes/image" + DESCRIPTION = """ +Returns latent tensor dimensions, +and passes the latent through unchanged. + +""" + def getsize(self, latent): + if len(latent["samples"].shape) == 5: + B, C, T, H, W = latent["samples"].shape + elif len(latent["samples"].shape) == 4: + B, C, H, W = latent["samples"].shape + T = 0 + else: + raise ValueError("Invalid latent shape") + + return {"ui": { + "text": [f"{B}x{C}x{T}x{H}x{W}"]}, + "result": (latent, B, C, T, H, W) + } + +class ImageBatchRepeatInterleaving: + RETURN_TYPES = ("IMAGE", "MASK",) + FUNCTION = "repeat" + CATEGORY = "KJNodes/image" + DESCRIPTION = """ +Repeats each image in a batch by the specified number of times. +Example batch of 5 images: 0, 1 ,2, 3, 4 +with repeats 2 becomes batch of 10 images: 0, 0, 1, 1, 2, 2, 3, 3, 4, 4 +""" + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "images": ("IMAGE",), + "repeats": ("INT", {"default": 1, "min": 1, "max": 4096}), + }, + "optional": { + "mask": ("MASK",), + } + } + + def repeat(self, images, repeats, mask=None): + original_count = images.shape[0] + total_count = original_count * repeats + + repeated_images = torch.repeat_interleave(images, repeats=repeats, dim=0) + if mask is not None: + mask = torch.repeat_interleave(mask, repeats=repeats, dim=0) + else: + mask = torch.zeros((total_count, images.shape[1], images.shape[2]), + device=images.device, dtype=images.dtype) + for i in range(original_count): + mask[i * repeats] = 1.0 + + return (repeated_images, mask) + +class ImageUpscaleWithModelBatched: + @classmethod + def INPUT_TYPES(s): + return {"required": { "upscale_model": ("UPSCALE_MODEL",), + "images": ("IMAGE",), + "per_batch": ("INT", {"default": 16, "min": 1, "max": 4096, "step": 1}), + }, + "optional": { + "downscale_ratio": ("FLOAT", {"default": 1.0, "min": 0.01, "max": 1.0, "step": 0.01}), + "downscale_method": (["nearest-exact", "bilinear", "area", "bicubic", "lanczos"], {"default": "lanczos"}), + "precision": (["float32", "float16", "bfloat16"], {"default": "float32"}), + }} + RETURN_TYPES = ("IMAGE",) + FUNCTION = "upscale" + CATEGORY = "KJNodes/image" + DESCRIPTION = """ +Same as ComfyUI native model upscaling node, +but allows setting sub-batches for reduced VRAM usage. +Optionally downscale the result with a ratio. +""" + def upscale(self, upscale_model, images, per_batch, downscale_ratio=1.0, downscale_method="lanczos", precision="float32"): + dtype = torch.float16 if precision == "float16" else torch.bfloat16 if precision == "bfloat16" else torch.float32 + device = model_management.get_torch_device() + upscale_model.to(device, dtype=dtype) + in_img = images.movedim(-1,-3).to(dtype) + + steps = in_img.shape[0] + pbar = ProgressBar(steps) + t = [] + + for start_idx in range(0, in_img.shape[0], per_batch): + sub_images = upscale_model(in_img[start_idx:start_idx+per_batch].to(device)) + t.append(sub_images.cpu()) + # Calculate the number of images processed in this batch + batch_count = sub_images.shape[0] + # Update the progress bar by the number of images processed in this batch + pbar.update(batch_count) + upscale_model.cpu() + + t = torch.cat(t, dim=0).permute(0, 2, 3, 1).cpu().float() + + # Apply downscaling if ratio is less than 1.0 + if downscale_ratio < 1.0: + original_height = t.shape[1] + original_width = t.shape[2] + new_height = int(original_height * downscale_ratio) + new_width = int(original_width * downscale_ratio) + t = t.movedim(-1, 1) + t = common_upscale(t, new_width, new_height, downscale_method, "disabled") + t = t.movedim(1, -1) + + return (t,) + +class ImageNormalize_Neg1_To_1: + @classmethod + def INPUT_TYPES(s): + return {"required": { + "images": ("IMAGE",), + + }} + RETURN_TYPES = ("IMAGE",) + FUNCTION = "normalize" + CATEGORY = "KJNodes/image" + DESCRIPTION = """ +Normalize the images to be in the range [-1, 1] +""" + + def normalize(self,images): + images = images * 2.0 - 1.0 + return (images,) + +class RemapImageRange: + @classmethod + def INPUT_TYPES(s): + return {"required": { + "image": ("IMAGE",), + "min": ("FLOAT", {"default": 0.0,"min": -10.0, "max": 1.0, "step": 0.01}), + "max": ("FLOAT", {"default": 1.0,"min": 0.0, "max": 10.0, "step": 0.01}), + "clamp": ("BOOLEAN", {"default": True}), + }, + } + + RETURN_TYPES = ("IMAGE",) + FUNCTION = "remap" + CATEGORY = "KJNodes/image" + DESCRIPTION = """ +Remaps the image values to the specified range. +""" + + def remap(self, image, min, max, clamp): + if image.dtype == torch.float16: + image = image.to(torch.float32) + image = min + image * (max - min) + if clamp: + image = torch.clamp(image, min=0.0, max=1.0) + return (image, ) + +class SplitImageChannels: + @classmethod + def INPUT_TYPES(s): + return {"required": { + "image": ("IMAGE",), + }, + } + + RETURN_TYPES = ("IMAGE", "IMAGE", "IMAGE", "MASK") + RETURN_NAMES = ("red", "green", "blue", "mask") + FUNCTION = "split" + CATEGORY = "KJNodes/image" + DESCRIPTION = """ +Splits image channels into images where the selected channel +is repeated for all channels, and the alpha as a mask. +""" + + def split(self, image): + red = image[:, :, :, 0:1] # Red channel + green = image[:, :, :, 1:2] # Green channel + blue = image[:, :, :, 2:3] # Blue channel + if image.shape[3] == 4: + alpha = image[:, :, :, 4] # Alpha channel + else: + alpha = torch.zeros(image.shape[0], image.shape[1], image.shape[2], device=image.device) + + # Repeat the selected channel for all channels + red = torch.cat([red, red, red], dim=3) + green = torch.cat([green, green, green], dim=3) + blue = torch.cat([blue, blue, blue], dim=3) + return (red, green, blue, alpha) + +class MergeImageChannels: + @classmethod + def INPUT_TYPES(s): + return {"required": { + "red": ("IMAGE",), + "green": ("IMAGE",), + "blue": ("IMAGE",), + + }, + "optional": { + "alpha": ("MASK", {"default": None}), + }, + } + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("image",) + FUNCTION = "merge" + CATEGORY = "KJNodes/image" + DESCRIPTION = """ +Merges channel data into an image. +""" + + def merge(self, red, green, blue, alpha=None): + image = torch.stack([ + red[..., 0, None], # Red channel + green[..., 1, None], # Green channel + blue[..., 2, None] # Blue channel + ], dim=-1) + image = image.squeeze(-2) + if alpha is not None: + image = torch.cat([image, alpha.unsqueeze(-1)], dim=-1) + return (image,) + +class ImagePadForOutpaintMasked: + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "image": ("IMAGE",), + "left": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 8}), + "top": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 8}), + "right": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 8}), + "bottom": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 8}), + "feathering": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 1}), + }, + "optional": { + "mask": ("MASK",), + } + } + + RETURN_TYPES = ("IMAGE", "MASK") + FUNCTION = "expand_image" + + CATEGORY = "image" + + def expand_image(self, image, left, top, right, bottom, feathering, mask=None): + if mask is not None: + if torch.allclose(mask, torch.zeros_like(mask)): + logging.warning("The incoming mask is fully black. Handling it as None.") + mask = None + B, H, W, C = image.size() + + new_image = torch.ones( + (B, H + top + bottom, W + left + right, C), + dtype=torch.float32, + ) * 0.5 + + new_image[:, top:top + H, left:left + W, :] = image + + if mask is None: + new_mask = torch.ones( + (B, H + top + bottom, W + left + right), + dtype=torch.float32, + ) + + t = torch.zeros( + (B, H, W), + dtype=torch.float32 + ) + else: + # If a mask is provided, pad it to fit the new image size + mask = F.pad(mask, (left, right, top, bottom), mode='constant', value=0) + mask = 1 - mask + t = torch.zeros_like(mask) + + if feathering > 0 and feathering * 2 < H and feathering * 2 < W: + + for i in range(H): + for j in range(W): + dt = i if top != 0 else H + db = H - i if bottom != 0 else H + + dl = j if left != 0 else W + dr = W - j if right != 0 else W + + d = min(dt, db, dl, dr) + + if d >= feathering: + continue + + v = (feathering - d) / feathering + + if mask is None: + t[:, i, j] = v * v + else: + t[:, top + i, left + j] = v * v + + if mask is None: + new_mask[:, top:top + H, left:left + W] = t + return (new_image, new_mask,) + else: + return (new_image, mask,) + +class ImagePadForOutpaintTargetSize: + upscale_methods = ["nearest-exact", "bilinear", "area", "bicubic", "lanczos"] + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "image": ("IMAGE",), + "target_width": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 8}), + "target_height": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 8}), + "feathering": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 1}), + "upscale_method": (s.upscale_methods,), + }, + "optional": { + "mask": ("MASK",), + } + } + + RETURN_TYPES = ("IMAGE", "MASK") + FUNCTION = "expand_image" + + CATEGORY = "image" + + def expand_image(self, image, target_width, target_height, feathering, upscale_method, mask=None): + B, H, W, C = image.size() + new_height = H + new_width = W + # Calculate the scaling factor while maintaining aspect ratio + scaling_factor = min(target_width / W, target_height / H) + + # Check if the image needs to be downscaled + if scaling_factor < 1: + image = image.movedim(-1,1) + # Calculate the new width and height after downscaling + new_width = int(W * scaling_factor) + new_height = int(H * scaling_factor) + + # Downscale the image + image_scaled = common_upscale(image, new_width, new_height, upscale_method, "disabled").movedim(1,-1) + else: + # If downscaling is not needed, use the original image dimensions + image_scaled = image + + # Ensure mask dimensions match image dimensions + if mask is not None: + mask_scaled = mask.unsqueeze(0) # Add an extra dimension for batch size + mask_scaled = F.interpolate(mask_scaled, size=(new_height, new_width), mode="nearest") + mask_scaled = mask_scaled.squeeze(0) # Remove the extra dimension after interpolation + else: + mask_scaled = None + + # Calculate how much padding is needed to reach the target dimensions + pad_top = max(0, (target_height - new_height) // 2) + pad_bottom = max(0, target_height - new_height - pad_top) + pad_left = max(0, (target_width - new_width) // 2) + pad_right = max(0, target_width - new_width - pad_left) + + # Now call the original expand_image with the calculated padding + return ImagePadForOutpaintMasked.expand_image(self, image_scaled, pad_left, pad_top, pad_right, pad_bottom, feathering, mask_scaled) + +class ImagePrepForICLora: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "reference_image": ("IMAGE",), + "output_width": ("INT", {"default": 1024, "min": 1, "max": 4096, "step": 1}), + "output_height": ("INT", {"default": 1024, "min": 1, "max": 4096, "step": 1}), + "border_width": ("INT", {"default": 0, "min": 0, "max": 4096, "step": 1}), + }, + "optional": { + "latent_image": ("IMAGE",), + "latent_mask": ("MASK",), + "reference_mask": ("MASK",), + } + } + + RETURN_TYPES = ("IMAGE", "MASK") + FUNCTION = "expand_image" + + CATEGORY = "image" + + def expand_image(self, reference_image, output_width, output_height, border_width, latent_image=None, reference_mask=None, latent_mask=None): + + if reference_mask is not None: + if torch.allclose(reference_mask, torch.zeros_like(reference_mask)): + logging.warning("The incoming mask is fully black. Handling it as None.") + reference_mask = None + image = reference_image + if latent_image is not None: + if image.shape[0] != latent_image.shape[0]: + image = image.repeat(latent_image.shape[0], 1, 1, 1) + B, H, W, C = image.size() + + # Handle mask + if reference_mask is not None: + resized_mask = torch.nn.functional.interpolate( + reference_mask.unsqueeze(1), + size=(H, W), + mode='nearest' + ).squeeze(1) + image = image * resized_mask.unsqueeze(-1) + + # Calculate new width maintaining aspect ratio + new_width = int((W / H) * output_height) + + # Resize image to new height while maintaining aspect ratio + resized_image = common_upscale(image.movedim(-1,1), new_width, output_height, "lanczos", "disabled").movedim(1,-1) + + # Create padded image + if latent_image is None: + pad_image = torch.zeros((B, output_height, output_width, C), device=image.device) + else: + resized_latent_image = common_upscale(latent_image.movedim(-1,1), output_width, output_height, "lanczos", "disabled").movedim(1,-1) + pad_image = resized_latent_image + if latent_mask is not None: + resized_latent_mask = torch.nn.functional.interpolate( + latent_mask.unsqueeze(1), + size=(pad_image.shape[1], pad_image.shape[2]), + mode='nearest' + ).squeeze(1) + + if border_width > 0: + border = torch.zeros((B, output_height, border_width, C), device=image.device) + padded_image = torch.cat((resized_image, border, pad_image), dim=2) + if latent_mask is not None: + padded_mask = torch.zeros((B, padded_image.shape[1], padded_image.shape[2]), device=image.device) + padded_mask[:, :, (new_width + border_width):] = resized_latent_mask + else: + padded_mask = torch.ones((B, padded_image.shape[1], padded_image.shape[2]), device=image.device) + padded_mask[:, :, :new_width + border_width] = 0 + else: + padded_image = torch.cat((resized_image, pad_image), dim=2) + if latent_mask is not None: + padded_mask = torch.zeros((B, padded_image.shape[1], padded_image.shape[2]), device=image.device) + padded_mask[:, :, new_width:] = resized_latent_mask + else: + padded_mask = torch.ones((B, padded_image.shape[1], padded_image.shape[2]), device=image.device) + padded_mask[:, :, :new_width] = 0 + + return (padded_image, padded_mask) + + +class ImageAndMaskPreview(SaveImage): + def __init__(self): + self.output_dir = folder_paths.get_temp_directory() + self.type = "temp" + self.prefix_append = "_temp_" + ''.join(random.choice("abcdefghijklmnopqrstupvxyz") for x in range(5)) + self.compress_level = 4 + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "mask_opacity": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}), + "mask_color": ("STRING", {"default": "255, 255, 255", "tooltip": "RGB (255,255,255) or RGBA (255,255,255,128) or Hex (#RRGGBB / #RRGGBBAA)"}), + "pass_through": ("BOOLEAN", {"default": False}), + }, + "optional": { + "image": ("IMAGE",), + "mask": ("MASK",), + }, + "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"}, + } + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("composite",) + FUNCTION = "execute" + CATEGORY = "KJNodes/masking" + DESCRIPTION = """ +Preview an image or a mask, when both inputs are used +composites the mask on top of the image. +with pass_through on the preview is disabled and the +composite is returned from the composite slot instead, +this allows for the preview to be passed for video combine +nodes for example. Supports RGBA for mask_color to adjust transparency per color. +""" + + def execute(self, mask_opacity, mask_color, pass_through, filename_prefix="ComfyUI", image=None, mask=None, prompt=None, extra_pnginfo=None): + if mask is not None and image is None: + preview = mask.reshape((-1, 1, mask.shape[-2], mask.shape[-1])).movedim(1, -1).expand(-1, -1, -1, 3) + elif mask is None and image is not None: + preview = image + elif mask is not None and image is not None: + mask_adjusted = mask * mask_opacity + mask_image = mask.reshape((-1, 1, mask.shape[-2], mask.shape[-1])).movedim(1, -1).expand(-1, -1, -1, 3).clone() + + # Use helper function to parse color string + color_list = string_to_color(mask_color) + + # Apply RGB channels + mask_image[:, :, :, 0] = color_list[0] / 255 # Red channel + mask_image[:, :, :, 1] = color_list[1] / 255 # Green channel + mask_image[:, :, :, 2] = color_list[2] / 255 # Blue channel + + if len(color_list) == 4: # Apply Alpha channel if present + alpha_factor = color_list[3] / 255.0 + mask_adjusted = mask_adjusted * alpha_factor + + destination, source = node_helpers.image_alpha_fix(image, mask_image) + destination = destination.clone().movedim(-1, 1) + preview = composite(destination, source.movedim(-1, 1), 0, 0, mask_adjusted, 1, True).movedim(1, -1) + if pass_through: + return (preview, ) + return(self.save_images(preview, filename_prefix, prompt, extra_pnginfo)) + +def crossfade(images_1, images_2, alpha): + crossfade = (1 - alpha) * images_1 + alpha * images_2 + return crossfade +def ease_in(t): + return t * t +def ease_out(t): + return 1 - (1 - t) * (1 - t) +def ease_in_out(t): + return 3 * t * t - 2 * t * t * t +def bounce(t): + if t < 0.5: + return ease_out(t * 2) * 0.5 + else: + return ease_in((t - 0.5) * 2) * 0.5 + 0.5 +def elastic(t): + return math.sin(13 * math.pi / 2 * t) * math.pow(2, 10 * (t - 1)) +def glitchy(t): + return t + 0.1 * math.sin(40 * t) +def exponential_ease_out(t): + return 1 - (1 - t) ** 4 + +easing_functions = { + "linear": lambda t: t, + "ease_in": ease_in, + "ease_out": ease_out, + "ease_in_out": ease_in_out, + "bounce": bounce, + "elastic": elastic, + "glitchy": glitchy, + "exponential_ease_out": exponential_ease_out, +} + +class CrossFadeImages: + + RETURN_TYPES = ("IMAGE",) + FUNCTION = "crossfadeimages" + CATEGORY = "KJNodes/image" + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "images_1": ("IMAGE",), + "images_2": ("IMAGE",), + "interpolation": (["linear", "ease_in", "ease_out", "ease_in_out", "bounce", "elastic", "glitchy", "exponential_ease_out"],), + "transition_start_index": ("INT", {"default": 1,"min": -4096, "max": 4096, "step": 1}), + "transitioning_frames": ("INT", {"default": 1,"min": 0, "max": 4096, "step": 1}), + "start_level": ("FLOAT", {"default": 0.0,"min": 0.0, "max": 1.0, "step": 0.01}), + "end_level": ("FLOAT", {"default": 1.0,"min": 0.0, "max": 1.0, "step": 0.01}), + }, + } + + def crossfadeimages(self, images_1, images_2, transition_start_index, transitioning_frames, interpolation, start_level, end_level): + + crossfade_images = [] + + if transition_start_index < 0: + transition_start_index = len(images_1) + transition_start_index + if transition_start_index < 0: + raise ValueError("Transition start index is out of range for images_1.") + + transitioning_frames = min(transitioning_frames, len(images_1) - transition_start_index, len(images_2)) + + alphas = torch.linspace(start_level, end_level, transitioning_frames) + for i in range(transitioning_frames): + alpha = alphas[i] + image1 = images_1[transition_start_index + i] + image2 = images_2[i] + easing_function = easing_functions.get(interpolation) + alpha = easing_function(alpha) # Apply the easing function to the alpha value + + crossfade_image = crossfade(image1, image2, alpha) + crossfade_images.append(crossfade_image) + + # Convert crossfade_images to tensor + crossfade_images = torch.stack(crossfade_images, dim=0) + + # Append the beginning of images_1 (before the transition) + beginning_images_1 = images_1[:transition_start_index] + crossfade_images = torch.cat([beginning_images_1, crossfade_images], dim=0) + + # Append the remaining frames of images_2 (after the transition) + remaining_images_2 = images_2[transitioning_frames:] + if len(remaining_images_2) > 0: + crossfade_images = torch.cat([crossfade_images, remaining_images_2], dim=0) + + return (crossfade_images, ) + +class CrossFadeImagesMulti: + RETURN_TYPES = ("IMAGE",) + FUNCTION = "crossfadeimages" + CATEGORY = "KJNodes/image" + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "inputcount": ("INT", {"default": 2, "min": 2, "max": 1000, "step": 1}), + "image_1": ("IMAGE",), + "interpolation": (["linear", "ease_in", "ease_out", "ease_in_out", "bounce", "elastic", "glitchy", "exponential_ease_out"],), + "transitioning_frames": ("INT", {"default": 1,"min": 0, "max": 4096, "step": 1}), + }, + "optional": { + "image_2": ("IMAGE",), + } + } + + def crossfadeimages(self, inputcount, transitioning_frames, interpolation, **kwargs): + + image_1 = kwargs["image_1"] + first_image_shape = image_1.shape + first_image_device = image_1.device + height = image_1.shape[1] + width = image_1.shape[2] + + easing_function = easing_functions[interpolation] + + for c in range(1, inputcount): + frames = [] + new_image = kwargs.get(f"image_{c + 1}", torch.zeros(first_image_shape)).to(first_image_device) + new_image_height = new_image.shape[1] + new_image_width = new_image.shape[2] + + if new_image_height != height or new_image_width != width: + new_image = common_upscale(new_image.movedim(-1, 1), width, height, "lanczos", "disabled") + new_image = new_image.movedim(1, -1) # Move channels back to the last dimension + + last_frame_image_1 = image_1[-1] + first_frame_image_2 = new_image[0] + + for frame in range(transitioning_frames): + t = frame / (transitioning_frames - 1) + alpha = easing_function(t) + alpha_tensor = torch.tensor(alpha, dtype=last_frame_image_1.dtype, device=last_frame_image_1.device) + frame_image = crossfade(last_frame_image_1, first_frame_image_2, alpha_tensor) + frames.append(frame_image) + + frames = torch.stack(frames) + image_1 = torch.cat((image_1, frames, new_image), dim=0) + + return image_1, + +def transition_images(images_1, images_2, alpha, transition_type, blur_radius, reverse): + width = images_1.shape[1] + height = images_1.shape[0] + + mask = torch.zeros_like(images_1, device=images_1.device) + + alpha = alpha.item() + if reverse: + alpha = 1 - alpha + + #transitions from matteo's essential nodes + if "horizontal slide" in transition_type: + pos = round(width * alpha) + mask[:, :pos, :] = 1.0 + elif "vertical slide" in transition_type: + pos = round(height * alpha) + mask[:pos, :, :] = 1.0 + elif "box" in transition_type: + box_w = round(width * alpha) + box_h = round(height * alpha) + x1 = (width - box_w) // 2 + y1 = (height - box_h) // 2 + x2 = x1 + box_w + y2 = y1 + box_h + mask[y1:y2, x1:x2, :] = 1.0 + elif "circle" in transition_type: + radius = math.ceil(math.sqrt(pow(width, 2) + pow(height, 2)) * alpha / 2) + c_x = width // 2 + c_y = height // 2 + x = torch.arange(0, width, dtype=torch.float32, device="cpu") + y = torch.arange(0, height, dtype=torch.float32, device="cpu") + y, x = torch.meshgrid((y, x), indexing="ij") + circle = ((x - c_x) ** 2 + (y - c_y) ** 2) <= (radius ** 2) + mask[circle] = 1.0 + elif "horizontal door" in transition_type: + bar = math.ceil(height * alpha / 2) + if bar > 0: + mask[:bar, :, :] = 1.0 + mask[-bar:,:, :] = 1.0 + elif "vertical door" in transition_type: + bar = math.ceil(width * alpha / 2) + if bar > 0: + mask[:, :bar,:] = 1.0 + mask[:, -bar:,:] = 1.0 + elif "fade" in transition_type: + mask[:, :, :] = alpha + + mask = gaussian_blur(mask, blur_radius) + + return images_1 * (1 - mask) + images_2 * mask + +def gaussian_blur(mask, blur_radius): + if blur_radius > 0: + kernel_size = int(blur_radius * 2) + 1 + if kernel_size % 2 == 0: + kernel_size += 1 # Ensure kernel size is odd + sigma = blur_radius / 3 + x = torch.arange(-kernel_size // 2 + 1, kernel_size // 2 + 1, dtype=torch.float32) + x = torch.exp(-0.5 * (x / sigma) ** 2) + kernel1d = x / x.sum() + kernel2d = kernel1d[:, None] * kernel1d[None, :] + kernel2d = kernel2d.to(mask.device) + kernel2d = kernel2d.expand(mask.shape[2], 1, kernel2d.shape[0], kernel2d.shape[1]) + mask = mask.permute(2, 0, 1).unsqueeze(0) # Change to [C, H, W] and add batch dimension + mask = F.conv2d(mask, kernel2d, padding=kernel_size // 2, groups=mask.shape[1]) + mask = mask.squeeze(0).permute(1, 2, 0) # Change back to [H, W, C] + return mask + +class TransitionImagesMulti: + RETURN_TYPES = ("IMAGE",) + FUNCTION = "transition" + CATEGORY = "KJNodes/image" + DESCRIPTION = """ +Creates transitions between images. +""" + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "inputcount": ("INT", {"default": 2, "min": 2, "max": 1000, "step": 1}), + "image_1": ("IMAGE",), + "interpolation": (["linear", "ease_in", "ease_out", "ease_in_out", "bounce", "elastic", "glitchy", "exponential_ease_out"],), + "transition_type": (["horizontal slide", "vertical slide", "box", "circle", "horizontal door", "vertical door", "fade"],), + "transitioning_frames": ("INT", {"default": 2,"min": 2, "max": 4096, "step": 1}), + "blur_radius": ("FLOAT", {"default": 0.0,"min": 0.0, "max": 100.0, "step": 0.1}), + "reverse": ("BOOLEAN", {"default": False}), + "device": (["CPU", "GPU"], {"default": "CPU"}), + }, + "optional": { + "image_2": ("IMAGE",), + } + } + + def transition(self, inputcount, transitioning_frames, transition_type, interpolation, device, blur_radius, reverse, **kwargs): + + gpu = model_management.get_torch_device() + + image_1 = kwargs["image_1"] + height = image_1.shape[1] + width = image_1.shape[2] + first_image_shape = image_1.shape + first_image_device = image_1.device + + easing_function = easing_functions[interpolation] + + for c in range(1, inputcount): + frames = [] + new_image = kwargs.get(f"image_{c + 1}", torch.zeros(first_image_shape)).to(first_image_device) + new_image_height = new_image.shape[1] + new_image_width = new_image.shape[2] + + if new_image_height != height or new_image_width != width: + new_image = common_upscale(new_image.movedim(-1, 1), width, height, "lanczos", "disabled") + new_image = new_image.movedim(1, -1) # Move channels back to the last dimension + + last_frame_image_1 = image_1[-1] + first_frame_image_2 = new_image[0] + if device == "GPU": + last_frame_image_1 = last_frame_image_1.to(gpu) + first_frame_image_2 = first_frame_image_2.to(gpu) + + if reverse: + last_frame_image_1, first_frame_image_2 = first_frame_image_2, last_frame_image_1 + + for frame in range(transitioning_frames): + t = frame / (transitioning_frames - 1) + alpha = easing_function(t) + alpha_tensor = torch.tensor(alpha, dtype=last_frame_image_1.dtype, device=last_frame_image_1.device) + frame_image = transition_images(last_frame_image_1, first_frame_image_2, alpha_tensor, transition_type, blur_radius, reverse) + frames.append(frame_image) + + frames = torch.stack(frames).cpu() + image_1 = torch.cat((image_1, frames, new_image), dim=0) + + return image_1.cpu(), + +class TransitionImagesInBatch: + RETURN_TYPES = ("IMAGE",) + FUNCTION = "transition" + CATEGORY = "KJNodes/image" + DESCRIPTION = """ +Creates transitions between images in a batch. +""" + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "images": ("IMAGE",), + "interpolation": (["linear", "ease_in", "ease_out", "ease_in_out", "bounce", "elastic", "glitchy", "exponential_ease_out"],), + "transition_type": (["horizontal slide", "vertical slide", "box", "circle", "horizontal door", "vertical door", "fade"],), + "transitioning_frames": ("INT", {"default": 1,"min": 0, "max": 4096, "step": 1}), + "blur_radius": ("FLOAT", {"default": 0.0,"min": 0.0, "max": 100.0, "step": 0.1}), + "reverse": ("BOOLEAN", {"default": False}), + "device": (["CPU", "GPU"], {"default": "CPU"}), + }, + } + + #transitions from matteo's essential nodes + def transition(self, images, transitioning_frames, transition_type, interpolation, device, blur_radius, reverse): + if images.shape[0] == 1: + return images, + + gpu = model_management.get_torch_device() + + easing_function = easing_functions[interpolation] + + images_list = [] + pbar = ProgressBar(images.shape[0] - 1) + for i in range(images.shape[0] - 1): + frames = [] + image_1 = images[i] + image_2 = images[i + 1] + + if device == "GPU": + image_1 = image_1.to(gpu) + image_2 = image_2.to(gpu) + + if reverse: + image_1, image_2 = image_2, image_1 + + for frame in range(transitioning_frames): + t = frame / (transitioning_frames - 1) + alpha = easing_function(t) + alpha_tensor = torch.tensor(alpha, dtype=image_1.dtype, device=image_1.device) + frame_image = transition_images(image_1, image_2, alpha_tensor, transition_type, blur_radius, reverse) + frames.append(frame_image) + pbar.update(1) + + frames = torch.stack(frames).cpu() + images_list.append(frames) + images = torch.cat(images_list, dim=0) + + return images.cpu(), + +class ImageBatchJoinWithTransition: + RETURN_TYPES = ("IMAGE",) + FUNCTION = "transition_batches" + CATEGORY = "KJNodes/image" + DESCRIPTION = """ +Transitions between two batches of images, starting at a specified index in the first batch. +During the transition, frames from both batches are blended frame-by-frame, so the video keeps playing. +""" + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "images_1": ("IMAGE",), + "images_2": ("IMAGE",), + "start_index": ("INT", {"default": 0, "min": -10000, "max": 10000, "step": 1}), + "interpolation": (["linear", "ease_in", "ease_out", "ease_in_out", "bounce", "elastic", "glitchy", "exponential_ease_out"],), + "transition_type": (["horizontal slide", "vertical slide", "box", "circle", "horizontal door", "vertical door", "fade"],), + "transitioning_frames": ("INT", {"default": 1, "min": 1, "max": 4096, "step": 1}), + "blur_radius": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 100.0, "step": 0.1}), + "reverse": ("BOOLEAN", {"default": False}), + "device": (["CPU", "GPU"], {"default": "CPU"}), + }, + } + + def transition_batches(self, images_1, images_2, start_index, interpolation, transition_type, transitioning_frames, blur_radius, reverse, device): + if images_1.shape[0] == 0 or images_2.shape[0] == 0: + raise ValueError("Both input batches must have at least one image.") + + if start_index < 0: + start_index = images_1.shape[0] + start_index + if start_index < 0 or start_index > images_1.shape[0]: + raise ValueError("start_index is out of range.") + + gpu = model_management.get_torch_device() + easing_function = easing_functions[interpolation] + out_frames = [] + + # Add images from images_1 up to start_index + if start_index > 0: + out_frames.append(images_1[:start_index]) + + # Determine how many frames we can blend + max_transition = min(transitioning_frames, images_1.shape[0] - start_index, images_2.shape[0]) + + # Blend corresponding frames from both batches + for i in range(max_transition): + img1 = images_1[start_index + i] + img2 = images_2[i] + if device == "GPU": + img1 = img1.to(gpu) + img2 = img2.to(gpu) + if reverse: + img1, img2 = img2, img1 + t = i / (max_transition - 1) if max_transition > 1 else 1.0 + alpha = easing_function(t) + alpha_tensor = torch.tensor(alpha, dtype=img1.dtype, device=img1.device) + frame_image = transition_images(img1, img2, alpha_tensor, transition_type, blur_radius, reverse) + out_frames.append(frame_image.cpu().unsqueeze(0)) + + # Add remaining images from images_2 after transition + if images_2.shape[0] > max_transition: + out_frames.append(images_2[max_transition:]) + + # Concatenate all frames + out = torch.cat(out_frames, dim=0) + return (out.cpu(),) + +class ShuffleImageBatch: + RETURN_TYPES = ("IMAGE",) + FUNCTION = "shuffle" + CATEGORY = "KJNodes/image" + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "images": ("IMAGE",), + "seed": ("INT", {"default": 123,"min": 0, "max": 0xffffffffffffffff, "step": 1}), + }, + } + + def shuffle(self, images, seed): + torch.manual_seed(seed) + B, H, W, C = images.shape + indices = torch.randperm(B) + shuffled_images = images[indices] + + return shuffled_images, + +class GetImageRangeFromBatch: + + RETURN_TYPES = ("IMAGE", "MASK", ) + FUNCTION = "imagesfrombatch" + CATEGORY = "KJNodes/image" + DESCRIPTION = """ +Returns a range of images from a batch. +""" + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "start_index": ("INT", {"default": 0,"min": -1, "max": 4096, "step": 1}), + "num_frames": ("INT", {"default": 1,"min": 1, "max": 4096, "step": 1}), + }, + "optional": { + "images": ("IMAGE",), + "masks": ("MASK",), + } + } + + def imagesfrombatch(self, start_index, num_frames, images=None, masks=None): + chosen_images = None + chosen_masks = None + + # Process images if provided + if images is not None: + if start_index == -1: + start_index = max(0, len(images) - num_frames) + if start_index < 0 or start_index >= len(images): + raise ValueError("Start index is out of range") + end_index = min(start_index + num_frames, len(images)) + chosen_images = images[start_index:end_index] + + # Process masks if provided + if masks is not None: + if start_index == -1: + start_index = max(0, len(masks) - num_frames) + if start_index < 0 or start_index >= len(masks): + raise ValueError("Start index is out of range for masks") + end_index = min(start_index + num_frames, len(masks)) + chosen_masks = masks[start_index:end_index] + + return (chosen_images, chosen_masks,) + +class RandomImageFromBatch(io.ComfyNode): + @classmethod + def define_schema(cls): + template = io.MatchType.Template("input_type", [io.Image, io.Mask]) + return io.Schema( + node_id="RandomImageFromBatch", + display_name="Random Image From Batch", + search_aliases=["random", "mask", "sequence", "frame"], + category="KJNodes/image", + description="Picks a sequence of frames from an image or mask batch within a selected index range. " + "At randomness=0 the picks are evenly spaced across the range; at randomness=1 they are " + "uniformly random without replacement; values in between blend linearly. " + "Output is always sorted by batch index. Negative indices count from the end (-1 = last).", + inputs=[ + io.MatchType.Input("input", template=template, + tooltip="Image or mask batch to sample from."), + io.Int.Input("start_index", default=0, min=-4096, max=4096, + tooltip="Inclusive start of the sampling range. Negative values count from the end."), + io.Int.Input("end_index", default=-1, min=-4096, max=4096, + tooltip="Inclusive end of the sampling range. -1 means the last frame."), + io.Int.Input("num_frames", default=1, min=1, max=4096, + tooltip="How many frames to pick from the range."), + io.Float.Input("randomness", default=1.0, min=0.0, max=1.0, step=0.01, + tooltip="0 = evenly spaced across the range, 1 = uniformly random without replacement, " + "in-between = linear blend (jittered even spacing)."), + io.Int.Input("min_distance", default=0, min=0, max=4096, + tooltip="Minimum gap (in frames) between consecutive picks. 0 = no minimum. " + "Picks are pushed forward to satisfy this; later picks may clamp to the range end."), + io.Int.Input("max_distance", default=0, min=0, max=4096, + tooltip="Maximum gap (in frames) between consecutive picks. 0 = no maximum. " + "Picks are pulled in to satisfy this, which may compress the sequence toward the start."), + io.Int.Input("seed", default=0, min=0, max=0xffffffffffffffff, step=1, + tooltip="Random seed for reproducible sampling. Ignored when randomness is 0."), + ], + outputs=[ + io.MatchType.Output(template=template, display_name="output"), + ], + ) + + @classmethod + def execute(cls, input, start_index, end_index, num_frames, randomness, min_distance, max_distance, seed) -> io.NodeOutput: + n = input.shape[0] + if n == 0: + raise ValueError("Input batch is empty.") + + s = start_index if start_index >= 0 else n + start_index + e = end_index if end_index >= 0 else n + end_index + s = max(0, min(s, n - 1)) + e = max(0, min(e, n - 1)) + if e < s: + s, e = e, s + range_size = e - s + 1 + + if num_frames == 1: + even = [(s + e) / 2] + else: + even = [s + i * (e - s) / (num_frames - 1) for i in range(num_frames)] + + if randomness <= 0: + picks_float = even + else: + rng = random.Random(seed) + if num_frames <= range_size: + random_picks = rng.sample(range(s, e + 1), num_frames) + else: + random_picks = [rng.randint(s, e) for _ in range(num_frames)] + random_picks.sort() + picks_float = [(1 - randomness) * ev + randomness * rp for ev, rp in zip(even, random_picks)] + + picks = sorted(max(s, min(e, int(round(p)))) for p in picks_float) + + if num_frames > 1 and (min_distance > 0 or max_distance > 0): + adjusted = [picks[0]] + for i in range(1, len(picks)): + prev = adjusted[-1] + target = picks[i] + if min_distance > 0 and target - prev < min_distance: + target = prev + min_distance + if max_distance > 0 and target - prev > max_distance: + target = prev + max_distance + adjusted.append(min(e, max(s, target))) + picks = adjusted + + idx = torch.tensor(picks, dtype=torch.long, device=input.device) + chosen = input.index_select(0, idx) + + return io.NodeOutput(chosen) + +class ImageBatchExtendWithOverlap: + + RETURN_TYPES = ("IMAGE", "IMAGE", "IMAGE", ) + RETURN_NAMES = ("source_images", "start_images", "extended_images") + OUTPUT_TOOLTIPS = ( + "The original source images (passthrough)", + "The input images used as the starting point for extension", + "The extended images with overlap, if no new images are provided this will be empty", + ) + FUNCTION = "imagesfrombatch" + CATEGORY = "KJNodes/image" + DESCRIPTION = """ +Helper node for video generation extension +First input source and overlap amount to get the starting frames for the extension. +Then on another copy of the node provide the newly generated frames and choose how to overlap them. +""" + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "source_images": ("IMAGE", {"tooltip": "The source images to extend"}), + "overlap": ("INT", {"default": 13,"min": 1, "max": 4096, "step": 1, "tooltip": "Number of overlapping frames between source and new images"}), + "overlap_side": (["source", "new_images"], {"default": "source", "tooltip": "Which side to overlap on"}), + "overlap_mode": (["cut", "linear_blend", "ease_in_out", "filmic_crossfade", "perceptual_crossfade"], {"default": "linear_blend", "tooltip": "Method to use for overlapping frames"}), + }, + "optional": { + "new_images": ("IMAGE", {"tooltip": "The new images to extend with"}), + } + } + + def imagesfrombatch(self, source_images, overlap, overlap_side, overlap_mode, new_images=None): + if overlap > len(source_images): + return source_images, source_images, source_images + + if new_images is not None: + if source_images.shape[1:3] != new_images.shape[1:3]: + raise ValueError(f"Source and new images must have the same shape: {source_images.shape[1:3]} vs {new_images.shape[1:3]}") + # Determine where to place the overlap + prefix = source_images[:-overlap] + if overlap_side == "source": + blend_src = source_images[-overlap:] + blend_dst = new_images[:overlap] + elif overlap_side == "new_images": + blend_src = new_images[:overlap] + blend_dst = source_images[-overlap:] + suffix = new_images[overlap:] + + if overlap_mode == "linear_blend": + # Vectorized version - process all frames at once + alpha = torch.linspace(0, 1, overlap + 2, device=blend_src.device, dtype=blend_src.dtype)[1:-1] + alpha = alpha.view(-1, 1, 1, 1) # Shape: [overlap, 1, 1, 1] + blended_images = (1 - alpha) * blend_src + alpha * blend_dst + extended_images = torch.cat((prefix, blended_images, suffix), dim=0) + + elif overlap_mode == "filmic_crossfade": + gamma = 2.2 + alpha = torch.linspace(0, 1, overlap + 2, device=blend_src.device, dtype=blend_src.dtype)[1:-1] + alpha = alpha.view(-1, 1, 1, 1) + linear_src = torch.pow(blend_src, gamma) + linear_dst = torch.pow(blend_dst, gamma) + blended = (1 - alpha) * linear_src + alpha * linear_dst + blended_images = torch.pow(blended, 1.0 / gamma) + extended_images = torch.cat((prefix, blended_images, suffix), dim=0) + + elif overlap_mode == "perceptual_crossfade": + import kornia + alpha = torch.linspace(0, 1, overlap + 2, device=blend_src.device, dtype=blend_src.dtype)[1:-1] + + src_nchw = blend_src.movedim(-1, 1) + dst_nchw = blend_dst.movedim(-1, 1) + lab_src = kornia.color.rgb_to_lab(src_nchw) + lab_dst = kornia.color.rgb_to_lab(dst_nchw) + + # Blend in LAB space + alpha = alpha.view(-1, 1, 1, 1) # [N,1,1,1] for broadcasting + blended_lab = (1 - alpha) * lab_src + alpha * lab_dst + + # Convert back to RGB and reshape + blended_rgb = kornia.color.lab_to_rgb(blended_lab) + blended_images = blended_rgb.movedim(1, -1) # [N,C,H,W] -> [N,H,W,C] + extended_images = torch.cat((prefix, blended_images, suffix), dim=0) + + elif overlap_mode == "ease_in_out": + # Vectorized ease_in_out + t = torch.linspace(0, 1, overlap + 2, device=blend_src.device, dtype=blend_src.dtype)[1:-1] + eased_t = 3 * t * t - 2 * t * t * t # ease_in_out formula + eased_t = eased_t.view(-1, 1, 1, 1) + blended_images = (1 - eased_t) * blend_src + eased_t * blend_dst + extended_images = torch.cat((prefix, blended_images, suffix), dim=0) + + elif overlap_mode == "cut": + extended_images = torch.cat((prefix, suffix), dim=0) + if overlap_side == "new_images": + extended_images = torch.cat((source_images, new_images[overlap:]), dim=0) + elif overlap_side == "source": + extended_images = torch.cat((source_images[:-overlap], new_images), dim=0) + else: + extended_images = torch.zeros((1, 64, 64, 3), device="cpu") + + start_images = source_images[-overlap:] + + return (source_images, start_images, extended_images) + +class GetLatentRangeFromBatch: + + RETURN_TYPES = ("LATENT", ) + FUNCTION = "latentsfrombatch" + CATEGORY = "KJNodes/latents" + DESCRIPTION = """ +Returns a range of latents from a batch. +""" + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "latents": ("LATENT",), + "start_index": ("INT", {"default": 0,"min": -1, "max": 4096, "step": 1}), + "num_frames": ("INT", {"default": 1,"min": -1, "max": 4096, "step": 1}), + }, + } + + def latentsfrombatch(self, latents, start_index, num_frames): + chosen_latents = None + samples = latents["samples"] + if len(samples.shape) == 4: + B, C, H, W = samples.shape + num_latents = B + elif len(samples.shape) == 5: + B, C, T, H, W = samples.shape + num_latents = T + + if start_index == -1: + start_index = max(0, num_latents - num_frames) + if start_index < 0 or start_index >= num_latents: + raise ValueError("Start index is out of range") + + end_index = num_latents if num_frames == -1 else min(start_index + num_frames, num_latents) + + if len(samples.shape) == 4: + chosen_latents = samples[start_index:end_index] + elif len(samples.shape) == 5: + chosen_latents = samples[:, :, start_index:end_index] + + return ({"samples": chosen_latents.contiguous(),},) + +class InsertLatentToIndex: + + RETURN_TYPES = ("LATENT", ) + FUNCTION = "insert" + CATEGORY = "KJNodes/latents" + DESCRIPTION = """ +Inserts a latent at the specified index into the original latent batch. +""" + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "source": ("LATENT",), + "destination": ("LATENT",), + "index": ("INT", {"default": 0,"min": -1, "max": 4096, "step": 1}), + }, + } + + def insert(self, source, destination, index): + samples_destination = destination["samples"] + samples_source = source["samples"].to(samples_destination) + + if len(samples_source.shape) == 4: + B, C, H, W = samples_source.shape + num_latents = B + elif len(samples_source.shape) == 5: + B, C, T, H, W = samples_source.shape + num_latents = T + + if index >= num_latents or index < 0: + raise ValueError(f"Index {index} out of bounds for tensor with {num_latents} latents") + + if len(samples_source.shape) == 4: + joined_latents = torch.cat([ + samples_destination[:index], + samples_source, + samples_destination[index+1:] + ], dim=0) + else: + joined_latents = torch.cat([ + samples_destination[:, :, :index], + samples_source, + samples_destination[:, :, index+1:] + ], dim=2) + + return ({"samples": joined_latents,},) + +class ImageBatchFilter: + + RETURN_TYPES = ("IMAGE", "STRING",) + RETURN_NAMES = ("images", "removed_indices",) + FUNCTION = "filter" + CATEGORY = "KJNodes/image" + DESCRIPTION = "Removes empty images from a batch" + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "images": ("IMAGE",), + "empty_color": ("STRING", {"default": "0, 0, 0"}), + "empty_threshold": ("FLOAT", {"default": 0.01,"min": 0.0, "max": 1.0, "step": 0.01}), + }, + "optional": { + "replacement_image": ("IMAGE",), + } + } + + def filter(self, images, empty_color, empty_threshold, replacement_image=None): + B, H, W, C = images.shape + + input_images = images.clone() + + empty_color_list = [int(color.strip()) for color in empty_color.split(',')] + empty_color_tensor = torch.tensor(empty_color_list, dtype=torch.float32).to(input_images.device) + + color_diff = torch.abs(input_images - empty_color_tensor) + mean_diff = color_diff.mean(dim=(1, 2, 3)) + + empty_indices = mean_diff <= empty_threshold + empty_indices_string = ', '.join([str(i) for i in range(B) if empty_indices[i]]) + + if replacement_image is not None: + B_rep, H_rep, W_rep, C_rep = replacement_image.shape + replacement = replacement_image.clone() + if (H_rep != images.shape[1]) or (W_rep != images.shape[2]) or (C_rep != images.shape[3]): + replacement = common_upscale(replacement.movedim(-1, 1), W, H, "lanczos", "center").movedim(1, -1) + input_images[empty_indices] = replacement[0] + + return (input_images, empty_indices_string,) + else: + non_empty_images = input_images[~empty_indices] + return (non_empty_images, empty_indices_string,) + +class GetImagesFromBatchIndexed: + + RETURN_TYPES = ("IMAGE",) + FUNCTION = "indexedimagesfrombatch" + CATEGORY = "KJNodes/image" + DESCRIPTION = """ +Selects and returns the images at the specified indices as an image batch. +""" + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "images": ("IMAGE",), + "indexes": ("STRING", {"default": "0, 1, 2", "multiline": True}), + }, + } + + def indexedimagesfrombatch(self, images, indexes): + + # Parse the indexes string into a list of integers + index_list = [int(index.strip()) for index in indexes.split(',')] + + # Convert list of indices to a PyTorch tensor + indices_tensor = torch.tensor(index_list, dtype=torch.long) + + # Select the images at the specified indices + chosen_images = images[indices_tensor] + + return (chosen_images,) + +class InsertImagesToBatchIndexed: + + RETURN_TYPES = ("IMAGE",) + FUNCTION = "insertimagesfrombatch" + CATEGORY = "KJNodes/image" + DESCRIPTION = """ +Inserts images at the specified indices into the original image batch. +""" + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "original_images": ("IMAGE",), + "images_to_insert": ("IMAGE",), + "indexes": ("STRING", {"default": "0, 1, 2", "multiline": True}), + }, + "optional": { + "mode": (["replace", "insert"],), + } + } + + def insertimagesfrombatch(self, original_images, images_to_insert, indexes, mode="replace"): + if indexes == "": + return (original_images,) + + input_images = original_images.clone() + + # Parse the indexes string into a list of integers + index_list = [int(index.strip()) for index in indexes.split(',')] + + # Convert list of indices to a PyTorch tensor + indices_tensor = torch.tensor(index_list, dtype=torch.long) + + # Ensure the images_to_insert is a tensor + if not isinstance(images_to_insert, torch.Tensor): + images_to_insert = torch.tensor(images_to_insert) + + if mode == "replace": + # Replace the images at the specified indices + for index, image in zip(indices_tensor, images_to_insert): + input_images[index] = image + else: + # Create a list to hold the new image sequence + new_images = [] + insert_offset = 0 + + for i in range(len(input_images) + len(indices_tensor)): + if insert_offset < len(indices_tensor) and i == indices_tensor[insert_offset]: + # Use modulo to cycle through images_to_insert + new_images.append(images_to_insert[insert_offset % len(images_to_insert)]) + insert_offset += 1 + else: + new_images.append(input_images[i - insert_offset]) + + # Convert the list back to a tensor + input_images = torch.stack(new_images, dim=0) + + return (input_images,) + +class PadImageBatchInterleaved: + + RETURN_TYPES = ("IMAGE", "MASK",) + RETURN_NAMES = ("images", "masks",) + FUNCTION = "pad" + CATEGORY = "KJNodes/image" + DESCRIPTION = """ +Inserts empty frames between the images in a batch. +""" + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "images": ("IMAGE",), + "empty_frames_per_image": ("INT", {"default": 1,"min": 0, "max": 4096, "step": 1}), + "pad_frame_value": ("FLOAT", {"default": 0.0,"min": 0.0, "max": 1.0, "step": 0.01}), + "add_after_last": ("BOOLEAN", {"default": False}), + }, + } + + def pad(self, images, empty_frames_per_image, pad_frame_value, add_after_last): + B, H, W, C = images.shape + + # Handle single frame case specifically + if B == 1: + total_frames = 1 + empty_frames_per_image if add_after_last else 1 + else: + # Original B images + (B-1) sets of empty frames between them + total_frames = B + (B-1) * empty_frames_per_image + # Add additional empty frames after the last image if requested + if add_after_last: + total_frames += empty_frames_per_image + + # Create new tensor with zeros (empty frames) + padded_batch = torch.ones((total_frames, H, W, C), + dtype=images.dtype, + device=images.device) * pad_frame_value + # Create mask tensor (1 for original frames, 0 for empty frames) + mask = torch.zeros((total_frames, H, W), + dtype=images.dtype, + device=images.device) + + # Fill in original images at their new positions + for i in range(B): + if B == 1: + # For single frame, just place it at the beginning + new_pos = 0 + else: + # Each image is separated by empty_frames_per_image blank frames + new_pos = i * (empty_frames_per_image + 1) + + padded_batch[new_pos] = images[i] + mask[new_pos] = 1.0 # Mark this as an original frame + + return (padded_batch, mask) + +class ReplaceImagesInBatch: + + RETURN_TYPES = ("IMAGE", "MASK",) + FUNCTION = "replace" + CATEGORY = "KJNodes/image" + DESCRIPTION = """ +Replaces the images in a batch, starting from the specified start index, +with the replacement images. +""" + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "start_index": ("INT", {"default": 1,"min": 0, "max": 4096, "step": 1}), + }, + "optional": { + "original_images": ("IMAGE",), + "replacement_images": ("IMAGE",), + "original_masks": ("MASK",), + "replacement_masks": ("MASK",), + } + } + + def replace(self, original_images=None, replacement_images=None, start_index=1, original_masks=None, replacement_masks=None): + images = None + masks = None + + if original_images is not None and replacement_images is not None: + if start_index >= len(original_images): + raise ValueError("ReplaceImagesInBatch: Start index is out of range") + end_index = start_index + len(replacement_images) + if end_index > len(original_images): + raise ValueError("ReplaceImagesInBatch: End index is out of range") + + original_images_copy = original_images.clone() + if original_images_copy.shape[2] != replacement_images.shape[2] or original_images_copy.shape[3] != replacement_images.shape[3]: + replacement_images = common_upscale(replacement_images.movedim(-1, 1), original_images_copy.shape[1], original_images_copy.shape[2], "lanczos", "center").movedim(1, -1) + + original_images_copy[start_index:end_index] = replacement_images + images = original_images_copy + else: + images = torch.zeros((1, 64, 64, 3)) + + if original_masks is not None and replacement_masks is not None: + if start_index >= len(original_masks): + raise ValueError("ReplaceImagesInBatch: Start index is out of range") + end_index = start_index + len(replacement_masks) + if end_index > len(original_masks): + raise ValueError("ReplaceImagesInBatch: End index is out of range") + + original_masks_copy = original_masks.clone() + if original_masks_copy.shape[1] != replacement_masks.shape[1] or original_masks_copy.shape[2] != replacement_masks.shape[2]: + replacement_masks = common_upscale(replacement_masks.unsqueeze(1), original_masks_copy.shape[1], original_masks_copy.shape[2], "nearest-exact", "center").squeeze(0) + + original_masks_copy[start_index:end_index] = replacement_masks + masks = original_masks_copy + else: + masks = torch.zeros((1, 64, 64)) + + return (images, masks) + + +class ReverseImageBatch: + + RETURN_TYPES = ("IMAGE",) + FUNCTION = "reverseimagebatch" + CATEGORY = "KJNodes/image" + DESCRIPTION = """ +Reverses the order of the images in a batch. +""" + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "images": ("IMAGE",), + }, + } + + def reverseimagebatch(self, images): + reversed_images = torch.flip(images, [0]) + return (reversed_images, ) + +class ImageBatchMulti: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "inputcount": ("INT", {"default": 2, "min": 2, "max": 1000, "step": 1}), + "image_1": ("IMAGE", ), + }, + "optional": { + "image_2": ("IMAGE", ), + } + } + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("images",) + FUNCTION = "combine" + CATEGORY = "KJNodes/image" + DESCRIPTION = """ +Creates an image batch from multiple images. +You can set how many inputs the node has, +with the **inputcount** and clicking update. +""" + + def combine(self, inputcount, **kwargs): + first = kwargs["image_1"] + h, w = first.shape[1], first.shape[2] + + # determine output shape + max_ch = first.shape[-1] + total_frames = first.shape[0] + for c in range(1, inputcount): + img = kwargs.get(f"image_{c + 1}") + if img is not None: + max_ch = max(max_ch, img.shape[-1]) + total_frames += img.shape[0] + else: + total_frames += first.shape[0] + + # pre-allocate output + out = torch.empty((total_frames, h, w, max_ch), dtype=first.dtype) + offset = 0 + + for c in range(inputcount): + img = kwargs.get(f"image_{c + 1}", torch.zeros((first.shape[0], h, w, max_ch), dtype=first.dtype)) + + if img.shape[1:3] != (h, w): + img = common_upscale(img.movedim(-1, 1), w, h, "bilinear", "center").movedim(1, -1) + + if img.shape[-1] < max_ch: + img = torch.nn.functional.pad(img, (0, max_ch - img.shape[-1]), mode='constant', value=1.0) + + n = img.shape[0] + out[offset:offset + n].copy_(img, non_blocking=True) + offset += n + del img + + return (out.cpu(),) + + +class ImageTensorList: + @classmethod + def INPUT_TYPES(s): + return {"required": { + "image1": ("IMAGE",), + "image2": ("IMAGE",), + }} + + RETURN_TYPES = ("IMAGE",) + FUNCTION = "append" + CATEGORY = "KJNodes/image" + DESCRIPTION = """ +Creates an image list from the input images. +""" + + def append(self, image1, image2): + image_list = [] + if isinstance(image1, torch.Tensor) and isinstance(image2, torch.Tensor): + image_list = [image1, image2] + elif isinstance(image1, list) and isinstance(image2, torch.Tensor): + image_list = image1 + [image2] + elif isinstance(image1, torch.Tensor) and isinstance(image2, list): + image_list = [image1] + image2 + elif isinstance(image1, list) and isinstance(image2, list): + image_list = image1 + image2 + return image_list, + +class ImageAddMulti: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "inputcount": ("INT", {"default": 2, "min": 2, "max": 1000, "step": 1}), + "image_1": ("IMAGE", ), + "image_2": ("IMAGE", ), + "blending": ( + [ 'add', + 'subtract', + 'multiply', + 'difference', + ], + { + "default": 'add' + }), + "blend_amount": ("FLOAT", {"default": 0.5, "min": 0, "max": 1, "step": 0.01}), + }, + } + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("images",) + FUNCTION = "add" + CATEGORY = "KJNodes/image" + DESCRIPTION = """ +Add blends multiple images together. +You can set how many inputs the node has, +with the **inputcount** and clicking update. +""" + + def add(self, inputcount, blending, blend_amount, **kwargs): + image = kwargs["image_1"] + for c in range(1, inputcount): + new_image = kwargs[f"image_{c + 1}"] + if blending == "add": + image = torch.add(image * blend_amount, new_image * blend_amount) + elif blending == "subtract": + image = torch.sub(image * blend_amount, new_image * blend_amount) + elif blending == "multiply": + image = torch.mul(image * blend_amount, new_image * blend_amount) + elif blending == "difference": + image = torch.sub(image, new_image) + return (image,) + + +class ImageConcatMulti(io.ComfyNode): + @classmethod + def define_schema(cls): + # image_1 drives the output type; image_2 (and JS-added image_3+) can independently be IMAGE or MASK + type_template = io.MatchType.Template("multi_image_or_mask", allowed_types=[io.Image, io.Mask]) + return io.Schema( + node_id="ImageConcatMulti", + display_name="Image Concatenate Multi", + category="KJNodes/image", + description=( + "Creates an image from multiple images or masks.\n" + "Set the input count and click 'Update inputs' to add more slots.\n" + "The output type follows image_1; other inputs are converted to match." + ), + accept_all_inputs=True, # JS dynamically adds image_3..image_N beyond the declared inputs + inputs=[ + io.Int.Input("inputcount", default=2, min=2, max=1000, step=1), + io.MatchType.Input("image_1", template=type_template), + io.Combo.Input("direction", options=['right', 'down', 'left', 'up'], default='right'), + io.Boolean.Input("match_image_size", default=False), + io.MultiType.Input("image_2", types=[io.Image, io.Mask], optional=True), + ], + outputs=[ + io.MatchType.Output(template=type_template, display_name="output"), + ], + ) + + @classmethod + def execute(cls, inputcount, image_1, direction, match_image_size, image_2=None, **kwargs) -> io.NodeOutput: + kwargs["image_1"] = image_1 + if image_2 is not None: + kwargs["image_2"] = image_2 + image = image_1 + first_image_shape = image.shape + device = model_management.intermediate_device() + dtype = model_management.intermediate_dtype() + for c in range(1, inputcount): + key = f"image_{c + 1}" + new_image = kwargs[key] if key in kwargs else torch.zeros( + first_image_shape, dtype=dtype, device=device + ) + image = ImageConcanate.concatenate(image, new_image, direction, match_image_size, first_image_shape=first_image_shape) + return io.NodeOutput(image) + +class PreviewAnimation: + def __init__(self): + self.output_dir = folder_paths.get_temp_directory() + self.type = "temp" + self.prefix_append = "_temp_" + ''.join(random.choice("abcdefghijklmnopqrstupvxyz") for x in range(5)) + self.compress_level = 1 + + methods = {"default": 4, "fastest": 0, "slowest": 6} + @classmethod + def INPUT_TYPES(s): + return {"required": + { + "fps": ("FLOAT", {"default": 8.0, "min": 0.01, "max": 1000.0, "step": 0.01}), + }, + "optional": { + "images": ("IMAGE", ), + "masks": ("MASK", ), + }, + } + + RETURN_TYPES = () + FUNCTION = "preview" + OUTPUT_NODE = True + CATEGORY = "KJNodes/image" + + def preview(self, fps, images=None, masks=None): + filename_prefix = "AnimPreview" + full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(filename_prefix, self.output_dir) + results = list() + + pil_images = [] + + if images is not None and masks is not None: + for image in images: + i = 255. * image.cpu().numpy() + img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8)) + pil_images.append(img) + for mask in masks: + if pil_images: + mask_np = mask.cpu().numpy() + mask_np = np.clip(mask_np * 255, 0, 255).astype(np.uint8) # Convert to values between 0 and 255 + mask_img = Image.fromarray(mask_np, mode='L') + img = pil_images.pop(0) # Remove and get the first image + img = img.convert("RGBA") # Convert base image to RGBA + + # Create a new RGBA image based on the grayscale mask + rgba_mask_img = Image.new("RGBA", img.size, (255, 255, 255, 255)) + rgba_mask_img.putalpha(mask_img) # Use the mask image as the alpha channel + + # Composite the RGBA mask onto the base image + composited_img = Image.alpha_composite(img, rgba_mask_img) + pil_images.append(composited_img) # Add the composited image back + + elif images is not None and masks is None: + for image in images: + i = 255. * image.cpu().numpy() + img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8)) + pil_images.append(img) + + elif masks is not None and images is None: + for mask in masks: + mask_np = 255. * mask.cpu().numpy() + mask_img = Image.fromarray(np.clip(mask_np, 0, 255).astype(np.uint8)) + pil_images.append(mask_img) + else: + logging.warning("PreviewAnimation: No images or masks provided") + return { "ui": { "images": results, "animated": (None,), "text": "empty" }} + + num_frames = len(pil_images) + + c = len(pil_images) + for i in range(0, c, num_frames): + file = f"{filename}_{counter:05}_.webp" + pil_images[i].save(os.path.join(full_output_folder, file), save_all=True, duration=int(1000.0/fps), append_images=pil_images[i + 1:i + num_frames], lossless=False, quality=50, method=0) + results.append({ + "filename": file, + "subfolder": subfolder, + "type": self.type + }) + counter += 1 + + animated = num_frames != 1 + return { "ui": { "images": results, "animated": (animated,), "text": [f"{num_frames}x{pil_images[0].size[0]}x{pil_images[0].size[1]}"] } } + +class ImageResizeKJ: + upscale_methods = ["nearest-exact", "bilinear", "area", "bicubic", "lanczos"] + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "image": ("IMAGE",), + "width": ("INT", { "default": 512, "min": 0, "max": MAX_RESOLUTION, "step": 1, }), + "height": ("INT", { "default": 512, "min": 0, "max": MAX_RESOLUTION, "step": 1, }), + "upscale_method": (s.upscale_methods,), + "keep_proportion": ("BOOLEAN", { "default": False }), + "divisible_by": ("INT", { "default": 2, "min": 0, "max": 512, "step": 1, }), + }, + "optional" : { + #"width_input": ("INT", { "forceInput": True}), + #"height_input": ("INT", { "forceInput": True}), + "get_image_size": ("IMAGE",), + "crop": (["disabled","center", 0], { "tooltip": "0 will do the default center crop, this is a workaround for the widget order changing with the new frontend, as in old workflows the value of this widget becomes 0 automatically" }), + } + } + + RETURN_TYPES = ("IMAGE", "INT", "INT",) + RETURN_NAMES = ("IMAGE", "width", "height",) + FUNCTION = "resize" + CATEGORY = "KJNodes/image" + DEPRECATED = True + DESCRIPTION = """ +DEPRECATED! + +Due to ComfyUI frontend changes, this node should no longer be used, please check the +v2 of the node. This node is only kept to not completely break older workflows. + +""" + + def resize(self, image, width, height, keep_proportion, upscale_method, divisible_by, + width_input=None, height_input=None, get_image_size=None, crop="disabled"): + B, H, W, C = image.shape + + if width_input: + width = width_input + if height_input: + height = height_input + if get_image_size is not None: + _, height, width, _ = get_image_size.shape + + if keep_proportion and get_image_size is None: + # If one of the dimensions is zero, calculate it to maintain the aspect ratio + if width == 0 and height != 0: + ratio = height / H + width = round(W * ratio) + elif height == 0 and width != 0: + ratio = width / W + height = round(H * ratio) + elif width != 0 and height != 0: + # Scale based on which dimension is smaller in proportion to the desired dimensions + ratio = min(width / W, height / H) + width = round(W * ratio) + height = round(H * ratio) + else: + if width == 0: + width = W + if height == 0: + height = H + + if divisible_by > 1 and get_image_size is None: + width = width - (width % divisible_by) + height = height - (height % divisible_by) + + if crop == 0: #workaround for old workflows + crop = "center" + + image = image.movedim(-1,1) + image = common_upscale(image, width, height, upscale_method, crop) + image = image.movedim(1,-1) + + return(image, image.shape[2], image.shape[1],) + +class ImageResizeKJv2: + upscale_methods = ["nearest-exact", "bilinear", "area", "bicubic", "lanczos", "nvidia_rtx_vsr"] + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "image": ("IMAGE",), + "width": ("INT", { "default": 512, "min": 0, "max": MAX_RESOLUTION, "step": 1, }), + "height": ("INT", { "default": 512, "min": 0, "max": MAX_RESOLUTION, "step": 1, }), + "upscale_method": (s.upscale_methods,), + "keep_proportion": (["stretch", "resize", "pad", "pad_edge", "pad_edge_pixel", "crop", "pillarbox_blur", "total_pixels"], { "default": False }), + "pad_color": ("STRING", { "default": "0, 0, 0", "tooltip": "Color to use for padding."}), + "crop_position": (["center", "top", "bottom", "left", "right"], { "default": "center" }), + "divisible_by": ("INT", { "default": 2, "min": 0, "max": 512, "step": 1, }), + }, + "optional" : { + "mask": ("MASK",), + "device": (["cpu", "gpu"],), + #"per_batch": ("INT", { "default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 1, "tooltip": "Process images in sub-batches to reduce memory usage. 0 disables sub-batching."}), + }, + "hidden": { + "unique_id": "UNIQUE_ID", + }, + } + + RETURN_TYPES = ("IMAGE", "INT", "INT", "MASK",) + RETURN_NAMES = ("IMAGE", "width", "height", "mask",) + FUNCTION = "resize" + CATEGORY = "KJNodes/image" + DESCRIPTION = """ +Resizes the image to the specified width and height. +Size can be retrieved from the input. + +Keep proportions keeps the aspect ratio of the image, by +highest dimension. +""" + + def resize(self, image, width, height, keep_proportion, upscale_method, divisible_by, pad_color, crop_position, unique_id, device="cpu", mask=None, per_batch=64): + B, H, W, C = image.shape + + # Treat ComfyUI's 64x64 placeholder mask as no mask + if mask is not None and mask.shape[-2:] == (64, 64) and (H != 64 or W != 64): + mask = None + + # Scale mask to match image dimensions if they differ + if mask is not None and mask.shape[-2:] != (H, W): + mask = common_upscale(mask.unsqueeze(1), W, H, "bilinear", crop="disabled").squeeze(1) + + if device == "gpu": + if upscale_method == "lanczos": + raise ValueError("Lanczos is not supported on the GPU") + device = model_management.get_torch_device() + else: + device = torch.device("cpu") + + pillarbox_blur = keep_proportion == "pillarbox_blur" + + # Initialize padding variables + pad_left = pad_right = pad_top = pad_bottom = 0 + + if keep_proportion in ["resize", "total_pixels"] or keep_proportion.startswith("pad") or pillarbox_blur: + if keep_proportion == "total_pixels": + total_pixels = width * height + aspect_ratio = W / H + new_height = int(math.sqrt(total_pixels / aspect_ratio)) + new_width = int(math.sqrt(total_pixels * aspect_ratio)) + + # If one of the dimensions is zero, calculate it to maintain the aspect ratio + elif width == 0 and height == 0: + new_width = W + new_height = H + elif width == 0 and height != 0: + ratio = height / H + new_width = round(W * ratio) + new_height = height + elif height == 0 and width != 0: + ratio = width / W + new_width = width + new_height = round(H * ratio) + elif width != 0 and height != 0: + ratio = min(width / W, height / H) + new_width = round(W * ratio) + new_height = round(H * ratio) + else: + new_width = width + new_height = height + + if keep_proportion.startswith("pad") or pillarbox_blur: + # Calculate padding based on position + if crop_position == "center": + pad_left = (width - new_width) // 2 + pad_right = width - new_width - pad_left + pad_top = (height - new_height) // 2 + pad_bottom = height - new_height - pad_top + elif crop_position == "top": + pad_left = (width - new_width) // 2 + pad_right = width - new_width - pad_left + pad_top = 0 + pad_bottom = height - new_height + elif crop_position == "bottom": + pad_left = (width - new_width) // 2 + pad_right = width - new_width - pad_left + pad_top = height - new_height + pad_bottom = 0 + elif crop_position == "left": + pad_left = 0 + pad_right = width - new_width + pad_top = (height - new_height) // 2 + pad_bottom = height - new_height - pad_top + elif crop_position == "right": + pad_left = width - new_width + pad_right = 0 + pad_top = (height - new_height) // 2 + pad_bottom = height - new_height - pad_top + + width = new_width + height = new_height + else: + if width == 0: + width = W + if height == 0: + height = H + + if divisible_by > 1: + width = width - (width % divisible_by) + height = height - (height % divisible_by) + + # Preflight estimate (log-only when batching is active) + if per_batch != 0 and B > per_batch: + try: + bytes_per_elem = image.element_size() # typically 4 for float32 + est_total_bytes = B * height * width * C * bytes_per_elem + est_mb = est_total_bytes / (1024 * 1024) + msg = f"Resize v2estimated output ~{est_mb:.2f} MB; batching {per_batch}/{B}" + if unique_id and PromptServer is not None: + try: + PromptServer.instance.send_progress_text(msg, unique_id) + except Exception: + pass + else: + logging.info(f"[ImageResizeKJv2] estimated output ~{est_mb:.2f} MB; batching {per_batch}/{B}") + except Exception: + pass + + # NVIDIA RTX Video Super Resolution setup + nvvfx_sr = None + nvvfx_ctx = None + if upscale_method == "nvidia_rtx_vsr": + try: + import nvvfx + except ImportError: + raise ImportError("NVIDIA RTX Video Super Resolution is not available. Please install the nvidia-vfx library and ensure you have a compatible NVIDIA GPU.") + nvvfx_ctx = nvvfx.VideoSuperRes(nvvfx.effects.QualityLevel.ULTRA) + nvvfx_sr = nvvfx_ctx.__enter__() + nvvfx_sr.output_width = max(8, round(width / 8) * 8) + nvvfx_sr.output_height = max(8, round(height / 8) * 8) + nvvfx_sr.load() + + def _process_subbatch(in_image, in_mask, pad_left, pad_right, pad_top, pad_bottom): + # Avoid unnecessary clones; only move if needed + out_image = in_image if in_image.device == device else in_image.to(device) + out_mask = None if in_mask is None else (in_mask if in_mask.device == device else in_mask.to(device)) + + # Crop logic + if keep_proportion == "crop": + old_height = out_image.shape[-3] + old_width = out_image.shape[-2] + old_aspect = old_width / old_height + new_aspect = width / height + if old_aspect > new_aspect: + crop_w = round(old_height * new_aspect) + crop_h = old_height + else: + crop_w = old_width + crop_h = round(old_width / new_aspect) + if crop_position == "center": + x = (old_width - crop_w) // 2 + y = (old_height - crop_h) // 2 + elif crop_position == "top": + x = (old_width - crop_w) // 2 + y = 0 + elif crop_position == "bottom": + x = (old_width - crop_w) // 2 + y = old_height - crop_h + elif crop_position == "left": + x = 0 + y = (old_height - crop_h) // 2 + elif crop_position == "right": + x = old_width - crop_w + y = (old_height - crop_h) // 2 + out_image = out_image.narrow(-2, x, crop_w).narrow(-3, y, crop_h) + if out_mask is not None: + out_mask = out_mask.narrow(-1, x, crop_w).narrow(-2, y, crop_h) + + if upscale_method == "nvidia_rtx_vsr": + # Process each frame through RTX Video Super Resolution + frames_chw = out_image.movedim(-1, 1).cuda().contiguous() + upscaled_frames = [] + for j in range(frames_chw.shape[0]): + dlpack_out = nvvfx_sr.run(frames_chw[j]).image + upscaled_frames.append(torch.from_dlpack(dlpack_out).clone()) + out_image = torch.stack(upscaled_frames, dim=0).movedim(1, -1).cpu() + if out_mask is not None: + out_mask = common_upscale(out_mask.unsqueeze(1), width, height, "bilinear", crop="disabled").squeeze(1) + else: + out_image = common_upscale(out_image.movedim(-1,1), width, height, upscale_method, crop="disabled").movedim(1,-1) + if out_mask is not None: + if upscale_method == "lanczos": + out_mask = common_upscale(out_mask.unsqueeze(1).repeat(1, 3, 1, 1), width, height, upscale_method, crop="disabled").movedim(1,-1)[:, :, :, 0] + else: + out_mask = common_upscale(out_mask.unsqueeze(1), width, height, upscale_method, crop="disabled").squeeze(1) + + # Pad logic + if (keep_proportion.startswith("pad") or pillarbox_blur) and (pad_left > 0 or pad_right > 0 or pad_top > 0 or pad_bottom > 0): + padded_width = width + pad_left + pad_right + padded_height = height + pad_top + pad_bottom + if divisible_by > 1: + width_remainder = padded_width % divisible_by + height_remainder = padded_height % divisible_by + if width_remainder > 0: + extra_width = divisible_by - width_remainder + pad_right += extra_width + if height_remainder > 0: + extra_height = divisible_by - height_remainder + pad_bottom += extra_height + + pad_mode = ( + "pillarbox_blur" if pillarbox_blur else + "edge" if keep_proportion == "pad_edge" else + "edge_pixel" if keep_proportion == "pad_edge_pixel" else + "color" + ) + out_image, out_mask = ImagePadKJ.pad(self, out_image, pad_left, pad_right, pad_top, pad_bottom, 0, pad_color, pad_mode, mask=out_mask) + + return out_image, out_mask + + # If batching disabled (per_batch==0) or batch fits, process whole batch + if per_batch == 0 or B <= per_batch: + out_image, out_mask = _process_subbatch(image, mask, pad_left, pad_right, pad_top, pad_bottom) + else: + chunks = [] + mask_chunks = [] if mask is not None else None + total_batches = (B + per_batch - 1) // per_batch + current_batch = 0 + for start_idx in range(0, B, per_batch): + current_batch += 1 + end_idx = min(start_idx + per_batch, B) + sub_img = image[start_idx:end_idx] + sub_mask = mask[start_idx:end_idx] if mask is not None else None + sub_out_img, sub_out_mask = _process_subbatch(sub_img, sub_mask, pad_left, pad_right, pad_top, pad_bottom) + chunks.append(sub_out_img.cpu()) + if mask is not None: + mask_chunks.append(sub_out_mask.cpu() if sub_out_mask is not None else None) + # Per-batch progress update + if unique_id and PromptServer is not None: + try: + PromptServer.instance.send_progress_text( + f"Resize v2batch {current_batch}/{total_batches} · images {end_idx}/{B}", + unique_id + ) + except Exception: + pass + else: + logging.info(f"[ImageResizeKJv2] batch {current_batch}/{total_batches} · images {end_idx}/{B}") + out_image = torch.cat(chunks, dim=0) + if mask is not None and any(m is not None for m in mask_chunks): + out_mask = torch.cat([m for m in mask_chunks if m is not None], dim=0) + else: + out_mask = None + + # Cleanup NVIDIA RTX VSR context + if nvvfx_ctx is not None: + nvvfx_ctx.__exit__(None, None, None) + + # Progress UI + if unique_id and PromptServer is not None: + try: + num_elements = out_image.numel() + element_size = out_image.element_size() + memory_size_mb = (num_elements * element_size) / (1024 * 1024) + PromptServer.instance.send_progress_text( + f"Output: {out_image.shape[0]} x {out_image.shape[2]} x {out_image.shape[1]} | {memory_size_mb:.2f}MB", + unique_id + ) + except Exception: + pass + + return (out_image.cpu(), out_image.shape[2], out_image.shape[1], out_mask.cpu() if out_mask is not None else torch.zeros(1, 64,64, device=torch.device("cpu"), dtype=torch.float32)) + +class LoadAndResizeImage: + _color_channels = ["alpha", "red", "green", "blue"] + @classmethod + def INPUT_TYPES(s): + input_dir = folder_paths.get_input_directory() + files = [f.name for f in pathlib.Path(input_dir).iterdir() if f.is_file()] + return {"required": + { + "image": (sorted(files), {"image_upload": True}), + "resize": ("BOOLEAN", { "default": False }), + "width": ("INT", { "default": 512, "min": 0, "max": MAX_RESOLUTION, "step": 8, }), + "height": ("INT", { "default": 512, "min": 0, "max": MAX_RESOLUTION, "step": 8, }), + "repeat": ("INT", { "default": 1, "min": 1, "max": 4096, "step": 1, }), + "keep_proportion": ("BOOLEAN", { "default": False }), + "divisible_by": ("INT", { "default": 2, "min": 0, "max": 512, "step": 1, }), + "mask_channel": (s._color_channels, {"tooltip": "Channel to use for the mask output"}), + "background_color": ("STRING", { "default": "", "tooltip": "Fills the alpha channel with the specified color."}), + }, + } + + CATEGORY = "KJNodes/image" + RETURN_TYPES = ("IMAGE", "MASK", "INT", "INT", "STRING",) + RETURN_NAMES = ("image", "mask", "width", "height","image_path",) + FUNCTION = "load_image" + + def load_image(self, image, resize, width, height, repeat, keep_proportion, divisible_by, mask_channel, background_color): + image_path = folder_paths.get_annotated_filepath(image) + + img = node_helpers.pillow(Image.open, image_path) + img = ImageOps.exif_transpose(img) + + # Process the background_color using the helper function + if background_color: + color_list = string_to_color(background_color) + # Ensure we have RGBA (add alpha if only RGB) + if len(color_list) == 3: + bg_color_rgba = tuple(color_list) + (255,) + else: + bg_color_rgba = tuple(color_list) + else: + bg_color_rgba = None # No background color specified + + output_images = [] + output_masks = [] + w, h = None, None + + excluded_formats = ['MPO'] + + W, H = img.size + if resize: + if keep_proportion: + ratio = min(width / W, height / H) + width = round(W * ratio) + height = round(H * ratio) + else: + if width == 0: + width = W + if height == 0: + height = H + + if divisible_by > 1: + width = width - (width % divisible_by) + height = height - (height % divisible_by) + else: + width, height = W, H + + for frame in ImageSequence.Iterator(img): + frame = node_helpers.pillow(ImageOps.exif_transpose, frame) + + if frame.mode == 'I': + frame = frame.point(lambda i: i * (1 / 255)) + + if frame.mode == 'P': + frame = frame.convert("RGBA") + elif 'A' in frame.getbands(): + frame = frame.convert("RGBA") + + # Extract alpha channel if it exists + if 'A' in frame.getbands() and bg_color_rgba: + alpha_mask = np.array(frame.getchannel('A')).astype(np.float32) / 255.0 + alpha_mask = 1. - torch.from_numpy(alpha_mask) + bg_image = Image.new("RGBA", frame.size, bg_color_rgba) + # Composite the frame onto the background + frame = Image.alpha_composite(bg_image, frame) + else: + alpha_mask = torch.zeros((1, 64, 64), dtype=torch.float32, device="cpu") + + image = frame.convert("RGB") + + if len(output_images) == 0: + w = image.size[0] + h = image.size[1] + + if image.size[0] != w or image.size[1] != h: + continue + if resize: + image = image.resize((width, height), Image.Resampling.BILINEAR) + + image = np.array(image).astype(np.float32) / 255.0 + image = torch.from_numpy(image)[None,] + + c = mask_channel[0].upper() + if c in frame.getbands(): + if resize: + frame = frame.resize((width, height), Image.Resampling.BILINEAR) + mask = np.array(frame.getchannel(c)).astype(np.float32) / 255.0 + mask = torch.from_numpy(mask) + if c == 'A' and bg_color_rgba: + mask = alpha_mask + elif c == 'A': + mask = 1. - mask + else: + mask = torch.zeros((1, 64, 64), dtype=torch.float32, device="cpu") + + output_images.append(image) + output_masks.append(mask.unsqueeze(0)) + + if len(output_images) > 1 and img.format not in excluded_formats: + output_image = torch.cat(output_images, dim=0) + output_mask = torch.cat(output_masks, dim=0) + else: + output_image = output_images[0] + output_mask = output_masks[0] + if repeat > 1: + output_image = output_image.repeat(repeat, 1, 1, 1) + output_mask = output_mask.repeat(repeat, 1, 1) + + return (output_image, output_mask, width, height, image_path) + + + # @classmethod + # def IS_CHANGED(s, image, **kwargs): + # 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(s, image): + if not folder_paths.exists_annotated_filepath(image): + return "Invalid image file: {}".format(image) + + return True + +class LoadImagesFromFolderKJ: + # Dictionary to store folder hashes + folder_hashes = {} + + @classmethod + def IS_CHANGED(cls, folder, **kwargs): + if folder and not os.path.isabs(folder) and args.base_directory: + folder = os.path.join(args.base_directory, folder) + if not folder or not os.path.isdir(folder): + return float("NaN") + + valid_extensions = ['.jpg', '.jpeg', '.png', '.webp', '.tga'] + include_subfolders = kwargs.get('include_subfolders', False) + + file_data = [] + if include_subfolders: + for root, _, files in os.walk(folder): + for file in files: + if any(file.lower().endswith(ext) for ext in valid_extensions): + path = os.path.join(root, file) + try: + mtime = os.path.getmtime(path) + file_data.append((path, mtime)) + except OSError: + pass + else: + for file in sorted(os.listdir(folder)): + if any(file.lower().endswith(ext) for ext in valid_extensions): + path = os.path.join(folder, file) + try: + mtime = os.path.getmtime(path) + file_data.append((path, mtime)) + except OSError: + pass + + file_data.sort() + + combined_hash = hashlib.md5() + combined_hash.update(folder.encode('utf-8')) + combined_hash.update(str(len(file_data)).encode('utf-8')) + + for path, mtime in file_data: + combined_hash.update(f"{path}:{mtime}".encode('utf-8')) + + current_hash = combined_hash.hexdigest() + + old_hash = cls.folder_hashes.get(folder) + cls.folder_hashes[folder] = current_hash + + if old_hash == current_hash: + return old_hash + + return current_hash + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "folder": ("STRING", {"default": ""}), + "width": ("INT", {"default": 1024, "min": -1, "step": 1}), + "height": ("INT", {"default": 1024, "min": -1, "step": 1}), + "keep_aspect_ratio": (["crop", "pad", "stretch",],), + }, + "optional": { + "image_load_cap": ("INT", {"default": 0, "min": 0, "step": 1}), + "start_index": ("INT", {"default": 0, "min": 0, "step": 1}), + "include_subfolders": ("BOOLEAN", {"default": False}), + } + } + + RETURN_TYPES = ("IMAGE", "MASK", "INT", "STRING",) + RETURN_NAMES = ("image", "mask", "count", "image_path",) + FUNCTION = "load_images" + CATEGORY = "KJNodes/image" + DESCRIPTION = """Loads images from a folder into a batch, images are resized and loaded into a batch.""" + + def load_images(self, folder, width, height, image_load_cap, start_index, keep_aspect_ratio, include_subfolders=False): + if folder and not os.path.isabs(folder) and args.base_directory: + folder = os.path.join(args.base_directory, folder) + if not folder or not os.path.isdir(folder): + raise FileNotFoundError(f"Folder '{folder}' cannot be found.") + + valid_extensions = ['.jpg', '.jpeg', '.png', '.webp', '.tga'] + image_paths = [] + if include_subfolders: + for root, _, files in os.walk(folder): + for file in files: + if any(file.lower().endswith(ext) for ext in valid_extensions): + image_paths.append(os.path.join(root, file)) + else: + for file in sorted(os.listdir(folder)): + if any(file.lower().endswith(ext) for ext in valid_extensions): + image_paths.append(os.path.join(folder, file)) + + dir_files = sorted(image_paths) + + if len(dir_files) == 0: + raise FileNotFoundError(f"No files in directory '{folder}'.") + + # start at start_index + dir_files = dir_files[start_index:] + + images = [] + masks = [] + image_path_list = [] + + limit_images = False + if image_load_cap > 0: + limit_images = True + image_count = 0 + + pbar = ProgressBar(len(dir_files)) + + for image_path in dir_files: + if os.path.isdir(image_path): + continue + if limit_images and image_count >= image_load_cap: + break + i = Image.open(image_path) + i = ImageOps.exif_transpose(i) + + # Resize image to maximum dimensions + if width == -1 and height == -1: + width = i.size[0] + height = i.size[1] + if i.size != (width, height): + i = self.resize_with_aspect_ratio(i, width, height, keep_aspect_ratio) + + + image = i.convert("RGB") + image = np.array(image).astype(np.float32) / 255.0 + image = torch.from_numpy(image)[None,] + + if 'A' in i.getbands(): + mask = np.array(i.getchannel('A')).astype(np.float32) / 255.0 + mask = 1. - torch.from_numpy(mask) + if mask.shape != (height, width): + mask = torch.nn.functional.interpolate(mask.unsqueeze(0).unsqueeze(0), + size=(height, width), + mode='bilinear', + align_corners=False).squeeze() + else: + mask = torch.zeros((height, width), dtype=torch.float32, device="cpu") + + images.append(image) + masks.append(mask) + image_path_list.append(image_path) + image_count += 1 + pbar.update(1) + + if len(images) == 1: + return (images[0], masks[0], 1, image_path_list) + + elif len(images) > 1: + image1 = images[0] + mask1 = masks[0].unsqueeze(0) + + for image2 in images[1:]: + image1 = torch.cat((image1, image2), dim=0) + + for mask2 in masks[1:]: + mask1 = torch.cat((mask1, mask2.unsqueeze(0)), dim=0) + + return (image1, mask1, len(images), image_path_list) + def resize_with_aspect_ratio(self, img, width, height, mode): + if mode == "stretch": + return img.resize((width, height), Image.Resampling.LANCZOS) + + img_width, img_height = img.size + aspect_ratio = img_width / img_height + target_ratio = width / height + + if mode == "crop": + # Calculate dimensions for center crop + if aspect_ratio > target_ratio: + # Image is wider - crop width + new_width = int(height * aspect_ratio) + img = img.resize((new_width, height), Image.Resampling.LANCZOS) + left = (new_width - width) // 2 + return img.crop((left, 0, left + width, height)) + else: + # Image is taller - crop height + new_height = int(width / aspect_ratio) + img = img.resize((width, new_height), Image.Resampling.LANCZOS) + top = (new_height - height) // 2 + return img.crop((0, top, width, top + height)) + + elif mode == "pad": + pad_color = self.get_edge_color(img) + # Calculate dimensions for padding + if aspect_ratio > target_ratio: + # Image is wider - pad height + new_height = int(width / aspect_ratio) + img = img.resize((width, new_height), Image.Resampling.LANCZOS) + padding = (height - new_height) // 2 + padded = Image.new('RGBA', (width, height), pad_color) + padded.paste(img, (0, padding)) + return padded + else: + # Image is taller - pad width + new_width = int(height * aspect_ratio) + img = img.resize((new_width, height), Image.Resampling.LANCZOS) + padding = (width - new_width) // 2 + padded = Image.new('RGBA', (width, height), pad_color) + padded.paste(img, (padding, 0)) + return padded + def get_edge_color(self, img): + """Sample edges and return dominant color""" + width, height = img.size + img = img.convert('RGBA') + + # Create 1-pixel high/wide images from edges + top = img.crop((0, 0, width, 1)) + bottom = img.crop((0, height-1, width, height)) + left = img.crop((0, 0, 1, height)) + right = img.crop((width-1, 0, width, height)) + + # Combine edges into single image + edges = Image.new('RGBA', (width*2 + height*2, 1)) + edges.paste(top, (0, 0)) + edges.paste(bottom, (width, 0)) + edges.paste(left.resize((height, 1)), (width*2, 0)) + edges.paste(right.resize((height, 1)), (width*2 + height, 0)) + + # Get median color + stat = ImageStat.Stat(edges) + median = tuple(map(int, stat.median)) + return median + +class ImageGridtoBatch: + @classmethod + def INPUT_TYPES(s): + return {"required": { + "image": ("IMAGE", ), + "columns": ("INT", {"default": 3, "min": 1, "max": 8, "tooltip": "The number of columns in the grid."}), + "rows": ("INT", {"default": 0, "min": 1, "max": 8, "tooltip": "The number of rows in the grid. Set to 0 for automatic calculation."}), + } + } + + RETURN_TYPES = ("IMAGE",) + FUNCTION = "decompose" + CATEGORY = "KJNodes/image" + DESCRIPTION = "Converts a grid of images to a batch of images." + + def decompose(self, image, columns, rows): + B, H, W, C = image.shape + + # Calculate cell width, rounding down + cell_width = W // columns + + if rows == 0: + # If rows is 0, calculate number of full rows + cell_height = H // columns + rows = H // cell_height + else: + # If rows is specified, adjust cell_height + cell_height = H // rows + + # Crop the image to fit full cells + image = image[:, :rows*cell_height, :columns*cell_width, :] + + # Reshape and permute the image to get the grid + image = image.view(B, rows, cell_height, columns, cell_width, C) + image = image.permute(0, 1, 3, 2, 4, 5).contiguous() + image = image.view(B, rows * columns, cell_height, cell_width, C) + + # Reshape to the final batch tensor + img_tensor = image.view(-1, cell_height, cell_width, C) + + return (img_tensor,) + +class SaveImageKJ: + def __init__(self): + self.type = "output" + self.prefix_append = "" + self.compress_level = 4 + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "images": ("IMAGE", {"tooltip": "The images to save."}), + "filename_prefix": ("STRING", {"default": "ComfyUI", "tooltip": "The prefix for the file to save. This may include formatting information such as %date:yyyy-MM-dd% or %Empty Latent Image.width% to include values from nodes."}), + "output_folder": ("STRING", {"default": "output", "tooltip": "The folder to save the images to."}), + }, + "optional": { + "caption_file_extension": ("STRING", {"default": ".txt", "tooltip": "The extension for the caption file. Limited to plain-text/data formats."}), + "caption": ("STRING", {"forceInput": True, "tooltip": "string to save as .txt file"}), + }, + "hidden": { + "prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO" + }, + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("filename",) + FUNCTION = "save_images" + + OUTPUT_NODE = True + + CATEGORY = "KJNodes/image" + DESCRIPTION = "Saves the input images to your ComfyUI output directory." + + def save_images(self, images, output_folder, filename_prefix="ComfyUI", prompt=None, extra_pnginfo=None, caption=None, caption_file_extension=".txt"): + filename_prefix += self.prefix_append + + if os.path.isabs(output_folder): + if not os.path.exists(output_folder): + os.makedirs(output_folder, exist_ok=True) + full_output_folder = output_folder + _, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(filename_prefix, output_folder, images[0].shape[1], images[0].shape[0]) + else: + self.output_dir = folder_paths.get_output_directory() + full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(filename_prefix, self.output_dir, images[0].shape[1], images[0].shape[0]) + + # sanitize caption extension: strip path components so it can't traverse out of the chosen folder, and allowlist to text/data formats + if caption is not None: + caption_file_extension = os.path.basename(caption_file_extension) + if caption_file_extension and not caption_file_extension.startswith("."): + caption_file_extension = "." + caption_file_extension + if caption_file_extension.lower() not in SaveStringKJ.ALLOWED_EXTENSIONS: + raise ValueError(f"Disallowed caption extension '{caption_file_extension}'. Allowed: {', '.join(SaveStringKJ.ALLOWED_EXTENSIONS)}") + + results = list() + for (batch_number, image) in enumerate(images): + i = 255. * image.cpu().numpy() + img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8)) + metadata = None + if not args.disable_metadata: + metadata = PngInfo() + if prompt is not None: + metadata.add_text("prompt", json.dumps(prompt)) + if extra_pnginfo is not None: + for x in extra_pnginfo: + metadata.add_text(x, json.dumps(extra_pnginfo[x])) + + filename_with_batch_num = filename.replace("%batch_num%", str(batch_number)) + base_file_name = f"{filename_with_batch_num}_{counter:05}_" + file = f"{base_file_name}.png" + img.save(os.path.join(full_output_folder, file), pnginfo=metadata, compress_level=self.compress_level) + results.append({ + "filename": file, + "subfolder": subfolder, + "type": self.type + }) + if caption is not None: + txt_file = base_file_name + caption_file_extension + file_path = os.path.join(full_output_folder, txt_file) + + if os.path.commonpath((os.path.abspath(full_output_folder), os.path.abspath(file_path))) != os.path.abspath(full_output_folder): + raise ValueError(f"Refusing to write caption outside the target folder: {file_path}") + with open(file_path, "w", encoding="utf-8") as f: + f.write(caption) + + counter += 1 + + return file, + +class SaveStringKJ: + ALLOWED_EXTENSIONS = [".txt", ".caption", ".json", ".yaml", ".yml", ".md", ".csv", ".tsv", ".xml", ".log", ".ini", ".toml"] + + def __init__(self): + self.output_dir = folder_paths.get_output_directory() + self.type = "output" + self.prefix_append = "" + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "string": ("STRING", {"forceInput": True, "tooltip": "string to save as .txt file"}), + "filename_prefix": ("STRING", {"default": "text", "tooltip": "The prefix for the file to save. This may include formatting information such as %date:yyyy-MM-dd% or %Empty Latent Image.width% to include values from nodes."}), + "output_folder": ("STRING", {"default": "output", "tooltip": "Subfolder within the ComfyUI output directory to save to. Paths resolving outside the output directory are rejected."}), + }, + "optional": { + "file_extension": ("STRING", {"default": ".txt", "tooltip": "The extension for the saved file. Limited to plain-text/data formats."}), + }, + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("filename",) + FUNCTION = "save_string" + + OUTPUT_NODE = True + + CATEGORY = "KJNodes/misc" + DESCRIPTION = "Saves the input string to your ComfyUI output directory." + + def save_string(self, string, output_folder, filename_prefix="text", file_extension=".txt"): + filename_prefix += self.prefix_append + + output_dir = os.path.abspath(self.output_dir) + if output_folder and output_folder != "output": + sub = os.path.splitdrive(output_folder)[1].replace("\\", "/").lstrip("/") + target_dir = os.path.abspath(os.path.join(output_dir, sub)) + else: + target_dir = output_dir + + try: + inside = os.path.commonpath((output_dir, target_dir)) == output_dir + except ValueError: + inside = False + if not inside: + raise ValueError(f"output_folder must resolve within the ComfyUI output directory: {target_dir}") + os.makedirs(target_dir, exist_ok=True) + + full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(filename_prefix, target_dir) + + file_extension = os.path.basename(file_extension) + if file_extension and not file_extension.startswith("."): + file_extension = "." + file_extension + + if file_extension.lower() not in self.ALLOWED_EXTENSIONS: + raise ValueError(f"Disallowed file extension '{file_extension}'. Allowed: {', '.join(self.ALLOWED_EXTENSIONS)}") + + base_file_name = f"{filename_prefix}_{counter:05}_" + + txt_file = base_file_name + file_extension + file_path = os.path.join(full_output_folder, txt_file) + while os.path.exists(file_path): + counter += 1 + base_file_name = f"{filename_prefix}_{counter:05}_" + txt_file = base_file_name + file_extension + file_path = os.path.join(full_output_folder, txt_file) + + if os.path.commonpath((os.path.abspath(full_output_folder), os.path.abspath(file_path))) != os.path.abspath(full_output_folder): + raise ValueError(f"Refusing to write outside the target folder: {file_path}") + with open(file_path, 'w', encoding="utf-8") as f: + f.write(string) + + return file_path, + +class FastPreview: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "image": ("IMAGE", ), + "format": (["JPEG", "PNG"], {"default": "JPEG"}), + "max_size": ("INT", {"default": 768, "min": 128, "max": 4096, "step": 64, + "tooltip": "Maximum width or height for the preview. Images larger than this are downscaled before encoding."}), + }, + "hidden": { + "unique_id": "UNIQUE_ID", + "prompt_id": "PROMPT_ID", + }, + } + + RETURN_TYPES = () + FUNCTION = "preview" + CATEGORY = "KJNodes/experimental" + OUTPUT_NODE = True + DESCRIPTION = "Fast image preview using binary websocket, bypassing base64/JSON overhead." + + def preview(self, image, format, max_size, unique_id=None, prompt_id=None): + arr = image[0].cpu().mul(255).clamp(0, 255).byte().numpy() + h, w = arr.shape[:2] + + if w > max_size or h > max_size: + scale = max_size / max(w, h) + new_w, new_h = int(w * scale), int(h * scale) + if HAS_CV2: + arr = cv2.resize(arr, (new_w, new_h), interpolation=cv2.INTER_LINEAR) + pil_image = Image.fromarray(arr) + else: + pil_image = Image.fromarray(arr).resize((new_w, new_h), Image.BILINEAR) + else: + pil_image = Image.fromarray(arr) + + if format == "JPEG" and pil_image.mode != "RGB": + pil_image = pil_image.convert("RGB") + + if PromptServer is not None and unique_id is not None: + server = PromptServer.instance + client_supports_metadata = False + if hasattr(BinaryEventTypes, "PREVIEW_IMAGE_WITH_METADATA"): + try: + from comfy_api import feature_flags + client_supports_metadata = feature_flags.supports_feature( + server.sockets_metadata, server.client_id, "supports_preview_metadata" + ) + except Exception: + client_supports_metadata = False + + if client_supports_metadata: + server.send_sync( + BinaryEventTypes.PREVIEW_IMAGE_WITH_METADATA, + ( + (format, pil_image, None), + { + "node_id": unique_id, + "display_node_id": unique_id, + "prompt_id": prompt_id or "", + }, + ), + server.client_id, + ) + else: + server.send_sync( + BinaryEventTypes.UNENCODED_PREVIEW_IMAGE, + (format, pil_image, None), + server.client_id, + ) + + return {"ui": {"fast_preview": [True]}, "result": ()} + + +class FastPreviewBatch(io.ComfyNode): + @classmethod + def define_schema(cls): + return io.Schema( + node_id="FastPreviewBatch", + display_name="Fast Preview Batch", + category="KJNodes/experimental", + description="Encodes an image batch as an all-I-frame H.264 MP4 thumbnail strip " + "and shows it as an interactive grid. Click a tile to enlarge with " + "prev/next browsing. Avoids materializing N PNGs.", + inputs=[ + io.MultiType.Input("input", [io.Image, io.Mask], tooltip="Image or mask batch to preview."), + io.Int.Input("max_thumb_size", default=512, min=512, max=1024, step=8, + tooltip="Detail-view (mp4) thumbnail max side. Strip thumbs for the grid are auto-capped at 256."), + io.Int.Input("crf", default=25, min=0, max=51, step=1, + tooltip="H.264 CRF. Lower = higher quality / larger file."), + io.Int.Input("max_grid_frames", default=1024, min=1, max=4096, step=1, + tooltip="If batch exceeds this, frames are stride-sampled evenly."), + ], + is_output_node=True, + ) + + @classmethod + def execute(cls, input, max_thumb_size, crf, max_grid_frames) -> io.NodeOutput: + import av + import threading + import queue as _queue + if input.ndim == 3: + images = input.reshape((-1, 1, input.shape[-2], input.shape[-1])).movedim(1, -1).expand(-1, -1, -1, 3) + else: + images = input + B, H, W, _ = images.shape + + if B > max_grid_frames: + idx = torch.linspace(0, B - 1, max_grid_frames).round().long().tolist() + else: + idx = list(range(B)) + total = len(idx) + + scale = min(1.0, max_thumb_size / max(H, W)) + new_w = max(2, int(round(W * scale))) + new_h = max(2, int(round(H * scale))) + # yuv420p needs even dimensions + new_w -= new_w & 1 + new_h -= new_h & 1 + + # Strip thumbs serve the grid only; cap at 256 so the tiled JPEG stays well + # under any browser image-decode limit regardless of detail-view size. + STRIP_MAX = 256 + strip_scale = min(1.0, STRIP_MAX / max(new_h, new_w)) + strip_w = max(2, int(round(new_w * strip_scale))) + strip_h = max(2, int(round(new_h * strip_scale))) + + output_dir = folder_paths.get_temp_directory() + prefix = "kj_batch_preview_" + ''.join(random.choice("abcdefghijklmnopqrstuvwxyz") for _ in range(6)) + full_output_folder, filename, counter, subfolder, _ = folder_paths.get_save_image_path( + prefix, output_dir, new_w, new_h + ) + file = f"{filename}_{counter:05}_.mp4" + filepath = os.path.join(full_output_folder, file) + strip_file = f"{filename}_{counter:05}_grid.jpg" + strip_path = os.path.join(full_output_folder, strip_file) + + # Square-ish tiling for the JS grid renderer. + strip_cols = max(1, int(math.ceil(math.sqrt(total)))) + strip_rows = int(math.ceil(total / strip_cols)) + strip_arr = np.zeros((strip_rows * strip_h, strip_cols * strip_w, 3), dtype=np.uint8) + + fps = 30 + container = av.open(filepath, mode="w") + try: + stream = container.add_stream("libx264", rate=Fraction(fps, 1)) + stream.width = new_w + stream.height = new_h + stream.pix_fmt = "yuv420p" + stream.options = {"crf": str(crf), "preset": "ultrafast", "g": "1", "tune": "fastdecode"} + + chunk_size = 32 + need_resize = (new_h, new_w) != (H, W) + need_strip_resize = (strip_h, strip_w) != (new_h, new_w) + mode = 'area' if scale < 1.0 else 'bilinear' + work_device = model_management.get_torch_device() + + def _to_numpy_nhwc_u8(t): + return (t.mul(255).clamp(0, 255) + .to(dtype=torch.uint8, device='cpu') + .permute(0, 2, 3, 1).contiguous().numpy()) + + # Producer: GPU resize + transfer; consumer (this thread): PyAV encode. + # PyTorch GPU ops, host transfers, and PyAV's libx264 call all release the + # GIL, so threading actually overlaps the two stages. + frame_queue = _queue.Queue(maxsize=2) + producer_error = [None] + + def producer(): + try: + for c_start in range(0, total, chunk_size): + c_idx = idx[c_start:c_start + chunk_size] + sel = (images[c_idx, ..., :3].permute(0, 3, 1, 2).contiguous() + .to(device=work_device, non_blocking=True)) + sel_video = F.interpolate(sel, size=(new_h, new_w), mode=mode) if need_resize else sel + sel_strip = F.interpolate(sel_video, size=(strip_h, strip_w), mode='area') if need_strip_resize else sel_video + video_frames = _to_numpy_nhwc_u8(sel_video) + strip_frames = video_frames if sel_strip is sel_video else _to_numpy_nhwc_u8(sel_strip) + del sel, sel_video, sel_strip + frame_queue.put((c_start, video_frames, strip_frames)) + except Exception as e: + producer_error[0] = e + finally: + frame_queue.put(None) + + producer_thread = threading.Thread(target=producer, daemon=True) + producer_thread.start() + + pbar = ProgressBar(total) + while True: + item = frame_queue.get() + if item is None: + break + c_start, video_frames, strip_frames = item + for i in range(video_frames.shape[0]): + global_idx = c_start + i + sr = global_idx // strip_cols + sc = global_idx % strip_cols + strip_arr[sr * strip_h:(sr + 1) * strip_h, sc * strip_w:(sc + 1) * strip_w] = strip_frames[i] + frame = av.VideoFrame.from_ndarray(video_frames[i], format="rgb24") + for packet in stream.encode(frame): + container.mux(packet) + pbar.update(1) + + producer_thread.join() + if producer_error[0] is not None: + raise producer_error[0] + + for packet in stream.encode(): + container.mux(packet) + finally: + container.close() + + Image.fromarray(strip_arr).save(strip_path, quality=85) + + return io.NodeOutput(ui={"kj_batch_preview": [{ + "filename": file, + "subfolder": subfolder, + "type": "temp", + "frame_count": total, + "fps": fps, + "thumb_w": new_w, + "thumb_h": new_h, + "strip_filename": strip_file, + "strip_cols": strip_cols, + "strip_cell_w": strip_w, + "strip_cell_h": strip_h, + }]}) + + +class ImageCropByMaskAndResize: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "image": ("IMAGE", ), + "mask": ("MASK", ), + "base_resolution": ("INT", { "default": 512, "min": 0, "max": MAX_RESOLUTION, "step": 8, }), + "padding": ("INT", { "default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 1, }), + "min_crop_resolution": ("INT", { "default": 128, "min": 0, "max": MAX_RESOLUTION, "step": 8, }), + "max_crop_resolution": ("INT", { "default": 512, "min": 0, "max": MAX_RESOLUTION, "step": 8, }), + }, + } + + RETURN_TYPES = ("IMAGE", "MASK", "BBOX", ) + RETURN_NAMES = ("images", "masks", "bbox",) + FUNCTION = "crop" + CATEGORY = "KJNodes/image" + + def crop_by_mask(self, mask, padding=0, min_crop_resolution=None, max_crop_resolution=None): + """ + Calculate bounding box from mask with proper padding boundary protection + Ensures crop region never exceeds original image boundaries + """ + iy, ix = (mask == 1).nonzero(as_tuple=True) + h0, w0 = mask.shape + + # Handle empty mask + if iy.numel() == 0: + x_c = w0 / 2.0 + y_c = h0 / 2.0 + width = 0 + height = 0 + else: + x_min = ix.min().item() + x_max = ix.max().item() + y_min = iy.min().item() + y_max = iy.max().item() + width = x_max - x_min + 1 # Include boundary pixels + height = y_max - y_min + 1 + x_c = (x_min + x_max) / 2.0 + y_c = (y_min + y_max) / 2.0 + + # Apply min/max resolution constraints + if min_crop_resolution: + width = max(width, min_crop_resolution) + height = max(height, min_crop_resolution) + if max_crop_resolution: + width = min(width, max_crop_resolution) + height = min(height, max_crop_resolution) + + # Critical: Limit padding expansion to available image space + # Calculate maximum possible padding for each direction + max_padding_x = min((w0 - width) // 2, padding) + max_padding_y = min((h0 - height) // 2, padding) + + # Apply constrained padding + final_width = width + 2 * max_padding_x + final_height = height + 2 * max_padding_y + + # Ensure final dimensions don't exceed image bounds + final_width = min(final_width, w0) + final_height = min(final_height, h0) + + # Calculate top-left corner with boundary protection + # Center the crop while respecting image boundaries + x0 = max(0, min(int(x_c - final_width / 2), w0 - final_width)) + y0 = max(0, min(int(y_c - final_height / 2), h0 - final_height)) + + return (x0, y0, final_width, final_height) + + def crop(self, image, mask, base_resolution, padding=0, min_crop_resolution=128, max_crop_resolution=512): + """ + Main crop and resize function with uniform target dimensions for all batch items + """ + mask = mask.round() + image_list = [] + mask_list = [] + bbox_list = [] + + # Step 1: Calculate individual bounding boxes + bbox_params = [] + aspect_ratios = [] + for i in range(image.shape[0]): + x0, y0, w, h = self.crop_by_mask(mask[i], padding, min_crop_resolution, max_crop_resolution) + bbox_params.append((x0, y0, w, h)) + aspect_ratios.append(w / h) + + # Step 2: Calculate uniform target dimensions based on maximum aspect ratio + max_w = max([w for x0, y0, w, h in bbox_params]) + max_h = max([h for x0, y0, w, h in bbox_params]) + max_aspect_ratio = max(aspect_ratios) + + # Round up to nearest multiple of 16 for stable processing + max_w = (max_w + 15) // 16 * 16 + max_h = (max_h + 15) // 16 * 16 + + # Determine target dimensions maintaining aspect ratio + if max_aspect_ratio > 1: + target_width = base_resolution + target_height = int(base_resolution / max_aspect_ratio) + else: + target_height = base_resolution + target_width = int(base_resolution * max_aspect_ratio) + + # Ensure target dimensions are multiples of 16 + target_width = (target_width + 15) // 16 * 16 + target_height = (target_height + 15) // 16 * 16 + + # Step 3: Process each image with uniform crop size + for i in range(image.shape[0]): + orig_x0, orig_y0, orig_w, orig_h = bbox_params[i] + + # Calculate center of original bounding box + x_center = orig_x0 + orig_w / 2 + y_center = orig_y0 + orig_h / 2 + + # Define uniform crop region centered on each image's bounding box + # This ensures all crops have exactly the same dimensions + x0_new = max(0, min(int(x_center - max_w / 2), image.shape[2] - max_w)) + y0_new = max(0, min(int(y_center - max_h / 2), image.shape[1] - max_h)) + x1_new = x0_new + max_w + y1_new = y0_new + max_h + + # Extract cropped regions + cropped_image = image[i][y0_new:y1_new, x0_new:x1_new, :] + cropped_mask = mask[i][y0_new:y1_new, x0_new:x1_new] + + # Resize to exact target dimensions + # Image with lanczos interpolation + cropped_image = cropped_image.unsqueeze(0).movedim(-1, 1) + cropped_image = common_upscale(cropped_image, target_width, target_height, "lanczos", "disabled") + cropped_image = cropped_image.movedim(1, -1).squeeze(0) + + # Mask with bilinear interpolation + cropped_mask = cropped_mask.unsqueeze(0).unsqueeze(0) + cropped_mask = common_upscale(cropped_mask, target_width, target_height, 'bilinear', "disabled") + cropped_mask = cropped_mask.squeeze(0).squeeze(0) + + image_list.append(cropped_image) + mask_list.append(cropped_mask) + bbox_list.append((x0_new, y0_new, x1_new, y1_new)) + + return (torch.stack(image_list), torch.stack(mask_list), bbox_list) + +class ImageCropByMask: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "image": ("IMAGE", ), + "mask": ("MASK", ), + }, + } + + RETURN_TYPES = ("IMAGE", ) + RETURN_NAMES = ("image", ) + FUNCTION = "crop" + CATEGORY = "KJNodes/image" + DESCRIPTION = "Crops the input images based on the provided mask." + + def crop(self, image, mask): + B, H, W, C = image.shape + mask = mask.round() + + # Find bounding box for each batch + crops = [] + + for b in range(B): + # Get coordinates of non-zero elements + rows = torch.any(mask[min(b, mask.shape[0]-1)] > 0, dim=1) + cols = torch.any(mask[min(b, mask.shape[0]-1)] > 0, dim=0) + + # Find boundaries + y_min, y_max = torch.where(rows)[0][[0, -1]] + x_min, x_max = torch.where(cols)[0][[0, -1]] + + # Crop image and mask + crop = image[b:b+1, y_min:y_max+1, x_min:x_max+1, :] + crops.append(crop) + + # Stack results back together + cropped_images = torch.cat(crops, dim=0) + + return (cropped_images, ) + + + +class ImageUncropByMask: + + @classmethod + def INPUT_TYPES(s): + return {"required": + { + "destination": ("IMAGE",), + "source": ("IMAGE",), + "mask": ("MASK",), + "bbox": (BBOX_TYPES,), + }, + } + + CATEGORY = "KJNodes/image" + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("image",) + FUNCTION = "uncrop" + + def uncrop(self, destination, source, mask, bbox=None): + + output_list = [] + + B, H, W, C = destination.shape + + bbox = normalize_bboxes(bbox, "xyxy") + + for i in range(source.shape[0]): + x0, y0, x1, y1 = bbox[i] + bbox_height = y1 - y0 + bbox_width = x1 - x0 + + # Resize source image to match the bounding box dimensions + #resized_source = F.interpolate(source[i].unsqueeze(0).movedim(-1, 1), size=(bbox_height, bbox_width), mode='bilinear', align_corners=False) + resized_source = common_upscale(source[i].unsqueeze(0).movedim(-1, 1), bbox_width, bbox_height, "lanczos", "disabled") + resized_source = resized_source.movedim(1, -1).squeeze(0) + + # Resize mask to match the bounding box dimensions + resized_mask = common_upscale(mask[i].unsqueeze(0).unsqueeze(0), bbox_width, bbox_height, "bilinear", "disabled") + resized_mask = resized_mask.squeeze(0).squeeze(0) + + # Calculate padding values + pad_left = x0 + pad_right = W - x1 + pad_top = y0 + pad_bottom = H - y1 + + # Pad the resized source image and mask to fit the destination dimensions + padded_source = F.pad(resized_source, pad=(0, 0, pad_left, pad_right, pad_top, pad_bottom), mode='constant', value=0) + padded_mask = F.pad(resized_mask, pad=(pad_left, pad_right, pad_top, pad_bottom), mode='constant', value=0) + + # Ensure the padded mask has the correct shape + padded_mask = padded_mask.unsqueeze(2).expand(-1, -1, destination[i].shape[2]) + # Ensure the padded source has the correct shape + padded_source = padded_source.unsqueeze(2).expand(-1, -1, -1, destination[i].shape[2]).squeeze(2) + + # Combine the destination and padded source images using the mask + result = destination[i] * (1.0 - padded_mask) + padded_source * padded_mask + + output_list.append(result) + + + return (torch.stack(output_list),) + +class ImageCropByMaskBatch: + @classmethod + def INPUT_TYPES(s): + return {"required": { + "image": ("IMAGE", ), + "masks": ("MASK", ), + "width": ("INT", {"default": 512, "min": 0, "max": MAX_RESOLUTION, "step": 8, }), + "height": ("INT", {"default": 512, "min": 0, "max": MAX_RESOLUTION, "step": 8, }), + "padding": ("INT", {"default": 0, "min": 0, "max": 4096, "step": 1, }), + "preserve_size": ("BOOLEAN", {"default": False}), + "bg_color": ("STRING", {"default": "0, 0, 0", "tooltip": "Color as RGB values in range 0-255 or 0.0-1.0, or color name or hex code"}), + } + } + + RETURN_TYPES = ("IMAGE", "MASK", ) + RETURN_NAMES = ("images", "masks",) + FUNCTION = "crop" + CATEGORY = "KJNodes/image" + DESCRIPTION = "Crops the input images based on the provided masks." + + def crop(self, image, masks, width, height, bg_color, padding, preserve_size): + B, H, W, C = image.shape + BM, HM, WM = masks.shape + mask_count = BM + if HM != H or WM != W: + masks = F.interpolate(masks.unsqueeze(1), size=(H, W), mode='nearest-exact').squeeze(1) + output_images = [] + output_masks = [] + + # Parse background color using helper function + color_list = string_to_color(bg_color) + bg_color = [x / 255.0 for x in color_list] + + # For each mask + for i in range(mask_count): + curr_mask = masks[i] + + # Find bounds + y_indices, x_indices = torch.nonzero(curr_mask, as_tuple=True) + if len(y_indices) == 0 or len(x_indices) == 0: + continue + + # Get exact bounds with padding + min_y = max(0, y_indices.min().item() - padding) + max_y = min(H, y_indices.max().item() + 1 + padding) + min_x = max(0, x_indices.min().item() - padding) + max_x = min(W, x_indices.max().item() + 1 + padding) + + # Ensure mask has correct shape for multiplication + curr_mask = curr_mask.unsqueeze(-1).expand(-1, -1, C) + + # Crop image and mask together + cropped_img = image[0, min_y:max_y, min_x:max_x, :] + cropped_mask = curr_mask[min_y:max_y, min_x:max_x, :] + + crop_h, crop_w = cropped_img.shape[0:2] + new_w = crop_w + new_h = crop_h + + if not preserve_size or crop_w > width or crop_h > height: + scale = min(width/crop_w, height/crop_h) + new_w = int(crop_w * scale) + new_h = int(crop_h * scale) + + # Resize RGB + resized_img = common_upscale(cropped_img.permute(2,0,1).unsqueeze(0), new_w, new_h, "lanczos", "disabled").squeeze(0).permute(1,2,0) + resized_mask = torch.nn.functional.interpolate( + cropped_mask.permute(2,0,1).unsqueeze(0), + size=(new_h, new_w), + mode='nearest' + ).squeeze(0).permute(1,2,0) + else: + resized_img = cropped_img + resized_mask = cropped_mask + + # Create empty tensors + new_img = torch.zeros((height, width, 3), dtype=image.dtype) + new_mask = torch.zeros((height, width), dtype=image.dtype) + + # Pad both + pad_x = (width - new_w) // 2 + pad_y = (height - new_h) // 2 + new_img[pad_y:pad_y+new_h, pad_x:pad_x+new_w, :] = resized_img + if len(resized_mask.shape) == 3: + resized_mask = resized_mask[:,:,0] # Take first channel if 3D + new_mask[pad_y:pad_y+new_h, pad_x:pad_x+new_w] = resized_mask + + output_images.append(new_img) + output_masks.append(new_mask) + + if not output_images: + return (torch.zeros((0, height, width, 3), dtype=image.dtype),) + + out_rgb = torch.stack(output_images, dim=0) + out_masks = torch.stack(output_masks, dim=0) + + # Apply mask to RGB + mask_expanded = out_masks.unsqueeze(-1).expand(-1, -1, -1, 3) + background_color = torch.tensor(bg_color, dtype=torch.float32, device=image.device) + out_rgb = out_rgb * mask_expanded + background_color * (1 - mask_expanded) + + return (out_rgb, out_masks) + +class ImagePadKJ: + @classmethod + def INPUT_TYPES(s): + return {"required": { + "image": ("IMAGE", ), + "left": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 1, }), + "right": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 1, }), + "top": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 1, }), + "bottom": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 1, }), + "extra_padding": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 1, }), + "pad_mode": (["edge", "edge_pixel", "color", "pillarbox_blur"],), + "color": ("STRING", {"default": "0, 0, 0", "tooltip": "Color as RGB values in range 0-255 or 0.0-1.0, or color name or hex code"}), + }, + "optional": { + "mask": ("MASK", ), + "target_width": ("INT", {"default": 512, "min": 0, "max": MAX_RESOLUTION, "step": 1, "forceInput": True}), + "target_height": ("INT", {"default": 512, "min": 0, "max": MAX_RESOLUTION, "step": 1, "forceInput": True}), + } + } + + RETURN_TYPES = ("IMAGE", "MASK", ) + RETURN_NAMES = ("images", "masks",) + FUNCTION = "pad" + CATEGORY = "KJNodes/image" + DESCRIPTION = "Pad the input image and optionally mask with the specified padding." + + def pad(self, image, left, right, top, bottom, extra_padding, color, pad_mode, mask=None, target_width=None, target_height=None): + B, H, W, C = image.shape + # Resize masks to image dimensions if necessary + if mask is not None: + BM, HM, WM = mask.shape + if HM != H or WM != W: + mask = F.interpolate(mask.unsqueeze(1), size=(H, W), mode='nearest-exact').squeeze(1) + + # Parse background color using helper function + color_list = string_to_color(color) + bg_color = [x / 255.0 for x in color_list] + if len(bg_color) == 1: + bg_color = bg_color * 3 # Grayscale to RGB + bg_color = torch.tensor(bg_color, dtype=image.dtype, device=image.device) + + # Calculate padding sizes with extra padding + if target_width is not None and target_height is not None: + if extra_padding > 0: + image = common_upscale(image.movedim(-1, 1), W - extra_padding, H - extra_padding, "lanczos", "disabled").movedim(1, -1) + B, H, W, C = image.shape + + padded_width = target_width + padded_height = target_height + pad_left = (padded_width - W) // 2 + pad_right = padded_width - W - pad_left + pad_top = (padded_height - H) // 2 + pad_bottom = padded_height - H - pad_top + else: + pad_left = left + extra_padding + pad_right = right + extra_padding + pad_top = top + extra_padding + pad_bottom = bottom + extra_padding + + padded_width = W + pad_left + pad_right + padded_height = H + pad_top + pad_bottom + + # Pillarbox blur mode + if pad_mode == "pillarbox_blur": + def _gaussian_blur_nchw(img_nchw, sigma_px): + if sigma_px <= 0: + return img_nchw + radius = max(1, int(3.0 * float(sigma_px))) + k = 2 * radius + 1 + x = torch.arange(-radius, radius + 1, device=img_nchw.device, dtype=img_nchw.dtype) + k1 = torch.exp(-(x * x) / (2.0 * float(sigma_px) * float(sigma_px))) + k1 = k1 / k1.sum() + kx = k1.view(1, 1, 1, k) + ky = k1.view(1, 1, k, 1) + c = img_nchw.shape[1] + kx = kx.repeat(c, 1, 1, 1) + ky = ky.repeat(c, 1, 1, 1) + img_nchw = F.conv2d(img_nchw, kx, padding=(0, radius), groups=c) + img_nchw = F.conv2d(img_nchw, ky, padding=(radius, 0), groups=c) + return img_nchw + + out_image = torch.zeros((B, padded_height, padded_width, C), dtype=image.dtype, device=image.device) + for b in range(B): + scale_fill = max(padded_width / float(W), padded_height / float(H)) if (W > 0 and H > 0) else 1.0 + bg_w = max(1, int(round(W * scale_fill))) + bg_h = max(1, int(round(H * scale_fill))) + src_b = image[b].movedim(-1, 0).unsqueeze(0) + bg = common_upscale(src_b, bg_w, bg_h, "bilinear", crop="disabled") + y0 = max(0, (bg_h - padded_height) // 2) + x0 = max(0, (bg_w - padded_width) // 2) + y1 = min(bg_h, y0 + padded_height) + x1 = min(bg_w, x0 + padded_width) + bg = bg[:, :, y0:y1, x0:x1] + if bg.shape[2] != padded_height or bg.shape[3] != padded_width: + pad_h = padded_height - bg.shape[2] + pad_w = padded_width - bg.shape[3] + pad_top_fix = max(0, pad_h // 2) + pad_bottom_fix = max(0, pad_h - pad_top_fix) + pad_left_fix = max(0, pad_w // 2) + pad_right_fix = max(0, pad_w - pad_left_fix) + bg = F.pad(bg, (pad_left_fix, pad_right_fix, pad_top_fix, pad_bottom_fix), mode="replicate") + sigma = max(1.0, 0.006 * float(min(padded_height, padded_width))) + bg = _gaussian_blur_nchw(bg, sigma_px=sigma) + if C >= 3: + r, g, bch = bg[:, 0:1], bg[:, 1:2], bg[:, 2:3] + luma = 0.2126 * r + 0.7152 * g + 0.0722 * bch + gray = torch.cat([luma, luma, luma], dim=1) + desat = 0.20 + rgb = torch.cat([r, g, bch], dim=1) + rgb = rgb * (1.0 - desat) + gray * desat + bg[:, 0:3, :, :] = rgb + dim = 0.35 + bg = torch.clamp(bg * dim, 0.0, 1.0) + out_image[b] = bg.squeeze(0).movedim(0, -1) + out_image[:, pad_top:pad_top+H, pad_left:pad_left+W, :] = image + # Mask handling for pillarbox_blur + if mask is not None: + fg_mask = mask + out_masks = torch.ones((B, padded_height, padded_width), dtype=image.dtype, device=image.device) + out_masks[:, pad_top:pad_top+H, pad_left:pad_left+W] = fg_mask + else: + out_masks = torch.ones((B, padded_height, padded_width), dtype=image.dtype, device=image.device) + out_masks[:, pad_top:pad_top+H, pad_left:pad_left+W] = 0.0 + return (out_image, out_masks) + + # Standard pad logic (edge/color) + out_image = torch.zeros((B, padded_height, padded_width, C), dtype=image.dtype, device=image.device) + for b in range(B): + if pad_mode == "edge": + # Pad with edge color (mean) + top_edge = image[b, 0, :, :] + bottom_edge = image[b, H-1, :, :] + left_edge = image[b, :, 0, :] + right_edge = image[b, :, W-1, :] + out_image[b, :pad_top, :, :] = top_edge.mean(dim=0) + out_image[b, pad_top+H:, :, :] = bottom_edge.mean(dim=0) + out_image[b, :, :pad_left, :] = left_edge.mean(dim=0) + out_image[b, :, pad_left+W:, :] = right_edge.mean(dim=0) + out_image[b, pad_top:pad_top+H, pad_left:pad_left+W, :] = image[b] + elif pad_mode == "edge_pixel": + # Pad with exact edge pixel values + for y in range(pad_top): + out_image[b, y, pad_left:pad_left+W, :] = image[b, 0, :, :] + for y in range(pad_top+H, padded_height): + out_image[b, y, pad_left:pad_left+W, :] = image[b, H-1, :, :] + for x in range(pad_left): + out_image[b, pad_top:pad_top+H, x, :] = image[b, :, 0, :] + for x in range(pad_left+W, padded_width): + out_image[b, pad_top:pad_top+H, x, :] = image[b, :, W-1, :] + out_image[b, :pad_top, :pad_left, :] = image[b, 0, 0, :] + out_image[b, :pad_top, pad_left+W:, :] = image[b, 0, W-1, :] + out_image[b, pad_top+H:, :pad_left, :] = image[b, H-1, 0, :] + out_image[b, pad_top+H:, pad_left+W:, :] = image[b, H-1, W-1, :] + out_image[b, pad_top:pad_top+H, pad_left:pad_left+W, :] = image[b] + else: + # Pad with specified background color + out_image[b, :, :, :] = bg_color.unsqueeze(0).unsqueeze(0) + out_image[b, pad_top:pad_top+H, pad_left:pad_left+W, :] = image[b] + + if mask is not None: + out_masks = torch.nn.functional.pad( + mask, + (pad_left, pad_right, pad_top, pad_bottom), + mode='replicate' + ) + else: + out_masks = torch.ones((B, padded_height, padded_width), dtype=image.dtype, device=image.device) + for m in range(B): + out_masks[m, pad_top:pad_top+H, pad_left:pad_left+W] = 0.0 + + return (out_image, out_masks) + +# extends https://github.com/Kosinkadink/ComfyUI-VideoHelperSuite +class LoadVideosFromFolder: + @classmethod + def __init__(cls): + try: + cls.vhs_nodes = importlib.import_module("ComfyUI-VideoHelperSuite.videohelpersuite") + except ImportError: + try: + cls.vhs_nodes = importlib.import_module("comfyui-videohelpersuite.videohelpersuite") + except ImportError: + # Fallback to sys.modules search for Windows compatibility + import sys + vhs_module = None + for module_name in sys.modules: + if 'videohelpersuite' in module_name and 'videohelpersuite' in sys.modules[module_name].__dict__: + vhs_module = sys.modules[module_name] + break + + if vhs_module is None: + # Try direct access to the videohelpersuite submodule + for module_name in sys.modules: + if module_name.endswith('videohelpersuite'): + vhs_module = sys.modules[module_name] + break + + if vhs_module is not None: + cls.vhs_nodes = vhs_module + else: + raise ImportError("This node requires ComfyUI-VideoHelperSuite to be installed.") + + except ImportError: + raise ImportError("This node requires ComfyUI-VideoHelperSuite to be installed.") + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "video": ("STRING", {"default": "X://insert/path/"},), + "force_rate": ("FLOAT", {"default": 0, "min": 0, "max": 60, "step": 1, "disable": 0}), + "custom_width": ("INT", {"default": 0, "min": 0, "max": 4096, 'disable': 0}), + "custom_height": ("INT", {"default": 0, "min": 0, "max": 4096, 'disable': 0}), + "frame_load_cap": ("INT", {"default": 0, "min": 0, "max": 10000, "step": 1, "disable": 0}), + "skip_first_frames": ("INT", {"default": 0, "min": 0, "max": 10000, "step": 1}), + "select_every_nth": ("INT", {"default": 1, "min": 1, "max": 1000, "step": 1}), + "output_type": (["batch", "grid"], {"default": "batch"}), + "grid_max_columns": ("INT", {"default": 4, "min": 1, "max": 16, "step": 1, "disable": 1}), + "add_label": ( "BOOLEAN", {"default": False} ), + }, + "hidden": { + "force_size": "STRING", + "unique_id": "UNIQUE_ID" + }, + } + + CATEGORY = "KJNodes/misc" + + RETURN_TYPES = ("IMAGE", ) + RETURN_NAMES = ("IMAGE", ) + + FUNCTION = "load_video" + + def load_video(self, output_type, grid_max_columns, add_label=False, **kwargs): + if kwargs.get('video') and not os.path.isabs(kwargs['video']) and args.base_directory: + kwargs['video'] = os.path.join(args.base_directory, kwargs['video']) + + if self.vhs_nodes is None: + raise ImportError("This node requires ComfyUI-VideoHelperSuite to be installed.") + videos_list = [] + filenames = [] + for f in sorted(os.listdir(kwargs['video'])): + if os.path.isfile(os.path.join(kwargs['video'], f)): + file_parts = f.split('.') + if len(file_parts) > 1 and (file_parts[-1].lower() in ['webm', 'mp4', 'mkv', 'gif', 'mov']): + videos_list.append(os.path.join(kwargs['video'], f)) + filenames.append(f) + + kwargs.pop('video') + loaded_videos = [] + for idx, video in enumerate(videos_list): + video_tensor = self.vhs_nodes.load_video_nodes.load_video(video=video, **kwargs)[0] + if add_label: + # Add filename label above video (without extension) + if video_tensor.dim() == 4: + _, h, w, c = video_tensor.shape + else: + h, w, c = video_tensor.shape + # Remove extension from filename + label_text = filenames[idx].rsplit('.', 1)[0] + font_size = max(16, w // 20) + try: + font = ImageFont.truetype("arial.ttf", font_size) + except OSError: + font = ImageFont.load_default() + dummy_img = Image.new("RGB", (w, 10), (0,0,0)) + draw = ImageDraw.Draw(dummy_img) + text_bbox = draw.textbbox((0,0), label_text, font=font) + extra_padding = max(12, font_size // 2) # More padding under the font + label_height = text_bbox[3] - text_bbox[1] + extra_padding + label_img = Image.new("RGB", (w, label_height), (0,0,0)) + draw = ImageDraw.Draw(label_img) + draw.text((w//2 - (text_bbox[2]-text_bbox[0])//2, 4), label_text, font=font, fill=(255,255,255)) + label_np = np.asarray(label_img).astype(np.float32) / 255.0 + label_tensor = torch.from_numpy(label_np) + if c == 1: + label_tensor = label_tensor.mean(dim=2, keepdim=True) + elif c == 4: + alpha = torch.ones((label_height, w, 1), dtype=label_tensor.dtype) + label_tensor = torch.cat([label_tensor, alpha], dim=2) + if video_tensor.dim() == 4: + label_tensor = label_tensor.unsqueeze(0).expand(video_tensor.shape[0], -1, -1, -1) + video_tensor = torch.cat([label_tensor, video_tensor], dim=1) + else: + video_tensor = torch.cat([label_tensor, video_tensor], dim=0) + loaded_videos.append(video_tensor) + if output_type == "batch": + out_tensor = torch.cat(loaded_videos) + elif output_type == "grid": + rows = (len(loaded_videos) + grid_max_columns - 1) // grid_max_columns + # Pad the last row if needed + total_slots = rows * grid_max_columns + while len(loaded_videos) < total_slots: + loaded_videos.append(torch.zeros_like(loaded_videos[0])) + # Create grid by rows + row_tensors = [] + for row_idx in range(rows): + start_idx = row_idx * grid_max_columns + end_idx = start_idx + grid_max_columns + row_videos = loaded_videos[start_idx:end_idx] + # Pad all videos in this row to the same height + heights = [v.shape[1] for v in row_videos] + max_height = max(heights) + padded_row_videos = [] + for v in row_videos: + pad_height = max_height - v.shape[1] + if pad_height > 0: + # Pad (frames, H, W, C) or (H, W, C) + if v.dim() == 4: + pad = (0,0, 0,0, 0,pad_height, 0,0) # (C,W,H,F) + v = torch.nn.functional.pad(v, (0,0,0,0,0,pad_height,0,0)) + else: + v = torch.nn.functional.pad(v, (0,0,0,0,pad_height,0)) + padded_row_videos.append(v) + row_tensor = torch.cat(padded_row_videos, dim=2) # Concatenate horizontally + row_tensors.append(row_tensor) + out_tensor = torch.cat(row_tensors, dim=1) # Concatenate rows vertically + return out_tensor, + + @classmethod + def IS_CHANGED(s, video, **kwargs): + if s.vhs_nodes is not None: + return s.vhs_nodes.utils.hash_path(video) + return None + + +class EncodeVideoComponents(io.ComfyNode): + @classmethod + def define_schema(cls): + position_options = ["center", "top", "bottom", "left", "right"] + options = [ + io.DynamicCombo.Option(key="stretch", inputs=[]), + io.DynamicCombo.Option(key="resize", inputs=[]), + io.DynamicCombo.Option(key="total_pixels", inputs=[]), + io.DynamicCombo.Option(key="crop", inputs=[ + io.Combo.Input("crop_position", options=position_options, tooltip="Position to crop from."), + ]), + io.DynamicCombo.Option(key="pad", inputs=[ + io.String.Input("pad_color", default="0, 0, 0", tooltip="Color to use for padding."), + io.Combo.Input("pad_position", options=position_options, tooltip="Position to align the image within the padded area."), + ]), + io.DynamicCombo.Option(key="pad_edge", inputs=[ + io.Combo.Input("pad_position", options=position_options, tooltip="Position to align the image within the padded area."), + ]), + io.DynamicCombo.Option(key="pad_edge_pixel", inputs=[ + io.Combo.Input("pad_position", options=position_options, tooltip="Position to align the image within the padded area."), + ]), + io.DynamicCombo.Option(key="pillarbox_blur", inputs=[ + io.Combo.Input("pad_position", options=position_options, tooltip="Position to align the image within the padded area."), + ]), + ] + return io.Schema( + node_id="EncodeVideoComponents", + search_aliases=["video to latent", "encode video", "vae encode video"], + display_name="Encode Video Components", + category="KJNodes/image", + description="Extracts video frames, resizes them, and encodes with a VAE directly, avoiding storing the full image tensor.", + inputs=[ + io.Video.Input("video", tooltip="The video to extract and encode."), + io.Vae.Input("vae", tooltip="The VAE model to use for encoding."), + io.Int.Input("width", default=768, min=0, max=16384, step=2, tooltip="Target width for the frames before encoding. 0 = original width."), + io.Int.Input("height", default=512, min=0, max=16384, step=2, tooltip="Target height for the frames before encoding. 0 = original height."), + io.Int.Input("max_frames", default=0, min=0, max=999999, step=1, tooltip="Maximum number of frames. 0 = no limit."), + io.Combo.Input("upscale_method", options=["nearest-exact", "bilinear", "area", "bicubic", "lanczos"], default="lanczos", tooltip="Interpolation method for resizing."), + io.DynamicCombo.Input( + "keep_proportion", + options=options, + display_name="Keep Proportion", + tooltip="How to handle aspect ratio mismatch when resizing.", + ), + ], + outputs=[ + io.Latent.Output(display_name="latent"), + io.Audio.Output(display_name="audio"), + io.Float.Output(display_name="fps"), + io.Int.Output(display_name="frame_count", tooltip="Number pixel space frames after any possible cropping"), + ], + ) + + @staticmethod + def _compute_resize_params(mode, position, width, height, src_w, src_h): + """Compute target resize dimensions, crop region, and padding from keep_proportion mode.""" + if width == 0: + width = src_w + if height == 0: + height = src_h + pillarbox_blur = mode == "pillarbox_blur" + pad_left = pad_right = pad_top = pad_bottom = 0 + crop_region = None # (x, y, crop_w, crop_h) or None + + if mode in ["resize", "total_pixels"] or mode.startswith("pad") or pillarbox_blur: + if mode == "total_pixels": + total_pixels = width * height + aspect_ratio = src_w / src_h + new_height = int(math.sqrt(total_pixels / aspect_ratio)) + new_width = int(math.sqrt(total_pixels * aspect_ratio)) + else: + ratio = min(width / src_w, height / src_h) + new_width = round(src_w * ratio) + new_height = round(src_h * ratio) + + if mode.startswith("pad") or pillarbox_blur: + if position == "center": + pad_left = (width - new_width) // 2 + pad_right = width - new_width - pad_left + pad_top = (height - new_height) // 2 + pad_bottom = height - new_height - pad_top + elif position == "top": + pad_left = (width - new_width) // 2 + pad_right = width - new_width - pad_left + pad_top = 0 + pad_bottom = height - new_height + elif position == "bottom": + pad_left = (width - new_width) // 2 + pad_right = width - new_width - pad_left + pad_top = height - new_height + pad_bottom = 0 + elif position == "left": + pad_left = 0 + pad_right = width - new_width + pad_top = (height - new_height) // 2 + pad_bottom = height - new_height - pad_top + elif position == "right": + pad_left = width - new_width + pad_right = 0 + pad_top = (height - new_height) // 2 + pad_bottom = height - new_height - pad_top + + width = new_width + height = new_height + + if mode == "crop": + old_aspect = src_w / src_h + new_aspect = width / height + if old_aspect > new_aspect: + crop_w = round(src_h * new_aspect) + crop_h = src_h + else: + crop_w = src_w + crop_h = round(src_w / new_aspect) + if position == "center": + x = (src_w - crop_w) // 2 + y = (src_h - crop_h) // 2 + elif position == "top": + x = (src_w - crop_w) // 2 + y = 0 + elif position == "bottom": + x = (src_w - crop_w) // 2 + y = src_h - crop_h + elif position == "left": + x = 0 + y = (src_h - crop_h) // 2 + elif position == "right": + x = src_w - crop_w + y = (src_h - crop_h) // 2 + crop_region = (x, y, crop_w, crop_h) + + return width, height, crop_region, (pad_left, pad_right, pad_top, pad_bottom) + + @classmethod + def execute(cls, video, vae, width, height, max_frames, upscale_method, keep_proportion) -> io.NodeOutput: + import av + import itertools + + mode = keep_proportion["keep_proportion"] + position = keep_proportion.get("crop_position") or keep_proportion.get("pad_position", "center") + pad_color = keep_proportion.get("pad_color", "0, 0, 0") + target_dtype = vae.vae_dtype + + # Access VideoFromFile internals for efficient per-frame decode + source = video.get_stream_source() + start_time = getattr(video, '_VideoFromFile__start_time', 0) + duration = getattr(video, '_VideoFromFile__duration', 0) + + # Get frame count for progress bar, capped by max_frames + try: + total_frames = video.get_frame_count() + except (ValueError, AttributeError): + total_frames = 0 + if max_frames > 0 and total_frames > 0: + total_frames = min(total_frames, max_frames) + pbar = ProgressBar(total_frames) if total_frames > 0 else None + + # Lanczos requires PIL (CPU-only), all other methods use torch on GPU + use_gpu = upscale_method != "lanczos" + device = model_management.get_torch_device() if use_gpu else torch.device("cpu") + + # --- Decode video frames with per-frame resize + dtype cast --- + with av.open(source, mode='r') as container: + video_stream = container.streams.video[0] + start_pts = int(start_time / video_stream.time_base) + end_pts = int((start_time + duration) / video_stream.time_base) if duration else 0 + container.seek(start_pts, stream=video_stream) + + res_w, res_h, crop_region, padding = None, None, None, (0, 0, 0, 0) + frames = [] + for frame in container.decode(video_stream): + if frame.pts < start_pts: + continue + if duration and frame.pts >= end_pts: + break + if max_frames > 0 and len(frames) >= max_frames: + break + + if res_w is None: + src_h, src_w = frame.height, frame.width + res_w, res_h, crop_region, padding = cls._compute_resize_params( + mode, position, width, height, src_w, src_h + ) + + # Decode to tensor and normalize + img = torch.from_numpy(frame.to_ndarray(format='rgb24')).to(device=device, dtype=torch.float32) / 255.0 + + # Crop if needed (before resize) + if crop_region is not None: + cx, cy, cw, ch = crop_region + img = img[cy:cy+ch, cx:cx+cw, :] + + # Resize (GPU for torch-native methods, CPU/PIL for lanczos) + img = common_upscale( + img.unsqueeze(0).movedim(-1, 1), res_w, res_h, upscale_method, crop="disabled" + ).movedim(1, -1).squeeze(0).to(dtype=target_dtype, device="cpu") + + frames.append(img) + if pbar is not None: + pbar.update(1) + + frame_rate = video_stream.average_rate if video_stream.average_rate else 1 + + s = torch.stack(frames) if frames else torch.zeros(0, height, width, 3, dtype=target_dtype) + + # Pad logic (applied on the full stack since padding modes like pillarbox_blur need all frames) + pillarbox_blur = mode == "pillarbox_blur" + pad_left, pad_right, pad_top, pad_bottom = padding + if (mode.startswith("pad") or pillarbox_blur) and (pad_left > 0 or pad_right > 0 or pad_top > 0 or pad_bottom > 0): + pad_mode = ( + "pillarbox_blur" if pillarbox_blur else + "edge" if mode == "pad_edge" else + "edge_pixel" if mode == "pad_edge_pixel" else + "color" + ) + s, _ = ImagePadKJ.pad(None, s, pad_left, pad_right, pad_top, pad_bottom, 0, pad_color, pad_mode) + + # Trim frames to a count valid for the VAE's temporal compression + try: + temporal_compress = vae.downscale_ratio[0] + temporal_decompress = vae.upscale_ratio[0] + valid_frames = temporal_decompress(temporal_compress(s.shape[0])) + if valid_frames < s.shape[0]: + logging.warning(f"[EncodeVideoComponents] Trimming {s.shape[0] - valid_frames} frames ({s.shape[0]} -> {valid_frames}) to match VAE temporal compression ratio") + s = s[:valid_frames] + except (TypeError, IndexError): + pass + + t = vae.encode(s) + + # --- Extract audio in a separate pass --- + audio = None + if isinstance(source, BytesIO): + source.seek(0) + with av.open(source, mode='r') as container: + if len(container.streams.audio): + audio_stream = container.streams.audio[-1] + if start_time > 0: + audio_start_pts = int(start_time / audio_stream.time_base) + container.seek(audio_start_pts, stream=audio_stream) + audio_frames = [] + resample = av.audio.resampler.AudioResampler(format='fltp').resample + aframes = itertools.chain.from_iterable( + map(resample, container.decode(audio_stream)) + ) + has_first_frame = False + for aframe in aframes: + offset_seconds = start_time - aframe.time + to_skip = int(offset_seconds * audio_stream.sample_rate) + if to_skip < aframe.samples: + has_first_frame = True + break + if has_first_frame: + audio_frames.append(aframe.to_ndarray()[..., to_skip:]) + for aframe in aframes: + if duration and aframe.time > start_time + duration: + break + audio_frames.append(aframe.to_ndarray()) + if audio_frames: + audio_data = np.concatenate(audio_frames, axis=1) + if duration: + audio_data = audio_data[..., :int(duration * audio_stream.sample_rate)] + audio = { + "waveform": torch.from_numpy(audio_data).unsqueeze(0), + "sample_rate": int(audio_stream.sample_rate) if audio_stream.sample_rate else 1, + } + + return io.NodeOutput({"samples": t}, audio, float(frame_rate), s.shape[0]) + + +class DecodeAndSaveVideo(io.ComfyNode): + @classmethod + def define_schema(cls): + return io.Schema( + node_id="DecodeAndSaveVideo", + search_aliases=["video to latent", "decode video"], + display_name="Decode and Save Video", + category="KJNodes/image", + description="Decodes video frames and audio from latent representations, combines them, and saves as a video file, without keeping intermediate images in memory.", + inputs=[ + io.Latent.Input("video_latent", tooltip="The latent representation of the video frames."), + io.Latent.Input("audio_latent", optional=True, tooltip="The latent representation of the audio frames."), + io.Float.Input("fps", default=25.0, min=0.0, max=999.0, step=0.01, tooltip="Frame rate for the output video."), + io.String.Input("filename_prefix", default="video/ComfyUI", tooltip="The prefix for the file to save. This may include formatting information such as %date:yyyy-MM-dd% or %Empty Latent Image.width% to include values from nodes."), + io.Combo.Input("format", options=Types.VideoContainer.as_input(), default="auto", tooltip="The format to save the video as."), + io.Combo.Input("codec", options=Types.VideoCodec.as_input(), default="auto", tooltip="The codec to use for the video."), + io.Vae.Input("video_vae", tooltip="The VAE model to use for encoding."), + io.Vae.Input("audio_vae", optional=True, tooltip="The VAE model to use for decoding audio."), + io.DynamicCombo.Input("tiling", options=[ + io.DynamicCombo.Option(key="disabled", inputs=[]), + io.DynamicCombo.Option(key="enabled", inputs=[ + io.Int.Input("tile_size", default=512, min=64, max=4096, step=32, tooltip="Size of the tiles to decode. Smaller tiles use less memory but take more time."), + io.Int.Input("overlap", default=64, min=0, max=4096, step=32, tooltip="Amount of overlap between tiles. Higher overlap can improve quality at the edges of tiles but uses more memory and takes more time."), + io.Int.Input("temporal_size", default=4096, min=8, max=4096, step=4, tooltip="Only used for video VAEs: Amount of frames to decode at a time. Higher value than number of frames = disabled"), + io.Int.Input("temporal_overlap", default=16, min=4, max=4096, step=4, tooltip="Only used for video VAEs: Amount of frames to overlap. Higher overlap can improve quality at the edges of temporal tiles but uses more memory and takes more time."), + ]), + ]), + ], + hidden=[io.Hidden.prompt, io.Hidden.extra_pnginfo], + is_output_node=True, + ) + + @classmethod + def execute(cls, video_latent, video_vae, filename_prefix, format, codec, tiling, audio_latent=None, audio_vae=None, fps=25.0) -> io.NodeOutput: + if tiling["tiling"] == "enabled": + tile_size = tiling["tile_size"] + overlap = tiling["overlap"] + temporal_size = tiling["temporal_size"] + temporal_overlap = tiling["temporal_overlap"] + + if tile_size < overlap * 4: + overlap = tile_size // 4 + if temporal_size < temporal_overlap * 2: + temporal_overlap = temporal_overlap // 2 + temporal_compression = video_vae.temporal_compression_decode() + if temporal_compression is not None: + temporal_size = max(2, temporal_size // temporal_compression) + temporal_overlap = max(1, min(temporal_size // 2, temporal_overlap // temporal_compression)) + else: + temporal_size = None + temporal_overlap = None + + compression = video_vae.spacial_compression_decode() + + images = cls.decode_tiled(video_vae, video_latent["samples"], + tile_t=max(2, temporal_size), + tile_x=tile_size // compression, + tile_y=tile_size // compression, + overlap=(temporal_overlap if temporal_overlap is not None else 1, max(1, overlap // compression), max(1, overlap // compression)), + ).movedim(1, -1) + if len(images.shape) == 5: #Combine batches + images = images.reshape(-1, images.shape[-3], images.shape[-2], images.shape[-1]) + else: + images = cls.decode_video(video_vae, video_latent) + + if audio_latent is not None: + if audio_vae is None: + raise ValueError("Audio VAE must be provided if audio latent is provided.") + audio = cls.decode_audio(audio_latent, audio_vae) + else: + audio = None + + video = InputImpl.VideoFromComponents(Types.VideoComponents(images=images, audio=audio, frame_rate=Fraction(fps))) + file, subfolder = cls.save_video(video, filename_prefix, format, codec) + + return io.NodeOutput(ui=ui.PreviewVideo([ui.SavedResult(file, subfolder, io.FolderType.output)])) + + @classmethod + def decode_video(cls, vae, samples): + samples_in = samples["samples"] + if samples_in.is_nested: + samples_in = samples_in.unbind()[0] + + vae.throw_exception_if_invalid() + pixel_samples = None + do_tile = False + if vae.latent_dim == 2 and samples_in.ndim == 5: + samples_in = samples_in[:, :, 0] + try: + memory_used = vae.memory_used_decode(samples_in.shape, vae.vae_dtype) + model_management.load_models_gpu([vae.patcher], memory_required=memory_used, force_full_load=True) + free_memory = vae.patcher.get_free_memory(vae.device) + batch_number = int(free_memory / memory_used) + batch_number = max(1, batch_number) + + for x in range(0, samples_in.shape[0], batch_number): + samples = samples_in[x:x+batch_number].to(vae.vae_dtype).to(vae.device) + out = vae.process_output(vae.first_stage_model.decode(samples).to(vae.output_device).to(torch.float16)) + if pixel_samples is None: + pixel_samples = torch.empty((samples_in.shape[0],) + tuple(out.shape[1:]), device=vae.output_device, dtype=out.dtype) + pixel_samples[x:x+batch_number] = out + except Exception as e: + model_management.raise_non_oom(e) + logging.warning("Warning: Ran out of memory when regular VAE decoding, retrying with tiled VAE decoding.") + do_tile = True + + if do_tile: + dims = samples_in.ndim - 2 + if dims == 1 or cls.extra_1d_channel is not None: + pixel_samples = vae.decode_tiled_1d(samples_in) + elif dims == 2: + pixel_samples = vae.decode_tiled_2d(samples_in) + elif dims == 3: + tile = 256 // vae.spacial_compression_decode() + overlap = tile // 4 + pixel_samples = vae.decode_tiled_3d(samples_in, tile_x=tile, tile_y=tile, overlap=(1, overlap, overlap)) + + pixel_samples = pixel_samples.to(vae.output_device).movedim(1,-1) + + if len(pixel_samples.shape) == 5: #Combine batches + pixel_samples = pixel_samples.reshape(-1, pixel_samples.shape[-3], pixel_samples.shape[-2], pixel_samples.shape[-1]) + return pixel_samples + + @classmethod + def decode_tiled(cls, vae, samples, tile_t=999, tile_x=32, tile_y=32, overlap=(1, 8, 8)): + vae.throw_exception_if_invalid() + memory_used = vae.memory_used_decode(samples.shape, vae.vae_dtype) + model_management.load_models_gpu([vae.patcher], memory_required=memory_used, force_full_load=vae.disable_offload) + decode_fn = lambda a: vae.first_stage_model.decode(a.to(vae.vae_dtype).to(vae.device)).to(torch.float16) + return vae.process_output(tiled_scale_multidim(samples, decode_fn, tile=(tile_t, tile_x, tile_y), overlap=overlap, + upscale_amount=vae.upscale_ratio, out_channels=vae.output_channels, index_formulas=vae.upscale_index_formula, output_device=vae.output_device)) + + + @classmethod + def decode_audio(cls, samples, audio_vae): + audio_latent = samples["samples"] + if audio_latent.is_nested: + audio_latent = audio_latent.unbind()[-1] + audio = audio_vae.decode(audio_latent) + # Post-PR #13486: audio_vae is a comfy.sd.VAE wrapper returning channels-last (BTC). + # Pre-PR: audio_vae is a raw AudioVAE returning channels-first (BCT). + if hasattr(audio_vae, "first_stage_model"): + audio = audio.movedim(-1, 1) + audio = audio.to(audio_latent.device) + output_audio_sample_rate = getattr( + audio_vae, + "audio_sample_rate_output", + getattr(audio_vae, "output_sample_rate", None), + ) + if output_audio_sample_rate is None: + output_audio_sample_rate = getattr( + getattr(audio_vae, "first_stage_model", None), "output_sample_rate", 44100 + ) + return {"waveform": audio, "sample_rate": int(output_audio_sample_rate)} + + @classmethod + def save_video(cls, video, filename_prefix, format, codec) -> io.NodeOutput: + width, height = video.get_dimensions() + full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path( + filename_prefix, + folder_paths.get_output_directory(), + width, + height + ) + saved_metadata = None + if not args.disable_metadata: + metadata = {} + if cls.hidden.extra_pnginfo is not None: + metadata.update(cls.hidden.extra_pnginfo) + if cls.hidden.prompt is not None: + metadata["prompt"] = cls.hidden.prompt + if len(metadata) > 0: + saved_metadata = metadata + file = f"{filename}_{counter:05}_.{Types.VideoContainer.get_extension(format)}" + video.save_to( + os.path.join(full_output_folder, file), + format=Types.VideoContainer(format), + codec=codec, + metadata=saved_metadata + ) + return file, subfolder + + +class PreviewImageOrMask(io.ComfyNode): + @classmethod + def define_schema(cls): + return io.Schema( + node_id="PreviewImageOrMask", + display_name="Preview Image Or Mask", + category="KJNodes/misc", + description="Previews the input images or masks.", + search_aliases=["output"], + inputs=[ + io.MultiType.Input("input", [io.Image, io.Mask], tooltip="The image or mask to preview."), + ], + hidden=[io.Hidden.prompt, io.Hidden.extra_pnginfo], + is_output_node=True, + ) + + @classmethod + def execute(cls, input) -> io.NodeOutput: + if input.ndim == 3: + return io.NodeOutput(ui=ui.PreviewMask(input, cls=cls)) + return io.NodeOutput(ui=ui.PreviewImage(input, cls=cls)) + diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/image_transform_node.py b/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/image_transform_node.py new file mode 100644 index 0000000000000000000000000000000000000000..4a0abdc9d183ce0540b7919be94e9edf66201ded --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/image_transform_node.py @@ -0,0 +1,542 @@ +import numpy as np +import torch +import random +import math +import os +import json + +from PIL import Image + +from comfy.utils import common_upscale +from comfy_api.latest import io +import folder_paths +from nodes import MAX_RESOLUTION + +from ..utility.utility import string_to_color, normalize_bboxes, bbox_to_bounding_box + +def _upscale_mask(mask, width, height, method, crop): + if method == "lanczos": + return common_upscale(mask.unsqueeze(1).repeat(1, 3, 1, 1), width, height, method, crop).movedim(1, -1)[:, :, :, 0] + return common_upscale(mask.unsqueeze(1), width, height, method, crop).squeeze(1) + + +def _resize_single_channel(tensor, width, height): + """Resize a 3D (B,H,W) tensor using bilinear interpolation.""" + return common_upscale(tensor.unsqueeze(1), width, height, "bilinear", "disabled").squeeze(1) + + +def _pad_inputs(): + """Shared pad_top/bottom/left/right input definitions for extra_padding options.""" + return [ + io.Int.Input("pad_top", default=0, min=0, max=MAX_RESOLUTION, step=1, tooltip="Padding pixels on top."), + io.Int.Input("pad_bottom", default=0, min=0, max=MAX_RESOLUTION, step=1, tooltip="Padding pixels on bottom."), + io.Int.Input("pad_left", default=0, min=0, max=MAX_RESOLUTION, step=1, tooltip="Padding pixels on left."), + io.Int.Input("pad_right", default=0, min=0, max=MAX_RESOLUTION, step=1, tooltip="Padding pixels on right."), + ] + + +def _apply_padding(tensor, pad_top, pad_bottom, pad_left, pad_right, mode, edge_mode="clamp", fill_rgb=None): + """Apply padding to a BHWC tensor. Returns the padded tensor. + mode: 'color' or 'edge' + edge_mode: 'clamp', 'repeat', 'mirror' (only used when mode='edge') + fill_rgb: list of [r, g, b] float values 0-1 (only used when mode='color') + """ + h, w = tensor.shape[1], tensor.shape[2] + new_h = h + pad_top + pad_bottom + new_w = w + pad_left + pad_right + + if mode == "color": + fill = fill_rgb or [0.0, 0.0, 0.0] + padded = torch.zeros(tensor.shape[0], new_h, new_w, tensor.shape[3], device=tensor.device, dtype=tensor.dtype) + for c in range(min(3, tensor.shape[3])): + padded[:, :, :, c] = fill[c] + padded[:, pad_top:pad_top+h, pad_left:pad_left+w, :] = tensor + return padded + + # mode == "edge" + if edge_mode == "clamp": + padded = torch.zeros(tensor.shape[0], new_h, new_w, tensor.shape[3], device=tensor.device, dtype=tensor.dtype) + padded[:, pad_top:pad_top+h, pad_left:pad_left+w, :] = tensor + if pad_top > 0: + padded[:, :pad_top, pad_left:pad_left+w, :] = tensor[:, 0:1, :, :].expand(-1, pad_top, -1, -1) + if pad_bottom > 0: + padded[:, pad_top+h:, pad_left:pad_left+w, :] = tensor[:, -1:, :, :].expand(-1, pad_bottom, -1, -1) + if pad_left > 0: + padded[:, :, :pad_left, :] = padded[:, :, pad_left:pad_left+1, :].expand(-1, -1, pad_left, -1) + if pad_right > 0: + padded[:, :, pad_left+w:, :] = padded[:, :, pad_left+w-1:pad_left+w, :].expand(-1, -1, pad_right, -1) + return padded + elif edge_mode == "repeat": + tiles_x = (new_w + w - 1) // w + 1 + tiles_y = (new_h + h - 1) // h + 1 + tiled = tensor.repeat(1, tiles_y, tiles_x, 1) + # Offset so original content lands at (pad_top, pad_left) in output + off_x = (w - pad_left % w) % w + off_y = (h - pad_top % h) % h + return tiled[:, off_y:off_y+new_h, off_x:off_x+new_w, :] + elif edge_mode == "mirror": + flipped_h = tensor.flip(2) + flipped_v = tensor.flip(1) + flipped_hv = tensor.flip(1).flip(2) + mirror_block = torch.cat([ + torch.cat([tensor, flipped_h], dim=2), + torch.cat([flipped_v, flipped_hv], dim=2), + ], dim=1) + mb_h, mb_w = mirror_block.shape[1], mirror_block.shape[2] + tiles_x = (new_w + mb_w - 1) // mb_w + 1 + tiles_y = (new_h + mb_h - 1) // mb_h + 1 + tiled = mirror_block.repeat(1, tiles_y, tiles_x, 1) + # Offset so original content lands at (pad_top, pad_left) in output + off_x = (mb_w - pad_left % mb_w) % mb_w + off_y = (mb_h - pad_top % mb_h) % mb_h + return tiled[:, off_y:off_y+new_h, off_x:off_x+new_w, :] + return tensor + + +class ImageTransformKJ(io.ComfyNode): + @classmethod + def define_schema(cls): + return io.Schema( + node_id="ImageTransformKJ", + display_name="Image Transform KJ", + category="KJNodes/image", + search_aliases=["resize", "crop", "pad", "upscale", "keep proportion", "bbox", "bounding box", "transform", "rotate", "mirror"], + is_experimental=True, + description=""" +Interactive image transform node: crop, resize, pad, and rotate. +Connect an image input — the preview appears automatically. + +Cropping: +Click + drag to draw a crop region. +Drag inside to move, drag edges/corners to resize. +Right-click to delete a region. +Ctrl to snap to grid. +Shift + resize to constrain aspect ratio. +Alt + resize to resize symmetrically. + +Padding: +Shift + drag to adjust padding position. + +Rotate button enables rotation cross (drag to rotate, right-click to reset). +Set target_width/height to resize output (0 = keep original). +Use keep_proportion to control how the image fits the target. +Use extra_padding to add padding with color or edge fill (clamp/repeat/mirror).""", + inputs=[ + io.MatchType.Input("image", io.MatchType.Template("img_or_mask", [io.Image, io.Mask]), tooltip="The image or mask to transform."), + io.Mask.Input("mask", optional=True, tooltip="Optional mask to transform alongside the image."), + io.Int.Input("target_width", default=0, min=0, max=MAX_RESOLUTION, step=1, tooltip="Target output width. 0 = keep original dimensions."), + io.Int.Input("target_height", default=0, min=0, max=MAX_RESOLUTION, step=1, tooltip="Target output height. 0 = keep original dimensions."), + io.Combo.Input("upscale_method", options=["nearest-exact", "bilinear", "area", "bicubic", "lanczos"], default="lanczos", tooltip="Interpolation method for resizing."), + io.DynamicCombo.Input("keep_proportion", options=[ + io.DynamicCombo.Option(key="keep_long_edge", inputs=[]), + io.DynamicCombo.Option(key="keep_short_edge", inputs=[]), + io.DynamicCombo.Option(key="total_pixels", inputs=[]), + io.DynamicCombo.Option(key="stretch", inputs=[]), + io.DynamicCombo.Option(key="crop", inputs=[]), + io.DynamicCombo.Option(key="pad_color", inputs=[ + io.Float.Input("pad_x", default=0.5, min=0.0, max=1.0, step=0.01, + tooltip="Horizontal position of content within padding (0=left, 0.5=center, 1=right). Shift+drag content in preview to adjust."), + io.Float.Input("pad_y", default=0.5, min=0.0, max=1.0, step=0.01, + tooltip="Vertical position of content within padding (0=top, 0.5=center, 1=bottom). Shift+drag content in preview to adjust."), + ]), + io.DynamicCombo.Option(key="pad_edge", inputs=[ + io.Combo.Input("edge_mode", options=["clamp", "repeat", "mirror"], default="clamp", + tooltip="clamp: extend edge pixels. repeat: tile the image. mirror: tile with mirroring."), + io.Float.Input("pad_x", default=0.5, min=0.0, max=1.0, step=0.01, + tooltip="Horizontal position of content within padding (0=left, 0.5=center, 1=right). Shift+drag content in preview to adjust."), + io.Float.Input("pad_y", default=0.5, min=0.0, max=1.0, step=0.01, + tooltip="Vertical position of content within padding (0=top, 0.5=center, 1=bottom). Shift+drag content in preview to adjust."), + ]), + io.DynamicCombo.Option(key="multiplier", inputs=[ + io.Float.Input("width_mult", default=1.0, min=0.01, max=16.0, step=0.05, + tooltip="Multiply the crop width by this factor."), + io.Float.Input("height_mult", default=1.0, min=0.01, max=16.0, step=0.05, + tooltip="Multiply the crop height by this factor."), + ]), + ]), + io.Int.Input("divisible_by", default=2, min=0, max=512, step=1), + io.DynamicCombo.Input("extra_padding", options=[ + io.DynamicCombo.Option(key="disabled", inputs=[]), + io.DynamicCombo.Option(key="pad_color", inputs=_pad_inputs()), + io.DynamicCombo.Option(key="pad_edge", inputs=_pad_inputs() + [ + io.Combo.Input("edge_mode", options=["clamp", "repeat", "mirror"], default="clamp", + tooltip="clamp: extend edge pixels. repeat: tile the image. mirror: tile with mirroring."), + ]), + io.DynamicCombo.Option(key="pad_crop_color", inputs=_pad_inputs()), + io.DynamicCombo.Option(key="pad_crop_edge", inputs=_pad_inputs() + [ + io.Combo.Input("edge_mode", options=["clamp", "repeat", "mirror"], default="clamp", + tooltip="clamp: extend edge pixels. repeat: tile the image. mirror: tile with mirroring."), + ]), + ]), + io.DynamicCombo.Input("invert_crop", options=[ + io.DynamicCombo.Option(key="disabled", inputs=[]), + io.DynamicCombo.Option(key="enabled", inputs=[]), + ]), + io.String.Input("bboxes", default="", socketless=True, advanced=True), + ], + outputs=[ + io.MatchType.Output(io.MatchType.Template("img_or_mask", [io.Image, io.Mask]), id="cropped", display_name="output", is_output_list=True), + io.Mask.Output("cropped_mask", display_name="output_mask", is_output_list=True), + io.BBOX.Output("bbox", display_name="bbox", is_output_list=True), + io.Mask.Output("bbox_mask", display_name="bbox_mask", is_output_list=True), + io.Int.Output("width", display_name="width", tooltip="Width of the output image."), + io.Int.Output("height", display_name="height", tooltip="Height of the output image."), + ], + ) + + + @classmethod + def execute(cls, image, target_width, target_height, upscale_method, keep_proportion, divisible_by, + extra_padding, invert_crop, bboxes, mask=None): + # Unpack DynamicCombos + edge_mode = keep_proportion.get("edge_mode", "clamp") + pad_x = keep_proportion.get("pad_x", 0.5) + pad_y = keep_proportion.get("pad_y", 0.5) + width_mult = keep_proportion.get("width_mult", 1.0) + height_mult = keep_proportion.get("height_mult", 1.0) + keep_proportion = keep_proportion["keep_proportion"] + extra_top = extra_padding.get("pad_top", 0) + extra_bottom = extra_padding.get("pad_bottom", 0) + extra_left = extra_padding.get("pad_left", 0) + extra_right = extra_padding.get("pad_right", 0) + extra_edge_mode = extra_padding.get("edge_mode", "clamp") + extra_pad_mode = extra_padding.get("extra_padding", "disabled") + invert_crop = invert_crop["invert_crop"] + + + # Parse fill color from bboxes JSON (shared color picker) + fill_color_rgb = [0, 0, 0] + if bboxes: + try: + _parsed_tmp = json.loads(bboxes) + if isinstance(_parsed_tmp, dict) and "fillColor" in _parsed_tmp: + fill_color_rgb = string_to_color(_parsed_tmp["fillColor"]) + except (json.JSONDecodeError, Exception): + pass + fill_rgb = [c / 255.0 for c in fill_color_rgb[:3]] + + # Handle mask input (3D) by converting to image-like 4D tensor + input_is_mask = image.ndim == 3 + if input_is_mask: + image = image.unsqueeze(-1).repeat(1, 1, 1, 3) + + # Save input image as temp preview file for JS canvas + temp_dir = folder_paths.get_temp_directory() + pil_img = Image.fromarray((image[0].cpu().numpy() * 255).astype(np.uint8)) + preview_filename = f"crop_preview_{random.randint(0, 0xFFFFFF):06x}.webp" + pil_img.save(os.path.join(temp_dir, preview_filename), format="WEBP", quality=80) + preview_ui = {"preview_filename": [preview_filename]} + + img_height = image.shape[1] + img_width = image.shape[2] + + # Parse bboxes and rotation + bbox_list = [] + rotation = 0.0 + if bboxes: + try: + parsed = json.loads(bboxes) + # New format: { bboxes: [...], rotation: N } + if isinstance(parsed, dict): + bbox_list = [b for b in parsed.get("bboxes", []) if b and all(k in b for k in ("startX", "startY", "endX", "endY"))] + rotation = parsed.get("rotation", 0.0) + # Legacy format: [bbox, bbox, ...] + elif isinstance(parsed, list): + bbox_list = [b for b in parsed if b and all(k in b for k in ("startX", "startY", "endX", "endY"))] + except json.JSONDecodeError: + pass + + # Content mask tracks which pixels are actual image content (1=content, 0=fill) + content_mask = torch.ones(1, img_height, img_width, device=image.device) + + # Apply rotation before cropping + if rotation != 0: + from torchvision.transforms.functional import rotate as tv_rotate + import torch.nn.functional as F + # Use shared fill color for rotation corners (unless edge mode) + rot_fill = fill_rgb + + is_edge_mode = extra_pad_mode in ("pad_edge", "pad_crop_edge") or keep_proportion == "pad_edge" + if is_edge_mode: + h, w = image.shape[1], image.shape[2] + pad_amt = max(h, w) + img_chw = image.movedim(-1, 1) + img_padded = F.pad(img_chw, [pad_amt, pad_amt, pad_amt, pad_amt], mode='replicate') + img_rotated = tv_rotate(img_padded, -rotation, expand=False, fill=rot_fill) + ch, cw = img_rotated.shape[2], img_rotated.shape[3] + cy, cx = ch // 2, cw // 2 + image = img_rotated[:, :, cy - h // 2:cy - h // 2 + h, cx - w // 2:cx - w // 2 + w].movedim(1, -1) + if mask is not None: + mask_padded = F.pad(mask.unsqueeze(1), [pad_amt, pad_amt, pad_amt, pad_amt], mode='replicate') + mask_rotated = tv_rotate(mask_padded, -rotation, expand=False, fill=[0.0]) + mask = mask_rotated[:, :, cy - h // 2:cy - h // 2 + h, cx - w // 2:cx - w // 2 + w].squeeze(1) + # Content mask: rotate the same way (no padding — just rotate and crop) + cm_padded = F.pad(content_mask.unsqueeze(1), [pad_amt, pad_amt, pad_amt, pad_amt], mode='constant', value=0) + cm_rotated = tv_rotate(cm_padded, -rotation, expand=False, fill=[0.0]) + content_mask = cm_rotated[:, :, cy - h // 2:cy - h // 2 + h, cx - w // 2:cx - w // 2 + w].squeeze(1) + else: + image = tv_rotate(image.movedim(-1, 1), -rotation, expand=True, fill=rot_fill).movedim(1, -1) + if mask is not None: + mask = tv_rotate(mask.unsqueeze(1), -rotation, expand=True, fill=[0.0]).squeeze(1) + # Content mask: rotate with expand, fill=0 + content_mask = tv_rotate(content_mask.unsqueeze(1), -rotation, expand=True, fill=[0.0]).squeeze(1) + img_height = image.shape[1] + img_width = image.shape[2] + + # Normalize mask dimensions to match image + if mask is not None: + if mask.shape[-2] != img_height or mask.shape[-1] != img_width: + if mask.shape[-2] == img_width and mask.shape[-1] == img_height: + mask = mask.transpose(-2, -1) + else: + mask = _resize_single_channel(mask, img_width, img_height) + + # "Pad first" modes: apply extra padding to the full image before cropping + # Skip for keep_proportion pad modes — those handle extra padding via target subtraction + is_pad_first = extra_pad_mode in ("pad_color", "pad_edge") + kp_is_pad_mode = keep_proportion in ("pad_color", "pad_edge") + if is_pad_first and not kp_is_pad_mode and (extra_top > 0 or extra_bottom > 0 or extra_left > 0 or extra_right > 0): + pad_mode = "color" if extra_pad_mode == "pad_color" else "edge" + padded_img = _apply_padding(image, extra_top, extra_bottom, extra_left, extra_right, pad_mode, extra_edge_mode, fill_rgb) + + image = padded_img + img_height = image.shape[1] + img_width = image.shape[2] + # Expand content mask and user mask + cm_new = torch.zeros(1, img_height, img_width, device=content_mask.device) + cm_new[:, extra_top:extra_top+content_mask.shape[1], extra_left:extra_left+content_mask.shape[2]] = content_mask + content_mask = cm_new + if mask is not None: + m_new = torch.zeros(mask.shape[0], img_height, img_width, device=mask.device, dtype=mask.dtype) + m_new[:, extra_top:extra_top+mask.shape[1], extra_left:extra_left+mask.shape[2]] = mask + mask = m_new + + # If no bboxes, treat the full image as a single bbox + if not bbox_list: + bbox_list = [None] + + all_cropped = [] + all_cropped_masks = [] + all_bbox_tuples = [] + all_bbox_masks = [] + + for bbox_data in bbox_list: + has_bbox = bbox_data is not None + + if has_bbox: + preview_width = bbox_data.get("previewWidth", 0) + preview_height = bbox_data.get("previewHeight", 0) + sx = img_width / preview_width if preview_width > 0 else 1.0 + sy = img_height / preview_height if preview_height > 0 else 1.0 + + x_min = int(min(bbox_data["startX"], bbox_data["endX"]) * sx) + y_min = int(min(bbox_data["startY"], bbox_data["endY"]) * sy) + x_max = int(max(bbox_data["startX"], bbox_data["endX"]) * sx) + y_max = int(max(bbox_data["startY"], bbox_data["endY"]) * sy) + + x_min = max(0, min(x_min, img_width - 1)) + y_min = max(0, min(y_min, img_height - 1)) + x_max = max(x_min + 1, min(x_max, img_width)) + y_max = max(y_min + 1, min(y_max, img_height)) + + cropped = image[:, y_min:y_max, x_min:x_max, :] + cropped_content_mask = content_mask[:, y_min:y_max, x_min:x_max] + all_bbox_tuples.append((x_min, y_min, x_max - x_min, y_max - y_min)) + bm = torch.zeros(1, img_height, img_width) + bm[0, y_min:y_max, x_min:x_max] = 1.0 + all_bbox_masks.append(bm) + cropped_mask = mask[:, y_min:y_max, x_min:x_max] if mask is not None else None + else: + cropped = image + cropped_content_mask = content_mask + all_bbox_tuples.append((0, 0, img_width, img_height)) + all_bbox_masks.append(torch.ones(1, img_height, img_width)) + cropped_mask = mask + x_min, y_min, x_max, y_max = 0, 0, img_width, img_height + + # Multiplier mode: compute target from crop dims * multiplier + if keep_proportion == "multiplier": + crop_h, crop_w = cropped.shape[1], cropped.shape[2] + tw = round(crop_w * width_mult) + th = round(crop_h * height_mult) + target_width = tw + target_height = th + + # Resize cropped image if target dimensions are set + if target_width > 0 or target_height > 0: + crop_h, crop_w = cropped.shape[1], cropped.shape[2] + tw = target_width if target_width > 0 else crop_w + th = target_height if target_height > 0 else crop_h + + # Subtract extra padding from target so content + padding = original target + # For pad-first + non-pad keep_proportion, padding is on the source (don't subtract) + # For pad modes or pad-crop, subtract so padding is in the output + has_extra = extra_top > 0 or extra_bottom > 0 or extra_left > 0 or extra_right > 0 + kp_is_pad = keep_proportion in ("pad_color", "pad_edge") + if has_extra and (kp_is_pad or not is_pad_first): + if target_width > 0: + tw = max(1, tw - extra_left - extra_right) + if target_height > 0: + th = max(1, th - extra_top - extra_bottom) + + if keep_proportion == "keep_long_edge": + ratio = min(tw / crop_w, th / crop_h) + tw = round(crop_w * ratio) + th = round(crop_h * ratio) + elif keep_proportion == "keep_short_edge": + ratio = max(tw / crop_w, th / crop_h) + tw = round(crop_w * ratio) + th = round(crop_h * ratio) + elif keep_proportion == "total_pixels": + total_pixels = tw * th + aspect_ratio = crop_w / crop_h + th = int(math.sqrt(total_pixels / aspect_ratio)) + tw = int(math.sqrt(total_pixels * aspect_ratio)) + elif keep_proportion == "crop": + ratio = max(tw / crop_w, th / crop_h) + scale_w = round(crop_w * ratio) + scale_h = round(crop_h * ratio) + samples = common_upscale(cropped.movedim(-1, 1), scale_w, scale_h, upscale_method, "center") + cropped = samples.movedim(1, -1) + if cropped_mask is not None: + cropped_mask = _upscale_mask(cropped_mask, scale_w, scale_h, upscale_method, "center") + cropped_content_mask = _resize_single_channel(cropped_content_mask, scale_w, scale_h) + cx = (scale_w - tw) // 2 + cy = (scale_h - th) // 2 + cropped = cropped[:, cy:cy+th, cx:cx+tw, :] + if cropped_mask is not None: + cropped_mask = cropped_mask[:, cy:cy+th, cx:cx+tw] + cropped_content_mask = cropped_content_mask[:, cy:cy+th, cx:cx+tw] + elif keep_proportion in ("pad_color", "pad_edge"): + ratio = min(tw / crop_w, th / crop_h) + scale_w = round(crop_w * ratio) + scale_h = round(crop_h * ratio) + samples = common_upscale(cropped.movedim(-1, 1), scale_w, scale_h, upscale_method, "disabled") + resized = samples.movedim(1, -1) + # pad_x/pad_y position across full target (not just content area) + full_tw = target_width if target_width > 0 else crop_w + full_th = target_height if target_height > 0 else crop_h + pad_left = round((full_tw - scale_w) * pad_x) + pad_top = round((full_th - scale_h) * pad_y) + pad_right = full_tw - pad_left - scale_w + pad_bottom = full_th - pad_top - scale_h + tw = full_tw + th = full_th + + pad_mode = "edge" if keep_proportion == "pad_edge" else "color" + cropped = _apply_padding(resized, pad_top, pad_bottom, pad_left, pad_right, pad_mode, edge_mode, fill_rgb) + if cropped_mask is not None: + mask_resized = _upscale_mask(cropped_mask, scale_w, scale_h, upscale_method, "disabled") + mask_padded = torch.zeros(mask_resized.shape[0], th, tw, device=mask_resized.device, dtype=mask_resized.dtype) + mask_padded[:, pad_top:pad_top+scale_h, pad_left:pad_left+scale_w] = mask_resized + cropped_mask = mask_padded + # Update content mask for padding area + cm_resized = _resize_single_channel(cropped_content_mask, scale_w, scale_h) + cm_padded = torch.zeros(1, th, tw, device=cropped_content_mask.device) + cm_padded[:, pad_top:pad_top+scale_h, pad_left:pad_left+scale_w] = cm_resized + cropped_content_mask = cm_padded + + if divisible_by > 1: + tw = tw - (tw % divisible_by) + th = th - (th % divisible_by) + + if tw > 0 and th > 0: + if keep_proportion in ("stretch", "keep_long_edge", "keep_short_edge", "total_pixels", "multiplier"): + cropped = common_upscale(cropped.movedim(-1, 1), tw, th, upscale_method, "disabled").movedim(1, -1) + if cropped_mask is not None: + cropped_mask = _upscale_mask(cropped_mask, tw, th, upscale_method, "disabled") + cropped_content_mask = _resize_single_channel(cropped_content_mask, tw, th) + else: + cropped = cropped[:, :th, :tw, :] + if cropped_mask is not None: + cropped_mask = cropped_mask[:, :th, :tw] + cropped_content_mask = cropped_content_mask[:, :th, :tw] + + # Enforce divisible_by even when no target dimensions are set + elif divisible_by > 1: + final_w = cropped.shape[2] - (cropped.shape[2] % divisible_by) + final_h = cropped.shape[1] - (cropped.shape[1] % divisible_by) + if final_w != cropped.shape[2] or final_h != cropped.shape[1]: + cropped = cropped[:, :final_h, :final_w, :] + if cropped_mask is not None: + cropped_mask = cropped_mask[:, :final_h, :final_w] + cropped_content_mask = cropped_content_mask[:, :final_h, :final_w] + + # Apply extra padding (skip for pad-first and keep_proportion pad modes which handle it above) + kp_handles_ep = keep_proportion in ("pad_color", "pad_edge") + if not is_pad_first and not kp_handles_ep and (extra_top > 0 or extra_bottom > 0 or extra_left > 0 or extra_right > 0): + h_cur, w_cur = cropped.shape[1], cropped.shape[2] + pad_mode = "edge" if extra_pad_mode == "pad_crop_edge" else "color" + cropped = _apply_padding(cropped, extra_top, extra_bottom, extra_left, extra_right, pad_mode, extra_edge_mode, fill_rgb) + new_h, new_w = cropped.shape[1], cropped.shape[2] + if cropped_mask is not None: + padded_mask = torch.zeros(cropped_mask.shape[0], new_h, new_w, device=cropped_mask.device, dtype=cropped_mask.dtype) + padded_mask[:, extra_top:extra_top+h_cur, extra_left:extra_left+w_cur] = cropped_mask + cropped_mask = padded_mask + cm_h, cm_w = cropped_content_mask.shape[-2], cropped_content_mask.shape[-1] + if cm_h != h_cur or cm_w != w_cur: + cropped_content_mask = _resize_single_channel(cropped_content_mask, w_cur, h_cur) + cm_ep = torch.zeros(1, new_h, new_w, device=cropped_content_mask.device) + cm_ep[:, extra_top:extra_top+h_cur, extra_left:extra_left+w_cur] = cropped_content_mask + cropped_content_mask = cm_ep + + # If no mask was provided, output a zeros mask matching the cropped image + if cropped_mask is None: + cropped_mask = torch.zeros(1, cropped.shape[1], cropped.shape[2]) + + # Apply fill mask — marks filled/padded areas as 1 in the output mask + # Combines with incoming mask: 1 where either input mask is 1 OR area is filled + if cropped_content_mask is not None: + out_h, out_w = cropped_mask.shape[1], cropped_mask.shape[2] + cm_h, cm_w = cropped_content_mask.shape[1], cropped_content_mask.shape[2] + if cm_h != out_h or cm_w != out_w: + cropped_content_mask = _resize_single_channel(cropped_content_mask, out_w, out_h) + # fill_mask: 1 where filled, 0 where content + fill_mask = 1.0 - cropped_content_mask.clamp(0, 1) + # Combine: output mask is max of incoming mask and fill mask + cropped_mask = torch.max(cropped_mask, fill_mask) + + # Invert crop: output area outside the bbox instead of inside + if invert_crop == "enabled" and has_bbox: + inverted = image.clone() + for c in range(min(3, inverted.shape[3])): + inverted[:, y_min:y_max, x_min:x_max, c] = fill_rgb[c] + cropped = inverted + + # Convert back to mask if input was a mask + if input_is_mask: + cropped = cropped[:, :, :, 0] + + all_cropped.append(cropped) + all_cropped_masks.append(cropped_mask) + + width, height = all_cropped[0].shape[2], all_cropped[0].shape[1] + + return io.NodeOutput(all_cropped, all_cropped_masks, all_bbox_tuples, all_bbox_masks, width, height, ui=preview_ui) + + +class BBOXToBoundingBoxKJ(io.ComfyNode): + @classmethod + def define_schema(cls): + return io.Schema( + node_id="BBOXToBoundingBoxKJ", + display_name="BBOX to Bounding Box KJ", + category="KJNodes/image", + search_aliases=["bbox", "bounding box", "convert", "crop region"], + description="Convert a KJNodes BBOX to the native BOUNDING_BOX dict used by core nodes such as Crop Image.", + inputs=[ + io.BBOX.Input("bbox", tooltip="BBOX from any KJNodes node. Single boxes and lists of boxes are both accepted."), + io.Combo.Input("bbox_format", options=["xywh", "xyxy"], default="xywh", + tooltip="Layout of the incoming values."), + io.Int.Input("index", default=0, min=0, max=4096, + tooltip="When the input holds multiple boxes, pick which one to output. Clamped to the last box."), + ], + outputs=[ + io.BoundingBox.Output("bounding_box"), + ], + ) + + @classmethod + def execute(cls, bbox, bbox_format, index) -> io.NodeOutput: + boxes = normalize_bboxes(bbox, bbox_format) + return io.NodeOutput(bbox_to_bounding_box(boxes[min(index, len(boxes) - 1)], bbox_format)) diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/lora_nodes.py b/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/lora_nodes.py new file mode 100644 index 0000000000000000000000000000000000000000..e2de4fd9c261dcbfb79e847bad29de3b38f88cce --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/lora_nodes.py @@ -0,0 +1,673 @@ +import torch +import comfy.model_management +import comfy.utils +import folder_paths +import os +import logging +from tqdm import tqdm +import numpy as np +from comfy_api.latest import io + +device = comfy.model_management.get_torch_device() + +CLAMP_QUANTILE = 0.99 + +def extract_lora(diff, key, rank, algorithm, lora_type, lowrank_iters=7, adaptive_param=1.0, clamp_quantile=True): + """ + Extracts LoRA weights from a weight difference tensor using SVD. + """ + conv2d = (len(diff.shape) == 4) + kernel_size = None if not conv2d else diff.size()[2:4] + conv2d_3x3 = conv2d and kernel_size != (1, 1) + out_dim, in_dim = diff.size()[0:2] + + if conv2d: + if conv2d_3x3: + diff = diff.flatten(start_dim=1) + else: + diff = diff.squeeze() + + diff_float = diff.float() + if algorithm == "svd_lowrank": + U, S, V = torch.svd_lowrank(diff_float, q=min(rank, in_dim, out_dim), niter=lowrank_iters) + U = U @ torch.diag(S) + Vh = V.t() + else: + #torch.linalg.svdvals() + U, S, Vh = torch.linalg.svd(diff_float) + # Flexible rank selection logic like locon: https://github.com/KohakuBlueleaf/LyCORIS/blob/main/tools/extract_locon.py + if "adaptive" in lora_type: + if lora_type == "adaptive_ratio": + min_s = torch.max(S) * adaptive_param + lora_rank = torch.sum(S > min_s).item() + elif lora_type == "adaptive_energy": + energy = torch.cumsum(S**2, dim=0) + total_energy = torch.sum(S**2) + threshold = adaptive_param * total_energy # e.g., adaptive_param=0.95 for 95% + lora_rank = torch.sum(energy < threshold).item() + 1 + elif lora_type == "adaptive_quantile": + s_cum = torch.cumsum(S, dim=0) + min_cum_sum = adaptive_param * torch.sum(S) + lora_rank = torch.sum(s_cum < min_cum_sum).item() + elif lora_type == "adaptive_fro": + S_squared = S.pow(2) + S_fro_sq = float(torch.sum(S_squared)) + sum_S_squared = torch.cumsum(S_squared, dim=0) / S_fro_sq + lora_rank = int(torch.searchsorted(sum_S_squared, adaptive_param**2)) + 1 + lora_rank = max(1, min(lora_rank, len(S))) + else: + pass # Will print after capping + + # Cap adaptive rank by the specified max rank + lora_rank = min(lora_rank, rank) + + # Calculate and print actual fro percentage retained after capping + if lora_type == "adaptive_fro": + S_squared = S.pow(2) + s_fro = torch.sqrt(torch.sum(S_squared)) + s_red_fro = torch.sqrt(torch.sum(S_squared[:lora_rank])) + fro_percent = float(s_red_fro / s_fro) + logging.info(f"{key} Extracted LoRA rank: {lora_rank}, Frobenius retained: {fro_percent:.1%}") + else: + logging.info(f"{key} Extracted LoRA rank: {lora_rank}") + else: + lora_rank = rank + + lora_rank = max(1, lora_rank) + lora_rank = min(out_dim, in_dim, lora_rank) + + U = U[:, :lora_rank] + S = S[:lora_rank] + U = U @ torch.diag(S) + Vh = Vh[:lora_rank, :] + + if clamp_quantile: + dist = torch.cat([U.flatten(), Vh.flatten()]) + if dist.numel() > 100_000: + # Sample 100,000 elements for quantile estimation + idx = torch.randperm(dist.numel(), device=dist.device)[:100_000] + dist_sample = dist[idx] + hi_val = torch.quantile(dist_sample, CLAMP_QUANTILE) + else: + hi_val = torch.quantile(dist, CLAMP_QUANTILE) + low_val = -hi_val + + U = U.clamp(low_val, hi_val) + Vh = Vh.clamp(low_val, hi_val) + if conv2d: + U = U.reshape(out_dim, lora_rank, 1, 1) + Vh = Vh.reshape(lora_rank, in_dim, kernel_size[0], kernel_size[1]) + return (U, Vh) + + +def calc_lora_model(model_diff, rank, prefix_model, prefix_lora, output_sd, lora_type, algorithm, lowrank_iters, out_dtype, bias_diff=False, adaptive_param=1.0, clamp_quantile=True): + # Get key names from module structure without materializing weights + sd_keys = [] + for name, _ in model_diff.model.named_parameters(): + if prefix_model is None or name.startswith(prefix_model): + sd_keys.append(name) + for name, _ in model_diff.model.named_buffers(): + if prefix_model is None or name.startswith(prefix_model): + sd_keys.append(name) + + total_keys = len([k for k in sd_keys if k.endswith(".weight") or (bias_diff and k.endswith(".bias"))]) + progress_bar = tqdm(total=total_keys, desc=f"Extracting LoRA ({prefix_lora.strip('.')})") + comfy_pbar = comfy.utils.ProgressBar(total_keys) + + # Process one weight at a time to minimize memory usage + for k in sd_keys: + if k.endswith(".weight"): + # Patch and retrieve single weight + weight_diff = model_diff.patch_weight_to_device(k, return_weight=True) + if weight_diff is None: + progress_bar.update(1) + comfy_pbar.update(1) + continue + if weight_diff.ndim == 5: + logging.info(f"Skipping 5D tensor for key {k}") + del weight_diff + progress_bar.update(1) + comfy_pbar.update(1) + continue + if lora_type != "full": + if weight_diff.ndim < 2: + if bias_diff: + output_sd["{}{}.diff".format(prefix_lora, k[len(prefix_model):-7])] = weight_diff.contiguous().to(out_dtype).cpu() + del weight_diff + progress_bar.update(1) + comfy_pbar.update(1) + continue + try: + out = extract_lora(weight_diff.to(device), k, rank, algorithm, lora_type, lowrank_iters=lowrank_iters, adaptive_param=adaptive_param, clamp_quantile=clamp_quantile) + output_sd["{}{}.lora_up.weight".format(prefix_lora, k[len(prefix_model):-7])] = out[0].contiguous().to(out_dtype).cpu() + output_sd["{}{}.lora_down.weight".format(prefix_lora, k[len(prefix_model):-7])] = out[1].contiguous().to(out_dtype).cpu() + except Exception as e: + logging.warning(f"Could not generate lora weights for key {k}, error {e}") + else: + output_sd["{}{}.diff".format(prefix_lora, k[len(prefix_model):-7])] = weight_diff.contiguous().to(out_dtype).cpu() + del weight_diff + progress_bar.update(1) + comfy_pbar.update(1) + + elif bias_diff and k.endswith(".bias"): + weight = model_diff.patch_weight_to_device(k, return_weight=True) + if weight is not None: + output_sd["{}{}.diff_b".format(prefix_lora, k[len(prefix_model):-5])] = weight.contiguous().to(out_dtype).cpu() + del weight + progress_bar.update(1) + comfy_pbar.update(1) + + progress_bar.close() + del model_diff + comfy.model_management.soft_empty_cache() + return output_sd + + +class LoraExtractKJ(io.ComfyNode): + @classmethod + def define_schema(cls): + return io.Schema( + node_id="LoraExtractKJ", + category="KJNodes/lora", + is_output_node=True, + inputs=[ + io.MultiType.Input("finetuned", [io.Model, io.Clip], tooltip="The finetuned model or clip to extract LoRA from."), + io.MultiType.Input("original", [io.Model, io.Clip], tooltip="The original base model or clip to diff against."), + io.String.Input("filename_prefix", default="loras/ComfyUI_extracted_lora"), + io.Int.Input("rank", default=64, min=1, max=4096, step=1, tooltip="The rank to use for standard LoRA, or maximum rank limit for adaptive methods."), + io.Combo.Input("lora_type", options=["standard", "full", "adaptive_ratio", "adaptive_quantile", "adaptive_energy", "adaptive_fro"]), + io.Combo.Input("algorithm", options=["svd_linalg", "svd_lowrank"], default="svd_lowrank", tooltip="SVD algorithm to use, svd_lowrank is faster but less accurate."), + io.Int.Input("lowrank_iters", default=7, min=1, max=100, step=1, tooltip="The number of subspace iterations for lowrank SVD algorithm."), + io.Combo.Input("output_dtype", options=["fp16", "bf16", "fp32"], default="fp16"), + io.Boolean.Input("bias_diff", default=True), + io.Float.Input("adaptive_param", default=0.15, min=0.0, max=1.0, step=0.01, tooltip="For ratio mode, this is the ratio of the maximum singular value. For quantile mode, this is the quantile of the singular values. For fro mode, this is the Frobenius norm retention ratio."), + io.Boolean.Input("clamp_quantile", default=False), + ], + ) + + + @classmethod + def execute(cls, finetuned, original, filename_prefix, rank, lora_type, algorithm, lowrank_iters, output_dtype, bias_diff, adaptive_param, clamp_quantile) -> io.NodeOutput: + if algorithm == "svd_lowrank" and lora_type != "standard": + raise ValueError("svd_lowrank algorithm is only supported for standard LoRA extraction.") + + dtype = {"bf16": torch.bfloat16, "fp16": torch.float16, "fp32": torch.float32}[output_dtype] + + output_dir = folder_paths.get_output_directory() + full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(filename_prefix, output_dir) + + output_sd = {} + + is_clip = hasattr(finetuned, "patcher") + + if is_clip: + clip_diff = finetuned.clone() + kp = original.get_key_patches() + kp = {k: v for k, v in kp.items() if not k.endswith(".position_ids") and not k.endswith(".logit_scale")} + clip_diff.add_patches(kp, -1.0, 1.0) + output_sd = calc_lora_model(clip_diff.patcher, rank, "", "text_encoders.", output_sd, lora_type, algorithm, lowrank_iters, dtype, bias_diff=bias_diff, adaptive_param=adaptive_param, clamp_quantile=clamp_quantile) + else: + m = finetuned.clone() + kp = original.get_key_patches("diffusion_model.") + m.add_patches(kp, -1.0, 1.0) + output_sd = calc_lora_model(m, rank, "diffusion_model.", "diffusion_model.", output_sd, lora_type, algorithm, lowrank_iters, dtype, bias_diff=bias_diff, adaptive_param=adaptive_param, clamp_quantile=clamp_quantile) + + if "adaptive" in lora_type: + rank_str = f"{lora_type}_{adaptive_param:.2f}" + else: + rank_str = rank + output_checkpoint = f"{filename}_rank_{rank_str}_{output_dtype}_{counter:05}_.safetensors" + output_checkpoint = os.path.join(full_output_folder, output_checkpoint) + + comfy.utils.save_torch_file(output_sd, output_checkpoint, metadata=None) + return io.NodeOutput() + +class LoraReduceRank(io.ComfyNode): + @classmethod + def define_schema(cls): + return io.Schema( + node_id="LoraReduceRankKJ", + display_name="LoraReduceRank", + category="KJNodes/lora", + description="Resize a LoRA model by reducing its rank. Based on kohya's sd-scripts: https://github.com/kohya-ss/sd-scripts/blob/main/networks/resize_lora.py", + is_output_node=True, + is_experimental=True, + inputs=[ + io.Combo.Input("lora_name", options=folder_paths.get_filename_list("loras"), tooltip="The name of the LoRA."), + io.Int.Input("new_rank", default=8, min=1, max=4096, step=1, tooltip="The new rank to resize the LoRA. Acts as max rank when using dynamic_method."), + io.Combo.Input("dynamic_method", options=["disabled", "sv_ratio", "sv_cumulative", "sv_fro", "sv_knee"], default="disabled", tooltip="Method to use for dynamically determining new alphas and dims. sv_knee finds the elbow point in the singular value curve."), + io.Float.Input("dynamic_param", default=0.2, min=0.0, max=2.0, step=0.01, tooltip="Parameter for dynamic methods. For sv_knee: sensitivity (1.0=standard knee, <1.0=more aggressive/lower rank, >1.0=more conservative)."), + io.Combo.Input("output_dtype", options=["match_original", "fp16", "bf16", "fp32"], default="match_original", tooltip="Data type to save the LoRA as."), + io.Boolean.Input("verbose", default=True), + ], + ) + + @classmethod + def execute(cls, lora_name, new_rank, dynamic_method, dynamic_param, output_dtype, verbose) -> io.NodeOutput: + lora_path = folder_paths.get_full_path("loras", lora_name) + lora_sd, metadata = comfy.utils.load_torch_file(lora_path, return_metadata=True) + + if output_dtype == "fp16": + save_dtype = torch.float16 + elif output_dtype == "bf16": + save_dtype = torch.bfloat16 + elif output_dtype == "fp32": + save_dtype = torch.float32 + elif output_dtype == "match_original": + first_weight_key = next(k for k in lora_sd if k.endswith(".weight") and isinstance(lora_sd[k], torch.Tensor)) + save_dtype = lora_sd[first_weight_key].dtype + + new_lora_sd = {} + for k, v in lora_sd.items(): + new_lora_sd[k.replace(".default", "")] = v + del lora_sd + logging.info("Resizing Lora...") + output_sd, old_dim, new_alpha, rank_list = resize_lora_model(new_lora_sd, new_rank, save_dtype, device, dynamic_method, dynamic_param, verbose) + + if metadata is None: + metadata = {} + + comment = metadata.get("ss_training_comment", "") + + if dynamic_method == "disabled": + metadata["ss_training_comment"] = f"dimension is resized from {old_dim} to {new_rank}; {comment}" + metadata["ss_network_dim"] = str(new_rank) + metadata["ss_network_alpha"] = str(new_alpha) + else: + metadata["ss_training_comment"] = f"Dynamic resize with {dynamic_method}: {dynamic_param} from {old_dim}; {comment}" + metadata["ss_network_dim"] = "Dynamic" + metadata["ss_network_alpha"] = "Dynamic" + + for key in list(output_sd.keys()): + value = output_sd[key] + if type(value) == torch.Tensor and value.dtype.is_floating_point and value.dtype != save_dtype: + output_sd[key] = value.to(save_dtype) + + output_dir = folder_paths.get_output_directory() + output_filename_prefix = "loras/" + lora_name + + full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(output_filename_prefix, output_dir) + output_dtype_str = f"_{output_dtype}" if output_dtype != "match_original" else "" + average_rank = str(int(np.mean(rank_list))) + rank_str = new_rank if dynamic_method == "disabled" else f"dynamic_{average_rank}" + output_checkpoint = f"{filename.replace('.safetensors', '')}_resized_from_{old_dim}_to_{rank_str}{output_dtype_str}_{counter:05}_.safetensors" + output_checkpoint = os.path.join(full_output_folder, output_checkpoint) + logging.info(f"Saving resized LoRA to {output_checkpoint}") + + comfy.utils.save_torch_file(output_sd, output_checkpoint, metadata=metadata) + return io.NodeOutput() + +# Convert LoRA to different rank approximation (should only be used to go to lower rank) +# This code is based off the extract_lora_from_models.py file which is based on https://github.com/cloneofsimo/lora/blob/develop/lora_diffusion/cli_svd.py +# Thanks to cloneofsimo + +# This version is based on +# https://github.com/kohya-ss/sd-scripts/blob/main/networks/resize_lora.py + +MIN_SV = 1e-6 + +LORA_DOWN_UP_FORMATS = [ + ("lora_down", "lora_up"), # sd-scripts LoRA + ("lora_A", "lora_B"), # PEFT LoRA + ("down", "up"), # ControlLoRA +] + +# Indexing functions +def index_sv_cumulative(S, target): + original_sum = float(torch.sum(S)) + cumulative_sums = torch.cumsum(S, dim=0) / original_sum + index = int(torch.searchsorted(cumulative_sums, target)) + 1 + index = max(1, min(index, len(S) - 1)) + + return index + + +def index_sv_fro(S, target): + S_squared = S.pow(2) + S_fro_sq = float(torch.sum(S_squared)) + sum_S_squared = torch.cumsum(S_squared, dim=0) / S_fro_sq + index = int(torch.searchsorted(sum_S_squared, target**2)) + 1 + index = max(1, min(index, len(S) - 1)) + + return index + + +def index_sv_ratio(S, target): + max_sv = S[0] + min_sv = max_sv / target + index = int(torch.sum(S > min_sv).item()) + index = max(1, min(index, len(S) - 1)) + + return index + + +def index_sv_knee(S, sensitivity=1.0): + """Find the knee/elbow point in the singular value curve. + Uses the Kneedle method: normalizes the curve to [0,1] on both axes, + then finds the point with maximum distance from a reference line. + + sensitivity controls the aggressiveness: + 1.0 = standard knee point + < 1.0 = more aggressive (lower rank), e.g. 0.5 roughly halves the knee rank + > 1.0 = more conservative (higher rank) + The reference line tilts based on sensitivity: at lower values, + the line favors keeping fewer singular values.""" + n = len(S) + if n <= 2: + return 1 + + S_np = S.cpu().float().numpy() + + # Normalize x and y to [0, 1] + x = np.linspace(0, 1, n) + y = (S_np - S_np[-1]) / (S_np[0] - S_np[-1] + 1e-10) + + # Reference line from (0, 1) to (1, 1-sensitivity) + # At sensitivity=1.0: line goes (0,1)->(1,0), standard kneedle + # At sensitivity=0.5: line goes (0,1)->(1,0.5), steeper = more aggressive + y_line = 1.0 - sensitivity * x + + # Signed distance: positive means curve is above the line (keep) + distances = y - y_line + + # Find the point of maximum positive distance + index = int(np.argmax(distances)) + index = max(1, min(index, n - 1)) + return index + + +# Modified from Kohaku-blueleaf's extract/merge functions +def _svd_extract(weight_2d, lora_rank, dynamic_method, dynamic_param, device, scale=1): + """Shared SVD extraction for both linear and conv weights. + Dynamic mode: single full SVD (need all singular values for rank selection). + Disabled mode: svd_lowrank only (rank is known, much faster for large matrices).""" + weight_2d = weight_2d.to(device) + if weight_2d.dtype != torch.float32: + weight_2d = weight_2d.float() + + if dynamic_method and dynamic_method != "disabled": + # Full SVD: we need all singular values for dynamic rank selection, + # and we reuse U/Vh directly — one SVD, no wasted work + U, S, Vh = torch.linalg.svd(weight_2d, full_matrices=False) + param_dict = rank_resize(S, lora_rank, dynamic_method, dynamic_param, scale) + lora_rank = param_dict["new_rank"] + U = U[:, :lora_rank] + S = S[:lora_rank] + Vh = Vh[:lora_rank, :] + else: + # Randomized lowrank SVD: only compute top-k, much faster when rank << min(m,n) + U, S, V = torch.svd_lowrank(weight_2d, q=lora_rank, niter=7) + Vh = V.t() + param_dict = {"new_rank": lora_rank, "new_alpha": float(scale * lora_rank)} + del V + + sqrt_S = torch.diag(torch.sqrt(S)) + U = U @ sqrt_S + Vh = sqrt_S @ Vh + + return U, Vh, param_dict + + +def extract_conv(weight, lora_rank, dynamic_method, dynamic_param, device, scale=1): + out_size, in_size, kernel_size, _ = weight.size() + + U, Vh, param_dict = _svd_extract( + weight.reshape(out_size, -1), lora_rank, dynamic_method, dynamic_param, device, scale + ) + lora_rank = param_dict["new_rank"] + + param_dict["lora_down"] = Vh.reshape(lora_rank, in_size, kernel_size, kernel_size).cpu() + param_dict["lora_up"] = U.reshape(out_size, lora_rank, 1, 1).cpu() + del U, Vh, weight + return param_dict + + +def extract_linear(weight, lora_rank, dynamic_method, dynamic_param, device, scale=1): + out_size, in_size = weight.size() + + U, Vh, param_dict = _svd_extract( + weight, lora_rank, dynamic_method, dynamic_param, device, scale + ) + lora_rank = param_dict["new_rank"] + + param_dict["lora_down"] = Vh.reshape(lora_rank, in_size).cpu() + param_dict["lora_up"] = U.reshape(out_size, lora_rank).cpu() + del U, Vh, weight + return param_dict + + +def merge_conv(lora_down, lora_up, device): + in_rank, in_size, kernel_size, k_ = lora_down.shape + out_size, out_rank, _, _ = lora_up.shape + assert in_rank == out_rank and kernel_size == k_, f"rank {in_rank} {out_rank} or kernel {kernel_size} {k_} mismatch" + + lora_down = lora_down.to(device) + lora_up = lora_up.to(device) + + merged = lora_up.reshape(out_size, -1) @ lora_down.reshape(in_rank, -1) + weight = merged.reshape(out_size, in_size, kernel_size, kernel_size) + del lora_up, lora_down + return weight + + +def merge_linear(lora_down, lora_up, device): + in_rank, in_size = lora_down.shape + out_size, out_rank = lora_up.shape + assert in_rank == out_rank, f"rank {in_rank} {out_rank} mismatch" + + lora_down = lora_down.to(device) + lora_up = lora_up.to(device) + + weight = lora_up @ lora_down + del lora_up, lora_down + return weight + + +def merge_conv3d(lora_down, lora_up, device): + in_rank, in_size, kD, kH, kW = lora_down.shape + out_size, out_rank, _, _, _ = lora_up.shape + assert in_rank == out_rank, f"rank {in_rank} {out_rank} mismatch" + + lora_down = lora_down.to(device) + lora_up = lora_up.to(device) + + merged = lora_up.reshape(out_size, -1) @ lora_down.reshape(in_rank, -1) + weight = merged.reshape(out_size, in_size, kD, kH, kW) + del lora_up, lora_down + return weight + + +def extract_conv3d(weight, lora_rank, dynamic_method, dynamic_param, device, scale=1): + out_size, in_size, kD, kH, kW = weight.size() + + U, Vh, param_dict = _svd_extract( + weight.reshape(out_size, -1), lora_rank, dynamic_method, dynamic_param, device, scale + ) + lora_rank = param_dict["new_rank"] + + param_dict["lora_down"] = Vh.reshape(lora_rank, in_size, kD, kH, kW).cpu() + param_dict["lora_up"] = U.reshape(out_size, lora_rank, 1, 1, 1).cpu() + del U, Vh, weight + return param_dict + + +# Calculate new rank + + +def rank_resize(S, rank, dynamic_method, dynamic_param, scale=1): + param_dict = {} + + if dynamic_method == "sv_ratio": + # Calculate new dim and alpha based off ratio + new_rank = index_sv_ratio(S, dynamic_param) + 1 + new_alpha = float(scale * new_rank) + + elif dynamic_method == "sv_cumulative": + # Calculate new dim and alpha based off cumulative sum + new_rank = index_sv_cumulative(S, dynamic_param) + 1 + new_alpha = float(scale * new_rank) + + elif dynamic_method == "sv_fro": + # Calculate new dim and alpha based off sqrt sum of squares + new_rank = index_sv_fro(S, dynamic_param) + 1 + new_alpha = float(scale * new_rank) + + elif dynamic_method == "sv_knee": + # Knee/elbow detection in singular value curve + new_rank = index_sv_knee(S, dynamic_param) + 1 + new_alpha = float(scale * new_rank) + else: + new_rank = rank + new_alpha = float(scale * new_rank) + + if S[0] <= MIN_SV: # Zero matrix, set dim to 1 + new_rank = 1 + new_alpha = float(scale * new_rank) + elif new_rank > rank: # cap max rank at rank + new_rank = rank + new_alpha = float(scale * new_rank) + + # Calculate resize info + s_sum = torch.sum(torch.abs(S)) + s_rank = torch.sum(torch.abs(S[:new_rank])) + + S_squared = S.pow(2) + s_fro = torch.sqrt(torch.sum(S_squared)) + s_red_fro = torch.sqrt(torch.sum(S_squared[:new_rank])) + fro_percent = float(s_red_fro / s_fro) + + param_dict["new_rank"] = new_rank + param_dict["new_alpha"] = new_alpha + param_dict["sum_retained"] = (s_rank) / s_sum + param_dict["fro_retained"] = fro_percent + param_dict["max_ratio"] = S[0] / S[new_rank - 1] + + return param_dict + + +def resize_lora_model(lora_sd, new_rank, save_dtype, device, dynamic_method, dynamic_param, verbose): + max_old_rank = None + new_alpha = None + verbose_str = "\n" + fro_list = [] + rank_list = [] + + if dynamic_method: + logging.info(f"Dynamically determining new alphas and dims based off {dynamic_method}: {dynamic_param}, max rank is {new_rank}") + + lora_down_weight = None + lora_up_weight = None + + o_lora_sd = lora_sd.copy() + block_down_name = None + block_up_name = None + + total_keys = len([k for k in lora_sd if k.endswith(".weight")]) + + pbar = comfy.utils.ProgressBar(total_keys) + for key, value in tqdm(lora_sd.items(), leave=True, desc="Resizing LoRA weights"): + key_parts = key.split(".") + block_down_name = None + for _format in LORA_DOWN_UP_FORMATS: + # Currently we only match lora_down_name in the last two parts of key + # because ("down", "up") are general words and may appear in block_down_name + if len(key_parts) >= 2 and _format[0] == key_parts[-2]: + block_down_name = ".".join(key_parts[:-2]) + lora_down_name = "." + _format[0] + lora_up_name = "." + _format[1] + weight_name = "." + key_parts[-1] + break + if len(key_parts) >= 1 and _format[0] == key_parts[-1]: + block_down_name = ".".join(key_parts[:-1]) + lora_down_name = "." + _format[0] + lora_up_name = "." + _format[1] + weight_name = "" + break + + if block_down_name is None: + # This parameter is not lora_down + continue + + # Now weight_name can be ".weight" or "" + # Find corresponding lora_up and alpha + block_up_name = block_down_name + lora_down_weight = value + lora_up_weight = lora_sd.get(block_up_name + lora_up_name + weight_name, None) + lora_alpha = lora_sd.get(block_down_name + ".alpha", None) + + weights_loaded = lora_down_weight is not None and lora_up_weight is not None + + if weights_loaded: + + conv2d = len(lora_down_weight.size()) == 4 + conv3d = len(lora_down_weight.size()) == 5 + old_rank = lora_down_weight.size()[0] + max_old_rank = max(max_old_rank or 0, old_rank) + + # Skip if merged weight would be too large (>100k elements in any dimension) + if conv2d: + in_rank, in_size, kernel_size, _ = lora_down_weight.shape + out_size, out_rank, _, _ = lora_up_weight.shape + merged_size = out_size * in_size * kernel_size * kernel_size + elif conv3d: + in_rank, in_size, kD, kH, kW = lora_down_weight.shape + out_size, out_rank, _, _, _ = lora_up_weight.shape + merged_size = out_size * in_size * kD * kH * kW + else: + in_rank, in_size = lora_down_weight.shape + out_size, out_rank = lora_up_weight.shape + merged_size = out_size * in_size + + if merged_size > 100_000_000: # Skip if >100M elements + logging.warning(f"Skipping {block_down_name}: merged weight too large ({merged_size:,} elements)") + tqdm.write(f"SKIPPED: {block_down_name} - too large ({merged_size:,} elements)") + pbar.update(1) + continue + + if lora_alpha is None: + scale = 1.0 + else: + scale = lora_alpha / old_rank + + if conv2d: + full_weight_matrix = merge_conv(lora_down_weight, lora_up_weight, device) + param_dict = extract_conv(full_weight_matrix, new_rank, dynamic_method, dynamic_param, device, scale) + elif conv3d: + full_weight_matrix = merge_conv3d(lora_down_weight, lora_up_weight, device) + param_dict = extract_conv3d(full_weight_matrix, new_rank, dynamic_method, dynamic_param, device, scale) + else: + full_weight_matrix = merge_linear(lora_down_weight, lora_up_weight, device) + param_dict = extract_linear(full_weight_matrix, new_rank, dynamic_method, dynamic_param, device, scale) + + if verbose and "fro_retained" in param_dict: + max_ratio = param_dict["max_ratio"] + sum_retained = param_dict["sum_retained"] + fro_retained = param_dict["fro_retained"] + if not np.isnan(fro_retained): + fro_list.append(float(fro_retained)) + log_str = f"{block_down_name:75} | sum(S) retained: {sum_retained:.1%}, fro retained: {fro_retained:.1%}, max(S) ratio: {max_ratio:0.1f}, new dim: {param_dict['new_rank']}" + tqdm.write(log_str) + verbose_str += log_str + + if verbose and dynamic_method: + verbose_str += f", dynamic | dim: {param_dict['new_rank']}, alpha: {param_dict['new_alpha']}\n" + else: + verbose_str += "\n" + + new_alpha = param_dict["new_alpha"] + o_lora_sd[block_down_name + lora_down_name + weight_name] = param_dict["lora_down"].to(save_dtype).contiguous() + o_lora_sd[block_up_name + lora_up_name + weight_name] = param_dict["lora_up"].to(save_dtype).contiguous() + o_lora_sd[block_down_name + ".alpha"] = torch.tensor(param_dict["new_alpha"]).to(save_dtype) + + block_down_name = None + block_up_name = None + lora_down_weight = None + lora_up_weight = None + weights_loaded = False + rank_list.append(param_dict["new_rank"]) + del param_dict + pbar.update(1) + + if verbose: + logging.info(f"Average Frobenius norm retention: {np.mean(fro_list):.2%} | std: {np.std(fro_list):0.3f}") + return o_lora_sd, max_old_rank, new_alpha, rank_list diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/ltxv_nodes.py b/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/ltxv_nodes.py new file mode 100644 index 0000000000000000000000000000000000000000..0d79521db3711a04f6e56f3fe27d3cc5c9b65724 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/ltxv_nodes.py @@ -0,0 +1,2324 @@ +from comfy_extras.nodes_lt import get_noise_mask, LTXVAddGuide, _append_guide_attention_entry +import types +import math +from typing import Tuple +import comfy +from comfy_api.latest import io +import numpy as np +import torch +import logging +import comfy.model_management as mm +import comfy.ldm.modules.attention as _comfy_attn +from comfy.ldm.lightricks.model import apply_rotary_emb as _apply_rope +try: + from comfy.ldm.lightricks.model import GuideAttentionMask as _GuideAttentionMask, _attention_with_guide_mask as _ltx_attn_with_guide_mask +except ImportError: + _GuideAttentionMask = None + _ltx_attn_with_guide_mask = None +device = mm.get_torch_device() +import latent_preview + +class LTXVAddGuideMulti(LTXVAddGuide): + + @classmethod + def define_schema(cls): + options = [] + for num_guides in range(1, 21): # 1 to 20 guides + guide_inputs = [] + for i in range(1, num_guides + 1): + guide_inputs.extend([ + io.Image.Input(f"image_{i}"), + io.Int.Input( + f"frame_idx_{i}", + default=0, + min=-9999, + max=9999, + tooltip=f"Frame index for guide {i}.", + ), + io.Float.Input(f"strength_{i}", default=1.0, min=0.0, max=10.0, step=0.01, tooltip=f"Strength for guide {i}."), + ]) + options.append(io.DynamicCombo.Option( + key=str(num_guides), + inputs=guide_inputs + )) + + return io.Schema( + node_id="LTXVAddGuideMulti", + category="KJNodes/ltxv", + description="Add multiple guide images at specified frame indices with strengths, uses DynamicCombo which requires ComfyUI 0.8.1 and frontend 1.33.4 or later.", + inputs=[ + io.Conditioning.Input("positive", tooltip="Positive conditioning to which guide keyframe info will be added"), + io.Conditioning.Input("negative", tooltip="Negative conditioning to which guide keyframe info will be added"), + io.Vae.Input("vae", tooltip="Video VAE used to encode the guide images"), + io.Latent.Input("latent", tooltip="Video latent, guides are added to the end of this latent"), + io.DynamicCombo.Input( + "num_guides", + options=options, + display_name="Number of Guides", + tooltip="Select how many guide images to use", + ), + ], + outputs=[ + io.Conditioning.Output(display_name="positive"), + io.Conditioning.Output(display_name="negative"), + io.Latent.Output(display_name="latent", tooltip="Video latent with added guides"), + ], + ) + + @classmethod + def execute(cls, positive, negative, vae, latent, num_guides) -> io.NodeOutput: + scale_factors = vae.downscale_index_formula + latent_image = latent["samples"] + noise_mask = get_noise_mask(latent) + + _, _, latent_length, latent_height, latent_width = latent_image.shape + + # num_guides is a dict containing the inputs from the selected option + # e.g., {'image_1': tensor, 'frame_idx_1': 0, 'strength_1': 1.0, 'image_2': tensor, 'frame_idx_2': 20, 'strength_2': 0.8, ...} + + image_keys = sorted([k for k in num_guides.keys() if k.startswith('image_')]) + + for img_key in image_keys: + i = img_key.split('_')[1] + + img = num_guides[f"image_{i}"] + f_idx = num_guides[f"frame_idx_{i}"] + strength = num_guides[f"strength_{i}"] + + image_1, t = cls.encode(vae, latent_width, latent_height, img, scale_factors) + + frame_idx, latent_idx = cls.get_latent_index(positive, latent_length, len(image_1), f_idx, scale_factors) + assert latent_idx + t.shape[2] <= latent_length, "Conditioning frames exceed the length of the latent sequence." + + positive, negative, latent_image, noise_mask = cls.append_keyframe( + positive, + negative, + frame_idx, + latent_image, + noise_mask, + t, + strength, + scale_factors, + ) + + # Track this guide for per-reference attention control. + pre_filter_count = t.shape[2] * t.shape[3] * t.shape[4] + guide_latent_shape = list(t.shape[2:]) # [F, H, W] + positive, negative = _append_guide_attention_entry(positive, negative, pre_filter_count, guide_latent_shape, strength=strength) + + return io.NodeOutput(positive, negative, {"samples": latent_image, "noise_mask": noise_mask}) + +class LTXVAddGuidesFromBatch(LTXVAddGuide): + + @classmethod + def define_schema(cls): + return io.Schema( + node_id="LTXVAddGuidesFromBatch", + category="conditioning/ltxv", + description="Adds multiple guide images from a batch to the latent at corresponding frame indices. Non-black images in the batch are used as guides.", + inputs=[ + io.Conditioning.Input("positive"), + io.Conditioning.Input("negative"), + io.Vae.Input("vae"), + io.Latent.Input("latent"), + io.Image.Input("images", tooltip="Batch of images - non-black images will be used as guides"), + io.Float.Input("strength", default=1.0, min=0.0, max=10.0, step=0.01, tooltip="Strength for all guides."), + ], + outputs=[ + io.Conditioning.Output(display_name="positive"), + io.Conditioning.Output(display_name="negative"), + io.Latent.Output(display_name="latent"), + ], + ) + + @classmethod + def execute(cls, positive, negative, vae, latent, images, strength) -> io.NodeOutput: + scale_factors = vae.downscale_index_formula + latent_image = latent["samples"] + noise_mask = get_noise_mask(latent) + + _, _, latent_length, latent_height, latent_width = latent_image.shape + + # Process each image in the batch + batch_size = images.shape[0] + + for i in range(batch_size): + img = images[i:i+1] + + # Check if image is not black and use batch index as frame index + if img.max() > 0.001: + f_idx = i + + image_1, t = cls.encode(vae, latent_width, latent_height, img, scale_factors) + + frame_idx, latent_idx = cls.get_latent_index(positive, latent_length, len(image_1), f_idx, scale_factors) + + if latent_idx + t.shape[2] <= latent_length: + positive, negative, latent_image, noise_mask = cls.append_keyframe( + positive, + negative, + frame_idx, + latent_image, + noise_mask, + t, + strength, + scale_factors, + ) + + # Track this guide for per-reference attention control. + pre_filter_count = t.shape[2] * t.shape[3] * t.shape[4] + guide_latent_shape = list(t.shape[2:]) # [F, H, W] + positive, negative = _append_guide_attention_entry(positive, negative, pre_filter_count, guide_latent_shape, strength=strength) + else: + print(f"Warning: Skipping guide at index {i} - conditioning frames exceed latent sequence length") + + return io.NodeOutput(positive, negative, {"samples": latent_image, "noise_mask": noise_mask}) + + +class LTXVAudioVideoMask(io.ComfyNode): + + @classmethod + def define_schema(cls): + return io.Schema( + node_id="LTXVAudioVideoMask", + category="KJNodes/ltxv", + description="Creates noise masks for video and audio latents based on specified time ranges. New content is generated within these masked regions", + inputs=[ + io.Latent.Input("video_latent", optional=True), + io.Latent.Input("audio_latent", optional=True), + io.Float.Input("video_fps", default=25, min=0.0, max=100.0, step=0.01), + io.Float.Input("video_start_time", default=0.0, min=0.0, max=10000.0, step=0.01, tooltip="Start time in seconds for the video mask."), + io.Float.Input("video_end_time", default=5.0, min=0.0, max=10000.0, step=0.01, tooltip="End time in seconds for the video mask."), + io.Float.Input("audio_start_time", default=0.0, min=0.0, max=10000.0, step=0.01, tooltip="Start time in seconds for the audio mask."), + io.Float.Input("audio_end_time", default=5.0, min=0.0, max=10000.0, step=0.01, tooltip="End time in seconds for the audio mask."), + io.Combo.Input( + "max_length", + options=["truncate", "pad", "partial"], + default="truncate", + tooltip="'truncate': cut latent to end_time length. 'pad': extend latent to end_time. 'partial': mask range within existing latent.", + ), + io.Combo.Input("existing_mask_mode", options=["add", "subtract", "overwrite"], optional=True, default="add", tooltip="How to combine with existing noise masks if present. 'add' will take the max of existing and new mask, 'overwrite' will replace with new mask. 'subtract' will set the masked region to 0 instead of 1, effectively unmasking it."), + ], + outputs=[ + io.Latent.Output(display_name="video_latent"), + io.Latent.Output(display_name="audio_latent"), + ], + ) + + @classmethod + def execute(cls, video_fps, video_start_time, video_end_time, audio_start_time, audio_end_time, max_length="truncate", existing_mask_mode="add", video_latent=None, audio_latent=None,) -> io.NodeOutput: + + time_scale_factor = 8 + mel_hop_length = 160 + sampling_rate = 16000 + latent_downsample_factor = 4 + audio_latents_per_second = (sampling_rate / mel_hop_length / latent_downsample_factor) # 25 + + if video_latent is not None: + video_latent_frame_count = video_latent["samples"].shape[2] + + video_pixel_frame_start_raw = int(round(video_start_time * video_fps)) + video_pixel_frame_end_raw = int(round(video_end_time * video_fps)) + + # Calculate required latent frames based on end time + required_latent_frames = (video_pixel_frame_end_raw - 1) // time_scale_factor + 1 + + # Handle different max_length modes + if max_length == "pad" and required_latent_frames > video_latent_frame_count: + # Pad video latent if required frames exceed current length + pad_frames = required_latent_frames - video_latent_frame_count + padding = torch.zeros( + video_latent["samples"].shape[0], + video_latent["samples"].shape[1], + pad_frames, + video_latent["samples"].shape[3], + video_latent["samples"].shape[4], + dtype=video_latent["samples"].dtype, + device=video_latent["samples"].device + ) + video_samples = torch.cat([video_latent["samples"], padding], dim=2) + video_latent_frame_count = video_samples.shape[2] + elif max_length == "truncate": + # Truncate to the end_time + video_samples = video_latent["samples"][:, :, :required_latent_frames] + video_latent_frame_count = video_samples.shape[2] + else: # partial + video_samples = video_latent["samples"] + + # Now calculate indices based on potentially padded latent + video_pixel_frame_count = (video_latent_frame_count - 1) * time_scale_factor + 1 + xp = np.array([0] + list(range(1, video_pixel_frame_count + time_scale_factor, time_scale_factor))) + + # video_frame_index_start = index of the value in xp rounding up + video_latent_frame_index_start = np.searchsorted(xp, video_pixel_frame_start_raw, side="left") + # video_frame_index_end = index of the value in xp rounding down + video_latent_frame_index_end = np.searchsorted(xp, video_pixel_frame_end_raw, side="right") - 1 + + video_latent_frame_index_start = max(0, video_latent_frame_index_start) + video_latent_frame_index_end = min(video_latent_frame_index_end, video_latent_frame_count) + + # Get existing noise mask if present, otherwise create new one + if "noise_mask" in video_latent and video_latent["noise_mask"] is not None and existing_mask_mode != "overwrite": + video_mask = video_latent["noise_mask"].clone() + # Adjust mask size based on mode + if max_length == "pad" and video_samples.shape[2] > video_latent["samples"].shape[2]: + # Pad the mask if we padded the samples + mask_padding = torch.zeros( + video_mask.shape[0], + video_mask.shape[1], + video_samples.shape[2] - video_mask.shape[2], + video_mask.shape[3], + video_mask.shape[4], + dtype=video_mask.dtype, + device=video_mask.device + ) + video_mask = torch.cat([video_mask, mask_padding], dim=2) + elif max_length == "truncate": + # Truncate the mask to match truncated samples + video_mask = video_mask[:, :, :video_samples.shape[2]] + else: + video_mask = torch.zeros_like(video_samples)[:, :1] + + video_mask[:, :, video_latent_frame_index_start:video_latent_frame_index_end] = 1.0 if existing_mask_mode != "subtract" else 0.0 + # ensure all padded frames are also masked + if max_length == "pad" and video_samples.shape[2] > video_latent["samples"].shape[2]: + video_mask[:, :, video_latent["samples"].shape[2]:] = 1.0 if existing_mask_mode != "subtract" else 0.0 + video_latent = video_latent.copy() + video_latent["samples"] = video_samples + video_latent["noise_mask"] = video_mask + + if audio_latent is not None: + audio_latent_frame_count = audio_latent["samples"].shape[2] + + audio_latent_frame_index_start = int(round(audio_start_time * audio_latents_per_second)) + audio_latent_frame_index_end = int(round(audio_end_time * audio_latents_per_second)) + 1 + + # Handle different max_length modes + if max_length == "pad" and audio_latent_frame_index_end > audio_latent_frame_count: + # Pad audio latent if end index exceeds current length + pad_frames = audio_latent_frame_index_end - audio_latent_frame_count + padding = torch.zeros( + audio_latent["samples"].shape[0], + audio_latent["samples"].shape[1], + pad_frames, + audio_latent["samples"].shape[3], + dtype=audio_latent["samples"].dtype, + device=audio_latent["samples"].device + ) + audio_samples = torch.cat([audio_latent["samples"], padding], dim=2) + audio_latent_frame_count = audio_samples.shape[2] + elif max_length == "truncate": + # Truncate to the end_time + audio_samples = audio_latent["samples"][:, :, :audio_latent_frame_index_end] + audio_latent_frame_count = audio_samples.shape[2] + else: # partial + audio_samples = audio_latent["samples"] + + audio_latent_frame_index_start = max(0, audio_latent_frame_index_start) + audio_latent_frame_index_end = min(audio_latent_frame_index_end, audio_latent_frame_count) + + # Get existing noise mask if present, otherwise create new one + if "noise_mask" in audio_latent and audio_latent["noise_mask"] is not None and existing_mask_mode != "overwrite": + audio_mask = audio_latent["noise_mask"].clone() + # Adjust mask size based on mode + if max_length == "pad" and audio_samples.shape[2] > audio_latent["samples"].shape[2]: + # Pad the mask if we padded the samples + mask_padding = torch.zeros( + audio_mask.shape[0], + audio_mask.shape[1], + audio_samples.shape[2] - audio_mask.shape[2], + audio_mask.shape[3], + dtype=audio_mask.dtype, + device=audio_mask.device + ) + audio_mask = torch.cat([audio_mask, mask_padding], dim=2) + elif max_length == "truncate": + # Truncate the mask to match truncated samples + audio_mask = audio_mask[:, :, :audio_samples.shape[2]] + else: + audio_mask = torch.zeros_like(audio_samples) + + audio_mask[:, :, audio_latent_frame_index_start:audio_latent_frame_index_end] = 1.0 + # ensure all padded frames are also masked + if max_length == "pad" and audio_samples.shape[2] > audio_latent["samples"].shape[2]: + audio_mask[:, :, audio_latent["samples"].shape[2]:] = 1.0 + audio_latent = audio_latent.copy() + audio_latent["samples"] = audio_samples + audio_latent["noise_mask"] = audio_mask + + return io.NodeOutput(video_latent, audio_latent) + +def _compute_attention(self, query, context, attn_precision=None, transformer_options={}, mask=None): + """Compute attention and return the result. Cleans up intermediate tensors. + + When `mask` is non-None, bypass wrap_attn and call attention_pytorch directly — + sage and similar backends drop arbitrary masks. + """ + k = self.k_norm(self.to_k(context)).to(query.dtype) + v = self.to_v(context).to(query.dtype) + if mask is None: + x = comfy.ldm.modules.attention.optimized_attention(query, k, v, heads=self.heads, attn_precision=attn_precision, transformer_options=transformer_options).flatten(2) + else: + x = comfy.ldm.modules.attention.attention_pytorch(query, k, v, heads=self.heads, mask=mask, attn_precision=attn_precision, _inside_attn_wrapper=True, transformer_options=transformer_options).flatten(2) + del k, v + return x + +def nag_attention(self, query, context_positive, nag_context, attn_precision=None, transformer_options={}, mask=None): + # mask (e.g. PromptRelay's temporal routing) only applies to the real-context, not NAG context + x_positive = _compute_attention(self, query, context_positive, attn_precision, transformer_options, mask=mask) + x_negative = _compute_attention(self, query, nag_context, attn_precision, transformer_options) + return x_positive, x_negative + +def normalized_attention_guidance(self, x_positive, x_negative): + if self.inplace: + nag_guidance = x_negative.mul_(self.nag_scale - 1).neg_().add_(x_positive, alpha=self.nag_scale) + del x_negative + else: + nag_guidance = x_negative * (self.nag_scale - 1) + del x_negative + nag_guidance = (x_positive * self.nag_scale).sub_(nag_guidance) + + norm_positive = torch.norm(x_positive, p=1, dim=-1, keepdim=True) + norm_guidance = torch.norm(nag_guidance, p=1, dim=-1, keepdim=True) + + scale = norm_guidance / norm_positive + torch.nan_to_num_(scale, nan=10.0) + mask = scale > self.nag_tau + del scale + + adjustment = (norm_positive * self.nag_tau) / (norm_guidance + 1e-7) + del norm_positive, norm_guidance + + nag_guidance.mul_(torch.where(mask, adjustment, 1.0)) + del mask, adjustment + + if self.inplace: + return nag_guidance.sub_(x_positive).mul_(self.nag_alpha).add_(x_positive) + else: + nag_guidance.mul_(self.nag_alpha) + return nag_guidance.add_(x_positive * (1 - self.nag_alpha)) + +#region NAG +def ltxv_crossattn_forward_nag(self, x, context, mask=None, transformer_options={}, **kwargs): + + if mask is None: + mask_provider = transformer_options.get("promptrelay_mask_fn") + if mask_provider is not None: + mask = mask_provider(x.shape[1], context.shape[1], x.dtype, x.device, transformer_options) + + # Single or [pos, neg] pair + if context.shape[0] == 1: + x_pos, context_pos = x, context + x_neg, context_neg = None, None + else: + x_pos, x_neg = torch.chunk(x, 2, dim=0) + context_pos, context_neg = torch.chunk(context, 2, dim=0) + + # Positive + q_pos = self.q_norm(self.to_q(x_pos)) + del x_pos + + x_positive, x_negative = nag_attention(self, q_pos, context_pos, self.nag_context, attn_precision=self.attn_precision, transformer_options=transformer_options, mask=mask) + del context_pos, q_pos + + x_pos_out = normalized_attention_guidance(self, x_positive, x_negative) + del x_positive, x_negative + + # Negative + if x_neg is not None and context_neg is not None: + q_neg = self.q_norm(self.to_q(x_neg)) + k_neg = self.k_norm(self.to_k(context_neg)) + v_neg = self.to_v(context_neg) + + x_neg_out = comfy.ldm.modules.attention.optimized_attention(q_neg, k_neg, v_neg, heads=self.heads, attn_precision=self.attn_precision, transformer_options=transformer_options) + out = torch.cat([x_pos_out, x_neg_out], dim=0) + else: + out = x_pos_out + + if self.to_gate_logits is not None: + gate_logits = self.to_gate_logits(x) # (B, T, H) + b, t, _ = out.shape + out = out.view(b, t, self.heads, self.dim_head) + gates = 2.0 * torch.sigmoid(gate_logits) # zero-init -> identity + out = out * gates.unsqueeze(-1) + out = out.view(b, t, self.heads * self.dim_head) + + return self.to_out(out) + + +class LTXVCrossAttentionPatch: + def __init__(self, context, nag_scale, nag_alpha, nag_tau, inplace=True): + self.nag_context = context + self.nag_scale = nag_scale + self.nag_alpha = nag_alpha + self.nag_tau = nag_tau + self.inplace = inplace + + def __get__(self, obj, objtype=None): + # Create bound method with stored parameters + def wrapped_attention(self_module, *args, **kwargs): + self_module.nag_context = self.nag_context + self_module.nag_scale = self.nag_scale + self_module.nag_alpha = self.nag_alpha + self_module.nag_tau = self.nag_tau + self_module.inplace = self.inplace + + return ltxv_crossattn_forward_nag(self_module, *args, **kwargs) + return types.MethodType(wrapped_attention, obj) + +class LTX2_NAG(io.ComfyNode): + + @classmethod + def define_schema(cls): + return io.Schema( + node_id="LTX2_NAG", + display_name="LTX2 NAG", + category="KJNodes/ltxv", + description="https://github.com/ChenDarYen/Normalized-Attention-Guidance", + is_experimental=True, + inputs=[ + io.Model.Input("model"), + io.Float.Input("nag_scale", default=11.0, min=0.0, max=100.0, step=0.001, tooltip="Strength of negative guidance effect"), + io.Float.Input("nag_alpha", default=0.25, min=0.0, max=1.0, step=0.001, tooltip="Mixing coefficient in that controls the balance between the normalized guided representation and the original positive representation."), + io.Float.Input("nag_tau", default=2.5, min=0.0, max=10.0, step=0.001, tooltip="Clipping threshold that controls how much the guided attention can deviate from the positive attention."), + io.Conditioning.Input("nag_cond_video", optional=True), + io.Conditioning.Input("nag_cond_audio", optional=True), + io.Boolean.Input("inplace", default=True, optional=True, tooltip="If true, modifies tensors in place to save memory. Leads to different numerical results which may change the output slightly."), + ], + outputs=[ + io.Model.Output(display_name="model"), + ], + ) + + @classmethod + def execute(cls, model, nag_scale, nag_alpha, nag_tau, nag_cond_video=None, nag_cond_audio=None, inplace=True) -> io.NodeOutput: + if nag_scale == 0: + return io.NodeOutput(model) + + device = mm.get_torch_device() + offload_device = mm.unet_offload_device() + dtype = model.model.manual_cast_dtype + if dtype is None: + dtype = model.model.diffusion_model.dtype + + model_clone = model.clone() + + diffusion_model = model_clone.get_model_object("diffusion_model") + img_dim = diffusion_model.inner_dim + audio_dim = diffusion_model.audio_inner_dim + + context_video = context_audio = None + + if nag_cond_video is not None: + context_video = nag_cond_video[0][0].to(device, dtype) + vid_split = getattr(diffusion_model, "cross_attention_dim", None) + if vid_split is not None and context_video.shape[-1] == vid_split + diffusion_model.audio_cross_attention_dim: + context_video = context_video[:, :, :vid_split] + if diffusion_model.caption_proj_before_connector and diffusion_model.caption_projection_first_linear: + diffusion_model.caption_projection.to(device) + context_video = diffusion_model.caption_projection(context_video) + diffusion_model.caption_projection.to(offload_device) + if hasattr(diffusion_model, "video_embeddings_connector"): + diffusion_model.video_embeddings_connector.to(device) + context_video = diffusion_model.video_embeddings_connector(context_video)[0] + diffusion_model.video_embeddings_connector.to(offload_device) + context_video = context_video.view(1, -1, img_dim) + for idx, block in enumerate(diffusion_model.transformer_blocks): + patched_attn2 = LTXVCrossAttentionPatch(context_video, nag_scale, nag_alpha, nag_tau, inplace=inplace).__get__(block.attn2, block.__class__) + model_clone.add_object_patch(f"diffusion_model.transformer_blocks.{idx}.attn2.forward", patched_attn2) + + if nag_cond_audio is not None and diffusion_model.audio_caption_projection is not None: + context_audio = nag_cond_audio[0][0].to(device, dtype) + vid_split = getattr(diffusion_model, "cross_attention_dim", None) + if vid_split is not None and context_audio.shape[-1] == vid_split + diffusion_model.audio_cross_attention_dim: + context_audio = context_audio[:, :, vid_split:] + if diffusion_model.caption_proj_before_connector and diffusion_model.caption_projection_first_linear: + diffusion_model.audio_caption_projection.to(device) + context_audio = diffusion_model.audio_caption_projection(context_audio) + diffusion_model.audio_caption_projection.to(offload_device) + if hasattr(diffusion_model, "audio_embeddings_connector"): + diffusion_model.audio_embeddings_connector.to(device) + context_audio = diffusion_model.audio_embeddings_connector(context_audio)[0] + diffusion_model.audio_embeddings_connector.to(offload_device) + context_audio = context_audio.view(1, -1, audio_dim) + for idx, block in enumerate(diffusion_model.transformer_blocks): + patched_audio_attn2 = LTXVCrossAttentionPatch(context_audio, nag_scale, nag_alpha, nag_tau, inplace=inplace).__get__(block.audio_attn2, block.__class__) + model_clone.add_object_patch(f"diffusion_model.transformer_blocks.{idx}.audio_attn2.forward", patched_audio_attn2) + + return io.NodeOutput(model_clone) + + +def ffn_chunked_forward(self, x): + if x.shape[1] > self.dim_threshold: + chunk_size = x.shape[1] // self.num_chunks + for i in range(self.num_chunks): + start_idx = i * chunk_size + end_idx = (i + 1) * chunk_size if i < self.num_chunks - 1 else x.shape[1] + x[:, start_idx:end_idx] = self.net(x[:, start_idx:end_idx]) + return x + else: + return self.net(x) + +class LTXVffnChunkPatch: + def __init__(self, num_chunks, dim_threshold=4096): + self.num_chunks = num_chunks + self.dim_threshold = dim_threshold + + def __get__(self, obj, objtype=None): + def wrapped_forward(self_module, *args, **kwargs): + self_module.num_chunks = self.num_chunks + self_module.dim_threshold = self.dim_threshold + return ffn_chunked_forward(self_module, *args, **kwargs) + return types.MethodType(wrapped_forward, obj) + +class LTXVChunkFeedForward(io.ComfyNode): + + @classmethod + def define_schema(cls): + return io.Schema( + node_id="LTXVChunkFeedForward", + display_name="LTXV Chunk FeedForward", + category="KJNodes/ltxv", + description="EXPERIMENTAL AND MAY CHANGE THE MODEL OUTPUT!! Chunks feedforward activations to reduce peak VRAM usage.", + is_experimental=True, + inputs=[ + io.Model.Input("model"), + io.Int.Input("chunks", default=2, min=1, max=100, step=1, tooltip="Number of chunks to split the feedforward activations into to reduce peak VRAM usage."), + io.Int.Input("dim_threshold", default=4096, min=0, max=16384, step=256, tooltip="Dimension threshold above which to apply chunking."), + ], + outputs=[ + io.Model.Output(display_name="model"), + ], + ) + + @classmethod + def execute(cls, model, chunks, dim_threshold) -> io.NodeOutput: + if chunks == 1: + return io.NodeOutput(model) + + model_clone = model.clone() + diffusion_model = model_clone.get_model_object("diffusion_model") + + for idx, block in enumerate(diffusion_model.transformer_blocks): + patched_attn2 = LTXVffnChunkPatch(chunks, dim_threshold).__get__(block.ff, block.__class__) + model_clone.add_object_patch(f"diffusion_model.transformer_blocks.{idx}.ff.forward", patched_attn2) + + return io.NodeOutput(model_clone) + + + +#borrowed VideoHelperSuite https://github.com/Kosinkadink/ComfyUI-VideoHelperSuite/blob/main/videohelpersuite/latent_preview.py +import server +from threading import Thread +import torch.nn.functional as F +import time +import struct +from PIL import Image +from io import BytesIO +serv = server.PromptServer.instance + +class WrappedPreviewer(): + def __init__(self, latent_rgb_factors, latent_rgb_factors_bias, rate=8, taeltx=None): + self.first_preview = True + self.taeltx = taeltx + self.last_time = 0 + self.c_index = 0 + self.rate = rate + self.latent_rgb_factors = torch.tensor(latent_rgb_factors, device="cpu").transpose(0, 1) + self.latent_rgb_factors_bias = torch.tensor(latent_rgb_factors_bias, device="cpu") if latent_rgb_factors_bias is not None else None + + def decode_latent_to_preview_image(self, preview_format, x0): + if x0.ndim == 5: + #Keep batch major + x0 = x0.movedim(2,1) + x0 = x0.reshape((-1,)+x0.shape[-3:]) + num_images = x0.size(0) + new_time = time.time() + num_previews = int((new_time - self.last_time) * self.rate) + self.last_time = self.last_time + num_previews/self.rate + if num_previews > num_images: + num_previews = num_images + elif num_previews <= 0: + return None + if self.first_preview: + self.first_preview = False + serv.send_sync('VHS_latentpreview', {'length':num_images, 'rate': self.rate, 'id': serv.last_node_id}) + self.last_time = new_time + 1/self.rate + if self.c_index + num_previews > num_images: + x0 = x0.roll(-self.c_index, 0)[:num_previews] + else: + x0 = x0[self.c_index:self.c_index + num_previews] + Thread(target=self.process_previews, args=(x0, self.c_index, + num_images)).run() + self.c_index = (self.c_index + num_previews) % num_images + return None + def process_previews(self, image_tensor, ind, leng): + max_size = 512 + min_size = 256 + image_tensor = self.decode_latent_to_preview(image_tensor) + + if image_tensor.size(1) < min_size or image_tensor.size(2) < min_size: + image_tensor = F.interpolate(image_tensor.movedim(-1,0), scale_factor=4, mode='nearest').movedim(0,-1) + + if image_tensor.size(1) > max_size or image_tensor.size(2) > max_size: + image_tensor = image_tensor.movedim(-1,0) + if image_tensor.size(2) < image_tensor.size(3): + height = (max_size * image_tensor.size(2)) // image_tensor.size(3) + image_tensor = F.interpolate(image_tensor, (height,max_size), mode='nearest') + else: + width = (max_size * image_tensor.size(3)) // image_tensor.size(2) + image_tensor = F.interpolate(image_tensor, (max_size, width), mode='nearest') + image_tensor = image_tensor.movedim(0,-1) + + previews_ubyte = (image_tensor.clamp(0, 1).mul(0xFF)).to(device="cpu", dtype=torch.uint8) + + # Send VHS preview + for preview in previews_ubyte: + i = Image.fromarray(preview.numpy()) + message = BytesIO() + message.write((1).to_bytes(length=4, byteorder='big')*2) + message.write(ind.to_bytes(length=4, byteorder='big')) + message.write(struct.pack('16p', (serv.last_node_id or "").encode('ascii'))) + i.save(message, format="JPEG", quality=95, compress_level=1) + #NOTE: send sync already uses call_soon_threadsafe + serv.send_sync(server.BinaryEventTypes.PREVIEW_IMAGE, + message.getvalue(), serv.client_id) + if self.taeltx is not None: + ind = (ind + 1) % ((leng-1) * 8 + 1) + else: + ind = (ind + 1) % leng + + def decode_latent_to_preview(self, x0): + if self.taeltx is not None: + x0 = x0.unsqueeze(0).to(dtype=self.taeltx.first_stage_model.decoder[1].weight.dtype, device=device) + x_sample = self.taeltx.first_stage_model.decode(x0)[0].permute(1, 2, 3, 0) + return x_sample + else: + self.latent_rgb_factors = self.latent_rgb_factors.to(dtype=x0.dtype, device=x0.device) + if self.latent_rgb_factors_bias is not None: + self.latent_rgb_factors_bias = self.latent_rgb_factors_bias.to(dtype=x0.dtype, device=x0.device) + latent_image = F.linear(x0.movedim(1, -1), self.latent_rgb_factors, + bias=self.latent_rgb_factors_bias) + + #low = latent_image.quantile(0.01) + #high = latent_image.quantile(0.99) + #latent_image = ((latent_image - low) / (high - low)).clamp(0, 1) + latent_image = torch.sigmoid(latent_image) + return latent_image + + +def get_ltx_rgb_factors(is_23): + """Return (latent_rgb_factors, latent_rgb_factors_bias) for LTX (is_23=False) or + LTX2/LTXAV (is_23=True). Exposed so other nodes can build a WrappedPreviewer with + the right LTX factors without duplicating the 128-row tables.""" + return _get_ltx_rgb_factors_impl(is_23) + + +def _get_ltx_rgb_factors_impl(is_23): + if not is_23: + latent_rgb_factors = [ + [ 0.0350, 0.0159, 0.0132], + [ 0.0025, -0.0021, -0.0003], + [ 0.0286, 0.0028, 0.0020], + [ 0.0280, -0.0114, -0.0202], + [-0.0186, 0.0073, 0.0092], + [ 0.0027, 0.0097, -0.0113], + [-0.0069, -0.0032, -0.0024], + [-0.0323, -0.0370, -0.0457], + [ 0.0174, 0.0164, 0.0106], + [-0.0097, 0.0061, 0.0035], + [-0.0130, -0.0042, -0.0012], + [-0.0102, -0.0002, -0.0091], + [-0.0025, 0.0063, 0.0161], + [ 0.0003, 0.0037, 0.0108], + [ 0.0152, 0.0082, 0.0143], + [ 0.0317, 0.0203, 0.0312], + [-0.0092, -0.0233, -0.0119], + [-0.0405, -0.0226, -0.0023], + [ 0.0376, 0.0397, 0.0352], + [ 0.0171, -0.0043, -0.0095], + [ 0.0482, 0.0341, 0.0213], + [ 0.0031, -0.0046, -0.0018], + [-0.0486, -0.0383, -0.0294], + [-0.0071, -0.0272, -0.0123], + [ 0.0320, 0.0218, 0.0289], + [ 0.0327, 0.0088, -0.0116], + [-0.0098, -0.0240, -0.0111], + [ 0.0094, -0.0116, 0.0021], + [ 0.0309, 0.0092, 0.0165], + [-0.0065, -0.0077, -0.0107], + [ 0.0179, 0.0114, 0.0038], + [-0.0018, -0.0030, -0.0026], + [-0.0002, 0.0076, -0.0029], + [-0.0131, -0.0059, -0.0170], + [ 0.0055, 0.0066, -0.0038], + [ 0.0154, 0.0063, 0.0090], + [ 0.0186, 0.0175, 0.0188], + [-0.0166, -0.0381, -0.0428], + [ 0.0121, 0.0015, -0.0153], + [ 0.0118, 0.0050, 0.0019], + [ 0.0125, 0.0259, 0.0231], + [ 0.0046, 0.0130, 0.0081], + [ 0.0271, 0.0250, 0.0250], + [-0.0054, -0.0347, -0.0326], + [-0.0438, -0.0262, -0.0228], + [-0.0191, -0.0256, -0.0173], + [-0.0205, -0.0058, 0.0042], + [ 0.0404, 0.0434, 0.0346], + [-0.0242, -0.0177, -0.0146], + [ 0.0161, 0.0223, 0.0168], + [-0.0240, -0.0320, -0.0299], + [-0.0019, 0.0043, 0.0008], + [-0.0060, -0.0133, -0.0244], + [-0.0048, -0.0225, -0.0167], + [ 0.0267, 0.0133, 0.0152], + [ 0.0222, 0.0167, 0.0028], + [ 0.0015, -0.0062, 0.0013], + [-0.0241, -0.0178, -0.0079], + [ 0.0040, -0.0081, -0.0097], + [-0.0064, 0.0133, -0.0011], + [-0.0204, -0.0231, -0.0304], + [ 0.0011, -0.0011, 0.0145], + [-0.0283, -0.0259, -0.0260], + [ 0.0038, 0.0171, -0.0029], + [ 0.0637, 0.0424, 0.0409], + [ 0.0092, 0.0163, 0.0188], + [ 0.0082, 0.0055, -0.0179], + [-0.0177, -0.0286, -0.0147], + [ 0.0171, 0.0242, 0.0398], + [-0.0129, 0.0095, -0.0071], + [-0.0154, 0.0036, 0.0128], + [-0.0081, -0.0009, 0.0118], + [-0.0067, -0.0178, -0.0230], + [-0.0022, -0.0125, -0.0003], + [-0.0032, -0.0039, -0.0022], + [-0.0005, -0.0127, -0.0131], + [-0.0143, -0.0157, -0.0165], + [-0.0262, -0.0263, -0.0270], + [ 0.0063, 0.0127, 0.0178], + [ 0.0092, 0.0133, 0.0150], + [-0.0106, -0.0068, 0.0032], + [-0.0214, -0.0022, 0.0171], + [-0.0104, -0.0266, -0.0362], + [ 0.0021, 0.0048, -0.0005], + [ 0.0345, 0.0431, 0.0402], + [-0.0275, -0.0110, -0.0195], + [ 0.0203, 0.0251, 0.0224], + [ 0.0016, -0.0037, -0.0094], + [ 0.0241, 0.0198, 0.0114], + [-0.0003, 0.0027, 0.0141], + [ 0.0012, -0.0052, -0.0084], + [ 0.0057, -0.0028, -0.0163], + [-0.0488, -0.0545, -0.0509], + [-0.0076, -0.0025, -0.0014], + [-0.0249, -0.0142, -0.0367], + [ 0.0136, 0.0041, 0.0135], + [ 0.0007, 0.0034, -0.0053], + [-0.0068, -0.0109, 0.0029], + [ 0.0006, -0.0237, -0.0094], + [-0.0149, -0.0177, -0.0131], + [-0.0105, 0.0039, 0.0216], + [ 0.0242, 0.0200, 0.0180], + [-0.0339, -0.0153, -0.0195], + [ 0.0104, 0.0151, 0.0120], + [-0.0043, 0.0089, 0.0047], + [ 0.0157, -0.0030, 0.0008], + [ 0.0126, 0.0102, -0.0040], + [ 0.0040, 0.0114, 0.0137], + [ 0.0423, 0.0473, 0.0436], + [-0.0128, -0.0066, -0.0152], + [-0.0337, -0.0087, -0.0026], + [-0.0052, 0.0235, 0.0291], + [ 0.0079, 0.0154, 0.0260], + [-0.0539, -0.0377, -0.0358], + [-0.0188, 0.0062, -0.0035], + [-0.0186, 0.0041, -0.0083], + [ 0.0045, -0.0049, 0.0053], + [ 0.0172, 0.0071, 0.0042], + [-0.0003, -0.0078, -0.0096], + [-0.0209, -0.0132, -0.0135], + [-0.0074, 0.0017, 0.0099], + [-0.0038, 0.0070, 0.0014], + [-0.0013, -0.0017, 0.0073], + [ 0.0030, 0.0105, 0.0105], + [ 0.0154, -0.0168, -0.0235], + [-0.0108, -0.0038, 0.0047], + [-0.0298, -0.0347, -0.0436], + [-0.0206, -0.0189, -0.0139] + ] + latent_rgb_factors_bias = [0.2796, 0.1101, -0.0047] + else: + latent_rgb_factors = [[0.002269406570121646, -0.02110900916159153, -0.009850316680967808], [-0.016038373112678528, -0.012462412007153034, -0.01112896017730236], [0.025274179875850677, 0.011209743097424507, 0.025426799431443214], [0.04690725728869438, 0.041542328894138336, 0.03568895906209946], [-0.02388044260442257, -0.0018645941745489836, 0.01858334057033062], [0.03720448538661003, 0.0220357533544302, 0.027937663719058037], [-0.07273884862661362, -0.09326262027025223, -0.11579664051532745], [-0.063837431371212, 0.00026216846890747547, 0.03042735904455185], [0.02903873845934868, 0.042082373052835464, 0.030649805441498756], [0.03777873516082764, 0.0322984978556633, -0.005671461578458548], [-0.0075670829974114895, -0.012113905511796474, -0.01638956367969513], [0.026524530723690987, 0.060518112033605576, 0.059549521654844284], [0.10093028098344803, 0.10073262453079224, 0.0505094900727272], [0.03725508227944374, 0.015382086858153343, 0.005786076188087463], [-0.03139607608318329, -0.01690264232456684, -0.0013519978383556008], [-0.027200624346733093, -0.02517341822385788, -0.008874989114701748], [0.024963486939668655, 0.04293748363852501, 0.05582639202475548], [-0.0364827960729599, -0.026975594460964203, -0.021950015798211098], [0.027655167505145073, 0.025136707350611687, 0.043967027217149734], [0.035822272300720215, 0.013104500249028206, 0.01113432738929987], [0.05353763327002525, 0.013606574386358261, -0.018720127642154694], [-0.013587888330221176, -0.01689346879720688, -0.027842802926898003], [0.059415675699710846, 0.03734271228313446, 0.04562298208475113], [-0.02946414425969124, -0.038338612765073776, 0.001805233070626855], [0.03921474143862724, 0.0651894062757492, 0.10681862384080887], [-0.00744189927354455, 0.007951526902616024, 0.020728807896375656], [-0.04038553684949875, -0.05215264856815338, -0.07213657349348068], [-0.004655141849070787, 0.01305423304438591, 0.026104029268026352], [0.03434251993894577, 0.018448110669851303, 0.013096392154693604], [0.0022075253073126078, -0.0011812079465016723, 0.0002940484555438161], [-0.00043441299931146204, 0.02366728149354458, 0.035889431834220886], [-0.030657343566417694, -0.024926183745265007, -0.012355240061879158], [-0.018955843523144722, -0.017360301688313484, -0.008214764297008514], [-0.01113052573055029, -0.01201171800494194, -0.002986249281093478], [0.018902746960520744, 0.01758778840303421, 0.026414571329951286], [-0.019977254793047905, -0.01605399139225483, -0.019136475399136543], [-0.00300968368537724, -0.017609693109989166, -0.013655650429427624], [0.0022096361499279737, 0.017998533323407173, 0.01815750263631344], [0.05186990648508072, 0.03285299986600876, 0.016072165220975876], [0.012626334093511105, 0.0013884707586839795, -0.012077193707227707], [-0.0037861645687371492, -0.013902144506573677, -0.01911942847073078], [-0.014163163490593433, -0.00513274222612381, -0.014303527772426605], [-0.010461323894560337, 0.009658926166594028, 0.01644069515168667], [-0.008665377274155617, 0.002501955023035407, -0.009703717194497585], [-0.03404829278588295, -0.02546044997870922, -0.014914450235664845], [0.04997691139578819, 0.06592527031898499, 0.073111392557621], [0.027394814416766167, 0.024555068463087082, 0.019957970827817917], [-0.027501430362462997, -0.01673700101673603, -0.03089248389005661], [-0.018696032464504242, -0.0020940247923135757, 0.015244065783917904], [-0.0062704551964998245, -0.0067006442695856094, -0.007532030809670687], [0.014871004968881607, 0.009914354421198368, 0.020960720255970955], [0.03662937879562378, 0.04413224756717682, 0.04220828413963318], [-0.011242181062698364, -0.013539309613406658, -0.016438307240605354], [-0.014854325912892818, 0.0038217694964259863, -0.002461288822814822], [-0.014826249331235886, 0.0009719038498587906, -0.012078499421477318], [-0.029396841302514076, -0.01432017982006073, 0.013018904253840446], [0.02755064144730568, 0.028369395062327385, 0.01640605367720127], [0.12049165368080139, 0.1395745575428009, 0.14566579461097717], [0.019721267744898796, 0.009739740751683712, 0.0023876908235251904], [-0.007320966571569443, 0.0065013207495212555, 0.01603059470653534], [0.007391378283500671, -0.0073603675700724125, -0.01770283281803131], [0.02984853833913803, 0.012391146272420883, 0.010563627816736698], [-0.013479884713888168, -0.008637298829853535, -0.013457189314067364], [0.04127075523138046, 0.03032625839114189, 0.024770958349108696], [-0.06524652987718582, -0.012209279462695122, 0.02087211236357689], [-0.1179763451218605, -0.060323599725961685, -0.07592175155878067], [-0.07122819870710373, -0.04385707899928093, -0.022124603390693665], [-0.04682473465800285, -0.022610662505030632, -0.010107148438692093], [-0.0054328180849552155, -0.010368981398642063, -0.008167334832251072], [0.029181398451328278, 0.030588403344154358, 0.028090540319681168], [0.016619984060525894, 0.004931286443024874, -0.006450849585235119], [0.01035264041274786, 0.002237115055322647, 0.0013903985964134336], [-0.04313831403851509, -0.061772625893354416, -0.08946335315704346], [0.0150345079600811, 0.007781678810715675, 0.0011013159528374672], [-0.013585779815912247, 0.008117705583572388, 0.020367907360196114], [-0.172962948679924, -0.16406646370887756, -0.1668281853199005], [0.0083833709359169, 0.0015236001927405596, -0.01731627807021141], [0.021939430385828018, 0.018004458397626877, 0.014768349006772041], [0.008083095774054527, -0.013463049195706844, -0.022061636671423912], [0.024328550323843956, 0.0128010343760252, 0.014966367743909359], [0.05850301682949066, 0.027980001643300056, 0.02225641906261444], [0.09690416604280472, 0.06929530203342438, 0.03253814950585365], [0.048208240419626236, 0.025294817984104156, 0.023508133366703987], [-0.026432134211063385, -0.040383171290159225, -0.03950457274913788], [-0.021598653867840767, -0.017070941627025604, -0.010933087207376957], [0.011645167134702206, 0.002806191798299551, 0.003779367310926318], [0.10478592664003372, 0.08954174816608429, 0.06555330753326416], [0.015151776373386383, -0.016160616651177406, -0.024905217811465263], [0.019659176468849182, 0.008487952873110771, 0.002426224760711193], [-0.05173315480351448, -0.026337839663028717, -0.02127116546034813], [0.016987523064017296, 0.006270893849432468, 0.0015798212261870503], [0.007938026450574398, -0.005250005517154932, -0.020408453419804573], [0.013017759658396244, 0.01654384844005108, 0.04163840040564537], [-0.009886542335152626, -0.026848411187529564, -0.03070281818509102], [0.01108171883970499, 0.01827266439795494, -0.007332107983529568], [-0.0285995751619339, -0.031727731227874756, -0.03370537981390953], [0.005299570970237255, 0.05678633600473404, 0.02825017087161541], [-0.055322226136922836, -0.09084303677082062, -0.12999044358730316], [0.01844066195189953, 0.031044499948620796, 0.021148500964045525], [-0.004471115302294493, 0.005830412730574608, 0.00911418441683054], [-0.04053843766450882, -0.016424428671598434, -0.0010634599020704627], [0.03858831524848938, 0.007309338077902794, -0.005618985276669264], [0.01423253770917654, -0.0055681923404335976, 3.394074519746937e-05], [0.11455483734607697, 0.14653916656970978, 0.1488018035888672], [-0.005231931805610657, -0.0033921014983206987, -0.000995257287286222], [0.01449565589427948, 0.019586293026804924, 0.04565812274813652], [-0.005179048050194979, -0.011201606132090092, -0.0008710073889233172], [-0.015361929312348366, 0.00778581015765667, -0.008238887414336205], [-0.1147838830947876, -0.09109023958444595, -0.050579313188791275], [0.09037500619888306, 0.09597006440162659, 0.10811734944581985], [0.001873677596449852, -0.01772197335958481, -0.07681205868721008], [-0.020383257418870926, -0.016072455793619156, -0.01077069528400898], [-0.060444317758083344, -0.05499502643942833, -0.06153025105595589], [-0.016717270016670227, 0.026493264362215996, 0.021835654973983765], [0.008203534409403801, 0.00418612826615572, 0.013867748901247978], [0.0789225772023201, 0.05467747151851654, 0.016568133607506752], [-0.15149451792240143, -0.1526806503534317, -0.14325062930583954], [0.00538366474211216, 0.010192245244979858, -0.00449327751994133], [-0.004906965419650078, -0.005569908302277327, -0.02096559666097164], [0.024530155584216118, 0.010962833650410175, 0.0034586559049785137], [0.03551010414958, 0.017310436815023422, 0.007064413744956255], [0.11111932247877121, 0.09825586527585983, 0.08827318251132965], [-0.051722846925258636, -0.047595202922821045, -0.03763044252991676], [-0.02975175902247429, -0.02153967320919037, -0.021425534039735794], [-0.03462936729192734, -0.025198571383953094, -0.017322326079010963], [-0.016921017318964005, -0.012419789098203182, -0.0154880927875638], [-0.08035065978765488, -0.08451078832149506, -0.09623870998620987], [-0.03870908170938492, -0.04211008921265602, -0.04383759945631027]] + latent_rgb_factors_bias = [-0.6957847476005554, -0.7276281118392944, -0.7405748963356018] + return latent_rgb_factors, latent_rgb_factors_bias + + +def _unwrap_upscale_model(latent_upscale_model): + # newer comfy wraps the upsampler in a ModelPatcher + return getattr(latent_upscale_model, "model", latent_upscale_model) + +def prepare_callback(model, steps, x0_output_dict=None, shape=None, latent_upscale_model=None, vae=None, rate=8, taeltx=False, num_keyframes=0, is_23=False): + latent_rgb_factors, latent_rgb_factors_bias = get_ltx_rgb_factors(is_23) + upscaler = _unwrap_upscale_model(latent_upscale_model) if latent_upscale_model is not None else None + upscaler_dtype = next(upscaler.parameters()).dtype if upscaler is not None else None + preview_format = "JPEG" + if preview_format not in ["JPEG", "PNG"]: + preview_format = "JPEG" + + previewer = WrappedPreviewer(latent_rgb_factors, latent_rgb_factors_bias, rate=rate, taeltx=vae if taeltx else None) + + pbar = comfy.utils.ProgressBar(steps) + def callback(step, x0, x, total_steps): + if x0 is not None and shape is not None: + cut = math.prod(shape[1:]) + x0 = x0[:, :, :cut].reshape([x0.shape[0]] + list(shape)[1:]) + + if num_keyframes > 0: + x0 = x0[:, :, :-num_keyframes] + + if upscaler is not None: + x0 = vae.first_stage_model.per_channel_statistics.un_normalize(x0) + x0 = upscaler(x0.to(upscaler_dtype)) + x0 = vae.first_stage_model.per_channel_statistics.normalize(x0) + + preview_bytes = None + if previewer: + preview_bytes = previewer.decode_latent_to_preview_image(preview_format, x0) + pbar.update_absolute(step + 1, total_steps, preview_bytes) + return callback + +class OuterSampleCallbackWrapper: + def __init__(self, latent_upscale_model=None, vae=None, preview_rate=8, taeltx=False): + self.latent_upscale_model = latent_upscale_model + self.vae = vae + self.preview_rate = preview_rate + self.taeltx = taeltx + self.x0_output = {} + + def __call__(self, executor, noise, latent_image, sampler, sigmas, denoise_mask, callback, disable_pbar, seed, latent_shapes): + guider = executor.class_obj + diffusion_model = guider.model_patcher.model.diffusion_model + is_23 = not diffusion_model.caption_projection_first_linear + + original_callback = callback + if self.latent_upscale_model is not None: + _unwrap_upscale_model(self.latent_upscale_model).to(device) + if self.vae is not None and self.taeltx: + self.vae.first_stage_model.to(device) + + num_keyframes = 0 + if 'positive' in guider.conds and len(guider.conds['positive']) > 0: + keyframe_idxs = guider.conds['positive'][0].get('keyframe_idxs') + if keyframe_idxs is not None: + num_keyframes = len(torch.unique(keyframe_idxs[0, 0, :, 0])) + + new_callback = prepare_callback(guider.model_patcher, len(sigmas) -1, shape=latent_shapes[0] if len(latent_shapes) > 1 else None, + x0_output_dict=self.x0_output, latent_upscale_model=self.latent_upscale_model, vae=self.vae, rate=self.preview_rate, taeltx=self.taeltx, num_keyframes=num_keyframes, is_23=is_23) + # Wrapper that calls both callbacks + def combined_callback(step, x0, x, total_steps): + new_callback(step, x0, x, total_steps) + if original_callback is not None: + original_callback(step, x0, x, total_steps) + out = executor(noise, latent_image, sampler, sigmas, denoise_mask, combined_callback, disable_pbar, seed, latent_shapes=latent_shapes) + if self.latent_upscale_model is not None: + _unwrap_upscale_model(self.latent_upscale_model).to(mm.unet_offload_device()) + return out + +class LTX2SamplingPreviewOverride(io.ComfyNode): + @classmethod + def define_schema(cls) -> io.Schema: + return io.Schema( + node_id="LTX2SamplingPreviewOverride", + display_name="LTX2 Sampling Preview Override", + description="Overrides the LTX2 preview sampling preview function, temporary measure until previews are in comfy core", + category="KJNodes/ltxv", + is_experimental=True, + inputs=[ + io.Model.Input("model", tooltip="The model to add preview override to."), + io.MultiType.Input(io.Float.Input("preview_rate", default=8.0, min=1.0, max=60.0, step=0.01, tooltip="Preview frame rate."), [io.Int]), + io.LatentUpscaleModel.Input("latent_upscale_model", optional=True, tooltip="Optional upscale model to use for higher resolution previews."), + io.Vae.Input("vae", optional=True, tooltip="VAE model to use normalizing the latents for the upscale model."), + ], + outputs=[ + io.Model.Output(tooltip="The model with Sampling Preview Override."), + ], + ) + + @classmethod + def execute(cls, model, preview_rate, latent_upscale_model=None, vae=None) -> io.NodeOutput: + model = model.clone() + taeltx = False + if vae is not None: + if vae.first_stage_model.__class__.__name__ == "TAEHV": + taeltx = True + latent_upscale_model=None + model.add_wrapper_with_key(comfy.patcher_extension.WrappersMP.OUTER_SAMPLE, "sampling_preview", OuterSampleCallbackWrapper(latent_upscale_model, vae, preview_rate, taeltx)) + return io.NodeOutput(model) + + +# based on https://github.com/Lightricks/ComfyUI-LTXVideo/blob/cd5d371518afb07d6b3641be8012f644f25269fc/easy_samplers.py#L916 +class OuterSampleAudioNormalizationWrapper: + def __init__(self, audio_normalization_factors): + self.audio_normalization_factors = audio_normalization_factors + + def __call__(self, executor, noise, latent_image, sampler, sigmas, denoise_mask, callback, disable_pbar, seed, latent_shapes): + guider = executor.class_obj + ltxav = guider.model_patcher.model.diffusion_model + + x0_output = {} + self.total_steps = sigmas.shape[-1] - 1 + pbar = comfy.utils.ProgressBar(self.total_steps) + self.full_step = 0 + + previewer = latent_preview.get_previewer(guider.model_patcher.load_device, guider.model_patcher.model.latent_format) + def custom_callback(step, x0, x, total_steps): + if x0_output is not None: + x0_output["x0"] = x0 + + preview_bytes = None + if previewer: + preview_bytes = previewer.decode_latent_to_preview_image("JPEG", x0) + self.full_step += 1 + pbar.update_absolute(self.full_step, self.total_steps, preview_bytes) + + callback = custom_callback + + audio_normalization_factors = self.audio_normalization_factors.strip().split(",") + audio_normalization_factors = [float(factor) for factor in audio_normalization_factors] + + # Extend normalization factors to match the length of sigmas + sigmas_len = self.total_steps + if len(audio_normalization_factors) < sigmas_len and len(audio_normalization_factors) > 0: + audio_normalization_factors.extend([audio_normalization_factors[-1]] * (sigmas_len - len(audio_normalization_factors))) + + # Calculate indices where both normalization factors are not 1.0 + sampling_split_indices = [i + 1 for i, a in enumerate(audio_normalization_factors) if a != 1.0] + + # Split sigmas according to sampling_split_indices + def split_by_indices(arr, indices): + """ + Splits arr into chunks according to indices (split points). + Indices are treated as starting a new chunk at each index in the list. + """ + if not indices: + return [arr] + split_points = sorted(set(indices)) + chunks = [] + prev = 0 + for idx in split_points: + if prev < idx: + chunks.append(arr[prev : idx + 1]) + prev = idx + if prev < len(arr): + chunks.append(arr[prev:]) + return chunks + + sigmas_chunks = split_by_indices(sigmas, sampling_split_indices) + + i = 0 + for sigmas_chunk in sigmas_chunks: + i += len(sigmas_chunk) - 1 + latent_image = executor(noise, latent_image, sampler, sigmas_chunk, denoise_mask, callback, disable_pbar, seed, latent_shapes=latent_shapes) + + if "x0" in x0_output: + latent_image = guider.model_patcher.model.process_latent_out(x0_output["x0"]) + + if i - 1 < len(audio_normalization_factors): + vx, ax = ltxav.separate_audio_and_video_latents(comfy.utils.unpack_latents(latent_image, latent_shapes), None) + if denoise_mask is not None: + audio_mask = ltxav.separate_audio_and_video_latents(comfy.utils.unpack_latents(denoise_mask, latent_shapes), None)[1] + ax = ax * audio_mask * audio_normalization_factors[i - 1] + ax * (1 - audio_mask) + else: + ax = ax * audio_normalization_factors[i - 1] + latent_image = comfy.utils.pack_latents(ltxav.recombine_audio_and_video_latents(vx, ax))[0] + + print("After %d steps, the audio latent was normalized by %f" % (i, audio_normalization_factors[i - 1])) + + return latent_image + + +class LTX2AudioLatentNormalizingSampling(io.ComfyNode): + @classmethod + def define_schema(cls) -> io.Schema: + return io.Schema( + node_id="LTX2AudioLatentNormalizingSampling", + display_name="LTX2 Audio Latent Normalizing Sampling", + description="Improves LTX2 generated audio quality by normalizing audio latents at specified sampling steps.", + category="KJNodes/ltxv", + is_experimental=True, + inputs=[ + io.Model.Input("model", tooltip="The model to add preview override to."), + io.String.Input("audio_normalization_factors", default="1,1,0.25,1,1,0.25,1,1", tooltip="Comma-separated list of audio normalization factors to apply at each sampling step. For example, '1,1,0.25,1,1,0.25,1,1' will apply a factor of 0.25 at the 3rd and 6th steps."), + ], + outputs=[ + io.Model.Output(tooltip="The model with Audio Latent Normalizing Sampling."), + ], + ) + + @classmethod + def execute(cls, model, audio_normalization_factors) -> io.NodeOutput: + model = model.clone() + model.add_wrapper_with_key(comfy.patcher_extension.WrappersMP.OUTER_SAMPLE, "ltx2_audio_normalization", OuterSampleAudioNormalizationWrapper(audio_normalization_factors)) + return io.NodeOutput(model) + + +class LTXVImgToVideoInplaceKJ(io.ComfyNode): + @classmethod + def define_schema(cls): + options = [] + for num_images in range(1, 21): # 1 to 20 images + image_inputs = [] + for i in range(1, num_images + 1): + image_inputs.extend([ + io.Image.Input(f"image_{i}", optional=True, tooltip=f"Image {i} to insert into the video latent."), + io.Int.Input( + f"index_{i}", + default=0, + min=-9999, + max=9999, + step=1, + tooltip=f"Frame index for image {i} (in pixel space).", + optional=True, + ), + io.Float.Input(f"strength_{i}", default=1.0, min=0.0, max=1.0, step=0.01, tooltip=f"Strength for image {i}."), + ]) + options.append(io.DynamicCombo.Option( + key=str(num_images), + inputs=image_inputs + )) + + return io.Schema( + node_id="LTXVImgToVideoInplaceKJ", + category="KJNodes/ltxv", + description="Replaces video latent frames with the encoded input images, uses DynamicCombo which requires ComfyUI 0.8.1 and frontend 1.33.4 or later.", + inputs=[ + io.Vae.Input("vae", tooltip="Video VAE used to encode the images"), + io.Latent.Input("latent", tooltip="Video latent to insert images into"), + io.DynamicCombo.Input( + "num_images", + options=options, + display_name="Number of Images", + tooltip="Select how many images to insert", + ), + ], + outputs=[ + io.Latent.Output(display_name="latent", tooltip="The video latent with the images inserted and latent noise mask updated."), + ], + ) + + @classmethod + def execute(cls, vae, latent, num_images) -> io.NodeOutput: + + samples = latent["samples"].clone() + scale_factors = vae.downscale_index_formula + _, height_scale_factor, width_scale_factor = scale_factors + + batch, _, latent_frames, latent_height, latent_width = samples.shape + width = latent_width * width_scale_factor + height = latent_height * height_scale_factor + + # Get existing noise mask if present, otherwise create new one + if "noise_mask" in latent: + conditioning_latent_frames_mask = latent["noise_mask"].clone() + else: + conditioning_latent_frames_mask = torch.ones( + (batch, 1, latent_frames, 1, 1), + dtype=torch.float32, + device=samples.device, + ) + + # num_images is a dict containing the inputs from the selected option + # e.g., {'image_1': tensor, 'frame_idx_1': 0, 'strength_1': 1.0, 'image_2': tensor, 'frame_idx_2': 20, 'strength_2': 0.8, ...} + + image_keys = sorted([k for k in num_images.keys() if k.startswith('image_')]) + + for img_key in image_keys: + i = img_key.split('_')[1] + + image = num_images[f"image_{i}"] + if image is None: + continue + index = num_images.get(f"index_{i}") + if index is None: + continue + strength = num_images[f"strength_{i}"] + + if image.shape[1] != height or image.shape[2] != width: + pixels = comfy.utils.common_upscale(image.movedim(-1, 1), width, height, "bilinear", "center").movedim(1, -1) + else: + pixels = image + encode_pixels = pixels[:, :, :, :3] + t = vae.encode(encode_pixels) + + # Convert pixel frame index to latent index + time_scale_factor = scale_factors[0] + + # Handle negative indexing in pixel space + pixel_frame_count = (latent_frames - 1) * time_scale_factor + 1 + if index < 0: + index = pixel_frame_count + index + + # Convert to latent index + latent_idx = index // time_scale_factor + + # Clamp to valid range + latent_idx = max(0, min(latent_idx, latent_frames - 1)) + + # Calculate end index, ensuring we don't exceed latent_frames + end_index = min(latent_idx + t.shape[2], latent_frames) + + # Replace samples at the specified index range + samples[:, :, latent_idx:end_index] = t[:, :, :end_index - latent_idx] + + # Update mask at the specified index range + conditioning_latent_frames_mask[:, :, latent_idx:end_index] = 1.0 - strength + + return io.NodeOutput({"samples": samples, "noise_mask": conditioning_latent_frames_mask}) + +try: + import triton + import triton.language as tl + HAS_TRITON = True +except ImportError: + HAS_TRITON = False + +if HAS_TRITON: + @triton.jit + def _rms_norm_scale_shift_kernel( + X_ptr, Out_ptr, Scale_ptr, Shift_ptr, + N, scale_n_rows, # scale_n_rows: actual rows in scale/shift (handles broadcast) + eps: tl.constexpr, + BLOCK_N: tl.constexpr, + IS_BF16: tl.constexpr, + HAS_BROADCAST: tl.constexpr, # True when scale_n_rows < n_rows + ): + row = tl.program_id(0) + cols = tl.arange(0, BLOCK_N) + mask = cols < N + off = row * N + cols + + x = tl.load(X_ptr + off, mask=mask, other=0.0).to(tl.float32) + + mean_sq = tl.sum(x * x, axis=0) / N + rrms = tl.math.rsqrt(mean_sq + eps) + x_norm = x * rrms + + # Fast path: no broadcast, scale row == x row + if HAS_BROADCAST: + scale_row = row % scale_n_rows + scale_off = scale_row * N + cols + else: + scale_off = off + scale = tl.load(Scale_ptr + scale_off, mask=mask, other=0.0).to(tl.float32) + shift = tl.load(Shift_ptr + scale_off, mask=mask, other=0.0).to(tl.float32) + out = x_norm * (1.0 + scale) + shift + + if IS_BF16: + tl.store(Out_ptr + off, out.to(tl.bfloat16), mask=mask) + else: + tl.store(Out_ptr + off, out.to(tl.float16), mask=mask) + + @triton.jit + def _rope_qk_split_kernel( + Q_ptr, K_ptr, Cos_ptr, Sin_ptr, + H, T, D, # D = dim_head (full, even) + TH, # T * H, precomputed to avoid runtime multiply in index decomp + # q/k layout: [B, T, H*D] contiguous → q[b,t,h*D + d] + # cos/sin layout: [B, H, T, D//2] contiguous + IS_BF16: tl.constexpr, + BLOCK_HD: tl.constexpr, # >= D//2 + ): + # grid: (B*H*T,) + bht = tl.program_id(0) + t = bht % T + h = (bht // T) % H + b = bht // TH + + D_half = D // 2 + cols = tl.arange(0, BLOCK_HD) + mask = cols < D_half + + # offset into q/k tensor: [B, T, H*D] + qk_base = (b * T + t) * (H * D) + h * D + # offset into cos/sin: [B, H, T, D//2] + cs_base = (b * H * T + h * T + t) * D_half + + # load both halves of q and k + q_x = tl.load(Q_ptr + qk_base + cols, mask=mask, other=0.0).to(tl.float32) + q_y = tl.load(Q_ptr + qk_base + D_half + cols, mask=mask, other=0.0).to(tl.float32) + k_x = tl.load(K_ptr + qk_base + cols, mask=mask, other=0.0).to(tl.float32) + k_y = tl.load(K_ptr + qk_base + D_half + cols, mask=mask, other=0.0).to(tl.float32) + cos = tl.load(Cos_ptr + cs_base + cols, mask=mask, other=1.0).to(tl.float32) + sin = tl.load(Sin_ptr + cs_base + cols, mask=mask, other=0.0).to(tl.float32) + + # split RoPE: out_x = x*cos - y*sin, out_y = y*cos + x*sin + q_ox = q_x * cos - q_y * sin + q_oy = q_y * cos + q_x * sin + k_ox = k_x * cos - k_y * sin + k_oy = k_y * cos + k_x * sin + + if IS_BF16: + tl.store(Q_ptr + qk_base + cols, q_ox.to(tl.bfloat16), mask=mask) + tl.store(Q_ptr + qk_base + D_half + cols, q_oy.to(tl.bfloat16), mask=mask) + tl.store(K_ptr + qk_base + cols, k_ox.to(tl.bfloat16), mask=mask) + tl.store(K_ptr + qk_base + D_half + cols, k_oy.to(tl.bfloat16), mask=mask) + else: + tl.store(Q_ptr + qk_base + cols, q_ox.to(tl.float16), mask=mask) + tl.store(Q_ptr + qk_base + D_half + cols, q_oy.to(tl.float16), mask=mask) + tl.store(K_ptr + qk_base + cols, k_ox.to(tl.float16), mask=mask) + tl.store(K_ptr + qk_base + D_half + cols, k_oy.to(tl.float16), mask=mask) + + @triton.jit + def _rms_norm_dual_scale_shift_kernel( + X_ptr, Out1_ptr, Out2_ptr, + Scale1_ptr, Shift1_ptr, Scale2_ptr, Shift2_ptr, + N, scale_n_rows, + eps: tl.constexpr, + BLOCK_N: tl.constexpr, + IS_BF16: tl.constexpr, + HAS_BROADCAST: tl.constexpr, + ): + """Compute rms_norm(x) once, write two differently-scaled outputs.""" + row = tl.program_id(0) + cols = tl.arange(0, BLOCK_N) + mask = cols < N + off = row * N + cols + + x = tl.load(X_ptr + off, mask=mask, other=0.0).to(tl.float32) + mean_sq = tl.sum(x * x, axis=0) / N + x_norm = x * tl.math.rsqrt(mean_sq + eps) + + if HAS_BROADCAST: + scale_row = row % scale_n_rows + s_off = scale_row * N + cols + else: + s_off = off + + scale1 = tl.load(Scale1_ptr + s_off, mask=mask, other=0.0).to(tl.float32) + shift1 = tl.load(Shift1_ptr + s_off, mask=mask, other=0.0).to(tl.float32) + scale2 = tl.load(Scale2_ptr + s_off, mask=mask, other=0.0).to(tl.float32) + shift2 = tl.load(Shift2_ptr + s_off, mask=mask, other=0.0).to(tl.float32) + + out1 = x_norm * (1.0 + scale1) + shift1 + out2 = x_norm * (1.0 + scale2) + shift2 + + if IS_BF16: + tl.store(Out1_ptr + off, out1.to(tl.bfloat16), mask=mask) + tl.store(Out2_ptr + off, out2.to(tl.bfloat16), mask=mask) + else: + tl.store(Out1_ptr + off, out1.to(tl.float16), mask=mask) + tl.store(Out2_ptr + off, out2.to(tl.float16), mask=mask) + + +def fused_norm_scale_shift(x, scale, shift, eps=1e-6, use_triton=True): + if use_triton and HAS_TRITON and x.is_cuda: + orig_shape = x.shape + hidden = x.shape[-1] + x_2d = x.contiguous().reshape(-1, hidden) + n_rows = x_2d.shape[0] + # Flatten scale/shift without expanding — kernel handles broadcast via modulo + scale_2d = scale.contiguous().reshape(-1, hidden) + shift_2d = shift.contiguous().reshape(-1, hidden) + scale_n_rows = scale_2d.shape[0] + out = torch.empty_like(x_2d) + BLOCK_N = triton.next_power_of_2(hidden) + num_warps = min(max(BLOCK_N // 256, 1), 16) + _rms_norm_scale_shift_kernel[(n_rows,)]( + x_2d, out, scale_2d, shift_2d, + hidden, scale_n_rows, + eps=eps, BLOCK_N=BLOCK_N, + IS_BF16=(x.dtype == torch.bfloat16), + HAS_BROADCAST=(scale_n_rows < n_rows), + num_warps=num_warps, + ) + return out.view(orig_shape) + else: + return comfy.ldm.common_dit.rms_norm(x, eps=eps) * (1 + scale) + shift + + +def fused_rope_qk(q, k, freqs_cis, use_triton=True): + """Apply split RoPE to q and k in one fused kernel pass. + q, k: [B, T, H*D] contiguous + freqs_cis: (cos, sin, split_pe) where cos/sin: [B, H, T, D//2] + Falls back to original apply_rotary_emb if preconditions not met. + """ + + if not (use_triton and HAS_TRITON and q.is_cuda): + return _apply_rope(q, freqs_cis), _apply_rope(k, freqs_cis) + + cos, sin = freqs_cis[0], freqs_cis[1] + split_pe = freqs_cis[2] if len(freqs_cis) > 2 else False + + if not split_pe or cos.ndim != 4 or q.ndim != 3: + return _apply_rope(q, freqs_cis), _apply_rope(k, freqs_cis) + + B_cos, H, T_cos, D_half = cos.shape + D = D_half * 2 + if q.shape != (B_cos, T_cos, H * D) or k.shape != (B_cos, T_cos, H * D): + return _apply_rope(q, freqs_cis), _apply_rope(k, freqs_cis) + + q = q.contiguous() + k = k.contiguous() + cos_c = cos.contiguous() + sin_c = sin.contiguous() + + BLOCK_HD = triton.next_power_of_2(D_half) + num_warps = min(max(BLOCK_HD // 32, 1), 8) + _rope_qk_split_kernel[(B_cos * H * T_cos,)]( + q, k, cos_c, sin_c, + H, T_cos, D, + T_cos * H, + IS_BF16=(q.dtype == torch.bfloat16), + BLOCK_HD=BLOCK_HD, + num_warps=num_warps, + ) + return q, k + + +def fused_norm_dual_scale_shift(x, scale1, shift1, scale2, shift2, eps=1e-6, use_triton=True): + """RMS-norm x once, return two scaled outputs: (x_norm*(1+s1)+b1, x_norm*(1+s2)+b2).""" + if use_triton and HAS_TRITON and x.is_cuda: + orig_shape = x.shape + hidden = x.shape[-1] + x_2d = x.contiguous().reshape(-1, hidden) + n_rows = x_2d.shape[0] + scale1_2d = scale1.contiguous().reshape(-1, hidden) + shift1_2d = shift1.contiguous().reshape(-1, hidden) + scale2_2d = scale2.contiguous().reshape(-1, hidden) + shift2_2d = shift2.contiguous().reshape(-1, hidden) + scale_n_rows = scale1_2d.shape[0] + out1 = torch.empty_like(x_2d) + out2 = torch.empty_like(x_2d) + BLOCK_N = triton.next_power_of_2(hidden) + num_warps = min(max(BLOCK_N // 256, 1), 16) + _rms_norm_dual_scale_shift_kernel[(n_rows,)]( + x_2d, out1, out2, + scale1_2d, shift1_2d, scale2_2d, shift2_2d, + hidden, scale_n_rows, + eps=eps, BLOCK_N=BLOCK_N, + IS_BF16=(x.dtype == torch.bfloat16), + HAS_BROADCAST=(scale_n_rows < n_rows), + num_warps=num_warps, + ) + return out1.view(orig_shape), out2.view(orig_shape) + else: + x_norm = comfy.ldm.common_dit.rms_norm(x, eps=eps) + return x_norm * (1 + scale1) + shift1, x_norm * (1 + scale2) + shift2 + + +def _apply_text_cross_attention_patched( + self, x, context, attn, scale_shift_table, prompt_scale_shift_table, + timestep, prompt_timestep, attention_mask, transformer_options, +): + """Drop-in replacement for _apply_text_cross_attention with fused norm+scale+shift. + Patched onto the block instance so self._apply_text_cross_attention resolves here.""" + if self.cross_attention_adaln: + shift_q, scale_q, gate = self.get_ada_values(scale_shift_table, x.shape[0], timestep, slice(6, 9)) + batch_size = x.shape[0] + shift_kv, scale_kv = ( + prompt_scale_shift_table[None, None].to(device=x.device, dtype=x.dtype) + + prompt_timestep.reshape(batch_size, prompt_timestep.shape[1], 2, -1) + ).unbind(dim=2) + attn_input = fused_norm_scale_shift(x, scale_q, shift_q, + use_triton=getattr(self, 'use_triton_kernels', True)) + del shift_q, scale_q + encoder_hidden_states = context * (1 + scale_kv) + shift_kv + del scale_kv, shift_kv + return attn(attn_input, context=encoder_hidden_states, + mask=attention_mask, transformer_options=transformer_options) * gate + return attn( + comfy.ldm.common_dit.rms_norm(x), context=context, + mask=attention_mask, transformer_options=transformer_options, + ) + + +def ltx2_forward( + self, x: Tuple[torch.Tensor, torch.Tensor], v_context=None, a_context=None, attention_mask=None, v_timestep=None, a_timestep=None, + v_pe=None, a_pe=None, v_cross_pe=None, a_cross_pe=None, v_cross_scale_shift_timestep=None, a_cross_scale_shift_timestep=None, + v_cross_gate_timestep=None, a_cross_gate_timestep=None, transformer_options=None, self_attention_mask=None, + v_prompt_timestep=None, a_prompt_timestep=None, **kwargs + ) -> Tuple[torch.Tensor, torch.Tensor]: + run_vx = transformer_options.get("run_vx", True) + run_ax = transformer_options.get("run_ax", True) + use_triton = getattr(self, 'use_triton_kernels', True) + video_scale = getattr(self, 'video_scale', 1.0) + audio_scale = getattr(self, 'audio_scale', 1.0) + audio_to_video_scale = getattr(self, 'audio_to_video_scale', 1.0) + video_to_audio_scale = getattr(self, 'video_to_audio_scale', 1.0) + + vx, ax = x + run_ax = run_ax and ax.numel() > 0 and audio_scale != 0.0 + run_a2v = run_vx and transformer_options.get("a2v_cross_attn", True) and ax.numel() > 0 and audio_to_video_scale != 0.0 + run_v2a = run_ax and transformer_options.get("v2a_cross_attn", True) and video_to_audio_scale != 0.0 + + if run_vx: + # video self-attention + vshift_msa, vscale_msa = (self.get_ada_values(self.scale_shift_table, vx.shape[0], v_timestep, slice(0, 2))) + norm_vx = fused_norm_scale_shift(vx, vscale_msa, vshift_msa, use_triton=use_triton) + del vshift_msa, vscale_msa + # inline attn1 only when triton is active (needed to intercept q/k for fused RoPE) + if use_triton: + _a1 = self.attn1 + q = _a1.to_q(norm_vx) + k = _a1.to_k(norm_vx) + v = _a1.to_v(norm_vx) + + q = _a1.q_norm(q) + k = _a1.k_norm(k) + + if v_pe is not None: + q, k = fused_rope_qk(q, k, v_pe, use_triton=True) + + if self_attention_mask is None: + _sa_out = _comfy_attn.optimized_attention(q, k, v, _a1.heads, attn_precision=_a1.attn_precision, transformer_options=transformer_options) + elif _GuideAttentionMask is not None and isinstance(self_attention_mask, _GuideAttentionMask): + _sa_out = _ltx_attn_with_guide_mask(q, k, v, _a1.heads, self_attention_mask, attn_precision=_a1.attn_precision, transformer_options=transformer_options) + else: + _sa_out = _comfy_attn.optimized_attention_masked(q, k, v, _a1.heads, self_attention_mask, attn_precision=_a1.attn_precision, transformer_options=transformer_options) + del q, k, v + if _a1.to_gate_logits is not None: + _gate = _a1.to_gate_logits(norm_vx) + _b, _t, _ = _sa_out.shape + _sa_out = _sa_out.view(_b, _t, _a1.heads, _a1.dim_head) + _sa_out.mul_((2.0 * torch.sigmoid(_gate)).unsqueeze(-1)) + _sa_out = _sa_out.view(_b, _t, _a1.heads * _a1.dim_head) + del _gate + + attn1_out = _a1.to_out(_sa_out) + del _sa_out, norm_vx + else: + attn1_out = self.attn1(norm_vx, pe=v_pe, mask=self_attention_mask, transformer_options=transformer_options) + del norm_vx + # video cross-attention + vgate_msa = self.get_ada_values(self.scale_shift_table, vx.shape[0], v_timestep, slice(2, 3))[0] + vx.addcmul_(attn1_out, vgate_msa, value=video_scale) + del vgate_msa, attn1_out + vx.add_(self._apply_text_cross_attention( + vx, v_context, self.attn2, self.scale_shift_table, + getattr(self, 'prompt_scale_shift_table', None), + v_timestep, v_prompt_timestep, attention_mask, transformer_options,), + alpha=video_scale + ) + # audio + if run_ax: + # audio self-attention + ashift_msa, ascale_msa = (self.get_ada_values(self.audio_scale_shift_table, ax.shape[0], a_timestep, slice(0, 2))) + norm_ax = fused_norm_scale_shift(ax, ascale_msa, ashift_msa, use_triton=use_triton) + del ashift_msa, ascale_msa + attn1_out = self.audio_attn1(norm_ax, pe=a_pe, transformer_options=transformer_options) + del norm_ax + # audio cross-attention + agate_msa = self.get_ada_values(self.audio_scale_shift_table, ax.shape[0], a_timestep, slice(2, 3))[0] + ax.addcmul_(attn1_out, agate_msa, value=audio_scale) + del agate_msa, attn1_out + ax.add_(self._apply_text_cross_attention( + ax, a_context, self.audio_attn2, self.audio_scale_shift_table, + getattr(self, 'audio_prompt_scale_shift_table', None), + a_timestep, a_prompt_timestep, attention_mask, transformer_options,), + alpha=audio_scale + ) + + # video - audio cross attention. + if run_a2v or run_v2a: + if run_a2v and run_v2a: + # Fetch all 4 scale/shift values per table at once (avoids two get_ada_values calls each) + # and compute rms_norm once per tensor via dual kernel. + v_ca_vals = self.get_ada_values(self.scale_shift_table_a2v_ca_video[:4, :], vx.shape[0], v_cross_scale_shift_timestep) + a_ca_vals = self.get_ada_values(self.scale_shift_table_a2v_ca_audio[:4, :], ax.shape[0], a_cross_scale_shift_timestep) + vx_a2v, vx_v2a = fused_norm_dual_scale_shift(vx, v_ca_vals[0], v_ca_vals[1], v_ca_vals[2], v_ca_vals[3], use_triton=use_triton) + ax_a2v, ax_v2a = fused_norm_dual_scale_shift(ax, a_ca_vals[0], a_ca_vals[1], a_ca_vals[2], a_ca_vals[3], use_triton=use_triton) + del v_ca_vals, a_ca_vals + elif run_a2v: + sv, bv = self.get_ada_values(self.scale_shift_table_a2v_ca_video[:4, :], vx.shape[0], v_cross_scale_shift_timestep)[:2] + sa, ba = self.get_ada_values(self.scale_shift_table_a2v_ca_audio[:4, :], ax.shape[0], a_cross_scale_shift_timestep)[:2] + vx_a2v = fused_norm_scale_shift(vx, sv, bv, use_triton=use_triton) + ax_a2v = fused_norm_scale_shift(ax, sa, ba, use_triton=use_triton) + del sv, bv, sa, ba + else: # only v2a + sv, bv = self.get_ada_values(self.scale_shift_table_a2v_ca_video[:4, :], vx.shape[0], v_cross_scale_shift_timestep)[2:4] + sa, ba = self.get_ada_values(self.scale_shift_table_a2v_ca_audio[:4, :], ax.shape[0], a_cross_scale_shift_timestep)[2:4] + vx_v2a = fused_norm_scale_shift(vx, sv, bv, use_triton=use_triton) + ax_v2a = fused_norm_scale_shift(ax, sa, ba, use_triton=use_triton) + del sv, bv, sa, ba + + # audio to video cross attention + if run_a2v: + a2v_out = self.audio_to_video_attn(vx_a2v, context=ax_a2v, pe=v_cross_pe, k_pe=a_cross_pe, transformer_options=transformer_options) + del vx_a2v, ax_a2v + gate_out_a2v = self.get_ada_values(self.scale_shift_table_a2v_ca_video[4:, :], vx.shape[0], v_cross_gate_timestep)[0] + vx.addcmul_(a2v_out, gate_out_a2v, value=audio_to_video_scale) + del gate_out_a2v, a2v_out + + # video to audio cross attention + if run_v2a: + v2a_out = self.video_to_audio_attn(ax_v2a, context=vx_v2a, pe=a_cross_pe, k_pe=v_cross_pe, transformer_options=transformer_options) + del ax_v2a, vx_v2a + gate_out_v2a = self.get_ada_values(self.scale_shift_table_a2v_ca_audio[4:, :], ax.shape[0], a_cross_gate_timestep)[0] + ax.addcmul_(v2a_out, gate_out_v2a, value=video_to_audio_scale) + del gate_out_v2a, v2a_out + + # video feedforward + if run_vx: + vshift_mlp, vscale_mlp = self.get_ada_values(self.scale_shift_table, vx.shape[0], v_timestep, slice(3, 5)) + vx_scaled = fused_norm_scale_shift(vx, vscale_mlp, vshift_mlp, use_triton=use_triton) + del vshift_mlp, vscale_mlp + + ff_out = self.ff(vx_scaled) + del vx_scaled + + vgate_mlp = self.get_ada_values(self.scale_shift_table, vx.shape[0], v_timestep, slice(5, 6))[0] + vx.addcmul_(ff_out, vgate_mlp, value=video_scale) + del vgate_mlp, ff_out + + # audio feedforward + if run_ax: + ashift_mlp, ascale_mlp = self.get_ada_values(self.audio_scale_shift_table, ax.shape[0], a_timestep, slice(3, 5)) + ax_scaled = fused_norm_scale_shift(ax, ascale_mlp, ashift_mlp, use_triton=use_triton) + del ashift_mlp, ascale_mlp + + ff_out = self.audio_ff(ax_scaled) + del ax_scaled + + agate_mlp = self.get_ada_values(self.audio_scale_shift_table, ax.shape[0], a_timestep, slice(5, 6))[0] + ax.addcmul_(ff_out, agate_mlp, value=audio_scale) + del agate_mlp, ff_out + + return vx, ax + +class LTX2ForwardPatch: + def __init__(self, video, audio, audio_to_video, video_to_audio, use_triton=True): + self.video_scale = video + self.audio_scale = audio + self.video_to_audio_scale = video_to_audio + self.audio_to_video_scale = audio_to_video + self.use_triton_kernels = use_triton + def __get__(self, obj, objtype=None): + def wrapped_forward(self_module, *args, **kwargs): + self_module.video_scale = self.video_scale + self_module.audio_scale = self.audio_scale + self_module.video_to_audio_scale = self.video_to_audio_scale + self_module.audio_to_video_scale = self.audio_to_video_scale + self_module.use_triton_kernels = self.use_triton_kernels + self_module._apply_text_cross_attention = types.MethodType( + _apply_text_cross_attention_patched, self_module) + return ltx2_forward(self_module, *args, **kwargs) + return types.MethodType(wrapped_forward, obj) + +class LTX2AttentionTunerPatch(io.ComfyNode): + + @classmethod + def define_schema(cls): + return io.Schema( + node_id="LTX2AttentionTunerPatch", + display_name="LTX2 Attention Tuner Patch", + category="KJNodes/ltxv", + description="EXPERIMENTAL! Custom LTX2 forward pass with attention scaling factors per modality, also reduces peak VRAM usage.", + is_experimental=True, + inputs=[ + io.Model.Input("model"), + io.String.Input("blocks", default="", tooltip="Comma separated list of transformer block indices to apply the patch to. Leave empty to apply to all blocks."), + io.Float.Input("video_scale", default=1.0, min=0.0, max=100, step=0.01, tooltip="Scaling factor for video attention."), + io.Float.Input("audio_scale", default=1.0, min=0.0, max=100, step=0.01, tooltip="Scaling factor for audio attention."), + io.Float.Input("audio_to_video_scale", default=1.0, min=0.0, max=100, step=0.01, tooltip="Scaling factor for video attention."), + io.Float.Input("video_to_audio_scale", default=1.0, min=0.0, max=100, step=0.01, tooltip="Scaling factor for audio attention."), + io.Boolean.Input("triton_kernels", default=True, tooltip="Use Triton fused kernels for norm+scale+shift and rope application operations, can be very slightly faster."), + ], + outputs=[ + io.Model.Output(display_name="model"), + ], + ) + + @classmethod + def execute(cls, model, blocks, video_scale, audio_scale, audio_to_video_scale, video_to_audio_scale, triton_kernels) -> io.NodeOutput: + model_clone = model.clone() + diffusion_model = model_clone.get_model_object("diffusion_model") + + # Parse selected block indices + if blocks.strip() == "": + selected_blocks = set(range(len(diffusion_model.transformer_blocks))) + else: + selected_blocks = set(int(idx) for idx in blocks.strip().split(",")) + + logging.info(f"Applying LTX2 Attention Tuner Patch with custom scales to blocks: {sorted(selected_blocks)}, triton_kernels={triton_kernels}") + + # Apply patch to all blocks, but use 1.0 scales for non-selected blocks + for idx in range(len(diffusion_model.transformer_blocks)): + block = diffusion_model.transformer_blocks[idx] + if idx in selected_blocks: + patched_forward = LTX2ForwardPatch(video_scale, audio_scale, audio_to_video_scale, video_to_audio_scale, use_triton=triton_kernels).__get__(block, block.__class__) + else: + patched_forward = LTX2ForwardPatch(1.0, 1.0, 1.0, 1.0, use_triton=triton_kernels).__get__(block, block.__class__) + model_clone.add_object_patch(f"diffusion_model.transformer_blocks.{idx}.forward", patched_forward) + + return io.NodeOutput(model_clone) + +class LTX2MemoryEfficientSageAttentionPatch(io.ComfyNode): + + @classmethod + def define_schema(cls): + return io.Schema( + node_id="LTX2MemoryEfficientSageAttentionPatch", + display_name="LTX2 Mem Eff Sage Attention Patch", + category="KJNodes/ltxv", + description="EXPERIMENTAL! Activates custom sageattention to reduce peak VRAM usage, overrides the attention mode. Requires latest sageattention version.", + is_experimental=True, + inputs=[ + io.Model.Input("model"), + io.Boolean.Input("triton_kernels", default=True, tooltip="Use Triton fused RoPE kernel on the self-attention Q/K. Requires Triton."), + ], + outputs=[ + io.Model.Output(display_name="model"), + ], + ) + + @classmethod + def execute(cls, model, triton_kernels) -> io.NodeOutput: + if _cuda_archs is None: + raise RuntimeError("sageattention is not new enough version or could not determine CUDA architecture, cannot apply LTX2 Memory Efficient Sage Attention Patch.") + model_clone = model.clone() + diffusion_model = model_clone.get_model_object("diffusion_model") + + logging.info(f"Applying LTX2 Memory Efficient Sage Attention Patch to all transformer blocks, triton_kernels={triton_kernels}") + + for idx, block in enumerate(diffusion_model.transformer_blocks): + block.attn1.use_triton_kernels = triton_kernels + model_clone.add_object_patch(f"diffusion_model.transformer_blocks.{idx}.attn1.forward", ltx2_sageattn_forward.__get__(block.attn1, block.attn1.__class__)) + + return io.NodeOutput(model_clone) + + +def get_cuda_version(): + try: + version = torch.version.cuda + if version is not None: + major, minor = version.split('.') + return int(major), int(minor) + else: + return 0, 0 + except Exception: + return 0, 0 + +sageplus_sm89_available = False +_cuda_archs = None +try: + from sageattention.core import per_thread_int8_triton, per_warp_int8_cuda, per_block_int8_triton, per_channel_fp8, get_cuda_arch_versions, attn_false + _cuda_archs = get_cuda_arch_versions() +except Exception: + pass +try: + from sageattention.core import _qattn_sm89 + cuda_version = get_cuda_version() + sageplus_sm89_available = hasattr(_qattn_sm89, 'qk_int8_sv_f8_accum_f16_fuse_v_scale_attn_inst_buf') and cuda_version >= (12, 8) +except ImportError: + try: + from sageattention.core import sm89_compile as _qattn_sm89 + except ImportError: + _qattn_sm89 = None +try: + from sageattention.core import _qattn_sm80 +except ImportError: + try: + from sageattention.core import sm80_compile as _qattn_sm80 + except ImportError: + _qattn_sm80 = None +try: + from sageattention.core import _qattn_sm90 +except ImportError: + try: + from sageattention.core import sm90_compile as _qattn_sm90 + except ImportError: + _qattn_sm90 = None + +from comfy.ldm.lightricks.model import apply_rotary_emb +try: + from comfy.ldm.flux.math import apply_rope as _wan_apply_rope +except ImportError: + _wan_apply_rope = None +try: + from comfy.ldm.wan.model import WanT2VCrossAttention as _WanT2VCrossAttention, WanI2VCrossAttention as _WanI2VCrossAttention +except ImportError: + _WanT2VCrossAttention = _WanI2VCrossAttention = None +try: + from comfy.ldm.minimax.model import MiniMaxH3Model as _MiniMaxH3Model + from comfy.quant_ops import ck as _ck +except ImportError: + _MiniMaxH3Model = None + _ck = None + + +if HAS_TRITON: + # Vendored from sageattention's quant_per_thread.py with the row offsets promoted to int64 to avoid overflow on large sequences. + @triton.jit + def _quant_query_per_thread_int8_i64_kernel(Input, Output, Scale, L, + stride_iz, stride_ih, stride_in, + stride_oz, stride_oh, stride_on, + stride_sz, stride_sh, + C: tl.constexpr, BLK: tl.constexpr): + off_blk = tl.program_id(0) // 8 + off_tld = tl.program_id(0) % 8 + off_h = tl.program_id(1) + off_b = tl.program_id(2) + + offs_n = off_blk * BLK + tl.arange(0, BLK // 8) * 8 + off_tld + offs_k = tl.arange(0, C) + + input_ptrs = Input + off_b * stride_iz + off_h * stride_ih + offs_n[:, None].to(tl.int64) * stride_in + offs_k[None, :] + output_ptrs = Output + off_b * stride_oz + off_h * stride_oh + offs_n[:, None].to(tl.int64) * stride_on + offs_k[None, :] + scale_ptrs = Scale + off_b * stride_sz + off_h * stride_sh + off_blk * 8 + off_tld + + x = tl.load(input_ptrs, mask=offs_n[:, None] < L) + x = x.to(tl.float32) + scale = tl.max(tl.abs(x)) / 127. + 0.0000001 + x_int8 = x / scale + x_int8 += 0.5 * tl.where(x_int8 >= 0, 1, -1) + x_int8 = x_int8.to(tl.int8) + tl.store(output_ptrs, x_int8, mask=offs_n[:, None] < L) + tl.store(scale_ptrs, scale) + + @triton.jit + def _quant_key_per_thread_int8_i64_kernel(Input, Output, Scale, L, + stride_iz, stride_ih, stride_in, + stride_oz, stride_oh, stride_on, + stride_sz, stride_sh, + C: tl.constexpr, BLK: tl.constexpr): + off_blk = tl.program_id(0) // 4 + off_tld = tl.program_id(0) % 4 + off_h = tl.program_id(1) + off_b = tl.program_id(2) + + offs_n0 = off_blk * BLK + tl.arange(0, BLK // 8) * 8 + off_tld * 2 + offs_n1 = off_blk * BLK + tl.arange(0, BLK // 8) * 8 + off_tld * 2 + 1 + offs_k = tl.arange(0, C) + + input_ptrs0 = Input + off_b * stride_iz + off_h * stride_ih + offs_n0[:, None].to(tl.int64) * stride_in + offs_k[None, :] + input_ptrs1 = Input + off_b * stride_iz + off_h * stride_ih + offs_n1[:, None].to(tl.int64) * stride_in + offs_k[None, :] + output_ptrs0 = Output + off_b * stride_oz + off_h * stride_oh + offs_n0[:, None].to(tl.int64) * stride_on + offs_k[None, :] + output_ptrs1 = Output + off_b * stride_oz + off_h * stride_oh + offs_n1[:, None].to(tl.int64) * stride_on + offs_k[None, :] + scale_ptrs = Scale + off_b * stride_sz + off_h * stride_sh + off_blk * 4 + off_tld + + x0 = tl.load(input_ptrs0, mask=offs_n0[:, None] < L) + x1 = tl.load(input_ptrs1, mask=offs_n1[:, None] < L) + x0 = x0.to(tl.float32) + x1 = x1.to(tl.float32) + scale = max(tl.max(tl.abs(x0)), tl.max(tl.abs(x1))) / 127. + 0.0000001 + x0_int8 = x0 / scale + x1_int8 = x1 / scale + x0_int8 += 0.5 * tl.where(x0_int8 >= 0, 1, -1) + x1_int8 += 0.5 * tl.where(x1_int8 >= 0, 1, -1) + x0_int8 = x0_int8.to(tl.int8) + x1_int8 = x1_int8.to(tl.int8) + tl.store(output_ptrs0, x0_int8, mask=offs_n0[:, None] < L) + tl.store(output_ptrs1, x1_int8, mask=offs_n1[:, None] < L) + tl.store(scale_ptrs, scale) + + +def _per_thread_int8_i64(q, k, km=None, BLKQ=128, WARPQ=32, BLKK=64, WARPK=64, tensor_layout="NHD"): + # Drop-in for sageattention's per_thread_int8_triton, launching the int64-safe kernels above. + q_int8 = torch.empty(q.shape, dtype=torch.int8, device=q.device) + k_int8 = torch.empty(k.shape, dtype=torch.int8, device=k.device) + + if km is not None: + k = k - km + + if tensor_layout == "HND": + b, h_qo, qo_len, head_dim = q.shape + _, h_kv, kv_len, _ = k.shape + stride_bz_q, stride_h_q, stride_seq_q = q.stride(0), q.stride(1), q.stride(2) + stride_bz_qo, stride_h_qo, stride_seq_qo = q_int8.stride(0), q_int8.stride(1), q_int8.stride(2) + stride_bz_k, stride_h_k, stride_seq_k = k.stride(0), k.stride(1), k.stride(2) + stride_bz_ko, stride_h_ko, stride_seq_ko = k_int8.stride(0), k_int8.stride(1), k_int8.stride(2) + elif tensor_layout == "NHD": + b, qo_len, h_qo, head_dim = q.shape + _, kv_len, h_kv, _ = k.shape + stride_bz_q, stride_h_q, stride_seq_q = q.stride(0), q.stride(2), q.stride(1) + stride_bz_qo, stride_h_qo, stride_seq_qo = q_int8.stride(0), q_int8.stride(2), q_int8.stride(1) + stride_bz_k, stride_h_k, stride_seq_k = k.stride(0), k.stride(2), k.stride(1) + stride_bz_ko, stride_h_ko, stride_seq_ko = k_int8.stride(0), k_int8.stride(2), k_int8.stride(1) + else: + raise ValueError(f"Unknown tensor layout: {tensor_layout}") + + q_scale = torch.empty((b, h_qo, (qo_len + BLKQ - 1) // BLKQ * (BLKQ // WARPQ) * 8), device=q.device, dtype=torch.float32) + k_scale = torch.empty((b, h_kv, (kv_len + BLKK - 1) // BLKK * (BLKK // WARPK) * 4), device=q.device, dtype=torch.float32) + + grid = ((qo_len + BLKQ - 1) // BLKQ * (BLKQ // WARPQ) * 8, h_qo, b) + _quant_query_per_thread_int8_i64_kernel[grid]( + q, q_int8, q_scale, qo_len, + stride_bz_q, stride_h_q, stride_seq_q, + stride_bz_qo, stride_h_qo, stride_seq_qo, + q_scale.stride(0), q_scale.stride(1), + C=head_dim, BLK=WARPQ + ) + + grid = ((kv_len + BLKK - 1) // BLKK * (BLKK // WARPK) * 4, h_kv, b) + _quant_key_per_thread_int8_i64_kernel[grid]( + k, k_int8, k_scale, kv_len, + stride_bz_k, stride_h_k, stride_seq_k, + stride_bz_ko, stride_h_ko, stride_seq_ko, + k_scale.stride(0), k_scale.stride(1), + C=head_dim, BLK=WARPK + ) + + return q_int8, q_scale, k_int8, k_scale + + +def _sageattn_int8_fp8_nhd(qkv, dtype): + # qkv: [q, k, v], each [batch, seq_len, num_heads, head_dim] in NHD layout. Returns o in the same layout. + # The list is consumed so the only references to the float q/k/v are the locals below, letting `del` + # actually free them before the kernel runs — attention is the VRAM peak in these models. + q, k, v = qkv + qkv.clear() + head_dim_og = q.shape[-1] + + tensor_layout="NHD" + _tensor_layout = 0 # NHD + _is_caual = 0 + _qk_quant_gran = 3 + _return_lse = 0 + sm_scale = head_dim_og**-0.5 + quant_v_scale_max = 448.0 + + if _cuda_archs[0] in {"sm80", "sm86"}: + # mean-sub in-place: passing km= makes the triton quant materialize k - km as a full float copy + k.sub_(k.mean(dim=1, keepdim=True)) + q_int8, q_scale, k_int8, k_scale = _per_thread_int8_i64(q, k, tensor_layout=tensor_layout, BLKQ=128, WARPQ=32, BLKK=64, WARPK=64) + del q, k + o = torch.empty(q_int8.size(), dtype=dtype, device=q_int8.device) + v_fp16 = v.to(torch.float16) + del v + _qattn_sm80.qk_int8_sv_f16_accum_f32_attn(q_int8, k_int8, v_fp16, o, q_scale, k_scale, _tensor_layout, _is_caual, _qk_quant_gran, sm_scale, _return_lse) + elif _cuda_archs[0] == "sm75": + k.sub_(k.mean(dim=1, keepdim=True)) + q_int8, q_scale, k_int8, k_scale = per_block_int8_triton(q, k, sm_scale=sm_scale, tensor_layout=tensor_layout) + del q, k + o, _ = attn_false(q_int8, k_int8, v, q_scale, k_scale, tensor_layout=tensor_layout, output_dtype=dtype, attn_mask=None, return_lse=False) + del v + elif _cuda_archs[0] == "sm89": + if not sageplus_sm89_available: + pv_accum_dtype = "fp32+fp32" + else: + pv_accum_dtype = "fp32+fp16" + quant_v_scale_max = 2.25 + k.sub_(k.mean(dim=1, keepdim=True)) + q_int8, q_scale, k_int8, k_scale = _per_thread_int8_i64(q, k, tensor_layout=tensor_layout, BLKQ=128, WARPQ=32, BLKK=64, WARPK=64) + del q, k + v_fp8, v_scale, _ = per_channel_fp8(v, tensor_layout=tensor_layout, scale_max=quant_v_scale_max, smooth_v=False) + del v + o = torch.empty(q_int8.size(), dtype=dtype, device=q_int8.device) + if pv_accum_dtype == "fp32+fp16": + _qattn_sm89.qk_int8_sv_f8_accum_f16_fuse_v_scale_attn_inst_buf(q_int8, k_int8, v_fp8, o, q_scale, k_scale, v_scale, _tensor_layout, _is_caual, _qk_quant_gran, sm_scale, _return_lse) + elif pv_accum_dtype == "fp32+fp32": + _qattn_sm89.qk_int8_sv_f8_accum_f32_fuse_v_scale_attn_inst_buf(q_int8, k_int8, v_fp8, o, q_scale, k_scale, v_scale, _tensor_layout, _is_caual, _qk_quant_gran, sm_scale, _return_lse) + del v_fp8, v_scale + elif _cuda_archs[0] == "sm90": + k.sub_(k.mean(dim=1, keepdim=True)) + q_int8, q_scale, k_int8, k_scale = _per_thread_int8_i64(q, k, tensor_layout=tensor_layout, BLKQ=64, WARPQ=16, BLKK=128, WARPK=128) + del q, k + v_fp8, v_scale, _ = per_channel_fp8(v, tensor_layout=tensor_layout, smooth_v=False) + del v + o = torch.empty(q_int8.size(), dtype=dtype, device=q_int8.device) + _qattn_sm90.qk_int8_sv_f8_accum_f32_fuse_v_scale_attn_inst_buf(q_int8, k_int8, v_fp8, o, q_scale, k_scale, v_scale, _tensor_layout, _is_caual, _qk_quant_gran, sm_scale, _return_lse) + del v_fp8, v_scale + elif _cuda_archs[0] == "sm120": + if not sageplus_sm89_available: + pv_accum_dtype = "fp32" + else: + pv_accum_dtype = "fp32+fp16" + quant_v_scale_max = 2.25 + _qk_quant_gran = 2 # per warp + # km kept here: the CUDA quant fuses the mean-sub with no temp copy + q_int8, q_scale, k_int8, k_scale = per_warp_int8_cuda(q, k, km=k.mean(dim=1, keepdim=True), tensor_layout=tensor_layout, BLKQ=128, WARPQ=32, BLKK=64) + del q, k + v_fp8, v_scale, _ = per_channel_fp8(v, tensor_layout=tensor_layout, scale_max=quant_v_scale_max, smooth_v=False) + del v + o = torch.empty(q_int8.size(), dtype=dtype, device=q_int8.device) + if pv_accum_dtype == "fp32": + _qattn_sm89.qk_int8_sv_f8_accum_f32_fuse_v_scale_attn(q_int8, k_int8, v_fp8, o, q_scale, k_scale, v_scale, _tensor_layout, _is_caual, _qk_quant_gran, sm_scale, _return_lse) + elif pv_accum_dtype == "fp32+fp16": + _qattn_sm89.qk_int8_sv_f8_accum_f16_fuse_v_scale_attn_inst_buf(q_int8, k_int8, v_fp8, o, q_scale, k_scale, v_scale, _tensor_layout, _is_caual, _qk_quant_gran, sm_scale, _return_lse) + del v_fp8, v_scale + + del q_int8, q_scale, k_int8, k_scale + return o + + +def ltx2_sageattn_forward(self, x, context=None, mask=None, pe=None, k_pe=None, transformer_options={}): + dtype = x.dtype + context = x if context is None else context + + # query + q = self.to_q(x) + q = self.q_norm(q) + # key + k = self.to_k(context) + k = self.k_norm(k) + # apply RoPE — fuse q+k into one kernel when both share the same pe + if pe is not None: + use_triton = getattr(self, 'use_triton_kernels', False) + if k_pe is None: + q, k = fused_rope_qk(q, k, pe, use_triton=use_triton) + else: + q = apply_rotary_emb(q, pe) + k = apply_rotary_emb(k, k_pe) + # value + v = self.to_v(context) + + # Sage kernels don't support masking — fall back to the upstream masked paths. + if mask is not None: + if _GuideAttentionMask is not None and isinstance(mask, _GuideAttentionMask): + o = _ltx_attn_with_guide_mask(q, k, v, self.heads, mask, attn_precision=self.attn_precision, transformer_options=transformer_options) + else: + o = _comfy_attn.optimized_attention_masked(q, k, v, self.heads, mask, attn_precision=self.attn_precision, transformer_options=transformer_options) + if self.to_gate_logits is not None: + gate_logits = self.to_gate_logits(x) + _b, _t, _ = o.shape + o = o.view(_b, _t, self.heads, self.dim_head) + o.mul_((2.0 * torch.sigmoid(gate_logits)).unsqueeze(-1)) + o = o.view(_b, _t, self.heads * self.dim_head) + del gate_logits + return self.to_out(o) + + # Reshape from [batch, seq_len, total_dim] to [batch, seq_len, num_heads, head_dim] + batch_size, seq_len, _ = q.shape + head_dim_og = self.dim_head + + q = q.view(batch_size, seq_len, self.heads, head_dim_og) + k = k.view(batch_size, k.shape[1], self.heads, head_dim_og) + v = v.view(batch_size, v.shape[1], self.heads, head_dim_og) + + qkv = [q, k, v] + del q, k, v + o = _sageattn_int8_fp8_nhd(qkv, dtype) + + # o is [B, T, H, D] from sage kernel (NHD layout) + if self.to_gate_logits is not None: + gate_logits = self.to_gate_logits(x) # (B, T, H) + o.mul_((2.0 * torch.sigmoid(gate_logits)).unsqueeze(-1)) + del gate_logits + + return self.to_out(o.view(batch_size, seq_len, -1)) + + +def wan_sageattn_forward(self, x, freqs, transformer_options={}): + r""" + Args: + x(Tensor): Shape [B, L, num_heads, C / num_heads] + freqs(Tensor): Rope freqs, shape [1024, C / num_heads / 2] + """ + dtype = x.dtype + b, s, n, d = *x.shape[:2], self.num_heads, self.head_dim + + q = self.norm_q(self.q(x)).view(b, s, n, d) + k = self.norm_k(self.k(x)).view(b, s, n, d) + q, k = _wan_apply_rope(q, k, freqs) + v = self.v(x).view(b, s, n, d) + + qkv = [q, k, v] + del q, k, v + o = _sageattn_int8_fp8_nhd(qkv, dtype) + + return self.o(o.view(b, s, n * d)) + + +def wan_t2v_cross_sageattn_forward(self, x, context, transformer_options={}, **kwargs): + dtype = x.dtype + b, s, n, d = *x.shape[:2], self.num_heads, self.head_dim + + q = self.norm_q(self.q(x)).view(b, s, n, d) + k = self.norm_k(self.k(context)).view(b, -1, n, d) + v = self.v(context).view(b, -1, n, d) + + qkv = [q, k, v] + del q, k, v + o = _sageattn_int8_fp8_nhd(qkv, dtype) + + return self.o(o.view(b, s, n * d)) + + +def wan_i2v_cross_sageattn_forward(self, x, context, context_img_len, transformer_options={}): + dtype = x.dtype + b, s, n, d = *x.shape[:2], self.num_heads, self.head_dim + + context_img = context[:, :context_img_len] + context = context[:, context_img_len:] + + q = self.norm_q(self.q(x)).view(b, s, n, d) + k_img = self.norm_k_img(self.k_img(context_img)).view(b, -1, n, d) + v_img = self.v_img(context_img).view(b, -1, n, d) + # local q reference survives the helper's consume — still needed for the text attention below + qkv_img = [q, k_img, v_img] + del k_img, v_img + img_o = _sageattn_int8_fp8_nhd(qkv_img, dtype) + + k = self.norm_k(self.k(context)).view(b, -1, n, d) + v = self.v(context).view(b, -1, n, d) + qkv = [q, k, v] + del q, k, v + o = _sageattn_int8_fp8_nhd(qkv, dtype) + + o.add_(img_o) + del img_o + return self.o(o.view(b, s, n * d)) + + +def minimax_sageattn_forward(self, x, rope_freqs=None, transformer_options={}): + # x: [S, hidden], unbatched packed sequence; q/k/v are NHD views into the fused qkv buffer + dtype = x.dtype + s = x.shape[0] + q, k, v = self.qkv_proj(x).split(self.heads * self.head_dim, dim=-1) + q = q.view(1, s, self.heads, self.head_dim) + k = k.view(1, s, self.heads, self.head_dim) + v = v.view(1, s, self.heads, self.head_dim) + if rope_freqs is not None: + # same fused per-head RMSNorm + partial split-half rope the stock forward uses, in place on the qkv buffer + qw = mm.cast_to(self.q_norm.weight, device=x.device) + kw = mm.cast_to(self.k_norm.weight, device=x.device) + _ck.rms_rope_split_half_(q, k, rope_freqs, qw, kw, epsilon=self.q_norm.eps, rot_dim=rope_freqs.shape[-3] * 2) + else: + q = self.q_norm(q) + k = self.k_norm(k) + + qkv = [q, k, v] + del q, k, v + o = _sageattn_int8_fp8_nhd(qkv, dtype) + + return self.out_proj(o.view(s, self.heads * self.head_dim)) + + +class WanVideoMemoryEfficientSageAttentionPatch(io.ComfyNode): + + @classmethod + def define_schema(cls): + return io.Schema( + node_id="WanVideoMemoryEfficientSageAttentionPatch", + display_name="WanVideo Mem Eff Sage Attention Patch", + category="KJNodes/wan", + description="EXPERIMENTAL! Activates custom sageattention on the WanVideo self-attention to reduce peak VRAM usage, overrides the attention mode. Requires latest sageattention version.", + is_experimental=True, + inputs=[ + io.Model.Input("model"), + ], + outputs=[ + io.Model.Output(display_name="model"), + ], + ) + + @classmethod + def execute(cls, model) -> io.NodeOutput: + if _cuda_archs is None: + raise RuntimeError("sageattention is not new enough version or could not determine CUDA architecture, cannot apply WanVideo Memory Efficient Sage Attention Patch.") + if _wan_apply_rope is None: + raise RuntimeError("Could not import apply_rope from comfy.ldm.flux.math, cannot apply WanVideo Memory Efficient Sage Attention Patch.") + model_clone = model.clone() + diffusion_model = model_clone.get_model_object("diffusion_model") + + logging.info("Applying WanVideo Memory Efficient Sage Attention Patch to all transformer blocks") + + for idx, block in enumerate(diffusion_model.blocks): + model_clone.add_object_patch(f"diffusion_model.blocks.{idx}.self_attn.forward", wan_sageattn_forward.__get__(block.self_attn, block.self_attn.__class__)) + cross_attn = getattr(block, "cross_attn", None) + # exact type match on purpose: subclasses like WanT2VCrossAttentionGather have different semantics + if cross_attn is not None and type(cross_attn) is _WanI2VCrossAttention: + model_clone.add_object_patch(f"diffusion_model.blocks.{idx}.cross_attn.forward", wan_i2v_cross_sageattn_forward.__get__(cross_attn, cross_attn.__class__)) + elif cross_attn is not None and type(cross_attn) is _WanT2VCrossAttention: + model_clone.add_object_patch(f"diffusion_model.blocks.{idx}.cross_attn.forward", wan_t2v_cross_sageattn_forward.__get__(cross_attn, cross_attn.__class__)) + + return io.NodeOutput(model_clone) + + +class MiniMaxH3MemoryEfficientSageAttentionPatch(io.ComfyNode): + + @classmethod + def define_schema(cls): + return io.Schema( + node_id="MiniMaxH3MemoryEfficientSageAttentionPatch", + display_name="MiniMax H3 Mem Eff Sage Attention Patch", + category="KJNodes/minimax", + description="EXPERIMENTAL! Activates custom sageattention on the MiniMax H3 self-attention to reduce peak VRAM usage, overrides the attention mode. Requires latest sageattention version.", + is_experimental=True, + inputs=[ + io.Model.Input("model"), + ], + outputs=[ + io.Model.Output(display_name="model"), + ], + ) + + @classmethod + def execute(cls, model) -> io.NodeOutput: + if _cuda_archs is None: + raise RuntimeError("sageattention is not new enough version or could not determine CUDA architecture, cannot apply MiniMax H3 Memory Efficient Sage Attention Patch.") + if _ck is None: + raise RuntimeError("This ComfyUI version does not support MiniMax H3, cannot apply MiniMax H3 Memory Efficient Sage Attention Patch.") + model_clone = model.clone() + diffusion_model = model_clone.get_model_object("diffusion_model") + if _MiniMaxH3Model is not None and not isinstance(diffusion_model, _MiniMaxH3Model): + raise RuntimeError("MiniMax H3 Memory Efficient Sage Attention Patch can only be applied to a MiniMax H3 model.") + + logging.info("Applying MiniMax H3 Memory Efficient Sage Attention Patch to all transformer blocks") + + for idx, block in enumerate(diffusion_model.blocks): + model_clone.add_object_patch(f"diffusion_model.blocks.{idx}.attn.forward", minimax_sageattn_forward.__get__(block.attn, block.attn.__class__)) + + return io.NodeOutput(model_clone) + + +import folder_paths + +class LTX2LoraLoaderAdvanced(io.ComfyNode): + + @classmethod + def define_schema(cls): + return io.Schema( + node_id="LTX2LoraLoaderAdvanced", + display_name="LTX2 LoRA Loader Advanced", + category="KJNodes/ltxv", + description="Advanced LoRA loader with per-block strength control for LTX2 models", + is_experimental=True, + inputs=[ + io.Combo.Input("lora_name", options=folder_paths.get_filename_list("loras"), tooltip="The name of the LoRA."), + io.Model.Input("model", tooltip="The diffusion model the LoRA will be applied to."), + io.Float.Input("strength_model", default=1.0, min=-100.0, max=100.0, step=0.01, tooltip="How strongly to modify the diffusion model. This value can be negative."), + io.String.Input("opt_lora_path", optional=True, force_input=True,tooltip="Absolute path of the LoRA."), + io.Custom("SELECTEDDITBLOCKS").Input("blocks", optional=True, tooltip="Selected DiT blocks configuration"), + io.Float.Input("video", default=1.0, min=0.0, max=1.0, step=0.01, tooltip="Strength for video attention layers."), + io.Float.Input("video_to_audio", default=1.0, min=0.0, max=1.0, step=0.01, tooltip="Strength for video to audio cross-attention layers."), + io.Float.Input("audio", default=1.0, min=0.0, max=1.0, step=0.01, tooltip="Strength for audio attention layers."), + io.Float.Input("audio_to_video", default=1.0, min=0.0, max=1.0, step=0.01, tooltip="Strength for audio to video cross-attention layers."), + io.Float.Input("other", default=1.0, min=0.0, max=1.0, step=0.01, tooltip="Strength for layers not caught by other layer filters."), + ], + outputs=[ + io.Model.Output(display_name="model", tooltip="The modified diffusion model."), + io.String.Output(display_name="rank", tooltip="Possible rank of the LoRA."), + io.String.Output(display_name="loaded_keys_info", tooltip="List of loaded keys and their alpha values."), + ], + ) + + @classmethod + def execute(cls, model, lora_name, strength_model, video, video_to_audio, audio, audio_to_video, other, opt_lora_path=None, blocks=None) -> io.NodeOutput: + from comfy.utils import load_torch_file + import comfy.lora + + if opt_lora_path: + lora_path = opt_lora_path + else: + lora_path = folder_paths.get_full_path("loras", lora_name) + + lora = load_torch_file(lora_path, safe_load=True) + + # Find the first key that ends with "weight" + rank = "unknown" + weight_key = next((key for key in lora.keys() if key.endswith('weight')), None) + # Print the shape of the value corresponding to the key + if weight_key: + print(f"Shape of the first 'weight' key ({weight_key}): {lora[weight_key].shape}") + rank = str(lora[weight_key].shape[0]) + else: + print("No key ending with 'weight' found.") + rank = "Couldn't find rank" + + key_map = {} + if model is not None: + key_map = comfy.lora.model_lora_keys_unet(model.model, key_map) + + loaded = comfy.lora.load_lora(lora, key_map) + + keys_to_delete = [] + + # First apply blocks filtering if provided + if blocks is not None: + for block in blocks: + for key in list(loaded.keys()): + match = False + if isinstance(key, str) and block in key: + match = True + elif isinstance(key, tuple): + for k in key: + if block in k: + match = True + break + + if match: + ratio = blocks[block] + if ratio == 0: + keys_to_delete.append(key) + else: + # Only modify LoRA adapters, skip diff tuples + value = loaded[key] + if hasattr(value, 'weights'): + weights_list = list(value.weights) + weights_list[2] = ratio + loaded[key].weights = tuple(weights_list) + + # Then apply layer-based attention strength filtering (takes priority) + for key in list(loaded.keys()): + if key in keys_to_delete: + continue + + key_str = key if isinstance(key, str) else (key[0] if isinstance(key, tuple) else str(key)) + + # Determine the strength multiplier based on layer name + # Check more specific patterns first + strength_multiplier = None + + # Video to audio cross-attention (check first - most specific) + if "video_to_audio_attn" in key_str: + strength_multiplier = video_to_audio + # Audio to video cross-attention + elif "audio_to_video_attn" in key_str: + strength_multiplier = audio_to_video + # Audio layers + elif "audio_attn" in key_str or "audio_ff.net" in key_str: + strength_multiplier = audio + # Video layers (check last - most general) + elif "attn" in key_str or "ff.net" in key_str: + strength_multiplier = video + # Everything else not caught by above filters + else: + strength_multiplier = other + + # Apply strength or mark for deletion + if strength_multiplier is not None: + if strength_multiplier == 0: + keys_to_delete.append(key) + elif strength_multiplier != 1.0: + value = loaded[key] + if hasattr(value, 'weights'): + weights_list = list(value.weights) + # Handle case where alpha (weights[2]) might be None + current_alpha = weights_list[2] if weights_list[2] is not None else 1.0 + weights_list[2] = current_alpha * strength_multiplier + loaded[key].weights = tuple(weights_list) + + for key in keys_to_delete: + if key in loaded: + del loaded[key] + + # Build list of loaded keys and their alphas + loaded_keys_list = [] + for key, value in loaded.items(): + if hasattr(value, 'weights'): + key_str = key if isinstance(key, str) else str(key) + alpha = value.weights[2] if value.weights[2] is not None else "None" + loaded_keys_list.append(f"{key_str}: alpha={alpha}") + else: + key_str = key if isinstance(key, str) else str(key) + loaded_keys_list.append(f"{key_str}: type={type(value).__name__}") + + if model is not None: + new_modelpatcher = model.clone() + k = new_modelpatcher.add_patches(loaded, strength_model) + + # Add not loaded keys to the info + k = set(k) + not_loaded = [] + for x in loaded: + if x not in k: + key_str = x if isinstance(x, str) else str(x) + not_loaded.append(f"NOT LOADED: {key_str}") + + if not_loaded: + loaded_keys_list.extend(not_loaded) + + loaded_keys_info = "\n".join(loaded_keys_list) + + return io.NodeOutput(new_modelpatcher, rank, loaded_keys_info) diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/mask_nodes.py b/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/mask_nodes.py new file mode 100644 index 0000000000000000000000000000000000000000..ca4dc69ce972751f85d2b80e1c3e5e2de1d82099 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/mask_nodes.py @@ -0,0 +1,1689 @@ +import torch +import torch.nn.functional as F +from torchvision.transforms import functional as TF +from PIL import Image, ImageDraw, ImageFilter, ImageFont +import scipy.ndimage +import numpy as np +from contextlib import nullcontext +import os +from tqdm import tqdm +import logging + +from comfy import model_management +from comfy.utils import ProgressBar +from comfy.utils import common_upscale +from nodes import MAX_RESOLUTION + +import folder_paths + +from ..utility.utility import tensor2pil, pil2tensor + +script_directory = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +main_device = model_management.get_torch_device() +offload_device = model_management.unet_offload_device() + +class BatchCLIPSeg: + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(s): + + return {"required": + { + "images": ("IMAGE",), + "text": ("STRING", {"multiline": False}), + "threshold": ("FLOAT", {"default": 0.5,"min": 0.0, "max": 10.0, "step": 0.001}), + "binary_mask": ("BOOLEAN", {"default": True}), + "combine_mask": ("BOOLEAN", {"default": False}), + "use_cuda": ("BOOLEAN", {"default": True}), + }, + "optional": + { + "blur_sigma": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 100.0, "step": 0.1}), + "opt_model": ("CLIPSEGMODEL", ), + "prev_mask": ("MASK", {"default": None}), + "image_bg_level": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}), + "invert": ("BOOLEAN", {"default": False}), + } + } + + CATEGORY = "KJNodes/masking" + RETURN_TYPES = ("MASK", "IMAGE", ) + RETURN_NAMES = ("Mask", "Image", ) + FUNCTION = "segment_image" + DESCRIPTION = """ +Segments an image or batch of images using CLIPSeg. +""" + + def segment_image(self, images, text, threshold, binary_mask, combine_mask, use_cuda, blur_sigma=0.0, opt_model=None, prev_mask=None, invert= False, image_bg_level=0.5): + from transformers import CLIPSegProcessor, CLIPSegForImageSegmentation + import torchvision.transforms as transforms + offload_device = model_management.unet_offload_device() + device = model_management.get_torch_device() + if not use_cuda: + device = torch.device("cpu") + dtype = model_management.unet_dtype() + + if opt_model is None: + checkpoint_path = os.path.join(folder_paths.models_dir,'clip_seg', 'clipseg-rd64-refined-fp16') + if not hasattr(self, "model"): + try: + if not os.path.exists(checkpoint_path): + from huggingface_hub import snapshot_download + snapshot_download(repo_id="Kijai/clipseg-rd64-refined-fp16", local_dir=checkpoint_path, local_dir_use_symlinks=False) + self.model = CLIPSegForImageSegmentation.from_pretrained(checkpoint_path) + except Exception: + checkpoint_path = "CIDAS/clipseg-rd64-refined" + self.model = CLIPSegForImageSegmentation.from_pretrained(checkpoint_path) + processor = CLIPSegProcessor.from_pretrained(checkpoint_path) + + else: + self.model = opt_model['model'] + processor = opt_model['processor'] + + self.model.to(dtype).to(device) + + B, H, W, C = images.shape + images = images.to(device) + + autocast_condition = (dtype != torch.float32) and not model_management.is_device_mps(device) + with torch.autocast(model_management.get_autocast_device(device), dtype=dtype) if autocast_condition else nullcontext(): + + PIL_images = [Image.fromarray(np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8)) for image in images ] + prompt = [text] * len(images) + input_prc = processor(text=prompt, images=PIL_images, return_tensors="pt") + + for key in input_prc: + input_prc[key] = input_prc[key].to(device) + outputs = self.model(**input_prc) + + mask_tensor = torch.sigmoid(outputs.logits) + mask_tensor = (mask_tensor - mask_tensor.min()) / (mask_tensor.max() - mask_tensor.min()) + mask_tensor = torch.where(mask_tensor > (threshold), mask_tensor, torch.tensor(0, dtype=torch.float)) + + if len(mask_tensor.shape) == 2: + mask_tensor = mask_tensor.unsqueeze(0) + mask_tensor = F.interpolate(mask_tensor.unsqueeze(1), size=(H, W), mode='nearest') + mask_tensor = mask_tensor.squeeze(1) + + self.model.to(offload_device) + + if binary_mask: + mask_tensor = (mask_tensor > 0).float() + if blur_sigma > 0: + kernel_size = int(6 * int(blur_sigma) + 1) + blur = transforms.GaussianBlur(kernel_size=(kernel_size, kernel_size), sigma=(blur_sigma, blur_sigma)) + mask_tensor = blur(mask_tensor) + + if combine_mask: + mask_tensor = torch.max(mask_tensor, dim=0)[0] + mask_tensor = mask_tensor.unsqueeze(0).repeat(len(images),1,1) + + del outputs + model_management.soft_empty_cache() + + if prev_mask is not None: + if prev_mask.shape != mask_tensor.shape: + prev_mask = F.interpolate(prev_mask.unsqueeze(1), size=(H, W), mode='nearest') + mask_tensor = mask_tensor + prev_mask.to(device) + torch.clamp(mask_tensor, min=0.0, max=1.0) + + if invert: + mask_tensor = 1 - mask_tensor + + image_tensor = images * mask_tensor.unsqueeze(-1) + (1 - mask_tensor.unsqueeze(-1)) * image_bg_level + image_tensor = torch.clamp(image_tensor, min=0.0, max=1.0).cpu().float() + + mask_tensor = mask_tensor.cpu().float() + + return mask_tensor, image_tensor, + +class DownloadAndLoadCLIPSeg: + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(s): + + return {"required": + { + "model": ( + [ 'Kijai/clipseg-rd64-refined-fp16', + 'CIDAS/clipseg-rd64-refined', + ], + ), + }, + } + + CATEGORY = "KJNodes/masking" + RETURN_TYPES = ("CLIPSEGMODEL",) + RETURN_NAMES = ("clipseg_model",) + FUNCTION = "segment_image" + DESCRIPTION = """ +Downloads and loads CLIPSeg model with huggingface_hub, +to ComfyUI/models/clip_seg +""" + + def segment_image(self, model): + from transformers import CLIPSegProcessor, CLIPSegForImageSegmentation + checkpoint_path = os.path.join(folder_paths.models_dir,'clip_seg', os.path.basename(model)) + if not hasattr(self, "model"): + if not os.path.exists(checkpoint_path): + from huggingface_hub import snapshot_download + snapshot_download(repo_id=model, local_dir=checkpoint_path, local_dir_use_symlinks=False) + self.model = CLIPSegForImageSegmentation.from_pretrained(checkpoint_path) + + processor = CLIPSegProcessor.from_pretrained(checkpoint_path) + + clipseg_model = {} + clipseg_model['model'] = self.model + clipseg_model['processor'] = processor + + return clipseg_model, + +class CreateTextMask: + + RETURN_TYPES = ("IMAGE", "MASK",) + FUNCTION = "createtextmask" + CATEGORY = "KJNodes/text" + DESCRIPTION = """ +Creates a text image and mask. +Looks for fonts from this folder: +ComfyUI/custom_nodes/ComfyUI-KJNodes/fonts + +If start_rotation and/or end_rotation are different values, +creates animation between them. +""" + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "invert": ("BOOLEAN", {"default": False}), + "frames": ("INT", {"default": 1,"min": 1, "max": 4096, "step": 1}), + "text_x": ("INT", {"default": 0,"min": 0, "max": 4096, "step": 1}), + "text_y": ("INT", {"default": 0,"min": 0, "max": 4096, "step": 1}), + "font_size": ("INT", {"default": 32,"min": 8, "max": 4096, "step": 1}), + "font_color": ("STRING", {"default": "white"}), + "text": ("STRING", {"default": "HELLO!", "multiline": True}), + "font": (folder_paths.get_filename_list("kjnodes_fonts"), ), + "width": ("INT", {"default": 512,"min": 16, "max": 4096, "step": 1}), + "height": ("INT", {"default": 512,"min": 16, "max": 4096, "step": 1}), + "start_rotation": ("INT", {"default": 0,"min": 0, "max": 359, "step": 1}), + "end_rotation": ("INT", {"default": 0,"min": -359, "max": 359, "step": 1}), + }, + } + + def createtextmask(self, frames, width, height, invert, text_x, text_y, text, font_size, font_color, font, start_rotation, end_rotation): + # Define the number of images in the batch + batch_size = frames + out = [] + masks = [] + rotation = start_rotation + if start_rotation != end_rotation: + rotation_increment = (end_rotation - start_rotation) / (batch_size - 1) + + font_path = folder_paths.get_full_path("kjnodes_fonts", font) + # Generate the text + for i in range(batch_size): + image = Image.new("RGB", (width, height), "black") + draw = ImageDraw.Draw(image) + font = ImageFont.truetype(font_path, font_size) + + # Split the text into lines and wrap words to fit width + text_lines = text.split('\n') + lines = [] + for text_line in text_lines: + if text_line.strip() == "": + # Preserve empty lines for multiple newlines + lines.append("") + continue + words = text_line.split() + current_line = [] + for word in words: + if current_line: + test_line = " ".join(current_line + [word]) + else: + test_line = word + try: + test_line_width = font.getbbox(test_line)[2] + except Exception: + test_line_width = font.getsize(test_line)[0] + if test_line_width <= width - 2 * text_x: + current_line.append(word) + else: + lines.append(" ".join(current_line)) + current_line = [word] + if current_line: + lines.append(" ".join(current_line)) + + # Draw each line of text separately + y_offset = text_y + for line in lines: + text_width = font.getlength(line) + text_height = font_size + text_center_x = text_x + text_width / 2 + text_center_y = y_offset + text_height / 2 + try: + draw.text((text_x, y_offset), line, font=font, fill=font_color, features=['-liga']) + except Exception: + draw.text((text_x, y_offset), line, font=font, fill=font_color) + y_offset += text_height # Move to the next line + + if start_rotation != end_rotation: + image = image.rotate(rotation, center=(text_center_x, text_center_y)) + rotation += rotation_increment + + image = np.array(image).astype(np.float32) / 255.0 + image = torch.from_numpy(image)[None,] + mask = image[:, :, :, 0] + masks.append(mask) + out.append(image) + + if invert: + return (1.0 - torch.cat(out, dim=0), 1.0 - torch.cat(masks, dim=0),) + return (torch.cat(out, dim=0),torch.cat(masks, dim=0),) + +class ColorToMask: + + RETURN_TYPES = ("MASK",) + FUNCTION = "clip" + CATEGORY = "KJNodes/masking" + DESCRIPTION = """ +Converts chosen RGB value to a mask. +With batch inputs, the **per_batch** +controls the number of images processed at once. +""" + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "images": ("IMAGE",), + "invert": ("BOOLEAN", {"default": False}), + "red": ("INT", {"default": 0,"min": 0, "max": 255, "step": 1}), + "green": ("INT", {"default": 0,"min": 0, "max": 255, "step": 1}), + "blue": ("INT", {"default": 0,"min": 0, "max": 255, "step": 1}), + "threshold": ("INT", {"default": 10,"min": 0, "max": 255, "step": 1}), + "per_batch": ("INT", {"default": 16, "min": 1, "max": 4096, "step": 1}), + }, + } + + def clip(self, images, red, green, blue, threshold, invert, per_batch): + + color = torch.tensor([red, green, blue], dtype=torch.uint8) + black = torch.tensor([0, 0, 0], dtype=torch.uint8) + white = torch.tensor([255, 255, 255], dtype=torch.uint8) + + if invert: + black, white = white, black + + steps = images.shape[0] + pbar = ProgressBar(steps) + tensors_out = [] + + for start_idx in range(0, images.shape[0], per_batch): + + # Calculate color distances + color_distances = torch.norm(images[start_idx:start_idx+per_batch] * 255 - color, dim=-1) + + # Create a mask based on the threshold + mask = color_distances <= threshold + + # Apply the mask to create new images + mask_out = torch.where(mask.unsqueeze(-1), white, black).float() + mask_out = mask_out.mean(dim=-1) + + tensors_out.append(mask_out.cpu()) + batch_count = mask_out.shape[0] + pbar.update(batch_count) + + tensors_out = torch.cat(tensors_out, dim=0) + tensors_out = torch.clamp(tensors_out, min=0.0, max=1.0) + return tensors_out, + +class CreateFluidMask: + + RETURN_TYPES = ("IMAGE", "MASK") + FUNCTION = "createfluidmask" + CATEGORY = "KJNodes/masking/generate" + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "invert": ("BOOLEAN", {"default": False}), + "frames": ("INT", {"default": 1,"min": 1, "max": 4096, "step": 1}), + "width": ("INT", {"default": 256,"min": 16, "max": 4096, "step": 1}), + "height": ("INT", {"default": 256,"min": 16, "max": 4096, "step": 1}), + "inflow_count": ("INT", {"default": 3,"min": 0, "max": 255, "step": 1}), + "inflow_velocity": ("INT", {"default": 1,"min": 0, "max": 255, "step": 1}), + "inflow_radius": ("INT", {"default": 8,"min": 0, "max": 255, "step": 1}), + "inflow_padding": ("INT", {"default": 50,"min": 0, "max": 255, "step": 1}), + "inflow_duration": ("INT", {"default": 60,"min": 0, "max": 255, "step": 1}), + }, + } + #using code from https://github.com/GregTJ/stable-fluids + def createfluidmask(self, frames, width, height, invert, inflow_count, inflow_velocity, inflow_radius, inflow_padding, inflow_duration): + from ..utility.fluid import Fluid + try: + from scipy.special import erf + except ImportError: + from scipy.spatial import erf + out = [] + masks = [] + RESOLUTION = width, height + DURATION = frames + + INFLOW_PADDING = inflow_padding + INFLOW_DURATION = inflow_duration + INFLOW_RADIUS = inflow_radius + INFLOW_VELOCITY = inflow_velocity + INFLOW_COUNT = inflow_count + + logging.info('Generating fluid solver, this may take some time.') + fluid = Fluid(RESOLUTION, 'dye') + + center = np.floor_divide(RESOLUTION, 2) + r = np.min(center) - INFLOW_PADDING + + points = np.linspace(-np.pi, np.pi, INFLOW_COUNT, endpoint=False) + points = tuple(np.array((np.cos(p), np.sin(p))) for p in points) + normals = tuple(-p for p in points) + points = tuple(r * p + center for p in points) + + inflow_velocity = np.zeros_like(fluid.velocity) + inflow_dye = np.zeros(fluid.shape) + for p, n in zip(points, normals): + mask = np.linalg.norm(fluid.indices - p[:, None, None], axis=0) <= INFLOW_RADIUS + inflow_velocity[:, mask] += n[:, None] * INFLOW_VELOCITY + inflow_dye[mask] = 1 + + + for f in range(DURATION): + logging.info(f'Computing frame {f + 1} of {DURATION}.') + if f <= INFLOW_DURATION: + fluid.velocity += inflow_velocity + fluid.dye += inflow_dye + + curl = fluid.step()[1] + # Using the error function to make the contrast a bit higher. + # Any other sigmoid function e.g. smoothstep would work. + curl = (erf(curl * 2) + 1) / 4 + + color = np.dstack((curl, np.ones(fluid.shape), fluid.dye)) + color = (np.clip(color, 0, 1) * 255).astype('uint8') + image = np.array(color).astype(np.float32) / 255.0 + image = torch.from_numpy(image)[None,] + mask = image[:, :, :, 0] + masks.append(mask) + out.append(image) + + if invert: + return (1.0 - torch.cat(out, dim=0),1.0 - torch.cat(masks, dim=0),) + return (torch.cat(out, dim=0),torch.cat(masks, dim=0),) + +class CreateAudioMask: + + RETURN_TYPES = ("IMAGE",) + FUNCTION = "createaudiomask" + CATEGORY = "KJNodes/deprecated" + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "invert": ("BOOLEAN", {"default": False}), + "frames": ("INT", {"default": 16,"min": 1, "max": 255, "step": 1}), + "scale": ("FLOAT", {"default": 0.5,"min": 0.0, "max": 2.0, "step": 0.01}), + "audio_path": ("STRING", {"default": "audio.wav"}), + "width": ("INT", {"default": 256,"min": 16, "max": 4096, "step": 1}), + "height": ("INT", {"default": 256,"min": 16, "max": 4096, "step": 1}), + }, + } + + def createaudiomask(self, frames, width, height, invert, audio_path, scale): + try: + import librosa + except ImportError as e: + raise ImportError("Can not import librosa. Install it with 'pip install librosa'") from e + batch_size = frames + out = [] + masks = [] + if audio_path == "audio.wav": #I don't know why relative path won't work otherwise... + audio_path = os.path.join(script_directory, audio_path) + audio, sr = librosa.load(audio_path) + spectrogram = np.abs(librosa.stft(audio)) + + for i in range(batch_size): + image = Image.new("RGB", (width, height), "black") + draw = ImageDraw.Draw(image) + frame = spectrogram[:, i] + circle_radius = int(height * np.mean(frame)) + circle_radius *= scale + circle_center = (width // 2, height // 2) # Calculate the center of the image + + draw.ellipse([(circle_center[0] - circle_radius, circle_center[1] - circle_radius), + (circle_center[0] + circle_radius, circle_center[1] + circle_radius)], + fill='white') + + image = np.array(image).astype(np.float32) / 255.0 + image = torch.from_numpy(image)[None,] + mask = image[:, :, :, 0] + masks.append(mask) + out.append(image) + + if invert: + return (1.0 - torch.cat(out, dim=0),) + return (torch.cat(out, dim=0),torch.cat(masks, dim=0),) + +class CreateGradientMask: + + RETURN_TYPES = ("MASK",) + FUNCTION = "createmask" + CATEGORY = "KJNodes/masking/generate" + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "invert": ("BOOLEAN", {"default": False}), + "frames": ("INT", {"default": 0,"min": 0, "max": 255, "step": 1}), + "width": ("INT", {"default": 256,"min": 16, "max": 4096, "step": 1}), + "height": ("INT", {"default": 256,"min": 16, "max": 4096, "step": 1}), + }, + } + def createmask(self, frames, width, height, invert): + # Define the number of images in the batch + batch_size = frames + out = [] + # Create an empty array to store the image batch + image_batch = np.zeros((batch_size, height, width), dtype=np.float32) + # Generate the black to white gradient for each image + for i in range(batch_size): + gradient = np.linspace(1.0, 0.0, width, dtype=np.float32) + time = i / frames # Calculate the time variable + offset_gradient = gradient - time # Offset the gradient values based on time + image_batch[i] = offset_gradient.reshape(1, -1) + output = torch.from_numpy(image_batch) + mask = output + out.append(mask) + if invert: + return (1.0 - torch.cat(out, dim=0),) + return (torch.cat(out, dim=0),) + +class CreateFadeMask: + + RETURN_TYPES = ("MASK",) + FUNCTION = "createfademask" + CATEGORY = "KJNodes/deprecated" + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "invert": ("BOOLEAN", {"default": False}), + "frames": ("INT", {"default": 2,"min": 2, "max": 10000, "step": 1}), + "width": ("INT", {"default": 256,"min": 16, "max": 4096, "step": 1}), + "height": ("INT", {"default": 256,"min": 16, "max": 4096, "step": 1}), + "interpolation": (["linear", "ease_in", "ease_out", "ease_in_out"],), + "start_level": ("FLOAT", {"default": 1.0,"min": 0.0, "max": 1.0, "step": 0.01}), + "midpoint_level": ("FLOAT", {"default": 0.5,"min": 0.0, "max": 1.0, "step": 0.01}), + "end_level": ("FLOAT", {"default": 0.0,"min": 0.0, "max": 1.0, "step": 0.01}), + "midpoint_frame": ("INT", {"default": 0,"min": 0, "max": 4096, "step": 1}), + }, + } + + def createfademask(self, frames, width, height, invert, interpolation, start_level, midpoint_level, end_level, midpoint_frame): + def ease_in(t): + return t * t + + def ease_out(t): + return 1 - (1 - t) * (1 - t) + + def ease_in_out(t): + return 3 * t * t - 2 * t * t * t + + batch_size = frames + out = [] + image_batch = np.zeros((batch_size, height, width), dtype=np.float32) + + if midpoint_frame == 0: + midpoint_frame = batch_size // 2 + + for i in range(batch_size): + if i <= midpoint_frame: + t = i / midpoint_frame + if interpolation == "ease_in": + t = ease_in(t) + elif interpolation == "ease_out": + t = ease_out(t) + elif interpolation == "ease_in_out": + t = ease_in_out(t) + color = start_level - t * (start_level - midpoint_level) + else: + t = (i - midpoint_frame) / (batch_size - midpoint_frame) + if interpolation == "ease_in": + t = ease_in(t) + elif interpolation == "ease_out": + t = ease_out(t) + elif interpolation == "ease_in_out": + t = ease_in_out(t) + color = midpoint_level - t * (midpoint_level - end_level) + + color = np.clip(color, 0, 255) + image = np.full((height, width), color, dtype=np.float32) + image_batch[i] = image + + output = torch.from_numpy(image_batch) + mask = output + out.append(mask) + + if invert: + return (1.0 - torch.cat(out, dim=0),) + return (torch.cat(out, dim=0),) + +class CreateFadeMaskAdvanced: + + RETURN_TYPES = ("MASK",) + FUNCTION = "createfademask" + CATEGORY = "KJNodes/masking/generate" + DESCRIPTION = """ +Create a batch of masks interpolated between given frames and values. +Uses same syntax as Fizz' BatchValueSchedule. +First value is the frame index (not that this starts from 0, not 1) +and the second value inside the brackets is the float value of the mask in range 0.0 - 1.0 + +For example the default values: +0:(0.0) +7:(1.0) +15:(0.0) + +Would create a mask batch fo 16 frames, starting from black, +interpolating with the chosen curve to fully white at the 8th frame, +and interpolating from that to fully black at the 16th frame. +""" + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "points_string": ("STRING", {"default": "0:(0.0),\n7:(1.0),\n15:(0.0)\n", "multiline": True}), + "invert": ("BOOLEAN", {"default": False}), + "frames": ("INT", {"default": 16,"min": 2, "max": 10000, "step": 1}), + "width": ("INT", {"default": 512,"min": 1, "max": 4096, "step": 1}), + "height": ("INT", {"default": 512,"min": 1, "max": 4096, "step": 1}), + "interpolation": (["linear", "ease_in", "ease_out", "ease_in_out", "none", "default_to_black"],), + }, + } + + def createfademask(self, frames, width, height, invert, points_string, interpolation): + def ease_in(t): + return t * t + + def ease_out(t): + return 1 - (1 - t) * (1 - t) + + def ease_in_out(t): + return 3 * t * t - 2 * t * t * t + + # Parse the input string into a list of tuples + points = [] + points_string = points_string.rstrip(',\n') + for point_str in points_string.split(','): + frame_str, color_str = point_str.split(':') + frame = int(frame_str.strip()) + color = float(color_str.strip()[1:-1]) # Remove parentheses around color + points.append((frame, color)) + + # Check if the last frame is already in the points + if (interpolation != "default_to_black") and (len(points) == 0 or points[-1][0] != frames - 1): + # If not, add it with the color of the last specified frame + points.append((frames - 1, points[-1][1] if points else 0)) + + # Sort the points by frame number + points.sort(key=lambda x: x[0]) + + batch_size = frames + out = [] + image_batch = np.zeros((batch_size, height, width), dtype=np.float32) + + # Index of the next point to interpolate towards + next_point = 1 + + for i in range(batch_size): + while next_point < len(points) and i > points[next_point][0]: + next_point += 1 + + # Interpolate between the previous point and the next point + prev_point = next_point - 1 + + if interpolation == "none": + exact_match = False + for p in points: + if p[0] == i: # Exact frame match + color = p[1] + exact_match = True + break + if not exact_match: + color = points[prev_point][1] + + elif interpolation == "default_to_black": + exact_match = False + for p in points: + if p[0] == i: # Exact frame match + color = p[1] + exact_match = True + break + if not exact_match: + color = 0 + else: + t = (i - points[prev_point][0]) / (points[next_point][0] - points[prev_point][0]) + if interpolation == "ease_in": + t = ease_in(t) + elif interpolation == "ease_out": + t = ease_out(t) + elif interpolation == "ease_in_out": + t = ease_in_out(t) + elif interpolation == "linear": + pass # No need to modify `t` for linear interpolation + + color = points[prev_point][1] - t * (points[prev_point][1] - points[next_point][1]) + + color = np.clip(color, 0, 255) + image = np.full((height, width), color, dtype=np.float32) + image_batch[i] = image + + output = torch.from_numpy(image_batch) + mask = output + out.append(mask) + + if invert: + return (1.0 - torch.cat(out, dim=0),) + return (torch.cat(out, dim=0),) + +class CreateMagicMask: + + RETURN_TYPES = ("MASK", "MASK",) + RETURN_NAMES = ("mask", "mask_inverted",) + FUNCTION = "createmagicmask" + CATEGORY = "KJNodes/masking/generate" + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "frames": ("INT", {"default": 16,"min": 2, "max": 4096, "step": 1}), + "depth": ("INT", {"default": 12,"min": 1, "max": 500, "step": 1}), + "distortion": ("FLOAT", {"default": 1.5,"min": 0.0, "max": 100.0, "step": 0.01}), + "seed": ("INT", {"default": 123,"min": 0, "max": 99999999, "step": 1}), + "transitions": ("INT", {"default": 1,"min": 1, "max": 20, "step": 1}), + "frame_width": ("INT", {"default": 512,"min": 16, "max": 4096, "step": 1}), + "frame_height": ("INT", {"default": 512,"min": 16, "max": 4096, "step": 1}), + }, + } + + def createmagicmask(self, frames, transitions, depth, distortion, seed, frame_width, frame_height): + from ..utility.magictex import coordinate_grid, random_transform, magic + import matplotlib.pyplot as plt + rng = np.random.default_rng(seed) + out = [] + coords = coordinate_grid((frame_width, frame_height)) + + # Calculate the number of frames for each transition + frames_per_transition = frames // transitions + + # Generate a base set of parameters + base_params = { + "coords": random_transform(coords, rng), + "depth": depth, + "distortion": distortion, + } + for t in range(transitions): + # Generate a second set of parameters that is at most max_diff away from the base parameters + params1 = base_params.copy() + params2 = base_params.copy() + + params1['coords'] = random_transform(coords, rng) + params2['coords'] = random_transform(coords, rng) + + for i in range(frames_per_transition): + # Compute the interpolation factor + alpha = i / frames_per_transition + + # Interpolate between the two sets of parameters + params = params1.copy() + params['coords'] = (1 - alpha) * params1['coords'] + alpha * params2['coords'] + + tex = magic(**params) + + dpi = frame_width / 10 + fig = plt.figure(figsize=(10, 10), dpi=dpi) + + ax = fig.add_subplot(111) + plt.subplots_adjust(left=0, right=1, bottom=0, top=1) + + ax.get_yaxis().set_ticks([]) + ax.get_xaxis().set_ticks([]) + ax.imshow(tex, aspect='auto') + + fig.canvas.draw() + img = np.array(fig.canvas.renderer._renderer) + + plt.close(fig) + + pil_img = Image.fromarray(img).convert("L") + mask = torch.tensor(np.array(pil_img)) / 255.0 + + out.append(mask) + + return (torch.stack(out, dim=0), 1.0 - torch.stack(out, dim=0),) + +class CreateShapeMask: + + RETURN_TYPES = ("MASK", "MASK",) + RETURN_NAMES = ("mask", "mask_inverted",) + FUNCTION = "createshapemask" + CATEGORY = "KJNodes/masking/generate" + DESCRIPTION = """ +Creates a mask or batch of masks with the specified shape. +Locations are center locations. +Grow value is the amount to grow the shape on each frame, creating animated masks. +""" + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "shape": ( + [ 'circle', + 'square', + 'triangle', + ], + { + "default": 'circle' + }), + "frames": ("INT", {"default": 1,"min": 1, "max": 4096, "step": 1}), + "location_x": ("INT", {"default": 256,"min": 0, "max": 4096, "step": 1}), + "location_y": ("INT", {"default": 256,"min": 0, "max": 4096, "step": 1}), + "grow": ("INT", {"default": 0, "min": -512, "max": 512, "step": 1}), + "frame_width": ("INT", {"default": 512,"min": 16, "max": 4096, "step": 1}), + "frame_height": ("INT", {"default": 512,"min": 16, "max": 4096, "step": 1}), + "shape_width": ("INT", {"default": 128,"min": 8, "max": 4096, "step": 1}), + "shape_height": ("INT", {"default": 128,"min": 8, "max": 4096, "step": 1}), + }, + } + + def createshapemask(self, frames, frame_width, frame_height, location_x, location_y, shape_width, shape_height, grow, shape): + # Define the number of images in the batch + batch_size = frames + out = [] + color = "white" + for i in range(batch_size): + image = Image.new("RGB", (frame_width, frame_height), "black") + draw = ImageDraw.Draw(image) + + # Calculate the size for this frame and ensure it's not less than 0 + current_width = max(0, shape_width + i*grow) + current_height = max(0, shape_height + i*grow) + + if shape == 'circle' or shape == 'square': + # Define the bounding box for the shape + left_up_point = (location_x - current_width // 2, location_y - current_height // 2) + right_down_point = (location_x + current_width // 2, location_y + current_height // 2) + two_points = [left_up_point, right_down_point] + + if shape == 'circle': + draw.ellipse(two_points, fill=color) + elif shape == 'square': + draw.rectangle(two_points, fill=color) + + elif shape == 'triangle': + # Define the points for the triangle + left_up_point = (location_x - current_width // 2, location_y + current_height // 2) # bottom left + right_down_point = (location_x + current_width // 2, location_y + current_height // 2) # bottom right + top_point = (location_x, location_y - current_height // 2) # top point + draw.polygon([top_point, left_up_point, right_down_point], fill=color) + + image = pil2tensor(image) + mask = image[:, :, :, 0] + out.append(mask) + outstack = torch.cat(out, dim=0) + return (outstack, 1.0 - outstack,) + +class CreateVoronoiMask: + + RETURN_TYPES = ("MASK", "MASK",) + RETURN_NAMES = ("mask", "mask_inverted",) + FUNCTION = "createvoronoi" + CATEGORY = "KJNodes/masking/generate" + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "frames": ("INT", {"default": 16,"min": 2, "max": 4096, "step": 1}), + "num_points": ("INT", {"default": 15,"min": 1, "max": 4096, "step": 1}), + "line_width": ("INT", {"default": 4,"min": 1, "max": 4096, "step": 1}), + "speed": ("FLOAT", {"default": 0.5,"min": 0.0, "max": 1.0, "step": 0.01}), + "frame_width": ("INT", {"default": 512,"min": 16, "max": 4096, "step": 1}), + "frame_height": ("INT", {"default": 512,"min": 16, "max": 4096, "step": 1}), + }, + } + + def createvoronoi(self, frames, num_points, line_width, speed, frame_width, frame_height): + from scipy.spatial import Voronoi + from matplotlib import pyplot as plt + # Define the number of images in the batch + batch_size = frames + out = [] + + # Calculate aspect ratio + aspect_ratio = frame_width / frame_height + + # Create start and end points for each point, considering the aspect ratio + start_points = np.random.rand(num_points, 2) + start_points[:, 0] *= aspect_ratio + + end_points = np.random.rand(num_points, 2) + end_points[:, 0] *= aspect_ratio + + for i in range(batch_size): + # Interpolate the points' positions based on the current frame + t = (i * speed) / (batch_size - 1) # normalize to [0, 1] over the frames + t = np.clip(t, 0, 1) # ensure t is in [0, 1] + points = (1 - t) * start_points + t * end_points # lerp + + # Adjust points for aspect ratio + points[:, 0] *= aspect_ratio + + vor = Voronoi(points) + + # Create a blank image with a white background + fig, ax = plt.subplots() + plt.subplots_adjust(left=0, right=1, bottom=0, top=1) + ax.set_xlim([0, aspect_ratio]); ax.set_ylim([0, 1]) # adjust x limits + ax.axis('off') + ax.margins(0, 0) + fig.set_size_inches(aspect_ratio * frame_height/100, frame_height/100) # adjust figure size + ax.fill_between([0, 1], [0, 1], color='white') + + # Plot each Voronoi ridge + for simplex in vor.ridge_vertices: + simplex = np.asarray(simplex) + if np.all(simplex >= 0): + plt.plot(vor.vertices[simplex, 0], vor.vertices[simplex, 1], 'k-', linewidth=line_width) + + fig.canvas.draw() + img = np.array(fig.canvas.renderer._renderer) + + plt.close(fig) + + pil_img = Image.fromarray(img).convert("L") + mask = torch.tensor(np.array(pil_img)) / 255.0 + + out.append(mask) + + return (torch.stack(out, dim=0), 1.0 - torch.stack(out, dim=0),) + +class GetMaskSizeAndCount: + @classmethod + def INPUT_TYPES(s): + return {"required": { + "mask": ("MASK",), + }} + + RETURN_TYPES = ("MASK","INT", "INT", "INT",) + RETURN_NAMES = ("mask", "width", "height", "count",) + FUNCTION = "getsize" + CATEGORY = "KJNodes/masking" + DESCRIPTION = """ +Returns the width, height and batch size of the mask, +and passes it through unchanged. + +""" + + def getsize(self, mask): + width = mask.shape[2] + height = mask.shape[1] + count = mask.shape[0] + return {"ui": { + "text": [f"{count}x{width}x{height}"]}, + "result": (mask, width, height, count) + } + +class GrowMaskWithBlur: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "mask": ("MASK",), + "expand": ("INT", {"default": 0, "min": -MAX_RESOLUTION, "max": MAX_RESOLUTION, "step": 1}), + "incremental_expandrate": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 100.0, "step": 0.1}), + "tapered_corners": ("BOOLEAN", {"default": True}), + "flip_input": ("BOOLEAN", {"default": False}), + "blur_radius": ("FLOAT", { + "default": 0.0, + "min": 0.0, + "max": 100, + "step": 0.1 + }), + "lerp_alpha": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}), + "decay_factor": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}), + }, + "optional": { + "fill_holes": ("BOOLEAN", {"default": False}), + }, + } + + CATEGORY = "KJNodes/masking" + RETURN_TYPES = ("MASK", "MASK",) + RETURN_NAMES = ("mask", "mask_inverted",) + FUNCTION = "expand_mask" + DESCRIPTION = """ +# GrowMaskWithBlur +- mask: Input mask or mask batch +- expand: Expand or contract mask or mask batch by a given amount +- incremental_expandrate: increase expand rate by a given amount per frame +- tapered_corners: use tapered corners +- flip_input: flip input mask +- blur_radius: value higher than 0 will blur the mask +- lerp_alpha: alpha value for interpolation between frames +- decay_factor: decay value for interpolation between frames +- fill_holes: fill holes in the mask (slow)""" + + def expand_mask(self, mask, expand, tapered_corners, flip_input, blur_radius, incremental_expandrate, lerp_alpha, decay_factor, fill_holes=False): + import kornia.morphology as morph + alpha = lerp_alpha + decay = decay_factor + if flip_input: + mask = 1.0 - mask + + growmask = mask.reshape((-1, mask.shape[-2], mask.shape[-1])) + out = [] + previous_output = None + current_expand = expand + for m in tqdm(growmask, desc="Expanding/Contracting Mask"): + output = m.unsqueeze(0).unsqueeze(0).to(main_device) # Add batch and channel dims for kornia + if abs(round(current_expand)) > 0 and output.max() > 0: + # Create kernel - kornia expects kernel on same device as input + if tapered_corners: + kernel = torch.tensor([[0, 1, 0], + [1, 1, 1], + [0, 1, 0]], dtype=torch.float32, device=output.device) + else: + kernel = torch.tensor([[1, 1, 1], + [1, 1, 1], + [1, 1, 1]], dtype=torch.float32, device=output.device) + + for _ in range(abs(round(current_expand))): + if current_expand < 0: + output = morph.erosion(output, kernel) + else: + output = morph.dilation(output, kernel) + + output = output.squeeze(0).squeeze(0) # Remove batch and channel dims + + if current_expand < 0: + current_expand -= abs(incremental_expandrate) + else: + current_expand += abs(incremental_expandrate) + + if fill_holes: + binary_mask = output > 0 + output_np = binary_mask.cpu().numpy() + filled = scipy.ndimage.binary_fill_holes(output_np) + output = torch.from_numpy(filled.astype(np.float32)).to(output.device) + + if alpha < 1.0 and previous_output is not None: + output = alpha * output + (1 - alpha) * previous_output + if decay < 1.0 and previous_output is not None: + output += decay * previous_output + output = output / output.max() + previous_output = output + out.append(output.cpu()) + + if blur_radius != 0: + # Convert the tensor list to PIL images, apply blur, and convert back + for idx, tensor in enumerate(out): + # Convert tensor to PIL image + pil_image = tensor2pil(tensor.cpu().detach())[0] + # Apply Gaussian blur + pil_image = pil_image.filter(ImageFilter.GaussianBlur(blur_radius)) + # Convert back to tensor + out[idx] = pil2tensor(pil_image) + blurred = torch.cat(out, dim=0) + return (blurred, 1.0 - blurred) + else: + return (torch.stack(out, dim=0), 1.0 - torch.stack(out, dim=0),) + +class MaskBatchMulti: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "inputcount": ("INT", {"default": 2, "min": 2, "max": 1000, "step": 1}), + "mask_1": ("MASK", ), + "mask_2": ("MASK", ), + }, + } + + RETURN_TYPES = ("MASK",) + RETURN_NAMES = ("masks",) + FUNCTION = "combine" + CATEGORY = "KJNodes/masking" + DESCRIPTION = """ +Creates an image batch from multiple masks. +You can set how many inputs the node has, +with the **inputcount** and clicking update. +""" + + def combine(self, inputcount, **kwargs): + mask = kwargs["mask_1"] + for c in range(1, inputcount): + new_mask = kwargs[f"mask_{c + 1}"] + if mask.shape[1:] != new_mask.shape[1:]: + new_mask = F.interpolate(new_mask.unsqueeze(1), size=(mask.shape[1], mask.shape[2]), mode="bicubic").squeeze(1) + mask = torch.cat((mask, new_mask), dim=0) + return (mask,) + +class OffsetMask: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "mask": ("MASK",), + "x": ("INT", { "default": 0, "min": -4096, "max": MAX_RESOLUTION, "step": 1, "display": "number" }), + "y": ("INT", { "default": 0, "min": -4096, "max": MAX_RESOLUTION, "step": 1, "display": "number" }), + "angle": ("INT", { "default": 0, "min": -360, "max": 360, "step": 1, "display": "number" }), + "duplication_factor": ("INT", { "default": 1, "min": 1, "max": 1000, "step": 1, "display": "number" }), + "roll": ("BOOLEAN", { "default": False }), + "incremental": ("BOOLEAN", { "default": False }), + "padding_mode": ( + [ + 'empty', + 'border', + 'reflection', + + ], { + "default": 'empty' + }), + } + } + + RETURN_TYPES = ("MASK",) + RETURN_NAMES = ("mask",) + FUNCTION = "offset" + CATEGORY = "KJNodes/masking" + DESCRIPTION = """ +Offsets the mask by the specified amount. + - mask: Input mask or mask batch + - x: Horizontal offset + - y: Vertical offset + - angle: Angle in degrees + - roll: roll edge wrapping + - duplication_factor: Number of times to duplicate the mask to form a batch + - border padding_mode: Padding mode for the mask +""" + + def offset(self, mask, x, y, angle, roll=False, incremental=False, duplication_factor=1, padding_mode="empty"): + # Create duplicates of the mask batch + mask = mask.repeat(duplication_factor, 1, 1).clone() + + batch_size, height, width = mask.shape + + if angle != 0 and incremental: + for i in range(batch_size): + rotation_angle = angle * (i+1) + mask[i] = TF.rotate(mask[i].unsqueeze(0), rotation_angle).squeeze(0) + elif angle > 0: + for i in range(batch_size): + mask[i] = TF.rotate(mask[i].unsqueeze(0), angle).squeeze(0) + + if roll: + if incremental: + for i in range(batch_size): + shift_x = min(x*(i+1), width-1) + shift_y = min(y*(i+1), height-1) + if shift_x != 0: + mask[i] = torch.roll(mask[i], shifts=shift_x, dims=1) + if shift_y != 0: + mask[i] = torch.roll(mask[i], shifts=shift_y, dims=0) + else: + shift_x = min(x, width-1) + shift_y = min(y, height-1) + if shift_x != 0: + mask = torch.roll(mask, shifts=shift_x, dims=2) + if shift_y != 0: + mask = torch.roll(mask, shifts=shift_y, dims=1) + else: + + for i in range(batch_size): + if incremental: + temp_x = min(x * (i+1), width-1) + temp_y = min(y * (i+1), height-1) + else: + temp_x = min(x, width-1) + temp_y = min(y, height-1) + if temp_x > 0: + if padding_mode == 'empty': + mask[i] = torch.cat([torch.zeros((height, temp_x)), mask[i, :, :-temp_x]], dim=1) + elif padding_mode in ['replicate', 'reflect']: + mask[i] = F.pad(mask[i, :, :-temp_x], (0, temp_x), mode=padding_mode) + elif temp_x < 0: + if padding_mode == 'empty': + mask[i] = torch.cat([mask[i, :, :temp_x], torch.zeros((height, -temp_x))], dim=1) + elif padding_mode in ['replicate', 'reflect']: + mask[i] = F.pad(mask[i, :, -temp_x:], (temp_x, 0), mode=padding_mode) + + if temp_y > 0: + if padding_mode == 'empty': + mask[i] = torch.cat([torch.zeros((temp_y, width)), mask[i, :-temp_y, :]], dim=0) + elif padding_mode in ['replicate', 'reflect']: + mask[i] = F.pad(mask[i, :-temp_y, :], (0, temp_y), mode=padding_mode) + elif temp_y < 0: + if padding_mode == 'empty': + mask[i] = torch.cat([mask[i, :temp_y, :], torch.zeros((-temp_y, width))], dim=0) + elif padding_mode in ['replicate', 'reflect']: + mask[i] = F.pad(mask[i, -temp_y:, :], (temp_y, 0), mode=padding_mode) + + return mask, + +class RoundMask: + @classmethod + def INPUT_TYPES(s): + return {"required": { + "mask": ("MASK",), + }} + + RETURN_TYPES = ("MASK",) + FUNCTION = "round" + CATEGORY = "KJNodes/masking" + DESCRIPTION = """ +Rounds the mask or batch of masks to a binary mask. +RoundMask example + +""" + + def round(self, mask): + mask = mask.round() + return (mask,) + +class ResizeMask: + upscale_methods = ["nearest-exact", "bilinear", "area", "bicubic", "lanczos"] + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "mask": ("MASK",), + "width": ("INT", { "default": 512, "min": 0, "max": MAX_RESOLUTION, "step": 1, "display": "number" }), + "height": ("INT", { "default": 512, "min": 0, "max": MAX_RESOLUTION, "step": 1, "display": "number" }), + "keep_proportions": ("BOOLEAN", { "default": False }), + "upscale_method": (s.upscale_methods,), + "crop": (["disabled","center"],), + } + } + + RETURN_TYPES = ("MASK", "INT", "INT",) + RETURN_NAMES = ("mask", "width", "height",) + FUNCTION = "resize" + CATEGORY = "KJNodes/masking" + DESCRIPTION = """ +Resizes the mask or batch of masks to the specified width and height. +""" + + def resize(self, mask, width, height, keep_proportions, upscale_method,crop): + if keep_proportions: + _, oh, ow = mask.shape + width = ow if width == 0 else width + height = oh if height == 0 else height + ratio = min(width / ow, height / oh) + width = round(ow*ratio) + height = round(oh*ratio) + + if upscale_method == "lanczos": + out_mask = common_upscale(mask.unsqueeze(1).repeat(1, 3, 1, 1), width, height, upscale_method, crop=crop).movedim(1,-1)[:, :, :, 0] + else: + out_mask = common_upscale(mask.unsqueeze(1), width, height, upscale_method, crop=crop).squeeze(1) + + return(out_mask, out_mask.shape[2], out_mask.shape[1],) + +class RemapMaskRange: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "mask": ("MASK",), + "min": ("FLOAT", {"default": 0.0,"min": -10.0, "max": 1.0, "step": 0.01}), + "max": ("FLOAT", {"default": 1.0,"min": 0.0, "max": 10.0, "step": 0.01}), + } + } + + RETURN_TYPES = ("MASK",) + RETURN_NAMES = ("mask",) + FUNCTION = "remap" + CATEGORY = "KJNodes/masking" + DESCRIPTION = """ +Sets new min and max values for the mask. +""" + + def remap(self, mask, min, max): + + # Find the maximum value in the mask + mask_max = torch.max(mask) + + # If the maximum mask value is zero, avoid division by zero by setting it to 1 + mask_max = mask_max if mask_max > 0 else 1 + + # Scale the mask values to the new range defined by min and max + # The highest pixel value in the mask will be scaled to max + scaled_mask = (mask / mask_max) * (max - min) + min + + # Clamp the values to ensure they are within [0.0, 1.0] + scaled_mask = torch.clamp(scaled_mask, min=0.0, max=1.0) + + return (scaled_mask, ) + + +def get_mask_polygon(self, mask_np): + import cv2 + """Helper function to get polygon points from mask""" + # Find contours + contours, _ = cv2.findContours(mask_np, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + + if not contours: + return None + + # Get the largest contour + largest_contour = max(contours, key=cv2.contourArea) + + # Approximate polygon + epsilon = 0.02 * cv2.arcLength(largest_contour, True) + polygon = cv2.approxPolyDP(largest_contour, epsilon, True) + + return polygon.squeeze() + +class SeparateMasks: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "mask": ("MASK", ), + "size_threshold_width" : ("INT", {"default": 256, "min": 0.0, "max": 4096, "step": 1}), + "size_threshold_height" : ("INT", {"default": 256, "min": 0.0, "max": 4096, "step": 1}), + "mode": (["convex_polygons", "area", "box"],), + "max_poly_points": ("INT", {"default": 8, "min": 3, "max": 32, "step": 1}), + + }, + } + + RETURN_TYPES = ("MASK",) + RETURN_NAMES = ("mask",) + FUNCTION = "separate" + CATEGORY = "KJNodes/masking" + OUTPUT_NODE = True + DESCRIPTION = "Separates a mask into multiple masks based on the size of the connected components." + + def polygon_to_mask(self, polygon, shape): + import cv2 + mask = np.zeros((shape[0], shape[1]), dtype=np.uint8) # Fixed shape handling + + if len(polygon.shape) == 2: # Check if polygon points are valid + polygon = polygon.astype(np.int32) + cv2.fillPoly(mask, [polygon], 1) + return mask + + def get_mask_polygon(self, mask_np, max_points): + import cv2 + contours, _ = cv2.findContours(mask_np, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + if not contours: + return None + + largest_contour = max(contours, key=cv2.contourArea) + hull = cv2.convexHull(largest_contour) + + # Initialize with smaller epsilon for more points + perimeter = cv2.arcLength(hull, True) + + min_eps = perimeter * 0.001 # Much smaller minimum + max_eps = perimeter * 0.2 # Smaller maximum + + best_approx = None + best_diff = float('inf') + max_iterations = 20 + + #print(f"Target points: {max_points}, Perimeter: {perimeter}") + + for i in range(max_iterations): + curr_eps = (min_eps + max_eps) / 2 + approx = cv2.approxPolyDP(hull, curr_eps, True) + points_diff = len(approx) - max_points + + #print(f"Iteration {i}: points={len(approx)}, eps={curr_eps:.4f}") + + if abs(points_diff) < best_diff: + best_approx = approx + best_diff = abs(points_diff) + + if len(approx) > max_points: + min_eps = curr_eps * 1.1 # More gradual adjustment + elif len(approx) < max_points: + max_eps = curr_eps * 0.9 # More gradual adjustment + else: + return approx.squeeze() + + if abs(max_eps - min_eps) < perimeter * 0.0001: # Relative tolerance + break + + # If we didn't find exact match, return best approximation + return best_approx.squeeze() if best_approx is not None else hull.squeeze() + + def separate(self, mask: torch.Tensor, size_threshold_width: int, size_threshold_height: int, max_poly_points: int, mode: str): + B, H, W = mask.shape + separated = [] + + mask = mask.round() + + for b in range(B): + mask_np = mask[b].cpu().numpy().astype(np.uint8) + structure = np.ones((3, 3), dtype=np.int8) + labeled, ncomponents = scipy.ndimage.label(mask_np, structure=structure) + pbar = ProgressBar(ncomponents) + + for component in range(1, ncomponents + 1): + component_mask_np = (labeled == component).astype(np.uint8) + + rows = np.any(component_mask_np, axis=1) + cols = np.any(component_mask_np, axis=0) + y_min, y_max = np.where(rows)[0][[0, -1]] + x_min, x_max = np.where(cols)[0][[0, -1]] + + width = x_max - x_min + 1 + height = y_max - y_min + 1 + centroid_x = (x_min + x_max) / 2 # Calculate x centroid + logging.info(f"Component {component}: width={width}, height={height}, x_pos={centroid_x}") + + if width >= size_threshold_width and height >= size_threshold_height: + if mode == "convex_polygons": + polygon = self.get_mask_polygon(component_mask_np, max_poly_points) + if polygon is not None: + poly_mask = self.polygon_to_mask(polygon, (H, W)) + poly_mask = torch.tensor(poly_mask, device=mask.device) + separated.append((centroid_x, poly_mask)) + elif mode == "box": + # Create bounding box mask + box_mask = np.zeros((H, W), dtype=np.uint8) + box_mask[y_min:y_max+1, x_min:x_max+1] = 1 + box_mask = torch.tensor(box_mask, device=mask.device) + separated.append((centroid_x, box_mask)) + else: + area_mask = torch.tensor(component_mask_np, device=mask.device) + separated.append((centroid_x, area_mask)) + pbar.update(1) + + if len(separated) > 0: + # Sort by x position and extract only the masks + separated.sort(key=lambda x: x[0]) + separated = [x[1] for x in separated] + out_masks = torch.stack(separated, dim=0) + return out_masks, + else: + return torch.empty((1, 64, 64), device=mask.device), + + +class ConsolidateMasksKJ: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "masks": ("MASK",), + "width": ("INT", {"default": 512, "min": 0, "max": 4096, "step": 64}), + "height": ("INT", {"default": 512, "min": 0, "max": 4096, "step": 64}), + "padding": ("INT", {"default": 0, "min": 0, "max": 4096, "step": 1}), + }, + } + + RETURN_TYPES = ("MASK",) + FUNCTION = "consolidate" + + CATEGORY = "KJNodes/masking" + DESCRIPTION = "Consolidates a batch of separate masks by finding the largest group of masks that fit inside a tile of the given width and height (including the padding), and repeating until no more masks can be combined." + + def consolidate(self, masks, width=512, height=512, padding=0): + B, H, W = masks.shape + + def mask_fits(coords, candidate_coords): + x_min, y_min, x_max, y_max = coords + cx_min, cy_min, cx_max, cy_max = candidate_coords + nx_min, ny_min = min(x_min, cx_min), min(y_min, cy_min) + nx_max, ny_max = max(x_max, cx_max), max(y_max, cy_max) + if nx_min + width < nx_max + padding or ny_min + height < ny_max + padding: + return False, coords + return True, (nx_min, ny_min, nx_max, ny_max) + + separated = [] + final_masks = [] + for b in range(B): + m = masks[b] + rows, cols = m.any(dim=1), m.any(dim=0) + y_min, y_max = torch.where(rows)[0][[0, -1]] + x_min, x_max = torch.where(cols)[0][[0, -1]] + w = x_max - x_min + 1 + h = y_max - y_min + 1 + separated.append(((x_min.item(), y_min.item(), x_max.item(), y_max.item()), m)) + + separated.sort(key=lambda x: x[0]) + fits = [] + for i, masks in enumerate(separated): + coord = masks[0] + fits_in_box = [] + for j, cand_mask in enumerate(separated): + if i == j: + continue + r, coord = mask_fits(coord, cand_mask[0]) + if r: + fits_in_box.append(j) + fits.append((i, fits_in_box)) + fits.sort(key=lambda x: -len(x[1])) + seen = [] + unique_fits = [] + for idx, fs in fits: + uniq = [i for i in fs if i not in seen] + unique_fits.append((idx, fs, uniq)) + seen.extend(uniq) + unique_fits.sort(key=lambda x: (-len(x[1]), -len(x[2]))) + merged = [] + for mask_idx, fitting_masks, _ in unique_fits: + if mask_idx in merged: + continue + fitting_masks = [i for i in fitting_masks if i not in merged] + combined_mask = separated[mask_idx][1].clone() + for i in fitting_masks: + combined_mask += separated[i][1] + merged.append(i) + merged.append(mask_idx) + final_masks.append(combined_mask) + + logging.info(f"Consolidated {B} masks into {len(final_masks)}") + return (torch.stack(final_masks, dim=0),) + + +class DrawMaskOnImage: + @classmethod + def INPUT_TYPES(s): + return {"required": { + "image": ("IMAGE", ), + "mask": ("MASK", ), + "color": ("STRING", {"default": "0, 0, 0", "tooltip": "Color as RGB/RGBA values in range 0-255 or 0.0-1.0, separated by commas. Ex: 255, 0, 0, 128"}), + }, + "optional": { + "device": (["cpu", "gpu"], {"default": "cpu", "tooltip": "Device to use for processing"}), + } + } + + RETURN_TYPES = ("IMAGE", ) + RETURN_NAMES = ("images",) + FUNCTION = "apply" + CATEGORY = "KJNodes/masking" + DESCRIPTION = "Applies the provided masks to the input images with Alpha Blending support." + + def apply(self, image, mask, color, device="cpu"): + B, H, W, C = image.shape + BM, HM, WM = mask.shape + + processing_device = main_device if device == "gpu" else torch.device("cpu") + + in_masks = mask.clone().to(processing_device) + in_images = image.clone().to(processing_device) + + # Resize mask if dimensions don't match + if HM != H or WM != W: + in_masks = F.interpolate(mask.unsqueeze(1), size=(H, W), mode='nearest-exact').squeeze(1) + # Handle batch size mismatch + if B > BM: + in_masks = in_masks.repeat((B + BM - 1) // BM, 1, 1)[:B] + elif BM > B: + in_masks = in_masks[:B] + + output_images = [] + + # Parse Color String (Handle RGB, RGBA, and Hex formats) + color = color.strip() + color_values = [] + + if color.startswith('#'): + # Handle hex format (#RGB, #RGBA, #RRGGBB, #RRGGBBAA) + hex_color = color.lstrip('#') + if len(hex_color) == 3: # #RGB + color_values = [int(c*2, 16) / 255.0 for c in hex_color] + elif len(hex_color) == 4: # #RGBA + color_values = [int(c*2, 16) / 255.0 for c in hex_color] + elif len(hex_color) == 6: # #RRGGBB + color_values = [int(hex_color[i:i+2], 16) / 255.0 for i in (0, 2, 4)] + elif len(hex_color) == 8: # #RRGGBBAA + color_values = [int(hex_color[i:i+2], 16) / 255.0 for i in (0, 2, 4, 6)] + else: + raise ValueError(f"Invalid hex color format: {color}") + else: + # Handle comma-separated RGB/RGBA format + for x in color.split(","): + val = float(x.strip()) + color_values.append(val / 255.0 if val > 1.0 else val) + + rgb = color_values[:3] + alpha_val = color_values[3] if len(color_values) == 4 else 1.0 + + fill_color = torch.tensor(rgb, dtype=torch.float32, device=processing_device) + + for i in tqdm(range(B), desc="DrawMaskOnImage batch"): + curr_mask = in_masks[i] # [H, W] + img_idx = min(i, B - 1) + curr_image = in_images[img_idx] # [H, W, C] + + blend_factor = curr_mask.unsqueeze(-1) * alpha_val + img_channels = curr_image.shape[-1] + + if img_channels == 4: + img_rgb = curr_image[..., :3] + img_a = curr_image[..., 3:] + out_rgb = img_rgb * (1 - blend_factor) + fill_color * blend_factor + out_a = torch.maximum(img_a, blend_factor) + masked_image = torch.cat((out_rgb, out_a), dim=-1) + else: + masked_image = curr_image * (1 - blend_factor) + fill_color * blend_factor + output_images.append(masked_image) + + if not output_images: + return (torch.zeros((0, H, W, C), dtype=image.dtype),) + + out_tensor = torch.stack(output_images, dim=0).cpu() + + return (out_tensor, ) + +class BlockifyMask: + @classmethod + def INPUT_TYPES(s): + return {"required": { + "masks": ("MASK",), + "block_size": ("INT", {"default": 32, "min": 8, "max": 512, "step": 1, "tooltip": "Size of blocks in pixels (smaller = smaller blocks)"}), + }, + "optional": { + "device": (["cpu", "gpu"], {"default": "cpu", "tooltip": "Device to use for processing"}), + } + } + + RETURN_TYPES = ("MASK", ) + RETURN_NAMES = ("mask",) + FUNCTION = "process" + CATEGORY = "KJNodes/masking" + DESCRIPTION = "Creates a block mask by dividing the bounding box of each mask into blocks of the specified size and filling in blocks that contain any part of the original mask." + + def process(self, masks, block_size, device="cpu"): + processing_device = main_device if device == "gpu" else torch.device("cpu") + + masks = masks.to(processing_device) + batch_size, height, width = masks.shape + + result_masks = torch.zeros_like(masks) + + for i in tqdm(range(batch_size), desc="BlockifyMask batch"): + mask = masks[i] + + # Find bounding box efficiently + mask_bool = mask > 0 + if not mask_bool.any(): + continue + + y_indices = torch.nonzero(mask_bool.any(dim=1), as_tuple=True)[0] + x_indices = torch.nonzero(mask_bool.any(dim=0), as_tuple=True)[0] + + if len(y_indices) == 0 or len(x_indices) == 0: + continue + + y_min, y_max = y_indices[0], y_indices[-1] + x_min, x_max = x_indices[0], x_indices[-1] + + bbox_width = x_max - x_min + 1 + bbox_height = y_max - y_min + 1 + + # Calculate block grid + w_divisions = max(1, bbox_width // block_size) + h_divisions = max(1, bbox_height // block_size) + + w_slice = bbox_width // w_divisions + h_slice = bbox_height // h_divisions + + # Create coordinate grids only for bbox region + y_coords = torch.arange(y_min, y_max + 1, device=processing_device).view(-1, 1) + x_coords = torch.arange(x_min, x_max + 1, device=processing_device).view(1, -1) + + # Calculate block indices for bbox region + w_block_indices = (x_coords - x_min) // w_slice + h_block_indices = (y_coords - y_min) // h_slice + + # Clamp to valid range + w_block_indices = w_block_indices.clamp(0, w_divisions - 1) + h_block_indices = h_block_indices.clamp(0, h_divisions - 1) + + # Create unique block IDs by combining h and w indices + block_ids = h_block_indices * w_divisions + w_block_indices + + # Get mask region within bbox + mask_region = mask[y_min:y_max+1, x_min:x_max+1] + + # Find which blocks have content using scatter_add + max_blocks = h_divisions * w_divisions + block_content = torch.zeros(max_blocks, device=processing_device) + block_content.scatter_add_(0, block_ids.flatten(), mask_region.flatten()) + + # Create result for blocks that have content + has_content = block_content > 0 + block_mask = has_content[block_ids] + + # Fill the result + result_masks[i, y_min:y_max+1, x_min:x_max+1] = block_mask.float() + + return (result_masks.clamp(0, 1),) diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/model_optimization_nodes.py b/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/model_optimization_nodes.py new file mode 100644 index 0000000000000000000000000000000000000000..d67708b470447c1139e7a12b74c48fc678a435bd --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/model_optimization_nodes.py @@ -0,0 +1,2529 @@ +import os +import sys +import logging +import torch +import importlib +import math +import datetime +from tqdm import tqdm + +import folder_paths +import comfy.model_management as mm +from comfy.cli_args import args +from comfy.ldm.modules.attention import wrap_attn, optimized_attention, attention_pytorch +import comfy.utils +import comfy.sd +import comfy.ops + +try: + from comfy_api.latest import io + v3_available = True +except ImportError: + v3_available = False + logging.warning("ComfyUI v3 node API not available, please update ComfyUI to access latest v3 nodes.") + +sageattn_modes = ["disabled", "auto", "sageattn_qk_int8_pv_fp16_cuda", "sageattn_qk_int8_pv_fp16_triton", "sageattn_qk_int8_pv_fp8_cuda", "sageattn_qk_int8_pv_fp8_cuda++", "sageattn3", "sageattn3_per_block_mean"] + +def get_sage_func(sage_attention, allow_compile=False): + logging.info(f"Using sage attention mode: {sage_attention}") + if sage_attention == "auto": + from sageattention import sageattn + def sage_func(q, k, v, is_causal=False, attn_mask=None, tensor_layout="NHD"): + return sageattn(q, k, v, is_causal=is_causal, attn_mask=attn_mask, tensor_layout=tensor_layout) + elif sage_attention == "sageattn_qk_int8_pv_fp16_cuda": + from sageattention import sageattn_qk_int8_pv_fp16_cuda + def sage_func(q, k, v, is_causal=False, attn_mask=None, tensor_layout="NHD"): + return sageattn_qk_int8_pv_fp16_cuda(q, k, v, is_causal=is_causal, attn_mask=attn_mask, pv_accum_dtype="fp32", tensor_layout=tensor_layout) + elif sage_attention == "sageattn_qk_int8_pv_fp16_triton": + from sageattention import sageattn_qk_int8_pv_fp16_triton + def sage_func(q, k, v, is_causal=False, attn_mask=None, tensor_layout="NHD"): + return sageattn_qk_int8_pv_fp16_triton(q, k, v, is_causal=is_causal, attn_mask=attn_mask, tensor_layout=tensor_layout) + elif sage_attention == "sageattn_qk_int8_pv_fp8_cuda": + from sageattention import sageattn_qk_int8_pv_fp8_cuda + def sage_func(q, k, v, is_causal=False, attn_mask=None, tensor_layout="NHD"): + return sageattn_qk_int8_pv_fp8_cuda(q, k, v, is_causal=is_causal, attn_mask=attn_mask, pv_accum_dtype="fp32+fp32", tensor_layout=tensor_layout) + elif sage_attention == "sageattn_qk_int8_pv_fp8_cuda++": + from sageattention import sageattn_qk_int8_pv_fp8_cuda + def sage_func(q, k, v, is_causal=False, attn_mask=None, tensor_layout="NHD"): + return sageattn_qk_int8_pv_fp8_cuda(q, k, v, is_causal=is_causal, attn_mask=attn_mask, pv_accum_dtype="fp32+fp16", tensor_layout=tensor_layout) + elif "sageattn3" in sage_attention: + from sageattn3 import sageattn3_blackwell + def sage_func(q, k, v, is_causal=False, attn_mask=None, tensor_layout="NHD", **kwargs): + q, k, v = [x.transpose(1, 2) if tensor_layout == "NHD" else x for x in (q, k, v)] + out = sageattn3_blackwell(q, k, v, is_causal=is_causal, attn_mask=attn_mask, per_block_mean=(sage_attention == "sageattn3_per_block_mean")) + return out.transpose(1, 2) if tensor_layout == "NHD" else out + + if not allow_compile: + sage_func = torch.compiler.disable()(sage_func) + + @wrap_attn + def attention_sage(q, k, v, heads, mask=None, attn_precision=None, skip_reshape=False, skip_output_reshape=False, **kwargs): + if kwargs.get("low_precision_attention", True) is False: + return attention_pytorch(q, k, v, heads, mask=mask, skip_reshape=skip_reshape, skip_output_reshape=skip_output_reshape, **kwargs) + in_dtype = v.dtype + if q.dtype == torch.float32 or k.dtype == torch.float32 or v.dtype == torch.float32: + q, k, v = q.to(torch.float16), k.to(torch.float16), v.to(torch.float16) + if skip_reshape: + b, _, _, dim_head = q.shape + tensor_layout="HND" + else: + b, _, dim_head = q.shape + dim_head //= heads + q, k, v = map( + lambda t: t.view(b, -1, heads, dim_head), + (q, k, v), + ) + tensor_layout="NHD" + if mask is not None: + # add a batch dimension if there isn't already one + if mask.ndim == 2: + mask = mask.unsqueeze(0) + # add a heads dimension if there isn't already one + if mask.ndim == 3: + mask = mask.unsqueeze(1) + # Copy only when a row offset would actually exceed int32. + seq_dim = 2 if tensor_layout == "HND" else 1 + if any((t.shape[seq_dim] - 1) * t.stride(seq_dim) >= 2**31 for t in (q, k, v)): + q, k, v = q.contiguous(), k.contiguous(), v.contiguous() + out = sage_func(q, k, v, attn_mask=mask, is_causal=False, tensor_layout=tensor_layout).to(in_dtype) + if tensor_layout == "HND": + if not skip_output_reshape: + out = ( + out.transpose(1, 2).reshape(b, -1, heads * dim_head) + ) + else: + if skip_output_reshape: + out = out.transpose(1, 2) + else: + out = out.reshape(b, -1, heads * dim_head) + return out + return attention_sage + + +from comfy.patcher_extension import CallbacksMP +class PathchSageAttentionKJ(): + @classmethod + def INPUT_TYPES(s): + return {"required": { + "model": ("MODEL",), + "sage_attention": (sageattn_modes, {"default": False, "tooltip": "Patch the attention of the model passing through this node to use sageattn. To revert, run this node again with the disabled option. Requires the sageattention library to be installed."}), + }, + "optional": { + "allow_compile": ("BOOLEAN", {"default": False, "tooltip": "Allow the use of torch.compile for the sage attention function, requires latest sageattn 2.2.0 or higher."}) + } + } + + RETURN_TYPES = ("MODEL", ) + FUNCTION = "patch" + DESCRIPTION = "Experimental node for patching attention mode. This doesn't use the model patching system and thus can't be disabled without running the node again with 'disabled' option." + EXPERIMENTAL = True + CATEGORY = "KJNodes/experimental" + + def patch(self, model, sage_attention, allow_compile=False): + if sage_attention == "disabled": + return model, + + model_clone = model.clone() + + new_attention = get_sage_func(sage_attention, allow_compile=allow_compile) + def attention_override_sage(func, *args, **kwargs): + return new_attention.__wrapped__(*args, **kwargs) + + # attention override + model_clone.model_options["transformer_options"]["optimized_attention_override"] = attention_override_sage + + return model_clone, + + +def get_flash_func(allow_compile=False, cast_dtype=torch.float16): + # Prefer FA2 (broad arch support, plain tensor return); fall back to FA3 + # (flash_attn_interface), which has no dropout arg and returns (out, lse). + is_fa3 = False + try: + from flash_attn import flash_attn_func + except ImportError: + try: + from flash_attn_interface import flash_attn_func + is_fa3 = True + except ImportError: + raise ImportError( + "Flash attention not found. Install either FA2 ('flash_attn') or " + "FA3 ('flash_attn_interface', pip package 'flash-attn-3')." + ) + logging.info(f"Using flash attention {'3' if is_fa3 else '2'}: cast_dtype={cast_dtype}") + + # q, k, v in NHD layout (b, seq, heads, dim_head) + def flash_func(q, k, v): + if is_fa3: + out = flash_attn_func(q, k, v, causal=False) + else: + out = flash_attn_func(q, k, v, dropout_p=0.0, causal=False) + # FA3 returns (out, softmax_lse); FA2 returns the tensor directly + return out[0] if isinstance(out, tuple) else out + + if not allow_compile: + flash_func = torch.compiler.disable()(flash_func) + + if torch.cuda.is_available(): + probe = torch.zeros(1, 8, 2, 64, dtype=cast_dtype, device="cuda") + flash_func(probe, probe, probe) + + @wrap_attn + def attention_flash(q, k, v, heads, mask=None, attn_precision=None, skip_reshape=False, skip_output_reshape=False, **kwargs): + if mask is not None: + raise RuntimeError("Flash attention does not support attention masks") + in_dtype = v.dtype + # flash_attn only supports fp16/bf16 + if q.dtype == torch.float32 or k.dtype == torch.float32 or v.dtype == torch.float32: + q, k, v = q.to(cast_dtype), k.to(cast_dtype), v.to(cast_dtype) + # flash_attn wants NHD layout (b, seq, heads, dim_head) + if skip_reshape: + # input is HND (b, heads, seq, dim_head) + b, _, _, dim_head = q.shape + q, k, v = map(lambda t: t.transpose(1, 2), (q, k, v)) + else: + b, _, dim_head = q.shape + dim_head //= heads + q, k, v = map(lambda t: t.view(b, -1, heads, dim_head), (q, k, v)) + out = flash_func(q, k, v).to(in_dtype) + if skip_output_reshape: + out = out.transpose(1, 2) # NHD -> HND + else: + out = out.reshape(b, -1, heads * dim_head) + return out + return attention_flash + + +class PatchFlashAttentionKJ(): + @classmethod + def INPUT_TYPES(s): + return {"required": { + "model": ("MODEL",), + }, + "optional": { + "allow_compile": ("BOOLEAN", {"default": False, "tooltip": "Allow torch.compile to trace into the flash attention function. If disabled (default), the function is wrapped with torch.compiler.disable() for compatibility, matching the sage attention node."}), + }} + + RETURN_TYPES = ("MODEL", ) + FUNCTION = "patch" + DESCRIPTION = "Experimental node for patching attention to use flash attention, without the silent SDPA fallback the ComfyUI default does. Patches the attention of the model passing through this node; to disable, bypass or disconnect this node. Requires the flash_attn library to be installed." + EXPERIMENTAL = True + CATEGORY = "KJNodes/experimental" + + def patch(self, model, allow_compile=False): + # match the model's compute dtype for the fp32 downcast, fall back to fp16 + inference_dtype = model.model.get_dtype_inference() if hasattr(model.model, "get_dtype_inference") else torch.float16 + cast_dtype = inference_dtype if inference_dtype in (torch.float16, torch.bfloat16) else torch.float16 + + new_attention = get_flash_func( + allow_compile=allow_compile, + cast_dtype=cast_dtype, + ) + + model_clone = model.clone() + def attention_override_flash(func, *args, **kwargs): + return new_attention.__wrapped__(*args, **kwargs) + + # attention override + model_clone.model_options["transformer_options"]["optimized_attention_override"] = attention_override_flash + + return model_clone, + + +class CheckpointLoaderKJ(): + @classmethod + def INPUT_TYPES(s): + return {"required": { + "ckpt_name": (folder_paths.get_filename_list("checkpoints"), {"tooltip": "The name of the checkpoint (model) to load."}), + "weight_dtype": (["default", "fp8_e4m3fn", "fp8_e4m3fn_fast", "fp8_e5m2", "fp16", "bf16", "fp32"],), + "compute_dtype": (["default", "fp16", "bf16", "fp32"], {"default": "default", "tooltip": "The compute dtype to use for the model."}), + "patch_cublaslinear": ("BOOLEAN", {"default": False, "tooltip": "Enable or disable the cublas_ops arg"}), + "sage_attention": (sageattn_modes, {"default": False, "tooltip": "Patch comfy attention to use sageattn."}), + "enable_fp16_accumulation": ("BOOLEAN", {"default": False, "tooltip": "Enable torch.backends.cuda.matmul.allow_fp16_accumulation, required minimum pytorch version 2.7.1"}), + }} + + RETURN_TYPES = ("MODEL", "CLIP", "VAE") + FUNCTION = "load" + DESCRIPTION = "Experimental node for patching torch.nn.Linear with CublasLinear." + EXPERIMENTAL = True + CATEGORY = "KJNodes/model_loaders" + + def load(self, ckpt_name, weight_dtype, compute_dtype, patch_cublaslinear, sage_attention, enable_fp16_accumulation): + DTYPE_MAP = { + "fp8_e4m3fn": torch.float8_e4m3fn, + "fp8_e5m2": torch.float8_e5m2, + "fp16": torch.float16, + "bf16": torch.bfloat16, + "fp32": torch.float32 + } + model_options = {} + if dtype := DTYPE_MAP.get(weight_dtype): + model_options["dtype"] = dtype + logging.info(f"Setting {ckpt_name} weight dtype to {dtype}") + + if weight_dtype == "fp8_e4m3fn_fast": + model_options["dtype"] = torch.float8_e4m3fn + model_options["fp8_optimizations"] = True + + if patch_cublaslinear: + args.fast.add("cublas_ops") + else: + args.fast.discard("cublas_ops") + + ckpt_path = folder_paths.get_full_path_or_raise("checkpoints", ckpt_name) + model, clip, vae, _ = comfy.sd.load_checkpoint_guess_config( + ckpt_path, + output_vae=True, + output_clip=True, + embedding_directory=folder_paths.get_folder_paths("embeddings"), + model_options=model_options) + + if dtype := DTYPE_MAP.get(compute_dtype): + model.set_model_compute_dtype(dtype) + model.force_cast_weights = False + logging.info(f"Setting {ckpt_name} compute dtype to {dtype}") + + if enable_fp16_accumulation: + if hasattr(torch.backends.cuda.matmul, "allow_fp16_accumulation"): + torch.backends.cuda.matmul.allow_fp16_accumulation = True + else: + raise RuntimeError("Failed to set fp16 accumulation, requires pytorch version 2.7.1 or higher") + else: + if hasattr(torch.backends.cuda.matmul, "allow_fp16_accumulation"): + torch.backends.cuda.matmul.allow_fp16_accumulation = False + + if sage_attention != "disabled": + new_attention = get_sage_func(sage_attention) + def attention_override_sage(func, *args, **kwargs): + return new_attention.__wrapped__(*args, **kwargs) + + # attention override + model.model_options["transformer_options"]["optimized_attention_override"] = attention_override_sage + + return model, clip, vae + + +class DiffusionModelSelector(): + @classmethod + def INPUT_TYPES(s): + ltx2_connector_models = folder_paths.get_filename_list("text_encoders") + ltx2_connector_models = [m for m in ltx2_connector_models if "connector" in m.lower()] + return {"required": { + "model_name": (folder_paths.get_filename_list("diffusion_models") + ltx2_connector_models, {"tooltip": "The name of the checkpoint (model) to load."}), + }, + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("model_path",) + FUNCTION = "get_path" + DESCRIPTION = "Returns the path to the model as a string." + EXPERIMENTAL = True + CATEGORY = "KJNodes/model_loaders" + + def get_path(self, model_name): + if "connector" in model_name.lower(): + model_path = folder_paths.get_full_path_or_raise("text_encoders", model_name) + else: + model_path = folder_paths.get_full_path_or_raise("diffusion_models", model_name) + return (model_path,) + +def _load_diffusion_model_kj(unet_path, model_options=None, extra_state_dict=None, disable_dynamic=False): + model_options = {} if model_options is None else dict(model_options) + + sd, metadata = comfy.utils.load_torch_file(unet_path, return_metadata=True) + if extra_state_dict is not None: + extra_sd = comfy.utils.load_torch_file(extra_state_dict) + sd.update(extra_sd) + del extra_sd + + diffusion_model_prefix = comfy.sd.model_detection.unet_prefix_from_state_dict(sd) + sd = comfy.utils.state_dict_prefix_replace(sd, {diffusion_model_prefix: ""}, filter_keys=False) + + model = comfy.sd.load_diffusion_model_state_dict( + sd, + model_options=model_options, + metadata=metadata, + disable_dynamic=disable_dynamic, + ) + + model.cached_patcher_init = (_load_diffusion_model_kj, (unet_path, model_options, extra_state_dict)) + return model + +class DiffusionModelLoaderKJ(): + @classmethod + def INPUT_TYPES(s): + return {"required": { + "model_name": (folder_paths.get_filename_list("diffusion_models"), {"tooltip": "The name of the checkpoint (model) to load."}), + "weight_dtype": (["default", "fp8_e4m3fn", "fp8_e4m3fn_fast", "fp8_e5m2", "fp16", "bf16", "fp32"],), + "compute_dtype": (["default", "fp16", "bf16", "fp32"], {"default": "default", "tooltip": "The compute dtype to use for the model."}), + "patch_cublaslinear": ("BOOLEAN", {"default": False, "tooltip": "Enable or disable the cublas_ops arg"}), + "sage_attention": (sageattn_modes, {"default": False, "tooltip": "Patch comfy attention to use sageattn."}), + "enable_fp16_accumulation": ("BOOLEAN", {"default": False, "tooltip": "Enable torch.backends.cuda.matmul.allow_fp16_accumulation, requires pytorch 2.7.0 nightly."}), + }, + "optional": { + "extra_state_dict": ("STRING", {"forceInput": True, "tooltip": "The full path to an additional state dict to load, this will be merged with the main state dict. Useful for example to add VACE module to a WanVideoModel. You can use DiffusionModelSelector to easily get the path."}), + } + } + + RETURN_TYPES = ("MODEL",) + FUNCTION = "patch_and_load" + DESCRIPTION = "Node for patching torch.nn.Linear with CublasLinear." + EXPERIMENTAL = True + CATEGORY = "KJNodes/model_loaders" + + def patch_and_load(self, model_name, weight_dtype, compute_dtype, patch_cublaslinear, sage_attention, enable_fp16_accumulation, extra_state_dict=None): + DTYPE_MAP = { + "fp8_e4m3fn": torch.float8_e4m3fn, + "fp8_e5m2": torch.float8_e5m2, + "fp16": torch.float16, + "bf16": torch.bfloat16, + "fp32": torch.float32 + } + model_options = {} + if dtype := DTYPE_MAP.get(weight_dtype): + model_options["dtype"] = dtype + logging.info(f"Setting {model_name} weight dtype to {dtype}") + + if weight_dtype == "fp8_e4m3fn_fast": + model_options["dtype"] = torch.float8_e4m3fn + model_options["fp8_optimizations"] = True + + if enable_fp16_accumulation: + if hasattr(torch.backends.cuda.matmul, "allow_fp16_accumulation"): + torch.backends.cuda.matmul.allow_fp16_accumulation = True + else: + raise RuntimeError("Failed to set fp16 accumulation, this requires pytorch 2.7.1 or higher") + else: + if hasattr(torch.backends.cuda.matmul, "allow_fp16_accumulation"): + torch.backends.cuda.matmul.allow_fp16_accumulation = False + + if patch_cublaslinear: + args.fast.add("cublas_ops") + else: + args.fast.discard("cublas_ops") + + unet_path = folder_paths.get_full_path_or_raise("diffusion_models", model_name) + + model = _load_diffusion_model_kj(unet_path, model_options=model_options, extra_state_dict=extra_state_dict) + if dtype := DTYPE_MAP.get(compute_dtype): + model.set_model_compute_dtype(dtype) + model.force_cast_weights = False + logging.info(f"Setting {model_name} compute dtype to {dtype}") + + if sage_attention != "disabled": + new_attention = get_sage_func(sage_attention) + def attention_override_sage(func, *args, **kwargs): + return new_attention.__wrapped__(*args, **kwargs) + + # attention override + model.model_options["transformer_options"]["optimized_attention_override"] = attention_override_sage + + return (model,) + +class ModelPatchTorchSettings: + @classmethod + def INPUT_TYPES(s): + return {"required": { + "model": ("MODEL",), + "enable_fp16_accumulation": ("BOOLEAN", {"default": False, "tooltip": "Enable torch.backends.cuda.matmul.allow_fp16_accumulation, requires pytorch 2.7.0 nightly."}), + }} + + RETURN_TYPES = ("MODEL",) + FUNCTION = "patch" + DESCRIPTION = "Adds callbacks to model to set torch settings before and after running the model." + EXPERIMENTAL = True + CATEGORY = "KJNodes/experimental" + + def patch(self, model, enable_fp16_accumulation): + model_clone = model.clone() + + def patch_enable_fp16_accum(model): + logging.info("Patching torch settings: torch.backends.cuda.matmul.allow_fp16_accumulation = True") + torch.backends.cuda.matmul.allow_fp16_accumulation = True + def patch_disable_fp16_accum(model): + logging.info("Patching torch settings: torch.backends.cuda.matmul.allow_fp16_accumulation = False") + torch.backends.cuda.matmul.allow_fp16_accumulation = False + + if enable_fp16_accumulation: + if hasattr(torch.backends.cuda.matmul, "allow_fp16_accumulation"): + model_clone.add_callback(CallbacksMP.ON_PRE_RUN, patch_enable_fp16_accum) + model_clone.add_callback(CallbacksMP.ON_CLEANUP, patch_disable_fp16_accum) + else: + raise RuntimeError("Failed to set fp16 accumulation, this requires pytorch 2.7.1 or higher") + else: + if hasattr(torch.backends.cuda.matmul, "allow_fp16_accumulation"): + model_clone.add_callback(CallbacksMP.ON_PRE_RUN, patch_disable_fp16_accum) + else: + raise RuntimeError("Failed to set fp16 accumulation, this requires pytorch 2.7.1 or higher") + + return (model_clone,) + + +class PatchModelPatcherOrder: + @classmethod + def INPUT_TYPES(s): + return {"required": { + "model": ("MODEL",), + "patch_order": (["object_patch_first", "weight_patch_first"], {"default": "weight_patch_first", "tooltip": "Patch the comfy patch_model function to load weight patches (LoRAs) before compiling the model"}), + "full_load": (["enabled", "disabled", "auto"], {"default": "auto", "tooltip": "Disabling may help with memory issues when loading large models, when changing this you should probably force model reload to avoid issues!"}), + }} + RETURN_TYPES = ("MODEL",) + FUNCTION = "patch" + CATEGORY = "KJNodes/deprecated" + DESCRIPTION = "NO LONGER NECESSARY OR FUNCTIONAL, keeping node for backwards compatibility. Use the TorchCompileModelAdvanced to use LoRA with torch.compile." + DEPRECATED = True + + def patch(self, model, patch_order, full_load): + return model, + + +class TorchCompileModelFluxAdvancedV2: + def __init__(self): + self._compiled = False + + @classmethod + def INPUT_TYPES(s): + return {"required": { + "model": ("MODEL",), + "backend": (["inductor", "cudagraphs"],), + "fullgraph": ("BOOLEAN", {"default": False, "tooltip": "Enable full graph mode"}), + "mode": (["default", "max-autotune", "max-autotune-no-cudagraphs", "reduce-overhead"], {"default": "default"}), + "double_blocks": ("BOOLEAN", {"default": True, "tooltip": "Compile double blocks"}), + "single_blocks": ("BOOLEAN", {"default": True, "tooltip": "Compile single blocks"}), + "dynamic": ("BOOLEAN", {"default": False, "tooltip": "Enable dynamic mode"}), + }, + "optional": { + "dynamo_cache_size_limit": ("INT", {"default": 64, "min": 0, "max": 1024, "step": 1, "tooltip": "torch._dynamo.config.cache_size_limit"}), + "force_parameter_static_shapes": ("BOOLEAN", {"default": True, "tooltip": "torch._dynamo.config.force_parameter_static_shapes"}), + } + } + RETURN_TYPES = ("MODEL",) + FUNCTION = "patch" + + CATEGORY = "KJNodes/torchcompile" + EXPERIMENTAL = True + DEPRECATED = True + DESCRIPTION = "Deprecated, use TorchCompileModelAdvanced instead." + + def patch(self, model, backend, mode, fullgraph, single_blocks, double_blocks, dynamic, dynamo_cache_size_limit=64, force_parameter_static_shapes=True): + from comfy_api.torch_helpers import set_torch_compile_wrapper + m = model.clone() + diffusion_model = m.get_model_object("diffusion_model") + torch._dynamo.config.cache_size_limit = dynamo_cache_size_limit + torch._dynamo.config.force_parameter_static_shapes = force_parameter_static_shapes + + compile_key_list = [] + + try: + if double_blocks: + for i, block in enumerate(diffusion_model.double_blocks): + print("Adding double block to compile list", i) + compile_key_list.append(f"diffusion_model.double_blocks.{i}") + if single_blocks: + for i, block in enumerate(diffusion_model.single_blocks): + compile_key_list.append(f"diffusion_model.single_blocks.{i}") + + set_torch_compile_wrapper(model=m, keys=compile_key_list, backend=backend, mode=mode, dynamic=dynamic, fullgraph=fullgraph) + except Exception as e: + raise RuntimeError("Failed to compile model") from e + + return (m, ) + + +class TorchCompileModelWanVideoV2: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "model": ("MODEL",), + "backend": (["inductor","cudagraphs"], {"default": "inductor"}), + "fullgraph": ("BOOLEAN", {"default": False, "tooltip": "Enable full graph mode"}), + "mode": (["default", "max-autotune", "max-autotune-no-cudagraphs", "reduce-overhead"], {"default": "default"}), + "dynamic": ("BOOLEAN", {"default": False, "tooltip": "Enable dynamic mode"}), + "compile_transformer_blocks_only": ("BOOLEAN", {"default": True, "tooltip": "Compile only transformer blocks, faster compile and less error prone"}), + "dynamo_cache_size_limit": ("INT", {"default": 64, "min": 0, "max": 1024, "step": 1, "tooltip": "torch._dynamo.config.cache_size_limit"}), + + }, + "optional": { + "force_parameter_static_shapes": ("BOOLEAN", {"default": True, "tooltip": "torch._dynamo.config.force_parameter_static_shapes"}), + }, + } + RETURN_TYPES = ("MODEL",) + FUNCTION = "patch" + + CATEGORY = "KJNodes/torchcompile" + EXPERIMENTAL = True + DEPRECATED = True + DESCRIPTION = "Deprecated, use TorchCompileModelAdvanced instead." + + def patch(self, model, backend, fullgraph, mode, dynamic, dynamo_cache_size_limit, compile_transformer_blocks_only, force_parameter_static_shapes=True): + from comfy_api.torch_helpers import set_torch_compile_wrapper + m = model.clone() + diffusion_model = m.get_model_object("diffusion_model") + torch._dynamo.config.cache_size_limit = dynamo_cache_size_limit + torch._dynamo.config.force_parameter_static_shapes = force_parameter_static_shapes + try: + if compile_transformer_blocks_only: + compile_key_list = [] + for i, block in enumerate(diffusion_model.blocks): + compile_key_list.append(f"diffusion_model.blocks.{i}") + else: + compile_key_list =["diffusion_model"] + + set_torch_compile_wrapper(model=m, keys=compile_key_list, backend=backend, mode=mode, dynamic=dynamic, fullgraph=fullgraph) + except Exception as e: + raise RuntimeError("Failed to compile model") from e + + return (m, ) + + +_aimdo_patched = False + +def patch_aimdo_for_compile(): + # reduce recompiles with dynamic VRAM + global _aimdo_patched + if _aimdo_patched: + return + _aimdo_patched = True + names = ("cast_bias_weight", "uncast_bias_weight", "cast_modules_with_vbar", "resolve_cast_module_with_vbar") + for name in names: + fn = getattr(comfy.ops, name, None) + if fn is not None: + setattr(comfy.ops, name, torch._dynamo.disable(fn)) + try: + import comfy_aimdo.torch as _at + _at.get_tensor_from_raw_ptr = torch._dynamo.disable(_at.get_tensor_from_raw_ptr) + except Exception: + pass + logging.info("KJNodes dynamic-compile: comfy.ops weight cast marked as eager graph break (recompile fix active).") + + +def skip_torch_compile_dict(guard_entries): + # don't recompile when transformer_options change + return [("transformer_options" not in entry.name) for entry in guard_entries] + + +def build_compile_kwargs(backend, mode, fullgraph, dynamic, use_guard_filter=True): + # torch.compile forbids passing mode and options together; an explicit mode wins, + # otherwise attach the guard filter via options on the default mode. + kw = {"backend": backend, "fullgraph": fullgraph, "dynamic": dynamic} + if mode and mode != "default": + kw["mode"] = mode + elif use_guard_filter: + kw["options"] = {"guard_filter_fn": skip_torch_compile_dict} + return kw + +import weakref as _kj_weakref + +_KJ_COMPILE_KEY = "torch.compile" +_KJ_COMPILED_BY_MODEL = _kj_weakref.WeakKeyDictionary() # BaseModel instance -> {key: compiled_module} + +# resolve compiled modules by the BaseModel actually executing +def _kj_apply_torch_compile_wrapper(executor, *args, **kwargs): + compiled = _KJ_COMPILED_BY_MODEL.get(executor.class_obj) + if not compiled: + return executor(*args, **kwargs) # this BaseModel wasn't compiled -> run eager, no swap + orig = {} + try: + for key, value in compiled.items(): + orig[key] = comfy.utils.get_attr(executor.class_obj, key) + comfy.utils.set_attr(executor.class_obj, key, value) + return executor(*args, **kwargs) + finally: + for key, value in orig.items(): + comfy.utils.set_attr(executor.class_obj, key, value) + + +def kj_set_torch_compile_wrapper(model, backend, options=None, mode=None, fullgraph=False, dynamic=None, keys=("diffusion_model",)): + WrappersMP = comfy.patcher_extension.WrappersMP + model.remove_wrappers_with_key(WrappersMP.APPLY_MODEL, _KJ_COMPILE_KEY) + if not keys: + keys = ["diffusion_model"] + compile_kwargs = {"backend": backend, "options": options, "mode": mode, "fullgraph": fullgraph, "dynamic": dynamic} + compiled_modules = {key: torch.compile(model=model.get_model_object(key), **compile_kwargs) for key in keys} + _KJ_COMPILED_BY_MODEL[model.model] = compiled_modules # register by the BaseModel that will execute + model.add_wrapper_with_key(WrappersMP.APPLY_MODEL, _KJ_COMPILE_KEY, _kj_apply_torch_compile_wrapper) + model.model_options["torch_compile_kwargs"] = compile_kwargs + + +class TorchCompileModelAdvanced: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "model": ("MODEL",), + "backend": (["inductor","cudagraphs"], {"default": "inductor"}), + "fullgraph": ("BOOLEAN", {"default": False, "tooltip": "Enable full graph mode"}), + "mode": (["default", "max-autotune", "max-autotune-no-cudagraphs", "reduce-overhead"], {"default": "default"}), + "dynamic": ( + ["auto", "true", "false"], + {"default": "false", "tooltip": "Use dynamic shape tracing."}, + ), + "compile_transformer_blocks_only": ("BOOLEAN", {"default": True, "tooltip": "Compile only transformer blocks, faster compile and less error prone"}), + "dynamo_cache_size_limit": ("INT", {"default": 64, "min": 0, "max": 1024, "step": 1, "tooltip": "torch._dynamo.config.cache_size_limit"}), + "debug_compile_keys": ("BOOLEAN", {"default": False, "tooltip": "Print the compile keys used for torch.compile"}), + }, + "optional": { + "disable_dynamic_vram": ("BOOLEAN", {"default": False, "tooltip": "Disable dynamic VRAM feature as it can cause issues with compile"}), + } + } + RETURN_TYPES = ("MODEL",) + FUNCTION = "patch" + CATEGORY = "KJNodes/torchcompile" + DESCRIPTION = "Advanced torch.compile patching for diffusion models." + EXPERIMENTAL = True + + def patch(self, model, backend, fullgraph, mode, dynamic, dynamo_cache_size_limit, compile_transformer_blocks_only, debug_compile_keys, disable_dynamic_vram=False): + if disable_dynamic_vram: + try: + m = model.clone(disable_dynamic=True) + except TypeError: + logging.warning("This ComfyUI version do not support disabling dynamic VRAM through a node. This may cause issues with torch.compile.") + m = model.clone() + else: + m = model.clone() + + diffusion_model = m.get_model_object("diffusion_model") + torch._dynamo.config.cache_size_limit = dynamo_cache_size_limit + + try: + compile_key_list = [] + if compile_transformer_blocks_only: + layer_types = ["double_blocks", "single_blocks", "layers", "transformer_blocks", "blocks", "visual_transformer_blocks", "text_transformer_blocks", "patch_blocks", "pixel_blocks"] + for layer_name in layer_types: + if hasattr(diffusion_model, layer_name): + blocks = getattr(diffusion_model, layer_name) + for i in range(len(blocks)): + compile_key_list.append(f"diffusion_model.{layer_name}.{i}") + if not compile_key_list: + logging.warning("No known transformer blocks found to compile, compiling entire diffusion model instead") + elif debug_compile_keys: + logging.info("TorchCompileModelAdvanced: Compile key list:") + for key in compile_key_list: + logging.info(f" - {key}") + if not compile_key_list: + compile_key_list =["diffusion_model"] + + dynamic_kv = {"true": True, "false": False, "auto": None} + try: + dynamic = dynamic_kv[dynamic] + except KeyError: + raise ValueError(f"Invalid dynamic arg {dynamic}") + + if not disable_dynamic_vram and getattr(m, "is_dynamic", lambda: False)(): + patch_aimdo_for_compile() # reduce recompiles with dynamic VRAM, will break the graph still but better than nothing + kj_set_torch_compile_wrapper(model=m, keys=compile_key_list, **build_compile_kwargs(backend, mode, fullgraph, dynamic)) + except Exception as e: + raise RuntimeError("Failed to compile model") from e + + return (m, ) + + +class TorchCompileModelQwenImage: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "model": ("MODEL",), + "backend": (["inductor","cudagraphs"], {"default": "inductor"}), + "fullgraph": ("BOOLEAN", {"default": False, "tooltip": "Enable full graph mode"}), + "mode": (["default", "max-autotune", "max-autotune-no-cudagraphs", "reduce-overhead"], {"default": "default"}), + "dynamic": ("BOOLEAN", {"default": False, "tooltip": "Enable dynamic mode"}), + "compile_transformer_blocks_only": ("BOOLEAN", {"default": True, "tooltip": "Compile only transformer blocks, faster compile and less error prone"}), + "dynamo_cache_size_limit": ("INT", {"default": 64, "min": 0, "max": 1024, "step": 1, "tooltip": "torch._dynamo.config.cache_size_limit"}), + }, + } + RETURN_TYPES = ("MODEL",) + FUNCTION = "patch" + + CATEGORY = "KJNodes/torchcompile" + EXPERIMENTAL = True + DEPRECATED = True + DESCRIPTION = "Deprecated, use TorchCompileModelAdvanced instead." + + def patch(self, model, backend, fullgraph, mode, dynamic, dynamo_cache_size_limit, compile_transformer_blocks_only): + from comfy_api.torch_helpers import set_torch_compile_wrapper + m = model.clone() + diffusion_model = m.get_model_object("diffusion_model") + torch._dynamo.config.cache_size_limit = dynamo_cache_size_limit + try: + if compile_transformer_blocks_only: + compile_key_list = [] + for i, block in enumerate(diffusion_model.transformer_blocks): + compile_key_list.append(f"diffusion_model.transformer_blocks.{i}") + else: + compile_key_list =["diffusion_model"] + + set_torch_compile_wrapper(model=m, keys=compile_key_list, backend=backend, mode=mode, dynamic=dynamic, fullgraph=fullgraph) + except Exception as e: + raise RuntimeError("Failed to compile model") from e + + return (m, ) + +class TorchCompileVAE: + def __init__(self): + self._compiled_encoder = False + self._compiled_decoder = False + + @classmethod + def INPUT_TYPES(s): + return {"required": { + "vae": ("VAE",), + "backend": (["inductor", "cudagraphs"],), + "fullgraph": ("BOOLEAN", {"default": False, "tooltip": "Enable full graph mode"}), + "mode": (["default", "max-autotune", "max-autotune-no-cudagraphs", "reduce-overhead"], {"default": "default"}), + "compile_encoder": ("BOOLEAN", {"default": True, "tooltip": "Compile encoder"}), + "compile_decoder": ("BOOLEAN", {"default": True, "tooltip": "Compile decoder"}), + }} + RETURN_TYPES = ("VAE",) + FUNCTION = "compile" + + CATEGORY = "KJNodes/torchcompile" + EXPERIMENTAL = True + + def compile(self, vae, backend, mode, fullgraph, compile_encoder, compile_decoder): + if compile_encoder: + if not self._compiled_encoder: + encoder_name = "encoder" + if hasattr(vae.first_stage_model, "taesd_encoder"): + encoder_name = "taesd_encoder" + + try: + setattr( + vae.first_stage_model, + encoder_name, + torch.compile( + getattr(vae.first_stage_model, encoder_name), + mode=mode, + fullgraph=fullgraph, + backend=backend, + ), + ) + self._compiled_encoder = True + except Exception as e: + raise RuntimeError("Failed to compile model") from e + if compile_decoder: + if not self._compiled_decoder: + decoder_name = "decoder" + if hasattr(vae.first_stage_model, "taesd_decoder"): + decoder_name = "taesd_decoder" + + try: + setattr( + vae.first_stage_model, + decoder_name, + torch.compile( + getattr(vae.first_stage_model, decoder_name), + mode=mode, + fullgraph=fullgraph, + backend=backend, + ), + ) + self._compiled_decoder = True + except Exception as e: + raise RuntimeError("Failed to compile model") from e + return (vae, ) + +class TorchCompileControlNet: + def __init__(self): + self._compiled= False + + @classmethod + def INPUT_TYPES(s): + return {"required": { + "controlnet": ("CONTROL_NET",), + "backend": (["inductor", "cudagraphs"],), + "fullgraph": ("BOOLEAN", {"default": False, "tooltip": "Enable full graph mode"}), + "mode": (["default", "max-autotune", "max-autotune-no-cudagraphs", "reduce-overhead"], {"default": "default"}), + }} + RETURN_TYPES = ("CONTROL_NET",) + FUNCTION = "compile" + + CATEGORY = "KJNodes/torchcompile" + EXPERIMENTAL = True + + def compile(self, controlnet, backend, mode, fullgraph): + if not self._compiled: + try: + # for i, block in enumerate(controlnet.control_model.double_blocks): + # print("Compiling controlnet double_block", i) + # controlnet.control_model.double_blocks[i] = torch.compile(block, mode=mode, fullgraph=fullgraph, backend=backend) + controlnet.control_model = torch.compile(controlnet.control_model, mode=mode, fullgraph=fullgraph, backend=backend) + self._compiled = True + except Exception as e: + self._compiled = False + raise RuntimeError("Failed to compile model") from e + + return (controlnet, ) + +class WanVideoTeaCacheKJ: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "model": ("MODEL",), + "rel_l1_thresh": ("FLOAT", {"default": 0.275, "min": 0.0, "max": 10.0, "step": 0.001, "tooltip": "Threshold for to determine when to apply the cache, compromise between speed and accuracy. When using coefficients a good value range is something between 0.2-0.4 for all but 1.3B model, which should be about 10 times smaller, same as when not using coefficients."}), + "start_percent": ("FLOAT", {"default": 0.1, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "The start percentage of the steps to use with TeaCache."}), + "end_percent": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "The end percentage of the steps to use with TeaCache."}), + "cache_device": (["main_device", "offload_device"], {"default": "offload_device", "tooltip": "Device to cache to"}), + "coefficients": (["disabled", "1.3B", "14B", "i2v_480", "i2v_720"], {"default": "i2v_480", "tooltip": "Coefficients for rescaling the relative l1 distance, if disabled the threshold value should be about 10 times smaller than the value used with coefficients."}), + } + } + + RETURN_TYPES = ("MODEL",) + RETURN_NAMES = ("model",) + FUNCTION = "patch_teacache" + CATEGORY = "KJNodes/deprecated" + DEPRECATED = True + DESCRIPTION = """DEPRECATED, use the native EasyCache or alternative custom node that's up to date instead of this.""" + EXPERIMENTAL = True + + def patch_teacache(self, model, rel_l1_thresh, start_percent, end_percent, cache_device, coefficients): + return model, + + +from comfy.ldm.flux.math import apply_rope + +def modified_wan_self_attention_forward(self, x, freqs, transformer_options={}): + r""" + Args: + x(Tensor): Shape [B, L, num_heads, C / num_heads] + freqs(Tensor): Rope freqs, shape [1024, C / num_heads / 2] + """ + b, s, n, d = *x.shape[:2], self.num_heads, self.head_dim + + # query, key, value function + def qkv_fn(x): + q = self.norm_q(self.q(x)).view(b, s, n, d) + k = self.norm_k(self.k(x)).view(b, s, n, d) + v = self.v(x).view(b, s, n * d) + return q, k, v + + q, k, v = qkv_fn(x) + q, k = apply_rope(q, k, freqs) + feta_scores = get_feta_scores(q, k, self.num_frames, self.enhance_weight) + + x = comfy.ldm.modules.attention.optimized_attention( + q.view(b, s, n * d), + k.view(b, s, n * d), + v, + heads=self.num_heads, + transformer_options=transformer_options, + ) + + x = self.o(x) + + x *= feta_scores + + return x + +from einops import rearrange +def get_feta_scores(query, key, num_frames, enhance_weight, num_heads=12): + img_q, img_k = query, key #torch.Size([2, 9216, 12, 128]) + + if img_q.ndim == 4: + B, ST, num_heads, head_dim = img_q.shape + elif img_q.ndim == 3: + B, ST, hidden_dim = img_q.shape + head_dim = hidden_dim // num_heads + + # Reshape from [B, ST, hidden_dim] to [B, ST, num_heads, head_dim] + img_q = img_q.view(B, ST, num_heads, head_dim) + img_k = img_k.view(B, ST, num_heads, head_dim) + + spatial_dim = ST // num_frames + + query_image = rearrange( + img_q, "B (T S) N C -> (B S) N T C", T=num_frames, S=spatial_dim, N=num_heads, C=head_dim + ) + key_image = rearrange( + img_k, "B (T S) N C -> (B S) N T C", T=num_frames, S=spatial_dim, N=num_heads, C=head_dim + ) + + return feta_score(query_image, key_image, head_dim, num_frames, enhance_weight) + +def feta_score(query_image, key_image, head_dim, num_frames, enhance_weight): + scale = head_dim**-0.5 + query_image = query_image * scale + attn_temp = query_image @ key_image.transpose(-2, -1) # translate attn to float32 + attn_temp = attn_temp.to(torch.float32) + attn_temp = attn_temp.softmax(dim=-1) + + # Reshape to [batch_size * num_tokens, num_frames, num_frames] + attn_temp = attn_temp.reshape(-1, num_frames, num_frames) + + # Create a mask for diagonal elements + diag_mask = torch.eye(num_frames, device=attn_temp.device).bool() + diag_mask = diag_mask.unsqueeze(0).expand(attn_temp.shape[0], -1, -1) + + # Zero out diagonal elements + attn_wo_diag = attn_temp.masked_fill(diag_mask, 0) + + # Calculate mean for each token's attention matrix + # Number of off-diagonal elements per matrix is n*n - n + num_off_diag = num_frames * num_frames - num_frames + mean_scores = attn_wo_diag.sum(dim=(1, 2)) / num_off_diag + + enhance_scores = mean_scores.mean() * (num_frames + enhance_weight) + enhance_scores = enhance_scores.clamp(min=1) + return enhance_scores + +import types +class WanAttentionPatch: + def __init__(self, num_frames, weight): + self.num_frames = num_frames + self.enhance_weight = weight + + def __get__(self, obj, objtype=None): + # Create bound method with stored parameters + def wrapped_attention(self_module, *args, **kwargs): + self_module.num_frames = self.num_frames + self_module.enhance_weight = self.enhance_weight + return modified_wan_self_attention_forward(self_module, *args, **kwargs) + return types.MethodType(wrapped_attention, obj) + +class WanVideoEnhanceAVideoKJ: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "model": ("MODEL",), + "latent": ("LATENT", {"tooltip": "Only used to get the latent count"}), + "weight": ("FLOAT", {"default": 2.0, "min": 0.0, "max": 10.0, "step": 0.001, "tooltip": "Strength of the enhance effect"}), + } + } + + RETURN_TYPES = ("MODEL",) + RETURN_NAMES = ("model",) + FUNCTION = "enhance" + CATEGORY = "KJNodes/wan" + DESCRIPTION = "https://github.com/NUS-HPC-AI-Lab/Enhance-A-Video" + EXPERIMENTAL = True + + def enhance(self, model, weight, latent): + if weight == 0: + return (model,) + + num_frames = latent["samples"].shape[2] + + model_clone = model.clone() + if 'transformer_options' not in model_clone.model_options: + model_clone.model_options['transformer_options'] = {} + model_clone.model_options["transformer_options"]["enhance_weight"] = weight + diffusion_model = model_clone.get_model_object("diffusion_model") + + compile_settings = getattr(model.model, "compile_settings", None) + for idx, block in enumerate(diffusion_model.blocks): + patched_attn = WanAttentionPatch(num_frames, weight).__get__(block.self_attn, block.__class__) + if compile_settings is not None: + patched_attn = torch.compile(patched_attn, mode=compile_settings["mode"], dynamic=compile_settings["dynamic"], fullgraph=compile_settings["fullgraph"], backend=compile_settings["backend"]) + + model_clone.add_object_patch(f"diffusion_model.blocks.{idx}.self_attn.forward", patched_attn) + + return (model_clone,) + +try: + from comfy.ldm.lightricks.model import apply_rotary_emb +except ImportError: + apply_rotary_emb = None + +try: + from comfy.ldm.lightricks.model import GuideAttentionMask as _GuideAttentionMask, _attention_with_guide_mask as _ltx_attn_with_guide_mask +except ImportError: + _GuideAttentionMask = None + _ltx_attn_with_guide_mask = None + + +def ltxv_feta_forward(self, x, context=None, mask=None, pe=None, k_pe=None, transformer_options={}): + q = self.to_q(x) + context = x if context is None else context + k = self.to_k(context) + v = self.to_v(context) + + q = self.q_norm(q) + k = self.k_norm(k) + + if pe is not None: + q = apply_rotary_emb(q, pe) + k = apply_rotary_emb(k, pe if k_pe is None else k_pe) + + feta_scores = get_feta_scores(q, k, self.num_frames, self.enhance_weight, self.heads) + + if mask is None: + out = comfy.ldm.modules.attention.optimized_attention(q, k, v, self.heads, attn_precision=self.attn_precision, transformer_options=transformer_options) + elif _GuideAttentionMask is not None and isinstance(mask, _GuideAttentionMask): + out = _ltx_attn_with_guide_mask(q, k, v, self.heads, mask, attn_precision=self.attn_precision, transformer_options=transformer_options) + else: + out = comfy.ldm.modules.attention.optimized_attention_masked(q, k, v, self.heads, mask, attn_precision=self.attn_precision, transformer_options=transformer_options) + + if self.to_gate_logits is not None: + gate_logits = self.to_gate_logits(x) # (B, T, H) + b, t, _ = out.shape + out = out.view(b, t, self.heads, self.dim_head) + gates = 2.0 * torch.sigmoid(gate_logits) # zero-init -> identity + out = out * gates.unsqueeze(-1) + out = out.view(b, t, self.heads * self.dim_head) + + return self.to_out(out) * feta_scores + + +class LTXCrossAttentionPatch: + def __init__(self, num_frames, weight): + self.num_frames = num_frames + self.enhance_weight = weight + + def __get__(self, obj, objtype=None): + # Create bound method with stored parameters + def wrapped_attention(self_module, *args, **kwargs): + self_module.num_frames = self.num_frames + self_module.enhance_weight = self.enhance_weight + return ltxv_feta_forward(self_module, *args, **kwargs) + return types.MethodType(wrapped_attention, obj) + +class LTXVEnhanceAVideoKJ: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "model": ("MODEL",), + "latent": ("LATENT", {"tooltip": "Only used to get the latent count"}), + "weight": ("FLOAT", {"default": 4.0, "min": 0.0, "max": 100.0, "step": 0.001, "tooltip": "Strength of the enhance effect"}), + } + } + + RETURN_TYPES = ("MODEL",) + RETURN_NAMES = ("model",) + FUNCTION = "enhance" + CATEGORY = "KJNodes/ltxv" + DESCRIPTION = "https://github.com/NUS-HPC-AI-Lab/Enhance-A-Video" + EXPERIMENTAL = True + + def enhance(self, model, weight, latent): + if weight == 0: + return (model,) + + num_frames = latent["samples"].shape[2] + + model_clone = model.clone() + if 'transformer_options' not in model_clone.model_options: + model_clone.model_options['transformer_options'] = {} + model_clone.model_options["transformer_options"]["enhance_weight"] = weight + diffusion_model = model_clone.get_model_object("diffusion_model") + + for idx, block in enumerate(diffusion_model.transformer_blocks): + patched_attn1 = LTXCrossAttentionPatch(num_frames, weight).__get__(block.attn1, block.__class__) + model_clone.add_object_patch(f"diffusion_model.transformer_blocks.{idx}.attn1.forward", patched_attn1) + return (model_clone,) + +def _wan_compute_attention(self, query, context, transformer_options={}): + k = self.norm_k(self.k(context)) + v = self.v(context) + return comfy.ldm.modules.attention.optimized_attention(query, k, v, heads=self.num_heads, transformer_options=transformer_options).flatten(2) + +def wan_nag_attention(self, query, context_positive, nag_context, transformer_options={}): + x_positive = _wan_compute_attention(self, query, context_positive, transformer_options) + x_negative = _wan_compute_attention(self, query, nag_context, transformer_options) + return x_positive, x_negative + +def normalized_attention_guidance(self, x_positive, x_negative): + if self.inplace: + nag_guidance = x_negative.mul_(self.nag_scale - 1).neg_().add_(x_positive, alpha=self.nag_scale) + del x_negative + else: + nag_guidance = x_negative * (self.nag_scale - 1) + del x_negative + nag_guidance = (x_positive * self.nag_scale).sub_(nag_guidance) + + norm_positive = torch.norm(x_positive, p=1, dim=-1, keepdim=True) + norm_guidance = torch.norm(nag_guidance, p=1, dim=-1, keepdim=True) + + scale = norm_guidance / norm_positive + torch.nan_to_num_(scale, nan=10.0) + mask = scale > self.nag_tau + del scale + + adjustment = (norm_positive * self.nag_tau) / (norm_guidance + 1e-7) + del norm_positive, norm_guidance + + nag_guidance.mul_(torch.where(mask, adjustment, 1.0)) + del mask, adjustment + + if self.inplace: + return nag_guidance.sub_(x_positive).mul_(self.nag_alpha).add_(x_positive) + else: + nag_guidance.mul_(self.nag_alpha) + return nag_guidance.add_(x_positive * (1 - self.nag_alpha)) + +#region NAG +def wan_crossattn_forward_nag(self, x, context, transformer_options={}, **kwargs): + # Determine batch splitting and context handling + if self.input_type == "default": + # Single or [pos, neg] pair + if context.shape[0] == 1: + x_pos, context_pos = x, context + x_neg, context_neg = None, None + else: + x_pos, x_neg = torch.chunk(x, 2, dim=0) + context_pos, context_neg = torch.chunk(context, 2, dim=0) + elif self.input_type == "batch": + # Standard batch, no CFG + x_pos, context_pos = x, context + x_neg, context_neg = None, None + + # Positive branch + q_pos = self.norm_q(self.q(x_pos)) + nag_context = self.nag_context + if self.input_type == "batch": + nag_context = nag_context.repeat(x_pos.shape[0], 1, 1) + del x_pos + + x_positive, x_negative = wan_nag_attention(self, q_pos, context_pos, nag_context, transformer_options=transformer_options) + del context_pos, q_pos + + x_pos_out = normalized_attention_guidance(self, x_positive, x_negative) + del x_positive, x_negative + + # Negative branch + if x_neg is not None and context_neg is not None: + q_neg = self.norm_q(self.q(x_neg)) + k_neg = self.norm_k(self.k(context_neg)) + v_neg = self.v(context_neg) + x_neg_out = comfy.ldm.modules.attention.optimized_attention(q_neg, k_neg, v_neg, heads=self.num_heads, transformer_options=transformer_options) + x = torch.cat([x_pos_out, x_neg_out], dim=0) + else: + x = x_pos_out + + return self.o(x) + +def wan_i2v_crossattn_forward_nag(self, x, context, context_img_len, transformer_options={}, **kwargs): + context_img = context[:, :context_img_len] + context = context[:, context_img_len:] + + q_img = self.norm_q(self.q(x)) + k_img = self.norm_k_img(self.k_img(context_img)) + v_img = self.v_img(context_img) + img_x = comfy.ldm.modules.attention.optimized_attention(q_img, k_img, v_img, heads=self.num_heads, transformer_options=transformer_options) + del q_img, k_img, v_img, context_img + + if context.shape[0] == 2: + x, x_real_negative = torch.chunk(x, 2, dim=0) + context_positive, context_negative = torch.chunk(context, 2, dim=0) + else: + context_positive = context + context_negative = None + + q = self.norm_q(self.q(x)) + + x_positive, x_negative = wan_nag_attention(self, q, context_positive, self.nag_context, transformer_options=transformer_options) + del q, context_positive + x = normalized_attention_guidance(self, x_positive, x_negative) + del x_positive, x_negative + + if context_negative is not None: + q_real_negative = self.norm_q(self.q(x_real_negative)) + k_real_negative = self.norm_k(self.k(context_negative)) + v_real_negative = self.v(context_negative) + x_real_negative = comfy.ldm.modules.attention.optimized_attention(q_real_negative, k_real_negative, v_real_negative, heads=self.num_heads, transformer_options=transformer_options) + x = torch.cat([x, x_real_negative], dim=0) + + return self.o(x + img_x) + + +class WanCrossAttentionPatch: + def __init__(self, context, nag_scale, nag_alpha, nag_tau, i2v=False, input_type="default", inplace=True): + self.nag_context = context + self.nag_scale = nag_scale + self.nag_alpha = nag_alpha + self.nag_tau = nag_tau + self.i2v = i2v + self.input_type = input_type + self.inplace = inplace + def __get__(self, obj, objtype=None): + # Create bound method with stored parameters + def wrapped_attention(self_module, *args, **kwargs): + self_module.nag_context = self.nag_context + self_module.nag_scale = self.nag_scale + self_module.nag_alpha = self.nag_alpha + self_module.nag_tau = self.nag_tau + self_module.input_type = self.input_type + self_module.inplace = self.inplace + if self.i2v: + return wan_i2v_crossattn_forward_nag(self_module, *args, **kwargs) + else: + return wan_crossattn_forward_nag(self_module, *args, **kwargs) + return types.MethodType(wrapped_attention, obj) + + +class WanVideoNAG: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "model": ("MODEL",), + "conditioning": ("CONDITIONING",), + "nag_scale": ("FLOAT", {"default": 11.0, "min": 0.0, "max": 100.0, "step": 0.001, "tooltip": "Strength of negative guidance effect"}), + "nag_alpha": ("FLOAT", {"default": 0.25, "min": 0.0, "max": 1.0, "step": 0.001, "tooltip": "Mixing coefficient in that controls the balance between the normalized guided representation and the original positive representation."}), + "nag_tau": ("FLOAT", {"default": 2.5, "min": 0.0, "max": 10.0, "step": 0.001, "tooltip": "Clipping threshold that controls how much the guided attention can deviate from the positive attention."}), + }, + "optional": { + "input_type": (["default", "batch"], {"tooltip": "Type of the model input"}), + "inplace": ("BOOLEAN", {"default": False, "tooltip": "If true, modifies tensors in place to save memory. Leads to different numerical results which may change the output slightly."}), + }, + + } + + RETURN_TYPES = ("MODEL",) + RETURN_NAMES = ("model",) + FUNCTION = "patch" + CATEGORY = "KJNodes/wan" + DESCRIPTION = "https://github.com/ChenDarYen/Normalized-Attention-Guidance" + EXPERIMENTAL = True + + def patch(self, model, conditioning, nag_scale, nag_alpha, nag_tau, input_type="default", inplace=False): + if nag_scale == 0: + return (model,) + + device = mm.get_torch_device() + dtype = mm.unet_dtype() + + model_clone = model.clone() + + diffusion_model = model_clone.get_model_object("diffusion_model") + + diffusion_model.text_embedding.to(device) + context = diffusion_model.text_embedding(conditioning[0][0].to(device, dtype)) + + for idx, block in enumerate(diffusion_model.blocks): + i2v = hasattr(block.cross_attn, "k_img") + patched_attn = WanCrossAttentionPatch(context, nag_scale, nag_alpha, nag_tau, i2v, input_type=input_type, inplace=inplace).__get__(block.cross_attn, block.__class__) + + model_clone.add_object_patch(f"diffusion_model.blocks.{idx}.cross_attn.forward", patched_attn) + + return (model_clone,) + + +# Krea2 (K2) per-token prompt weighting via attention value scaling. +# ComfyUI's (token:weight) syntax is stripped by the Qwen3-VL tokenizer, and embedding-level weighting barely +# works on LLM text encoders anyway. Instead we scale the VALUE vectors of the weighted prompt tokens inside +# every block's self-attention +_QWEN_IM_START, _QWEN_USER, _QWEN_NL, _QWEN_IM_END = 151644, 872, 198, 151645 + +def _krea2_user_content_span(ids): + # token span of the user prompt, between '<|im_start|>user\n' and the next '<|im_end|>' + for i in range(len(ids) - 2): + if ids[i] == _QWEN_IM_START and ids[i + 1] == _QWEN_USER and ids[i + 2] == _QWEN_NL: + start = i + 3 + end = start + while end < len(ids) and ids[end] != _QWEN_IM_END: + end += 1 + return start, end + return None, None + +def _krea2_token_ids(clip, text): + tok = clip.tokenize(text) + key = next(iter(tok)) + return [t[0] for t in tok[key][0]] + +def _find_subsequence(seq, sub, lo, hi): + out = [] + n = len(sub) + if n == 0: + return out + for i in range(lo, hi - n + 1): + if seq[i:i + n] == sub: + out.append(i) + return out + + +def krea2_attn_forward_weight(self, x, freqs=None, mask=None, transformer_options={}): + from einops import rearrange + from comfy.ldm.flux.math import apply_rope + + q, k, v, gate = self.wq(x), self.wk(x), self.wv(x), self.gate(x) + q = rearrange(q, "B L (H D) -> B H L D", H=self.heads) + k = rearrange(k, "B L (H D) -> B H L D", H=self.kvheads) + v = rearrange(v, "B L (H D) -> B H L D", H=self.kvheads) + # weights: list of (pos, v_factor, k_bias). v_factor scales the token's VALUE (removal/de-emphasis; <0 flips + # to subtract the concept). k_bias adds to the token's attention logit (emphasis: more of the image attends + # to it -> more of the concept), which works far better for amplification than scaling the value. + weights = transformer_options.get("krea2_token_weights") + if weights: + v = v.clone() + for pos, v_factor, _ in weights: + if v_factor != 1.0 and pos < v.shape[2]: + v[:, :, pos] = v[:, :, pos] * v_factor + q, k = self.qknorm(q, k) + if freqs is not None: + q, k = apply_rope(q, k, freqs) + if self.kvheads != self.heads: + rep = self.heads // self.kvheads + k = k.repeat_interleave(rep, dim=1) + v = v.repeat_interleave(rep, dim=1) + bias = None + if weights and any(kb != 0.0 for _, _, kb in weights): + bias = q.new_zeros(1, k.shape[2]) + for pos, _, kb in weights: + if kb != 0.0 and pos < bias.shape[1]: + bias[:, pos] = kb + if bias is not None: + out = attention_pytorch(q, k, v, self.heads, mask=bias, skip_reshape=True) + else: + out = optimized_attention(q, k, v, self.heads, mask=mask, skip_reshape=True, transformer_options=transformer_options) + return self.wo(out * torch.sigmoid(gate)) + +class Krea2WeightPatch: + def __get__(self, obj, objtype=None): + return types.MethodType(krea2_attn_forward_weight, obj) + + +class Krea2PromptWeight: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "clip": ("CLIP",), + "model": ("MODEL",), + "text": ("STRING", {"multiline": True, "default": "", "tooltip": "Prompt with per-token weights in parentheses, e.g. (word:-1) removes/represses a concept, (word:2) emphasizes it, plain text = 1.0. Works on Krea2 where ComfyUI's normal (word:weight) does nothing (the Qwen3-VL LLM encoder ignores it). weight<1 scales the token's attention VALUE (subtracts/removes at <0); weight>1 boosts how much the image ATTENDS to the token (adds more of it). Set sampler CFG to 1.0."}), + "strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 4.0, "step": 0.05, "tooltip": "Global multiplier on the weighting effect. Effect compounds over all 28 blocks; lower if results break up, raise for a stronger effect. Removal (weight<0) is the reliable direction; emphasis (weight>1) works but is looser."}), + }, + } + + RETURN_TYPES = ("MODEL", "CONDITIONING") + RETURN_NAMES = ("model", "conditioning") + FUNCTION = "encode" + CATEGORY = "KJNodes/experimental" + DESCRIPTION = "Per-token prompt weighting for Krea2 (K2) via attention value scaling. Use (word:-1) to remove a concept, (word:1.5) to emphasize one -- works through the Qwen3-VL encoder where normal weighting doesn't. Outputs the patched model + conditioning; set the sampler CFG to 1.0." + EXPERIMENTAL = True + + def encode(self, clip, model, text, strength): + import re + pattern = re.compile(r"\(([^():]+):(-?\d*\.?\d+)\)") + terms = [(m.group(1).strip(), float(m.group(2))) for m in pattern.finditer(text)] + clean = pattern.sub(lambda m: m.group(1), text) + + tok = clip.tokenize(clean) + key = next(iter(tok)) + ids = [t[0] for t in tok[key][0]] + cond = clip.encode_from_tokens_scheduled(tok) + cond_len = cond[0][0].shape[1] + visible_start = len(ids) - cond_len # conditioning == ids[visible_start:] + start, end = _krea2_user_content_span(ids) + if start is None: + start, end = visible_start, len(ids) + + weight_pairs = [] # (conditioning position, value multiplier) + for phrase, w in terms: + if w > 1.0: + v_factor, k_bias = 1.0, strength * (w - 1.0) * 2.0 # emphasis via attention boost + else: + v_factor, k_bias = 1.0 + strength * (w - 1.0), 0.0 # de-emphasis / removal via value scaling + positions = [] + for variant in (" " + phrase, phrase): # words usually carry a leading-space token in context + sub = _krea2_token_ids(clip, variant) + ps, pe = _krea2_user_content_span(sub) + if ps is None: + continue + sub = sub[ps:pe] + matches = _find_subsequence(ids, sub, start, end) + if matches: + for mi in matches: + positions.extend(mi + off - visible_start for off in range(len(sub))) + break + if not positions: + logging.warning(f"Krea2PromptWeight: phrase '{phrase}' not found in prompt; skipped.") + continue + for cp in positions: + if 0 <= cp < cond_len: + weight_pairs.append((cp, v_factor, k_bias)) + + if not weight_pairs: + return (model, cond) + logging.info(f"Krea2PromptWeight: weighting {weight_pairs}") + + model_clone = model.clone() + diffusion_model = model_clone.get_model_object("diffusion_model") + + transformer_options = model_clone.model_options.get("transformer_options", {}).copy() + transformer_options["krea2_token_weights"] = weight_pairs + model_clone.model_options["transformer_options"] = transformer_options + + for idx, block in enumerate(diffusion_model.blocks): + patched_attn = Krea2WeightPatch().__get__(block.attn, block.attn.__class__) + model_clone.add_object_patch(f"diffusion_model.blocks.{idx}.attn.forward", patched_attn) + + return (model_clone, cond) + +class SkipLayerGuidanceWanVideo: + @classmethod + def INPUT_TYPES(s): + return {"required": {"model": ("MODEL", ), + "blocks": ("STRING", {"default": "10", "multiline": False}), + "start_percent": ("FLOAT", {"default": 0.2, "min": 0.0, "max": 1.0, "step": 0.001}), + "end_percent": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.001}), + }} + RETURN_TYPES = ("MODEL",) + FUNCTION = "slg" + EXPERIMENTAL = True + DESCRIPTION = "Simplified skip layer guidance that only skips the uncond on selected blocks" + DEPRECATED = True + CATEGORY = "advanced/guidance" + + def slg(self, model, start_percent, end_percent, blocks): + def skip(args, extra_args): + transformer_options = extra_args.get("transformer_options", {}) + original_block = extra_args["original_block"] + + if not transformer_options: + raise ValueError("transformer_options not found in extra_args, currently SkipLayerGuidanceWanVideo only works with TeaCacheKJ") + if start_percent <= transformer_options["current_percent"] <= end_percent: + if args["img"].shape[0] == 2: + prev_img_uncond = args["img"][0].unsqueeze(0) + + new_args = { + "img": args["img"][1].unsqueeze(0), + "txt": args["txt"][1].unsqueeze(0), + "vec": args["vec"][1].unsqueeze(0), + "pe": args["pe"][1].unsqueeze(0) + } + + block_out = original_block(new_args) + + out = { + "img": torch.cat([prev_img_uncond, block_out["img"]], dim=0), + "txt": args["txt"], + "vec": args["vec"], + "pe": args["pe"] + } + else: + if transformer_options.get("cond_or_uncond") == [0]: + out = original_block(args) + else: + out = args + else: + out = original_block(args) + return out + + block_list = [int(x.strip()) for x in blocks.split(",")] + blocks = [int(i) for i in block_list] + logging.info(f"Selected blocks to skip uncond on: {blocks}") + + m = model.clone() + + for b in blocks: + #m.set_model_patch_replace(skip, "dit", "double_block", b) + model_options = m.model_options["transformer_options"].copy() + if "patches_replace" not in model_options: + model_options["patches_replace"] = {} + else: + model_options["patches_replace"] = model_options["patches_replace"].copy() + + if "dit" not in model_options["patches_replace"]: + model_options["patches_replace"]["dit"] = {} + else: + model_options["patches_replace"]["dit"] = model_options["patches_replace"]["dit"].copy() + + block = ("double_block", b) + + model_options["patches_replace"]["dit"][block] = skip + m.model_options["transformer_options"] = model_options + + + return (m, ) + +class CFGZeroStarAndInit: + @classmethod + def INPUT_TYPES(s): + return {"required": { + "model": ("MODEL",), + "use_zero_init": ("BOOLEAN", {"default": True}), + "zero_init_steps": ("INT", {"default": 0, "min": 0, "tooltip": "for zero init, starts from 0 so first step is always zeroed out if use_zero_init enabled"}), + }} + RETURN_TYPES = ("MODEL",) + FUNCTION = "patch" + DESCRIPTION = "https://github.com/WeichenFan/CFG-Zero-star" + CATEGORY = "KJNodes/experimental" + EXPERIMENTAL = True + + def patch(self, model, use_zero_init, zero_init_steps): + def cfg_zerostar(args): + #zero init + cond = args["cond"] + timestep = args["timestep"] + sigmas = args["model_options"]["transformer_options"]["sample_sigmas"] + matched_step_index = (sigmas == timestep[0]).nonzero() + if len(matched_step_index) > 0: + current_step_index = matched_step_index.item() + else: + for i in range(len(sigmas) - 1): + if (sigmas[i] - timestep[0]) * (sigmas[i + 1] - timestep[0]) <= 0: + current_step_index = i + break + else: + current_step_index = 0 + + if (current_step_index <= zero_init_steps) and use_zero_init: + return cond * 0 + + uncond = args["uncond"] + cond_scale = args["cond_scale"] + + batch_size = cond.shape[0] + + positive_flat = cond.view(batch_size, -1) + negative_flat = uncond.view(batch_size, -1) + + dot_product = torch.sum(positive_flat * negative_flat, dim=1, keepdim=True) + squared_norm = torch.sum(negative_flat ** 2, dim=1, keepdim=True) + 1e-8 + alpha = dot_product / squared_norm + alpha = alpha.view(batch_size, *([1] * (len(cond.shape) - 1))) + + noise_pred = uncond * alpha + cond_scale * (cond - uncond * alpha) + return noise_pred + + m = model.clone() + m.set_model_sampler_cfg_function(cfg_zerostar) + return (m, ) + +class GGUFLoaderKJ(io.ComfyNode): + @classmethod + def define_schema(cls): + # Get GGUF models safely, fallback to empty list if unet_gguf folder doesn't exist + try: + gguf_models = folder_paths.get_filename_list("unet_gguf") + ltx2_connector_models = folder_paths.get_filename_list("text_encoders") + ltx2_connector_models = [m for m in ltx2_connector_models if "connector" in m.lower()] + except KeyError: + gguf_models = [] + ltx2_connector_models = [] + + return io.Schema( + node_id="GGUFLoaderKJ", + category="KJNodes/model_loaders", + description="Loads a GGUF model with advanced options, requires [ComfyUI-GGUF](https://github.com/city96/ComfyUI-GGUF) to be installed.", + is_experimental=True, + inputs=[ + io.Combo.Input("model_name", options=gguf_models), + io.Combo.Input("extra_model_name", options=gguf_models + ltx2_connector_models + ["none"], default="none", tooltip="An extra gguf model to load and merge into the main model, for example VACE module"), + io.Combo.Input("dequant_dtype", options=["default", "target", "float32", "float16", "bfloat16"], default="default"), + io.Combo.Input("patch_dtype", options=["default", "target", "float32", "float16", "bfloat16"], default="default"), + io.Boolean.Input("patch_on_device", default=False), + io.Boolean.Input("enable_fp16_accumulation", default=False, tooltip="Enable torch.backends.cuda.matmul.allow_fp16_accumulation, required minimum pytorch version 2.7.1"), + io.Combo.Input("attention_override", options=["none", "sdpa", "sageattn", "xformers", "flashattn"], default="none", tooltip="Overrides the used attention implementation, requires the respective library to be installed"), + + ], + outputs=[io.Model.Output(),], + ) + + def attention_override_pytorch(func, *args, **kwargs): + new_attention = comfy.ldm.modules.attention.attention_pytorch + return new_attention.__wrapped__(*args, **kwargs) + def attention_override_sage(func, *args, **kwargs): + new_attention = comfy.ldm.modules.attention.attention_sage + return new_attention.__wrapped__(*args, **kwargs) + def attention_override_xformers(func, *args, **kwargs): + new_attention = comfy.ldm.modules.attention.attention_xformers + return new_attention.__wrapped__(*args, **kwargs) + def attention_override_flash(func, *args, **kwargs): + new_attention = comfy.ldm.modules.attention.attention_flash + return new_attention.__wrapped__(*args, **kwargs) + + ATTENTION_OVERRIDES = { + "sdpa": attention_override_pytorch, + "sageattn": attention_override_sage, + "xformers": attention_override_xformers, + "flashattn": attention_override_flash, + } + + + @classmethod + def _get_gguf_module(cls): + """Import GGUF module with version validation""" + for key, mod in sys.modules.items(): + if key.endswith("ComfyUI-GGUF") or key.endswith("comfyui-gguf"): + if hasattr(mod, "ops") and hasattr(mod, "nodes"): + return mod + + gguf_path = os.path.join(folder_paths.folder_names_and_paths["custom_nodes"][0][0], "ComfyUI-GGUF") + for module_name in ["ComfyUI-GGUF", "custom_nodes.ComfyUI-GGUF", "comfyui-gguf", "custom_nodes.comfyui-gguf", gguf_path, gguf_path.lower()]: + try: + module = importlib.import_module(module_name) + return module + except ImportError: + continue + + raise ImportError( + "Compatible ComfyUI-GGUF not found. " + "Please install/update from: https://github.com/city96/ComfyUI-GGUF" + ) + + @classmethod + def execute(cls, model_name, extra_model_name, dequant_dtype, patch_dtype, patch_on_device, attention_override, enable_fp16_accumulation): + gguf_nodes = cls._get_gguf_module() + ops = gguf_nodes.ops.GGMLOps() + + def set_linear_dtype(attr, value): + if value == "default": + setattr(ops.Linear, attr, None) + elif value == "target": + setattr(ops.Linear, attr, value) + else: + setattr(ops.Linear, attr, getattr(torch, value)) + + set_linear_dtype("dequant_dtype", dequant_dtype) + set_linear_dtype("patch_dtype", patch_dtype) + + # init model + extra = {} + model_path = folder_paths.get_full_path("unet", model_name) + try: + sd, extra = gguf_nodes.loader.gguf_sd_loader(model_path) + except TypeError: + sd = gguf_nodes.loader.gguf_sd_loader(model_path) + + if extra_model_name is not None and extra_model_name != "none": + if extra_model_name.endswith(".gguf"): + extra_model_full_path = folder_paths.get_full_path("unet", extra_model_name) + try: + extra_model, _ = gguf_nodes.loader.gguf_sd_loader(extra_model_full_path) + except TypeError: + extra_model = gguf_nodes.loader.gguf_sd_loader(extra_model_full_path) + elif "connector" in extra_model_name.lower(): + extra_model_full_path = folder_paths.get_full_path("text_encoders", extra_model_name) + extra_model = comfy.utils.load_torch_file(extra_model_full_path) + diffusion_model_prefix = comfy.model_detection.unet_prefix_from_state_dict(extra_model) + if diffusion_model_prefix == "model.diffusion_model.": + temp_sd = comfy.utils.state_dict_prefix_replace(extra_model, {diffusion_model_prefix: ""}, filter_keys=True) + if len(temp_sd) > 0: + extra_model = temp_sd + else: + raise ValueError("Extra model must also be a .gguf file") + sd.update(extra_model) + + model = comfy.sd.load_diffusion_model_state_dict( + sd, model_options={"custom_operations": ops}, metadata=extra.get("metadata", {}) + ) + if model is None: + raise RuntimeError(f"ERROR: Could not detect model type of: {model_path}") + + model = gguf_nodes.nodes.GGUFModelPatcher.clone(model) + model.patch_on_device = patch_on_device + + # attention override + if attention_override in cls.ATTENTION_OVERRIDES: + model.model_options["transformer_options"]["optimized_attention_override"] = cls.ATTENTION_OVERRIDES[attention_override] + + if enable_fp16_accumulation: + if hasattr(torch.backends.cuda.matmul, "allow_fp16_accumulation"): + torch.backends.cuda.matmul.allow_fp16_accumulation = True + else: + raise RuntimeError("Failed to set fp16 accumulation, requires pytorch version 2.7.1 or higher") + else: + if hasattr(torch.backends.cuda.matmul, "allow_fp16_accumulation"): + torch.backends.cuda.matmul.allow_fp16_accumulation = False + + return io.NodeOutput(model,) + +try: + from torch.nn.attention.flex_attention import flex_attention, BlockMask +except ImportError: + flex_attention = None + BlockMask = None + +class NABLA_AttentionKJ(): + @classmethod + def INPUT_TYPES(s): + return {"required": { + "model": ("MODEL",), + "latent": ("LATENT", {"tooltip": "Only used to get the latent shape"}), + "window_time": ("INT", {"default": 11, "min": 1, "tooltip": "Temporal attention window size"}), + "window_width": ("INT", {"default": 3, "min": 1, "tooltip": "Spatial attention window size"}), + "window_height": ("INT", {"default": 3, "min": 1, "tooltip": "Spatial attention window size"}), + "sparsity": ("FLOAT", {"default": 0.9, "min": 0.0, "max": 1.0, "step": 0.01}), + "torch_compile": ("BOOLEAN", {"default": True, "tooltip": "Most likely required for reasonable memory usage"}) + }, + } + + RETURN_TYPES = ("MODEL", ) + FUNCTION = "patch" + DESCRIPTION = "Experimental node for patching attention mode to use NABLA sparse attention for video models, currently only works with Kadinsky5" + CATEGORY = "KJNodes/experimental" + + def patch(self, model, latent, window_time, window_width, window_height, sparsity, torch_compile): + if flex_attention is None or BlockMask is None: + raise RuntimeError("can't import flex_attention from torch.nn.attention, requires newer pytorch version") + + model_clone = model.clone() + samples = latent["samples"] + + sparse_params = get_sparse_params(samples, window_time, window_height, window_width, sparsity) + nabla_attention = NABLA_Attention(sparse_params) + + def attention_override_nabla(func, *args, **kwargs): + return nabla_attention(*args, **kwargs) + + if torch_compile: + attention_override_nabla = torch.compile(attention_override_nabla, mode="max-autotune-no-cudagraphs", dynamic=True) + + # attention override + model_clone.model_options["transformer_options"]["optimized_attention_override"] = attention_override_nabla + + return model_clone, + + +class NABLA_Attention(): + def __init__(self, sparse_params): + self.sparse_params = sparse_params + + def __call__(self, q, k, v, heads, **kwargs): + if q.shape[-2] < 3000 or k.shape[-2] < 3000: + return optimized_attention(q, k, v, heads, **kwargs) + block_mask = self.nablaT_v2(q, k, self.sparse_params["sta_mask"], thr=self.sparse_params["P"]) + out = flex_attention(q, k, v, block_mask=block_mask).transpose(1, 2).contiguous().flatten(-2, -1) + return out + + def nablaT_v2(self, q, k, sta, thr=0.9): + # Map estimation + BLOCK_SIZE = 64 + B, h, S, D = q.shape + s1 = S // BLOCK_SIZE + qa = q.reshape(B, h, s1, BLOCK_SIZE, D).mean(-2) + ka = k.reshape(B, h, s1, BLOCK_SIZE, D).mean(-2).transpose(-2, -1) + map = qa @ ka + + map = torch.softmax(map / math.sqrt(D), dim=-1) + # Map binarization + vals, inds = map.sort(-1) + cvals = vals.cumsum_(-1) + mask = (cvals >= 1 - thr).int() + mask = mask.gather(-1, inds.argsort(-1)) + + mask = torch.logical_or(mask, sta) + + # BlockMask creation + kv_nb = mask.sum(-1).to(torch.int32) + kv_inds = mask.argsort(dim=-1, descending=True).to(torch.int32) + return BlockMask.from_kv_blocks(torch.zeros_like(kv_nb), kv_inds, kv_nb, kv_inds, BLOCK_SIZE=BLOCK_SIZE, mask_mod=None) + +def fast_sta_nabla(T, H, W, wT=3, wH=3, wW=3): + l = torch.Tensor([T, H, W]).amax() + r = torch.arange(0, l, 1, dtype=torch.int16, device=mm.get_torch_device()) + mat = (r.unsqueeze(1) - r.unsqueeze(0)).abs() + sta_t, sta_h, sta_w = ( + mat[:T, :T].flatten(), + mat[:H, :H].flatten(), + mat[:W, :W].flatten(), + ) + sta_t = sta_t <= wT // 2 + sta_h = sta_h <= wH // 2 + sta_w = sta_w <= wW // 2 + sta_hw = (sta_h.unsqueeze(1) * sta_w.unsqueeze(0)).reshape(H, H, W, W).transpose(1, 2).flatten() + sta = (sta_t.unsqueeze(1) * sta_hw.unsqueeze(0)).reshape(T, T, H * W, H * W).transpose(1, 2) + return sta.reshape(T * H * W, T * H * W) + + +def get_sparse_params(x, wT, wH, wW, sparsity=0.9): + B, C, T, H, W = x.shape + #print("x shape:", x.shape) + patch_size = (1, 2, 2) + T, H, W = ( + T // patch_size[0], + H // patch_size[1], + W // patch_size[2], + ) + sta_mask = fast_sta_nabla(T, H // 8, W // 8, wT, wH, wW) + sparse_params = { + "sta_mask": sta_mask.unsqueeze_(0).unsqueeze_(0), + "to_fractal": True, + "P": sparsity, + "wT": wT, + "wH": wH, + "wW": wW, + "add_sta": True, + "visual_shape": (T, H, W), + "method": "topcdf", + } + + return sparse_params + +from comfy.comfy_types.node_typing import IO +class StartRecordCUDAMemoryHistory(): + # @classmethod + # def IS_CHANGED(s): + # return True + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "input": (IO.ANY,), + "enabled": (["all", "state", "None"], {"default": "all", "tooltip": "None: disable, 'state': keep info for allocated memory, 'all': keep history of all alloc/free calls"}), + "context": (["all", "state", "alloc", "None"], {"default": "all", "tooltip": "None: no tracebacks, 'state': tracebacks for allocated memory, 'alloc': for alloc calls, 'all': for free calls"}), + "stacks": (["python", "all"], {"default": "all", "tooltip": "'python': Python/TorchScript/inductor frames, 'all': also C++ frames"}), + "max_entries": ("INT", {"default": 100000, "min": 1000, "max": 10000000, "tooltip": "Maximum number of entries to record"}), + }, + } + + RETURN_TYPES = (IO.ANY, ) + RETURN_NAMES = ("input", "output_path",) + FUNCTION = "start" + CATEGORY = "KJNodes/memory" + DESCRIPTION = "THIS NODE ALWAYS RUNS. Starts recording CUDA memory allocation history, can be ended and saved with EndRecordCUDAMemoryHistory. " + + def start(self, input, enabled, context, stacks, max_entries): + mm.soft_empty_cache() + torch.cuda.reset_peak_memory_stats(mm.get_torch_device()) + torch.cuda.memory._record_memory_history( + max_entries=max_entries, + enabled=enabled if enabled != "None" else None, + context=context if context != "None" else None, + stacks=stacks + ) + return input, + +class EndRecordCUDAMemoryHistory(): + @classmethod + def INPUT_TYPES(s): + return {"required": { + "input": (IO.ANY,), + "output_path": ("STRING", {"default": "comfy_cuda_memory_history"}, "Base path for saving the CUDA memory history file, timestamp and .pt extension will be added"), + }, + } + + RETURN_TYPES = (IO.ANY, "STRING",) + RETURN_NAMES = ("input", "output_path",) + FUNCTION = "end" + CATEGORY = "KJNodes/memory" + DESCRIPTION = "Records CUDA memory allocation history between start and end, saves to a file that can be analyzed here: https://docs.pytorch.org/memory_viz or with VisualizeCUDAMemoryHistory node" + + def end(self, input, output_path): + mm.soft_empty_cache() + time = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") + output_path = f"{output_path}{time}.pt" + torch.cuda.memory._dump_snapshot(output_path) + torch.cuda.memory._record_memory_history(enabled=None) + return input, output_path + +try: + from server import PromptServer +except ImportError: + PromptServer = None + +class VisualizeCUDAMemoryHistory(): + @classmethod + def INPUT_TYPES(s): + return {"required": { + "snapshot_path": ("STRING", ), + }, + "hidden": { + "unique_id": "UNIQUE_ID", + }, + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("output_path",) + FUNCTION = "visualize" + CATEGORY = "KJNodes/memory" + DESCRIPTION = "Visualizes a CUDA memory allocation history file, opens in browser" + OUTPUT_NODE = True + + def visualize(self, snapshot_path, unique_id): + import pickle + from torch.cuda import _memory_viz + import uuid + + from folder_paths import get_output_directory + output_dir = get_output_directory() + + with open(snapshot_path, "rb") as f: + snapshot = pickle.load(f) + + html = _memory_viz.trace_plot(snapshot) + html_filename = f"cuda_memory_history_{uuid.uuid4().hex}.html" + output_path = os.path.join(output_dir, "memory_history", html_filename) + os.makedirs(os.path.dirname(output_path), exist_ok=True) + + with open(output_path, "w", encoding="utf-8") as f: + f.write(html) + + api_url = f"http://localhost:8188/api/view?type=output&filename={html_filename}&subfolder=memory_history" + + # Progress UI + if unique_id and PromptServer is not None: + try: + PromptServer.instance.send_progress_text( + api_url, + unique_id + ) + except: + pass + + return api_url, + + +class ModelMemoryUseReportPatch: + @classmethod + def INPUT_TYPES(s): + return {"required": { + "model": ("MODEL",), + }} + + RETURN_TYPES = ("MODEL",) + FUNCTION = "patch" + DESCRIPTION = "Adds callbacks to model to report memory usage during after sampling" + EXPERIMENTAL = True + CATEGORY = "KJNodes/memory" + + def patch(self, model): + model_clone = model.clone() + device = mm.get_torch_device() + + def reset_mem_usage(model): + torch.cuda.reset_peak_memory_stats(device) + def report_mem_usage(model): + max_memory = torch.cuda.max_memory_allocated(device) / 1024**3 + max_reserved = torch.cuda.max_memory_reserved(device) / 1024**3 + logging.info(f"Sampling max allocated memory: {max_memory=:.3f} GB") + logging.info(f"Sampling max reserved memory: {max_reserved=:.3f} GB") + + model_clone.add_callback(CallbacksMP.ON_PRE_RUN, reset_mem_usage) + model_clone.add_callback(CallbacksMP.ON_CLEANUP, report_mem_usage) + + return (model_clone,) + + +class MemoryUsageFactorAdjustWrapper: + def __init__(self, memory_usage_factor, original_factor): + self.memory_usage_factor = memory_usage_factor + self.original_factor = original_factor + + def __call__(self, executor, model, noise_shape: torch.Tensor, *args, **kwargs): + m = model.clone() + m.model.memory_usage_factor = self.memory_usage_factor + logging.info(f"Temporarily set memory usage factor to {self.memory_usage_factor}") + try: + result = executor(m, noise_shape, *args, **kwargs) + finally: + logging.info(f"Model memory usage calculated, restoring original memory usage factor: {self.original_factor}") + m.model.memory_usage_factor = self.original_factor + return result + +class ModelMemoryUsageFactorOverride: + @classmethod + def INPUT_TYPES(s): + return {"required": { + "model": ("MODEL",), + "memory_usage_factor": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 100.0, "step": 0.001}), + }} + + RETURN_TYPES = ("MODEL",) + FUNCTION = "patch" + DESCRIPTION = "Overrides the memory usage factor of the model during sampling." + EXPERIMENTAL = True + CATEGORY = "KJNodes/memory" + + def patch(self, model, memory_usage_factor): + model_clone = model.clone() + original_memory_usage_factor = model_clone.model.memory_usage_factor + logging.info(f"Original memory usage factor: {original_memory_usage_factor}") + + wrapper = MemoryUsageFactorAdjustWrapper(memory_usage_factor, original_memory_usage_factor) + model_clone.add_wrapper_with_key( + comfy.patcher_extension.WrappersMP.PREPARE_SAMPLING, + "memory_usage_factor_adjust_prepare_sampling", + wrapper + ) + return (model_clone,) + +def wan_ffn_chunked_forward(self, x): + if x.shape[1] > self.dim_threshold: + chunks = torch.chunk(x, self.num_chunks, dim=1) + output_chunks = [] + for chunk in chunks: + output_chunks.append(torch.nn.Sequential.forward(self, chunk)) + chunked = torch.cat(output_chunks, dim=1) + return chunked + else: + return torch.nn.Sequential.forward(self, x) + +class WanffnChunkPatch: + def __init__(self, num_chunks, dim_threshold=4096): + self.num_chunks = num_chunks + self.dim_threshold = dim_threshold + + def __get__(self, obj, objtype=None): + def wrapped_forward(self_module, *args, **kwargs): + self_module.num_chunks = self.num_chunks + self_module.dim_threshold = self.dim_threshold + return wan_ffn_chunked_forward(self_module, *args, **kwargs) + return types.MethodType(wrapped_forward, obj) + +class WanChunkFeedForward(io.ComfyNode): + + @classmethod + def define_schema(cls): + return io.Schema( + node_id="WanChunkFeedForward", + display_name="Wan Chunk FeedForward", + category="KJNodes/wan", + description="EXPERIMENTAL AND MAY CHANGE THE MODEL OUTPUT!! Chunks feedforward activations to reduce peak VRAM usage.", + is_experimental=True, + inputs=[ + io.Model.Input("model"), + io.Int.Input("chunks", default=2, min=1, max=100, step=1, tooltip="Number of chunks to split the feedforward activations into to reduce peak VRAM usage."), + io.Int.Input("dim_threshold", default=4096, min=1024, max=16384, step=256, tooltip="Dimension threshold above which to apply chunking."), + ], + outputs=[ + io.Model.Output(display_name="model"), + ], + ) + + @classmethod + def execute(cls, model, chunks, dim_threshold) -> io.NodeOutput: + if chunks == 1: + return io.NodeOutput(model) + + model_clone = model.clone() + diffusion_model = model_clone.get_model_object("diffusion_model") + + for idx, block in enumerate(diffusion_model.blocks): + patched_ffn = WanffnChunkPatch(chunks, dim_threshold).__get__(block.ffn, block.__class__) + model_clone.add_object_patch(f"diffusion_model.blocks.{idx}.ffn.forward", patched_ffn) + + return io.NodeOutput(model_clone) + + +# Ideogram4 peak-VRAM patches (FFN sequence chunking + bf16 RoPE) +from comfy.ldm.lumina.model import FeedForward as _Ideogram4FeedForward +from comfy.ldm.modules.attention import optimized_attention_masked as _ideogram4_attn + + +def ideogram4_ffn_chunked_forward(self, x): + # x: (B, L, dim). Chunk over the token dim so the (B, L, hidden) SwiGLU + if x.shape[1] > self.kj_dim_threshold and self.kj_num_chunks > 1: + out = [_Ideogram4FeedForward.forward(self, c) for c in torch.chunk(x, self.kj_num_chunks, dim=1)] + return torch.cat(out, dim=1) + return _Ideogram4FeedForward.forward(self, x) + + +class Ideogram4FFNChunkPatch: + def __init__(self, num_chunks, dim_threshold): + self.num_chunks = num_chunks + self.dim_threshold = dim_threshold + + def __get__(self, obj, objtype=None): + def wrapped_forward(self_module, *args, **kwargs): + self_module.kj_num_chunks = self.num_chunks + self_module.kj_dim_threshold = self.dim_threshold + return ideogram4_ffn_chunked_forward(self_module, *args, **kwargs) + return types.MethodType(wrapped_forward, obj) + + +def _ideogram4_apply_rope_lowp(xq, xk, freqs_cis): + # (bf16/fp16) instead of being upcast to fp32 -> ~halves RoPE activation memory. + cos = freqs_cis[0].to(xq.dtype) + sin = freqs_cis[1].to(xq.dtype) + nsin = freqs_cis[2].to(xq.dtype) + + q_embed = xq * cos + qs = q_embed.shape[-1] // 2 + q_embed[..., :qs].addcmul_(xq[..., qs:], nsin) + q_embed[..., qs:].addcmul_(xq[..., :qs], sin) + + k_embed = xk * cos + ks = k_embed.shape[-1] // 2 + k_embed[..., :ks].addcmul_(xk[..., ks:], nsin) + k_embed[..., ks:].addcmul_(xk[..., :ks], sin) + return q_embed, k_embed + + +def ideogram4_attention_lowp_rope_forward(self, x, attn_mask, freqs_cis, transformer_options={}): + batch_size, seq_len, _ = x.shape + q, k, v = self.qkv(x).view(batch_size, seq_len, 3, self.num_heads, self.head_dim).unbind(dim=2) + q = self.norm_q(q).transpose(1, 2) + k = self.norm_k(k).transpose(1, 2) + v = v.transpose(1, 2) + q, k = _ideogram4_apply_rope_lowp(q, k, freqs_cis) + out = _ideogram4_attn(q, k, v, self.num_heads, attn_mask, skip_reshape=True, transformer_options=transformer_options) + return self.o(out) + + +class Ideogram4RopePatch: + def __get__(self, obj, objtype=None): + return types.MethodType(ideogram4_attention_lowp_rope_forward, obj) + + +class Ideogram4OptimizationsKJ(io.ComfyNode): + + @classmethod + def define_schema(cls): + return io.Schema( + node_id="Ideogram4OptimizationsKJ", + display_name="Ideogram4 Optimizations KJ", + category="KJNodes/experimental", + description="EXPERIMENTAL AND MAY CHANGE THE MODEL OUTPUT!! Reduces peak VRAM of the Ideogram4 forward. " + "chunk_ffn splits the SwiGLU activations over the token dim; bf16_rope applies RoPE in the model " + "dtype instead of upcasting to fp32. Both target the two largest transient tensors in the block.", + is_experimental=True, + inputs=[ + io.Model.Input("model"), + io.Boolean.Input("chunk_ffn", default=True, + tooltip="Chunk the feedforward activations over the sequence dim to cap the (B, L, hidden) intermediate."), + io.Int.Input("ffn_chunks", default=2, min=1, max=64, step=1, + tooltip="Number of chunks to split the feedforward sequence into. More chunks = lower peak, slightly more overhead."), + io.Int.Input("ffn_seq_threshold", default=1024, min=256, max=65536, step=256, + tooltip="Only chunk when the token sequence length exceeds this (skips chunking for tiny sequences)."), + io.Boolean.Input("bf16_rope", default=True, + tooltip="Apply RoPE in the input dtype instead of fp32. ~Halves RoPE activation memory; matches the HF reference dtype."), + ], + outputs=[ + io.Model.Output(display_name="model"), + ], + ) + + @classmethod + def execute(cls, model, chunk_ffn, ffn_chunks, ffn_seq_threshold, bf16_rope) -> io.NodeOutput: + if not chunk_ffn and not bf16_rope: + return io.NodeOutput(model) + + m = model.clone() + diffusion_model = m.get_model_object("diffusion_model") + + layers = getattr(diffusion_model, "layers", None) + if not layers or not hasattr(layers[0], "feed_forward") or not hasattr(layers[0], "attention"): + logging.warning("Ideogram4OptimizationsKJ: model does not look like Ideogram4 " + "(expected diffusion_model.layers[*].feed_forward/.attention); returning model unchanged.") + return io.NodeOutput(model) + + for idx, block in enumerate(layers): + if chunk_ffn and ffn_chunks > 1: + patched_ffn = Ideogram4FFNChunkPatch(ffn_chunks, ffn_seq_threshold).__get__(block.feed_forward, block.feed_forward.__class__) + m.add_object_patch(f"diffusion_model.layers.{idx}.feed_forward.forward", patched_ffn) + if bf16_rope: + patched_attn = Ideogram4RopePatch().__get__(block.attention, block.attention.__class__) + m.add_object_patch(f"diffusion_model.layers.{idx}.attention.forward", patched_attn) + + return io.NodeOutput(m) + + +from comfy.samplers import KSAMPLER +from comfy.k_diffusion.sampling import to_d + +def sample_selfrefinevideo(model, x, sigmas, stochastic_step_map, certain_percentage=0.999, uncertainty_threshold=0.25, extra_args=None, callback=None, disable=None, verbose=False, video_shape=None, seed=None): + extra_args = {} if extra_args is None else extra_args + sigma_in = x.new_ones([x.shape[0]]) + + if seed is not None: + generator = torch.Generator(torch.device("cpu")).manual_seed(seed) + + pbar = tqdm(total=len(sigmas) - 1, disable=disable, desc="Sampling") + + for i in range(len(sigmas) - 1): + + # Get stochastic steps for this noise level + current_num_anneal_steps = stochastic_step_map.get(i, 0) + use_stochastic = current_num_anneal_steps > 0 + m = current_num_anneal_steps + 1 if use_stochastic else 1 + + sigma, sigma_next = sigmas[i], sigmas[i + 1] + + prev_certain_mask = None + prev_denoised = None + prev_denoised_full = None + prev_x_next = None + prev_x_next_video = None + is_certain = False + + for ii in range(m): + if m > 1: + pbar.set_description(f"Step {i}/{len(sigmas)-1} (substep {ii+1}/{m})") + # Early exit if certain threshold reached + if is_certain: + x = prev_x_next + break + + # Determine input + noise = torch.randn(x.shape, device=torch.device("cpu"), generator=generator).to(x) + x_in = x if ii == 0 else (1.0 - sigma) * prev_denoised_full + sigma * noise + if ii > 0: + x = x_in + + denoised = model(x_in, sigmas[i] * sigma_in, **extra_args) + + if callback is not None: + callback({'x': x, 'i': i, 'sigma': sigmas[i], 'sigma_hat': sigmas[i], 'denoised': denoised}) + + # Compute next latents + d = to_d(x, sigma, denoised) + x_next = x + (sigma_next - sigma) * d + + # Separate video and audio if joint model + if d.ndim == 3 and video_shape is not None: + cut = math.prod(video_shape[1:]) + denoised_video = denoised[:, :, :cut].reshape([denoised.shape[0]] + list(video_shape)[1:]) + x_next_video = x_next[:, :, :cut].reshape([denoised.shape[0]] + list(video_shape)[1:]) + denoised_audio = denoised[:, :, cut:] + x_next_audio = x_next[:, :, cut:] + if verbose: + tqdm.write(f"Video shape: {denoised_video.shape}, Audio shape: {denoised_audio.shape}") + else: + denoised_video = denoised + x_next_video = x_next + denoised_audio = None + x_next_audio = None + + # Stochastic sampling with uncertainty masking + if use_stochastic and prev_denoised is not None: + # Compute uncertainty and masking on video part + diff = denoised_video - prev_denoised + uncertainty = torch.sqrt(torch.sum(diff ** 2, dim=1)) / denoised_video.shape[1] + certain_mask = uncertainty < uncertainty_threshold + + if verbose: + tqdm.write(f"Step {i}/{len(sigmas)-1} substep {ii+1}/{m}:") + tqdm.write(f"Uncertainty: min {uncertainty.min():.4f}, max {uncertainty.max():.4f}, threshold {uncertainty_threshold}") + tqdm.write(f"Certain pixels: {certain_mask.sum()}/{certain_mask.numel()} = {certain_mask.sum()/certain_mask.numel():.4f}") + + # Update certain mask (union with previous) + if prev_certain_mask is not None: + certain_mask = certain_mask | prev_certain_mask + + # Check certainty threshold + if certain_mask.sum() / certain_mask.numel() > certain_percentage: + is_certain = True + if verbose: + tqdm.write(f"{ii}/{current_num_anneal_steps}: Certain region is more than {certain_percentage}, we are certain") + + # Apply masking to video + certain_mask_float = certain_mask.float().unsqueeze(1) + x_next_video = certain_mask_float * prev_x_next_video + (1.0 - certain_mask_float) * x_next_video + denoised_video = certain_mask_float * prev_denoised + (1.0 - certain_mask_float) * denoised_video + + # Reconstruct full latents by replacing the video portion + if x_next_audio is not None: + # Flatten masked video back to match original format and replace video portion + x_next = x_next.clone() + x_next[:, :, :cut] = x_next_video.reshape([x_next_video.shape[0], x_next.shape[1], -1]) + # Also reconstruct full denoised for next iteration input + denoised_full = denoised.clone() + denoised_full[:, :, :cut] = denoised_video.reshape([denoised_video.shape[0], denoised.shape[1], -1]) + else: + # No audio separation + x_next = x_next_video + denoised_full = denoised_video + + prev_certain_mask = certain_mask + prev_denoised = denoised_video + prev_denoised_full = denoised_full + prev_x_next_video = x_next_video + prev_x_next = x_next + elif use_stochastic: + # For first stochastic step, create denoised_full if we have audio + if x_next_audio is not None: + denoised_full = denoised.clone() + denoised_full[:, :, :cut] = denoised_video.reshape([denoised_video.shape[0], denoised.shape[1], -1]) + else: + denoised_full = denoised_video + + prev_certain_mask = None + prev_denoised = denoised_video + prev_denoised_full = denoised_full + prev_x_next_video = x_next_video + prev_x_next = x_next + + # Update x for final step + if use_stochastic and ii == m - 1: + x = prev_x_next + elif not use_stochastic: + x = x_next + + pbar.update(1) + if m == 1: + pbar.set_description("Sampling") + pbar.close() + return x + +class SamplerSelfRefineVideo(io.ComfyNode): + @classmethod + def define_schema(cls): + default_ranges = [ + (2, 5, 3), # Range 1 + (6, 14, 1), # Range 2 + ] + + options = [] + + # Option 1: 2 ranges + range_inputs_2 = [] + for i in range(1, 3): + start_default, end_default, steps_default = default_ranges[i - 1] + range_inputs_2.extend([ + io.Int.Input(f"start_step{i}", default=start_default, min=0, max=999, step=1, tooltip=f"Start step for range {i}"), + io.Int.Input(f"end_step{i}", default=end_default, min=0, max=999, step=1, tooltip=f"End step for range {i}"), + io.Int.Input(f"steps_{i}", default=steps_default, min=1, max=100, step=1, tooltip=f"Number of P&P steps for range {i}"), + ]) + options.append(io.DynamicCombo.Option(key="2 ranges", inputs=range_inputs_2)) + + # Option 2: 1 range + range_inputs_1 = [] + for i in range(1, 2): + start_default, end_default, steps_default = default_ranges[i - 1] + range_inputs_1.extend([ + io.Int.Input(f"start_step{i}", default=start_default, min=0, max=999, step=1, tooltip=f"Start step for range {i}"), + io.Int.Input(f"end_step{i}", default=end_default, min=0, max=999, step=1, tooltip=f"End step for range {i}"), + io.Int.Input(f"steps_{i}", default=steps_default, min=1, max=100, step=1, tooltip=f"Number of P&P steps for range {i}"), + ]) + options.append(io.DynamicCombo.Option(key="1 range", inputs=range_inputs_1)) + + # Option 3: Manual string input + options.append(io.DynamicCombo.Option( + key="from_string", + inputs=[ + io.String.Input( + "stochastic_plan", + default="2-5:3,6-14:1", + multiline=True, + tooltip="Format: 'start-end:steps,start-end:steps' e.g. '2-5:3,6-14:1'" + ) + ] + )) + return io.Schema( + node_id="SamplerSelfRefineVideo", + category="KJNodes/samplers", + description="Attempt to implement https://github.com/agwmon/self-refine-video, for testing only, MAY NOT WORK AS INTENDED.", + is_experimental=True, + inputs=[ + io.DynamicCombo.Input("input_mode", options=options, tooltip="How to configure the step plan"), + io.Float.Input("certain_percentage", default=0.999, min=0.0, max=1.0, step=0.001, round=False, tooltip="Percentage of certain pixels to consider the frame as certain and skip further refinement"), + io.Float.Input("uncertainty_threshold", default=0.2, min=0.0, max=1.0, step=0.01, round=False, tooltip="Threshold of uncertainty to consider a pixel uncertain"), + io.Boolean.Input("verbose", default=False, tooltip="Enable verbose logging during sampling"), + io.Latent.Input("latent", optional=True, tooltip="Optional latent input to get input shape for LTX2 audio/video separation"), + io.Int.Input("seed", default=0, min=0, max=0xffffffffffffffff, step=1, tooltip="Seed for stochastic sampling"), + ], + outputs=[io.Sampler.Output()] + ) + + @classmethod + def execute(cls, input_mode, certain_percentage, uncertainty_threshold, seed, verbose, latent=None) -> io.NodeOutput: + video_shape = None + if latent is not None: + video_shape = latent["samples"].shape + + range_keys = sorted([k for k in input_mode.keys() if k.startswith('start_step')]) + stochastic_step_map = {} + if "stochastic_plan" in input_mode: + # Parse manual string format: "2-5:3,6-14:1" + plan_str = input_mode["stochastic_plan"] + ranges = plan_str.split(",") + for range_spec in ranges: + range_spec = range_spec.strip() + if not range_spec: + continue + try: + range_part, steps_part = range_spec.split(":") + start, end = range_part.split("-") + start, end, steps = int(start), int(end), int(steps_part) + for idx in range(start, end + 1): + stochastic_step_map[idx] = steps + except ValueError: + raise ValueError(f"Invalid format in stochastic_plan: '{range_spec}'. Expected format: 'start-end:steps'") + else: + range_keys = [k for k in input_mode.keys() if k.startswith('start_step')] + for start_key in range_keys: + i = start_key.replace('start_step', '') + start = input_mode.get(f"start_step{i}") + end = input_mode.get(f"end_step{i}") + steps = input_mode.get(f"steps_{i}") + + if start is not None and end is not None and steps is not None: + for idx in range(start, end + 1): + stochastic_step_map[idx] = steps + + sampler = KSAMPLER(sample_selfrefinevideo, { + "stochastic_step_map": stochastic_step_map, + "certain_percentage": certain_percentage, + "uncertainty_threshold": uncertainty_threshold, + "verbose": verbose, + "video_shape": video_shape, + "seed": seed, + }) + return io.NodeOutput(sampler) + + +# Multi-feature linear bias corrector for PiD (Flux2 backbone, 4-step). +# Calibrated on 124 natural-image samples (LOO-CV per-channel RMSE 0.027/0.026/0.024). +# Held-out validation on 20 unseen images: 60% reduction in total drift vs uncorrected. +# Features per row: [R_mean, G_mean, B_mean, R_std, G_std, B_std, +# R_mean*G_mean, R_mean*B_mean, G_mean*B_mean, intercept(1.0)] +# Columns: predicted bias for R, G, B (subtract from x0_pred at step 0). +PID_BIAS_COEF_FLUX2 = torch.tensor([ + [-0.130306, +0.127184, +0.014058], # R_mean + [-0.053279, -0.408929, +0.004243], # G_mean + [-0.009386, +0.109546, -0.134091], # B_mean + [-0.033373, -0.011615, -0.026129], # R_std + [+0.180052, +0.062021, +0.071317], # G_std + [-0.067958, -0.058595, -0.098645], # B_std + [-0.248116, -0.240633, -0.105600], # R_mean*G_mean + [+0.304035, +0.322566, +0.093224], # R_mean*B_mean + [-0.157648, -0.227127, -0.112368], # G_mean*B_mean + [-0.062814, +0.030765, +0.062735], # intercept +], dtype=torch.float32) + + +class PiDColorBiasCorrection: + @classmethod + def INPUT_TYPES(s): + return {"required": { + "model": ("MODEL",), + "strength": ("FLOAT", {"default": 1.0, "min": -20.0, "max": 20.0, "step": 0.01, + "tooltip": "Correction strength. 1.0 = full predicted bias subtracted. <1 = milder, >1 = stronger, 0 = disabled."}), + "backbone": (["flux2"], {"default": "flux2", + "tooltip": "Calibrated PiD backbone (currently only flux2 — others use the same model but coefficients differ)."}), + }} + RETURN_TYPES = ("MODEL",) + FUNCTION = "patch" + CATEGORY = "KJNodes/experimental" + EXPERIMENTAL = True + DESCRIPTION = ( + "PiD 4-step decoder color/brightness drift corrector. " + "Subtracts a per-channel bias from x0_pred at the first sampling step, " + "using a small linear model calibrated against the model's systematic drift " + "(model tends to brighten dark scenes and add a blue cast)." + ) + + def patch(self, model, strength, backbone): + if strength == 0.0 or backbone != "flux2": + return (model,) + coef_cpu = PID_BIAS_COEF_FLUX2 # (10, 3) + + def pid_bias_post_cfg(args): + denoised = args["denoised"] + # Step detection: only apply at the first sampling step. + # Use sample_sigmas like CFGZeroStarAndInit for robustness across schedules. + try: + sigmas = args["model_options"]["transformer_options"]["sample_sigmas"] + sigma = args.get("sigma", args.get("timestep")) + # First step matches sigmas[0] + if sigma is None or not torch.isclose(sigma.max(), sigmas[0]).item(): + return denoised + except (KeyError, AttributeError): + # Fallback heuristic: PiD's first step has sigma=0.999 + sigma = args.get("sigma") + if sigma is None or sigma.max().item() < 0.95: + return denoised + + coef = coef_cpu.to(denoised.device, dtype=denoised.dtype) + rgb_m = denoised.mean(dim=(0, 2, 3)) + rgb_s = denoised.std(dim=(0, 2, 3)) + one = torch.tensor(1.0, device=denoised.device, dtype=denoised.dtype) + feats = torch.stack([ + rgb_m[0], rgb_m[1], rgb_m[2], + rgb_s[0], rgb_s[1], rgb_s[2], + rgb_m[0] * rgb_m[1], rgb_m[0] * rgb_m[2], rgb_m[1] * rgb_m[2], + one, + ]) + bias = feats @ coef # (3,) + return denoised - strength * bias.view(1, 3, 1, 1) + + m = model.clone() + m.set_model_sampler_post_cfg_function(pid_bias_post_cfg) + return (m,) diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/nodes.py b/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/nodes.py new file mode 100644 index 0000000000000000000000000000000000000000..564616cdbced80d962ef7c3a43f80687dbb5fa91 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/nodes.py @@ -0,0 +1,3405 @@ +import torch +import torch.nn as nn +import numpy as np +from PIL import Image +import json +import re +import os +import time +import math +import importlib +import logging + +from comfy import model_management +import folder_paths +from nodes import MAX_RESOLUTION +from comfy.utils import common_upscale, ProgressBar, load_torch_file, save_torch_file, state_dict_prefix_replace +from comfy.comfy_types.node_typing import IO +from comfy_api.latest import io, ui +import comfy.latent_formats +import node_helpers +from io import BytesIO + +script_directory = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +folder_paths.add_model_folder_path("kjnodes_fonts", os.path.join(script_directory, "fonts")) + +class BOOLConstant: + @classmethod + def INPUT_TYPES(s): + return {"required": { + "value": ("BOOLEAN", {"default": True}), + }, + } + RETURN_TYPES = ("BOOLEAN",) + RETURN_NAMES = ("value",) + FUNCTION = "get_value" + CATEGORY = "KJNodes/constants" + SEARCH_ALIASES = ["boolean", "value"] + + def get_value(self, value): + return (value,) + +class INTConstant: + @classmethod + def INPUT_TYPES(s): + return {"required": { + "value": ("INT", {"default": 0, "min": -0xffffffffffffffff, "max": 0xffffffffffffffff}), + }, + } + RETURN_TYPES = ("INT",) + RETURN_NAMES = ("value",) + FUNCTION = "get_value" + CATEGORY = "KJNodes/constants" + SEARCH_ALIASES = ["integer", "value"] + + def get_value(self, value): + return (value,) + +class FloatConstant: + @classmethod + def INPUT_TYPES(s): + return {"required": { + "value": ("FLOAT", {"default": 0.0, "min": -0xffffffffffffffff, "max": 0xffffffffffffffff, "step": 0.00001}), + }, + } + + RETURN_TYPES = ("FLOAT",) + RETURN_NAMES = ("value",) + FUNCTION = "get_value" + CATEGORY = "KJNodes/constants" + SEARCH_ALIASES = ["float", "value"] + + def get_value(self, value): + return (round(value, 6),) + +class StringConstant: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "string": ("STRING", {"default": '', "multiline": False}), + } + } + RETURN_TYPES = ("STRING",) + FUNCTION = "passtring" + CATEGORY = "KJNodes/constants" + SEARCH_ALIASES = ["text", "value"] + + def passtring(self, string): + return (string, ) + +class StringConstantMultiline: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "string": ("STRING", {"default": "", "multiline": True}), + "strip_newlines": ("BOOLEAN", {"default": True}), + } + } + RETURN_TYPES = ("STRING",) + FUNCTION = "stringify" + CATEGORY = "KJNodes/constants" + SEARCH_ALIASES = ["text", "value"] + + def stringify(self, string, strip_newlines): + new_string = string + if strip_newlines: + new_string = new_string.replace('\n', '').strip() + return (new_string,) + + + +class ScaleBatchPromptSchedule: + + RETURN_TYPES = ("STRING",) + FUNCTION = "scaleschedule" + CATEGORY = "KJNodes/misc" + DESCRIPTION = """ +Scales a batch schedule from Fizz' nodes BatchPromptSchedule +to a different frame count. +""" + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "input_str": ("STRING", {"forceInput": True,"default": "0:(0.0),\n7:(1.0),\n15:(0.0)\n"}), + "old_frame_count": ("INT", {"forceInput": True,"default": 1,"min": 1, "max": 4096, "step": 1}), + "new_frame_count": ("INT", {"forceInput": True,"default": 1,"min": 1, "max": 4096, "step": 1}), + + }, + } + + def scaleschedule(self, old_frame_count, input_str, new_frame_count): + pattern = r'"(\d+)"\s*:\s*"(.*?)"(?:,|\Z)' + frame_strings = dict(re.findall(pattern, input_str)) + + # Calculate the scaling factor + scaling_factor = (new_frame_count - 1) / (old_frame_count - 1) + + # Initialize a dictionary to store the new frame numbers and strings + new_frame_strings = {} + + # Iterate over the frame numbers and strings + for old_frame, string in frame_strings.items(): + # Calculate the new frame number + new_frame = int(round(int(old_frame) * scaling_factor)) + + # Store the new frame number and corresponding string + new_frame_strings[new_frame] = string + + # Format the output string + output_str = ', '.join([f'"{k}":"{v}"' for k, v in sorted(new_frame_strings.items())]) + return (output_str,) + + +class GetLatentsFromBatchIndexed: + + RETURN_TYPES = ("LATENT",) + FUNCTION = "indexedlatentsfrombatch" + CATEGORY = "KJNodes/latents" + DESCRIPTION = """ +Selects and returns the latents at the specified indices as an latent batch. +""" + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "latents": ("LATENT",), + "indexes": ("STRING", {"default": "0, 1, 2", "multiline": True}), + "latent_format": (["BCHW", "BTCHW", "BCTHW"], {"default": "BCHW"}), + }, + } + + def indexedlatentsfrombatch(self, latents, indexes, latent_format): + + samples = latents.copy() + latent_samples = samples["samples"] + + # Parse the indexes string into a list of integers + index_list = [int(index.strip()) for index in indexes.split(',')] + + # Convert list of indices to a PyTorch tensor + indices_tensor = torch.tensor(index_list, dtype=torch.long) + + # Select the latents at the specified indices + if latent_format == "BCHW": + chosen_latents = latent_samples[indices_tensor] + elif latent_format == "BTCHW": + chosen_latents = latent_samples[:, indices_tensor] + elif latent_format == "BCTHW": + chosen_latents = latent_samples[:, :, indices_tensor] + + samples["samples"] = chosen_latents + return (samples,) + + +class ConditioningMultiCombine: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "inputcount": ("INT", {"default": 2, "min": 2, "max": 20, "step": 1}), + "operation": (["combine", "concat"], {"default": "combine"}), + "conditioning_1": ("CONDITIONING", ), + "conditioning_2": ("CONDITIONING", ), + }, + } + + RETURN_TYPES = ("CONDITIONING", "INT") + RETURN_NAMES = ("combined", "inputcount") + FUNCTION = "combine" + CATEGORY = "KJNodes/masking/conditioning" + DESCRIPTION = """ +Combines multiple conditioning nodes into one +""" + + def combine(self, inputcount, operation, **kwargs): + from nodes import ConditioningCombine + from nodes import ConditioningConcat + cond_combine_node = ConditioningCombine() + cond_concat_node = ConditioningConcat() + cond = kwargs["conditioning_1"] + for c in range(1, inputcount): + new_cond = kwargs[f"conditioning_{c + 1}"] + if operation == "combine": + cond = cond_combine_node.combine(cond, new_cond)[0] + elif operation == "concat": + cond = cond_concat_node.concat(cond, new_cond)[0] + return (cond, inputcount,) + +class AppendStringsToList: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "string1": ("STRING", {"default": '', "forceInput": True}), + "string2": ("STRING", {"default": '', "forceInput": True}), + } + } + RETURN_TYPES = ("STRING",) + FUNCTION = "joinstring" + CATEGORY = "KJNodes/text" + + def joinstring(self, string1, string2): + if not isinstance(string1, list): + string1 = [string1] + if not isinstance(string2, list): + string2 = [string2] + + joined_string = string1 + string2 + return (joined_string, ) + +class JoinStrings: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "delimiter": ("STRING", {"default": ' ', "multiline": False}), + }, + "optional": { + "string1": ("STRING", {"default": '', "forceInput": True}), + "string2": ("STRING", {"default": '', "forceInput": True}), + } + } + RETURN_TYPES = ("STRING",) + FUNCTION = "joinstring" + CATEGORY = "KJNodes/text" + + def joinstring(self, delimiter, string1="", string2=""): + joined_string = string1 + delimiter + string2 + return (joined_string, ) + +class JoinStringMulti: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "inputcount": ("INT", {"default": 2, "min": 2, "max": 1000, "step": 1}), + "string_1": ("STRING", {"default": '', "forceInput": True}), + "delimiter": ("STRING", {"default": ' ', "multiline": False}), + "return_list": ("BOOLEAN", {"default": False}), + }, + "optional": { + "string_2": ("STRING", {"default": '', "forceInput": True}), + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("string",) + FUNCTION = "combine" + CATEGORY = "KJNodes/text" + DESCRIPTION = """ +Creates single string, or a list of strings, from +multiple input strings. +You can set how many inputs the node has, +with the **inputcount** and clicking update. +""" + + def combine(self, inputcount, delimiter, **kwargs): + string = kwargs["string_1"] + return_list = kwargs["return_list"] + strings = [string] # Initialize a list with the first string + for c in range(1, inputcount): + new_string = kwargs.get(f"string_{c + 1}", "") + if not new_string: + continue + if return_list: + strings.append(new_string) # Add new string to the list + else: + string = string + delimiter + new_string + if return_list: + return (strings,) # Return the list of strings + else: + return (string,) # Return the combined string + +class CondPassThrough: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + }, + "optional": { + "positive": ("CONDITIONING", ), + "negative": ("CONDITIONING", ), + }, + } + + RETURN_TYPES = ("CONDITIONING", "CONDITIONING",) + RETURN_NAMES = ("positive", "negative") + FUNCTION = "passthrough" + CATEGORY = "KJNodes/misc" + DESCRIPTION = """ + Simply passes through the positive and negative conditioning, + workaround for Set node not allowing bypassed inputs. +""" + + def passthrough(self, positive=None, negative=None): + return (positive, negative,) + +class ModelPassThrough: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + }, + "optional": { + "model": ("MODEL", ), + }, + } + + RETURN_TYPES = ("MODEL", ) + RETURN_NAMES = ("model",) + FUNCTION = "passthrough" + CATEGORY = "KJNodes/misc" + DESCRIPTION = """ + Simply passes through the model, + workaround for Set node not allowing bypassed inputs. +""" + + def passthrough(self, model=None): + return (model,) + +def append_helper(t, mask, c, set_area_to_bounds, strength): + n = [t[0], t[1].copy()] + _, h, w = mask.shape + n[1]['mask'] = mask + n[1]['set_area_to_bounds'] = set_area_to_bounds + n[1]['mask_strength'] = strength + c.append(n) + +class ConditioningSetMaskAndCombine: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "positive_1": ("CONDITIONING", ), + "negative_1": ("CONDITIONING", ), + "positive_2": ("CONDITIONING", ), + "negative_2": ("CONDITIONING", ), + "mask_1": ("MASK", ), + "mask_2": ("MASK", ), + "mask_1_strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}), + "mask_2_strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}), + "set_cond_area": (["default", "mask bounds"],), + } + } + + RETURN_TYPES = ("CONDITIONING","CONDITIONING",) + RETURN_NAMES = ("combined_positive", "combined_negative",) + FUNCTION = "append" + CATEGORY = "KJNodes/masking/conditioning" + DESCRIPTION = """ +Bundles multiple conditioning mask and combine nodes into one,functionality is identical to ComfyUI native nodes +""" + + def append(self, positive_1, negative_1, positive_2, negative_2, mask_1, mask_2, set_cond_area, mask_1_strength, mask_2_strength): + c = [] + c2 = [] + set_area_to_bounds = False + if set_cond_area != "default": + set_area_to_bounds = True + if len(mask_1.shape) < 3: + mask_1 = mask_1.unsqueeze(0) + if len(mask_2.shape) < 3: + mask_2 = mask_2.unsqueeze(0) + for t in positive_1: + append_helper(t, mask_1, c, set_area_to_bounds, mask_1_strength) + for t in positive_2: + append_helper(t, mask_2, c, set_area_to_bounds, mask_2_strength) + for t in negative_1: + append_helper(t, mask_1, c2, set_area_to_bounds, mask_1_strength) + for t in negative_2: + append_helper(t, mask_2, c2, set_area_to_bounds, mask_2_strength) + return (c, c2) + +class ConditioningSetMaskAndCombine3: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "positive_1": ("CONDITIONING", ), + "negative_1": ("CONDITIONING", ), + "positive_2": ("CONDITIONING", ), + "negative_2": ("CONDITIONING", ), + "positive_3": ("CONDITIONING", ), + "negative_3": ("CONDITIONING", ), + "mask_1": ("MASK", ), + "mask_2": ("MASK", ), + "mask_3": ("MASK", ), + "mask_1_strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}), + "mask_2_strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}), + "mask_3_strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}), + "set_cond_area": (["default", "mask bounds"],), + } + } + + RETURN_TYPES = ("CONDITIONING","CONDITIONING",) + RETURN_NAMES = ("combined_positive", "combined_negative",) + FUNCTION = "append" + CATEGORY = "KJNodes/masking/conditioning" + DESCRIPTION = """ +Bundles multiple conditioning mask and combine nodes into one,functionality is identical to ComfyUI native nodes +""" + + def append(self, positive_1, negative_1, positive_2, positive_3, negative_2, negative_3, mask_1, mask_2, mask_3, set_cond_area, mask_1_strength, mask_2_strength, mask_3_strength): + c = [] + c2 = [] + set_area_to_bounds = False + if set_cond_area != "default": + set_area_to_bounds = True + if len(mask_1.shape) < 3: + mask_1 = mask_1.unsqueeze(0) + if len(mask_2.shape) < 3: + mask_2 = mask_2.unsqueeze(0) + if len(mask_3.shape) < 3: + mask_3 = mask_3.unsqueeze(0) + for t in positive_1: + append_helper(t, mask_1, c, set_area_to_bounds, mask_1_strength) + for t in positive_2: + append_helper(t, mask_2, c, set_area_to_bounds, mask_2_strength) + for t in positive_3: + append_helper(t, mask_3, c, set_area_to_bounds, mask_3_strength) + for t in negative_1: + append_helper(t, mask_1, c2, set_area_to_bounds, mask_1_strength) + for t in negative_2: + append_helper(t, mask_2, c2, set_area_to_bounds, mask_2_strength) + for t in negative_3: + append_helper(t, mask_3, c2, set_area_to_bounds, mask_3_strength) + return (c, c2) + +class ConditioningSetMaskAndCombine4: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "positive_1": ("CONDITIONING", ), + "negative_1": ("CONDITIONING", ), + "positive_2": ("CONDITIONING", ), + "negative_2": ("CONDITIONING", ), + "positive_3": ("CONDITIONING", ), + "negative_3": ("CONDITIONING", ), + "positive_4": ("CONDITIONING", ), + "negative_4": ("CONDITIONING", ), + "mask_1": ("MASK", ), + "mask_2": ("MASK", ), + "mask_3": ("MASK", ), + "mask_4": ("MASK", ), + "mask_1_strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}), + "mask_2_strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}), + "mask_3_strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}), + "mask_4_strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}), + "set_cond_area": (["default", "mask bounds"],), + } + } + + RETURN_TYPES = ("CONDITIONING","CONDITIONING",) + RETURN_NAMES = ("combined_positive", "combined_negative",) + FUNCTION = "append" + CATEGORY = "KJNodes/masking/conditioning" + DESCRIPTION = """ +Bundles multiple conditioning mask and combine nodes into one,functionality is identical to ComfyUI native nodes +""" + + def append(self, positive_1, negative_1, positive_2, positive_3, positive_4, negative_2, negative_3, negative_4, mask_1, mask_2, mask_3, mask_4, set_cond_area, mask_1_strength, mask_2_strength, mask_3_strength, mask_4_strength): + c = [] + c2 = [] + set_area_to_bounds = False + if set_cond_area != "default": + set_area_to_bounds = True + if len(mask_1.shape) < 3: + mask_1 = mask_1.unsqueeze(0) + if len(mask_2.shape) < 3: + mask_2 = mask_2.unsqueeze(0) + if len(mask_3.shape) < 3: + mask_3 = mask_3.unsqueeze(0) + if len(mask_4.shape) < 3: + mask_4 = mask_4.unsqueeze(0) + for t in positive_1: + append_helper(t, mask_1, c, set_area_to_bounds, mask_1_strength) + for t in positive_2: + append_helper(t, mask_2, c, set_area_to_bounds, mask_2_strength) + for t in positive_3: + append_helper(t, mask_3, c, set_area_to_bounds, mask_3_strength) + for t in positive_4: + append_helper(t, mask_4, c, set_area_to_bounds, mask_4_strength) + for t in negative_1: + append_helper(t, mask_1, c2, set_area_to_bounds, mask_1_strength) + for t in negative_2: + append_helper(t, mask_2, c2, set_area_to_bounds, mask_2_strength) + for t in negative_3: + append_helper(t, mask_3, c2, set_area_to_bounds, mask_3_strength) + for t in negative_4: + append_helper(t, mask_4, c2, set_area_to_bounds, mask_4_strength) + return (c, c2) + +class ConditioningSetMaskAndCombine5: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "positive_1": ("CONDITIONING", ), + "negative_1": ("CONDITIONING", ), + "positive_2": ("CONDITIONING", ), + "negative_2": ("CONDITIONING", ), + "positive_3": ("CONDITIONING", ), + "negative_3": ("CONDITIONING", ), + "positive_4": ("CONDITIONING", ), + "negative_4": ("CONDITIONING", ), + "positive_5": ("CONDITIONING", ), + "negative_5": ("CONDITIONING", ), + "mask_1": ("MASK", ), + "mask_2": ("MASK", ), + "mask_3": ("MASK", ), + "mask_4": ("MASK", ), + "mask_5": ("MASK", ), + "mask_1_strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}), + "mask_2_strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}), + "mask_3_strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}), + "mask_4_strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}), + "mask_5_strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}), + "set_cond_area": (["default", "mask bounds"],), + } + } + + RETURN_TYPES = ("CONDITIONING","CONDITIONING",) + RETURN_NAMES = ("combined_positive", "combined_negative",) + FUNCTION = "append" + CATEGORY = "KJNodes/masking/conditioning" + DESCRIPTION = """ +Bundles multiple conditioning mask and combine nodes into one,functionality is identical to ComfyUI native nodes +""" + + def append(self, positive_1, negative_1, positive_2, positive_3, positive_4, positive_5, negative_2, negative_3, negative_4, negative_5, mask_1, mask_2, mask_3, mask_4, mask_5, set_cond_area, mask_1_strength, mask_2_strength, mask_3_strength, mask_4_strength, mask_5_strength): + c = [] + c2 = [] + set_area_to_bounds = False + if set_cond_area != "default": + set_area_to_bounds = True + if len(mask_1.shape) < 3: + mask_1 = mask_1.unsqueeze(0) + if len(mask_2.shape) < 3: + mask_2 = mask_2.unsqueeze(0) + if len(mask_3.shape) < 3: + mask_3 = mask_3.unsqueeze(0) + if len(mask_4.shape) < 3: + mask_4 = mask_4.unsqueeze(0) + if len(mask_5.shape) < 3: + mask_5 = mask_5.unsqueeze(0) + for t in positive_1: + append_helper(t, mask_1, c, set_area_to_bounds, mask_1_strength) + for t in positive_2: + append_helper(t, mask_2, c, set_area_to_bounds, mask_2_strength) + for t in positive_3: + append_helper(t, mask_3, c, set_area_to_bounds, mask_3_strength) + for t in positive_4: + append_helper(t, mask_4, c, set_area_to_bounds, mask_4_strength) + for t in positive_5: + append_helper(t, mask_5, c, set_area_to_bounds, mask_5_strength) + for t in negative_1: + append_helper(t, mask_1, c2, set_area_to_bounds, mask_1_strength) + for t in negative_2: + append_helper(t, mask_2, c2, set_area_to_bounds, mask_2_strength) + for t in negative_3: + append_helper(t, mask_3, c2, set_area_to_bounds, mask_3_strength) + for t in negative_4: + append_helper(t, mask_4, c2, set_area_to_bounds, mask_4_strength) + for t in negative_5: + append_helper(t, mask_5, c2, set_area_to_bounds, mask_5_strength) + return (c, c2) + +class VRAM_Debug: + + @classmethod + + def INPUT_TYPES(s): + return { + "required": { + + "empty_cache": ("BOOLEAN", {"default": True}), + "gc_collect": ("BOOLEAN", {"default": True}), + "unload_all_models": ("BOOLEAN", {"default": False}), + }, + "optional": { + "any_input": (IO.ANY,), + "image_pass": ("IMAGE",), + "model_pass": ("MODEL",), + } + } + + RETURN_TYPES = (IO.ANY, "IMAGE","MODEL","INT", "INT",) + RETURN_NAMES = ("any_output", "image_pass", "model_pass", "freemem_before", "freemem_after") + FUNCTION = "VRAMdebug" + CATEGORY = "KJNodes/memory" + DESCRIPTION = """ +Returns the inputs unchanged, they are only used as triggers, +and performs comfy model management functions and garbage collection, +reports free VRAM before and after the operations. +""" + + def VRAMdebug(self, gc_collect, empty_cache, unload_all_models, image_pass=None, model_pass=None, any_input=None): + freemem_before = model_management.get_free_memory() + logging.info(f"VRAMdebug: free memory before: {freemem_before:,.0f}") + if empty_cache: + model_management.soft_empty_cache() + if unload_all_models: + model_management.unload_all_models() + if gc_collect: + import gc + gc.collect() + freemem_after = model_management.get_free_memory() + logging.info(f"VRAMdebug: free memory after: {freemem_after:,.0f}") + logging.info(f"VRAMdebug: freed memory: {freemem_after - freemem_before:,.0f}") + return {"ui": { + "text": [f"{freemem_before:,.0f}x{freemem_after:,.0f}"]}, + "result": (any_input, image_pass, model_pass, freemem_before, freemem_after) + } + +class SomethingToString: + @classmethod + + def INPUT_TYPES(s): + return { + "required": { + "input": (IO.ANY, ), + }, + "optional": { + "prefix": ("STRING", {"default": ""}), + "suffix": ("STRING", {"default": ""}), + } + } + RETURN_TYPES = ("STRING",) + FUNCTION = "stringify" + CATEGORY = "KJNodes/text" + DESCRIPTION = """ +Converts any type to a string. +""" + + def stringify(self, input, prefix="", suffix=""): + if isinstance(input, (int, float, bool, str)): + stringified = str(input) + elif isinstance(input, list): + stringified = ', '.join(str(item) for item in input) + else: + return input, + if prefix: # Check if prefix is not empty + stringified = prefix + stringified # Add the prefix + if suffix: # Check if suffix is not empty + stringified = stringified + suffix # Add the suffix + + return (stringified,) + +class Sleep: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "input": (IO.ANY, ), + "minutes": ("INT", {"default": 0, "min": 0, "max": 1439}), + "seconds": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 59.99, "step": 0.01}), + }, + } + RETURN_TYPES = (IO.ANY,) + FUNCTION = "sleepdelay" + CATEGORY = "KJNodes/misc" + DESCRIPTION = """ +Delays the execution for the input amount of time. +""" + + def sleepdelay(self, input, minutes, seconds): + total_seconds = minutes * 60 + seconds + time.sleep(total_seconds) + return input, + +class EmptyLatentImagePresets: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "dimensions": ( + [ + '512 x 512 (1:1)', + '768 x 512 (1.5:1)', + '960 x 512 (1.875:1)', + '1024 x 512 (2:1)', + '1024 x 576 (1.778:1)', + '1536 x 640 (2.4:1)', + '1344 x 768 (1.75:1)', + '1216 x 832 (1.46:1)', + '1152 x 896 (1.286:1)', + '1024 x 1024 (1:1)', + ], + { + "default": '512 x 512 (1:1)' + }), + + "invert": ("BOOLEAN", {"default": False}), + "batch_size": ("INT", { + "default": 1, + "min": 1, + "max": 4096 + }), + }, + } + + RETURN_TYPES = ("LATENT", "INT", "INT") + RETURN_NAMES = ("Latent", "Width", "Height") + FUNCTION = "generate" + CATEGORY = "KJNodes/latents" + + def generate(self, dimensions, invert, batch_size): + from nodes import EmptyLatentImage + result = [x.strip() for x in dimensions.split('x')] + + # Remove the aspect ratio part + result[0] = result[0].split('(')[0].strip() + result[1] = result[1].split('(')[0].strip() + + if invert: + width = int(result[1].split(' ')[0]) + height = int(result[0]) + else: + width = int(result[0]) + height = int(result[1].split(' ')[0]) + latent = EmptyLatentImage().generate(width, height, batch_size)[0] + + return (latent, int(width), int(height),) + +class EmptyLatentImageCustomPresets: + @classmethod + def INPUT_TYPES(cls): + try: + with open(os.path.join(script_directory, 'custom_dimensions.json')) as f: + dimensions_dict = json.load(f) + except FileNotFoundError: + dimensions_dict = [] + return { + "required": { + "dimensions": ( + [f"{d['label']} - {d['value']}" for d in dimensions_dict], + ), + + "invert": ("BOOLEAN", {"default": False}), + "batch_size": ("INT", { + "default": 1, + "min": 1, + "max": 4096 + }), + }, + } + + RETURN_TYPES = ("LATENT", "INT", "INT") + RETURN_NAMES = ("Latent", "Width", "Height") + FUNCTION = "generate" + CATEGORY = "KJNodes/latents" + DESCRIPTION = """ +Generates an empty latent image with the specified dimensions. +The choices are loaded from 'custom_dimensions.json' in the nodes folder. +""" + + def generate(self, dimensions, invert, batch_size): + from nodes import EmptyLatentImage + # Split the string into label and value + label, value = dimensions.split(' - ') + # Split the value into width and height + width, height = [x.strip() for x in value.split('x')] + + if invert: + width, height = height, width + + latent = EmptyLatentImage().generate(int(width), int(height), batch_size)[0] + + return (latent, int(width), int(height),) + +class WidgetToString: + @classmethod + def IS_CHANGED(cls,*,id,node_title,any_input,**kwargs): + if any_input is not None and (id != 0 or node_title != ""): + return float("NaN") + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "id": ("INT", {"default": 0, "min": 0, "max": 100000, "step": 1}), + "widget_name": ("STRING", {"multiline": False}), + "return_all": ("BOOLEAN", {"default": False}), + }, + "optional": { + "any_input": (IO.ANY, ), + "node_title": ("STRING", {"multiline": False}), + "allowed_float_decimals": ("INT", {"default": 2, "min": 0, "max": 10, "tooltip": "Number of decimal places to display for float values"}), + }, + "hidden": {"extra_pnginfo": "EXTRA_PNGINFO", + "prompt": "PROMPT", + "unique_id": "UNIQUE_ID",}, + } + + RETURN_TYPES = ("STRING", ) + FUNCTION = "get_widget_value" + CATEGORY = "KJNodes/text" + DESCRIPTION = """ +Selects a node and it's specified widget and outputs the value as a string. +If no node id or title is provided it will use the 'any_input' link and use that node. +To see node id's, enable "Node ID Badge Mode" in main settings. +Alternatively you can search with the node title. Node titles ONLY exist if they +are manually edited! +'widget_name' can be a comma separated list. +The 'any_input' is required for making sure the node you want the value from exists in the workflow. +""" + + def get_widget_value(self, id, widget_name, extra_pnginfo, prompt, unique_id, return_all=False, any_input=None, node_title="", allowed_float_decimals=2): + workflow = extra_pnginfo["workflow"] + #print(json.dumps(workflow, indent=4)) + results = [] + node_id = link_id = subgraph_prefix = None + link_to_node_map = {} + node_to_subgraph_map = {} # Track which subgraph each node belongs to + + # Parse unique_id - handle both "parent:id" format and simple int format + if isinstance(unique_id, str) and ":" in unique_id: + unique_id_parts = unique_id.split(":") + unique_id_int = int(unique_id_parts[-1]) # Use the last part as the node id + subgraph_prefix = ":".join(unique_id_parts[:-1]) # Store the parent prefix (e.g., "14") + else: + unique_id_int = int(unique_id) + + # Collect all nodes from main workflow and subgraphs + all_nodes = list(workflow.get("nodes", [])) + definitions = workflow.get("definitions", {}) + subgraphs = definitions.get("subgraphs", []) + + # Find which main workflow node references each subgraph + subgraph_id_to_parent = {} + for node in workflow.get("nodes", []): + node_type = node.get("type", "") + # Subgraph nodes have a UUID as their type + if "-" in node_type and len(node_type) == 36: # UUID format check + subgraph_id_to_parent[node_type] = node["id"] + + for subgraph in subgraphs: + subgraph_id = subgraph.get("id", "") + parent_node_id = subgraph_id_to_parent.get(subgraph_id) + + subgraph_nodes = subgraph.get("nodes", []) + for node in subgraph_nodes: + # Track which subgraph (parent node) this node belongs to + if parent_node_id is not None: + node_to_subgraph_map[node["id"]] = parent_node_id + all_nodes.extend(subgraph_nodes) + + # Also build link_to_node_map from subgraph links + subgraph_links = subgraph.get("links", []) + for link in subgraph_links: + # link format: [link_id, origin_id, origin_slot, target_id, target_slot, type] + if isinstance(link, dict): + link_to_node_map[link["id"]] = link["origin_id"] + elif isinstance(link, list) and len(link) >= 2: + link_to_node_map[link[0]] = link[1] + + for node in all_nodes: + if node_title: + if "title" in node: + if node["title"] == node_title: + node_id = node["id"] + break + else: + logging.warning("Node title not found.") + elif id != 0: + if node["id"] == id: + node_id = id + break + elif any_input is not None: + if node["type"] == "WidgetToString" and node["id"] == unique_id_int and not link_id: + for node_input in node["inputs"]: + if node_input["name"] == "any_input": + link_id = node_input["link"] + + # Construct a map of links to node IDs for future reference + node_outputs = node.get("outputs", None) + if not node_outputs: + continue + for output in node_outputs: + node_links = output.get("links", None) + if not node_links: + continue + for link in node_links: + link_to_node_map[link] = node["id"] + if link_id and link == link_id: + break + + if link_id: + node_id = link_to_node_map.get(link_id, None) + + if node_id is None: + raise ValueError("No matching node found for the given title or id") + + # Determine the correct prompt key + # First check if the target node is in a subgraph + target_subgraph_parent = node_to_subgraph_map.get(node_id) + + if target_subgraph_parent is not None: + # Target node is in a subgraph, use the parent node id as prefix + prompt_key = f"{target_subgraph_parent}:{node_id}" + elif subgraph_prefix is not None: + # We're in a subgraph, use our prefix + prompt_key = f"{subgraph_prefix}:{node_id}" + else: + prompt_key = str(node_id) + + # Try the prefixed key first, then fall back to just the node_id + if prompt_key not in prompt: + prompt_key = str(node_id) + + if prompt_key not in prompt: + raise KeyError(f"Node not found in prompt. Tried keys: '{target_subgraph_parent}:{node_id}' and '{node_id}'") + + values = prompt[prompt_key] + if "inputs" in values: + inputs = values["inputs"] + + # support comma-separated list and trim whitespace + widget_names = [] + if widget_name: + widget_names = [w.strip() for w in widget_name.split(",") if w.strip()] + + if return_all: + # Format items based on type + formatted_items = [] + for k, v in inputs.items(): + if isinstance(v, float): + item = f"{k}: {v:.{allowed_float_decimals}f}" + else: + item = f"{k}: {str(v)}" + formatted_items.append(item) + results.append(", ".join(formatted_items)) + + # Single widget name (trimmed) + elif len(widget_names) == 1: + name = widget_names[0] + if name in inputs: + v = inputs[name] + if isinstance(v, float): + v = f"{v:.{allowed_float_decimals}f}" + else: + v = str(v) + return (v, ) + else: + raise NameError(f"Widget not found: {node_id}.{name}") + + # Multiple widget names: return "name: value" pairs + elif len(widget_names) > 1: + formatted_items = [] + for name in widget_names: + if name not in inputs: + raise NameError(f"Widget not found: {node_id}.{name}") + v = inputs[name] + if isinstance(v, float): + v = f"{v:.{allowed_float_decimals}f}" + else: + v = str(v) + formatted_items.append(f"{name}: {v}") + return (", ".join(formatted_items), ) + + else: + # No valid widget name provided + raise NameError(f"Widget not found: {node_id}.{widget_name}") + + return (", ".join(results).strip(", "), ) + + +class DummyOut: + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "any_input": (IO.ANY, ), + } + } + + RETURN_TYPES = (IO.ANY,) + FUNCTION = "dummy" + CATEGORY = "KJNodes/misc" + OUTPUT_NODE = True + DESCRIPTION = """ +Does nothing, used to trigger generic workflow output. +A way to get previews in the UI without saving anything to disk. +""" + + def dummy(self, any_input): + return (any_input,) + +class FlipSigmasAdjusted: + @classmethod + def INPUT_TYPES(s): + return {"required": + {"sigmas": ("SIGMAS", ), + "divide_by_last_sigma": ("BOOLEAN", {"default": False}), + "divide_by": ("FLOAT", {"default": 1,"min": 1, "max": 255, "step": 0.01}), + "offset_by": ("INT", {"default": 1,"min": -100, "max": 100, "step": 1}), + } + } + RETURN_TYPES = ("SIGMAS", "STRING",) + RETURN_NAMES = ("SIGMAS", "sigmas_string",) + CATEGORY = "KJNodes/noise" + FUNCTION = "get_sigmas_adjusted" + + def get_sigmas_adjusted(self, sigmas, divide_by_last_sigma, divide_by, offset_by): + + sigmas = sigmas.flip(0) + if sigmas[0] == 0: + sigmas[0] = 0.0001 + adjusted_sigmas = sigmas.clone() + #offset sigma + for i in range(1, len(sigmas)): + offset_index = i - offset_by + if 0 <= offset_index < len(sigmas): + adjusted_sigmas[i] = sigmas[offset_index] + else: + adjusted_sigmas[i] = 0.0001 + if adjusted_sigmas[0] == 0: + adjusted_sigmas[0] = 0.0001 + if divide_by_last_sigma: + adjusted_sigmas = adjusted_sigmas / adjusted_sigmas[-1] + + sigma_np_array = adjusted_sigmas.numpy() + array_string = np.array2string(sigma_np_array, precision=2, separator=', ', threshold=np.inf) + adjusted_sigmas = adjusted_sigmas / divide_by + return (adjusted_sigmas, array_string,) + +class CustomSigmas: + @classmethod + def INPUT_TYPES(s): + return {"required": + { + "sigmas_string" :("STRING", {"default": "14.615, 6.475, 3.861, 2.697, 1.886, 1.396, 0.963, 0.652, 0.399, 0.152, 0.029","multiline": True}), + "interpolate_to_steps": ("INT", {"default": 10,"min": 0, "max": 255, "step": 1}), + } + } + RETURN_TYPES = ("SIGMAS",) + RETURN_NAMES = ("SIGMAS",) + CATEGORY = "KJNodes/noise" + FUNCTION = "customsigmas" + DESCRIPTION = """ +Creates a sigmas tensor from a string of comma separated values. +Examples: + +Nvidia's optimized AYS 10 step schedule for SD 1.5: +14.615, 6.475, 3.861, 2.697, 1.886, 1.396, 0.963, 0.652, 0.399, 0.152, 0.029 +SDXL: +14.615, 6.315, 3.771, 2.181, 1.342, 0.862, 0.555, 0.380, 0.234, 0.113, 0.029 +SVD: +700.00, 54.5, 15.886, 7.977, 4.248, 1.789, 0.981, 0.403, 0.173, 0.034, 0.002 +""" + def customsigmas(self, sigmas_string, interpolate_to_steps): + sigmas_list = sigmas_string.split(', ') + sigmas_float_list = [float(sigma) for sigma in sigmas_list] + sigmas_tensor = torch.FloatTensor(sigmas_float_list) + if len(sigmas_tensor) != interpolate_to_steps + 1: + sigmas_tensor = self.loglinear_interp(sigmas_tensor, interpolate_to_steps + 1) + sigmas_tensor[-1] = 0 + return (sigmas_tensor.float(),) + + def loglinear_interp(self, t_steps, num_steps): + """ + Performs log-linear interpolation of a given array of decreasing numbers. + """ + t_steps_np = t_steps.numpy() + + xs = np.linspace(0, 1, len(t_steps_np)) + ys = np.log(t_steps_np[::-1]) + + new_xs = np.linspace(0, 1, num_steps) + new_ys = np.interp(new_xs, xs, ys) + + interped_ys = np.exp(new_ys)[::-1].copy() + interped_ys_tensor = torch.tensor(interped_ys) + return interped_ys_tensor + +class StringToFloatList: + @classmethod + def INPUT_TYPES(s): + return {"required": + { + "string" :("STRING", {"default": "1, 2, 3", "multiline": True}), + } + } + RETURN_TYPES = ("FLOAT",) + RETURN_NAMES = ("FLOAT",) + CATEGORY = "KJNodes/misc" + FUNCTION = "createlist" + + def createlist(self, string): + float_list = [float(x.strip()) for x in string.split(',')] + return (float_list,) + + +class InjectNoiseToLatent: + @classmethod + def INPUT_TYPES(s): + return {"required": { + "latents":("LATENT",), + "strength": ("FLOAT", {"default": 0.1, "min": 0.0, "max": 200.0, "step": 0.0001}), + "noise": ("LATENT",), + "normalize": ("BOOLEAN", {"default": False}), + "average": ("BOOLEAN", {"default": False}), + }, + "optional":{ + "mask": ("MASK", ), + "mix_randn_amount": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1000.0, "step": 0.001}), + "seed": ("INT", {"default": 123,"min": 0, "max": 0xffffffffffffffff, "step": 1}), + } + } + + RETURN_TYPES = ("LATENT",) + FUNCTION = "injectnoise" + CATEGORY = "KJNodes/noise" + + def injectnoise(self, latents, strength, noise, normalize, average, mix_randn_amount=0, seed=None, mask=None): + samples = latents["samples"].clone().cpu() + noise = noise["samples"].clone().cpu() + if samples.shape != samples.shape: + raise ValueError("InjectNoiseToLatent: Latent and noise must have the same shape") + if average: + noised = (samples + noise) / 2 + else: + noised = samples + noise * strength + if normalize: + noised = noised / noised.std() + if mask is not None: + mask = torch.nn.functional.interpolate(mask.reshape((-1, 1, mask.shape[-2], mask.shape[-1])), size=(noised.shape[2], noised.shape[3]), mode="bilinear") + mask = mask.expand((-1,noised.shape[1],-1,-1)) + if mask.shape[0] < noised.shape[0]: + mask = mask.repeat((noised.shape[0] -1) // mask.shape[0] + 1, 1, 1, 1)[:noised.shape[0]] + noised = mask * noised + (1-mask) * samples + if mix_randn_amount > 0: + if seed is not None: + generator = torch.manual_seed(seed) + rand_noise = torch.randn(noised.size(), dtype=noised.dtype, layout=noised.layout, generator=generator, device="cpu") + noised = noised + (mix_randn_amount * rand_noise) + + return ({"samples":noised},) + +class SoundReactive: + @classmethod + def INPUT_TYPES(s): + return {"required": { + "sound_level": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 99999, "step": 0.01}), + "start_range_hz": ("INT", {"default": 150, "min": 0, "max": 9999, "step": 1}), + "end_range_hz": ("INT", {"default": 2000, "min": 0, "max": 9999, "step": 1}), + "multiplier": ("FLOAT", {"default": 1.0, "min": 0.01, "max": 99999, "step": 0.01}), + "smoothing_factor": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}), + "normalize": ("BOOLEAN", {"default": False}), + }, + } + + RETURN_TYPES = ("FLOAT","INT",) + RETURN_NAMES =("sound_level", "sound_level_int",) + FUNCTION = "react" + CATEGORY = "KJNodes/audio" + DESCRIPTION = """ +Reacts to the sound level of the input. +Uses your browsers sound input options and requires. +Meant to be used with realtime diffusion with autoqueue. +""" + + def react(self, sound_level, start_range_hz, end_range_hz, smoothing_factor, multiplier, normalize): + + sound_level *= multiplier + + if normalize: + sound_level /= 255 + + sound_level_int = int(sound_level) + return (sound_level, sound_level_int, ) + +class GenerateNoise: + @classmethod + def INPUT_TYPES(s): + return {"required": { + "width": ("INT", {"default": 512,"min": 16, "max": 4096, "step": 1}), + "height": ("INT", {"default": 512,"min": 16, "max": 4096, "step": 1}), + "batch_size": ("INT", {"default": 1, "min": 1, "max": 4096}), + "seed": ("INT", {"default": 123,"min": 0, "max": 0xffffffffffffffff, "step": 1}), + "multiplier": ("FLOAT", {"default": 1.0,"min": 0.0, "max": 4096, "step": 0.01}), + "constant_batch_noise": ("BOOLEAN", {"default": False}), + "normalize": ("BOOLEAN", {"default": False}), + }, + "optional": { + "model": ("MODEL", ), + "sigmas": ("SIGMAS", ), + "latent_channels": (['4', '16', ],), + "shape": (["BCHW", "BCTHW","BTCHW",],), + } + } + + RETURN_TYPES = ("LATENT",) + FUNCTION = "generatenoise" + CATEGORY = "KJNodes/noise" + DESCRIPTION = """ +Generates noise for injection or to be used as empty latents on samplers with add_noise off. +""" + + def generatenoise(self, batch_size, width, height, seed, multiplier, constant_batch_noise, normalize, sigmas=None, model=None, latent_channels=4, shape="BCHW"): + + generator = torch.manual_seed(seed) + if shape == "BCHW": + noise = torch.randn([batch_size, int(latent_channels), height // 8, width // 8], dtype=torch.float32, layout=torch.strided, generator=generator, device="cpu") + elif shape == "BCTHW": + noise = torch.randn([1, int(latent_channels), batch_size,height // 8, width // 8], dtype=torch.float32, layout=torch.strided, generator=generator, device="cpu") + elif shape == "BTCHW": + noise = torch.randn([1, batch_size, int(latent_channels), height // 8, width // 8], dtype=torch.float32, layout=torch.strided, generator=generator, device="cpu") + if sigmas is not None: + sigma = sigmas[0] - sigmas[-1] + sigma /= model.model.latent_format.scale_factor + noise *= sigma + + noise *=multiplier + + if normalize: + noise = noise / noise.std() + if constant_batch_noise: + noise = noise[0].repeat(batch_size, 1, 1, 1) + + + return ({"samples":noise}, ) + +def camera_embeddings(elevation, azimuth): + elevation = torch.as_tensor([elevation]) + azimuth = torch.as_tensor([azimuth]) + embeddings = torch.stack( + [ + torch.deg2rad( + (90 - elevation) - (90) + ), # Zero123 polar is 90-elevation + torch.sin(torch.deg2rad(azimuth)), + torch.cos(torch.deg2rad(azimuth)), + torch.deg2rad( + 90 - torch.full_like(elevation, 0) + ), + ], dim=-1).unsqueeze(1) + + return embeddings + +def interpolate_angle(start, end, fraction): + # Calculate the difference in angles and adjust for wraparound if necessary + diff = (end - start + 540) % 360 - 180 + # Apply fraction to the difference + interpolated = start + fraction * diff + # Normalize the result to be within the range of -180 to 180 + return (interpolated + 180) % 360 - 180 + + +class StableZero123_BatchSchedule: + @classmethod + def INPUT_TYPES(s): + return {"required": { "clip_vision": ("CLIP_VISION",), + "init_image": ("IMAGE",), + "vae": ("VAE",), + "width": ("INT", {"default": 256, "min": 16, "max": MAX_RESOLUTION, "step": 8}), + "height": ("INT", {"default": 256, "min": 16, "max": MAX_RESOLUTION, "step": 8}), + "batch_size": ("INT", {"default": 1, "min": 1, "max": 4096}), + "interpolation": (["linear", "ease_in", "ease_out", "ease_in_out"],), + "azimuth_points_string": ("STRING", {"default": "0:(0.0),\n7:(1.0),\n15:(0.0)\n", "multiline": True}), + "elevation_points_string": ("STRING", {"default": "0:(0.0),\n7:(0.0),\n15:(0.0)\n", "multiline": True}), + }} + + RETURN_TYPES = ("CONDITIONING", "CONDITIONING", "LATENT") + RETURN_NAMES = ("positive", "negative", "latent") + FUNCTION = "encode" + CATEGORY = "KJNodes/experimental" + + def encode(self, clip_vision, init_image, vae, width, height, batch_size, azimuth_points_string, elevation_points_string, interpolation): + output = clip_vision.encode_image(init_image) + pooled = output.image_embeds.unsqueeze(0) + pixels = common_upscale(init_image.movedim(-1,1), width, height, "bilinear", "center").movedim(1,-1) + encode_pixels = pixels[:,:,:,:3] + t = vae.encode(encode_pixels) + + def ease_in(t): + return t * t + def ease_out(t): + return 1 - (1 - t) * (1 - t) + def ease_in_out(t): + return 3 * t * t - 2 * t * t * t + + # Parse the azimuth input string into a list of tuples + azimuth_points = [] + azimuth_points_string = azimuth_points_string.rstrip(',\n') + for point_str in azimuth_points_string.split(','): + frame_str, azimuth_str = point_str.split(':') + frame = int(frame_str.strip()) + azimuth = float(azimuth_str.strip()[1:-1]) + azimuth_points.append((frame, azimuth)) + # Sort the points by frame number + azimuth_points.sort(key=lambda x: x[0]) + + # Parse the elevation input string into a list of tuples + elevation_points = [] + elevation_points_string = elevation_points_string.rstrip(',\n') + for point_str in elevation_points_string.split(','): + frame_str, elevation_str = point_str.split(':') + frame = int(frame_str.strip()) + elevation_val = float(elevation_str.strip()[1:-1]) + elevation_points.append((frame, elevation_val)) + # Sort the points by frame number + elevation_points.sort(key=lambda x: x[0]) + + # Index of the next point to interpolate towards + next_point = 1 + next_elevation_point = 1 + + positive_cond_out = [] + positive_pooled_out = [] + negative_cond_out = [] + negative_pooled_out = [] + + #azimuth interpolation + for i in range(batch_size): + # Find the interpolated azimuth for the current frame + while next_point < len(azimuth_points) and i >= azimuth_points[next_point][0]: + next_point += 1 + # If next_point is equal to the length of points, we've gone past the last point + if next_point == len(azimuth_points): + next_point -= 1 # Set next_point to the last index of points + prev_point = max(next_point - 1, 0) # Ensure prev_point is not less than 0 + + # Calculate fraction + if azimuth_points[next_point][0] != azimuth_points[prev_point][0]: # Prevent division by zero + fraction = (i - azimuth_points[prev_point][0]) / (azimuth_points[next_point][0] - azimuth_points[prev_point][0]) + if interpolation == "ease_in": + fraction = ease_in(fraction) + elif interpolation == "ease_out": + fraction = ease_out(fraction) + elif interpolation == "ease_in_out": + fraction = ease_in_out(fraction) + + # Use the new interpolate_angle function + interpolated_azimuth = interpolate_angle(azimuth_points[prev_point][1], azimuth_points[next_point][1], fraction) + else: + interpolated_azimuth = azimuth_points[prev_point][1] + # Interpolate the elevation + next_elevation_point = 1 + while next_elevation_point < len(elevation_points) and i >= elevation_points[next_elevation_point][0]: + next_elevation_point += 1 + if next_elevation_point == len(elevation_points): + next_elevation_point -= 1 + prev_elevation_point = max(next_elevation_point - 1, 0) + + if elevation_points[next_elevation_point][0] != elevation_points[prev_elevation_point][0]: + fraction = (i - elevation_points[prev_elevation_point][0]) / (elevation_points[next_elevation_point][0] - elevation_points[prev_elevation_point][0]) + if interpolation == "ease_in": + fraction = ease_in(fraction) + elif interpolation == "ease_out": + fraction = ease_out(fraction) + elif interpolation == "ease_in_out": + fraction = ease_in_out(fraction) + + interpolated_elevation = interpolate_angle(elevation_points[prev_elevation_point][1], elevation_points[next_elevation_point][1], fraction) + else: + interpolated_elevation = elevation_points[prev_elevation_point][1] + + cam_embeds = camera_embeddings(interpolated_elevation, interpolated_azimuth) + cond = torch.cat([pooled, cam_embeds.repeat((pooled.shape[0], 1, 1))], dim=-1) + + positive_pooled_out.append(t) + positive_cond_out.append(cond) + negative_pooled_out.append(torch.zeros_like(t)) + negative_cond_out.append(torch.zeros_like(pooled)) + + # Concatenate the conditions and pooled outputs + final_positive_cond = torch.cat(positive_cond_out, dim=0) + final_positive_pooled = torch.cat(positive_pooled_out, dim=0) + final_negative_cond = torch.cat(negative_cond_out, dim=0) + final_negative_pooled = torch.cat(negative_pooled_out, dim=0) + + # Structure the final output + final_positive = [[final_positive_cond, {"concat_latent_image": final_positive_pooled}]] + final_negative = [[final_negative_cond, {"concat_latent_image": final_negative_pooled}]] + + latent = torch.zeros([batch_size, 4, height // 8, width // 8]) + return (final_positive, final_negative, {"samples": latent}) + +def linear_interpolate(start, end, fraction): + return start + (end - start) * fraction + +class SV3D_BatchSchedule: + @classmethod + def INPUT_TYPES(s): + return {"required": { "clip_vision": ("CLIP_VISION",), + "init_image": ("IMAGE",), + "vae": ("VAE",), + "width": ("INT", {"default": 576, "min": 16, "max": MAX_RESOLUTION, "step": 8}), + "height": ("INT", {"default": 576, "min": 16, "max": MAX_RESOLUTION, "step": 8}), + "batch_size": ("INT", {"default": 21, "min": 1, "max": 4096}), + "interpolation": (["linear", "ease_in", "ease_out", "ease_in_out"],), + "azimuth_points_string": ("STRING", {"default": "0:(0.0),\n9:(180.0),\n20:(360.0)\n", "multiline": True}), + "elevation_points_string": ("STRING", {"default": "0:(0.0),\n9:(0.0),\n20:(0.0)\n", "multiline": True}), + }} + + RETURN_TYPES = ("CONDITIONING", "CONDITIONING", "LATENT") + RETURN_NAMES = ("positive", "negative", "latent") + FUNCTION = "encode" + CATEGORY = "KJNodes/experimental" + DESCRIPTION = """ +Allow scheduling of the azimuth and elevation conditions for SV3D. +Note that SV3D is still a video model and the schedule needs to always go forward +https://huggingface.co/stabilityai/sv3d +""" + + def encode(self, clip_vision, init_image, vae, width, height, batch_size, azimuth_points_string, elevation_points_string, interpolation): + output = clip_vision.encode_image(init_image) + pooled = output.image_embeds.unsqueeze(0) + pixels = common_upscale(init_image.movedim(-1,1), width, height, "bilinear", "center").movedim(1,-1) + encode_pixels = pixels[:,:,:,:3] + t = vae.encode(encode_pixels) + + def ease_in(t): + return t * t + def ease_out(t): + return 1 - (1 - t) * (1 - t) + def ease_in_out(t): + return 3 * t * t - 2 * t * t * t + + # Parse the azimuth input string into a list of tuples + azimuth_points = [] + azimuth_points_string = azimuth_points_string.rstrip(',\n') + for point_str in azimuth_points_string.split(','): + frame_str, azimuth_str = point_str.split(':') + frame = int(frame_str.strip()) + azimuth = float(azimuth_str.strip()[1:-1]) + azimuth_points.append((frame, azimuth)) + # Sort the points by frame number + azimuth_points.sort(key=lambda x: x[0]) + + # Parse the elevation input string into a list of tuples + elevation_points = [] + elevation_points_string = elevation_points_string.rstrip(',\n') + for point_str in elevation_points_string.split(','): + frame_str, elevation_str = point_str.split(':') + frame = int(frame_str.strip()) + elevation_val = float(elevation_str.strip()[1:-1]) + elevation_points.append((frame, elevation_val)) + # Sort the points by frame number + elevation_points.sort(key=lambda x: x[0]) + + # Index of the next point to interpolate towards + next_point = 1 + next_elevation_point = 1 + elevations = [] + azimuths = [] + # For azimuth interpolation + for i in range(batch_size): + # Find the interpolated azimuth for the current frame + while next_point < len(azimuth_points) and i >= azimuth_points[next_point][0]: + next_point += 1 + if next_point == len(azimuth_points): + next_point -= 1 + prev_point = max(next_point - 1, 0) + + if azimuth_points[next_point][0] != azimuth_points[prev_point][0]: + fraction = (i - azimuth_points[prev_point][0]) / (azimuth_points[next_point][0] - azimuth_points[prev_point][0]) + # Apply the ease function to the fraction + if interpolation == "ease_in": + fraction = ease_in(fraction) + elif interpolation == "ease_out": + fraction = ease_out(fraction) + elif interpolation == "ease_in_out": + fraction = ease_in_out(fraction) + + interpolated_azimuth = linear_interpolate(azimuth_points[prev_point][1], azimuth_points[next_point][1], fraction) + else: + interpolated_azimuth = azimuth_points[prev_point][1] + + # Interpolate the elevation + next_elevation_point = 1 + while next_elevation_point < len(elevation_points) and i >= elevation_points[next_elevation_point][0]: + next_elevation_point += 1 + if next_elevation_point == len(elevation_points): + next_elevation_point -= 1 + prev_elevation_point = max(next_elevation_point - 1, 0) + + if elevation_points[next_elevation_point][0] != elevation_points[prev_elevation_point][0]: + fraction = (i - elevation_points[prev_elevation_point][0]) / (elevation_points[next_elevation_point][0] - elevation_points[prev_elevation_point][0]) + # Apply the ease function to the fraction + if interpolation == "ease_in": + fraction = ease_in(fraction) + elif interpolation == "ease_out": + fraction = ease_out(fraction) + elif interpolation == "ease_in_out": + fraction = ease_in_out(fraction) + + interpolated_elevation = linear_interpolate(elevation_points[prev_elevation_point][1], elevation_points[next_elevation_point][1], fraction) + else: + interpolated_elevation = elevation_points[prev_elevation_point][1] + + azimuths.append(interpolated_azimuth) + elevations.append(interpolated_elevation) + + #print("azimuths", azimuths) + #print("elevations", elevations) + + # Structure the final output + final_positive = [[pooled, {"concat_latent_image": t, "elevation": elevations, "azimuth": azimuths}]] + final_negative = [[torch.zeros_like(pooled), {"concat_latent_image": torch.zeros_like(t),"elevation": elevations, "azimuth": azimuths}]] + + latent = torch.zeros([batch_size, 4, height // 8, width // 8]) + return (final_positive, final_negative, {"samples": latent}) + + +class Superprompt: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "instruction_prompt": ("STRING", {"default": 'Expand the following prompt to add more detail', "multiline": True}), + "prompt": ("STRING", {"default": '', "multiline": True, "forceInput": True}), + "max_new_tokens": ("INT", {"default": 128, "min": 1, "max": 4096, "step": 1}), + } + } + + RETURN_TYPES = ("STRING",) + FUNCTION = "process" + CATEGORY = "KJNodes/text" + DESCRIPTION = """ +# SuperPrompt +A T5 model fine-tuned on the SuperPrompt dataset for +upsampling text prompts to more detailed descriptions. +Meant to be used as a pre-generation step for text-to-image +models that benefit from more detailed prompts. +https://huggingface.co/roborovski/superprompt-v1 +""" + + def process(self, instruction_prompt, prompt, max_new_tokens): + device = model_management.get_torch_device() + from transformers import T5Tokenizer, T5ForConditionalGeneration + + checkpoint_path = os.path.join(script_directory, "models","superprompt-v1") + if not os.path.exists(checkpoint_path): + logging.info(f"Downloading model to: {checkpoint_path}") + from huggingface_hub import snapshot_download + snapshot_download(repo_id="roborovski/superprompt-v1", + local_dir=checkpoint_path, + local_dir_use_symlinks=False) + tokenizer = T5Tokenizer.from_pretrained("google/flan-t5-small", legacy=False) + + model = T5ForConditionalGeneration.from_pretrained(checkpoint_path, device_map=device) + model.to(device) + input_text = instruction_prompt + ": " + prompt + + input_ids = tokenizer(input_text, return_tensors="pt").input_ids.to(device) + outputs = model.generate(input_ids, max_new_tokens=max_new_tokens) + out = (tokenizer.decode(outputs[0])) + out = out.replace('', '') + out = out.replace('', '') + + return (out, ) + + +class CameraPoseVisualizer: + + @classmethod + def INPUT_TYPES(s): + return {"required": { + "pose_file_path": ("STRING", {"default": '', "multiline": False}), + "base_xval": ("FLOAT", {"default": 0.2,"min": 0, "max": 100, "step": 0.01}), + "zval": ("FLOAT", {"default": 0.3,"min": 0, "max": 100, "step": 0.01}), + "scale": ("FLOAT", {"default": 1.0,"min": 0.01, "max": 10.0, "step": 0.01}), + "use_exact_fx": ("BOOLEAN", {"default": False}), + "relative_c2w": ("BOOLEAN", {"default": True}), + "use_viewer": ("BOOLEAN", {"default": False}), + }, + "optional": { + "cameractrl_poses": ("CAMERACTRL_POSES", {"default": None}), + } + } + + RETURN_TYPES = ("IMAGE",) + FUNCTION = "plot" + CATEGORY = "KJNodes/misc" + DESCRIPTION = """ +Visualizes the camera poses, from Animatediff-Evolved CameraCtrl Pose +or a .txt file with RealEstate camera intrinsics and coordinates, in a 3D plot. +""" + + def plot(self, pose_file_path, scale, base_xval, zval, use_exact_fx, relative_c2w, use_viewer, cameractrl_poses=None): + import matplotlib as mpl + import matplotlib.pyplot as plt + from torchvision.transforms import ToTensor + + x_min = -2.0 * scale + x_max = 2.0 * scale + y_min = -2.0 * scale + y_max = 2.0 * scale + z_min = -2.0 * scale + z_max = 2.0 * scale + plt.rcParams['text.color'] = '#999999' + self.fig = plt.figure(figsize=(18, 7)) + self.fig.patch.set_facecolor('#353535') + self.ax = self.fig.add_subplot(projection='3d') + self.ax.set_facecolor('#353535') # Set the background color here + self.ax.grid(color='#999999', linestyle='-', linewidth=0.5) + self.plotly_data = None # plotly data traces + self.ax.set_aspect("auto") + self.ax.set_xlim(x_min, x_max) + self.ax.set_ylim(y_min, y_max) + self.ax.set_zlim(z_min, z_max) + self.ax.set_xlabel('x', color='#999999') + self.ax.set_ylabel('y', color='#999999') + self.ax.set_zlabel('z', color='#999999') + for text in self.ax.get_xticklabels() + self.ax.get_yticklabels() + self.ax.get_zticklabels(): + text.set_color('#999999') + logging.info('initialize camera pose visualizer') + + if pose_file_path != "": + with open(pose_file_path, 'r') as f: + poses = f.readlines() + w2cs = [np.asarray([float(p) for p in pose.strip().split(' ')[7:]]).reshape(3, 4) for pose in poses[1:]] + fxs = [float(pose.strip().split(' ')[1]) for pose in poses[1:]] + #print(poses) + elif cameractrl_poses is not None: + poses = cameractrl_poses + w2cs = [np.array(pose[7:]).reshape(3, 4) for pose in cameractrl_poses] + fxs = [pose[1] for pose in cameractrl_poses] + else: + raise ValueError("Please provide either pose_file_path or cameractrl_poses") + + total_frames = len(w2cs) + transform_matrix = np.asarray([[1, 0, 0, 0], [0, 0, 1, 0], [0, -1, 0, 0], [0, 0, 0, 1]]).reshape(4, 4) + last_row = np.zeros((1, 4)) + last_row[0, -1] = 1.0 + + w2cs = [np.concatenate((w2c, last_row), axis=0) for w2c in w2cs] + c2ws = self.get_c2w(w2cs, transform_matrix, relative_c2w) + + for frame_idx, c2w in enumerate(c2ws): + self.extrinsic2pyramid(c2w, frame_idx / total_frames, hw_ratio=1/1, base_xval=base_xval, + zval=(fxs[frame_idx] if use_exact_fx else zval)) + + # Create the colorbar + cmap = mpl.cm.rainbow + norm = mpl.colors.Normalize(vmin=0, vmax=total_frames) + colorbar = self.fig.colorbar(mpl.cm.ScalarMappable(norm=norm, cmap=cmap), ax=self.ax, orientation='vertical') + + # Change the colorbar label + colorbar.set_label('Frame', color='#999999') # Change the label and its color + + # Change the tick colors + colorbar.ax.yaxis.set_tick_params(colors='#999999') # Change the tick color + + # Change the tick frequency + # Assuming you want to set the ticks at every 10th frame + ticks = np.arange(0, total_frames, 10) + colorbar.ax.yaxis.set_ticks(ticks) + + plt.title('') + plt.draw() + buf = BytesIO() + plt.savefig(buf, format='png', bbox_inches='tight', pad_inches=0) + buf.seek(0) + img = Image.open(buf) + tensor_img = ToTensor()(img) + buf.close() + tensor_img = tensor_img.permute(1, 2, 0).unsqueeze(0) + if use_viewer: + time.sleep(1) + plt.show() + return (tensor_img,) + + def extrinsic2pyramid(self, extrinsic, color_map='red', hw_ratio=1/1, base_xval=1, zval=3): + import matplotlib.pyplot as plt + from mpl_toolkits.mplot3d.art3d import Poly3DCollection + vertex_std = np.array([[0, 0, 0, 1], + [base_xval, -base_xval * hw_ratio, zval, 1], + [base_xval, base_xval * hw_ratio, zval, 1], + [-base_xval, base_xval * hw_ratio, zval, 1], + [-base_xval, -base_xval * hw_ratio, zval, 1]]) + vertex_transformed = vertex_std @ extrinsic.T + meshes = [[vertex_transformed[0, :-1], vertex_transformed[1][:-1], vertex_transformed[2, :-1]], + [vertex_transformed[0, :-1], vertex_transformed[2, :-1], vertex_transformed[3, :-1]], + [vertex_transformed[0, :-1], vertex_transformed[3, :-1], vertex_transformed[4, :-1]], + [vertex_transformed[0, :-1], vertex_transformed[4, :-1], vertex_transformed[1, :-1]], + [vertex_transformed[1, :-1], vertex_transformed[2, :-1], vertex_transformed[3, :-1], vertex_transformed[4, :-1]]] + + color = color_map if isinstance(color_map, str) else plt.cm.rainbow(color_map) + + self.ax.add_collection3d( + Poly3DCollection(meshes, facecolors=color, linewidths=0.3, edgecolors=color, alpha=0.25)) + + def customize_legend(self, list_label): + from matplotlib.patches import Patch + import matplotlib.pyplot as plt + list_handle = [] + for idx, label in enumerate(list_label): + color = plt.cm.rainbow(idx / len(list_label)) + patch = Patch(color=color, label=label) + list_handle.append(patch) + plt.legend(loc='right', bbox_to_anchor=(1.8, 0.5), handles=list_handle) + + def get_c2w(self, w2cs, transform_matrix, relative_c2w): + if relative_c2w: + target_cam_c2w = np.array([ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1] + ]) + abs2rel = target_cam_c2w @ w2cs[0] + ret_poses = [target_cam_c2w, ] + [abs2rel @ np.linalg.inv(w2c) for w2c in w2cs[1:]] + else: + ret_poses = [np.linalg.inv(w2c) for w2c in w2cs] + ret_poses = [transform_matrix @ x for x in ret_poses] + return np.array(ret_poses, dtype=np.float32) + + + +class CheckpointPerturbWeights: + + @classmethod + def INPUT_TYPES(s): + return {"required": { + "model": ("MODEL",), + "joint_blocks": ("FLOAT", {"default": 0.02, "min": 0.001, "max": 10.0, "step": 0.001}), + "final_layer": ("FLOAT", {"default": 0.02, "min": 0.001, "max": 10.0, "step": 0.001}), + "rest_of_the_blocks": ("FLOAT", {"default": 0.02, "min": 0.001, "max": 10.0, "step": 0.001}), + "seed": ("INT", {"default": 123,"min": 0, "max": 0xffffffffffffffff, "step": 1}), + } + } + RETURN_TYPES = ("MODEL",) + FUNCTION = "mod" + OUTPUT_NODE = True + + CATEGORY = "KJNodes/experimental" + + def mod(self, seed, model, joint_blocks, final_layer, rest_of_the_blocks): + import copy + torch.manual_seed(seed) + torch.cuda.manual_seed_all(seed) + device = model_management.get_torch_device() + model_copy = copy.deepcopy(model) + model_copy.model.to(device) + keys = model_copy.model.diffusion_model.state_dict().keys() + + dict = {} + for key in keys: + dict[key] = model_copy.model.diffusion_model.state_dict()[key] + + pbar = ProgressBar(len(keys)) + for k in keys: + v = dict[k] + logging.info(f'{k}: {v.std()}') + if k.startswith('joint_blocks'): + multiplier = joint_blocks + elif k.startswith('final_layer'): + multiplier = final_layer + else: + multiplier = rest_of_the_blocks + dict[k] += torch.normal(torch.zeros_like(v) * v.mean(), torch.ones_like(v) * v.std() * multiplier).to(device) + pbar.update(1) + model_copy.model.diffusion_model.load_state_dict(dict) + return model_copy, + +class DifferentialDiffusionAdvanced(): + @classmethod + def INPUT_TYPES(s): + return {"required": { + "model": ("MODEL", ), + "samples": ("LATENT",), + "mask": ("MASK",), + "multiplier": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.001}), + }} + RETURN_TYPES = ("MODEL", "LATENT") + FUNCTION = "apply" + CATEGORY = "_for_testing" + INIT = False + + def apply(self, model, samples, mask, multiplier): + self.multiplier = multiplier + model = model.clone() + model.set_model_denoise_mask_function(self.forward) + s = samples.copy() + s["noise_mask"] = mask.reshape((-1, 1, mask.shape[-2], mask.shape[-1])) + return (model, s) + + def forward(self, sigma: torch.Tensor, denoise_mask: torch.Tensor, extra_options: dict): + model = extra_options["model"] + step_sigmas = extra_options["sigmas"] + sigma_to = model.inner_model.model_sampling.sigma_min + if step_sigmas[-1] > sigma_to: + sigma_to = step_sigmas[-1] + sigma_from = step_sigmas[0] + + ts_from = model.inner_model.model_sampling.timestep(sigma_from) + ts_to = model.inner_model.model_sampling.timestep(sigma_to) + current_ts = model.inner_model.model_sampling.timestep(sigma[0]) + + threshold = (current_ts - ts_to) / (ts_from - ts_to) / self.multiplier + + return (denoise_mask >= threshold).to(denoise_mask.dtype) + +class FluxBlockLoraSelect: + def __init__(self): + self.loaded_lora = None + + @classmethod + def INPUT_TYPES(s): + arg_dict = {} + argument = ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1000.0, "step": 0.01}) + + for i in range(19): + arg_dict["double_blocks.{}.".format(i)] = argument + + for i in range(38): + arg_dict["single_blocks.{}.".format(i)] = argument + + return {"required": arg_dict} + + RETURN_TYPES = ("SELECTEDDITBLOCKS", ) + RETURN_NAMES = ("blocks", ) + OUTPUT_TOOLTIPS = ("The modified diffusion model.",) + FUNCTION = "load_lora" + + CATEGORY = "KJNodes/experimental" + DESCRIPTION = "Select individual block alpha values, value of 0 removes the block altogether" + + def load_lora(self, **kwargs): + return (kwargs,) + +class HunyuanVideoBlockLoraSelect: + @classmethod + def INPUT_TYPES(s): + arg_dict = {} + argument = ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1000.0, "step": 0.01}) + + for i in range(20): + arg_dict["double_blocks.{}.".format(i)] = argument + + for i in range(40): + arg_dict["single_blocks.{}.".format(i)] = argument + + return {"required": arg_dict} + + RETURN_TYPES = ("SELECTEDDITBLOCKS", ) + RETURN_NAMES = ("blocks", ) + OUTPUT_TOOLTIPS = ("The modified diffusion model.",) + FUNCTION = "load_lora" + + CATEGORY = "KJNodes/hunyuanvideo" + DESCRIPTION = "Select individual block alpha values, value of 0 removes the block altogether" + + def load_lora(self, **kwargs): + return (kwargs,) + +class Wan21BlockLoraSelect: + @classmethod + def INPUT_TYPES(s): + arg_dict = {} + argument = ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1000.0, "step": 0.01}) + + for i in range(40): + arg_dict["blocks.{}.".format(i)] = argument + + return {"required": arg_dict} + + RETURN_TYPES = ("SELECTEDDITBLOCKS", ) + RETURN_NAMES = ("blocks", ) + OUTPUT_TOOLTIPS = ("The modified diffusion model.",) + FUNCTION = "load_lora" + + CATEGORY = "KJNodes/wan" + DESCRIPTION = "Select individual block alpha values, value of 0 removes the block altogether" + + def load_lora(self, **kwargs): + return (kwargs,) + +class LTX2BlockLoraSelect: + @classmethod + def INPUT_TYPES(s): + arg_dict = {} + argument = ("FLOAT", {"default": 0.0, "min": 0.0, "max": 10000.0, "step": 0.01}) + + for i in range(48): + arg_dict["blocks.{}.".format(i)] = argument + + return {"required": arg_dict} + + RETURN_TYPES = ("SELECTEDDITBLOCKS", ) + RETURN_NAMES = ("blocks", ) + OUTPUT_TOOLTIPS = ("The modified diffusion model.",) + FUNCTION = "load_lora" + + CATEGORY = "KJNodes/ltxv" + DESCRIPTION = "Select individual block alpha values, value of 0 removes the block altogether" + + def load_lora(self, **kwargs): + return (kwargs,) + + +class DiTBlockLoraLoader: + def __init__(self): + self.loaded_lora = None + + @classmethod + def INPUT_TYPES(s): + return {"required": { + "model": ("MODEL", {"tooltip": "The diffusion model the LoRA will be applied to."}), + "strength_model": ("FLOAT", {"default": 1.0, "min": -100.0, "max": 100.0, "step": 0.01, "tooltip": "How strongly to modify the diffusion model. This value can be negative."}), + + }, + "optional": { + "lora_name": (folder_paths.get_filename_list("loras"), {"tooltip": "The name of the LoRA."}), + "opt_lora_path": ("STRING", {"forceInput": True, "tooltip": "Absolute path of the LoRA."}), + "blocks": ("SELECTEDDITBLOCKS",), + } + } + + RETURN_TYPES = ("MODEL", "STRING", ) + RETURN_NAMES = ("model", "rank", ) + OUTPUT_TOOLTIPS = ("The modified diffusion model.", "possible rank of the LoRA.") + FUNCTION = "load_lora" + CATEGORY = "KJNodes/lora" + + def load_lora(self, model, strength_model, lora_name=None, opt_lora_path=None, blocks=None): + + import comfy.lora + + if opt_lora_path: + lora_path = opt_lora_path + else: + lora_path = folder_paths.get_full_path("loras", lora_name) + + lora = None + if self.loaded_lora is not None: + if self.loaded_lora[0] == lora_path: + lora = self.loaded_lora[1] + else: + self.loaded_lora = None + + if lora is None: + lora = load_torch_file(lora_path, safe_load=True) + self.loaded_lora = (lora_path, lora) + + # Find the first key that ends with "weight" + rank = "unknown" + weight_key = next((key for key in lora.keys() if key.endswith('weight')), None) + # Print the shape of the value corresponding to the key + if weight_key: + logging.info(f"Shape of the first 'weight' key ({weight_key}): {lora[weight_key].shape}") + rank = str(lora[weight_key].shape[0]) + else: + logging.warning("No key ending with 'weight' found.") + rank = "Couldn't find rank" + self.loaded_lora = (lora_path, lora) + + key_map = {} + if model is not None: + key_map = comfy.lora.model_lora_keys_unet(model.model, key_map) + + loaded = comfy.lora.load_lora(lora, key_map) + + if blocks is not None: + keys_to_delete = [] + + for block in blocks: + for key in list(loaded.keys()): + match = False + if isinstance(key, str) and block in key: + match = True + elif isinstance(key, tuple): + for k in key: + if block in k: + match = True + break + + if match: + ratio = blocks[block] + if ratio == 0: + keys_to_delete.append(key) + else: + # Only modify LoRA adapters, skip diff tuples + value = loaded[key] + if hasattr(value, 'weights'): + logging.info(f"Modifying LoRA adapter for key: {key}") + weights_list = list(value.weights) + weights_list[2] = ratio + loaded[key].weights = tuple(weights_list) + else: + logging.info(f"Skipping non-LoRA entry for key: {key}") + + for key in keys_to_delete: + del loaded[key] + + logging.info("loading lora keys:") + for key, value in loaded.items(): + if hasattr(value, 'weights'): + logging.info(f"Key: {key}, Alpha: {value.weights[2]}") + else: + logging.info(f"Key: {key}, Type: {type(value)}") + + if model is not None: + new_modelpatcher = model.clone() + k = new_modelpatcher.add_patches(loaded, strength_model) + + k = set(k) + for x in loaded: + if (x not in k): + logging.warning(f"NOT LOADED {x}") + + return (new_modelpatcher, rank) + +class CustomControlNetWeightsFluxFromList: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "list_of_floats": ("FLOAT", {"forceInput": True}, ), + }, + "optional": { + "uncond_multiplier": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}, ), + "cn_extras": ("CN_WEIGHTS_EXTRAS",), + "autosize": ("ACNAUTOSIZE", {"padding": 0}), + } + } + + RETURN_TYPES = ("CONTROL_NET_WEIGHTS", "TIMESTEP_KEYFRAME",) + RETURN_NAMES = ("CN_WEIGHTS", "TK_SHORTCUT") + FUNCTION = "load_weights" + DESCRIPTION = "Creates controlnet weights from a list of floats for Advanced-ControlNet" + + CATEGORY = "KJNodes/controlnet" + + def load_weights(self, list_of_floats: list[float], + uncond_multiplier: float=1.0, cn_extras: dict[str]={}): + + adv_control = importlib.import_module("ComfyUI-Advanced-ControlNet.adv_control") + ControlWeights = adv_control.utils.ControlWeights + TimestepKeyframeGroup = adv_control.utils.TimestepKeyframeGroup + TimestepKeyframe = adv_control.utils.TimestepKeyframe + + weights = ControlWeights.controlnet(weights_input=list_of_floats, uncond_multiplier=uncond_multiplier, extras=cn_extras) + logging.info(weights.weights_input) + return (weights, TimestepKeyframeGroup.default(TimestepKeyframe(control_weights=weights))) + +SHAKKERLABS_UNION_CONTROLNET_TYPES = { + "canny": 0, + "tile": 1, + "depth": 2, + "blur": 3, + "pose": 4, + "gray": 5, + "low quality": 6, +} + +class SetShakkerLabsUnionControlNetType: + @classmethod + def INPUT_TYPES(s): + return {"required": {"control_net": ("CONTROL_NET", ), + "type": (["auto"] + list(SHAKKERLABS_UNION_CONTROLNET_TYPES.keys()),) + }} + + CATEGORY = "conditioning/controlnet" + RETURN_TYPES = ("CONTROL_NET",) + + FUNCTION = "set_controlnet_type" + + def set_controlnet_type(self, control_net, type): + control_net = control_net.copy() + type_number = SHAKKERLABS_UNION_CONTROLNET_TYPES.get(type, -1) + if type_number >= 0: + control_net.set_extra_arg("control_type", [type_number]) + else: + control_net.set_extra_arg("control_type", []) + + return (control_net,) + +class ModelSaveKJ: + def __init__(self): + self.output_dir = folder_paths.get_output_directory() + + @classmethod + def INPUT_TYPES(s): + return {"required": { "model": ("MODEL",), + "filename_prefix": ("STRING", {"default": "diffusion_models/ComfyUI"}), + "model_key_prefix": ("STRING", {"default": "model.diffusion_model."}), + }, + "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},} + RETURN_TYPES = () + FUNCTION = "save" + OUTPUT_NODE = True + + CATEGORY = "advanced/model_merging" + + def save(self, model, filename_prefix, model_key_prefix, prompt=None, extra_pnginfo=None): + full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(filename_prefix, self.output_dir) + + output_checkpoint = f"{filename}_{counter:05}_.safetensors" + output_checkpoint = os.path.join(full_output_folder, output_checkpoint) + + load_models = [model] + + model_management.load_models_gpu(load_models) + default_prefix = "model.diffusion_model." + + sd = model.state_dict_for_saving(None, None, None) + + new_sd = {} + for k in sd: + if k.startswith(default_prefix): + new_key = model_key_prefix + k[len(default_prefix):] + else: + new_key = k # In case the key doesn't start with the default prefix, keep it unchanged + t = sd[k] + if not t.is_contiguous(): + t = t.contiguous() + new_sd[new_key] = t + logging.info(f"full_output_folder: {full_output_folder}") + if not os.path.exists(full_output_folder): + os.makedirs(full_output_folder) + save_torch_file(new_sd, os.path.join(full_output_folder, output_checkpoint)) + return {} + +class StyleModelApplyAdvanced: + @classmethod + def INPUT_TYPES(s): + return {"required": {"conditioning": ("CONDITIONING", ), + "style_model": ("STYLE_MODEL", ), + "clip_vision_output": ("CLIP_VISION_OUTPUT", ), + "strength": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.001}), + }} + RETURN_TYPES = ("CONDITIONING",) + FUNCTION = "apply_stylemodel" + CATEGORY = "KJNodes/experimental" + DESCRIPTION = "StyleModelApply but with strength parameter" + + def apply_stylemodel(self, clip_vision_output, style_model, conditioning, strength=1.0): + cond = style_model.get_cond(clip_vision_output).flatten(start_dim=0, end_dim=1).unsqueeze(dim=0) + cond = strength * cond + c = [] + for t in conditioning: + n = [torch.cat((t[0], cond), dim=1), t[1].copy()] + c.append(n) + return (c, ) + +class AudioConcatenate: + @classmethod + def INPUT_TYPES(s): + return {"required": { + "audio1": ("AUDIO",), + "audio2": ("AUDIO",), + "direction": ( + [ 'right', + 'left', + ], + { + "default": 'right' + }), + }} + + RETURN_TYPES = ("AUDIO",) + FUNCTION = "concanate" + CATEGORY = "KJNodes/audio" + DESCRIPTION = """ +Concatenates the audio1 to audio2 in the specified direction. +""" + + def concanate(self, audio1, audio2, direction): + sample_rate_1 = audio1["sample_rate"] + sample_rate_2 = audio2["sample_rate"] + if sample_rate_1 != sample_rate_2: + raise ValueError("Sample rates of the two audios do not match") + + waveform_1 = audio1["waveform"] + waveform_2 = audio2["waveform"] + + # Concatenate based on the specified direction + if direction == 'right': + concatenated_audio = torch.cat((waveform_1, waveform_2), dim=2) # Concatenate along width + elif direction == 'left': + concatenated_audio= torch.cat((waveform_2, waveform_1), dim=2) # Concatenate along width + return ({"waveform": concatenated_audio, "sample_rate": sample_rate_1},) + +class LeapfusionHunyuanI2V: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "model": ("MODEL",), + "latent": ("LATENT",), + "index": ("INT", {"default": 0, "min": -1, "max": 1000, "step": 1,"tooltip": "The index of the latent to be replaced. 0 for first frame and -1 for last"}), + "start_percent": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "The start percentage of steps to apply"}), + "end_percent": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "The end percentage of steps to apply"}), + "strength": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.001}), + } + } + + RETURN_TYPES = ("MODEL",) + FUNCTION = "patch" + + CATEGORY = "KJNodes/hunyuanvideo" + + def patch(self, model, latent, index, strength, start_percent, end_percent): + + def outer_wrapper(samples, index, start_percent, end_percent): + def unet_wrapper(apply_model, args): + steps = args["c"]["transformer_options"]["sample_sigmas"] + inp, timestep, c = args["input"], args["timestep"], args["c"] + matched_step_index = (steps == timestep).nonzero() + if len(matched_step_index) > 0: + current_step_index = matched_step_index.item() + else: + for i in range(len(steps) - 1): + # walk from beginning of steps until crossing the timestep + if (steps[i] - timestep[0]) * (steps[i + 1] - timestep[0]) <= 0: + current_step_index = i + break + else: + current_step_index = 0 + current_percent = current_step_index / (len(steps) - 1) + if samples is not None: + if start_percent <= current_percent <= end_percent: + inp[:, :, [index], :, :] = samples[:, :, [0], :, :].to(inp) + else: + inp[:, :, [index], :, :] = torch.zeros(1) + return apply_model(inp, timestep, **c) + return unet_wrapper + + samples = latent["samples"] * 0.476986 * strength + m = model.clone() + m.set_model_unet_function_wrapper(outer_wrapper(samples, index, start_percent, end_percent)) + + return (m,) + +class ImageNoiseAugmentation: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "image": ("IMAGE",), + "noise_aug_strength": ("FLOAT", {"default": None, "min": 0.0, "max": 100.0, "step": 0.001}), + "seed": ("INT", {"default": 123,"min": 0, "max": 0xffffffffffffffff, "step": 1}), + } + } + + RETURN_TYPES = ("IMAGE",) + FUNCTION = "add_noise" + CATEGORY = "KJNodes/image" + DESCRIPTION = """ + Add noise to an image. + """ + + def add_noise(self, image, noise_aug_strength, seed): + torch.manual_seed(seed) + sigma = torch.ones((image.shape[0],)).to(image.device, image.dtype) * noise_aug_strength + image_noise = torch.randn_like(image) * sigma[:, None, None, None] + image_noise = torch.where(image==-1, torch.zeros_like(image), image_noise) + image_out = image + image_noise + return image_out, + +class VAELoaderKJ: + video_taes = ["taehv", "lighttaew2_2", "lighttaew2_1", "lighttaehy1_5"] + image_taes = ["taesd", "taesdxl", "taesd3", "taef1"] + @staticmethod + def vae_list(s): + vaes = folder_paths.get_filename_list("vae") + approx_vaes = folder_paths.get_filename_list("vae_approx") + sdxl_taesd_enc = False + sdxl_taesd_dec = False + sd1_taesd_enc = False + sd1_taesd_dec = False + sd3_taesd_enc = False + sd3_taesd_dec = False + f1_taesd_enc = False + f1_taesd_dec = False + + for v in approx_vaes: + if v.startswith("taesd_decoder."): + sd1_taesd_dec = True + elif v.startswith("taesd_encoder."): + sd1_taesd_enc = True + elif v.startswith("taesdxl_decoder."): + sdxl_taesd_dec = True + elif v.startswith("taesdxl_encoder."): + sdxl_taesd_enc = True + elif v.startswith("taesd3_decoder."): + sd3_taesd_dec = True + elif v.startswith("taesd3_encoder."): + sd3_taesd_enc = True + elif v.startswith("taef1_encoder."): + f1_taesd_dec = True + elif v.startswith("taef1_decoder."): + f1_taesd_enc = True + else: + for tae in s.video_taes: + if v.startswith(tae): + vaes.append(v) + + if sd1_taesd_dec and sd1_taesd_enc: + vaes.append("taesd") + if sdxl_taesd_dec and sdxl_taesd_enc: + vaes.append("taesdxl") + if sd3_taesd_dec and sd3_taesd_enc: + vaes.append("taesd3") + if f1_taesd_dec and f1_taesd_enc: + vaes.append("taef1") + vaes.append("pixel_space") + return vaes + + @staticmethod + def load_taesd(name): + sd = {} + approx_vaes = folder_paths.get_filename_list("vae_approx") + + encoder = next(filter(lambda a: a.startswith("{}_encoder.".format(name)), approx_vaes)) + decoder = next(filter(lambda a: a.startswith("{}_decoder.".format(name)), approx_vaes)) + + enc = load_torch_file(folder_paths.get_full_path_or_raise("vae_approx", encoder)) + for k in enc: + sd["taesd_encoder.{}".format(k)] = enc[k] + + dec = load_torch_file(folder_paths.get_full_path_or_raise("vae_approx", decoder)) + for k in dec: + sd["taesd_decoder.{}".format(k)] = dec[k] + + if name == "taesd": + sd["vae_scale"] = torch.tensor(0.18215) + sd["vae_shift"] = torch.tensor(0.0) + elif name == "taesdxl": + sd["vae_scale"] = torch.tensor(0.13025) + sd["vae_shift"] = torch.tensor(0.0) + elif name == "taesd3": + sd["vae_scale"] = torch.tensor(1.5305) + sd["vae_shift"] = torch.tensor(0.0609) + elif name == "taef1": + sd["vae_scale"] = torch.tensor(0.3611) + sd["vae_shift"] = torch.tensor(0.1159) + return sd + + @classmethod + def INPUT_TYPES(s): + return { + "required": { "vae_name": (s.vae_list(s), ), + "device": (["main_device", "cpu"],), + "weight_dtype": (["bf16", "fp16", "fp32" ],), + } + } + + RETURN_TYPES = ("VAE",) + FUNCTION = "load_vae" + CATEGORY = "KJNodes/vae" + + def load_vae(self, vae_name, device, weight_dtype): + from comfy.sd import VAE + metadata = None + dtype = {"bf16": torch.bfloat16, "fp16": torch.float16, "fp32": torch.float32}[weight_dtype] + if device == "main_device": + device = model_management.get_torch_device() + elif device == "cpu": + device = torch.device("cpu") + + if vae_name == "pixel_space": + sd = {} + sd["pixel_space_vae"] = torch.tensor(1.0) + elif vae_name in self.image_taes: + sd = self.load_taesd(vae_name) + else: + if os.path.splitext(vae_name)[0] in self.video_taes: + vae_path = folder_paths.get_full_path_or_raise("vae_approx", vae_name) + else: + vae_path = folder_paths.get_full_path_or_raise("vae", vae_name) + sd, metadata = load_torch_file(vae_path, return_metadata=True) + + + is_audio_vae = ( + "vocoder.conv_post.weight" in sd + or "vocoder.vocoder.conv_post.weight" in sd + or "vocoder.resblocks.0.convs1.0.weight" in sd + or "vocoder.vocoder.resblocks.0.convs1.0.weight" in sd + ) + if is_audio_vae: + sd_audio = state_dict_prefix_replace(dict(sd), {"audio_vae.": "autoencoder.", "vocoder.": "vocoder."}, filter_keys=True) + vae = VAE(sd=sd_audio, metadata=metadata) + else: + vae = VAE(sd=sd, device=device, dtype=dtype, metadata=metadata) + vae.throw_exception_if_invalid() + return (vae,) + + +class VAEMergeKJ(io.ComfyNode): + @classmethod + def define_schema(cls): + return io.Schema( + node_id="VAEMergeKJ", + category="KJNodes/vae", + description="Merge two VAEs by weighted-averaging their weights. " + "ratio is the weight toward vae_2 (0.0 = pure vae_1, 1.0 = pure vae_2). " + "Both VAEs must share the same architecture (matching state dict keys and shapes).", + inputs=[ + io.Vae.Input("vae_1"), + io.Vae.Input("vae_2"), + io.Float.Input("ratio", default=0.5, min=0.0, max=1.0, step=0.01), + ], + outputs=[ + io.Vae.Output(display_name="vae"), + ], + ) + + @classmethod + def execute(cls, vae_1, vae_2, ratio) -> io.NodeOutput: + from comfy.sd import VAE + sd1 = vae_1.get_sd() + sd2 = vae_2.get_sd() + + mismatch = set(sd1.keys()) ^ set(sd2.keys()) + if mismatch: + raise ValueError( + "Cannot merge: VAE architectures differ ({} non-matching keys, e.g. {}).".format( + len(mismatch), list(mismatch)[:3])) + + merged = {} + for k, v1 in sd1.items(): + v2 = sd2[k] + if v1.shape != v2.shape: + raise ValueError("Cannot merge: shape mismatch for '{}' ({} vs {}).".format(k, tuple(v1.shape), tuple(v2.shape))) + # Only blend float weights; integer buffers (e.g. num_batches_tracked) are copied from vae_1. + if torch.is_floating_point(v1): + blended = torch.lerp(v1.float(), v2.to(device=v1.device).float(), ratio) + merged[k] = blended.to(dtype=v1.dtype) + else: + merged[k] = v1.clone() + + merged_vae = VAE(sd=merged, device=vae_1.device, dtype=vae_1.vae_dtype) + merged_vae.throw_exception_if_invalid() + return io.NodeOutput(merged_vae) + +from comfy.samplers import sampling_function, CFGGuider +class Guider_ScheduledCFG(CFGGuider): + + def set_cfg(self, cfg, start_percent, end_percent): + self.cfg = cfg + self.start_percent = start_percent + self.end_percent = end_percent + + def predict_noise(self, x, timestep, model_options={}, seed=None): + steps = model_options["transformer_options"]["sample_sigmas"] + if isinstance(timestep, torch.Tensor): + timestep_value = timestep.reshape(-1)[0].to(steps) + else: + timestep_value = torch.tensor(timestep, device=steps.device, dtype=steps.dtype) + matched_step_index = torch.isclose(steps, timestep_value).nonzero() + assert not (isinstance(self.cfg, list) and len(self.cfg) != (len(steps) - 1)), "cfg list length must match step count" + if len(matched_step_index) > 0: + current_step_index = matched_step_index.item() + else: + for i in range(len(steps) - 1): + # walk from beginning of steps until crossing the timestep + if (steps[i] - timestep_value) * (steps[i + 1] - timestep_value) <= 0: + current_step_index = i + break + else: + current_step_index = 0 + current_percent = current_step_index / (len(steps) - 1) + + if self.start_percent <= current_percent <= self.end_percent: + if isinstance(self.cfg, list): + cfg = self.cfg[current_step_index] + else: + cfg = self.cfg + uncond = self.conds.get("negative", None) + else: + uncond = None + cfg = 1.0 + + return sampling_function(self.inner_model, x, timestep, uncond, self.conds.get("positive", None), cfg, model_options=model_options, seed=seed) + +class ScheduledCFGGuidance: + @classmethod + def INPUT_TYPES(s): + return {"required": { + "model": ("MODEL",), + "positive": ("CONDITIONING", ), + "negative": ("CONDITIONING", ), + "cfg": ("FLOAT", {"default": 6.0, "min": 0.0, "max": 100.0, "step": 0.01}), + "start_percent": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step":0.01}), + "end_percent": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step":0.01}), + }, + } + RETURN_TYPES = ("GUIDER",) + FUNCTION = "get_guider" + CATEGORY = "KJNodes/experimental" + DESCRiPTION = """ +CFG Guider that allows for scheduled CFG changes over steps, the steps outside the range will use CFG 1.0 thus being processed faster. +cfg input can be a list of floats matching step count, or a single float for all steps. +""" + + def get_guider(self, model, cfg, positive, negative, start_percent, end_percent): + guider = Guider_ScheduledCFG(model) + guider.set_conds(positive, negative) + guider.set_cfg(cfg, start_percent, end_percent) + return (guider, ) + + +class ApplyRifleXRoPE_WanVideo: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "model": ("MODEL",), + "latent": ("LATENT", {"tooltip": "Only used to get the latent count"}), + "k": ("INT", {"default": 6, "min": 1, "max": 100, "step": 1, "tooltip": "Index of intrinsic frequency"}), + } + } + + RETURN_TYPES = ("MODEL",) + FUNCTION = "patch" + CATEGORY = "KJNodes/wan" + EXPERIMENTAL = True + DESCRIPTION = "Extends the potential frame count of HunyuanVideo using this method: https://github.com/thu-ml/RIFLEx" + + def patch(self, model, latent, k): + model_class = model.model.diffusion_model + + model_clone = model.clone() + num_frames = latent["samples"].shape[2] + d = model_class.dim // model_class.num_heads + + rope_embedder = EmbedND_RifleX( + d, + 10000.0, + [d - 4 * (d // 6), 2 * (d // 6), 2 * (d // 6)], + num_frames, + k + ) + + model_clone.add_object_patch(f"diffusion_model.rope_embedder", rope_embedder) + + return (model_clone, ) + +class ApplyRifleXRoPE_HunuyanVideo: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "model": ("MODEL",), + "latent": ("LATENT", {"tooltip": "Only used to get the latent count"}), + "k": ("INT", {"default": 4, "min": 1, "max": 100, "step": 1, "tooltip": "Index of intrinsic frequency"}), + } + } + + RETURN_TYPES = ("MODEL",) + FUNCTION = "patch" + CATEGORY = "KJNodes/hunyuanvideo" + EXPERIMENTAL = True + DESCRIPTION = "Extends the potential frame count of HunyuanVideo using this method: https://github.com/thu-ml/RIFLEx" + + def patch(self, model, latent, k): + model_class = model.model.diffusion_model + + model_clone = model.clone() + num_frames = latent["samples"].shape[2] + + pe_embedder = EmbedND_RifleX( + model_class.params.hidden_size // model_class.params.num_heads, + model_class.params.theta, + model_class.params.axes_dim, + num_frames, + k + ) + + model_clone.add_object_patch(f"diffusion_model.pe_embedder", pe_embedder) + + return (model_clone, ) + +def rope_riflex(pos, dim, theta, L_test, k): + from einops import rearrange + assert dim % 2 == 0 + if model_management.is_device_mps(pos.device) or model_management.is_intel_xpu() or model_management.is_directml_enabled(): + device = torch.device("cpu") + else: + device = pos.device + + scale = torch.linspace(0, (dim - 2) / dim, steps=dim//2, dtype=torch.float64, device=device) + omega = 1.0 / (theta**scale) + + # RIFLEX modification - adjust last frequency component if L_test and k are provided + if k and L_test: + omega[k-1] = 0.9 * 2 * torch.pi / L_test + + out = torch.einsum("...n,d->...nd", pos.to(dtype=torch.float32, device=device), omega) + out = torch.stack([torch.cos(out), -torch.sin(out), torch.sin(out), torch.cos(out)], dim=-1) + out = rearrange(out, "b n d (i j) -> b n d i j", i=2, j=2) + return out.to(dtype=torch.float32, device=pos.device) + +class EmbedND_RifleX(nn.Module): + def __init__(self, dim, theta, axes_dim, num_frames, k): + super().__init__() + self.dim = dim + self.theta = theta + self.axes_dim = axes_dim + self.num_frames = num_frames + self.k = k + + def forward(self, ids): + n_axes = ids.shape[-1] + emb = torch.cat( + [rope_riflex(ids[..., i], self.axes_dim[i], self.theta, self.num_frames, self.k if i == 0 else 0) for i in range(n_axes)], + dim=-3, + ) + return emb.unsqueeze(1) + + +class Timer: + def __init__(self, name): + self.name = name + self.start_time = None + self.elapsed = 0 + +class TimerNodeKJ: + @classmethod + + def INPUT_TYPES(s): + return { + "required": { + "any_input": (IO.ANY, ), + "mode": (["start", "stop"],), + "name": ("STRING", {"default": "Timer"}), + }, + "optional": { + "timer": ("TIMER",), + }, + } + + RETURN_TYPES = (IO.ANY, "TIMER", "INT", ) + RETURN_NAMES = ("any_output", "timer", "time") + FUNCTION = "timer" + CATEGORY = "KJNodes/misc" + + def timer(self, mode, name, any_input=None, timer=None): + if timer is None: + if mode == "start": + timer = Timer(name=name) + timer.start_time = time.time() + return {"ui": { + "text": [f"{timer.start_time}"]}, + "result": (any_input, timer, 0) + } + elif mode == "stop" and timer is not None: + end_time = time.time() + timer.elapsed = int((end_time - timer.start_time) * 1000) + timer.start_time = None + return (any_input, timer, timer.elapsed) + +class HunyuanVideoEncodeKeyframesToCond: + @classmethod + def INPUT_TYPES(s): + return {"required": { + "model": ("MODEL",), + "positive": ("CONDITIONING", ), + "vae": ("VAE", ), + "start_frame": ("IMAGE", ), + "end_frame": ("IMAGE", ), + "num_frames": ("INT", {"default": 33, "min": 2, "max": 4096, "step": 1}), + "tile_size": ("INT", {"default": 512, "min": 64, "max": 4096, "step": 64}), + "overlap": ("INT", {"default": 64, "min": 0, "max": 4096, "step": 32}), + "temporal_size": ("INT", {"default": 64, "min": 8, "max": 4096, "step": 4, "tooltip": "Only used for video VAEs: Amount of frames to encode at a time."}), + "temporal_overlap": ("INT", {"default": 8, "min": 4, "max": 4096, "step": 4, "tooltip": "Only used for video VAEs: Amount of frames to overlap."}), + }, + "optional": { + "negative": ("CONDITIONING", ), + } + } + + RETURN_TYPES = ("MODEL", "CONDITIONING","CONDITIONING","LATENT") + RETURN_NAMES = ("model", "positive", "negative", "latent") + FUNCTION = "encode" + + CATEGORY = "KJNodes/hunyuanvideo" + + def encode(self, model, positive, start_frame, end_frame, num_frames, vae, tile_size, overlap, temporal_size, temporal_overlap, negative=None): + + model_clone = model.clone() + + model_clone.add_object_patch("concat_keys", ("concat_image",)) + + + x = (start_frame.shape[1] // 8) * 8 + y = (start_frame.shape[2] // 8) * 8 + + if start_frame.shape[1] != x or start_frame.shape[2] != y: + x_offset = (start_frame.shape[1] % 8) // 2 + y_offset = (start_frame.shape[2] % 8) // 2 + start_frame = start_frame[:,x_offset:x + x_offset, y_offset:y + y_offset,:] + if end_frame.shape[1] != x or end_frame.shape[2] != y: + x_offset = (start_frame.shape[1] % 8) // 2 + y_offset = (start_frame.shape[2] % 8) // 2 + end_frame = end_frame[:,x_offset:x + x_offset, y_offset:y + y_offset,:] + + video_frames = torch.zeros(num_frames-2, start_frame.shape[1], start_frame.shape[2], start_frame.shape[3], device=start_frame.device, dtype=start_frame.dtype) + video_frames = torch.cat([start_frame, video_frames, end_frame], dim=0) + + concat_latent = vae.encode_tiled(video_frames[:,:,:,:3], tile_x=tile_size, tile_y=tile_size, overlap=overlap, tile_t=temporal_size, overlap_t=temporal_overlap) + + out_latent = {} + out_latent["samples"] = torch.zeros_like(concat_latent) + + out = [] + for conditioning in [positive, negative if negative is not None else []]: + c = [] + for t in conditioning: + d = t[1].copy() + d["concat_latent_image"] = concat_latent + n = [t[0], d] + c.append(n) + out.append(c) + if len(out) == 1: + out.append(out[0]) + return (model_clone, out[0], out[1], out_latent) + + +class LazySwitchKJ: + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "switch": ("BOOLEAN",), + "on_false": (IO.ANY, {"lazy": True}), + "on_true": (IO.ANY, {"lazy": True}), + }, + } + + RETURN_TYPES = (IO.ANY,) + FUNCTION = "switch" + CATEGORY = "KJNodes/misc" + DESCRIPTION = "Controls flow of execution based on a boolean switch." + + def check_lazy_status(self, switch, on_false=None, on_true=None): + if switch and on_true is None: + return ["on_true"] + if not switch and on_false is None: + return ["on_false"] + + def switch(self, switch, on_false = None, on_true=None): + value = on_true if switch else on_false + return (value,) + + +from comfy.patcher_extension import WrappersMP +from comfy.sampler_helpers import prepare_mask +class TTM_OuterSampleWrapper: + def __init__(self, mask, steps): + self.mask = mask + self.steps = steps + + def __call__(self, executor, noise, latent_image, sampler, sigmas, denoise_mask, callback, disable_pbar, seed, latent_shapes): + guider = executor.class_obj + guider.model_options + wrappers = guider.model_options["transformer_options"]["wrappers"] + w = wrappers.setdefault(WrappersMP.APPLY_MODEL, {}) + + if self.mask is not None: + motion_mask = self.mask.reshape((-1, 1, self.mask.shape[-2], self.mask.shape[-1])) + shape = latent_shapes[0] + motion_mask = prepare_mask(motion_mask, shape, noise.device) + + scale_latent_inpaint = guider.model_patcher.model.scale_latent_inpaint + w["TTM_ApplyModel_Wrapper"] = [TTM_ApplyModel_Wrapper(latent_image, noise, motion_mask, self.steps, scale_latent_inpaint)] + + out = executor(noise, latent_image, sampler, sigmas, denoise_mask, callback, disable_pbar, seed, latent_shapes=latent_shapes) + + return out + + +class TTM_ApplyModel_Wrapper: + def __init__(self, reference_samples, noise, motion_mask, steps, scale_latent_inpaint): + self.reference_samples = reference_samples + self.noise = noise + self.motion_mask = motion_mask + self.steps = steps + self.scale_latent_inpaint = scale_latent_inpaint + + def __call__(self, executor, x, t, c_concat, c_crossattn, control, transformer_options, **kwargs): + sigmas = transformer_options["sample_sigmas"] + + matched = (sigmas == t).nonzero(as_tuple=True)[0] + if matched.numel() > 0: + current_step_index = matched.item() + else: + crossing = ((sigmas[:-1] - t) * (sigmas[1:] - t) <= 0).nonzero(as_tuple=True)[0] + current_step_index = crossing.item() if crossing.numel() > 0 else 0 + + next_sigma = sigmas[current_step_index + 1] if current_step_index < len(sigmas) - 1 else sigmas[current_step_index] + + if current_step_index != 0 and current_step_index < self.steps: + noisy_latent = self.scale_latent_inpaint(x=x, sigma=torch.tensor([next_sigma]), noise=self.noise.to(x), latent_image=self.reference_samples.to(x)) + if self.motion_mask is not None: + x = x * (1-self.motion_mask).to(x) + noisy_latent * self.motion_mask.to(x) + else: + x = noisy_latent + + return executor(x, t, c_concat, c_crossattn, control, transformer_options, **kwargs) + + +class LatentInpaintTTM: + @classmethod + def INPUT_TYPES(s): + return {"required": { + "model": ("MODEL", ), + "steps": ("INT", {"default": 7, "min": 0, "max": 888, "step": 1, "tooltip": "Number of steps to apply TTM inpainting for."}), + }, + "optional": { + "mask": ("MASK", {"tooltip": "Latent mask where white (1.0) is the area to inpaint and black (0.0) is the area to keep unchanged."}), + } + } + RETURN_TYPES = ("MODEL",) + FUNCTION = "patch" + EXPERIMENTAL = True + DESCRIPTION = "https://github.com/time-to-move/TTM" + SEARCH_ALIASES = ["time to move"] + CATEGORY = "KJNodes/experimental" + + def patch(self, model, steps, mask=None): + m = model.clone() + m.add_wrapper_with_key(WrappersMP.OUTER_SAMPLE, "TTM_OuterSampleWrapper", TTM_OuterSampleWrapper(mask, steps)) + return (m, ) + + +class SimpleCalculatorKJ(io.ComfyNode): + @classmethod + def define_schema(cls): + template = io.Autogrow.TemplateNames(input=io.MultiType.Input("var", [io.Int, io.Float, io.Boolean], optional=True), names=["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k"], min=2) + return io.Schema( + node_id="SimpleCalculatorKJ", + category="KJNodes/misc", + description=""" +Calculator node that evaluates a mathematical expression using inputs a and b. + Supported operations: +, -, *, /, //, %, **, <<, >>, unary +/- + Supported comparisons: ==, !=, <, <=, >, >= + Supported logic: and, or, not + Supported functions: abs(), round(), min(), max(), pow(), sqrt(), sin(), cos(), tan(), log(), log10(), exp(), floor(), ceil() + Supported constants: pi, euler, True, False +""", + search_aliases=["math", "arithmetic", "expression", "logic"], + inputs=[ + io.String.Input("expression", default="a + b", multiline=True), + io.Autogrow.Input("variables", template=template), + ], + outputs=[ + io.Float.Output(), + io.Int.Output(), + io.Boolean.Output(), + ], + ) + + @classmethod + def execute(cls, variables, expression, a=None, b=None) -> io.NodeOutput: + import ast + import operator + + # Allowed operations + allowed_operators = { + ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul, ast.Div: operator.truediv, + ast.FloorDiv: operator.floordiv, ast.Mod: operator.mod, ast.Pow: operator.pow, + ast.USub: operator.neg, ast.UAdd: operator.pos, ast.LShift: operator.lshift, + ast.RShift: operator.rshift, ast.Eq: operator.eq, ast.NotEq: operator.ne, ast.Lt: operator.lt, + ast.LtE: operator.le, ast.Gt: operator.gt, ast.GtE: operator.ge, ast.And: operator.and_, + ast.Or: operator.or_, ast.Not: operator.not_, + } + + # Allowed functions + allowed_functions = { + 'abs': abs, 'round': round, 'min': min, 'max': max, + 'pow': pow, 'sqrt': math.sqrt, 'sin': math.sin, + 'cos': math.cos, 'tan': math.tan, 'log': math.log, + 'log10': math.log10, 'exp': math.exp, 'floor': math.floor, + 'ceil': math.ceil + } + + # Allowed constants - start with pi, e, True, False + allowed_names = {'pi': math.pi, 'euler': math.e, 'True': True, 'False': False} + + # Add all variables from autogrow to allowed_names + for var_name, var_value in variables.items(): + allowed_names[var_name] = var_value + + # Backwards compatibility: add a and b if they're provided (for old workflows) + if a is not None: + allowed_names['a'] = a + if b is not None: + allowed_names['b'] = b + + def eval_node(node): + if isinstance(node, ast.Constant): # Numbers and booleans + return node.value + elif isinstance(node, ast.Name): # Variables + if node.id in allowed_names: + return allowed_names[node.id] + raise ValueError(f"Name '{node.id}' is not allowed") + elif isinstance(node, ast.BinOp): # Binary operations + if type(node.op) not in allowed_operators: + raise ValueError(f"Operator {type(node.op).__name__} is not allowed") + left = eval_node(node.left) + right = eval_node(node.right) + return allowed_operators[type(node.op)](left, right) + elif isinstance(node, ast.UnaryOp): # Unary operations + if type(node.op) not in allowed_operators: + raise ValueError(f"Operator {type(node.op).__name__} is not allowed") + operand = eval_node(node.operand) + return allowed_operators[type(node.op)](operand) + elif isinstance(node, ast.Compare): # Comparison operations + left = eval_node(node.left) + for op, comparator in zip(node.ops, node.comparators): + if type(op) not in allowed_operators: + raise ValueError(f"Operator {type(op).__name__} is not allowed") + right = eval_node(comparator) + result = allowed_operators[type(op)](left, right) + if not result: + return False + left = right + return True + elif isinstance(node, ast.BoolOp): # Boolean operations (and, or) + if type(node.op) not in allowed_operators: + raise ValueError(f"Operator {type(node.op).__name__} is not allowed") + values = [eval_node(value) for value in node.values] + if isinstance(node.op, ast.And): + return all(values) + elif isinstance(node.op, ast.Or): + return any(values) + elif isinstance(node, ast.Call): # Function calls + if not isinstance(node.func, ast.Name): + raise ValueError("Only simple function calls are allowed") + if node.func.id not in allowed_functions: + raise ValueError(f"Function '{node.func.id}' is not allowed") + args = [eval_node(arg) for arg in node.args] + return allowed_functions[node.func.id](*args) + else: + raise ValueError(f"Node type {type(node).__name__} is not allowed") + + try: + tree = ast.parse(expression, mode='eval') + result = eval_node(tree.body) + return io.NodeOutput(float(result), int(result), bool(result)) + except Exception as e: + logging.error(f"CalculatorKJ Error: {str(e)}") + return io.NodeOutput(0.0, 0, False) + + +class GetTrackRange(io.ComfyNode): + @classmethod + def define_schema(cls): + return io.Schema( + node_id="GetTrackRange", + category="conditioning/video_models", + inputs=[ + io.Tracks.Input("tracks"), + io.Int.Input("start_index", default=24, min=-10000, max=10000, step=1), + io.Int.Input("num_frames", default=10, min=1, max=10000, step=1), + ], + outputs=[ + io.Tracks.Output(), + ], + ) + + @classmethod + def execute(cls, tracks, start_index, num_frames) -> io.NodeOutput: + track_path = tracks["track_path"] + mask = tracks["track_visibility"] + total_frames = track_path.shape[0] + + if start_index < 0: + start_index = total_frames + start_index + start_index = max(0, min(start_index, total_frames)) + + # Clamp end_index + end_index = max(0, min(start_index + num_frames, total_frames)) + + tracks_out = track_path[start_index:end_index, ...] + mask_out = mask[start_index:end_index, ...] + + out_track = { + "track_path": tracks_out, + "track_visibility": mask_out, + } + return io.NodeOutput(out_track) + +class AddNoiseToTrackPath(io.ComfyNode): + @classmethod + def define_schema(cls): + return io.Schema( + node_id="AddNoiseToTrackPath", + category="conditioning/video_models", + inputs=[ + io.Tracks.Input("tracks"), + io.Float.Input("strength", default=1.0, min=0.0, max=100.0, step=0.01), + io.Int.Input("seed", default=0, min=0, max=0xffffffffffffffff, step=1), + io.Float.Input("noise_x_ratio", default=1.0, min=0.0, max=100.0, step=0.01, + tooltip="Multiplier for horizontal noise component"), + io.Float.Input("noise_y_ratio", default=1.0, min=0.0, max=100.0, step=0.01, + tooltip="Multiplier for vertical noise component"), + io.Float.Input("noise_temporal_ratio", default=1.0, min=0.0, max=100.0, step=0.01, + tooltip="Multiplier for temporal (frame-to-frame) noise"), + ], + outputs=[ + io.Tracks.Output(), + ], + ) + + @classmethod + def execute(cls, tracks, strength, seed, noise_x_ratio, noise_y_ratio, noise_temporal_ratio) -> io.NodeOutput: + track_path = tracks["track_path"].clone() + mask = tracks["track_visibility"] + + torch.manual_seed(seed) + noise = torch.randn_like(track_path) * strength + + # Apply directional scaling to noise + noise[..., 0] *= noise_x_ratio # X coordinate noise + noise[..., 1] *= noise_y_ratio # Y coordinate noise + + # Apply temporal smoothing if temporal ratio is less than 1 + if noise_temporal_ratio < 1.0: + num_frames = track_path.shape[0] + smoothed_noise = noise.clone() + kernel_size = max(1, int((1.0 - noise_temporal_ratio) * 10)) + + for i in range(num_frames): + start_idx = max(0, i - kernel_size // 2) + end_idx = min(num_frames, i + kernel_size // 2 + 1) + smoothed_noise[i] = noise[start_idx:end_idx].mean(dim=0) + + noise = smoothed_noise * noise_temporal_ratio + noise * (1 - noise_temporal_ratio) + + track_path = track_path + noise + + out_track = { + "track_path": track_path, + "track_visibility": mask, + } + return io.NodeOutput(out_track) + + +class VAEDecodeLoopKJ: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "samples": ("LATENT", {"tooltip": "The latent to be decoded."}), + "vae": ("VAE", {"tooltip": "The VAE model used for decoding the latent."}), + "overlap_latent_frames": ("INT", {"default": 2, "min": 2, "max": 8, "step": 1, "tooltip": "Number of frames to blend for seamless loop, for Wan 2 works and HunyuanVideo 1.5 should use 4"}), + } + } + RETURN_TYPES = ("IMAGE",) + OUTPUT_TOOLTIPS = ("The decoded images.",) + FUNCTION = "decode" + CATEGORY = "KJNodes/vae" + DESCRIPTION = "Video latent VAE decoding to fix artifacts on loop seams." + + def decode(self, vae, samples, overlap_latent_frames): + latents = samples["samples"] + + images = vae.decode(latents) + if overlap_latent_frames <= 0: + if len(images.shape) == 5: + images = images.reshape(-1, images.shape[-3], images.shape[-2], images.shape[-1]) + return (images, ) + + end_frames = overlap_latent_frames + 1 + start_frames = overlap_latent_frames + + temp_images = vae.decode(torch.cat([latents[:, :, -end_frames:]] + [latents[:, :, :start_frames]], dim=2)).cpu().float() + + total_concat = end_frames + start_frames + temp_start = total_concat * 2 - 1 + main_start = total_concat + (overlap_latent_frames if overlap_latent_frames > 2 else 0) + + images = torch.cat([temp_images[:, temp_start:].to(images), images[:, main_start:]], dim=1) + if len(images.shape) == 5: + images = images.reshape(-1, images.shape[-3], images.shape[-2], images.shape[-1]) + + return (images, ) + +class WanImageToVideoSVIPro(io.ComfyNode): + @classmethod + def define_schema(cls): + return io.Schema( + node_id="WanImageToVideoSVIPro", + category="conditioning/video_models", + inputs=[ + io.Conditioning.Input("positive"), + io.Conditioning.Input("negative"), + io.Int.Input("length", default=81, min=1, max=MAX_RESOLUTION, step=4), + io.Latent.Input("anchor_samples"), + io.Latent.Input("prev_samples", optional=True), + io.Int.Input("motion_latent_count", default=1, min=0, max=128, step=1), + ], + outputs=[ + io.Conditioning.Output(display_name="positive"), + io.Conditioning.Output(display_name="negative"), + io.Latent.Output(display_name="latent"), + ], + ) + + @classmethod + def execute(cls, positive, negative, length, motion_latent_count, anchor_samples, prev_samples=None) -> io.NodeOutput: + anchor_latent = anchor_samples["samples"].clone() + + B, C, T, H, W = anchor_latent.shape + empty_latent = torch.zeros([B, 16, ((length - 1) // 4) + 1, H, W], device=model_management.intermediate_device()) + + total_latents = (length - 1) // 4 + 1 + device = anchor_latent.device + dtype = anchor_latent.dtype + + if prev_samples is None or motion_latent_count == 0: + padding_size = total_latents - anchor_latent.shape[2] + image_cond_latent = anchor_latent + else: + motion_latent = prev_samples["samples"][:, :, -motion_latent_count:].clone() + padding_size = total_latents - anchor_latent.shape[2] - motion_latent.shape[2] + image_cond_latent = torch.cat([anchor_latent, motion_latent], dim=2) + + padding = torch.zeros(1, C, padding_size, H, W, dtype=dtype, device=device) + padding = comfy.latent_formats.Wan21().process_out(padding) + image_cond_latent = torch.cat([image_cond_latent, padding], dim=2) + + mask = torch.ones((1, 1, empty_latent.shape[2], H, W), device=device, dtype=dtype) + mask[:, :, :1] = 0.0 + + positive = node_helpers.conditioning_set_values(positive, {"concat_latent_image": image_cond_latent, "concat_mask": mask}) + negative = node_helpers.conditioning_set_values(negative, {"concat_latent_image": image_cond_latent, "concat_mask": mask}) + + out_latent = {} + out_latent["samples"] = empty_latent + return io.NodeOutput(positive, negative, out_latent) + +class DeprecatedCompileNodeKJ: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "model": (IO.ANY,), + }, + } + RETURN_TYPES = (IO.ANY,) + FUNCTION = "passthrough" + CATEGORY = "KJNodes/deprecated" + DESCRIPTION = "This node has been replaced with TorchCompileModelAdvanced node, please use that instead." + def passthrough(self, model): + return (model,) + + +class VisualizeSigmasKJ(io.ComfyNode): + @classmethod + def define_schema(cls): + return io.Schema( + node_id="VisualizeSigmasKJ", + category="KJNodes/misc", + inputs=[ + io.Sigmas.Input("sigmas"), + io.Int.Input("start_step", default=0, min=-1, max=1000, step=1, + tooltip="Step index to mark as the start of a range (inclusive). Set to -1 to disable."), + io.Int.Input("end_step", default=-1, min=-1, max=1000, step=1, + tooltip="Step index to mark as the end of a range (inclusive). Set to - 1 to disable."), + ], + outputs=[ + io.Sigmas.Output(display_name="sigmas_out"), + io.Image.Output(display_name="image"), + ], + ) + + @classmethod + def execute(cls, sigmas, start_step=0, end_step=-1) -> io.NodeOutput: + + start_idx = 0 + end_idx = len(sigmas) - 1 + + if isinstance(start_step, float): + idxs = (sigmas <= start_step).nonzero(as_tuple=True)[0] + if len(idxs) > 0: + start_idx = idxs[0].item() + elif isinstance(start_step, int): + if start_step > 0: + start_idx = start_step + + if isinstance(end_step, float): + idxs = (sigmas >= end_step).nonzero(as_tuple=True)[0] + if len(idxs) > 0: + end_idx = idxs[-1].item() + elif isinstance(end_step, int): + if end_step != -1: + end_idx = end_step - 1 + + import matplotlib + matplotlib.use('Agg') + import matplotlib.pyplot as plt + sigmas_np = sigmas.cpu().numpy() + if not np.isclose(sigmas_np[-1], 0.0, atol=1e-6): + sigmas_np = np.append(sigmas_np, 0.0) + buf = BytesIO() + fig = plt.figure(facecolor='#353535') + ax = fig.add_subplot(111) + ax.set_facecolor('#353535') # Set axes background color + x_values = range(0, len(sigmas_np)) + ax.plot(x_values, sigmas_np) + # Annotate each sigma value + ax.scatter(x_values, sigmas_np, color='white', s=20, zorder=3) # Small dots at each sigma + for x, y in zip(x_values, sigmas_np): + # Show all annotations if few steps, or just show split step annotations + show_annotation = len(sigmas_np) <= 10 + is_split_step = (start_idx > 0 and x == start_idx) or (end_idx != -1 and x == end_idx + 1) + + if show_annotation or is_split_step: + color = 'orange' + if is_split_step: + color = 'yellow' + ax.annotate(f"{y:.3f}", (x, y), textcoords="offset points", xytext=(10, 1), ha='center', color=color, fontsize=12) + ax.set_xticks(x_values) + ax.set_title("Sigmas", color='white') # Title font color + ax.set_xlabel("Step", color='white') # X label font color + ax.set_ylabel("Sigma Value", color='white') # Y label font color + ax.tick_params(axis='x', colors='white', labelsize=10) # X tick color + ax.tick_params(axis='y', colors='white', labelsize=10) # Y tick color + # Add split point if end_step is defined + end_idx += 1 + if end_idx != -1 and 0 <= end_idx < len(sigmas_np) - 1: + ax.axvline(end_idx, color='red', linestyle='--', linewidth=2, label='end_step split') + # Add split point if start_step is defined + if start_idx > 0 and 0 <= start_idx < len(sigmas_np): + ax.axvline(start_idx, color='green', linestyle='--', linewidth=2, label='start_step split') + if (end_idx != -1 and 0 <= end_idx < len(sigmas_np)) or (start_idx > 0 and 0 <= start_idx < len(sigmas_np)): + handles, labels = ax.get_legend_handles_labels() + if labels: + ax.legend() + # Draw shaded range + range_start_idx = start_idx if start_idx > 0 else 0 + range_end_idx = end_idx if end_idx > 0 and end_idx < len(sigmas_np) else len(sigmas_np) - 1 + if range_start_idx < range_end_idx: + ax.axvspan(range_start_idx, range_end_idx, color='lightblue', alpha=0.1, label='Sampled Range') + + + plt.tight_layout() + fig.canvas.draw() + w, h = fig.canvas.get_width_height() + try: + buf = np.frombuffer(fig.canvas.tostring_argb(), dtype=np.uint8) + buf = buf.reshape(h, w, 4) + buf = buf[:, :, [1, 2, 3]] # Convert ARGB to RGB + except AttributeError: + buf = np.frombuffer(fig.canvas.tostring_rgb(), dtype=np.uint8) + buf = buf.reshape(h, w, 3).copy() + image = torch.from_numpy(buf).float() / 255.0 + image = image.unsqueeze(0) #(H, W, C) -> (1, H, W, C) + plt.close(fig) + + sigmas_out = sigmas[start_idx:end_idx + 1] if end_idx != -1 else sigmas[start_idx:] + + return io.NodeOutput(sigmas_out,image) + +class PreviewLatentNoiseMask(io.ComfyNode): + + @classmethod + def define_schema(cls): + return io.Schema( + node_id="PreviewLatentNoiseMask", + category="KJNodes/latents", + description="Previews the latent noise mask", + inputs=[ + io.Latent.Input("latent",), + ], + outputs=[ + io.Mask.Output(display_name="mask"), + ], + ) + + @classmethod + def execute(cls, latent) -> io.NodeOutput: + noise_mask = latent.get("noise_mask", None) + if noise_mask is None: + return io.NodeOutput(torch.zeros((1, 64, 64))) + noise_mask = noise_mask.clone() + + if noise_mask.ndim == 5: + noise_mask = noise_mask[0, 0] + + return io.NodeOutput(noise_mask) + +class PlaySoundKJ(io.ComfyNode): + """Plays audio in the browser when execution reaches this node.""" + + @classmethod + def define_schema(cls): + return io.Schema( + node_id="PlaySoundKJ", + category="KJNodes/audio", + description="Plays the input audio in the browser. Modes: 'always' plays on every execution, 'on_empty_queue' plays only when the queue finishes, 'on_change' plays only when the audio content changes. Duration limits playback length (0 = full audio).", + inputs=[ + io.AnyType.Input("any_input", optional=True), + io.Audio.Input("audio", optional=True), + io.String.Input("audio_path", default="", tooltip="Path to an audio file. Used when audio input is not connected."), + io.Combo.Input("mode", options=["always", "on_empty_queue", "on_change"], default="always"), + io.Float.Input("volume", default=0.5, min=0.0, max=1.0, step=0.01), + io.Float.Input("duration", default=5.0, min=0.0, max=300.0, step=0.1, tooltip="Duration in seconds to play. 0 = play full audio."), + ], + outputs=[ + io.AnyType.Output("any_output", display_name="any_output"), + ], + is_output_node=True, + ) + + @classmethod + def fingerprint_inputs(cls, **kwargs): + if kwargs.get("mode") == "on_change": + return False + return float("NaN") + + @staticmethod + def _generate_chime(): + sr = 32000 + C, Ab, Bb = 523.26, 421.30, 466.16 # note frequencies + e = 0.14 # eighth note — adjust to change tempo + S, M, H = 16, 7, 2.5 # staccato / medium / held decay + melody = [ + (C,e,S), (C,e,S), (C,e,S), (C,e*3,M), + (Ab,e*3,M), (Bb,e*3,M), (C,e*2,S), (Bb,e,S), (C,e*5,H), + ] + k = torch.exp(-torch.linspace(-2, 2, 15) ** 2) + k = (k / k.sum()).reshape(1, 1, -1) + parts = [] + for freq, dur, decay in melody: + t = torch.linspace(0, dur, int(sr * dur)) + tone = torch.tanh(3 * torch.sin(2 * math.pi * freq * t)) + tone = torch.nn.functional.conv1d(tone.reshape(1, 1, -1), k, padding=7).squeeze() + parts.append(tone * torch.exp(-t * decay)) + wav = torch.cat(parts) * 0.45 + return {"waveform": wav.unsqueeze(0).unsqueeze(0), "sample_rate": sr} + + @classmethod + def execute(cls, audio=None, audio_path="", mode="always", volume=0.5, duration=5.0, any_input=None) -> io.NodeOutput: + if audio is None: + if audio_path: + import av + with av.open(audio_path) as af: + stream = af.streams.audio[0] + sr = stream.codec_context.sample_rate + frames = [] + for frame in af.decode(streams=stream.index): + buf = torch.from_numpy(frame.to_ndarray()) + if buf.shape[0] != stream.channels: + buf = buf.view(-1, stream.channels).t() + frames.append(buf) + wav = torch.cat(frames, dim=1).float() + audio = {"waveform": wav.unsqueeze(0), "sample_rate": sr} + else: + audio = cls._generate_chime() + + preview = ui.PreviewAudio(audio, cls=cls) + ui_dict = preview.as_dict() + ui_dict["audio_hash"] = [hash(audio["waveform"].sum().item())] + return io.NodeOutput( + any_input, + ui=ui_dict, + ) diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/preview_override_node.py b/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/preview_override_node.py new file mode 100644 index 0000000000000000000000000000000000000000..f18efdd372c3e8cb7d9d368d2f728632ab615db7 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/preview_override_node.py @@ -0,0 +1,818 @@ +import base64 +import io as pyio +import logging +import queue +import threading +import time + +import numpy as np +import torch + +import comfy.model_management +import comfy.patcher_extension +import folder_paths +import latent_preview +from comfy_api.latest import io +from PIL import Image, ImageOps + +from .tiny_vae import load_tiny_vae_decoder + +try: + from .ltxv_nodes import WrappedPreviewer as _LTXWrappedPreviewer, get_ltx_rgb_factors as _ltx_rgb_factors +except Exception as e: + logging.warning(f"[KJ PreviewOverride] LTX preview helpers unavailable ({e}); LTX previews disabled.") + _LTXWrappedPreviewer = None + _ltx_rgb_factors = None + + +try: + from server import PromptServer +except ImportError: + PromptServer = None + +def _suppressed_preview_image(self_, preview_format, x0): + return None + + +class _AsyncPreviewEncoder: + """Off-thread encoder. Bounded FIFO drops-on-full so the sampler never blocks on us.""" + + _STOP = object() + + def __init__(self, max_in_flight=2): + self.q = queue.Queue(maxsize=max_in_flight) + self.thread = threading.Thread(target=self._run, name="kj_preview_encoder", daemon=True) + self.thread.start() + + def submit(self, fn): + try: + self.q.put_nowait(fn) + return True + except queue.Full: + return False + + def _run(self): + while True: + item = self.q.get() + if item is self._STOP: + return + try: + item() + except Exception: + logging.exception("[KJ Preview Override] async encoder error") + + def shutdown(self, drain_timeout=5.0): + try: + self.q.put(self._STOP, timeout=drain_timeout) + except queue.Full: + pass + self.thread.join(timeout=drain_timeout) + + +def _get_core_previewer(load_device, latent_format): + # Walk past custom-node hooks on get_previewer to reach the unwrapped core function. + fn = latent_preview.get_previewer + seen = set() + while hasattr(fn, "__wrapped__") and id(fn) not in seen: + seen.add(id(fn)) + fn = fn.__wrapped__ + return fn(load_device, latent_format) + + +def _decode_video_frames_l2rgb(x0, latent_format, max_frames, stride=1): + # Bulk-blocking GPU→CPU copy (not per-frame non_blocking) avoids torn frames at high res. + if x0.ndim != 5: + return [] + rgb_factors = getattr(latent_format, "latent_rgb_factors", None) + if rgb_factors is None: + return [] + try: + reshape = getattr(latent_format, "latent_rgb_factors_reshape", None) + if reshape is not None: + x0 = reshape(x0) + bias = getattr(latent_format, "latent_rgb_factors_bias", None) + factors = torch.tensor(rgb_factors, device=x0.device, dtype=x0.dtype).transpose(0, 1) + bias_t = torch.tensor(bias, device=x0.device, dtype=x0.dtype) if bias is not None else None + x = x0[0] + if stride > 1: + x = x[:, ::stride] + t_total = x.shape[1] + if max_frames > 0 and max_frames < t_total: + indices = np.linspace(0, t_total - 1, max_frames).round().astype(int).tolist() + x = x[:, indices] + x = x.movedim(0, -1) + rgb = torch.nn.functional.linear(x, factors, bias=bias_t) + rgb.add_(1.0).mul_(127.5).clamp_(0, 255) + rgb_cpu = rgb.to(torch.uint8).cpu().numpy() + return [Image.fromarray(rgb_cpu[i]) for i in range(rgb_cpu.shape[0])] + except Exception: + return [] + + +# PyPI PyAV wheels typically lack NVENC; probe once at import. +def _probe_nvenc(): + try: + import av # noqa + av.Codec("h264_nvenc", "w") + return True + except Exception: + return False + +_NVENC_AVAILABLE = _probe_nvenc() + +# NVENC H.264 rejects sub-145×49 inputs at avcodec_open2 — fall back to WebP for small frames. +_NVENC_MIN_W = 145 +_NVENC_MIN_H = 49 + +_nvenc_warned = False + + +def _encode_mp4_nvenc(frames, fps, max_res): + # Fragmented MP4 so the browser can decode mid-download. Returns (None, 0, 0) on failure + # (including too-small-for-NVENC), so caller falls through to WebP. + global _nvenc_warned + if not frames: + return None, 0, 0 + try: + import av + except Exception: + return None, 0, 0 + pil_frames = [] + for f in frames: + pf = f if f.mode == "RGB" else f.convert("RGB") + if max_res and max_res > 0 and (pf.width > max_res or pf.height > max_res): + pf = ImageOps.contain(pf, (max_res, max_res), Image.LANCZOS) + pil_frames.append(pf) + # yuv420p requires even dimensions. + w0, h0 = pil_frames[0].width, pil_frames[0].height + out_w, out_h = w0 & ~1, h0 & ~1 + if (out_w, out_h) != (w0, h0): + pil_frames = [pf.resize((out_w, out_h), Image.LANCZOS) for pf in pil_frames] + if out_w < _NVENC_MIN_W or out_h < _NVENC_MIN_H: + return None, 0, 0 + # Driver/GPU varies what option combos are accepted; bare preset always works. + option_candidates = [ + {"preset": "p1", "rc": "vbr", "cq": "23"}, + {"preset": "p1"}, + ] + last_err = None + for opts in option_candidates: + buf = pyio.BytesIO() + try: + container = av.open( + buf, mode="w", format="mp4", + options={"movflags": "frag_keyframe+empty_moov+default_base_moof"}, + ) + stream = container.add_stream("h264_nvenc", rate=int(max(1, fps))) + stream.width = out_w + stream.height = out_h + stream.pix_fmt = "yuv420p" + stream.options = opts + for pf in pil_frames: + for pkt in stream.encode(av.VideoFrame.from_image(pf)): + container.mux(pkt) + for pkt in stream.encode(): + container.mux(pkt) + container.close() + return base64.b64encode(buf.getvalue()).decode("ascii"), out_w, out_h + except Exception as e: + last_err = e + continue + if not _nvenc_warned: + _nvenc_warned = True + logging.warning(f"[KJ PreviewOverride] NVENC MP4 encode failed, using WebP fallback: {last_err}") + return None, 0, 0 + + +def _encode_animated_webp(frames, fps, quality, max_res): + if not frames: + return None, 0, 0 + pil_frames = [] + for f in frames: + pf = f + if pf.mode != "RGB": + pf = pf.convert("RGB") + if max_res and max_res > 0 and (pf.width > max_res or pf.height > max_res): + pf = ImageOps.contain(pf, (max_res, max_res), Image.LANCZOS) + pil_frames.append(pf) + duration_ms = max(1, int(round(1000 / max(1, fps)))) + buf = pyio.BytesIO() + try: + pil_frames[0].save( + buf, + format="WEBP", + save_all=True, + append_images=pil_frames[1:], + duration=duration_ms, + loop=0, + quality=quality, + method=4, + ) + except Exception as e: + logging.warning(f"Animated WebP encode failed: {e}") + return None, 0, 0 + return base64.b64encode(buf.getvalue()).decode("ascii"), pil_frames[0].width, pil_frames[0].height + + +def _interp_db_curve(t, xs, ys): + # Mirrors sampler_nodes._interp_curve. + if t <= xs[0]: + return ys[0] + if t >= xs[-1]: + return ys[-1] + for i in range(len(xs) - 1): + if xs[i] <= t <= xs[i + 1]: + span = xs[i + 1] - xs[i] + if span <= 0: + return ys[i] + f = (t - xs[i]) / span + return ys[i] + f * (ys[i + 1] - ys[i]) + return 0.0 + + +def _detect_detail_boost_curve(sampler, model_patcher, sigmas_list): + # Amount is already baked into ys by the editor, so peak ys == user-set amount. + try: + extra = getattr(sampler, "extra_options", None) or {} + xs = extra.get("db_curve_xs") + ys = extra.get("db_curve_ys") + if "db_wrapped_sampler" not in extra or not xs or not ys or len(xs) != len(ys) or len(xs) < 2: + return None + ms = model_patcher.get_model_object("model_sampling") + start_sigma = float(ms.percent_to_sigma(extra.get("db_start_percent", 0.0))) + end_sigma = float(ms.percent_to_sigma(extra.get("db_end_percent", 1.0))) + # None outside the gate so JS can distinguish "inactive" from "active with value 0". + out = [] + for s in sigmas_list: + sig = float(s) + if sig <= 0 or start_sigma <= end_sigma or sig >= start_sigma or sig <= end_sigma: + out.append(None) + continue + t = (start_sigma - sig) / (start_sigma - end_sigma) + out.append(_interp_db_curve(t, xs, ys)) + return out + except Exception as e: + logging.warning(f"[KJ PreviewOverride] DB curve detection failed: {e}") + return None + + +def _ltx_decode_to_pil(ltx_previewer, x0_5d, max_frames=None, stride=1): + # Pre-shape (B, C, T, H, W) → (B*T, C, H, W); WrappedPreviewer adds the sequence-batch dim. + if ltx_previewer is None or x0_5d.ndim != 5: + return [] + if stride > 1: + x0_5d = x0_5d[:, :, ::stride] + x_moved = x0_5d.movedim(2, 1) # (B, T, C, H, W) — must take shape AFTER movedim + x_in = x_moved.reshape((-1,) + x_moved.shape[-3:]) + rgb = ltx_previewer.decode_latent_to_preview(x_in) + if rgb is None: + return [] + if rgb.ndim == 3: + rgb = rgb.unsqueeze(0) + if rgb.ndim != 4: + return [] + t_total = rgb.shape[0] + if max_frames is not None and 0 < max_frames < t_total: + indices = np.linspace(0, t_total - 1, max_frames).round().astype(int).tolist() + rgb = rgb[indices] + u8 = (rgb * 255).clamp(0, 255).to(torch.uint8).cpu().numpy() + return [Image.fromarray(u8[i]) for i in range(u8.shape[0])] + + +def _ltx_full_vae_decode_to_pil(vae, x0_5d, max_frames=None, stride=1): + # vae.decode handles device + tiling. Slow vs TAEHV but full quality. Output shape + # varies by VAE; we accept (B, T, H, W, C) or (T, H, W, C) and normalize. + if vae is None or x0_5d.ndim != 5: + return [] + if stride > 1: + x0_5d = x0_5d[:, :, ::stride] + try: + images = vae.decode(x0_5d) + except Exception as e: + logging.warning(f"[KJ PreviewOverride] LTX VAE decode failed: {e}") + return [] + if images.ndim == 5: + images = images[0] + if images.ndim != 4: + return [] + t_total = images.shape[0] + if max_frames is not None and 0 < max_frames < t_total: + indices = np.linspace(0, t_total - 1, max_frames).round().astype(int).tolist() + images = images[indices] + u8 = (images.float() * 255).clamp(0, 255).to(torch.uint8).cpu().numpy() + return [Image.fromarray(u8[i]) for i in range(u8.shape[0])] + + +def _tiny_vae_decode_to_pil(decoder, x0, max_frames=None, stride=1): + # Raises on failure so the caller can disable the decoder instead of retrying every step. + if x0.ndim == 4: + rgb = decoder.decode(x0[:1])[0].movedim(0, -1).unsqueeze(0) + elif x0.ndim == 5: + indices = list(range(0, x0.shape[2], max(1, stride))) + if max_frames is not None and 0 < max_frames < len(indices): + picks = np.linspace(0, len(indices) - 1, max_frames).round().astype(int).tolist() + indices = [indices[i] for i in picks] + rgb = decoder.decode_video(x0[:1], frame_indices=indices) + else: + return [] + u8 = rgb.clamp(0, 1).mul(255).to(torch.uint8).cpu().numpy() + return [Image.fromarray(u8[i]) for i in range(u8.shape[0])] + + +def _is_ltx_latent_format(latent_format): + return "LTX" in type(latent_format).__name__ + + +def _is_ltx2_diffusion_model(model_patcher): + # Same probe as ltxv_nodes.OuterSampleCallbackWrapper. + try: + dm = model_patcher.model.diffusion_model + return not getattr(dm, "caption_projection_first_linear", True) + except Exception: + return False + + +def _ltx_num_keyframes(guider): + try: + positive = guider.conds.get("positive") if hasattr(guider, "conds") else None + if positive and len(positive) > 0: + kf = positive[0].get("keyframe_idxs") + if kf is not None: + return int(torch.unique(kf[0, 0, :, 0]).numel()) + except Exception: + pass + return 0 + + +def _normalize_packed_x0(x0, latent_shapes, num_keyframes): + # Restore standard video latents from flattened packs + if latent_shapes and len(latent_shapes) > 0: + target = latent_shapes[0] + if x0.ndim == 3 and len(target) >= 3: + cut = 1 + for d in target[1:]: + cut *= int(d) + x0 = x0[:, :, :cut].reshape([x0.shape[0]] + list(target)[1:]) + if num_keyframes > 0 and x0.ndim == 5: + x0 = x0[:, :, :-num_keyframes] + return x0 + + +class _PreviewOverrideWrapper: + def __init__(self, max_resolution, node_id, jpeg_quality, suppress_default, preview_frames=1, preview_fps=12, vae=None, tiny_vae="none"): + self.max_resolution = max_resolution + self.node_id = str(node_id) if node_id is not None else None + self.jpeg_quality = jpeg_quality + self.suppress_default = suppress_default + self.preview_frames = preview_frames + self.preview_fps = preview_fps + self.vae = vae + self.tiny_vae = tiny_vae + self.frames = [] + + def __call__(self, executor, noise, latent_image, sampler, sigmas, denoise_mask, callback, disable_pbar, seed, latent_shapes): + guider = executor.class_obj + model_patcher = guider.model_patcher + + is_ltx = _is_ltx_latent_format(model_patcher.model.latent_format) + is_ltx2 = is_ltx and _is_ltx2_diffusion_model(model_patcher) + num_keyframes = _ltx_num_keyframes(guider) if is_ltx else 0 + # Multi-stream models (audio+video) hand this wrapper the flat pack, not the nested view. + unpack_x0 = is_ltx or (latent_shapes is not None and len(latent_shapes) > 1) + + # Tiny VAE from models/vae_approx + tiny_vae = None + if self.tiny_vae and self.tiny_vae != "none" and load_tiny_vae_decoder is not None: + tiny_vae = load_tiny_vae_decoder(self.tiny_vae) + if tiny_vae is not None and latent_shapes and len(latent_shapes[0]) >= 2: + channels = int(latent_shapes[0][1]) + if channels != tiny_vae.latent_channels: + logging.warning( + f"[KJ PreviewOverride] '{self.tiny_vae}' decodes {tiny_vae.latent_channels}-channel " + f"latents but this model's are {channels}-channel; ignoring it." + ) + tiny_vae = None + + # LTX reuses the LTX-specific node's WrappedPreviewer; we call decode_latent_to_preview + # directly per step, bypassing its decode_latent_to_preview_image rate-limiting. + # If a non-TAEHV VAE is supplied, decode via vae.decode() for full quality (slower). + ltx_previewer = None + ltx_full_vae = None + vae_restore_device = None + if is_ltx: + try: + factors, bias = _ltx_rgb_factors(is_ltx2) + taeltx = None + if self.vae is not None: + if self.vae.first_stage_model.__class__.__name__ == "TAEHV": + # TAEHV-LTX decode needs the VAE on GPU; restored at end of __call__. + target_device = comfy.model_management.get_torch_device() + try: + for p in self.vae.first_stage_model.parameters(): + vae_restore_device = p.device + break + self.vae.first_stage_model.to(target_device) + taeltx = self.vae + except Exception as e: + logging.warning(f"[KJ PreviewOverride] Could not move TAEHV-LTX to GPU, skipping: {e}") + else: + # Comfy VAE.decode manages its own device — no pin-to-GPU needed. + ltx_full_vae = self.vae + ltx_previewer = _LTXWrappedPreviewer(factors, bias, rate=8, taeltx=taeltx) + except Exception as e: + logging.warning(f"[KJ PreviewOverride] LTX previewer setup failed: {e}") + + previewer = _get_core_previewer(model_patcher.load_device, model_patcher.model.latent_format) + # Latent2RGB fallback — used when the active previewer returns a non-PIL result + # (e.g. TAEHV/TAESD on a 5D latent). LTX skips this and goes through ltx_previewer. + fallback_previewer = None + try: + lf = model_patcher.model.latent_format + rgb_factors = getattr(lf, "latent_rgb_factors", None) + if rgb_factors is not None: + fallback_previewer = latent_preview.Latent2RGBPreviewer( + rgb_factors, + getattr(lf, "latent_rgb_factors_bias", None), + getattr(lf, "latent_rgb_factors_reshape", None), + ) + except Exception: + pass + + original_callback = callback + node_id = self.node_id + max_res = self.max_resolution + quality = self.jpeg_quality + self.frames = [] + + # N+1 boundaries for N steps: keep them all so the step marker advances through each. + sigmas_list = sigmas.detach().cpu().tolist() if sigmas is not None else [] + # Pre-seed so step 1 has a measurable Δ (model's first transformation from noise → x0). + initial_seed_cpu = None + try: + if sigmas is not None and len(sigmas) > 0: + # sigmas often lives on CPU while noise is on CUDA — align before the multiply. + s0 = sigmas[0].to(noise.device) if hasattr(sigmas[0], "to") else sigmas[0] + seeded = noise * s0 + if unpack_x0: + seeded = _normalize_packed_x0(seeded, latent_shapes, num_keyframes) + initial_seed_cpu = seeded.detach().float().cpu() + except Exception as e: + logging.warning(f"[KJ PreviewOverride] initial seed Δ pre-fill failed: {e}") + state = {"last_x0_cpu": initial_seed_cpu, "last_time": None, "step_ms_window": []} + total_steps_init = max(0, len(sigmas_list) - 1) + + # Boundary-0 message: sigmas (required by JS hover handler) plus optional noise preview. + if node_id is not None and PromptServer is not None: + init_payload = { + "node_id": node_id, + "step": 0, + "total": total_steps_init, + "sigma": sigmas_list[0] if sigmas_list else None, + "sigmas": sigmas_list, + } + db_curve = _detect_detail_boost_curve(sampler, model_patcher, sigmas_list) + if db_curve is not None: + init_payload["db_curve"] = db_curve + # Use Latent2RGB (or LTX previewer) directly — the model's default previewer (TAEHV) + # slices to one temporal frame and returns a shape PIL can't render on raw noise. + try: + lf = model_patcher.model.latent_format + rgb_factors = getattr(lf, "latent_rgb_factors", None) + if sigmas is not None and len(sigmas) > 0: + s0 = sigmas[0].to(noise.device) if hasattr(sigmas[0], "to") else sigmas[0] + init_latent = noise * s0 + else: + init_latent = noise + if unpack_x0: + init_latent = _normalize_packed_x0(init_latent, latent_shapes, num_keyframes) + pil_init = None + if tiny_vae is not None: + pil_frames = _tiny_vae_decode_to_pil(tiny_vae, init_latent, max_frames=1) + pil_init = pil_frames[0] if pil_frames else None + elif ltx_previewer is not None and init_latent.ndim == 5: + pil_frames = _ltx_decode_to_pil(ltx_previewer, init_latent, max_frames=1) + pil_init = pil_frames[0] if pil_frames else None + elif rgb_factors is not None: + noise_previewer = latent_preview.Latent2RGBPreviewer( + rgb_factors, + getattr(lf, "latent_rgb_factors_bias", None), + getattr(lf, "latent_rgb_factors_reshape", None), + ) + out = noise_previewer.decode_latent_to_preview(init_latent) + if isinstance(out, Image.Image): + pil_init = out + if pil_init is not None: + if pil_init.mode != "RGB": + pil_init = pil_init.convert("RGB") + if max_res and max_res > 0 and (pil_init.width > max_res or pil_init.height > max_res): + pil_init = ImageOps.contain(pil_init, (max_res, max_res), Image.LANCZOS) + ibuf = pyio.BytesIO() + pil_init.save(ibuf, format="JPEG", quality=quality) + init_payload["image"] = base64.b64encode(ibuf.getvalue()).decode("ascii") + init_payload["w"] = pil_init.width + init_payload["h"] = pil_init.height + except Exception as e: + logging.warning(f"Initial noise preview failed (sigmas still sent): {e}") + PromptServer.instance.send_sync("kj_preview_override", init_payload, PromptServer.instance.client_id) + + encoder = _AsyncPreviewEncoder() + animate_video = self.preview_frames > 1 + anim_frames = self.preview_frames + anim_fps = self.preview_fps + + + def new_callback(step, x0, x, total_steps_): + nonlocal tiny_vae + if previewer is not None or fallback_previewer is not None or ltx_previewer is not None or tiny_vae is not None: + try: + # NEVER rebind x0 — the sampler reuses the same tensor downstream + # (unpack_latents reshapes it). Preview mutations stay on x0_view. + x0_view = x0 + if unpack_x0: + x0_view = _normalize_packed_x0(x0_view, latent_shapes, num_keyframes) + + pil_frames = [] + max_pil = anim_frames if animate_video else 1 + if tiny_vae is not None: + try: + pil_frames = _tiny_vae_decode_to_pil(tiny_vae, x0_view, max_frames=max_pil) + except Exception as e: + # OOM at 16x upscale is the likely cause — drop to the cheap paths for good. + logging.warning(f"[KJ PreviewOverride] tiny VAE decode failed, falling back: {e}") + tiny_vae = None + if not pil_frames and ltx_full_vae is not None and x0_view.ndim == 5: + pil_frames = _ltx_full_vae_decode_to_pil(ltx_full_vae, x0_view, max_frames=max_pil) + if not pil_frames and ltx_previewer is not None and x0_view.ndim == 5: + try: + pil_frames = _ltx_decode_to_pil(ltx_previewer, x0_view, max_frames=max_pil) + except Exception as e: + logging.warning(f"LTX preview decode failed: {e}") + if not pil_frames and animate_video and x0_view.ndim == 5 and ltx_previewer is None: + pil_frames = _decode_video_frames_l2rgb( + x0_view, model_patcher.model.latent_format, anim_frames, + ) + + if not pil_frames: + for prev in (previewer, fallback_previewer): + if prev is None: + continue + try: + out = prev.decode_latent_to_preview(x0_view) + except Exception as e: + if prev is previewer: + logging.warning(f"Active previewer raised, trying Latent2RGB fallback: {e}") + continue + if isinstance(out, Image.Image): + pil_frames = [out] + break + elif prev is previewer: + logging.warning( + f"Preview override: {type(previewer).__name__} returned " + f"{type(out).__name__} instead of PIL.Image — falling back to Latent2RGB." + ) + + if not pil_frames: + if original_callback is not None: + original_callback(step, x0, x, total_steps_) + return + + pil_first = pil_frames[0] + if pil_first.mode != "RGB": + pil_first = pil_first.convert("RGB") + pil_frames[0] = pil_first + # Consumed by GetPreviewOverrideFramesKJ. + self.frames.append(pil_first) + + if node_id is not None and PromptServer is not None: + # x0_view (not x0) so LTX keyframe padding doesn't dampen the Δ norm. + x0_cpu_now = x0_view.detach().float().cpu() + prev_x0_cpu = state["last_x0_cpu"] + state["last_x0_cpu"] = x0_cpu_now + + now = time.perf_counter() + step_ms = None + if state["last_time"] is not None: + step_ms = (now - state["last_time"]) * 1000.0 + w = state["step_ms_window"] + w.append(step_ms) + if len(w) > 8: + w.pop(0) + state["last_time"] = now + avg_step_ms = (sum(state["step_ms_window"]) / len(state["step_ms_window"])) if state["step_ms_window"] else None + sigma_val = sigmas_list[step] if 0 <= step < len(sigmas_list) else None + sent_step = step + 1 + + def _encode_and_send( + pil_frames=pil_frames, x0_cpu_now=x0_cpu_now, prev_x0_cpu=prev_x0_cpu, + step_ms=step_ms, avg_step_ms=avg_step_ms, sigma_val=sigma_val, + sent_step=sent_step, total_steps_=total_steps_, + ): + if len(pil_frames) > 1: + # NVENC ~8x faster + ~5x smaller than PIL WebP when available. + b64, w_, h_, mime = None, 0, 0, None + if _NVENC_AVAILABLE: + b64, w_, h_ = _encode_mp4_nvenc(pil_frames, anim_fps, max_res) + if b64: + mime = "video/mp4" + if not b64: + b64, w_, h_ = _encode_animated_webp(pil_frames, anim_fps, quality, max_res) + mime = "image/webp" + else: + pil_send = pil_frames[0] + if max_res and max_res > 0 and (pil_send.width > max_res or pil_send.height > max_res): + pil_send = ImageOps.contain(pil_send, (max_res, max_res), Image.LANCZOS) + buf = pyio.BytesIO() + pil_send.save(buf, format="JPEG", quality=quality) + b64 = base64.b64encode(buf.getvalue()).decode("ascii") + w_, h_ = pil_send.width, pil_send.height + mime = "image/jpeg" + + if not b64: + return + + delta_v = None + if prev_x0_cpu is not None and prev_x0_cpu.shape == x0_cpu_now.shape: + diff = x0_cpu_now - prev_x0_cpu + delta_v = (diff.norm() / max(1, diff.numel()) ** 0.5).item() + + PromptServer.instance.send_sync( + "kj_preview_override", + { + "node_id": node_id, + "image": b64, + "mime": mime, + "w": w_, + "h": h_, + "step": sent_step, + "total": total_steps_, + "sigma": sigma_val, + "sigmas": None, + "delta": delta_v, + "step_ms": step_ms, + "avg_step_ms": avg_step_ms, + "fps": anim_fps if mime in ("video/mp4", "image/webp") else None, + }, + PromptServer.instance.client_id, + ) + + encoder.submit(_encode_and_send) + except Exception as e: + logging.warning(f"Preview override failed: {e}") + if original_callback is not None: + original_callback(step, x0, x, total_steps_) + + # Patch every concrete decode_latent_to_preview_image — subclasses like VHS's + # WrappedPreviewer override it and would otherwise still emit previews of their own. + prev_methods = [] + if self.suppress_default: + targets = [latent_preview.LatentPreviewer] + stack = list(latent_preview.LatentPreviewer.__subclasses__()) + while stack: + cls = stack.pop() + targets.append(cls) + stack.extend(cls.__subclasses__()) + for cls in targets: + if "decode_latent_to_preview_image" in cls.__dict__: + prev_methods.append((cls, cls.__dict__["decode_latent_to_preview_image"])) + cls.decode_latent_to_preview_image = _suppressed_preview_image + try: + # Seeds step 1's duration measurement (sampling-start → end of step 1). + state["last_time"] = time.perf_counter() + return executor(noise, latent_image, sampler, sigmas, denoise_mask, new_callback, disable_pbar, seed, latent_shapes=latent_shapes) + finally: + encoder.shutdown(drain_timeout=5.0) + for cls, prev in prev_methods: + cls.decode_latent_to_preview_image = prev + if vae_restore_device is not None and self.vae is not None: + try: + self.vae.first_stage_model.to(vae_restore_device) + except Exception: + pass + + +class ModelPreviewOverrideKJ(io.ComfyNode): + @classmethod + def define_schema(cls) -> io.Schema: + return io.Schema( + node_id="ModelPreviewOverrideKJ", + display_name="Model Preview Override", + category="KJNodes/sampling", + description=( + "Adds a dedicated live-preview frame on this node, with overridable max resolution. " + "Default ComfyUI preview caps at 512px; this node sends its own preview straight to a " + "DOM widget on the node so pixel-space models (Chroma Radiance, ZImage, HiDream-O1, …) " + "can be previewed at full sampler resolution." + ), + inputs=[ + io.Model.Input("model", tooltip="Model to attach the preview override to."), + io.Int.Input( + "max_resolution", + default=1024, + min=0, + max=8192, + step=8, + tooltip="Max preview side in pixels for the live widget. 0 = full sampler resolution (no downscale).", + ), + io.Int.Input( + "jpeg_quality", + default=80, + min=30, + max=100, + step=1, + tooltip="JPEG quality for the live preview transport.", + ), + io.Boolean.Input( + "suppress_default_preview", + default=True, + tooltip="Suppress the standard sampler-node preview overlay while sampling, so only this node's frame updates. Progress bar still advances normally.", + ), + io.Int.Input( + "preview_frames", + default=1, + min=1, + max=1024, + step=1, + tooltip="Frames to sample from each video step's latent for animated preview. " + "1 = single frame (current behavior, fastest). >1 = animated WebP playing back at preview_fps. " + "Only applies to video models (5D latents); ignored for image models.", + ), + io.Int.Input( + "preview_fps", + default=12, + min=1, + max=60, + step=1, + tooltip="Playback FPS for the animated WebP preview. Ignored when preview_frames=1.", + ), + io.Vae.Input( + "vae", + optional=True, + tooltip="Optional LTX VAE for true-RGB previews. TAEHV-LTX = fast tiny decode " + "(VAE pinned to GPU). Any other LTX VAE = full-quality decode via " + "vae.decode() — MUCH slower per step.", + ), + io.Combo.Input( + "tiny_vae", + options=["none"] + folder_paths.get_filename_list("vae_approx"), + default="none", + optional=True, + tooltip="Tiny VAE decoder from models/vae_approx for true-RGB previews. " + "Overrides Latent2RGB and the 'vae' input.", + ), + ], + outputs=[io.Model.Output(tooltip="Model with preview override attached.")], + hidden=[io.Hidden.unique_id], + is_experimental=True, + ) + + @classmethod + def execute(cls, model, max_resolution, jpeg_quality, suppress_default_preview, preview_frames, preview_fps, vae=None, tiny_vae="none") -> io.NodeOutput: + m = model.clone() + m.add_wrapper_with_key( + comfy.patcher_extension.WrappersMP.OUTER_SAMPLE, + "kj_preview_override", + _PreviewOverrideWrapper( + max_resolution, cls.hidden.unique_id, jpeg_quality, suppress_default_preview, + preview_frames, preview_fps, vae, tiny_vae, + ), + ) + return io.NodeOutput(m) + + +class GetPreviewOverrideFramesKJ(io.ComfyNode): + @classmethod + def define_schema(cls) -> io.Schema: + return io.Schema( + node_id="GetPreviewOverrideFramesKJ", + display_name="Get Preview Override Frames", + category="KJNodes/sampling", + description=( + "Returns the frames captured by Model Preview Override during the most recent sampling. " + "Wire 'model' from Model Preview Override (the same one feeding the sampler) and 'after_sample' " + "from after the sampler (LATENT/IMAGE) to enforce correct execution order." + ), + inputs=[ + io.Model.Input("model", tooltip="The model output by Model Preview Override (used to locate the captured frames)."), + io.MultiType.Input( + "after_sample", + [io.Latent, io.Image], + tooltip="Anything from after the sampler (LATENT or IMAGE). The value is ignored — it just forces this node to run after sampling.", + ), + ], + outputs=[io.Image.Output(display_name="frames")], + is_experimental=True, + ) + + @classmethod + def execute(cls, model, after_sample) -> io.NodeOutput: + wrappers = model.get_wrappers(comfy.patcher_extension.WrappersMP.OUTER_SAMPLE, "kj_preview_override") + if not wrappers: + raise RuntimeError("Get Preview Override Frames: no Model Preview Override wrapper found on this model.") + frames = wrappers[-1].frames + if not frames: + raise RuntimeError("Get Preview Override Frames: no frames captured. Ensure the sampler ran with this model.") + tensors = [] + for pil in frames: + arr = np.asarray(pil, dtype=np.float32) / 255.0 + tensors.append(torch.from_numpy(arr)) + return io.NodeOutput(torch.stack(tensors, dim=0)) diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/sharpen_nodes.py b/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/sharpen_nodes.py new file mode 100644 index 0000000000000000000000000000000000000000..dad2e8ce6c0bdae0511648eeb3dfeab20aa53bdc --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/sharpen_nodes.py @@ -0,0 +1,259 @@ +import torch +import torch.nn.functional as F + +from comfy import model_management +from comfy_api.latest import io + +class ImageSharpenKJ(io.ComfyNode): + @classmethod + def define_schema(cls): + return io.Schema( + node_id="ImageSharpenKJ", + category="KJNodes/image", + display_name="Image Sharpen KJ", + search_aliases=["sharpen", "unsharp mask", "deconvolution", "cas", "rcas", "high-pass", "postprocessing"], + description="""GPU-accelerated image sharpening with multiple methods. + +**RCAS** — AMD's Robust Contrast-Adaptive Sharpening (from FSR). +Single 5-tap cross filter that adapts to local contrast. +Minimal artifacts, good for general use with little tuning. + +**Adaptive USM** — Unsharp mask with local variance modulation. +Sharpens detail-rich areas more, flat/noisy areas less. +More controllable than RCAS via radius and threshold parameters. + +**High-Pass** — Extracts high-frequency detail and blends it back. +Gives a "clarity" enhancement feel. Uses radius to control detail scale. + +**Deconvolution** — Richardson-Lucy iterative deconvolution. +Can recover actual lost detail from blur, not just enhance edges. +Uses radius as the estimated blur kernel and iterations to control convergence.""", + inputs=[ + io.MatchType.Input("image", io.MatchType.Template("img_or_mask", [io.Image, io.Mask])), + io.DynamicCombo.Input("method", options=[ + io.DynamicCombo.Option(key="rcas", inputs=[ + io.Float.Input("strength", default=0.8, min=0.0, max=1.0, step=0.01, + tooltip="0 = no sharpening, 1 = full RCAS sharpening."), + ]), + io.DynamicCombo.Option(key="adaptive_usm", inputs=[ + io.Float.Input("strength", default=0.5, min=0.0, max=3.0, step=0.01, + tooltip="Sharpening multiplier. Values above 1.0 give aggressive sharpening."), + io.Float.Input("radius", default=1.0, min=0.5, max=5.0, step=0.1, + tooltip="Gaussian blur sigma for the unsharp mask. Larger = enhances coarser detail."), + io.Float.Input("threshold", default=0.05, min=0.0, max=1.0, step=0.01, + tooltip="Noise gate. Higher = only sharpen areas with more texture/detail. 0 = sharpen everything."), + ]), + io.DynamicCombo.Option(key="high_pass", inputs=[ + io.Float.Input("strength", default=0.5, min=0.0, max=3.0, step=0.01, + tooltip="Blend factor for high-frequency detail. Values above 1.0 give a punchier effect."), + io.Float.Input("radius", default=1.0, min=0.5, max=5.0, step=0.1, + tooltip="Gaussian blur sigma defining the frequency cutoff. Larger = enhances coarser detail."), + ]), + io.DynamicCombo.Option(key="deconvolution", inputs=[ + io.Float.Input("strength", default=0.5, min=0.0, max=1.0, step=0.01, + tooltip="Blend between original (0) and fully deconvolved (1)."), + io.Float.Input("radius", default=1.0, min=0.5, max=5.0, step=0.1, + tooltip="Sigma of the assumed Gaussian blur to reverse."), + io.Int.Input("iterations", default=10, min=1, max=100, step=1, + tooltip="Richardson-Lucy iterations. More = sharper but slower, diminishing returns past ~20."), + ]), + ]), + ], + outputs=[ + io.MatchType.Output(io.MatchType.Template("img_or_mask", [io.Image, io.Mask]), display_name="output"), + ], + ) + + @classmethod + def execute(cls, image, method) -> io.NodeOutput: + selected = method["method"] + strength = method.get("strength", 0.5) + if strength == 0: + return io.NodeOutput(image) + + is_mask = image.ndim == 3 # BHW + if is_mask: + image = image.unsqueeze(-1) # BHW -> BHWC with C=1 + + radius = method.get("radius", 1.0) + threshold = method.get("threshold", 0.05) + iterations = method.get("iterations", 10) + if selected == "rcas": + result = _rcas(image, strength) + elif selected == "adaptive_usm": + result = _adaptive_usm(image, strength, radius, threshold) + elif selected == "high_pass": + result = _high_pass(image, strength, radius) + else: + result = _deconvolution(image, strength, radius, iterations) + + if is_mask: + result = result.squeeze(-1) # BHWC -> BHW + + return io.NodeOutput(result) + + +def _rcas(image: torch.Tensor, strength: float) -> torch.Tensor: + """AMD FidelityFX RCAS — 5-tap cross filter with contrast-adaptive lobe.""" + device = model_management.get_torch_device() + intermediate_device = model_management.intermediate_device() + dtype = model_management.intermediate_dtype() + + B, H, W, C = image.shape + out = torch.empty(B, H, W, C, device=intermediate_device, dtype=dtype) + attenuation = strength + + for i in range(B): + img = image[i:i+1].to(device=device, dtype=dtype).permute(0, 3, 1, 2) + padded = F.pad(img, (1, 1, 1, 1), mode='reflect') + + n = padded[:, :, 0:H, 1:W+1] + s = padded[:, :, 2:H+2, 1:W+1] + w = padded[:, :, 1:H+1, 0:W] + e = padded[:, :, 1:H+1, 2:W+2] + center = img + + mn = torch.min(torch.min(torch.min(torch.min(n, s), w), e), center) + mx = torch.max(torch.max(torch.max(torch.max(n, s), w), e), center) + + # hitMin = -mn / (4*mx), hitMax = -(1-mx) / (4*(1-mn)) + hit_min = mn.div(mx * 4.0 + 1e-6).neg_() + hit_max = mx.neg().add_(1.0).div_(mn.neg().add_(1.0).mul_(4.0).add_(1e-6)).neg_() + + torch.max(hit_min, hit_max, out=hit_min) + lobe = hit_min.min(dim=1, keepdim=True).values + + lobe.mul_(attenuation) + lobe.clamp_(-0.1875, 0.0) + + # (center + lobe*(n+s+e+w)) / (1 + 4*lobe) + norm = lobe.mul(4.0).add_(1.0).reciprocal_() + neighbors = n + s + neighbors.add_(e).add_(w) + neighbors.mul_(lobe).add_(center).mul_(norm) + neighbors.clamp_(0.0, 1.0) + + out[i:i+1] = neighbors.permute(0, 2, 3, 1).to(device=intermediate_device) + + return out + + +def _gaussian_kernel_1d(sigma: float, device: torch.device, dtype: torch.dtype) -> torch.Tensor: + radius = max(int(3.0 * sigma + 0.5), 1) + x = torch.arange(-radius, radius + 1, device=device, dtype=dtype) + kernel = torch.exp(-0.5 * (x / sigma) ** 2) + kernel.div_(kernel.sum()) + return kernel + + +def _gaussian_blur(img: torch.Tensor, sigma: float) -> torch.Tensor: + """Separable Gaussian blur on BCHW tensor.""" + kernel_1d = _gaussian_kernel_1d(sigma, img.device, img.dtype) + k = kernel_1d.shape[0] + C = img.shape[1] + pad = k // 2 + + kh = kernel_1d.view(1, 1, 1, k).expand(C, -1, -1, -1) + blurred = F.conv2d(F.pad(img, (pad, pad, 0, 0), mode='reflect'), kh, groups=C) + + kv = kernel_1d.view(1, 1, k, 1).expand(C, -1, -1, -1) + blurred = F.conv2d(F.pad(blurred, (0, 0, pad, pad), mode='reflect'), kv, groups=C) + + return blurred + + +def _adaptive_usm(image: torch.Tensor, strength: float, radius: float, threshold: float) -> torch.Tensor: + """Unsharp mask modulated by local variance — sharpens texture, skips flat areas.""" + device = model_management.get_torch_device() + intermediate_device = model_management.intermediate_device() + dtype = model_management.intermediate_dtype() + + B, H, W, C = image.shape + out = torch.empty(B, H, W, C, device=intermediate_device, dtype=dtype) + var_sigma = radius * 1.5 + + for i in range(B): + img = image[i:i+1].to(device=device, dtype=dtype, copy=True).permute(0, 3, 1, 2) + + blurred = _gaussian_blur(img, radius) + detail = img - blurred + + # Local variance: E[x^2] - E[x]^2 + local_mean = _gaussian_blur(img, var_sigma) + local_mean_sq = _gaussian_blur(img.square(), var_sigma) + local_mean_sq.sub_(local_mean * local_mean).clamp_(min=0.0) + + # Convert to std deviation so threshold is in pixel-intensity scale + modulation = local_mean_sq.mean(dim=1, keepdim=True).sqrt_() + + if threshold > 0: + modulation.div_(threshold).clamp_(0.0, 1.0) + else: + modulation.fill_(1.0) + + img.addcmul_(detail, modulation, value=strength).clamp_(0.0, 1.0) + + out[i:i+1] = img.permute(0, 2, 3, 1).to(device=intermediate_device) + + return out + + +def _high_pass(image: torch.Tensor, strength: float, radius: float) -> torch.Tensor: + """High-pass sharpening: original + strength * (original - blur).""" + device = model_management.get_torch_device() + intermediate_device = model_management.intermediate_device() + dtype = model_management.intermediate_dtype() + + B, H, W, C = image.shape + out = torch.empty(B, H, W, C, device=intermediate_device, dtype=dtype) + + for i in range(B): + img = image[i:i+1].to(device=device, dtype=dtype).permute(0, 3, 1, 2) + + low_pass = _gaussian_blur(img, radius) + low_pass.neg_().add_(img).mul_(strength).add_(img).clamp_(0.0, 1.0) + + out[i:i+1] = low_pass.permute(0, 2, 3, 1).to(device=intermediate_device) + + return out + + +def _deconvolution(image: torch.Tensor, strength: float, radius: float, iterations: int) -> torch.Tensor: + """Richardson-Lucy deconvolution with Gaussian PSF.""" + device = model_management.get_torch_device() + intermediate_device = model_management.intermediate_device() + dtype = model_management.intermediate_dtype() + + B, H, W, C = image.shape + out = torch.empty(B, H, W, C, device=intermediate_device, dtype=dtype) + + # Gaussian PSF kernel (symmetric, so PSF^T = PSF) + kernel_1d = _gaussian_kernel_1d(radius, device, dtype) + k = kernel_1d.shape[0] + pad = k // 2 + kh = kernel_1d.view(1, 1, 1, k).expand(C, -1, -1, -1) + kv = kernel_1d.view(1, 1, k, 1).expand(C, -1, -1, -1) + + for i in range(B): + img = image[i:i+1].to(device=device, dtype=dtype).permute(0, 3, 1, 2) + estimate = img.clone() + + for _ in range(iterations): + # estimate *= (observed / (estimate * PSF)) * PSF^T + blurred = F.conv2d(F.pad(estimate, (pad, pad, 0, 0), mode='reflect'), kh, groups=C) + blurred = F.conv2d(F.pad(blurred, (0, 0, pad, pad), mode='reflect'), kv, groups=C) + + blurred.add_(1e-6) + torch.div(img, blurred, out=blurred) + + correction = F.conv2d(F.pad(blurred, (pad, pad, 0, 0), mode='reflect'), kh, groups=C) + correction = F.conv2d(F.pad(correction, (0, 0, pad, pad), mode='reflect'), kv, groups=C) + + estimate.mul_(correction) + + torch.lerp(img, estimate, strength, out=estimate) + estimate.clamp_(0.0, 1.0) + + out[i:i+1] = estimate.permute(0, 2, 3, 1).to(device=intermediate_device) + + return out diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/tiny_vae.py b/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/tiny_vae.py new file mode 100644 index 0000000000000000000000000000000000000000..3452fb951b7b9d69750011be0b9852b84237d664 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/tiny_vae.py @@ -0,0 +1,100 @@ +"""Loader for TAESD-family tiny VAE decoders that ComfyUI core can't build yet. + +Core's comfy.taesd.taesd.Decoder hardcodes a 64-wide stack with 3 upsample stages, so +checkpoints that differ fail to load — e.g. taeh3 (MiniMax H3): 24 latent channels, 96 +wide for the first half, 4 upsample stages for the VAE's 16x spatial ratio. + +These decoders are a flat nn.Sequential whose state dict keys are the positional module +indices, so the exact architecture is recoverable from the checkpoint: indices carrying +`N.conv.0.weight` are Blocks, indices carrying `N.weight` are convs (bias-less ones sit +right after an upsample), and the gaps are the parameterless modules. +""" + +import logging + +import torch +import torch.nn as nn + +import comfy.model_management +import comfy.utils +from comfy.taesd.taesd import Block, Clamp, conv + + +def build_tae_decoder(sd): + by_index = {} + for k, v in sd.items(): + head, _, rest = k.partition(".") + if not head.isdigit(): + raise ValueError(f"not a flat TAE decoder state dict (unexpected key '{k}')") + by_index.setdefault(int(head), {})[rest] = v + + modules = [] + for i in range(max(by_index) + 1): + entry = by_index.get(i) + if entry is None: + # index 0 is the input Clamp, 2 the ReLU after the input conv, the rest are upsamples + modules.append(Clamp() if i == 0 else nn.ReLU() if i == 2 else nn.Upsample(scale_factor=2)) + elif "conv.0.weight" in entry: + w = entry["conv.0.weight"] + # only pass the kwarg when it's needed — older ComfyUI has no midblock-GN variant + if "pool.0.weight" in entry: + modules.append(Block(w.shape[1], w.shape[0], use_midblock_gn=True)) + else: + modules.append(Block(w.shape[1], w.shape[0])) + elif "weight" in entry: + w = entry["weight"] + modules.append(conv(w.shape[1], w.shape[0], bias="bias" in entry)) + else: + raise ValueError(f"unrecognized TAE decoder module at index {i}: {sorted(entry)}") + return nn.Sequential(*modules) + + +class TinyVAEDecoder: + """Decode-only tiny VAE. Output is [0, 1], matching the TAE family's convention.""" + + def __init__(self, sd, device=None, dtype=None): + # keys may carry a "taesd_decoder."/"decoder." prefix; strip whatever is common + prefix = "" + first = next(iter(sd)) + if not first.split(".")[0].isdigit(): + prefix = first.split(".")[0] + "." + sd = {k[len(prefix):]: v for k, v in sd.items() if k.startswith(prefix)} + + self.device = device if device is not None else comfy.model_management.vae_device() + self.dtype = dtype if dtype is not None else comfy.model_management.vae_dtype( + self.device, [torch.float16, torch.bfloat16]) + self.model = build_tae_decoder(sd) + self.model.load_state_dict(sd) + self.model.eval().to(device=self.device, dtype=self.dtype) + + self.latent_channels = self.model[1].weight.shape[1] + self.upscale_ratio = 2 ** sum(isinstance(m, nn.Upsample) for m in self.model) + + def decode(self, latent): + """[B, C, H, W] -> [B, 3, H*ratio, W*ratio], float32 on the input device.""" + out = self.model(latent.to(device=self.device, dtype=self.dtype)) + return out.to(device=latent.device, dtype=torch.float32) + + def decode_video(self, latent, frame_indices=None): + """[B, C, T, H, W] -> [T, H*ratio, W*ratio, 3]. Decodes one frame at a time — + at 16x the full-resolution activations are the memory peak, not the weights.""" + x = latent[0] + indices = range(x.shape[1]) if frame_indices is None else frame_indices + frames = [self.decode(x[:, t].unsqueeze(0))[0].movedim(0, -1) for t in indices] + return torch.stack(frames, dim=0) + + +def load_tiny_vae_decoder(name, device=None, dtype=None): + """Load by vae_approx filename. Returns None (and logs) if it can't be used.""" + import folder_paths + + path = folder_paths.get_full_path("vae_approx", name) + if path is None: + logging.warning(f"[KJ TinyVAE] '{name}' not found in models/vae_approx.") + return None + try: + sd = comfy.utils.load_torch_file(path, safe_load=True) + return TinyVAEDecoder(sd, device=device, dtype=dtype) + except Exception as e: + logging.warning(f"[KJ TinyVAE] Could not load '{name}': {e}") + return None diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/triton_vae.py b/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/triton_vae.py new file mode 100644 index 0000000000000000000000000000000000000000..d92f4146d4b470a06b9cd1083b6131a7f08f6ef3 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-KJNodes/nodes/triton_vae.py @@ -0,0 +1,794 @@ +"""Triton VAE decode/encode kernels: fused norm+SiLU and optional int8 convs. + +Supported (auto-detected): Wan (RMSNorm), KL/Flux2/SDXL/SD1.5 (GroupNorm) +and LTXV/LTX2 (PixelNorm). int8 covers Wan, flux-family KL and LTX decoders. +""" +import torch +import torch.nn as nn +import torch.nn.functional as F +try: + import triton + import triton.language as tl +except ImportError as e: + raise ImportError("PatchTritonVAE requires triton (pip install triton, or triton-windows)") from e + +from comfy.ldm.wan.vae import RMS_norm, CausalConv3d +from comfy.ldm.modules.diffusionmodules.model import ResnetBlock +from comfy.ldm.lightricks.vae.pixel_norm import PixelNorm +from comfy.ldm.lightricks.vae.causal_conv3d import CausalConv3d as LTXCausalConv3d +from comfy.ldm.lightricks.vae.causal_video_autoencoder import ResnetBlock3D as LTXResnetBlock3D, Encoder as LTXEncoder, Decoder as LTXDecoder + +import os +import copy +import logging + +import comfy.ops + +PROFILE = os.environ.get("KJNODES_TRITON_PROFILE", "") == "1" +CL3D = torch.channels_last_3d + +import comfy.model_patcher +from comfy.patcher_extension import CallbacksMP +from comfy_api.latest import io + +@triton.jit +def _rms_silu_kernel(x_ptr, g_ptr, out_ptr, C, S, stride_b, eps, scale, rcount, BLOCK_S: tl.constexpr, BLOCK_C: tl.constexpr, + HAS_GAMMA: tl.constexpr = True, SILU: tl.constexpr = True, EPS_INSIDE: tl.constexpr = False): + pid_s = tl.program_id(0) + pid_b = tl.program_id(1) + offs_s = pid_s * BLOCK_S + tl.arange(0, BLOCK_S) + mask_s = offs_s < S + # int64 offsets: C*S per batch can exceed 2^31 at high resolutions + base = x_ptr + pid_b.to(tl.int64) * stride_b + acc = tl.zeros([BLOCK_S], dtype=tl.float32) + for c0 in range(0, C, BLOCK_C): + offs_c = c0 + tl.arange(0, BLOCK_C) + mask_c = offs_c < C + ptrs = base + offs_c[:, None].to(tl.int64) * S + offs_s[None, :] + v = tl.load(ptrs, mask=mask_c[:, None] & mask_s[None, :], other=0.0).to(tl.float32) + acc += tl.sum(v * v, axis=0) + if EPS_INSIDE: # pixel_norm: 1/sqrt(mean(x^2) + eps) + inv = scale / tl.sqrt(acc * rcount + eps) + else: + inv = scale / tl.maximum(tl.sqrt(acc), eps) + out_base = out_ptr + pid_b.to(tl.int64) * stride_b + for c0 in range(0, C, BLOCK_C): + offs_c = c0 + tl.arange(0, BLOCK_C) + mask_c = offs_c < C + m = mask_c[:, None] & mask_s[None, :] + ptrs = base + offs_c[:, None].to(tl.int64) * S + offs_s[None, :] + v = tl.load(ptrs, mask=m, other=0.0).to(tl.float32) + y = v * inv[None, :] + if HAS_GAMMA: + g = tl.load(g_ptr + offs_c, mask=mask_c, other=0.0).to(tl.float32) + y = y * g[:, None] + if SILU: + y = y * tl.sigmoid(y) + tl.store(out_base + offs_c[:, None].to(tl.int64) * S + offs_s[None, :], y.to(out_ptr.dtype.element_ty), mask=m) + + +@triton.jit +def _rms_silu_cl_kernel(x_ptr, g_ptr, out_ptr, amax_ptr, C, ROWS, eps, scale, rcount, BLOCK_S: tl.constexpr, BLOCK_C: tl.constexpr, + HAS_GAMMA: tl.constexpr = True, SILU: tl.constexpr = True, EPS_INSIDE: tl.constexpr = False, + AMAX: tl.constexpr = False): + pid = tl.program_id(0) + offs_s = pid * BLOCK_S + tl.arange(0, BLOCK_S) + offs_c = tl.arange(0, BLOCK_C) + mask_s = offs_s < ROWS + mask_c = offs_c < C + m = mask_s[:, None] & mask_c[None, :] + # int64 offsets: ROWS*C (= numel) can exceed 2^31 at high resolutions + ptrs = x_ptr + offs_s[:, None].to(tl.int64) * C + offs_c[None, :] + v = tl.load(ptrs, mask=m, other=0.0).to(tl.float32) + acc = tl.sum(v * v, axis=1) + if EPS_INSIDE: + inv = scale / tl.sqrt(acc * rcount + eps) + else: + inv = scale / tl.maximum(tl.sqrt(acc), eps) + y = v * inv[:, None] + if HAS_GAMMA: + g = tl.load(g_ptr + offs_c, mask=mask_c, other=0.0).to(tl.float32) + y = y * g[None, :] + if SILU: + y = y * tl.sigmoid(y) + if AMAX: + # free global amax for downstream int8 quantization + tl.atomic_max(amax_ptr, tl.max(tl.max(tl.abs(y), axis=1), axis=0)) + tl.store(out_ptr + offs_s[:, None].to(tl.int64) * C + offs_c[None, :], y.to(out_ptr.dtype.element_ty), mask=m) + + +@triton.jit +def _gn_stats_cl(x_ptr, sum_ptr, sumsq_ptr, S, + G: tl.constexpr, BLOCK_S: tl.constexpr, BLOCK_C: tl.constexpr): + pid = tl.program_id(0) + b = tl.program_id(1) + offs_c = tl.arange(0, BLOCK_C) + acc1 = tl.zeros([G], dtype=tl.float32) + acc2 = tl.zeros([G], dtype=tl.float32) + s0 = pid * BLOCK_S + step = tl.num_programs(0) * BLOCK_S + while s0 < S: + offs_s = s0 + tl.arange(0, BLOCK_S) + m = (offs_s < S)[:, None] + # int64 offsets: (B*S)*C can exceed 2^31 at high resolutions + ptr = x_ptr + (b.to(tl.int64) * S + offs_s[:, None]) * BLOCK_C + offs_c[None, :] + v = tl.load(ptr, mask=m, other=0.0).to(tl.float32) + v3 = tl.reshape(v, (BLOCK_S, G, BLOCK_C // G)) + acc1 += tl.sum(tl.sum(v3, axis=2), axis=0) + acc2 += tl.sum(tl.sum(v3 * v3, axis=2), axis=0) + s0 += step + tl.atomic_add(sum_ptr + b * G + tl.arange(0, G), acc1) + tl.atomic_add(sumsq_ptr + b * G + tl.arange(0, G), acc2) + + +@triton.jit +def _gn_silu_apply_cl(x_ptr, sum_ptr, sumsq_ptr, w_ptr, bias_ptr, out_ptr, amax_ptr, S, count, eps, + G: tl.constexpr, BLOCK_S: tl.constexpr, BLOCK_C: tl.constexpr, SILU: tl.constexpr, + AMAX: tl.constexpr = False): + pid = tl.program_id(0) + b = tl.program_id(1) + CS: tl.constexpr = BLOCK_C // G + offs_s = pid * BLOCK_S + tl.arange(0, BLOCK_S) + offs_c = tl.arange(0, BLOCK_C) + m = (offs_s < S)[:, None] + ptr = x_ptr + (b.to(tl.int64) * S + offs_s[:, None]) * BLOCK_C + offs_c[None, :] + v = tl.load(ptr, mask=m, other=0.0).to(tl.float32) + mean_g = tl.load(sum_ptr + b * G + tl.arange(0, G)) / count + var_g = tl.load(sumsq_ptr + b * G + tl.arange(0, G)) / count - mean_g * mean_g + var_g = tl.maximum(var_g, 0.0) # E[x²]-mean² can go slightly negative from fp32 cancellation + rstd_g = 1.0 / tl.sqrt(var_g + eps) + mean_c = tl.reshape(tl.broadcast_to(mean_g[:, None], (G, CS)), (BLOCK_C,)) + rstd_c = tl.reshape(tl.broadcast_to(rstd_g[:, None], (G, CS)), (BLOCK_C,)) + w = tl.load(w_ptr + offs_c).to(tl.float32) + bias = tl.load(bias_ptr + offs_c).to(tl.float32) + y = (v - mean_c[None, :]) * rstd_c[None, :] * w[None, :] + bias[None, :] + if SILU: + y = y * tl.sigmoid(y) + if AMAX: + # free global amax for downstream int8 quantization + masked = tl.where(m, tl.abs(y), 0.0) + tl.atomic_max(amax_ptr, tl.max(tl.max(masked, axis=1), axis=0)) + tl.store(out_ptr + (b.to(tl.int64) * S + offs_s[:, None]) * BLOCK_C + offs_c[None, :], + y.to(out_ptr.dtype.element_ty), mask=m) + + +# autotuned variants: benchmark configs once per shape key, cache the fastest +_rms_silu_kernel_tuned = triton.autotune( + configs=[triton.Config({"BLOCK_S": bs, "BLOCK_C": bc}, num_warps=w) + for bs in (128, 256, 512) for bc in (32, 64) for w in (4, 8)], + key=["C", "S"])(_rms_silu_kernel) + +_rms_silu_cl_kernel_tuned = triton.autotune( + configs=[triton.Config({"BLOCK_S": bs}, num_warps=w) for bs in (4, 8, 16, 32, 64) for w in (4, 8)], + key=["C", "ROWS"])(_rms_silu_cl_kernel) + +_gn_stats_cl_tuned = triton.autotune( + configs=[triton.Config({"BLOCK_S": bs}, num_warps=w) for bs in (8, 16, 32, 64) for w in (4, 8)], + key=["S"], reset_to_zero=["sum_ptr", "sumsq_ptr"])(_gn_stats_cl) + +_gn_silu_apply_cl_tuned = triton.autotune( + configs=[triton.Config({"BLOCK_S": bs}, num_warps=w) for bs in (8, 16, 32, 64) for w in (4, 8)], + key=["S", "count"])(_gn_silu_apply_cl) + + +def fused_rms_silu(x, gamma, scale, eps=1e-12, autotune=False, silu=True, eps_inside=False): + B, C = x.shape[0], x.shape[1] + S = x.numel() // (B * C) + x = x.contiguous() + out = torch.empty_like(x) + flags = {"HAS_GAMMA": gamma is not None, "SILU": silu, "EPS_INSIDE": eps_inside} + g = gamma if gamma is not None else x + if autotune: + grid = lambda meta: (triton.cdiv(S, meta["BLOCK_S"]), B) + _rms_silu_kernel_tuned[grid](x, g, out, C, S, C * S, eps, scale, 1.0 / C, **flags) + else: + _rms_silu_kernel[(triton.cdiv(S, 256), B)](x, g, out, C, S, C * S, eps, scale, 1.0 / C, + BLOCK_S=256, BLOCK_C=32, num_warps=8, **flags) + return out + + +def fused_rms_silu_cl(x, gamma, scale, eps=1e-12, autotune=False, silu=True, eps_inside=False, return_amax=False): + C = x.shape[1] + rows = x.numel() // C + out = torch.empty_like(x) + BLOCK_C = triton.next_power_of_2(C) + flags = {"HAS_GAMMA": gamma is not None, "SILU": silu, "EPS_INSIDE": eps_inside, "AMAX": return_amax} + g = gamma if gamma is not None else x + amax = torch.zeros(1, device=x.device, dtype=torch.float32) if return_amax else out + if autotune: + grid = lambda meta: (triton.cdiv(rows, meta["BLOCK_S"]),) + _rms_silu_cl_kernel_tuned[grid](x, g, out, amax, C, rows, eps, scale, 1.0 / C, BLOCK_C=BLOCK_C, **flags) + else: + BLOCK_S = max(1, 4096 // BLOCK_C) + _rms_silu_cl_kernel[(triton.cdiv(rows, BLOCK_S),)](x, g, out, amax, C, rows, eps, scale, 1.0 / C, + BLOCK_S=BLOCK_S, BLOCK_C=BLOCK_C, num_warps=4, **flags) + if return_amax: + return out, amax[0] + return out + + +def fused_gn_silu_cl(x, weight, bias, groups, eps, silu=True, autotune=False, return_amax=False): + B, C, H, W = x.shape + S = H * W + sums = torch.zeros(2, B * groups, device=x.device, dtype=torch.float32) + out = torch.empty_like(x) + count = S * (C // groups) + amax = torch.zeros(1, device=x.device, dtype=torch.float32) if return_amax else out + if autotune: + # program count beyond ~1024 only adds same-address atomic contention + grid_stats = lambda meta: (min(1024, triton.cdiv(S, meta["BLOCK_S"])), B) + _gn_stats_cl_tuned[grid_stats](x, sums[0], sums[1], S, G=groups, BLOCK_C=C) + grid_apply = lambda meta: (triton.cdiv(S, meta["BLOCK_S"]), B) + _gn_silu_apply_cl_tuned[grid_apply](x, sums[0], sums[1], weight, bias, out, amax, S, count, eps, + G=groups, BLOCK_C=C, SILU=silu, AMAX=return_amax) + else: + BLOCK_S = max(1, 8192 // C) + nprog = min(1024, triton.cdiv(S, BLOCK_S)) + _gn_stats_cl[(nprog, B)](x, sums[0], sums[1], S, G=groups, BLOCK_S=BLOCK_S, BLOCK_C=C, num_warps=8) + _gn_silu_apply_cl[(triton.cdiv(S, BLOCK_S), B)](x, sums[0], sums[1], weight, bias, out, amax, S, count, eps, + G=groups, BLOCK_S=BLOCK_S, BLOCK_C=C, SILU=silu, AMAX=return_amax, num_warps=8) + if return_amax: + return out, amax[0] + return out + + +class FusedRMSSiLU(nn.Module): + emit_amax = False # lets the downstream int8 conv skip its amax pass + + def __init__(self, rms, autotune=False): + super().__init__() + self.gamma = rms.gamma + self.scale = rms.scale + self.autotune = autotune + + def forward(self, x): + if not x.is_cuda: + return F.silu(F.normalize(x, dim=1) * self.scale * self.gamma.to(x)) + gamma = self.gamma + if gamma.device != x.device: # lowvram partial load keeps params offloaded on cpu + gamma = gamma.to(x.device) + gamma = gamma.reshape(-1) + if not gamma.is_contiguous(): + gamma = gamma.contiguous() + if x.ndim == 5 and x.is_contiguous(memory_format=torch.channels_last_3d) and not x.is_contiguous(): + if self.emit_amax: + out, amax = fused_rms_silu_cl(x, gamma, self.scale, autotune=self.autotune, return_amax=True) + out._kj_amax = amax + return out + return fused_rms_silu_cl(x, gamma, self.scale, autotune=self.autotune) + return fused_rms_silu(x, gamma, self.scale, autotune=self.autotune) + + +class FusedPixelNorm(nn.Module): + def __init__(self, pn, silu=True, autotune=False): + super().__init__() + self.eps = pn.eps + self.silu = silu + self.autotune = autotune + + def forward(self, x): + if x.is_cuda: + if x.ndim == 5 and x.is_contiguous(memory_format=torch.channels_last_3d) and not x.is_contiguous(): + return fused_rms_silu_cl(x, None, 1.0, self.eps, autotune=self.autotune, silu=self.silu, eps_inside=True) + return fused_rms_silu(x, None, 1.0, self.eps, autotune=self.autotune, silu=self.silu, eps_inside=True) + out = x / torch.sqrt(torch.mean(x ** 2, dim=1, keepdim=True) + self.eps) + if self.silu: + out = F.silu(out) + return out + + +class FusedGNSiLU(nn.Module): + emit_amax = False # lets the downstream int8 conv skip its amax pass + + def __init__(self, gn, silu=True, autotune=False): + super().__init__() + self.weight = gn.weight + self.bias = gn.bias + self.num_groups = gn.num_groups + self.eps = gn.eps + self.silu = silu + self.autotune = autotune + + def forward(self, x): + C = x.shape[1] + G = self.num_groups + weight, bias = self.weight, self.bias + if weight.device != x.device: # lowvram partial load keeps params offloaded on cpu + weight = weight.to(x.device) + bias = bias.to(x.device) + if x.is_cuda and x.ndim == 4 and (C & (C - 1)) == 0 and (G & (G - 1)) == 0 and C % G == 0 \ + and x.is_contiguous(memory_format=torch.channels_last) and not x.is_contiguous(): + if self.emit_amax and self.silu: + out, amax = fused_gn_silu_cl(x, weight, bias, self.num_groups, self.eps, self.silu, + autotune=self.autotune, return_amax=True) + out._kj_amax = amax + return out + return fused_gn_silu_cl(x, weight, bias, self.num_groups, self.eps, self.silu, + autotune=self.autotune) + out = F.group_norm(x, self.num_groups, weight, bias, self.eps) + if self.silu: + out = F.silu(out) + return out + + +def convert_conv_layout(model, channels_last=True): + for mod in model.modules(): + if isinstance(mod, nn.Conv3d): + mod.to(memory_format=torch.channels_last_3d if channels_last else torch.contiguous_format) + elif isinstance(mod, nn.Conv2d): + mod.to(memory_format=torch.channels_last if channels_last else torch.contiguous_format) + + +def build_object_patches(model, autotune=False): + """Norm fusion object patches; matches already-fused modules for rebuilds.""" + patches = {} + for name, mod in model.named_modules(): + if isinstance(mod, nn.Sequential): # Wan-style RMS_norm+SiLU pairs + for i in range(len(mod) - 1): + if isinstance(mod[i], RMS_norm) and isinstance(mod[i + 1], nn.SiLU) \ + and mod[i].gamma.ndim == 4 and mod[i].bias is None: + patches[f"{name}.{i}"] = FusedRMSSiLU(mod[i], autotune=autotune) + patches[f"{name}.{i + 1}"] = nn.Identity() + elif isinstance(mod[i], FusedRMSSiLU) and isinstance(mod[i + 1], nn.Identity): + mod[i].autotune = autotune + patches[f"{name}.{i}"] = mod[i] + patches[f"{name}.{i + 1}"] = mod[i + 1] + elif isinstance(mod, ResnetBlock) and not hasattr(mod, "temb_proj"): # KL-style + for norm_name in ("norm1", "norm2"): + norm = getattr(mod, norm_name) + if isinstance(norm, nn.GroupNorm): + patches[f"{name}.{norm_name}"] = FusedGNSiLU(norm, autotune=autotune) + elif isinstance(norm, FusedGNSiLU): + norm.autotune = autotune + patches[f"{name}.{norm_name}"] = norm + patches[f"{name}.swish"] = nn.Identity() + elif name.endswith("norm_out"): # KL encoder/decoder head, SiLU applied separately + if isinstance(mod, nn.GroupNorm): + patches[name] = FusedGNSiLU(mod, silu=False, autotune=autotune) + elif isinstance(mod, FusedGNSiLU): + mod.autotune = autotune + patches[name] = mod + elif isinstance(mod, LTXResnetBlock3D): + # conditioned decoder blocks apply a scale-shift between norm and SiLU + fuse_silu = not mod.timestep_conditioning + for norm_name in ("norm1", "norm2"): + norm = getattr(mod, norm_name) + if isinstance(norm, PixelNorm) and norm.dim == 1: + patches[f"{name}.{norm_name}"] = FusedPixelNorm(norm, silu=fuse_silu, autotune=autotune) + elif isinstance(norm, FusedPixelNorm): + norm.autotune = autotune + patches[f"{name}.{norm_name}"] = norm + if fuse_silu and f"{name}.norm1" in patches: + patches[f"{name}.non_linearity"] = nn.Identity() + elif isinstance(mod, (LTXEncoder, LTXDecoder)): # LTX head norm + conv_act + fuse_silu = isinstance(mod, LTXEncoder) or not mod.timestep_conditioning + if isinstance(mod.conv_norm_out, PixelNorm) and mod.conv_norm_out.dim == 1: + patches[f"{name}.conv_norm_out"] = FusedPixelNorm(mod.conv_norm_out, silu=fuse_silu, autotune=autotune) + elif isinstance(mod.conv_norm_out, FusedPixelNorm): + mod.conv_norm_out.autotune = autotune + patches[f"{name}.conv_norm_out"] = mod.conv_norm_out + if fuse_silu and f"{name}.conv_norm_out" in patches: + patches[f"{name}.conv_act"] = nn.Identity() + return patches + + +# int8 (W8A8) convolutions: implicit GEMM on int8 tensor cores + +@triton.jit +def _quant_pad_s8(x_ptr, c_ptr, out_ptr, amax_ptr, C, C_PAD, S_PAD, S_CACHE, NTOT, BLOCK: tl.constexpr): + # one-pass padded int8 buffer: [zero frames | cache | payload], channels zero-extended to C_PAD + pid = tl.program_id(0) + offs = pid * BLOCK + tl.arange(0, BLOCK) + m = offs < NTOT + inv = 127.0 / tl.maximum(tl.load(amax_ptr).to(tl.float32), 1e-8) + s_idx = offs // C_PAD + c = offs % C_PAD + valid = m & (c < C) + in_c = valid & (s_idx >= S_PAD) & (s_idx < S_PAD + S_CACHE) + in_x = valid & (s_idx >= S_PAD + S_CACHE) + v = tl.load(x_ptr + (s_idx - S_PAD - S_CACHE) * C + c, mask=in_x, other=0.0).to(tl.float32) + v += tl.load(c_ptr + (s_idx - S_PAD) * C + c, mask=in_c, other=0.0).to(tl.float32) + v = v * inv + v = tl.minimum(tl.maximum(v + tl.where(v >= 0, 0.5, -0.5), -127.0), 127.0) + tl.store(out_ptr + offs, v.to(tl.int8), mask=m) + + +@triton.jit +def _conv3d_s8(x_ptr, w_ptr, out_ptr, scale_ptr, bias_ptr, + H, W, C, C_out, C3, M, + BLOCK_M: tl.constexpr, BLOCK_N: tl.constexpr, BLOCK_K: tl.constexpr, + HAS_BIAS: tl.constexpr): + # the three dw taps of each (dt, dh) group are one contiguous 3C span in NDHWC + pid_m = tl.program_id(0) + pid_n = tl.program_id(1) + offs_m = pid_m * BLOCK_M + tl.arange(0, BLOCK_M) + offs_n = pid_n * BLOCK_N + tl.arange(0, BLOCK_N) + mask_m = offs_m < M + mask_n = offs_n < C_out + + w_o = offs_m % W + h_o = (offs_m // W) % H + t_o = offs_m // (W * H) + + acc = tl.zeros((BLOCK_M, BLOCK_N), dtype=tl.int32) + for g in tl.static_range(9): + dt = g // 3 + dh = g % 3 + t_i = t_o + dt + h_i = h_o + dh - 1 + h_ok = mask_m & (h_i >= 0) & (h_i < H) + base = ((t_i * H + h_i) * W + (w_o - 1)) * C + for k0 in range(0, C3, BLOCK_K): + dw = k0 // C # padded C is a multiple of BLOCK_K, one dw tap per chunk + m_ok = h_ok & (w_o + dw - 1 >= 0) & (w_o + dw - 1 < W) + offs_k = k0 + tl.arange(0, BLOCK_K) + a = tl.load(x_ptr + base[:, None] + offs_k[None, :], mask=m_ok[:, None], other=0) + b = tl.load(w_ptr + (g * C3 + offs_k)[:, None] * C_out + offs_n[None, :], + mask=mask_n[None, :], other=0) + acc = tl.dot(a, b, acc, out_dtype=tl.int32) + + y = acc.to(tl.float32) * tl.load(scale_ptr + offs_n, mask=mask_n, other=0.0) + if HAS_BIAS: + y += tl.load(bias_ptr + offs_n, mask=mask_n, other=0.0).to(tl.float32)[None, :] + tl.store(out_ptr + offs_m[:, None] * C_out + offs_n[None, :], + y.to(out_ptr.dtype.element_ty), mask=mask_m[:, None] & mask_n[None, :]) + + +@triton.jit +def _conv2d_s8(x_ptr, w_ptr, out_ptr, scale_ptr, bias_ptr, + H, W, C, C_out, C3, M, + BLOCK_M: tl.constexpr, BLOCK_N: tl.constexpr, BLOCK_K: tl.constexpr, + HAS_BIAS: tl.constexpr): + # 2D (3x3, pad 1) variant of _conv3d_s8: 3 dh groups, batch-aware rows + pid_m = tl.program_id(0) + pid_n = tl.program_id(1) + offs_m = pid_m * BLOCK_M + tl.arange(0, BLOCK_M) + offs_n = pid_n * BLOCK_N + tl.arange(0, BLOCK_N) + mask_m = offs_m < M + mask_n = offs_n < C_out + + w_o = offs_m % W + h_o = (offs_m // W) % H + b = offs_m // (W * H) + + acc = tl.zeros((BLOCK_M, BLOCK_N), dtype=tl.int32) + for g in tl.static_range(3): + h_i = h_o + g - 1 + h_ok = mask_m & (h_i >= 0) & (h_i < H) + base = ((b * H + h_i) * W + (w_o - 1)) * C + for k0 in range(0, C3, BLOCK_K): + dw = k0 // C + m_ok = h_ok & (w_o + dw - 1 >= 0) & (w_o + dw - 1 < W) + offs_k = k0 + tl.arange(0, BLOCK_K) + a = tl.load(x_ptr + base[:, None] + offs_k[None, :], mask=m_ok[:, None], other=0) + wb = tl.load(w_ptr + (g * C3 + offs_k)[:, None] * C_out + offs_n[None, :], + mask=mask_n[None, :], other=0) + acc = tl.dot(a, wb, acc, out_dtype=tl.int32) + + y = acc.to(tl.float32) * tl.load(scale_ptr + offs_n, mask=mask_n, other=0.0) + if HAS_BIAS: + y += tl.load(bias_ptr + offs_n, mask=mask_n, other=0.0).to(tl.float32)[None, :] + tl.store(out_ptr + offs_m[:, None] * C_out + offs_n[None, :], + y.to(out_ptr.dtype.element_ty), mask=mask_m[:, None] & mask_n[None, :]) + + +def _pack_int8_weight(weight, spatial_dims): + sw = (weight.detach().abs().amax(dim=tuple(range(1, weight.ndim)), keepdim=False).clamp(min=1e-8).float() / 127.0) + shape = [-1] + [1] * (weight.ndim - 1) + qw = (weight.detach().float() / sw.reshape(shape)).round_().clamp_(-127, 127).to(torch.int8) + c_in = weight.shape[1] + # zero-pad channels to keep every K chunk dw-aligned + c_pad = ((c_in + 63) // 64) * 64 + if c_pad != c_in: + pad = [0, 0] * spatial_dims + [0, c_pad - c_in] + qw = torch.nn.functional.pad(qw, pad) + perm = tuple(range(2, weight.ndim)) + (1, 0) + return qw.permute(*perm).reshape(-1, weight.shape[0]).contiguous(), sw, c_pad + + +class Int8CausalConv3d(CausalConv3d): + """int8 Wan CausalConv3d; subclass keeps isinstance cache routing and shares the weight Parameter.""" + + _logged = set() + _prof = [] + + def __init__(self, orig): + super().__init__(orig.in_channels, orig.out_channels, orig.kernel_size, + stride=orig.stride, padding=1) + self.weight = orig.weight + self.bias = orig.bias + qw, sw, self.int8_cpad = _pack_int8_weight(orig.weight, spatial_dims=3) + self.register_buffer("int8_qw", qw, persistent=False) + self.register_buffer("int8_sw", sw * (1.0 / 127.0), persistent=False) + self._last_amax = None + + def forward(self, x, cache_x=None, cache_list=None, cache_idx=None): + if not x.is_cuda or x.shape[0] != 1: + reason = "non-cuda input" if not x.is_cuda else f"batch size {x.shape[0]}" + if reason not in Int8CausalConv3d._logged: + Int8CausalConv3d._logged.add(reason) + logging.info(f"PatchTritonVAE int8: falling back to bf16 conv ({reason})") + return super().forward(x, cache_x=cache_x, cache_list=cache_list, cache_idx=cache_idx) + if "active" not in Int8CausalConv3d._logged: + Int8CausalConv3d._logged.add("active") + logging.info("PatchTritonVAE int8: int8 conv kernel path active") + if PROFILE: + ev0 = torch.cuda.Event(enable_timing=True) + ev1 = torch.cuda.Event(enable_timing=True) + ev0.record() + out = self._int8_forward(x, cache_x, cache_list, cache_idx) + ev1.record() + Int8CausalConv3d._prof.append((ev0, ev1)) + if len(Int8CausalConv3d._prof) >= 250: + torch.cuda.synchronize() + total = sum(a.elapsed_time(b) for a, b in Int8CausalConv3d._prof) + logging.info(f"PatchTritonVAE int8 profile: {len(Int8CausalConv3d._prof)} conv calls, {total:.0f} ms total") + Int8CausalConv3d._prof.clear() + return out + return self._int8_forward(x, cache_x, cache_list, cache_idx) + + def _int8_forward(self, x, cache_x, cache_list, cache_idx): + if cache_list is not None: + cache_x = cache_list[cache_idx] + cache_list[cache_idx] = None + + amax = getattr(x, "_kj_amax", None) + if not x.is_contiguous(memory_format=CL3D) or x.is_contiguous(): + x = x.contiguous(memory_format=CL3D) + B, C, T, H, W = x.shape + ct = 0 if cache_x is None else cache_x.shape[2] + pad = max(0, 2 - ct) + T_in = T + ct + pad + + if amax is None: + amax = x.abs().amax() + if ct and self._last_amax is not None: + # cache frames are bounded by the previous payload amax + amax = torch.maximum(amax, self._last_amax) + self._last_amax = amax.detach() + + xr = x.permute(0, 2, 3, 4, 1).reshape(-1) + if ct: + if not cache_x.is_contiguous(memory_format=CL3D) or cache_x.is_contiguous(): + cache_x = cache_x.contiguous(memory_format=CL3D) + cr = cache_x.permute(0, 2, 3, 4, 1).reshape(-1) + else: + cr = xr + C_pad = self.int8_cpad + hw = H * W + n_tot = T_in * hw * C_pad + qx = torch.empty(n_tot, device=x.device, dtype=torch.int8) + _quant_pad_s8[(triton.cdiv(n_tot, 4096),)](xr, cr, qx, amax, C, C_pad, pad * hw, ct * hw, n_tot, + BLOCK=4096, num_warps=8) + + C_out = self.out_channels + scale_vec = self.int8_sw * amax.float() + bias = self.bias + T_out = T_in - 2 + M = T_out * hw + out = torch.empty(M, C_out, device=x.device, dtype=x.dtype) + grid = (triton.cdiv(M, 128), triton.cdiv(C_out, 64)) + _conv3d_s8[grid](qx, self.int8_qw, out, scale_vec, + bias.float() if bias is not None else scale_vec, + H, W, C_pad, C_out, 3 * C_pad, M, + BLOCK_M=128, BLOCK_N=64, BLOCK_K=64, + HAS_BIAS=bias is not None, num_warps=4, num_stages=4) + return out.view(1, T_out, H, W, C_out).permute(0, 4, 1, 2, 3) + + +class Int8Conv2d(comfy.ops.disable_weight_init.Conv2d): + """int8 3x3 Conv2d for KL VAE ResnetBlocks; shares the weight Parameter.""" + + def __init__(self, orig): + super().__init__(orig.in_channels, orig.out_channels, orig.kernel_size, + stride=orig.stride, padding=orig.padding) + self.weight = orig.weight + self.bias = orig.bias + qw, sw, self.int8_cpad = _pack_int8_weight(orig.weight, spatial_dims=2) + self.register_buffer("int8_qw", qw, persistent=False) + self.register_buffer("int8_sw", sw * (1.0 / 127.0), persistent=False) + + def forward(self, x): + if not x.is_cuda: + return super().forward(x) + amax = getattr(x, "_kj_amax", None) + if not x.is_contiguous(memory_format=torch.channels_last) or x.is_contiguous(): + x = x.contiguous(memory_format=torch.channels_last) + B, C, H, W = x.shape + if amax is None: + amax = x.abs().amax() + xr = x.permute(0, 2, 3, 1).reshape(-1) + + C_pad = self.int8_cpad + n_tot = B * H * W * C_pad + qx = torch.empty(n_tot, device=x.device, dtype=torch.int8) + _quant_pad_s8[(triton.cdiv(n_tot, 4096),)](xr, xr, qx, amax, C, C_pad, 0, 0, n_tot, + BLOCK=4096, num_warps=8) + + C_out = self.out_channels + scale_vec = self.int8_sw * amax.float() + bias = self.bias + M = B * H * W + out = torch.empty(M, C_out, device=x.device, dtype=x.dtype) + grid = (triton.cdiv(M, 128), triton.cdiv(C_out, 64)) + _conv2d_s8[grid](qx, self.int8_qw, out, scale_vec, + bias.float() if bias is not None else scale_vec, + H, W, C_pad, C_out, 3 * C_pad, M, + BLOCK_M=128, BLOCK_N=64, BLOCK_K=64, + HAS_BIAS=bias is not None, num_warps=4, num_stages=4) + return out.view(B, H, W, C_out).permute(0, 3, 1, 2) + + +class Int8InnerConv3d(comfy.ops.disable_weight_init.Conv3d): + """int8 replacement for the plain Conv3d inside the LTX CausalConv3d wrapper.""" + + def __init__(self, orig): + super().__init__(orig.in_channels, orig.out_channels, orig.kernel_size, + stride=orig.stride, padding=orig.padding) + self.weight = orig.weight + self.bias = orig.bias + qw, sw, self.int8_cpad = _pack_int8_weight(orig.weight, spatial_dims=3) + self.register_buffer("int8_qw", qw, persistent=False) + self.register_buffer("int8_sw", sw * (1.0 / 127.0), persistent=False) + + def forward(self, x): + if not x.is_cuda or x.shape[0] != 1 or x.shape[2] < 3: + return super().forward(x) + if not x.is_contiguous(memory_format=CL3D) or x.is_contiguous(): + x = x.contiguous(memory_format=CL3D) + B, C, T_in, H, W = x.shape + amax = x.abs().amax() + xr = x.permute(0, 2, 3, 4, 1).reshape(-1) + + C_pad = self.int8_cpad + n_tot = T_in * H * W * C_pad + qx = torch.empty(n_tot, device=x.device, dtype=torch.int8) + _quant_pad_s8[(triton.cdiv(n_tot, 4096),)](xr, xr, qx, amax, C, C_pad, 0, 0, n_tot, + BLOCK=4096, num_warps=8) + + C_out = self.out_channels + scale_vec = self.int8_sw * amax.float() + bias = self.bias + T_out = T_in - 2 + M = T_out * H * W + out = torch.empty(M, C_out, device=x.device, dtype=x.dtype) + grid = (triton.cdiv(M, 128), triton.cdiv(C_out, 64)) + _conv3d_s8[grid](qx, self.int8_qw, out, scale_vec, + bias.float() if bias is not None else scale_vec, + H, W, C_pad, C_out, 3 * C_pad, M, + BLOCK_M=128, BLOCK_N=64, BLOCK_K=64, + HAS_BIAS=bias is not None, num_warps=4, num_stages=4) + return out.view(1, T_out, H, W, C_out).permute(0, 4, 1, 2, 3) + + +def _ltx_inner_eligible(conv): + return isinstance(conv, torch.nn.Conv3d) and tuple(conv.kernel_size) == (3, 3, 3) \ + and tuple(conv.stride) == (1, 1, 1) and tuple(conv.dilation) == (1, 1, 1) \ + and tuple(conv.padding) == (0, 1, 1) and conv.padding_mode == "zeros" \ + and conv.in_channels >= 64 and conv.in_channels % 32 == 0 and conv.out_channels >= 64 + + +def build_int8_patches(model): + # SD1.5/SDXL activation ranges break per-tensor int8; flux-family KL is fine + kl_ok = getattr(getattr(getattr(model, "decoder", None), "conv_in", None), "in_channels", 0) >= 16 + kl_skipped = False + patches = {} + for name, mod in model.named_modules(): + if not name.startswith("decoder."): + continue + if isinstance(mod, Int8CausalConv3d): + patches[name] = mod + elif isinstance(mod, CausalConv3d) and tuple(mod.kernel_size) == (3, 3, 3) \ + and tuple(mod.stride) == (1, 1, 1) and mod.in_channels >= 64 \ + and mod.in_channels % 32 == 0 and mod.out_channels >= 64: + patches[name] = Int8CausalConv3d(mod) + elif isinstance(mod, ResnetBlock): # KL image VAEs + if not kl_ok: + kl_skipped = True + continue + for conv_name in ("conv1", "conv2"): + conv = getattr(mod, conv_name) + if isinstance(conv, Int8Conv2d): + patches[f"{name}.{conv_name}"] = conv + elif isinstance(conv, torch.nn.Conv2d) and tuple(conv.kernel_size) == (3, 3) \ + and tuple(conv.stride) == (1, 1) and conv.in_channels >= 64 \ + and conv.out_channels >= 64: + patches[f"{name}.{conv_name}"] = Int8Conv2d(conv) + elif isinstance(mod, LTXResnetBlock3D): # LTXV/LTX2 + for conv_name in ("conv1", "conv2"): + wrapper = getattr(mod, conv_name) + if not isinstance(wrapper, LTXCausalConv3d): + continue + inner = wrapper.conv + if isinstance(inner, Int8InnerConv3d): + patches[f"{name}.{conv_name}.conv"] = inner + elif _ltx_inner_eligible(inner): + patches[f"{name}.{conv_name}.conv"] = Int8InnerConv3d(inner) + if kl_skipped: + logging.info("int8_conv: skipping SD1.5/SDXL-class VAE (4ch latent), int8 quality is insufficient there") + return patches + + +class PatchTritonVAE(io.ComfyNode): + @classmethod + def define_schema(cls): + return io.Schema( + node_id="PatchTritonVAE", + display_name="Patch Triton VAE", + category="KJNodes/experimental", + is_experimental=True, + description="Speeds up VAE decode/encode with fused Triton norm+SiLU kernels and channels_last conv layout. " + "Supported VAEs (auto-detected): Wan 2.1/2.2 video VAEs incl. Qwen-Image (RMSNorm, ~1.4x/1.15x), KL image VAEs " + "such as Flux/Flux2, SDXL and SD1.5 (GroupNorm, ~1.6-1.8x at 2048px), and LTXV/LTX2 video VAEs (PixelNorm; " + "timestep-conditioned decoder blocks get norm-only fusion). Other architectures are not supported. " + "Applied as object patches on a cloned patcher, so it only exists while this VAE is loaded.", + inputs=[ + io.Vae.Input("vae"), + io.Boolean.Input("fuse_norm_silu", default=True, tooltip="Replace norm+SiLU chains (RMSNorm for Wan, GroupNorm for KL VAEs) with fused Triton kernels (single pass, fp32 accumulation). Requires triton."), + io.Boolean.Input("channels_last", default=True, tooltip="Convert conv weights to channels_last memory format, removing cuDNN layout transposes around every conv. Required for the fused GroupNorm kernel to engage on KL VAEs."), + io.Boolean.Input("int8_conv", default=False, tooltip="EXPERIMENTAL: run the VAE decoder's 3x3 convolutions on int8 tensor cores (4x the bf16 rate on Ada+). Weights quantized per-out-channel, activations dynamically per-tensor; ~45-48 dB vs the bf16 decode, minor quality loss possible. Wan 2.1/2.2 and KL image VAEs (Flux2/SDXL/SD1.5), ignored for others."), + io.Boolean.Input("autotune", default=False, tooltip="Benchmark several kernel block-size configs on first use of each tensor shape and cache the fastest. Brief stutter per new resolution, usually a few percent faster after warmup."), + ], + outputs=[ + io.Vae.Output(display_name="vae"), + ], + ) + + @classmethod + def execute(cls, vae, fuse_norm_silu=True, channels_last=True, int8_conv=False, autotune=False) -> io.NodeOutput: + vae = copy.copy(vae) + model = vae.first_stage_model + if vae.patcher.is_dynamic(): + # dynamic-vram rematerializes weights per forward, discarding channels_last + model.to(vae.vae_dtype) + new_patcher = comfy.model_patcher.ModelPatcher( + model, load_device=vae.patcher.load_device, offload_device=vae.patcher.offload_device) + # parentless patchers get flagged is_dead on GC (leak warning + full gc) + new_patcher.parent = vae.patcher + vae.patcher = new_patcher + + def clear_dynamic_cast_flags(patcher, device_to, lowvram_model_memory, force_patch_weights, full_load): + # dynamic loads of the shared model set these without ever resetting them + for mod in patcher.model.modules(): + if hasattr(mod, "comfy_cast_weights") and not hasattr(mod, "prev_comfy_cast_weights"): + mod.comfy_cast_weights = False + mod._v_signature = None + vae.patcher.add_callback_with_key(CallbacksMP.ON_LOAD, "wan_vae_fused_clear_cast", clear_dynamic_cast_flags) + else: + vae.patcher = vae.patcher.clone() + + if fuse_norm_silu: + patches = build_object_patches(model, autotune=autotune) + if not patches: + raise RuntimeError("No fusable norm layers found, this node supports Wan video VAEs, KL image VAEs (Flux2/SDXL/SD1.5) and LTXV/LTX2 video VAEs") + for name, obj in patches.items(): + vae.patcher.add_object_patch(name, obj) + logging.info(f"PatchTritonVAE: registered {len(patches)} fused norm object patches") + + int8_applied = False + if int8_conv: + int8_patches = build_int8_patches(model) + if int8_patches: + for name, obj in int8_patches.items(): + vae.patcher.add_object_patch(name, obj) + int8_applied = True + logging.info(f"PatchTritonVAE: registered {len(int8_patches)} int8 conv object patches") + if fuse_norm_silu: + # fused norms hand their amax to the int8 convs + for obj in vae.patcher.object_patches.values(): + if isinstance(obj, (FusedRMSSiLU, FusedGNSiLU)): + obj.emit_amax = True + else: + logging.warning("PatchTritonVAE: int8_conv enabled but no eligible conv layers found (Wan video VAEs and flux-family KL VAEs only), skipping") + + if channels_last or int8_applied: + if int8_applied and not channels_last: + logging.info("PatchTritonVAE: channels_last enforced, the int8 conv path requires it") + vae.disable_offload = True + # reapply per load: weight staging restores the original contiguous layout + def reapply_channels_last(patcher, device_to, lowvram_model_memory, force_patch_weights, full_load): + convert_conv_layout(patcher.model, channels_last=True) + vae.patcher.add_callback_with_key(CallbacksMP.ON_LOAD, "wan_vae_fused_channels_last", reapply_channels_last) + else: + convert_conv_layout(model, channels_last=False) + return io.NodeOutput(vae) diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/pyproject.toml b/ComfyUI/custom_nodes/ComfyUI-KJNodes/pyproject.toml new file mode 100644 index 0000000000000000000000000000000000000000..5367cf832f5b1a4061fbc241cdc11e14e429545d --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-KJNodes/pyproject.toml @@ -0,0 +1,15 @@ +[project] +name = "comfyui-kjnodes" +description = "Various quality of life -nodes for ComfyUI, mostly just visual stuff to improve usability." +version = "1.4.9" +license = {file = "LICENSE"} +dependencies = ["pillow>=10.3.0", "color-matcher", "matplotlib"] + +[project.urls] +Repository = "https://github.com/kijai/ComfyUI-KJNodes" +# Used by Comfy Registry https://comfyregistry.org + +[tool.comfy] +PublisherId = "kijai" +DisplayName = "ComfyUI-KJNodes" +Icon = "https://avatars.githubusercontent.com/u/40791699" diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/requirements.txt b/ComfyUI/custom_nodes/ComfyUI-KJNodes/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..791fc4c5f2930db3e6f81d9256b7f91457b9d4d8 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-KJNodes/requirements.txt @@ -0,0 +1,5 @@ +pillow>=10.3.0 +color-matcher +matplotlib +mss +opencv-python-headless diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/utility/__pycache__/utility.cpython-312.pyc b/ComfyUI/custom_nodes/ComfyUI-KJNodes/utility/__pycache__/utility.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..44c19ab9deb615e38c5060898ecc7cd04e695e79 Binary files /dev/null and b/ComfyUI/custom_nodes/ComfyUI-KJNodes/utility/__pycache__/utility.cpython-312.pyc differ diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/utility/fluid.py b/ComfyUI/custom_nodes/ComfyUI-KJNodes/utility/fluid.py new file mode 100644 index 0000000000000000000000000000000000000000..c0691987f5249a031ecbb74329ba513d5788b691 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-KJNodes/utility/fluid.py @@ -0,0 +1,67 @@ +import numpy as np +from scipy.ndimage import map_coordinates, spline_filter +from scipy.sparse.linalg import factorized + +from .numerical import difference, operator + + +class Fluid: + def __init__(self, shape, *quantities, pressure_order=1, advect_order=3): + self.shape = shape + self.dimensions = len(shape) + + # Prototyping is simplified by dynamically + # creating advected quantities as needed. + self.quantities = quantities + for q in quantities: + setattr(self, q, np.zeros(shape)) + + self.indices = np.indices(shape) + self.velocity = np.zeros((self.dimensions, *shape)) + + laplacian = operator(shape, difference(2, pressure_order)) + self.pressure_solver = factorized(laplacian) + + self.advect_order = advect_order + + def step(self): + # Advection is computed backwards in time as described in Stable Fluids. + advection_map = self.indices - self.velocity + + # SciPy's spline filter introduces checkerboard divergence. + # A linear blend of the filtered and unfiltered fields based + # on some value epsilon eliminates this error. + def advect(field, filter_epsilon=10e-2, mode='constant'): + filtered = spline_filter(field, order=self.advect_order, mode=mode) + field = filtered * (1 - filter_epsilon) + field * filter_epsilon + return map_coordinates(field, advection_map, prefilter=False, order=self.advect_order, mode=mode) + + # Apply advection to each axis of the + # velocity field and each user-defined quantity. + for d in range(self.dimensions): + self.velocity[d] = advect(self.velocity[d]) + + for q in self.quantities: + setattr(self, q, advect(getattr(self, q))) + + # Compute the jacobian at each point in the + # velocity field to extract curl and divergence. + jacobian_shape = (self.dimensions,) * 2 + partials = tuple(np.gradient(d) for d in self.velocity) + jacobian = np.stack(partials).reshape(*jacobian_shape, *self.shape) + + divergence = jacobian.trace() + + # If this curl calculation is extended to 3D, the y-axis value must be negated. + # This corresponds to the coefficients of the levi-civita symbol in that dimension. + # Higher dimensions do not have a vector -> scalar, or vector -> vector, + # correspondence between velocity and curl due to differing isomorphisms + # between exterior powers in dimensions != 2 or 3 respectively. + curl_mask = np.triu(np.ones(jacobian_shape, dtype=bool), k=1) + curl = (jacobian[curl_mask] - jacobian[curl_mask.T]).squeeze() + + # Apply the pressure correction to the fluid's velocity field. + pressure = self.pressure_solver(divergence.flatten()).reshape(self.shape) + self.velocity -= np.gradient(pressure) + + return divergence, curl, pressure \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/utility/magictex.py b/ComfyUI/custom_nodes/ComfyUI-KJNodes/utility/magictex.py new file mode 100644 index 0000000000000000000000000000000000000000..e6d426f7deb3deb977604dd37581eb4e9fe9e6a9 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-KJNodes/utility/magictex.py @@ -0,0 +1,95 @@ +"""Generates psychedelic color textures in the spirit of Blender's magic texture shader using Python/Numpy + +https://github.com/cheind/magic-texture +""" +from typing import Tuple, Optional +import numpy as np + + +def coordinate_grid(shape: Tuple[int, int], dtype=np.float32): + """Returns a three-dimensional coordinate grid of given shape for use in `magic`.""" + x = np.linspace(-1, 1, shape[1], endpoint=True, dtype=dtype) + y = np.linspace(-1, 1, shape[0], endpoint=True, dtype=dtype) + X, Y = np.meshgrid(x, y) + XYZ = np.stack((X, Y, np.ones_like(X)), -1) + return XYZ + + +def random_transform(coords: np.ndarray, rng: np.random.Generator = None): + """Returns randomly transformed coordinates""" + H, W = coords.shape[:2] + rng = rng or np.random.default_rng() + m = rng.uniform(-1.0, 1.0, size=(3, 3)).astype(coords.dtype) + return (coords.reshape(-1, 3) @ m.T).reshape(H, W, 3) + + +def magic( + coords: np.ndarray, + depth: Optional[int] = None, + distortion: Optional[int] = None, + rng: np.random.Generator = None, +): + """Returns color magic color texture. + + The implementation is based on Blender's (https://www.blender.org/) magic + texture shader. The following adaptions have been made: + - we exchange the nested if-cascade by a probabilistic iterative approach + + Kwargs + ------ + coords: HxWx3 array + Coordinates transformed into colors by this method. See + `magictex.coordinate_grid` to generate the default. + depth: int (optional) + Number of transformations applied. Higher numbers lead to more + nested patterns. If not specified, randomly sampled. + distortion: float (optional) + Distortion of patterns. Larger values indicate more distortion, + lower values tend to generate smoother patterns. If not specified, + randomly sampled. + rng: np.random.Generator + Optional random generator to draw samples from. + + Returns + ------- + colors: HxWx3 array + Three channel color image in range [0,1] + """ + rng = rng or np.random.default_rng() + if distortion is None: + distortion = rng.uniform(1, 4) + if depth is None: + depth = rng.integers(1, 5) + + H, W = coords.shape[:2] + XYZ = coords + x = np.sin((XYZ[..., 0] + XYZ[..., 1] + XYZ[..., 2]) * distortion) + y = np.cos((-XYZ[..., 0] + XYZ[..., 1] - XYZ[..., 2]) * distortion) + z = -np.cos((-XYZ[..., 0] - XYZ[..., 1] + XYZ[..., 2]) * distortion) + + if depth > 0: + x *= distortion + y *= distortion + z *= distortion + y = -np.cos(x - y + z) + y *= distortion + + xyz = [x, y, z] + fns = [np.cos, np.sin] + for _ in range(1, depth): + axis = rng.choice(3) + fn = fns[rng.choice(2)] + signs = rng.binomial(n=1, p=0.5, size=4) * 2 - 1 + + xyz[axis] = signs[-1] * fn( + signs[0] * xyz[0] + signs[1] * xyz[1] + signs[2] * xyz[2] + ) + xyz[axis] *= distortion + + x, y, z = xyz + x /= 2 * distortion + y /= 2 * distortion + z /= 2 * distortion + c = 0.5 - np.stack((x, y, z), -1) + np.clip(c, 0, 1.0) + return c \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/utility/numerical.py b/ComfyUI/custom_nodes/ComfyUI-KJNodes/utility/numerical.py new file mode 100644 index 0000000000000000000000000000000000000000..b5b88bc63c45d63d8913e56cbd06eb7ab413fe4f --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-KJNodes/utility/numerical.py @@ -0,0 +1,25 @@ +from functools import reduce +from itertools import cycle +from math import factorial + +import numpy as np +import scipy.sparse as sp + + +def difference(derivative, accuracy=1): + # Central differences implemented based on the article here: + # http://web.media.mit.edu/~crtaylor/calculator.html + derivative += 1 + radius = accuracy + derivative // 2 - 1 + points = range(-radius, radius + 1) + coefficients = np.linalg.inv(np.vander(points)) + return coefficients[-derivative] * factorial(derivative - 1), points + + +def operator(shape, *differences): + # Credit to Philip Zucker for figuring out + # that kronsum's argument order is reversed. + # Without that bit of wisdom I'd have lost it. + differences = zip(shape, cycle(differences)) + factors = (sp.diags(*diff, shape=(dim,) * 2) for dim, diff in differences) + return reduce(lambda a, f: sp.kronsum(f, a, format='csc'), factors) \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/utility/utility.py b/ComfyUI/custom_nodes/ComfyUI-KJNodes/utility/utility.py new file mode 100644 index 0000000000000000000000000000000000000000..fcd11f39d7a92601a4721aab695a8596736fdf51 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-KJNodes/utility/utility.py @@ -0,0 +1,130 @@ +import torch +import numpy as np +from PIL import Image, ImageColor +from typing import Union, List +import logging + +# Utility functions from mtb nodes: https://github.com/melMass/comfy_mtb +def pil2tensor(image: Union[Image.Image, List[Image.Image]]) -> torch.Tensor: + if isinstance(image, list): + return torch.cat([pil2tensor(img) for img in image], dim=0) + + return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0) + + +BBOX_TYPES = "BBOX,BOUNDING_BOX" + +def normalize_bboxes(bboxes, bbox_format="xywh") -> List[tuple]: + """Accept KJNodes BBOX tuples and/or native BOUNDING_BOX dicts, single or nested, + and return a flat list of 4-tuples in bbox_format ("xywh" or "xyxy"). + Tuples are assumed to already be in bbox_format, dicts are converted.""" + if isinstance(bboxes, (torch.Tensor, np.ndarray)): + bboxes = bboxes.tolist() + if isinstance(bboxes, dict): + bboxes = [bboxes] + elif isinstance(bboxes, (list, tuple)): + if len(bboxes) == 4 and all(isinstance(v, (int, float)) for v in bboxes): + bboxes = [bboxes] + else: + raise ValueError(f"Unsupported bbox input: {type(bboxes).__name__}") + + out = [] + for bbox in bboxes: + if isinstance(bbox, (torch.Tensor, np.ndarray)): + bbox = bbox.tolist() + if isinstance(bbox, (list, tuple)) and bbox and not all(isinstance(v, (int, float)) for v in bbox): + out.extend(normalize_bboxes(bbox, bbox_format)) # nested list, e.g. per-frame boxes + continue + if isinstance(bbox, dict): + x, y = int(bbox["x"]), int(bbox["y"]) + w, h = int(bbox["width"]), int(bbox["height"]) + out.append((x, y, x + w, y + h) if bbox_format == "xyxy" else (x, y, w, h)) + elif isinstance(bbox, (list, tuple)) and len(bbox) >= 4: + out.append(tuple(int(v) for v in bbox[:4])) + else: + raise ValueError(f"Invalid bbox: {bbox}") + return out + + +def bbox_to_bounding_box(bbox, bbox_format="xywh") -> dict: + """Convert a single KJNodes BBOX to the native BOUNDING_BOX dict.""" + a, b, c, d = normalize_bboxes(bbox, bbox_format)[0] + if bbox_format == "xyxy": + return {"x": min(a, c), "y": min(b, d), "width": abs(c - a), "height": abs(d - b)} + return {"x": a, "y": b, "width": c, "height": d} + + +def np2tensor(img_np: Union[np.ndarray, List[np.ndarray]]) -> torch.Tensor: + if isinstance(img_np, list): + return torch.cat([np2tensor(img) for img in img_np], dim=0) + + return torch.from_numpy(img_np.astype(np.float32) / 255.0).unsqueeze(0) + + +def tensor2np(tensor: torch.Tensor): + if len(tensor.shape) == 3: # Single image + return np.clip(255.0 * tensor.cpu().numpy(), 0, 255).astype(np.uint8) + else: # Batch of images + return [np.clip(255.0 * t.cpu().numpy(), 0, 255).astype(np.uint8) for t in tensor] + +def tensor2pil(image: torch.Tensor) -> List[Image.Image]: + batch_count = image.size(0) if len(image.shape) > 3 else 1 + if batch_count > 1: + out = [] + for i in range(batch_count): + out.extend(tensor2pil(image[i])) + return out + + return [ + Image.fromarray( + np.clip(255.0 * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8) + ) + ] + +def string_to_color(color_string: str) -> List[int]: + color_list = [0, 0, 0] # Default fallback (black) + + if ',' in color_string: + # Handle CSV format (e.g., "255, 0, 0" or "255, 0, 0, 128" or "1.0, 0.5, 0.0") + try: + values = [float(channel.strip()) for channel in color_string.split(',')] + # Convert to 0-255 range if values are in 0-1 range + if all(0 <= v <= 1 for v in values): + color_list = [int(v * 255) for v in values] + else: + color_list = [int(v) for v in values] + except ValueError: + logging.warning(f"Invalid color format: {color_string}. Using default black.") + elif color_string.startswith('#') or (color_string.lstrip('#').isalnum() and not color_string.lstrip('#').replace('.', '', 1).isdigit()): + # Could be Hex format or color name + color_string_stripped = color_string.lstrip('#') + # Try hex first + if len(color_string_stripped) in [6, 8] and all(c in '0123456789ABCDEFabcdef' for c in color_string_stripped): + if len(color_string_stripped) == 6: # #RRGGBB + color_list = [int(color_string_stripped[i:i+2], 16) for i in (0, 2, 4)] + elif len(color_string_stripped) == 8: # #RRGGBBAA + color_list = [int(color_string_stripped[i:i+2], 16) for i in (0, 2, 4, 6)] + else: + # Try color name (e.g., "red", "blue", "cyan") + try: + rgb = ImageColor.getrgb(color_string) + color_list = list(rgb) + except ValueError: + logging.warning(f"Invalid color name or hex format: {color_string}. Using default black.") + else: + # Handle single value (grayscale) - can be int or float + try: + value = float(color_string.strip()) + # Convert to 0-255 range if it's a float between 0-1 + if 0 <= value <= 1: + value = int(value * 255) + else: + value = int(value) + color_list = [value, value, value] + except ValueError: + logging.warning(f"Invalid color format: {color_string}. Using default black.") + + # Clip values to valid range + color_list = np.clip(color_list, 0, 255).tolist() + + return color_list diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/web/js/appearance.js b/ComfyUI/custom_nodes/ComfyUI-KJNodes/web/js/appearance.js new file mode 100644 index 0000000000000000000000000000000000000000..9069207e2ca079363e757da234318c76d24806ee --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-KJNodes/web/js/appearance.js @@ -0,0 +1,23 @@ +const { app } = window.comfyAPI.app; + +app.registerExtension({ + name: "KJNodes.appearance", + nodeCreated(node) { + switch (node.comfyClass) { + case "INTConstant": + node.setSize([200, 58]); + node.color = "#1b4669"; + node.bgcolor = "#29699c"; + break; + case "FloatConstant": + node.setSize([200, 58]); + node.color = LGraphCanvas.node_colors.green.color; + node.bgcolor = LGraphCanvas.node_colors.green.bgcolor; + break; + case "ConditioningMultiCombine": + node.color = LGraphCanvas.node_colors.brown.color; + node.bgcolor = LGraphCanvas.node_colors.brown.bgcolor; + break; + } + } +}); diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/web/js/canvas_background.js b/ComfyUI/custom_nodes/ComfyUI-KJNodes/web/js/canvas_background.js new file mode 100644 index 0000000000000000000000000000000000000000..774258a56c6feb37c6addc0f151cbb095aa50e2d --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-KJNodes/web/js/canvas_background.js @@ -0,0 +1,485 @@ +const { app } = window.comfyAPI.app; + +// ── Helpers ───────────────────────────────────────────────────────────────── + +/** Linearly mix two hex colors: 0 = pure a, 1 = pure b. */ +function mixColors(a, b, t) { + const parse = (hex) => { + if (hex.length === 4) hex = `#${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}`; + return [parseInt(hex.slice(1, 3), 16), parseInt(hex.slice(3, 5), 16), parseInt(hex.slice(5, 7), 16)]; + }; + const ca = parse(a), cb = parse(b); + const r = Math.round(ca[0] + (cb[0] - ca[0]) * t); + const g = Math.round(ca[1] + (cb[1] - ca[1]) * t); + const bl = Math.round(ca[2] + (cb[2] - ca[2]) * t); + return `#${((1 << 24) | (r << 16) | (g << 8) | bl).toString(16).slice(1)}`; +} + +/** Ensure a color string has a leading '#' (PrimeVue ColorPicker omits it). */ +function asHex(color, fallback = "#000000") { + if (!color) return fallback; + return color.startsWith("#") ? color : `#${color}`; +} + +function getSetting(id, fallback) { + return app.ui.settings.getSettingValue(id) ?? fallback; +} + +// ── Pattern tiles ───────────────────────────────────────────────────────────── +// Each pattern is described by: +// tile(size) → [tileW, tileH] the period of the pattern in graph units. +// draw(ctx, size, t, fg, bg) draws ONE seamless tile of the foreground +// motif into a context already scaled so that +// [0..tileW] × [0..tileH] graph units fill the +// whole tile bitmap. The solid background is +// painted separately, so tiles are transparent +// except where the motif is drawn. +// +// At render time we rasterise one tile into a small OffscreenCanvas sized to the +// pattern's *on-screen* pixel size for the current zoom, then fill the canvas +// with createPattern("repeat"). This is O(1) per frame (the GPU repeats the +// tile) instead of stroking the whole graph every frame, and because the tile +// is rendered at native device resolution and blitted ~1:1 + +const patternDefs = { + dots: { + tile: (size) => [size, size], + draw: (ctx, size, t, fg) => { + ctx.fillStyle = fg; + ctx.beginPath(); + ctx.arc(size / 2, size / 2, t * 2, 0, Math.PI * 2); + ctx.fill(); + }, + }, + + grid: { + tile: (size) => [size, size], + draw: (ctx, size, t, fg) => { + ctx.fillStyle = fg; + ctx.fillRect(size - t, 0, t, size - t); + ctx.fillRect(0, size - t, size, t); + }, + }, + + "cross dots": { + tile: (size) => [size, size], + draw: (ctx, size, t, fg) => { + const half = size / 2; + const arm = Math.max(t + 1, size * 0.08); + const ht = t / 2; + ctx.fillStyle = fg; + ctx.fillRect(half - arm, half - ht, arm * 2, t); + ctx.fillRect(half - ht, half - arm, t, arm * 2); + }, + }, + + blueprint: { + tile: (size) => [size, size], + draw: (ctx, size, t, fg, bg) => { + const sub = size / 4; + ctx.fillStyle = mixColors(bg, fg, 0.3); + for (let i = sub; i < size; i += sub) { + ctx.fillRect(i, 0, t, size - t); + ctx.fillRect(0, i, size, t); + } + ctx.fillStyle = fg; + ctx.fillRect(size - t, 0, t, size - t); + ctx.fillRect(0, size - t, size, t); + }, + }, + + isometric: { + tile: (size) => [size, Math.round(size / 2)], + draw: (ctx, size, t, fg) => { + const w = size; + const h = Math.round(size / 2); + ctx.strokeStyle = fg; + ctx.lineWidth = t; + ctx.beginPath(); + ctx.moveTo(0, h / 2); + ctx.lineTo(w / 2, 0); + ctx.lineTo(w, h / 2); + ctx.moveTo(0, h / 2); + ctx.lineTo(w / 2, h); + ctx.lineTo(w, h / 2); + ctx.stroke(); + }, + }, + + hexagons: { + tile: (size) => [(size / 2) * 3, Math.round((size / 2) * Math.sqrt(3))], + draw: (ctx, size, t, fg) => { + const r = size / 2; + const h = Math.round(r * Math.sqrt(3)); + ctx.strokeStyle = fg; + ctx.lineWidth = t; + const hex = (cx, cy) => { + ctx.beginPath(); + for (let i = 0; i < 6; i++) { + const a = Math.PI / 3 * i; + const px = cx + r * Math.cos(a); + const py = cy + r * Math.sin(a); + if (i === 0) ctx.moveTo(px, py); + else ctx.lineTo(px, py); + } + ctx.closePath(); + ctx.stroke(); + }; + hex(r, h / 2); + hex(r * 2.5, 0); + hex(r * 2.5, h); + }, + }, + + octagons: { + tile: (size) => [size, size], + draw: (ctx, size, t, fg) => { + const s = size / (1 + Math.SQRT2); + const d = (size - s) / 2; + ctx.strokeStyle = fg; + ctx.lineWidth = t; + ctx.beginPath(); + ctx.moveTo(d, 0); + ctx.lineTo(size - d, 0); + ctx.lineTo(size, d); + ctx.lineTo(size, size - d); + ctx.lineTo(size - d, size); + ctx.lineTo(d, size); + ctx.lineTo(0, size - d); + ctx.lineTo(0, d); + ctx.closePath(); + ctx.stroke(); + }, + }, + + waves: { + tile: (size) => [size, size], + draw: (ctx, size, t, fg, bg) => { + const lines = Math.max(2, Math.round(size / 20)); + const spacing = size / lines; + ctx.lineWidth = t; + for (let i = 0; i < lines; i++) { + const blend = 0.1 + (i % 3) * 0.07; + const amp = spacing * (0.15 + (i % 4) * 0.05); + const baseY = spacing * i + spacing / 2; + ctx.strokeStyle = mixColors(bg, fg, blend); + ctx.beginPath(); + for (let x = 0; x <= size; x++) { + const y = baseY + Math.sin((x / size) * Math.PI * 2 + i * 0.8) * amp; + if (x === 0) ctx.moveTo(x, y); + else ctx.lineTo(x, y); + } + ctx.stroke(); + } + }, + }, + + "carbon fiber": { + tile: (size) => { + const cell = Math.max(2, Math.round(size / 4)); + return [cell * 4, cell * 4]; + }, + draw: (ctx, size, t, fg, bg) => { + const cell = Math.max(2, Math.round(size / 4)); + const s = cell * 4; + // Dark cells == background, left transparent so the bg shows through. + ctx.fillStyle = mixColors(bg, fg, 0.2); + for (let row = 0; row < 4; row++) { + for (let col = 0; col < 4; col++) { + if ((col + row) % 4 < 2) ctx.fillRect(col * cell, row * cell, cell, cell); + } + } + ctx.fillStyle = mixColors(bg, fg, 0.07); + const gt = Math.max(1, Math.round(t)); + for (let i = 1; i < 4; i++) { + ctx.fillRect(0, i * cell, s, gt); + ctx.fillRect(i * cell, 0, gt, s); + } + }, + }, +}; + +// Ordered list of choices: 'default' (LiteGraph's own grid) + 'none' + patterns. +const PATTERN_KEYS = ["default", "none", ...Object.keys(patternDefs)]; + +// Below this on-screen tile size (device px) the motif is an indiscernible +// smear, so we skip it and show the plain background. +const MIN_TILE_PX = 3; +// Cap the tile bitmap. This bounds the only per-zoom-frame work (re-rendering +// one tile to match the new on-screen size); beyond this the tile is gently +// upscaled by the pattern fill (few, large tiles when zoomed in → fine). +const MAX_TILE_PX = 256; + +// ── State ─────────────────────────────────────────────────────────────────── + +let currentType = "default"; +let currentFg = "#444444"; +let currentBg = "#212121"; +let currentSize = 32; +let currentThickness = 2; +let currentTileW = 32; // tile period (graph units) for the active pattern +let currentTileH = 32; +let dirty = true; + +// Bumped whenever a setting changes; lets the per-frame cache check compare a +// number instead of building a key string each frame (avoids GC churn on drag). +let settingsVersion = 0; + +// Cached tile bitmap + pattern — reused across frames; rebuilt only when the +// settings version or the on-screen tile size changes. Panning/dragging (scale +// unchanged) always hits the cache with zero allocation. +const patternCache = { ver: -1, iDw: -1, iDh: -1, pattern: null }; + +function invalidateCache() { + dirty = true; + settingsVersion++; + app.canvas?.setDirty(true, true); +} + +function readSettings() { + currentType = getSetting("KJNodes.canvasBg.pattern", "default"); + currentFg = asHex(getSetting("KJNodes.canvasBg.patternColor", "#444444")); + currentBg = asHex(getSetting("KJNodes.canvasBg.bgColor", "#212121")); + currentSize = getSetting("KJNodes.canvasBg.scale", 32); + currentThickness = getSetting("KJNodes.canvasBg.thickness", 2); + // Resolve the tile period once per change so the render loop never allocates. + const def = patternDefs[currentType]; + if (def) { + const dims = def.tile(currentSize); + currentTileW = dims[0]; + currentTileH = dims[1]; + } +} + +/** Build (or reuse) a repeat pattern of one tile rendered at `iDw × iDh` px. */ +function getPattern(ctx, def, tileW, tileH, iDw, iDh) { + if (patternCache.ver === settingsVersion && patternCache.iDw === iDw && patternCache.iDh === iDh && patternCache.pattern) { + return patternCache.pattern; + } + + const oc = new OffscreenCanvas(iDw, iDh); + const tctx = oc.getContext("2d"); + tctx.scale(iDw / tileW, iDh / tileH); + // Bake the background into the tile so the whole canvas is painted with a + // single opaque pattern fill (no separate solid-fill pass per frame). + tctx.fillStyle = currentBg; + tctx.fillRect(0, 0, tileW, tileH); + def.draw(tctx, currentSize, currentThickness, currentFg, currentBg); + + patternCache.pattern = ctx.createPattern(oc, "repeat"); + patternCache.ver = settingsVersion; + patternCache.iDw = iDw; + patternCache.iDh = iDh; + return patternCache.pattern; +} + +// ── Commands ──────────────────────────────────────────────────────────────── + +const patternCommands = PATTERN_KEYS.map((key) => ({ + id: `KJNodes.canvasBg.setPattern.${key}`, + label: key.charAt(0).toUpperCase() + key.slice(1), + menubarLabel: key.charAt(0).toUpperCase() + key.slice(1), + function: () => { + app.ui.settings.setSettingValue("KJNodes.canvasBg.pattern", key); + }, + active: () => getSetting("KJNodes.canvasBg.pattern", "default") === key, +})); + +// Single source of truth for slider bounds — commands reference these same objects. +const SCALE_ATTRS = { min: 8, max: 512, step: 4 }; +const THICKNESS_ATTRS = { min: 0.5, max: 20, step: 0.5 }; + +function stepSetting(id, fallback, attrs, direction) { + const cur = getSetting(id, fallback); + const next = cur + attrs.step * direction; + if (next >= attrs.min && next <= attrs.max) { + app.ui.settings.setSettingValue(id, next); + } +} + +const scaleCommands = [ + { + id: "KJNodes.canvasBg.increaseScale", + label: "Increase pattern scale", + menubarLabel: "Increase scale", + function: () => stepSetting("KJNodes.canvasBg.scale", 32, SCALE_ATTRS, 1), + active: () => false, + }, + { + id: "KJNodes.canvasBg.decreaseScale", + label: "Decrease pattern scale", + menubarLabel: "Decrease scale", + function: () => stepSetting("KJNodes.canvasBg.scale", 32, SCALE_ATTRS, -1), + active: () => false, + }, +]; + +const thicknessCommands = [ + { + id: "KJNodes.canvasBg.increaseThickness", + label: "Increase pattern thickness", + menubarLabel: "Increase thickness", + function: () => stepSetting("KJNodes.canvasBg.thickness", 2, THICKNESS_ATTRS, 1), + active: () => false, + }, + { + id: "KJNodes.canvasBg.decreaseThickness", + label: "Decrease pattern thickness", + menubarLabel: "Decrease thickness", + function: () => stepSetting("KJNodes.canvasBg.thickness", 2, THICKNESS_ATTRS, -1), + active: () => false, + }, +]; + +// ── Extension ─────────────────────────────────────────────────────────────── + +app.registerExtension({ + name: "KJNodes.CanvasBackground", + + commands: [...patternCommands, ...scaleCommands, ...thicknessCommands], + + menuCommands: [ + { + path: ["KJNodes", "Canvas Background"], + commands: patternCommands.map((c) => c.id), + }, + { + path: ["KJNodes", "Canvas Background"], + commands: scaleCommands.map((c) => c.id), + }, + { + path: ["KJNodes", "Canvas Background"], + commands: thicknessCommands.map((c) => c.id), + }, + ], + + settings: [ + { + id: "KJNodes.canvasBg.pattern", + name: "Background pattern", + category: ["KJNodes", "Canvas Background", "Pattern"], + tooltip: "Choose a background pattern for the node graph canvas. 'default' uses LiteGraph's built-in dot grid. 'none' disables all background drawing.", + type: "combo", + defaultValue: "default", + options: PATTERN_KEYS, + onChange: invalidateCache, + }, + { + id: "KJNodes.canvasBg.patternColor", + name: "Pattern color", + category: ["KJNodes", "Canvas Background", "Pattern color"], + tooltip: "Foreground color of the pattern", + type: "color", + defaultValue: "#444444", + onChange: invalidateCache, + }, + { + id: "KJNodes.canvasBg.bgColor", + name: "Background color", + category: ["KJNodes", "Canvas Background", "Background color"], + tooltip: "Background fill color behind the pattern", + type: "color", + defaultValue: "#212121", + onChange: invalidateCache, + }, + { + id: "KJNodes.canvasBg.scale", + name: "Pattern scale", + category: ["KJNodes", "Canvas Background", "Pattern scale"], + tooltip: "Size of one pattern tile in pixels", + type: "slider", + defaultValue: 32, + attrs: SCALE_ATTRS, + onChange: invalidateCache, + }, + { + id: "KJNodes.canvasBg.thickness", + name: "Pattern thickness", + category: ["KJNodes", "Canvas Background", "Pattern thickness"], + tooltip: "Thickness of lines and size of dots in pixels", + type: "slider", + defaultValue: 2, + attrs: THICKNESS_ATTRS, + onChange: invalidateCache, + }, + ], + + setup() { + const canvas = app.canvas; + if (!canvas) return; + + // onRenderBackground runs before LiteGraph applies its own graph + // transform and before it paints the clear color / background pattern. + // Returning true tells LiteGraph we handled the background, so it skips + // both. We then paint the whole canvas with a single repeat-pattern fill + const origCallback = canvas.onRenderBackground; + canvas.onRenderBackground = function (cvs, ctx) { + if (origCallback) { + const result = origCallback.call(this, cvs, ctx); + if (result) return true; + } + + if (dirty) { + readSettings(); + dirty = false; + } + + // If ComfyUI's own background image setting is active, don't interfere. + if (getSetting("Comfy.Canvas.BackgroundImage", "")) return false; + + // 'default' → let LiteGraph draw its built-in dot grid as usual. + if (currentType === "default") return false; + + const scale = this.ds.scale; + const offset = this.ds.offset; + const dpr = window.devicePixelRatio || 1; + + ctx.save(); + + const def = patternDefs[currentType]; + let painted = false; + if (def) { + const tileW = currentTileW; + const tileH = currentTileH; + const k = scale * dpr; + const onW = tileW * k; // on-screen tile size, device px + const onH = tileH * k; + + if (onW >= MIN_TILE_PX && onH >= MIN_TILE_PX) { + const iDw = Math.min(Math.round(onW), MAX_TILE_PX); + const iDh = Math.min(Math.round(onH), MAX_TILE_PX); + const pattern = getPattern(ctx, def, tileW, tileH, iDw, iDh); + + // Tile the (bg-baked, opaque) pattern over the whole canvas with a + // single nearest-neighbor fill. The tile is rendered at device resolution + const a = Math.round(onW) <= MAX_TILE_PX ? 1 : onW / iDw; + const d = Math.round(onH) <= MAX_TILE_PX ? 1 : onH / iDh; + const periodX = a * iDw; + const periodY = d * iDh; + let px = dpr * scale * offset[0]; + let py = dpr * scale * offset[1]; + px -= Math.floor(px / periodX) * periodX; + py -= Math.floor(py / periodY) * periodY; + + ctx.imageSmoothingEnabled = false; + ctx.setTransform(a, 0, 0, d, px, py); + ctx.fillStyle = pattern; + ctx.fillRect(-px / a, -py / d, cvs.width / a, cvs.height / d); + painted = true; + } + } + + if (!painted) { + // 'none', or tiles too small to discern → plain background only. + ctx.setTransform(1, 0, 0, 1, 0, 0); + ctx.fillStyle = currentBg; + ctx.fillRect(0, 0, cvs.width, cvs.height); + } + + ctx.restore(); + return true; + }; + + invalidateCache(); + }, +}); diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/web/js/context_windows_visualizer.js b/ComfyUI/custom_nodes/ComfyUI-KJNodes/web/js/context_windows_visualizer.js new file mode 100644 index 0000000000000000000000000000000000000000..3837fc0afbf8610eeb9c54c6ab878689b6ed1025 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-KJNodes/web/js/context_windows_visualizer.js @@ -0,0 +1,836 @@ +import { chainCallback, addMiddleClickPan, addWheelPassthrough } from './utility.js'; +const { app } = window.comfyAPI.app; + +// --- begin context window math (port of comfy/context_windows.py) --- +const MAX_WINDOWS = 512; +const TRUNC_MSG = `too many windows, truncated at ${MAX_WINDOWS}`; +function capReached(windows, errors) { + if (windows.length < MAX_WINDOWS) return false; + errors.push(TRUNC_MSG); + return true; +} + +function pythonRound(x) { + // python round() is half-even + const floor = Math.floor(x); + const diff = x - floor; + if (diff > 0.5) return floor + 1; + if (diff < 0.5) return floor; + return floor % 2 === 0 ? floor : floor + 1; +} + +function orderedHalving(val) { + const bin = BigInt(val).toString(2).padStart(64, '0').split('').reverse().join(''); + return Number(BigInt('0b' + bin)) / 18446744073709551616; +} + +function range(start, stop) { + const out = []; + for (let i = start; i < stop; i++) out.push(i); + return out; +} + +// loop min/max; avoids Math.min(...arr) spread-limit crash on long windows +function arrMin(a) { + let m = Infinity; + for (let i = 0; i < a.length; i++) if (a[i] < m) m = a[i]; + return m; +} +function arrMax(a) { + let m = -Infinity; + for (let i = 0; i < a.length; i++) if (a[i] > m) m = a[i]; + return m; +} + +function arraysEqual(a, b) { + if (a.length !== b.length) return false; + for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false; + return true; +} + +function doesWindowRollOver(window, numFrames) { + let prev = -1; + for (let i = 0; i < window.length; i++) { + const val = ((window[i] % numFrames) + numFrames) % numFrames; + if (val < prev) return [true, i]; + prev = val; + } + return [false, -1]; +} + +function shiftWindowToStart(window, numFrames) { + const startVal = window[0]; + for (let i = 0; i < window.length; i++) { + window[i] = ((window[i] - startVal) + numFrames) % numFrames; + } +} + +function shiftWindowToEnd(window, numFrames) { + shiftWindowToStart(window, numFrames); + const endDelta = numFrames - window[window.length - 1] - 1; + for (let i = 0; i < window.length; i++) window[i] += endDelta; +} + +// p: {length, overlap, stride, closedLoop, step} +function createWindows(numFrames, schedule, p, errors) { + const L = p.length; + if (schedule === 'standard_static') { + if (numFrames <= L) return [range(0, numFrames)]; + const delta = L - p.overlap; + if (delta <= 0) { + errors.push(`context_overlap (${p.overlap}) must be smaller than context_length (${L}); the sampler would error`); + return []; + } + const windows = []; + for (let startIdx = 0; startIdx < numFrames; startIdx += delta) { + const ending = startIdx + L; + if (ending >= numFrames) { + const finalStart = startIdx - (ending - numFrames); + windows.push(range(finalStart, finalStart + L)); + break; + } + windows.push(range(startIdx, startIdx + L)); + if (capReached(windows, errors)) break; + } + return windows; + } + if (schedule === 'standard_static_balanced') { + // standard_static count, placed evenly so seams are uniform (±1 frame) + if (numFrames <= L) return [range(0, numFrames)]; + if (L - p.overlap <= 0) errors.push(`context_overlap (${p.overlap}) >= context_length (${L}); windows collapse to stride 1`); + const strideTarget = Math.max(1, L - p.overlap); + let N = Math.ceil((numFrames - L) / strideTarget) + 1; // stride <= L => no gaps + if (N > MAX_WINDOWS) { + errors.push(TRUNC_MSG); + N = MAX_WINDOWS; + } + const windows = []; + for (let i = 0; i < N; i++) { + const start = Math.floor(i * (numFrames - L) / (N - 1) + 0.5); + windows.push(range(start, start + L)); + } + return windows; + } + if (schedule === 'batched') { + if (numFrames <= L) return [range(0, numFrames)]; + const windows = []; + for (let startIdx = 0; startIdx < numFrames; startIdx += L) { + windows.push(range(startIdx, Math.min(startIdx + L, numFrames))); + if (capReached(windows, errors)) break; + } + return windows; + } + if (schedule === 'batched_shifted') { + // zero-overlap windows whose seams roll each step (~1.0x compute) + if (numFrames <= L) return [range(0, numFrames)]; + const offset = pythonRound(numFrames * orderedHalving(p.step)) % L; // 0 at step 0 (plain batched) + const windows = []; + let start = 0; + if (offset > 0) { windows.push(range(0, offset)); start = offset; } + while (start < numFrames) { + const end = Math.min(start + L, numFrames); + windows.push(range(start, end)); + start = end; + if (capReached(windows, errors)) break; + } + return windows; + } + if (schedule === 'looped_uniform' || schedule === 'standard_uniform') { + const looped = schedule === 'looped_uniform'; + if (looped ? numFrames < L : numFrames <= L) return [range(0, numFrames)]; + const windows = []; + const strideCount = Math.min(p.stride, Math.ceil(Math.log2(numFrames / L)) + 1); + for (let sPow = 0; sPow < strideCount; sPow++) { + const contextStep = 1 << sPow; + const pad = pythonRound(numFrames * orderedHalving(p.step)); + const start = Math.floor(orderedHalving(p.step) * contextStep) + pad; + const stop = numFrames + pad + (looped && p.closedLoop ? 0 : -p.overlap); + const stepSize = L * contextStep - p.overlap; + if (stepSize <= 0) { + errors.push(`window advance is ${stepSize} (≤ 0) at stride ${contextStep}; the sampler would hang/error`); + continue; + } + for (let j = start; j < stop; j += stepSize) { + const w = []; + for (let e = j; e < j + L * contextStep; e += contextStep) { + w.push(((e % numFrames) + numFrames) % numFrames); + } + windows.push(w); + if (capReached(windows, errors)) return windows; + } + } + if (!looped) { + // shift roll-over windows to the end, fill gaps, drop duplicates + const deleteIdxs = []; + let winI = 0; + while (winI < windows.length) { + const [isRoll, rollIdx] = doesWindowRollOver(windows[winI], numFrames); + if (isRoll) { + const rollVal = windows[winI][rollIdx]; + shiftWindowToEnd(windows[winI], numFrames); + if (!windows[(winI + 1) % windows.length].includes(rollVal)) { + windows.splice(winI + 1, 0, range(rollVal, rollVal + L)); + } + } + for (let preI = 0; preI < winI; preI++) { + if (arraysEqual(windows[winI], windows[preI])) { + deleteIdxs.push(winI); + break; + } + } + winI++; + } + deleteIdxs.reverse(); + for (const i of deleteIdxs) windows.splice(i, 1); + } + return windows; + } + return [range(0, numFrames)]; +} + +// extra overlap on the last static window from its shove-back; 0 = even tiling +function staticFinalDelta(F, L, overlap) { + if (F <= L) return 0; + const d = L - overlap; + if (d <= 0) return 0; // invalid config, flagged elsewhere + const mod = (F - L) % d; + return mod === 0 ? 0 : d - mod; +} + +function linspace(a, b, n) { + if (n === 1) return [a]; + const out = []; + for (let i = 0; i < n; i++) out.push(a + (b - a) * i / (n - 1)); + return out; +} + +function weightsPyramid(length) { + const seq = []; + if (length % 2 === 0) { + const m = length / 2; + for (let i = 1; i <= m; i++) seq.push(i); + for (let i = m; i >= 1; i--) seq.push(i); + } else { + const m = (length + 1) / 2; + for (let i = 1; i < m; i++) seq.push(i); + seq.push(m); + for (let i = m - 1; i >= 1; i--) seq.push(i); + } + return seq; +} + +function weightsOverlapLinear(length, fullLength, idxs, overlap, errors) { + const w = new Array(length).fill(1); + if (overlap > length && errors) { + errors.push(`overlap-linear with context_overlap > context_length would error in the sampler`); + } + const ov = Math.min(overlap, length); + if (ov > 0) { + if (arrMin(idxs) > 0) { + const ramp = linspace(1e-37, 1, ov); + for (let i = 0; i < ov; i++) w[i] = ramp[i]; + } + if (arrMax(idxs) < fullLength - 1) { + const ramp = linspace(1, 1e-37, ov); + for (let i = 0; i < ov; i++) w[length - ov + i] = ramp[i]; + } + } + return w; +} + +function weightsRelative(idxList) { + // per-index triangular bias (sequential weighted avg == bias/Σbias share) + const first = idxList[0]; + const last = idxList[idxList.length - 1]; + return idxList.map((idx) => { + const bias = 1 - Math.abs(idx - (first + last) / 2) / ((last - first + 1e-2) / 2); + return Math.max(1e-2, bias); + }); +} + +// smooth overlap-linear: flat interior, raised-cosine ramps that sum to 1 +function weightsHann(length, fullLength, idxs, overlap, errors) { + const w = new Array(length).fill(1); + if (overlap > length && errors) { + errors.push(`hann with context_overlap (${overlap}) > context_length (${length}); ramp clamped`); + } + const ov = Math.min(Math.max(overlap, 0), length); + if (ov >= 1) { + const denom = Math.max(ov - 1, 1); + if (arrMin(idxs) > 0) { + for (let j = 0; j < ov; j++) w[j] = Math.max(0.5 * (1 - Math.cos(Math.PI * j / denom)), 1e-37); + } + if (arrMax(idxs) < fullLength - 1) { + for (let j = 0; j < ov; j++) w[length - ov + j] = Math.max(0.5 * (1 - Math.cos(Math.PI * (denom - j) / denom)), 1e-37); + } + } + return w; +} + +// smooth bell over the window (smooth twin of pyramid) +function weightsGaussian(length) { + if (length <= 1) return new Array(length).fill(1); + const c = (length - 1) / 2; + const std = Math.max(length / 4, 1e-6); + const out = []; + for (let i = 0; i < length; i++) out.push(Math.exp(-0.5 * ((i - c) / std) ** 2)); + return out; +} + +function computeViz(p) { + const errors = []; + const numFrames = Math.max(1, p.numFrames); + const rawWindows = createWindows(numFrames, p.schedule, { + length: Math.max(1, p.length), + overlap: Math.max(0, p.overlap), + stride: Math.max(1, p.stride), + closedLoop: p.closedLoop, + step: Math.max(0, p.step), + }, errors); + + const windows = rawWindows.map((idxList) => { + let anchor = -1; + if (p.causalFix) { + const a = idxList[0] - 1; + if (a >= 0 && a < numFrames) anchor = a; + } + let weights; + if (p.fuse === 'relative') weights = weightsRelative(idxList); + else if (p.fuse === 'flat') weights = new Array(idxList.length).fill(1); + else if (p.fuse === 'overlap-linear') weights = weightsOverlapLinear(idxList.length, numFrames, idxList, Math.max(0, p.overlap), errors); + else if (p.fuse === 'hann') weights = weightsHann(idxList.length, numFrames, idxList, Math.max(0, p.overlap), errors); + else if (p.fuse === 'gaussian') weights = weightsGaussian(idxList.length); + else weights = weightsPyramid(idxList.length); + const [rolls] = doesWindowRollOver(idxList, numFrames); + return { idxList, anchor, weights, rolls }; + }); + + // per-frame total weight (counts_final in the handler) + const totals = new Float64Array(numFrames); + for (const w of windows) { + for (let pos = 0; pos < w.idxList.length; pos++) totals[w.idxList[pos]] += w.weights[pos]; + } + const missing = []; + for (let f = 0; f < numFrames; f++) if (totals[f] === 0) missing.push(f); + let evals = 0; + for (const w of windows) evals += w.idxList.length + (w.anchor >= 0 ? 1 : 0); + + return { numFrames, windows, totals, missing, evals, errors: [...new Set(errors)] }; +} +// --- end context window math --- + +const STYLE_ID = 'kj-cwviz-style'; +function ensureStyles() { + if (document.getElementById(STYLE_ID)) return; + const s = document.createElement('style'); + s.id = STYLE_ID; + s.textContent = ` +.kj-cw-root { width:100%; height:100%; display:flex; flex-direction:column; background:#181818; overflow:hidden; user-select:none; border-radius:4px; } +.kj-cw-bar { display:flex; align-items:center; gap:6px; padding:4px 6px; background:#222; color:#ddd; font:12px sans-serif; flex:0 0 auto; } +.kj-cw-btn { background:#333; color:#ddd; border:1px solid #444; padding:1px 10px; cursor:pointer; font:12px sans-serif; border-radius:3px; } +.kj-cw-btn:hover { background:#444; } +.kj-cw-scrubber { position:relative; flex:1 1 auto; height:20px; background:#2a2a2a; border:1px solid #444; cursor:ew-resize; user-select:none; touch-action:none; border-radius:3px; overflow:hidden; } +.kj-cw-scrubber-fill { position:absolute; top:0; bottom:0; left:0; width:0; background:#3a5a7a; pointer-events:none; } +.kj-cw-scrubber-text { position:absolute; inset:0; display:flex; align-items:center; justify-content:center; color:#ddd; font:11px sans-serif; pointer-events:none; text-shadow:0 1px 2px rgba(0,0,0,0.6); } +.kj-cw-steps { width:42px; background:#2a2a2a; color:#ddd; border:1px solid #444; font:11px sans-serif; padding:2px 4px; border-radius:3px; } +.kj-cw-info { padding:2px 8px; color:#aaa; font:11px sans-serif; flex:0 0 auto; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; } +.kj-cw-info.error { color:#e06c5a; } +.kj-cw-canvas { flex:1 1 auto; min-height:0; width:100%; display:block; cursor:crosshair; } +`; + document.head.appendChild(s); +} + +const STEP_DEPENDENT = ['standard_uniform', 'looped_uniform', 'batched_shifted']; // schedules whose windows change per sampling step + +const WINDOW_HUE_STEP = 137.508; // golden angle, distinct adjacent colors +function windowColor(i, sat = 65, light = 60, alpha = 1) { + return `hsla(${(i * WINDOW_HUE_STEP) % 360}, ${sat}%, ${light}%, ${alpha})`; +} + +function pickTickStep(numFrames, plotW) { + const steps = [1, 2, 5, 10, 20, 25, 50, 100, 200, 500, 1000]; + for (const s of steps) { + if ((plotW / numFrames) * s >= 34) return s; + } + return steps[steps.length - 1]; +} + +app.registerExtension({ + name: 'KJNodes.ContextWindowsVisualizer', + async beforeRegisterNodeDef(nodeType, nodeData) { + if (nodeData?.name !== 'ContextWindowsVisualizerKJ') return; + + chainCallback(nodeType.prototype, 'onNodeCreated', function () { + ensureStyles(); + const node = this; + if (node.size[0] < 560 || node.size[1] < 480) node.setSize([Math.max(node.size[0], 560), Math.max(node.size[1], 480)]); + + const root = document.createElement('div'); + root.className = 'kj-cw-root'; + + const bar = document.createElement('div'); + bar.className = 'kj-cw-bar'; + const playBtn = document.createElement('button'); + playBtn.className = 'kj-cw-btn'; + playBtn.textContent = '▶'; + playBtn.title = 'Animate sampling steps (uniform schedules shift their windows each step)'; + const scrubber = document.createElement('div'); + scrubber.className = 'kj-cw-scrubber'; + const scrubberFill = document.createElement('div'); + scrubberFill.className = 'kj-cw-scrubber-fill'; + const scrubberText = document.createElement('span'); + scrubberText.className = 'kj-cw-scrubber-text'; + scrubber.appendChild(scrubberFill); + scrubber.appendChild(scrubberText); + const stepsInput = document.createElement('input'); + stepsInput.className = 'kj-cw-steps'; + stepsInput.type = 'number'; + stepsInput.min = '1'; + stepsInput.max = '999'; + stepsInput.title = 'Total sampling steps to scrub through'; + bar.appendChild(playBtn); + bar.appendChild(scrubber); + bar.appendChild(stepsInput); + + const info = document.createElement('div'); + info.className = 'kj-cw-info'; + const canvas = document.createElement('canvas'); + canvas.className = 'kj-cw-canvas'; + + root.appendChild(bar); + root.appendChild(info); + root.appendChild(canvas); + node.addDOMWidget('context_windows_viz', 'div', root, { serialize: false }); + + const removeMiddleClickPan = addMiddleClickPan(root); + addWheelPassthrough(root); + + const state = { + step: 0, + steps: 20, + viz: null, + latent: null, // converted params used for the viz + hoverWin: -1, + hoverFrame: -1, + mouseX: 0, + mouseY: 0, + layout: null, // hit-test geometry from last draw + }; + + const getWidgetValue = (name, fallback) => { + const w = node.widgets?.find((w) => w.name === name); + return w ? w.value : fallback; + }; + + const persist = () => { + node.properties = node.properties || {}; + node.properties.cw_viz = { step: state.step, steps: state.steps }; + }; + + const readParams = () => { + const units = getWidgetValue('frame_units', 'pixel'); + const factor = Math.max(getWidgetValue('temporal_downscale', 4), 1); + let numFrames = getWidgetValue('num_frames', 161); + let length = getWidgetValue('context_length', 81); + let overlap = getWidgetValue('context_overlap', 30); + if (units === 'pixel') { + numFrames = Math.max(Math.floor((numFrames - 1) / factor) + 1, 1); + length = Math.max(Math.floor((length - 1) / factor) + 1, 1); + overlap = Math.max(Math.floor((overlap - 1) / factor) + 1, 0); + } + const schedule = getWidgetValue('context_schedule', 'standard_static'); + // flag the oversized final seam plain standard_static can leave + const finalDelta = schedule === 'standard_static' ? staticFinalDelta(numFrames, length, overlap) : 0; + return { + units, + factor, + numFrames, + length, + overlap, + finalDelta, + schedule, + stride: getWidgetValue('context_stride', 1), + closedLoop: !!getWidgetValue('closed_loop', false), + fuse: getWidgetValue('fuse_method', 'pyramid'), + causalFix: !!getWidgetValue('causal_window_fix', false), + step: state.step, + }; + }; + + const updateScrubberUI = () => { + const t = state.steps > 1 ? state.step / (state.steps - 1) : 0; + scrubberFill.style.width = (t * 100) + '%'; + const stepIndependent = state.latent && !STEP_DEPENDENT.includes(state.latent.schedule); + scrubberText.textContent = `step ${state.step + 1} / ${state.steps}` + (stepIndependent ? ' (static schedule, no effect)' : ''); + stepsInput.value = String(state.steps); + }; + + const updateInfo = () => { + if (!state.viz) return; + const v = state.viz; + if (v.errors.length) { + info.classList.add('error'); + info.textContent = '⚠ ' + v.errors.join(' · '); + info.title = v.errors.join('\n'); + return; + } + info.classList.remove('error'); + const lat = state.latent; + const overhead = v.numFrames > 0 ? (v.evals / v.numFrames) : 0; + const px = lat.units === 'pixel' ? ` (${(v.numFrames - 1) * lat.factor + 1}px frames)` : ''; + let txt = `${v.windows.length} windows over ${v.numFrames} latent frames${px} · ${v.evals} frame evals/step (${overhead.toFixed(2)}×)`; + if (v.missing.length) txt += ` · ⚠ ${v.missing.length} uncovered frames`; + if (lat.schedule === 'standard_static' && lat.finalDelta > 0) { + txt += ` · ⚠ last window overlaps +${lat.finalDelta} frames; standard_static_balanced spreads seams evenly`; + } else if (lat.schedule === 'standard_static_balanced') { + txt += ` · balanced placement: even seams (±1 frame)`; + } else if (lat.schedule === 'batched_shifted') { + const off = pythonRound(v.numFrames * orderedHalving(lat.step)) % lat.length; + txt += v.windows.length > 1 ? ` · zero-overlap rolling seams (step offset ${off})` : ` · zero-overlap`; + } + info.textContent = txt; + info.title = txt; + }; + + let rafPending = false; + const scheduleRender = () => { + if (rafPending) return; + rafPending = true; + requestAnimationFrame(() => { + rafPending = false; + draw(); + }); + }; + + const recompute = () => { + state.latent = readParams(); + state.viz = computeViz(state.latent); + state.hoverWin = -1; + state.hoverFrame = -1; + updateScrubberUI(); + updateInfo(); + scheduleRender(); + }; + + const draw = () => { + const viz = state.viz; + if (!viz) return; + const dpr = window.devicePixelRatio || 1; + const cw = canvas.clientWidth; + const ch = canvas.clientHeight; + if (cw <= 0 || ch <= 0) return; + const W = Math.floor(cw * dpr); + const H = Math.floor(ch * dpr); + if (canvas.width !== W) canvas.width = W; + if (canvas.height !== H) canvas.height = H; + const ctx = canvas.getContext('2d'); + ctx.setTransform(dpr, 0, 0, dpr, 0, 0); + ctx.clearRect(0, 0, cw, ch); + ctx.fillStyle = '#181818'; + ctx.fillRect(0, 0, cw, ch); + + const n = viz.numFrames; + const pad = 8; + const gutter = 30; + const rulerH = 16; + const chartH = Math.max(50, Math.min(110, ch * 0.28)); + const chartGap = 18; + const plotX = pad + gutter; + const plotW = cw - plotX - pad; + const rowsY = pad + rulerH + 4; + const chartY = ch - pad - chartH; + const rowsH = chartY - chartGap - rowsY; + if (plotW <= 0 || rowsH <= 10) return; + const cellW = plotW / n; + const barW = Math.max(cellW - (cellW > 3 ? 0.5 : 0), 0.5); // cell width minus a hairline gap + const wins = viz.windows; + const rowH = wins.length ? Math.min(20, Math.max(3, rowsH / wins.length)) : 0; + state.layout = { plotX, plotW, rowsY, rowH, chartY, chartH, n }; + + // ruler + ctx.font = '10px sans-serif'; + ctx.textBaseline = 'middle'; + ctx.fillStyle = '#777'; + ctx.strokeStyle = '#3a3a3a'; + const tick = pickTickStep(n, plotW); + for (let f = 0; f < n; f += tick) { + const x = plotX + (f + 0.5) * cellW; + ctx.textAlign = 'center'; + ctx.fillText(String(f), x, pad + rulerH / 2); + ctx.beginPath(); + ctx.moveTo(x, rowsY); + ctx.lineTo(x, chartY + chartH); + ctx.stroke(); + } + // uncovered frames + if (viz.missing.length) { + ctx.fillStyle = 'rgba(220,70,60,0.25)'; + for (const f of viz.missing) ctx.fillRect(plotX + f * cellW, rowsY, cellW, (chartY + chartH) - rowsY); + } + + // window rows + const hov = state.hoverWin; + let drawnRows = wins.length; + for (let i = 0; i < wins.length; i++) { + const w = wins[i]; + const y = rowsY + i * rowH; + if (y + rowH > rowsY + rowsH + 1) { drawnRows = i; break; } + const dim = hov >= 0 && hov !== i; + const maxW = arrMax(w.weights); + // row label + if (rowH >= 9) { + ctx.fillStyle = dim ? '#555' : '#999'; + ctx.textAlign = 'right'; + ctx.fillText(String(i), plotX - 4, y + rowH / 2); + } + for (let pos = 0; pos < w.idxList.length; pos++) { + const f = w.idxList[pos]; + const wt = maxW > 0 ? w.weights[pos] / maxW : 1; + const alpha = (0.25 + 0.7 * wt) * (dim ? 0.25 : 1); + // also encode weight as cell height when rows are tall + const hFrac = rowH >= 8 ? 0.35 + 0.65 * wt : 1; + const cellH = Math.max(1.5, (rowH - 1) * hFrac); + ctx.fillStyle = windowColor(i, 65, 60, alpha); + ctx.fillRect(plotX + f * cellW, y + (rowH - 1 - cellH), barW, cellH); + } + // causal anchor marker + if (w.anchor >= 0 && !dim) { + ctx.strokeStyle = windowColor(i, 65, 75, 0.9); + ctx.setLineDash([2, 2]); + ctx.strokeRect(plotX + w.anchor * cellW + 0.5, y + 0.5, Math.max(cellW - 1, 1), Math.max(rowH - 2, 1)); + ctx.setLineDash([]); + } + if (hov === i) { + const lo = arrMin(w.idxList); + const hi = arrMax(w.idxList); + ctx.strokeStyle = '#fff'; + ctx.lineWidth = 1; + ctx.strokeRect(plotX + lo * cellW, y, (hi - lo + 1) * cellW, rowH - 1); + } + } + + state.layout.drawnRows = drawnRows; + // blend chart: stacked normalized contribution per frame + ctx.fillStyle = '#888'; + ctx.textAlign = 'left'; + ctx.fillText('blend (normalized contribution per frame)', plotX, chartY - 8); + if (drawnRows < wins.length) { + ctx.textAlign = 'right'; + ctx.fillText(`…+${wins.length - drawnRows} more windows (enlarge node)`, plotX + plotW, chartY - 8); + } + ctx.strokeStyle = '#3a3a3a'; + ctx.strokeRect(plotX, chartY, plotW, chartH); + const stackOffsets = new Float64Array(n); + for (let i = 0; i < wins.length; i++) { + const w = wins[i]; + const dim = hov >= 0 && hov !== i; + ctx.fillStyle = windowColor(i, dim ? 25 : 65, dim ? 38 : 58, dim ? 0.6 : 0.92); + for (let pos = 0; pos < w.idxList.length; pos++) { + const f = w.idxList[pos]; + const total = viz.totals[f]; + if (total <= 0) continue; + const h = (w.weights[pos] / total) * chartH; + const yOff = (stackOffsets[f] / total) * chartH; + stackOffsets[f] += w.weights[pos]; + ctx.fillRect(plotX + f * cellW, chartY + chartH - yOff - h, barW, h); + } + } + + // hover guide + tooltip + if (state.hoverFrame >= 0) { + const x = plotX + (state.hoverFrame + 0.5) * cellW; + ctx.strokeStyle = 'rgba(255,255,255,0.35)'; + ctx.beginPath(); + ctx.moveTo(x, rowsY); + ctx.lineTo(x, chartY + chartH); + ctx.stroke(); + } + const lines = []; + if (hov >= 0 && hov < wins.length) { + const w = wins[hov]; + const strided = w.idxList.length > 1 ? w.idxList[1] - w.idxList[0] : 1; + let l = `window ${hov + 1}/${wins.length}: ${w.idxList.length} frames [${w.idxList[0]}…${w.idxList[w.idxList.length - 1]}]`; + if (Math.abs(strided) > 1) l += ` stride ${strided}`; + if (w.rolls) l += ' (wraps)'; + if (w.anchor >= 0) l += ` · anchor ${w.anchor}`; + lines.push(l); + } + if (state.hoverFrame >= 0) { + const f = state.hoverFrame; + const cover = []; + for (let i = 0; i < wins.length; i++) { + const pos = wins[i].idxList.indexOf(f); + if (pos >= 0) cover.push([i, wins[i].weights[pos]]); + } + const px = state.latent.units === 'pixel' ? ` (≈px ${f * state.latent.factor})` : ''; + if (cover.length === 0) { + lines.push(`frame ${f}${px}: not covered by any window!`); + } else { + const total = viz.totals[f]; + const shares = cover.map(([i, wt]) => `w${i + 1} ${(100 * wt / total).toFixed(0)}%`).join(', '); + lines.push(`frame ${f}${px}: ${shares}`); + } + } + if (lines.length) { + ctx.font = '11px sans-serif'; + const tw = Math.max(...lines.map((l) => ctx.measureText(l).width)) + 12; + const th = lines.length * 15 + 8; + let tx = state.mouseX + 14; + let ty = state.mouseY + 14; + if (tx + tw > cw - 4) tx = state.mouseX - tw - 8; + if (ty + th > ch - 4) ty = state.mouseY - th - 8; + ctx.fillStyle = 'rgba(20,20,20,0.92)'; + ctx.strokeStyle = '#555'; + ctx.fillRect(tx, ty, tw, th); + ctx.strokeRect(tx, ty, tw, th); + ctx.fillStyle = '#ddd'; + ctx.textAlign = 'left'; + for (let i = 0; i < lines.length; i++) ctx.fillText(lines[i], tx + 6, ty + 12 + i * 15); + } + }; + + // hover handling + canvas.addEventListener('pointermove', (e) => { + if (!state.viz || !state.layout) return; + const rect = canvas.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + state.mouseX = x; + state.mouseY = y; + const lay = state.layout; + let frame = Math.floor((x - lay.plotX) / (lay.plotW / lay.n)); + if (x < lay.plotX || frame < 0 || frame >= lay.n) frame = -1; + let win = -1; + const visibleRows = Math.min(lay.drawnRows ?? state.viz.windows.length, state.viz.windows.length); + if (y >= lay.rowsY && y < lay.rowsY + lay.rowH * visibleRows && lay.rowH > 0) { + win = Math.floor((y - lay.rowsY) / lay.rowH); + if (win >= visibleRows) win = -1; + } else if (y >= lay.chartY && y <= lay.chartY + lay.chartH && frame >= 0) { + // pick the stacked band under the cursor + const total = state.viz.totals[frame]; + if (total > 0) { + const fromBottom = (lay.chartY + lay.chartH - y) / lay.chartH * total; + let acc = 0; + for (let i = 0; i < state.viz.windows.length; i++) { + const w = state.viz.windows[i]; + const pos = w.idxList.indexOf(frame); + if (pos < 0) continue; + acc += w.weights[pos]; + if (fromBottom <= acc) { win = i; break; } + } + } + } + if (win !== state.hoverWin || frame !== state.hoverFrame) { + state.hoverWin = win; + state.hoverFrame = frame; + } + scheduleRender(); + }); + canvas.addEventListener('pointerleave', () => { + state.hoverWin = -1; + state.hoverFrame = -1; + scheduleRender(); + }); + + // step scrubber + let scrubbing = false; + const scrubFromEvent = (e) => { + const rect = scrubber.getBoundingClientRect(); + const t = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width)); + const step = Math.round(t * (state.steps - 1)); + if (step !== state.step) { + state.step = step; + persist(); + recompute(); + } + }; + scrubber.addEventListener('pointerdown', (e) => { + if (e.button !== 0) return; + stopPlayback(); + scrubbing = true; + scrubber.setPointerCapture(e.pointerId); + scrubFromEvent(e); + e.preventDefault(); + }); + scrubber.addEventListener('pointermove', (e) => { + if (scrubbing) scrubFromEvent(e); + }); + const endScrub = (e) => { + if (!scrubbing) return; + scrubbing = false; + try { scrubber.releasePointerCapture(e.pointerId); } catch (_) { } + }; + scrubber.addEventListener('pointerup', endScrub); + scrubber.addEventListener('pointercancel', endScrub); + + stepsInput.addEventListener('change', () => { + const v = parseInt(stepsInput.value, 10); + if (Number.isFinite(v) && v >= 1 && v <= 999) { + state.steps = v; + if (state.step >= v) state.step = v - 1; + persist(); + recompute(); + } else { + stepsInput.value = String(state.steps); + } + }); + stepsInput.addEventListener('pointerdown', (e) => e.stopPropagation()); + + // step playback + let playHandle = 0; + const stopPlayback = () => { + if (playHandle) { + clearInterval(playHandle); + playHandle = 0; + playBtn.textContent = '▶'; + } + }; + playBtn.addEventListener('click', () => { + if (playHandle) { + stopPlayback(); + return; + } + playBtn.textContent = '⏸'; + playHandle = setInterval(() => { + state.step = (state.step + 1) % state.steps; + persist(); + recompute(); + }, 300); + }); + + // live updates on widget changes + const WATCHED = ['frame_units', 'temporal_downscale', 'num_frames', 'context_length', 'context_overlap', + 'context_schedule', 'context_stride', 'closed_loop', 'fuse_method', 'causal_window_fix']; + for (const name of WATCHED) { + const w = node.widgets?.find((w) => w.name === name); + if (!w) continue; + const orig = w.callback; + w.callback = function (...args) { + const r = orig ? orig.apply(this, args) : undefined; + recompute(); + return r; + }; + } + + const ro = new ResizeObserver(() => scheduleRender()); + ro.observe(canvas); + + chainCallback(node, 'onConfigure', function () { + const saved = this.properties?.cw_viz; + if (saved) { + if (Number.isFinite(saved.steps)) state.steps = Math.min(999, Math.max(1, saved.steps)); + if (Number.isFinite(saved.step)) state.step = Math.min(state.steps - 1, Math.max(0, saved.step)); + } + recompute(); + }); + + chainCallback(node, 'onRemoved', function () { + stopPlayback(); + ro.disconnect(); + removeMiddleClickPan(); + }); + + recompute(); + }); + }, +}); diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/web/js/contextmenu.js b/ComfyUI/custom_nodes/ComfyUI-KJNodes/web/js/contextmenu.js new file mode 100644 index 0000000000000000000000000000000000000000..9abccc68afa731ce54bb85a772a81eabc6ec6ca2 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-KJNodes/web/js/contextmenu.js @@ -0,0 +1,182 @@ +const { app } = window.comfyAPI.app; + +function addNode(name, nextTo, options) { + options = { side: "left", select: true, shiftY: 0, shiftX: 0, ...(options || {}) }; + const node = LiteGraph.createNode(name); + const graph = app.canvas?.graph || app.graph; + graph.add(node); + + node.pos = [ + options.side === "left" ? nextTo.pos[0] - (node.size[0] + options.offset): nextTo.pos[0] + nextTo.size[0] + options.offset, + nextTo.pos[1] + options.shiftY, + ]; + + // Automatically connect nodes + if (options.side === "left") { + // New node on left: connect new node's first output to nextTo's first free input + if (node.outputs && node.outputs.length > 0 && nextTo.inputs && nextTo.inputs.length > 0) { + for (let i = 0; i < nextTo.inputs.length; i++) { + if (!nextTo.inputs[i].link) { + node.pos[1] += i * (node.size[1] + 32); + node.connect(0, nextTo, i); + break; + } + } + } + } else { + // New node on right: connect nextTo's first free output to new node's first free input + if (nextTo.outputs && nextTo.outputs.length > 0 && node.inputs && node.inputs.length > 0) { + for (let o = 0; o < nextTo.outputs.length; o++) { + if (!nextTo.outputs[o].links || nextTo.outputs[o].links.length === 0) { + // Offset vertically by slot index so multiple Set nodes don't overlap + node.pos[1] += o * (node.size[1] + 32); + for (let i = 0; i < node.inputs.length; i++) { + if (!node.inputs[i].link) { + nextTo.connect(o, node, i); + break; + } + } + break; + } + } + } + } + + if (options.select) { + app.canvas.selectNode(node, false); + } + return node; +} + +// Expose addNode for use in setgetnodes.js +window.kjNodes = window.kjNodes || {}; +window.kjNodes.addNode = addNode; + +app.registerExtension({ + name: "KJNodes.Contextmenu", + settings: [ + { + id: "KJNodes.helpPopup", + name: "Help popups", + category: ["KJNodes", "General", "Help popups"], + tooltip: "Show help popups when hovering over KJNodes", + defaultValue: true, + type: "boolean", + }, + { + id: "KJNodes.showSetGetInConnectionMenu", + name: "Show Set/Get in connection menu", + category: ["KJNodes", "Set & Get", "Show Set/Get in connection menu"], + tooltip: "Add Set/Get entries to the slot connection menu (next to Add Reroute)", + type: "boolean", + defaultValue: true, + }, + ], + getNodeMenuItems(node) { + try { + const items = []; + + if (node.inputs && node.inputs.length > 0) { + const selectedForConvert = window.kjNodes.snapshotSelectedNodes(node); + items.push( + { + content: "Add GetNode", + callback: () => { addNode("GetNode", node, { side: "left", offset: 30 }); } + }, + { + content: "Add SetNode", + callback: () => { addNode("SetNode", node, { side: "right", offset: 30 }); } + }, + { + content: "Add PreviewAsTextNode", + callback: () => { addNode("PreviewAny", node, { side: "right", offset: 30 }); } + }, + { + content: "Convert all outputs to Set/Get", + callback: () => { + for (const n of selectedForConvert) window.kjNodes.convertOutputsToSetGet(node.graph, n); + } + }, + ); + } + + const cls = node.constructor?.comfyClass || node.comfyClass; + if (cls && window.kjNodes?.recreateNode) { + items.push({ + content: "Recreate node", + has_submenu: true, + submenu: { + options: [ + { content: "Keep widget values", callback: () => window.kjNodes.recreateNode(node, cls, { resetValues: false }) }, + { content: "Reset widget values", callback: () => window.kjNodes.recreateNode(node, cls, { resetValues: true }) }, + ], + }, + }); + } + + return items; + } catch (err) { + console.error("[KJNodes.Contextmenu] getNodeMenuItems failed:", err); + return []; + } + }, + async setup(app) { + const origShowConnectionMenu = app.canvas.showConnectionMenu.bind(app.canvas); + + // Inject "Add SetNode" / "Add GetNode" into the slot connection dropdown menu. + app.canvas.showConnectionMenu = function(optPass) { + const showSetGet = app.ui.settings.getSettingValue("KJNodes.showSetGetInConnectionMenu") ?? true; + if (!showSetGet) return origShowConnectionMenu(optPass); + + const isFrom = optPass.nodeFrom && optPass.slotFrom != null; + const nodeType = isFrom ? "SetNode" : "GetNode"; + const label = isFrom ? "Add SetNode" : "Add GetNode"; + + const OrigCM = LiteGraph.ContextMenu; + let interceptActive = true; + LiteGraph.ContextMenu = function(options, menuOpts) { + LiteGraph.ContextMenu = OrigCM; + if (!interceptActive) return new OrigCM(options, menuOpts); + interceptActive = false; + + const idx = options.indexOf("Add Reroute"); + if (idx !== -1) options.splice(idx + 1, 0, label); + + const origCb = menuOpts.callback; + menuOpts.callback = function(v, cbOpts, e) { + if (v === label) { + const node = LiteGraph.createNode(nodeType); + if (node) { + const graph = app.canvas?.graph || app.graph; + node.pos = [optPass.e?.canvasX ?? 0, optPass.e?.canvasY ?? 0]; + graph.add(node); + if (isFrom && optPass.nodeFrom) { + const slotIdx = typeof optPass.slotFrom === 'number' + ? optPass.slotFrom + : optPass.nodeFrom.findOutputSlot(optPass.slotFrom.name); + optPass.nodeFrom.connect(slotIdx, node, 0); + } else if (optPass.nodeTo) { + const slotIdx = typeof optPass.slotTo === 'number' + ? optPass.slotTo + : optPass.nodeTo.findInputSlot(optPass.slotTo.name); + node.connect(0, optPass.nodeTo, slotIdx); + } + graph.change(); + app.canvas.setDirty(true, true); + } + return; + } + return origCb?.call(this, v, cbOpts, e); + }; + + return new OrigCM(options, menuOpts); + }; + LiteGraph.ContextMenu.prototype = OrigCM.prototype; + + const result = origShowConnectionMenu(optPass); + LiteGraph.ContextMenu = OrigCM; + interceptActive = false; + return result; + }; + } +}); diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/web/js/editors/editor_base.js b/ComfyUI/custom_nodes/ComfyUI-KJNodes/web/js/editors/editor_base.js new file mode 100644 index 0000000000000000000000000000000000000000..963d2f7cfaea49efe520fabf7dff1581e9a9a705 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-KJNodes/web/js/editors/editor_base.js @@ -0,0 +1,770 @@ +import { addMiddleClickPan, captureVideoFrame, chainCallback, makeUUID, watchImageInputs } from '../utility.js'; + +export function createEditorStylesheet(id, className) { + let styleTag = document.head.querySelector(`#${id}`) + if (!styleTag) { + styleTag = document.createElement('style') + styleTag.type = 'text/css' + styleTag.id = id + styleTag.innerHTML = ` + .${className} { + position: absolute; + font: 12px monospace; + line-height: 1.5em; + padding: 10px; + z-index: 0; + overflow: hidden; + } + .${className} canvas { + position: relative; + z-index: 2; + } + ` + document.head.appendChild(styleTag) + } +} + +function styleMenuItem(menuItem) { + menuItem.style.display = "block"; + menuItem.style.padding = "5px"; + menuItem.style.color = "#FFF"; + menuItem.style.fontFamily = "Arial, sans-serif"; + menuItem.style.fontSize = "16px"; + menuItem.style.textDecoration = "none"; + menuItem.style.marginBottom = "5px"; +} + +function createMenuItem(id, textContent) { + let menuItem = document.createElement("a"); + menuItem.href = "#"; + menuItem.dataset.menuId = id; + menuItem.textContent = textContent; + styleMenuItem(menuItem); + return menuItem; +} + +function setupMenuItems(contextMenu, menuItems) { + menuItems.forEach(mi => { + mi.addEventListener('mouseover', function () { this.style.backgroundColor = "gray"; }); + mi.addEventListener('mouseout', function () { this.style.backgroundColor = "#202020"; }); + contextMenu.appendChild(mi); + }); +} + +export function createContextMenuElement(className) { + const menu = document.createElement("div"); + if (className) menu.className = className; + menu.id = `context-menu-${Math.random().toString(36).slice(2, 10)}`; + menu.style.display = "none"; + menu.style.position = "absolute"; + menu.style.backgroundColor = "#202020"; + menu.style.minWidth = "100px"; + menu.style.boxShadow = "0px 8px 16px 0px rgba(0,0,0,0.2)"; + menu.style.zIndex = "100"; + menu.style.padding = "5px"; + return menu; +} + +// ─── Base Editor Canvas ─── + +const maxDisplayDim = 1024; +const buttonRowHeight = 28; + +export class BaseEditorCanvas { + constructor(context, reset = false) { + this.node = context; + this.reset = reset; + this.bgImage = null; + this.margin = 14; + this.dragIndex = -1; + this.dragType = null; + this.dragOffset = null; + + this._uploadGeneration = 0; + } + + // ─── Shared Methods ─── + + setNodeWidth(width) { + this.node.setSize([width, this.node.size[1]]); + const nodeEl = document.querySelector(`[data-node-id="${this.node.id}"]`); + if (nodeEl) nodeEl.style.setProperty('--node-width', `${width}px`); + } + + // Scale factors: coord space → canvas space + get scaleX() { return this.width / this.coordWidth; } + get scaleY() { return this.height / this.coordHeight; } + + // Returns mouse position in coord space + getLocalMouse(e) { + const rect = this.canvas.getBoundingClientRect(); + const canvasScaleX = this.canvas.width / rect.width; + const canvasScaleY = this.canvas.height / rect.height; + const canvasX = (e.clientX - rect.left) * canvasScaleX - this.margin; + const canvasY = (e.clientY - rect.top) * canvasScaleY - this.margin; + return { + x: canvasX / this.scaleX, + y: canvasY / this.scaleY + }; + } + + // Clamp to coord space + clamp(x, y) { + return { + x: Math.max(0, Math.min(this.coordWidth, x)), + y: Math.max(0, Math.min(this.coordHeight, y)) + }; + } + + // ─── Canvas Setup ─── + + createCanvas(parentElement) { + this.canvas = document.createElement('canvas'); + this.canvas.width = this.width + this.margin * 2; + this.canvas.height = this.height + this.margin * 2; + this.ctx = this.canvas.getContext('2d'); + parentElement.appendChild(this.canvas); + } + + resizeCanvas() { + this.canvas.width = this.width + this.margin * 2; + this.canvas.height = this.height + this.margin * 2; + } + + // ─── Event Listeners ─── + + setupEventListeners() { + this._onDragMove = (e) => this.onMouseMove(e); + this._onDragEnd = (e) => this.onMouseUp(e); + this._removeMiddleClickPan = addMiddleClickPan(this.canvas); + this._onCanvasPointerDown = (e) => { e.stopPropagation(); this.onMouseDown(e); }; + this._onCanvasPointerMove = (e) => this.onMouseMove(e); + this._onCanvasContextMenu = (e) => { e.preventDefault(); e.stopPropagation(); }; + this.canvas.addEventListener('pointerdown', this._onCanvasPointerDown); + this.canvas.addEventListener('pointermove', this._onCanvasPointerMove); + this.canvas.addEventListener('contextmenu', this._onCanvasContextMenu); + } + + removeEventListeners() { + if (this.canvas) { + this.canvas.removeEventListener('pointerdown', this._onCanvasPointerDown); + this.canvas.removeEventListener('pointermove', this._onCanvasPointerMove); + this.canvas.removeEventListener('pointermove', this._onDragMove); + this.canvas.removeEventListener('pointerup', this._onDragEnd); + this.canvas.removeEventListener('contextmenu', this._onCanvasContextMenu); + } + document.removeEventListener('pointermove', this._onDragMove); + document.removeEventListener('pointerup', this._onDragEnd); + if (this._removeMiddleClickPan) this._removeMiddleClickPan(); + } + + startDocumentDrag(e) { + if (e && e.pointerId !== undefined) { + this.canvas.setPointerCapture(e.pointerId); + this._capturedPointerId = e.pointerId; + // With pointer capture, events fire on the canvas, not document + this.canvas.addEventListener('pointermove', this._onDragMove); + this.canvas.addEventListener('pointerup', this._onDragEnd); + } else { + document.addEventListener('pointermove', this._onDragMove); + document.addEventListener('pointerup', this._onDragEnd); + } + } + + endDrag() { + this.dragIndex = -1; + this.dragType = null; + this.dragOffset = null; + this.canvas.style.cursor = 'default'; + if (this._capturedPointerId !== undefined) { + try { this.canvas.releasePointerCapture(this._capturedPointerId); } catch (_) {} + this._capturedPointerId = undefined; + this.canvas.removeEventListener('pointermove', this._onDragMove); + this.canvas.removeEventListener('pointerup', this._onDragEnd); + } else { + document.removeEventListener('pointermove', this._onDragMove); + document.removeEventListener('pointerup', this._onDragEnd); + } + } + + // ─── Render Batching ─── + + render() { + if (this._renderPending) return; + this._renderPending = true; + requestAnimationFrame(() => { + this._renderPending = false; + this._render(); + }); + } + + // Subclasses must implement _render() + + // ─── Common Render Helpers ─── + + // Convert coord-space point to canvas-space pixel + toCanvas(x, y) { + return { x: x * this.scaleX, y: y * this.scaleY }; + } + + // Call at start of _render() — clears and draws background + beginRender() { + const ctx = this.ctx; + ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); + ctx.save(); + ctx.translate(this.margin, this.margin); + ctx.fillStyle = '#222'; + ctx.fillRect(0, 0, this.width, this.height); + ctx.strokeStyle = 'gray'; + ctx.lineWidth = 2; + ctx.strokeRect(0, 0, this.width, this.height); + if (this.bgImage) { + ctx.drawImage(this.bgImage, 0, 0, this.width, this.height); + } + } + + endRender() { + this.ctx.restore(); + } + + // ─── Image Handling ─── + + handleImageLoad = (img, downscaledImg) => { + // Set coord space to image dimensions, rescaling existing points if the space changed + const oldCoordW = this.coordWidth, oldCoordH = this.coordHeight; + this.coordWidth = img.width; + this.coordHeight = img.height; + this.widthWidget.value = img.width; + this.heightWidget.value = img.height; + if (oldCoordW && oldCoordH && (oldCoordW !== img.width || oldCoordH !== img.height)) { + this.onCoordSpaceResized?.(oldCoordW, oldCoordH); + } + this.onImageResize?.(img); + + // Cap display size to the current node width if the user has already resized it, + // otherwise fall back to maxDisplayDim. This prevents the node from expanding to + // fill the image — instead the image scales to fit the node. + const nodeCanvasW = Math.max(64, Math.round(this.node.size[0] - 45)); + const fitDim = Math.min(nodeCanvasW, maxDisplayDim); + let displayW = img.width, displayH = img.height; + if (displayW > fitDim || displayH > fitDim) { + const scale = fitDim / Math.max(displayW, displayH); + displayW = Math.round(displayW * scale); + displayH = Math.round(displayH * scale); + } + + if (displayW !== this.width || displayH !== this.height) { + this.width = displayW; + this.height = displayH; + this.resizeCanvas(); + + // Only expand the node width if it's narrower than the image — never shrink it + if (displayW + 45 > this.node.size[0]) this.setNodeWidth(displayW + 45); + this.onSizeChanged(); + if (this.node.graph) { + try { this.node.arrange?.(); } catch (_) {} + this.node.graph.setDirtyCanvas(true, true); + } + } + + // Use downscaled image if available to avoid holding full-res in memory + this.bgImage = downscaledImg || img; + this.render(); + this.onDataChanged(); + }; + + // resizeCanvas: if true (default), resize editor canvas to match image. + // If false, just store the image and update the background without resizing. + processImage = (img, { resize = true } = {}) => { + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + let width = img.width, height = img.height; + if (width > maxDisplayDim || height > maxDisplayDim) { + const scale = maxDisplayDim / Math.max(width, height); + width = Math.round(width * scale); + height = Math.round(height * scale); + } + canvas.width = width; + canvas.height = height; + ctx.drawImage(img, 0, 0, width, height); + + const { app } = window.comfyAPI.app; + const embed = app.ui.settings.getSettingValue("KJNodes.editors.embedBackgroundImage") ?? false; + + // Use the downscaled canvas directly as bgImage — drawImage accepts canvas elements, + // avoids a data URL round-trip, and is immediately available (no async decode). + const onStored = () => { + if (resize) { + this.handleImageLoad(img, canvas); + } else { + this.bgImage = canvas; + this.render(); + } + }; + + const gen = ++this._uploadGeneration; + if (embed) { + const base64String = canvas.toDataURL('image/webp', 0.5).replace(/^data:.+?,/, ''); + if (gen !== this._uploadGeneration) return; + this.node.properties.imgData = { type: 'image/webp', base64: base64String }; + onStored(); + } else { + canvas.toBlob((blob) => { + if (gen !== this._uploadGeneration) return; + const filename = `editor_bg_${this.node.id}_${Date.now()}.webp`; + const formData = new FormData(); + formData.append('image', blob, filename); + formData.append('type', 'temp'); + formData.append('overwrite', 'true'); + fetch('/upload/image', { method: 'POST', body: formData }) + .then(r => r.json()) + .then(result => { + if (gen !== this._uploadGeneration) return; + this.node.properties.imgData = { type: 'temp', filename: result.name }; + onStored(); + }) + .catch(e => console.error("Failed to upload editor background:", e)); + }, 'image/webp', 0.5); + } + }; + + handleImageFile = (file) => { + const url = URL.createObjectURL(file); + const img = new Image(); + img.onload = () => { URL.revokeObjectURL(url); this.processImage(img); }; + img.onerror = () => URL.revokeObjectURL(url); + img.src = url; + }; + + refreshBackgroundImage = () => { + const imgData = this.node.properties.imgData; + if (!imgData) return; + + const img = new Image(); + img.onerror = (e) => console.error("Background image failed to load:", e); + img.onload = () => { + // Just set the background — don't resize canvas, widget values are already correct from serialization + this.bgImage = img; + this.render(); + }; + + if (imgData.base64) { + const mimeType = imgData.type || 'image/png'; + img.src = `data:${mimeType};base64,${imgData.base64}`; + } else if (imgData.filename) { + img.src = `/view?filename=${encodeURIComponent(imgData.filename)}&type=temp&no-cache=${Date.now()}`; + } + }; + + // ─── Context Menu Helpers ─── + + // Set up document-level listeners for context menu behavior + setupContextMenuListeners(editorIdPrefix) { + this._onContextMenu = (e) => { + if (e.target.closest(`#${editorIdPrefix}-${this.node.uuid}`) || + this.node.contextMenu.contains(e.target)) { + e.preventDefault(); + } + }; + this._onDocClick = (e) => { + if (!this.node.contextMenu.contains(e.target)) { + this.node.contextMenu.style.display = 'none'; + } + }; + document.addEventListener('contextmenu', this._onContextMenu); + document.addEventListener('click', this._onDocClick); + } + + // Clean up previous editor instance + cleanupPreviousEditor(context) { + if (context.editor) { + context.editor.destroy(); + } + } + + // Full cleanup — override in subclass to add additional cleanup + destroy() { + this._uploadGeneration++; // Invalidate any pending async image uploads + this.removeEventListeners(); + if (this._onContextMenu) document.removeEventListener('contextmenu', this._onContextMenu); + if (this._onDocClick) document.removeEventListener('click', this._onDocClick); + if (this._onKeyUp) document.removeEventListener('keyup', this._onKeyUp); + } + + // Common menu actions + openImageFilePicker() { + const fileInput = document.createElement('input'); + fileInput.type = 'file'; + fileInput.accept = 'image/*'; + fileInput.addEventListener('change', (event) => { + const file = event.target.files[0]; + if (file) this.handleImageFile(file); + }); + fileInput.click(); + } + + clearBackgroundImage() { + this.bgImage = null; + this.node.properties.imgData = null; + this.render(); + } + + // Find a widget by name on the node + findWidget(name) { + const w = this.node.widgets.find(w => w.name === name); + if (!w) console.warn(`${this.constructor.name}: widget "${name}" not found`); + return w; + } + + // Show context menu at mouse position + showContextMenu(e) { + this._updateMenuToggleStates(); + const menu = this.node.contextMenu; + menu.style.display = 'block'; + menu.style.left = `${e.clientX}px`; + menu.style.top = `${e.clientY}px`; + // Adjust if menu overflows viewport + const rect = menu.getBoundingClientRect(); + if (rect.right > window.innerWidth) menu.style.left = `${Math.max(0, e.clientX - rect.width)}px`; + if (rect.bottom > window.innerHeight) menu.style.top = `${Math.max(0, e.clientY - rect.height)}px`; + } + + // ─── Width/Height Resize ─── + + setupSizeCallbacks() { + this.widthWidget.callback = () => { + const oldCoordW = this.coordWidth; + this.coordWidth = this.widthWidget.value; + this.onCoordSpaceResized(oldCoordW, this.coordHeight); + this.render(); + this.onDataChanged(); + }; + this.heightWidget.callback = () => { + const oldCoordH = this.coordHeight; + this.coordHeight = this.heightWidget.value; + this.onCoordSpaceResized(this.coordWidth, oldCoordH); + this.render(); + this.onDataChanged(); + }; + } + + // ─── imgData persistence ─── + // Call once from onNodeCreated to ensure imgData survives save/reload + static setupImgDataPersistence(node) { + chainCallback(node, "onSerialize", function (o) { + if (this.properties.imgData && o.properties) { + o.properties.imgData = this.properties.imgData; + } + }); + chainCallback(node, "onConfigure", function (info) { + if (info?.properties?.imgData) { + this.properties.imgData = info.properties.imgData; + } + }); + } + + // ─── Node registration helper ─── + // Call from onNodeCreated to set up the common editor scaffolding. + // config: { editorClass, editorKey, heightKey, className, menuItems, hiddenWidgets, initialSize, extraProperties } + // editorClass: the editor constructor (e.g. SplineEditor) + // editorKey: node property name for the DOM widget (e.g. 'splineEditor') + // heightKey: node property name for editor height (e.g. 'splineEditorHeight') + // className: CSS class for the editor container + // menuItems: { id: { label, action(editor), toggle?(editor) }, ... } — context menu definition + // menuClassName: optional CSS class for the context menu + // hiddenWidgets: widget names to hide + // initialSize: [width, height] for the node + // extraProperties: array of [name, default, type] to register via addProperty on first create + static setupNode(node, nodeData, config) { + const { editorClass, editorKey, heightKey, className, menuItems, + menuClassName, hiddenWidgets, initialSize, extraProperties } = config; + + if (!node.properties) node.properties = {}; + if (node.properties.imgData === undefined) node.properties.imgData = null; + + // File handling — set up once per node, delegate to current editor + node.pasteFile = (file) => { + if (node.editor && file.type.startsWith("image/")) { + node.editor.handleImageFile(file); + return true; + } + return false; + }; + node.onDragOver = (e) => { + if (node.editor && e.dataTransfer && e.dataTransfer.items) { + return [...e.dataTransfer.items].some(f => f.kind === "file" && f.type.startsWith("image/")); + } + return false; + }; + node.onDragDrop = (e) => { + if (!node.editor) return false; + let handled = false; + for (const file of e.dataTransfer.files) { + if (file.type.startsWith("image/")) { + node.editor.handleImageFile(file); + handled = true; + } + } + return handled; + }; + + for (const name of (hiddenWidgets || [])) { + const w = node.widgets.find(w => w.name === name); + if (w) w.hidden = true; + } + + const element = document.createElement("div"); + node.uuid = makeUUID(); + element.id = `${className}-${node.uuid}`; + node.previewMediaType = 'image'; + + node[editorKey] = node.addDOMWidget(nodeData.name, `${editorClass.name}Widget`, element, { + serialize: false, hideOnZoom: false, + getMinHeight: () => node[heightKey] || 550, + getMaxHeight: () => node[heightKey] || 550, + getHeight: () => node[heightKey] || 550, + }); + node[heightKey] = 550; + + node.contextMenu = createContextMenuElement(menuClassName); + node._menuDef = menuItems; + const menuEls = Object.entries(menuItems).map(([id, def]) => createMenuItem(id, def.label || id)); + setupMenuItems(node.contextMenu, menuEls); + document.body.appendChild(node.contextMenu); + + // Shared helper — loads the stored background image into the editor. + // alignToImage: if true, resizes the node to fit the image first (old behaviour). + // if false, scales the image to fit the current node size (new behaviour). + const _reloadBgImage = (alignToImage) => { + const imgData = node.properties.imgData; + if (!imgData) return; + const img = new Image(); + img.onload = () => { + if (!node.editor) return; + if (alignToImage) { + // Temporarily lift the node-size cap so processImage can expand the node + const savedSize = [node.size[0], node.size[1]]; + node.setSize([Math.min(img.width, maxDisplayDim) + 45, savedSize[1]]); + } + node.editor.processImage(img); + }; + if (imgData.base64) { + img.src = `data:${imgData.type || 'image/png'};base64,${imgData.base64}`; + } else if (imgData.filename) { + img.src = `/view?filename=${encodeURIComponent(imgData.filename)}&type=temp&no-cache=${Date.now()}`; + } + }; + + // Two side-by-side buttons in a single DOM widget row. + // "Reset canvas" — clears points and re-fits the image to the current node size. + // "Align to image" — resizes the node to match the image dimensions (legacy behaviour). + const buttonRow = document.createElement("div"); + buttonRow.style.cssText = "display:flex;gap:4px;width:100%;box-sizing:border-box;"; + const makeRowBtn = (label, onClick) => { + const btn = document.createElement("button"); + btn.textContent = label; + btn.style.cssText = "flex:1;height:24px;padding:0 6px;font:12px sans-serif;cursor:pointer;"; + btn.addEventListener("click", onClick); + return btn; + }; + buttonRow.appendChild(makeRowBtn("Reset canvas", () => { + try { + node.editor = new editorClass(node, true); + _reloadBgImage(false); + } catch (error) { console.error(`Error creating ${editorClass.name}:`, error); } + })); + buttonRow.appendChild(makeRowBtn("Align to image", () => { + try { + if (!node.editor) node.editor = new editorClass(node); + _reloadBgImage(true); + } catch (error) { console.error(`Error aligning ${editorClass.name}:`, error); } + })); + node.addDOMWidget("editor_buttons", "editorButtonRow", buttonRow, { + serialize: false, hideOnZoom: false, + getMinHeight: () => buttonRowHeight, getMaxHeight: () => buttonRowHeight, getHeight: () => buttonRowHeight, + }); + + node.setSize(initialSize); + node[editorKey].parentEl = document.createElement("div"); + node[editorKey].parentEl.className = className; + node[editorKey].parentEl.id = `${className}-${node.uuid}`; + element.appendChild(node[editorKey].parentEl); + + // Auto-create for new nodes; onConfigure cancels this for saved workflows + node._autoCreatePending = setTimeout(() => { + if (!node.editor) { + try { + node.editor = new editorClass(node); + for (const [name, value, type] of (extraProperties || [])) { + node.addProperty(name, value, type); + } + } catch (error) { console.error(`Error creating ${editorClass.name}:`, error); } + } + }, 0); + + BaseEditorCanvas.setupImgDataPersistence(node); + + chainCallback(node, "onResize", function () { + const editor = this.editor; + if (!editor) return; + const newWidth = Math.max(64, Math.round(this.size[0] - 45)); + if (newWidth === editor.width) return; + + // Only change display size — coord space stays the same + editor.width = newWidth; + editor.height = Math.round(newWidth * (editor.coordHeight / editor.coordWidth)); + + editor.resizeCanvas(); + editor.onSizeChanged(); + editor.render(); + }); + + chainCallback(node, "onConfigure", function () { + clearTimeout(this._autoCreatePending); + try { this.editor = new editorClass(this); } + catch (error) { console.error(`Error configuring ${editorClass.name}:`, error); } + // Allow watchImageInputs to operate only after configure + a delay, + // so refreshBackgroundImage has time to restore the saved image first. + node._bgWatchReady = false; + setTimeout(() => { node._bgWatchReady = true; }, 500); + }); + + chainCallback(node, "onExecuted", function (message) { + let bg_image = message["bg_image"]; + if (Array.isArray(bg_image)) bg_image = bg_image[0]; + if (bg_image) { + const img = new Image(); + img.src = `data:image/jpeg;base64,${bg_image}`; + img.onload = () => { + if (this.editor) this.editor.processImage(img); + }; + } + }); + + // Load background image from connected source node (LoadImage, VHS, etc.) + // _bgWatchReady is false during reload until the editor has restored its saved background. + // _bgFromConnectedSource tracks whether the current bg came from a connection (vs execution/paste). + node._bgFromConnectedSource = false; + node._bgWatchReady = true; // true for new nodes, set false during onConfigure + watchImageInputs(node, "bg_image", (sources) => { + if (!node.editor || !node._bgWatchReady) return; + const source = sources[0]; + if (source?.isVideo && source.videoEl) { + // Capture the first/current frame from a VHS-style video preview + node._bgFromConnectedSource = true; + captureVideoFrame(source.videoEl, (canvas) => { + if (node.editor) node.editor.processImage(canvas); + }); + } else if (source && !source.isVideo) { + node._bgFromConnectedSource = true; + const img = new Image(); + img.crossOrigin = "anonymous"; + img.onerror = (e) => console.error("[Editor] source image load error:", e); + img.onload = () => { if (node.editor) node.editor.processImage(img); }; + img.src = source.url; + } else if (!source && node._bgFromConnectedSource) { + node._bgFromConnectedSource = false; + node.editor.clearBackgroundImage(); + } + }); + + chainCallback(node, "onRemoved", function () { + if (this.editor) this.editor.destroy(); + if (this.contextMenu?.parentNode) this.contextMenu.parentNode.removeChild(this.contextMenu); + }); + } + + // ─── Shared constructor flow ─── + // Call from subclass constructor after setting up widgets and data. + // editorKey: e.g. 'pointsEditor' or 'splineEditor' + // heightKey: e.g. 'pointsEditorHeight' or 'splineEditorHeight' + // heightOffset: pixels added to canvas height for full node height (e.g. 310 or 460) + initEditor(editorKey, heightKey, heightOffset) { + this._editorKey = editorKey; + this._heightKey = heightKey; + this._heightOffset = heightOffset; + + this.createCanvas(this.node[editorKey].element); + + if (this.width > 256) this.setNodeWidth(this.width + 45); + this.node[heightKey] = this.height + 40; + this.node.setSize([this.node.size[0], this.height + heightOffset + buttonRowHeight]); + + this.setupEventListeners(); + this.render(); + } + + // Shared onSizeChanged — uses stored heightKey/heightOffset + onSizeChanged() { + this.node[this._heightKey] = this.height + 40; + this.node.setSize([this.node.size[0], this.height + this._heightOffset + buttonRowHeight]); + if (this.node.graph) this.node.graph.setDirtyCanvas(true, true); + } + + // Shared constructor preamble — cleanup, reset, context menu, coord/display init + initEditorPreamble(editorKey, className) { + this._className = className; + this.cleanupPreviousEditor(this.node); + if (this.reset && this.node[editorKey].element) { + this.node[editorKey].element.innerHTML = ''; + } + this.createContextMenu(); + } + + // Shared coord/display size init from widgets + saved node size + initDisplaySize() { + this.coordWidth = this.widthWidget.value; + this.coordHeight = this.heightWidget.value; + const savedWidth = Math.max(64, Math.round(this.node.size[0] - 45)); + this.width = Math.min(savedWidth, maxDisplayDim); + this.height = Math.round(this.width * (this.coordHeight / this.coordWidth)); + } + + // Shared context menu creation — clone to clear stale listeners, wire up action handlers + createContextMenu() { + const oldMenu = this.node.contextMenu; + const newMenu = oldMenu.cloneNode(true); + oldMenu.parentNode.replaceChild(newMenu, oldMenu); + this.node.contextMenu = newMenu; + this.setupContextMenuListeners(this._className); + + const self = this; + newMenu.addEventListener('click', (e) => { + e.preventDefault(); + if (e.target.tagName !== 'A') return; + const id = e.target.dataset.menuId; + const def = this.node._menuDef[id]; + if (def?.action) { + def.action(self); + self._updateMenuToggleStates(); + } + newMenu.style.display = 'none'; + }); + } + + // Update toggle item styling from menu definitions + _updateMenuToggleStates() { + const menuDef = this.node._menuDef; + this.node.contextMenu.querySelectorAll('a').forEach(item => { + const def = menuDef[item.dataset.menuId]; + if (def?.toggle) { + const on = def.toggle(this); + item.style.color = on ? '#4fc3f7' : '#FFF'; + item.style.borderLeft = on ? '3px solid #4fc3f7' : '3px solid transparent'; + item.style.paddingLeft = '8px'; + } + }); + } + + // ─── Hooks for subclasses ─── + // Override these instead of duplicating logic: + + // Called after data changes (render + update widgets) + onDataChanged() {} + + // Called on image load for editor-specific state (e.g., drawRuler = false) + onImageResize() {} + + // Called when coord space changes — override to rescale coordinates + onCoordSpaceResized(_oldWidth, _oldHeight) {} +} diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/web/js/editors/interpolation.js b/ComfyUI/custom_nodes/ComfyUI-KJNodes/web/js/editors/interpolation.js new file mode 100644 index 0000000000000000000000000000000000000000..728f923109856572ba9f6474e92fd842eb21ad13 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-KJNodes/web/js/editors/interpolation.js @@ -0,0 +1,211 @@ +// ─── Interpolation: SVG path d-string builders ─── +// Ported from protovis basis/cardinal/monotone/hermite algorithms + +const Interpolation = { + _basisPoint(t0, p0, t1, p1, t2, p2, t3, p3) { + return t0 * p0 + t1 * p1 + t2 * p2 + t3 * p3; + }, + + _pathBasis(p0, p1, p2, p3) { + const bp = Interpolation._basisPoint; + const x1 = bp(0, p0.x, 2/3, p1.x, 1/3, p2.x, 0, p3.x); + const y1 = bp(0, p0.y, 2/3, p1.y, 1/3, p2.y, 0, p3.y); + const x2 = bp(0, p0.x, 1/3, p1.x, 2/3, p2.x, 0, p3.x); + const y2 = bp(0, p0.y, 1/3, p1.y, 2/3, p2.y, 0, p3.y); + const x = bp(0, p0.x, 1/6, p1.x, 2/3, p2.x, 1/6, p3.x); + const y = bp(0, p0.y, 1/6, p1.y, 2/3, p2.y, 1/6, p3.y); + return `C${x1},${y1},${x2},${y2},${x},${y}`; + }, + + basis(points) { + if (points.length <= 2) return Interpolation.linear(points); + let d = ''; + let b0 = points[0], b1 = points[0], b2 = points[0], b3 = points[1]; + d += Interpolation._pathBasis(b0, b1, b2, b3); + for (let i = 2; i < points.length; i++) { + b0 = b1; b1 = b2; b2 = b3; b3 = points[i]; + d += Interpolation._pathBasis(b0, b1, b2, b3); + } + b0 = b1; b1 = b2; b2 = b3; + d += Interpolation._pathBasis(b0, b1, b2, b3); + b0 = b1; b1 = b2; + d += Interpolation._pathBasis(b0, b1, b2, b3); + return d; + }, + + _cardinalTangents(points, tension) { + const alpha = (1 - tension) / 2; + const tangents = []; + let f = points[0], g = points[1], h = points[2]; + for (let i = 3; i < points.length; i++) { + tangents.push({ x: alpha * (h.x - f.x), y: alpha * (h.y - f.y) }); + f = g; g = h; h = points[i]; + } + tangents.push({ x: alpha * (h.x - f.x), y: alpha * (h.y - f.y) }); + return tangents; + }, + + _hermite(points, tangents) { + if (tangents.length < 1 || + (points.length !== tangents.length && points.length !== tangents.length + 2)) + return ''; + const quad = points.length !== tangents.length; + let d = ''; + let g = points[0], h = points[1], i = tangents[0], j = i, k = 1; + + if (quad) { + d += `Q${h.x - i.x * 2/3},${h.y - i.y * 2/3},${h.x},${h.y}`; + g = points[1]; k = 2; + } + if (tangents.length > 1) { + j = tangents[1]; h = points[k]; k++; + d += `C${g.x + i.x},${g.y + i.y},${h.x - j.x},${h.y - j.y},${h.x},${h.y}`; + for (let idx = 2; idx < tangents.length; idx++, k++) { + h = points[k]; j = tangents[idx]; + d += `S${h.x - j.x},${h.y - j.y},${h.x},${h.y}`; + } + } + if (quad) { + const last = points[k]; + d += `Q${h.x + j.x * 2/3},${h.y + j.y * 2/3},${last.x},${last.y}`; + } + return d; + }, + + cardinal(points, tension) { + if (points.length <= 2) return Interpolation.linear(points); + return Interpolation._hermite(points, Interpolation._cardinalTangents(points, tension)); + }, + + _monotoneTangents(points) { + const n = points.length; + const d = [], m = [], dx = []; + for (let i = 0; i < n - 1; i++) d.push((points[i + 1].y - points[i].y) / (points[i + 1].x - points[i].x)); + m.push(d[0]); + for (let i = 1; i < n - 1; i++) m.push((d[i - 1] + d[i]) / 2); + m.push(d[n - 2]); + dx.push(points[1].x - points[0].x); + for (let i = 1; i < n - 1; i++) dx.push((points[i + 1].x - points[i - 1].x) / 2); + dx.push(points[n - 1].x - points[n - 2].x); + for (let i = 0; i < n - 1; i++) { if (Math.abs(d[i]) < 1e-7) { m[i] = 0; m[i + 1] = 0; } } + for (let i = 0; i < n - 1; i++) { + if (Math.abs(m[i]) >= 1e-5 && Math.abs(m[i + 1]) >= 1e-5) { + const alpha = m[i] / d[i], beta = m[i + 1] / d[i], sigma = alpha * alpha + beta * beta; + if (sigma > 9) { const k = 3 / Math.sqrt(sigma); m[i] = k * alpha * d[i]; m[i + 1] = k * beta * d[i]; } + } + } + const tangents = []; + for (let i = 0; i < n; i++) { const denom = 1 + m[i] * m[i]; tangents.push({ x: dx[i] / 3 / denom, y: m[i] * dx[i] / 3 / denom }); } + return tangents; + }, + + monotone(points) { + if (points.length <= 2) return Interpolation.linear(points); + return Interpolation._hermite(points, Interpolation._monotoneTangents(points)); + }, + + linear(points) { let d = ''; for (let i = 1; i < points.length; i++) d += `L${points[i].x},${points[i].y}`; return d; }, + stepBefore(points) { let d = ''; for (let i = 1; i < points.length; i++) d += `V${points[i].y}H${points[i].x}`; return d; }, + stepAfter(points) { let d = ''; for (let i = 1; i < points.length; i++) d += `H${points[i].x}V${points[i].y}`; return d; }, + + bezier(points) { + let d = ''; + for (let i = 1; i < points.length; i++) { + const prev = points[i - 1], cur = points[i]; + d += `C${prev.h2x ?? prev.x},${prev.h2y ?? prev.y},${cur.h1x ?? cur.x},${cur.h1y ?? cur.y},${cur.x},${cur.y}`; + } + return d; + }, + + ensureBezierHandles(points) { + for (let i = 0; i < points.length; i++) { + if (points[i].h1x !== undefined) continue; + const prev = points[Math.max(0, i - 1)], next = points[Math.min(points.length - 1, i + 1)]; + const dx = (next.x - prev.x) * 0.25, dy = (next.y - prev.y) * 0.25; + points[i].h1x = points[i].x - dx; points[i].h1y = points[i].y - dy; + points[i].h2x = points[i].x + dx; points[i].h2y = points[i].y + dy; + } + }, + + buildPathD(points, interpolation, tension) { + if (!points || points.length === 0) return ''; + let d = `M${points[0].x},${points[0].y}`; + if (points.length === 1) return d; + switch (interpolation) { + case 'basis': d += Interpolation.basis(points); break; + case 'cardinal': d += Interpolation.cardinal(points, tension); break; + case 'monotone': d += Interpolation.monotone(points); break; + case 'step-before': d += Interpolation.stepBefore(points); break; + case 'step-after': d += Interpolation.stepAfter(points); break; + case 'bezier': Interpolation.ensureBezierHandles(points); d += Interpolation.bezier(points); break; + case 'linear': default: d += Interpolation.linear(points); break; + } + return d; + }, + + drawOnCanvas(ctx, points, interpolation, tension) { + if (!points || points.length < 2) return; + ctx.beginPath(); + ctx.moveTo(points[0].x, points[0].y); + switch (interpolation) { + case 'basis': + if (points.length < 3) { ctx.lineTo(points[1].x, points[1].y); break; } + Interpolation._drawBasisCanvas(ctx, points); break; + case 'cardinal': + if (points.length < 3) { ctx.lineTo(points[1].x, points[1].y); break; } + Interpolation._drawHermiteCanvas(ctx, points, Interpolation._cardinalTangents(points, tension)); break; + case 'monotone': + if (points.length < 3) { ctx.lineTo(points[1].x, points[1].y); break; } + Interpolation._drawHermiteCanvas(ctx, points, Interpolation._monotoneTangents(points)); break; + case 'step-before': + for (let i = 1; i < points.length; i++) { ctx.lineTo(points[i - 1].x, points[i].y); ctx.lineTo(points[i].x, points[i].y); } break; + case 'step-after': + for (let i = 1; i < points.length; i++) { ctx.lineTo(points[i].x, points[i - 1].y); ctx.lineTo(points[i].x, points[i].y); } break; + case 'bezier': + Interpolation.ensureBezierHandles(points); + for (let i = 1; i < points.length; i++) { + const prev = points[i - 1], cur = points[i]; + ctx.bezierCurveTo(prev.h2x ?? prev.x, prev.h2y ?? prev.y, cur.h1x ?? cur.x, cur.h1y ?? cur.y, cur.x, cur.y); + } break; + case 'linear': default: + for (let i = 1; i < points.length; i++) ctx.lineTo(points[i].x, points[i].y); break; + } + }, + + _drawBasisCanvas(ctx, points) { + const bp = Interpolation._basisPoint; + const drawSeg = (p0, p1, p2, p3) => { + ctx.bezierCurveTo( + bp(0,p0.x,2/3,p1.x,1/3,p2.x,0,p3.x), bp(0,p0.y,2/3,p1.y,1/3,p2.y,0,p3.y), + bp(0,p0.x,1/3,p1.x,2/3,p2.x,0,p3.x), bp(0,p0.y,1/3,p1.y,2/3,p2.y,0,p3.y), + bp(0,p0.x,1/6,p1.x,2/3,p2.x,1/6,p3.x), bp(0,p0.y,1/6,p1.y,2/3,p2.y,1/6,p3.y)); + }; + let b0 = points[0], b1 = points[0], b2 = points[0], b3 = points[1]; + drawSeg(b0, b1, b2, b3); + for (let i = 2; i < points.length; i++) { b0 = b1; b1 = b2; b2 = b3; b3 = points[i]; drawSeg(b0, b1, b2, b3); } + b0 = b1; b1 = b2; b2 = b3; drawSeg(b0, b1, b2, b3); + b0 = b1; b1 = b2; drawSeg(b0, b1, b2, b3); + }, + + _drawHermiteCanvas(ctx, points, tangents) { + if (tangents.length < 1 || (points.length !== tangents.length && points.length !== tangents.length + 2)) return; + const quad = points.length !== tangents.length; + let g = points[0], h = points[1], i = tangents[0], j = i, k = 1; + let prevCp2x, prevCp2y; + if (quad) { ctx.quadraticCurveTo(h.x - i.x * 2/3, h.y - i.y * 2/3, h.x, h.y); g = points[1]; k = 2; } + if (tangents.length > 1) { + j = tangents[1]; h = points[k]; k++; + prevCp2x = h.x - j.x; prevCp2y = h.y - j.y; + ctx.bezierCurveTo(g.x + i.x, g.y + i.y, prevCp2x, prevCp2y, h.x, h.y); + for (let idx = 2; idx < tangents.length; idx++, k++) { + const prevH = h; h = points[k]; j = tangents[idx]; + const rcp1x = 2 * prevH.x - prevCp2x, rcp1y = 2 * prevH.y - prevCp2y; + prevCp2x = h.x - j.x; prevCp2y = h.y - j.y; + ctx.bezierCurveTo(rcp1x, rcp1y, prevCp2x, prevCp2y, h.x, h.y); + } + } + if (quad) { const last = points[k]; ctx.quadraticCurveTo(h.x + j.x * 2/3, h.y + j.y * 2/3, last.x, last.y); } + } +}; + +export { Interpolation }; diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/web/js/editors/point_editor_canvas.js b/ComfyUI/custom_nodes/ComfyUI-KJNodes/web/js/editors/point_editor_canvas.js new file mode 100644 index 0000000000000000000000000000000000000000..395825992b8599c83bf8c6c71dccbd18e7b3568a --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-KJNodes/web/js/editors/point_editor_canvas.js @@ -0,0 +1,370 @@ +import { chainCallback, rectHitTest, cursorForBboxMode } from '../utility.js'; +import { BaseEditorCanvas, createEditorStylesheet } from './editor_base.js'; +const { app } = window.comfyAPI.app; + +createEditorStylesheet('kj-pointseditor-stylesheet', 'points-editor'); + +app.registerExtension({ + name: 'KJNodes.PointEditor', + + async beforeRegisterNodeDef(nodeType, nodeData) { + if (nodeData?.name === 'PointsEditor') { + chainCallback(nodeType.prototype, "onNodeCreated", function () { + BaseEditorCanvas.setupNode(this, nodeData, { + editorClass: PointsEditor, + editorKey: 'pointsEditor', + heightKey: 'pointsEditorHeight', + className: 'points-editor', + menuItems: { + "Load Image": { action: (ed) => ed.openImageFilePicker() }, + "Clear Image": { action: (ed) => ed.clearBackgroundImage() }, + }, + hiddenWidgets: ["coordinates", "neg_coordinates", "bboxes"], + initialSize: [550, 550], + extraProperties: [ + ["points", this.constructor.type, "string"], + ["neg_points", this.constructor.type, "string"], + ], + }); + }); + } + } +}); + +class PointsEditor extends BaseEditorCanvas { + constructor(context, reset = false) { + super(context, reset); + this.initEditorPreamble('pointsEditor', 'points-editor'); + + this.pos_coordWidget = this.findWidget("coordinates"); + this.neg_coordWidget = this.findWidget("neg_coordinates"); + this.pointsStoreWidget = this.findWidget("points_store"); + this.widthWidget = this.findWidget("width"); + this.heightWidget = this.findWidget("height"); + this.bboxStoreWidget = this.findWidget("bbox_store"); + this.bboxWidget = this.findWidget("bboxes"); + + this.setupSizeCallbacks(); + this.pointsStoreWidget.callback = () => { + try { + const parsed = JSON.parse(this.pointsStoreWidget.value); + this.points = parsed.positive || []; + this.neg_points = parsed.negative || []; + } catch (e) { console.error("Error parsing points data:", e); } + this.render(); + this.updateData(); + }; + this.bboxStoreWidget.callback = () => { + try { this.bbox = JSON.parse(this.bboxStoreWidget.value); } + catch (e) { console.error("Error parsing bbox data:", e); } + this.render(); + this.updateData(); + }; + + this.initDisplaySize(); + this.points = []; + this.neg_points = []; + this.bbox = [{}]; + this.drawing = false; + this.selectedIndex = -1; + this.pointRadius = Math.max(4, Math.log(Math.max(1, Math.min(this.coordWidth, this.coordHeight))) * 4); + + if (!reset && this.pointsStoreWidget.value !== "") { + try { + const parsed = JSON.parse(this.pointsStoreWidget.value); + this.points = parsed.positive || []; + this.neg_points = parsed.negative || []; + this.bbox = JSON.parse(this.bboxStoreWidget.value); + } catch (e) { + console.error("Error parsing stored points:", e); + this.points = [{ x: this.coordWidth / 2, y: this.coordHeight / 2 }]; + this.neg_points = []; + this.bbox = [{}]; + } + } else { + this.points = [{ x: this.coordWidth / 2, y: this.coordHeight / 2 }]; + this.neg_points = []; + this.pointsStoreWidget.value = JSON.stringify({ positive: this.points, negative: this.neg_points }); + this.bboxStoreWidget.value = JSON.stringify(this.bbox); + } + + this.initEditor('pointsEditor', 'pointsEditorHeight', 310); + this.updateData(); + this.refreshBackgroundImage(); + } + + onDataChanged() { this.updateData(); } + + onCoordSpaceResized(oldWidth, oldHeight) { + const sx = this.coordWidth / oldWidth, sy = this.coordHeight / oldHeight; + const clampPt = (p) => { p.x = Math.max(0, Math.min(this.coordWidth, p.x * sx)); p.y = Math.max(0, Math.min(this.coordHeight, p.y * sy)); }; + for (const p of this.points) clampPt(p); + for (const p of this.neg_points) clampPt(p); + for (const b of this.bbox) { + if (b.startX != null) { + b.startX = Math.max(0, Math.min(this.coordWidth, b.startX * sx)); + b.startY = Math.max(0, Math.min(this.coordHeight, b.startY * sy)); + b.endX = Math.max(0, Math.min(this.coordWidth, b.endX * sx)); + b.endY = Math.max(0, Math.min(this.coordHeight, b.endY * sy)); + } + } + this.pointRadius = Math.max(4, Math.log(Math.max(1, Math.min(this.coordWidth, this.coordHeight))) * 4); + } + + // ─── Hit Testing ─── + + findPointAt(x, y, pointsArray) { + // Convert pixel radius to coord space + const r = this.pointRadius / Math.min(this.scaleX, this.scaleY); + for (let i = pointsArray.length - 1; i >= 0; i--) { + const p = pointsArray[i]; + const dx = p.x - x, dy = p.y - y; + if (dx * dx + dy * dy <= r * r) return i; + } + return -1; + } + + normBbox() { + const b = this.bbox[0]; + if (!b || Object.keys(b).length === 0) return null; + return { + x1: Math.min(b.startX, b.endX), y1: Math.min(b.startY, b.endY), + x2: Math.max(b.startX, b.endX), y2: Math.max(b.startY, b.endY), + }; + } + + hitTestBbox(mx, my) { + const n = this.normBbox(); + if (!n) return null; + return rectHitTest(mx, my, n.x1, n.y1, n.x2, n.y2, 10 / Math.min(this.scaleX, this.scaleY)); + } + + // ─── Mouse Handlers ─── + + onMouseDown(e) { + const mouse = this.getLocalMouse(e); + const clamped = this.clamp(mouse.x, mouse.y); + + if (e.shiftKey && e.button === 2) { + this.neg_points.push({ x: clamped.x, y: clamped.y }); + this.dragType = 'negative'; this.dragIndex = this.neg_points.length - 1; + this.selectedIndex = this.dragIndex; + this.dragOffset = { x: 0, y: 0 }; + this.render(); this.updateData(); this.startDocumentDrag(e); return; + } + if (e.shiftKey && e.button === 0) { + this.points.push({ x: clamped.x, y: clamped.y }); + this.dragType = 'positive'; this.dragIndex = this.points.length - 1; + this.selectedIndex = this.dragIndex; + this.dragOffset = { x: 0, y: 0 }; + this.render(); this.updateData(); this.startDocumentDrag(e); return; + } + + if (e.button === 2) { + const posIdx = this.findPointAt(mouse.x, mouse.y, this.points); + if (posIdx >= 0) { this.points.splice(posIdx, 1); this.render(); this.updateData(); return; } + const negIdx = this.findPointAt(mouse.x, mouse.y, this.neg_points); + if (negIdx >= 0) { this.neg_points.splice(negIdx, 1); this.render(); this.updateData(); return; } + const bboxHit = this.hitTestBbox(mouse.x, mouse.y); + if (bboxHit) { this.bbox = [{}]; this.render(); this.updateData(); return; } + this.showContextMenu(e); + return; + } + + if (e.button !== 0) return; + + if (e.ctrlKey) { + this.drawing = true; + this.dragType = 'bbox-draw'; + this.bbox[0] = { startX: clamped.x, startY: clamped.y, endX: clamped.x, endY: clamped.y }; + this.render(); this.startDocumentDrag(e); return; + } + + const bboxHit = this.hitTestBbox(mouse.x, mouse.y); + if (bboxHit) { + const n = this.normBbox(); + this.bboxAtDragStart = { startX: n.x1, startY: n.y1, endX: n.x2, endY: n.y2 }; + this.dragStart = { x: mouse.x, y: mouse.y }; + this.dragType = bboxHit === "move" ? 'bbox-move' : bboxHit; + this.dragIndex = 0; + this.startDocumentDrag(e); return; + } + + const posIdx = this.findPointAt(mouse.x, mouse.y, this.points); + if (posIdx >= 0) { + this.dragType = 'positive'; this.dragIndex = posIdx; this.selectedIndex = posIdx; + this.dragOffset = { x: mouse.x - this.points[posIdx].x, y: mouse.y - this.points[posIdx].y }; + this.render(); this.startDocumentDrag(e); return; + } + + const negIdx = this.findPointAt(mouse.x, mouse.y, this.neg_points); + if (negIdx >= 0) { + this.dragType = 'negative'; this.dragIndex = negIdx; this.selectedIndex = negIdx; + this.dragOffset = { x: mouse.x - this.neg_points[negIdx].x, y: mouse.y - this.neg_points[negIdx].y }; + this.render(); this.startDocumentDrag(e); return; + } + } + + onMouseMove(e) { + const mouse = this.getLocalMouse(e); + const clamped = this.clamp(mouse.x, mouse.y); + + if (!this.drawing && this.dragIndex < 0 && !this.dragType) { + const bboxHit = this.hitTestBbox(mouse.x, mouse.y); + const bboxCursor = cursorForBboxMode(bboxHit); + if (bboxCursor) { this.canvas.style.cursor = bboxCursor; } + else if (this.findPointAt(mouse.x, mouse.y, this.points) >= 0 || + this.findPointAt(mouse.x, mouse.y, this.neg_points) >= 0) { this.canvas.style.cursor = "move"; } + else { this.canvas.style.cursor = "default"; } + return; + } + + if (this.dragType === 'bbox-draw') { + this.bbox[0].endX = clamped.x; this.bbox[0].endY = clamped.y; + this.render(); return; + } + + if (this.dragType === 'bbox-move' && this.bboxAtDragStart) { + const dx = mouse.x - this.dragStart.x, dy = mouse.y - this.dragStart.y; + const bs = this.bboxAtDragStart; + const bw = bs.endX - bs.startX, bh = bs.endY - bs.startY; + const nx = Math.max(0, Math.min(this.coordWidth - bw, bs.startX + dx)); + const ny = Math.max(0, Math.min(this.coordHeight - bh, bs.startY + dy)); + this.bbox[0] = { startX: nx, startY: ny, endX: nx + bw, endY: ny + bh }; + this.render(); return; + } + + if (this.dragType?.startsWith('resize-') && this.bboxAtDragStart) { + const bs = this.bboxAtDragStart; + const offX = this.dragStart.x - (this.dragType === 'resize-tl' || this.dragType === 'resize-bl' ? bs.startX : bs.endX); + const offY = this.dragStart.y - (this.dragType === 'resize-tl' || this.dragType === 'resize-tr' ? bs.startY : bs.endY); + const cx = Math.max(0, Math.min(this.coordWidth, mouse.x - offX)); + const cy = Math.max(0, Math.min(this.coordHeight, mouse.y - offY)); + if (this.dragType === 'resize-tl') this.bbox[0] = { startX: bs.endX, startY: bs.endY, endX: cx, endY: cy }; + else if (this.dragType === 'resize-tr') this.bbox[0] = { startX: bs.startX, startY: bs.endY, endX: cx, endY: cy }; + else if (this.dragType === 'resize-bl') this.bbox[0] = { startX: bs.endX, startY: bs.startY, endX: cx, endY: cy }; + else this.bbox[0] = { startX: bs.startX, startY: bs.startY, endX: cx, endY: cy }; + this.render(); return; + } + + if (this.dragType === 'positive' && this.dragIndex >= 0 && this.dragIndex < this.points.length) { + const ox = this.dragOffset?.x || 0, oy = this.dragOffset?.y || 0; + const target = this.clamp(mouse.x - ox, mouse.y - oy); + this.points[this.dragIndex] = { x: target.x, y: target.y }; + this.render(); + } else if (this.dragType === 'negative' && this.dragIndex >= 0 && this.dragIndex < this.neg_points.length) { + const ox = this.dragOffset?.x || 0, oy = this.dragOffset?.y || 0; + const target = this.clamp(mouse.x - ox, mouse.y - oy); + this.neg_points[this.dragIndex] = { x: target.x, y: target.y }; + this.render(); + } + } + + onMouseUp() { + if (this.drawing || this.dragType) { + this.drawing = false; + this.bboxAtDragStart = null; + this.dragStart = null; + this.endDrag(); + this.updateData(); + } + } + + // ─── Rendering ─── + + _render() { + const ctx = this.ctx; + this.beginRender(); + + // Bounding box + const nb = this.normBbox(); + if (nb) { + const tl = this.toCanvas(nb.x1, nb.y1), br = this.toCanvas(nb.x2, nb.y2); + const bw = br.x - tl.x, bh = br.y - tl.y; + ctx.fillStyle = 'rgba(70, 130, 180, 0.3)'; + ctx.fillRect(tl.x, tl.y, bw, bh); + ctx.strokeStyle = 'rgba(0, 0, 0, 0.5)'; ctx.lineWidth = 4; + ctx.strokeRect(tl.x, tl.y, bw, bh); + ctx.strokeStyle = 'steelblue'; ctx.lineWidth = 2; + ctx.strokeRect(tl.x, tl.y, bw, bh); + // Center cross + const cx = (tl.x + br.x) / 2, cy = (tl.y + br.y) / 2; + ctx.strokeStyle = 'steelblue'; ctx.lineWidth = 1; + ctx.beginPath(); ctx.moveTo(cx - 5, cy); ctx.lineTo(cx + 5, cy); + ctx.moveTo(cx, cy - 5); ctx.lineTo(cx, cy + 5); ctx.stroke(); + // Corner handles + const hs = 5; + ctx.fillStyle = 'steelblue'; + for (const [hx, hy] of [[tl.x, tl.y], [br.x, tl.y], [tl.x, br.y], [br.x, br.y]]) { + ctx.fillRect(hx - hs, hy - hs, hs * 2, hs * 2); + } + } + + // Points + this._badgePositions = []; + this._drawPoints(ctx, this.points, '#139613', '#07f907', 'positive'); + // Negative points + this._drawPoints(ctx, this.neg_points, '#891616', '#f91111', 'negative'); + + this.endRender(); + } + + _drawPoints(ctx, points, color, selectedColor, type) { + const r = this.pointRadius; + + for (let i = 0; i < points.length; i++) { + const p = points[i]; + const cp = this.toCanvas(p.x, p.y); + const isSelected = (this.dragType === type && this.dragIndex === i); + // Outer circle + ctx.beginPath(); ctx.arc(cp.x, cp.y, r, 0, Math.PI * 2); + ctx.fillStyle = 'rgba(100, 100, 100, 0.25)'; ctx.fill(); + ctx.strokeStyle = isSelected ? selectedColor : color; ctx.lineWidth = 2; ctx.stroke(); + // Center dot + ctx.beginPath(); ctx.arc(cp.x, cp.y, 1.5, 0, Math.PI * 2); + ctx.fillStyle = 'red'; ctx.fill(); + // Index badge + const label = i.toString(); + const badgeR = label.length > 1 ? 8 : 7; + const off = r * 0.7; + const candidates = [ + { x: cp.x + off, y: cp.y - off }, + { x: cp.x - off, y: cp.y - off }, + { x: cp.x + off, y: cp.y + off }, + { x: cp.x - off, y: cp.y + off }, + ]; + const valid = candidates.filter(c => c.x - badgeR >= 0 && c.x + badgeR <= this.width && c.y - badgeR >= 0 && c.y + badgeR <= this.height); + const pool = valid.length > 0 ? valid : candidates; + let badge = pool[0]; + const minDist = badgeR * 2.2; + for (const c of pool) { + const overlaps = this._badgePositions.some(b => Math.abs(c.x - b.x) < minDist && Math.abs(c.y - b.y) < minDist); + if (!overlaps) { badge = c; break; } + } + this._badgePositions.push(badge); + + ctx.font = 'bold 10px sans-serif'; + ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; + ctx.beginPath(); ctx.arc(badge.x, badge.y, badgeR, 0, Math.PI * 2); + ctx.fillStyle = isSelected ? selectedColor : color; ctx.fill(); + ctx.fillStyle = '#fff'; + ctx.fillText(label, badge.x, badge.y + 0.5); + ctx.textAlign = 'start'; ctx.textBaseline = 'alphabetic'; + } + } + + // ─── Data ─── + + updateData() { + const combinedPoints = { positive: this.points || [], negative: this.neg_points || [] }; + this.pointsStoreWidget.value = JSON.stringify(combinedPoints); + this.pos_coordWidget.value = JSON.stringify(this.points || []); + this.neg_coordWidget.value = JSON.stringify(this.neg_points || []); + if (this.bbox.length !== 0) { + let bboxString = JSON.stringify(this.bbox); + this.bboxStoreWidget.value = bboxString; + this.bboxWidget.value = bboxString; + } + } + +} diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/web/js/editors/spline_editor_canvas.js b/ComfyUI/custom_nodes/ComfyUI-KJNodes/web/js/editors/spline_editor_canvas.js new file mode 100644 index 0000000000000000000000000000000000000000..00c943de6c965a5416c3efc4ffcda50493935f69 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-KJNodes/web/js/editors/spline_editor_canvas.js @@ -0,0 +1,720 @@ +import { chainCallback } from '../utility.js'; +import { BaseEditorCanvas, createEditorStylesheet } from './editor_base.js'; +import { Interpolation } from './interpolation.js'; +const { app } = window.comfyAPI.app; + +createEditorStylesheet('kj-splineeditor-stylesheet', 'spline-editor'); + +// ─── Hidden SVG for path sampling (singleton) ─── +let _sharedSampler = null; +let _samplerRefCount = 0; +class PathSampler { + constructor() { + const ns = 'http://www.w3.org/2000/svg'; + this._svg = document.createElementNS(ns, 'svg'); + this._svg.style.cssText = 'position:absolute;width:0;height:0;overflow:hidden;pointer-events:none;'; + this._svg.setAttribute('aria-hidden', 'true'); + document.body.appendChild(this._svg); + this._path = document.createElementNS(ns, 'path'); + this._svg.appendChild(this._path); + } + static acquire() { + if (_sharedSampler) { _samplerRefCount++; return _sharedSampler; } + _sharedSampler = new PathSampler(); + _samplerRefCount = 1; + return _sharedSampler; + } + setPath(d) { this._path.setAttribute('d', d); } + getTotalLength() { return this._path.getTotalLength(); } + getPointAtLength(len) { return this._path.getPointAtLength(len); } + release() { + _samplerRefCount--; + if (_samplerRefCount <= 0 && this._svg && this._svg.parentNode) { + this._svg.parentNode.removeChild(this._svg); + _sharedSampler = null; + } + } + findPointAtX(targetX, pathLength) { + let low = 0, high = pathLength, bestPoint = this.getPointAtLength(0); + const epsilon = 0.5; + while (high - low > epsilon) { + const mid = (low + high) / 2, point = this.getPointAtLength(mid); + if (Math.abs(point.x - targetX) < Math.abs(bestPoint.x - targetX)) bestPoint = point; + if (Math.abs(point.x - targetX) < 0.5) return point; + if (point.x < targetX) low = mid; else high = mid; + } + return bestPoint; + } +} + +// ─── Extension Registration ─── +app.registerExtension({ + name: 'KJNodes.SplineEditor', + + init() { + app.ui.settings.addSetting({ + id: "KJNodes.splineEditor.showControlLines", + name: "Show control lines", + category: ["KJNodes", "Editors", "Show control lines"], + tooltip: "Display straight lines between control points", + type: "boolean", + defaultValue: false, + }); + app.ui.settings.addSetting({ + id: "KJNodes.splineEditor.showSamplePoints", + name: "Show sample points", + category: ["KJNodes", "Editors", "Show sample points"], + tooltip: "Display the sampled output points as red dots", + type: "boolean", + defaultValue: false, + }); + app.ui.settings.addSetting({ + id: "KJNodes.splineEditor.showArrows", + name: "Show direction arrows", + category: ["KJNodes", "Editors", "Show direction arrows"], + tooltip: "Display chevron arrows on control points showing curve direction", + type: "boolean", + defaultValue: true, + }); + app.ui.settings.addSetting({ + id: "KJNodes.editors.embedBackgroundImage", + name: "Embed background image in workflow", + category: ["KJNodes", "Editors", "Embed background image"], + tooltip: "When enabled, editor background images are embedded as base64 in the workflow file (portable, larger files). When disabled, images are stored as temp files on the server (small workflows, survives refresh, but not portable).", + type: "boolean", + defaultValue: false, + }); + }, + + async beforeRegisterNodeDef(nodeType, nodeData) { + if (nodeData?.name === 'SplineEditor') { + chainCallback(nodeType.prototype, "onNodeCreated", function () { + BaseEditorCanvas.setupNode(this, nodeData, { + editorClass: SplineEditor, + editorKey: 'splineEditor', + heightKey: 'splineEditorHeight', + className: 'spline-editor', + menuClassName: 'spline-editor-context-menu', + menuItems: { + "Display control lines": { toggle: (ed) => ed.drawHandles, action: (ed) => { ed.drawHandles = !ed.drawHandles; app.ui.settings.setSettingValue("KJNodes.splineEditor.showControlLines", ed.drawHandles); ed.render(); } }, + "Display sample points": { toggle: (ed) => ed.drawSamplePoints, action: (ed) => { ed.drawSamplePoints = !ed.drawSamplePoints; app.ui.settings.setSettingValue("KJNodes.splineEditor.showSamplePoints", ed.drawSamplePoints); ed.updatePath(); } }, + "Display arrows": { toggle: (ed) => ed.showChevrons, action: (ed) => { ed.showChevrons = !ed.showChevrons; app.ui.settings.setSettingValue("KJNodes.splineEditor.showArrows", ed.showChevrons); ed.render(); } }, + "Background image": { action: (ed) => ed.openImageFilePicker() }, + "Invert point order": { action: (ed) => { ed.splines[ed.activeSplineIndex].points.reverse(); ed.render(); ed.updatePath(); } }, + "Clear Image": { action: (ed) => { ed.clearBackgroundImage(); ed.drawRuler = true; } }, + "Add new spline": { action: (ed) => { const idx = ed.splines.length; ed.splines.push({ points: [{ x: 0, y: ed.coordHeight }, { x: ed.coordWidth / 2, y: ed.coordHeight / 2 }, { x: ed.coordWidth, y: 0 }], color: ed.getSplineColor(idx), name: `Spline ${idx + 1}` }); ed.activeSplineIndex = idx; ed.render(); ed.updatePath(); } }, + "Add new single point": { action: (ed) => { const idx = ed.splines.length; ed.splines.push({ points: [{ x: ed.lastContextMenuPos.x, y: ed.lastContextMenuPos.y }], color: ed.getSplineColor(idx), name: `Spline ${idx + 1}`, isSinglePoint: true }); ed.activeSplineIndex = idx; ed.render(); ed.updatePath(); } }, + "Delete current spline": { action: (ed) => { if (ed.splines.length > 1) { ed.splines.splice(ed.activeSplineIndex, 1); ed.activeSplineIndex = Math.min(ed.activeSplineIndex, ed.splines.length - 1); ed.render(); ed.updatePath(); } } }, + "Next spline": { action: (ed) => { ed.activeSplineIndex = (ed.activeSplineIndex + 1) % ed.splines.length; ed.render(); ed.updatePath(); } }, + }, + hiddenWidgets: ["coordinates"], + initialSize: [550, 1000], + extraProperties: [ + ["points", this.constructor.type, "string"], + ], + }); + }); + } + } +}); + +// ─── SplineEditor class ─── +class SplineEditor extends BaseEditorCanvas { + constructor(context, reset = false) { + super(context, reset); + this.initEditorPreamble('splineEditor', 'spline-editor'); + + this.sampler = PathSampler.acquire(); + this.drawSamplePoints = app.ui.settings.getSettingValue("KJNodes.splineEditor.showSamplePoints") ?? false; + this.drawHandles = app.ui.settings.getSettingValue("KJNodes.splineEditor.showControlLines") ?? false; + this.showChevrons = app.ui.settings.getSettingValue("KJNodes.splineEditor.showArrows") ?? true; + this.drawRuler = true; + + this.coordWidget = this.findWidget("coordinates"); + this.interpolationWidget = this.findWidget("interpolation"); + this.pointsWidget = this.findWidget("points_to_sample"); + this.pointsStoreWidget = this.findWidget("points_store"); + this.tensionWidget = this.findWidget("tension"); + this.samplingMethodWidget = this.findWidget("sampling_method"); + this.widthWidget = this.findWidget("mask_width"); + this.heightWidget = this.findWidget("mask_height"); + + this.interpolationWidget.callback = () => this.updatePath(true); + this.samplingMethodWidget.callback = () => { + if (this.samplingMethod === "controlpoints") this.drawSamplePoints = true; + else this.drawSamplePoints = false; + if (this.samplingMethod === "path" || this.samplingMethod === "speed") this.showChevrons = true; + this.updatePath(true); + }; + this.tensionWidget.callback = () => this.updatePath(true); + this.pointsWidget.callback = () => this.updatePath(true); + this.setupSizeCallbacks(); + this.pointsStoreWidget.callback = () => { + this.parseSplineData(); + this.render(); this.updatePath(true); + }; + + this.initDisplaySize(); + this.splines = []; + this.activeSplineIndex = 0; + this.hoverSplineIndex = -1; + this.hoverIndex = -1; + this.sampledCoords = null; + this.lastContextMenuPos = { x: this.coordWidth / 2, y: this.coordHeight / 2 }; + + if (!reset && this.pointsStoreWidget.value !== "") { + this.parseSplineData(); + } else { + this.initializeDefaultSplines(); + this.pointsStoreWidget.value = JSON.stringify(this.splines); + } + + this._onKeyUp = (e) => { + if (e.key === 'Control' && this.subdividePreview) { this.subdividePreview = null; this.render(); } + }; + document.addEventListener('keyup', this._onKeyUp); + + this.initEditor('splineEditor', 'splineEditorHeight', 460); + this.updatePath(); + this.refreshBackgroundImage(); + } + + // ─── Widget value getters ─── + get interpolation() { return this.interpolationWidget.value; } + get tension() { return this.tensionWidget.value; } + get points_to_sample() { return this.pointsWidget.value; } + get samplingMethod() { return this.samplingMethodWidget.value; } + + // ─── Base class hooks ─── + + destroy() { + super.destroy(); + if (this.sampler) this.sampler.release(); + } + + onDataChanged() { this.updatePath(true); } + + onImageResize() { this.drawRuler = false; } + + onCoordSpaceResized(oldWidth, oldHeight) { + const sx = this.coordWidth / oldWidth, sy = this.coordHeight / oldHeight; + const cw = this.coordWidth, ch = this.coordHeight; + const clamp = (v, max) => Math.max(0, Math.min(max, v)); + for (const spline of this.splines) { + for (const p of spline.points) { + p.x = clamp(p.x * sx, cw); p.y = clamp(p.y * sy, ch); + if (p.h1x !== undefined) { p.h1x = clamp(p.h1x * sx, cw); p.h1y = clamp(p.h1y * sy, ch); } + if (p.h2x !== undefined) { p.h2x = clamp(p.h2x * sx, cw); p.h2y = clamp(p.h2y * sy, ch); } + } + } + } + + // ─── Spline Helpers ─── + + parseSplineData() { + try { + const parsed = JSON.parse(this.pointsStoreWidget.value); + if (Array.isArray(parsed) && parsed.length > 0 && parsed[0].hasOwnProperty('points')) { + this.splines = parsed; + } else { + this.splines = [{ points: parsed, color: "#1f77b4", name: "Spline 1" }]; + } + } catch (e) { + console.error("Error parsing spline data:", e); + this.initializeDefaultSplines(); + } + if (this.activeSplineIndex >= this.splines.length) { + this.activeSplineIndex = Math.max(0, this.splines.length - 1); + } + } + + initializeDefaultSplines() { + this.splines = [{ + points: [ + { x: 0, y: this.coordHeight }, + { x: this.coordWidth / 5 * 2, y: 50 + Math.random() * (this.coordHeight - 100) }, + { x: this.coordWidth, y: 0 } + ], + color: this.getSplineColor(0), + name: "Spline 1" + }]; + } + + getSplineColor(index) { + const colors = ["#1f77b4", "#ff7f0e", "#2ca02c", "#d62728", "#9467bd", "#8c564b", "#e377c2", "#7f7f7f", "#bcbd22", "#17becf"]; + return colors[index % colors.length]; + } + + // ─── Hit Testing ─── + + findPointAt(x, y) { + const activePoints = this.splines[this.activeSplineIndex]?.points; + if (!activePoints) return -1; + const r = 12 / Math.min(this.scaleX, this.scaleY); + for (let i = activePoints.length - 1; i >= 0; i--) { + const dx = activePoints[i].x - x, dy = activePoints[i].y - y; + if (dx * dx + dy * dy <= r * r) return i; + } + return -1; + } + + findHandleAt(x, y) { + if (this.interpolation !== 'bezier') return null; + const activePoints = this.splines[this.activeSplineIndex]?.points; + if (!activePoints) return null; + const r = 8 / Math.min(this.scaleX, this.scaleY); + for (let i = activePoints.length - 1; i >= 0; i--) { + const p = activePoints[i]; + if (i > 0 && p.h1x !== undefined) { const dx = p.h1x - x, dy = p.h1y - y; if (dx * dx + dy * dy <= r * r) return { index: i, type: 'h1' }; } + if (i < activePoints.length - 1 && p.h2x !== undefined) { const dx = p.h2x - x, dy = p.h2y - y; if (dx * dx + dy * dy <= r * r) return { index: i, type: 'h2' }; } + } + return null; + } + + findSplineAt(x, y) { + const threshold = 15 / Math.min(this.scaleX, this.scaleY); + let bestDist = Infinity, bestIdx = -1; + for (let si = 0; si < this.splines.length; si++) { + const spline = this.splines[si]; + if (spline.isSinglePoint || (spline.points && spline.points.length === 1)) { + const p = spline.points[0], d = Math.sqrt((p.x - x) ** 2 + (p.y - y) ** 2); + if (d < threshold && d < bestDist) { bestDist = d; bestIdx = si; } + continue; + } + const pathD = Interpolation.buildPathD(spline.points, this.interpolation, this.tension); + this.sampler.setPath(pathD); + const len = this.sampler.getTotalLength(); + const steps = Math.min(200, Math.max(50, Math.round(len / 3))); + for (let i = 0; i <= steps; i++) { + const pt = this.sampler.getPointAtLength(len * i / steps); + const d = Math.sqrt((pt.x - x) ** 2 + (pt.y - y) ** 2); + if (d < threshold && d < bestDist) { bestDist = d; bestIdx = si; } + } + } + return bestIdx; + } + + findClosestPoints(points, clickedPoint) { + if (points.length < 2) return null; + let distances = points.map((point, idx) => { + const dx = clickedPoint.x - point.x, dy = clickedPoint.y - point.y; + return { index: idx, distance: Math.sqrt(dx * dx + dy * dy) }; + }); + distances.sort((a, b) => a.distance - b.distance); + let i1 = distances[0].index, i2 = distances[1].index; + if (i1 > i2) [i1, i2] = [i2, i1]; + return { point1Index: i1, point2Index: i2 }; + } + + // ─── Mouse Handlers ─── + + onMouseDown(e) { + const mouse = this.getLocalMouse(e); + const clamped = this.clamp(mouse.x, mouse.y); + const activeSpline = this.splines[this.activeSplineIndex]; + const activePoints = activeSpline?.points; + + if (e.shiftKey && e.button === 0 && activePoints) { + activePoints.push({ x: clamped.x, y: clamped.y }); + this.render(); this.updatePath(); return; + } + if (e.ctrlKey && e.button === 0 && activePoints && activePoints.length >= 2) { + const closest = this.findClosestPoints(activePoints, clamped); + if (closest) { + const p1 = activePoints[closest.point1Index], p2 = activePoints[closest.point2Index]; + activePoints.splice(closest.point2Index, 0, { x: (p1.x + p2.x) / 2, y: (p1.y + p2.y) / 2 }); + this.render(); this.updatePath(); + } + return; + } + + if (e.button === 2) { + const ptIdx = this.findPointAt(mouse.x, mouse.y); + if (activePoints && ptIdx > 0 && ptIdx < activePoints.length - 1) { + activePoints.splice(ptIdx, 1); this.render(); this.updatePath(); return; + } + this.lastContextMenuPos = { x: clamped.x, y: clamped.y }; + this.showContextMenu(e); + return; + } + + if (e.button !== 0) return; + + // Bezier handle drag + const handle = this.findHandleAt(mouse.x, mouse.y); + if (handle && activePoints) { + this.dragIndex = handle.index; this.dragType = handle.type; + const p = activePoints[handle.index]; + const hx = handle.type === 'h1' ? p.h1x : p.h2x, hy = handle.type === 'h1' ? p.h1y : p.h2y; + this.dragOffset = { x: mouse.x - hx, y: mouse.y - hy }; + this.render(); this.startDocumentDrag(e); return; + } + + // Point drag + const ptIdx = this.findPointAt(mouse.x, mouse.y); + if (ptIdx >= 0) { + this.dragIndex = ptIdx; this.dragType = 'point'; this.hoverIndex = ptIdx; + const p = activePoints[ptIdx]; + this.dragOffset = { x: mouse.x - p.x, y: mouse.y - p.y }; + this.render(); this.startDocumentDrag(e); return; + } + + // Spline selection + const splineIdx = this.findSplineAt(mouse.x, mouse.y); + if (splineIdx >= 0 && splineIdx !== this.activeSplineIndex) { + this.activeSplineIndex = splineIdx; this.render(); this.updatePath(); return; + } + } + + onMouseMove(e) { + const mouse = this.getLocalMouse(e); + const clamped = this.clamp(mouse.x, mouse.y); + + // Bezier handle drag + if ((this.dragType === 'h1' || this.dragType === 'h2') && this.dragIndex >= 0) { + const activePoints = this.splines[this.activeSplineIndex]?.points; + if (activePoints && this.dragIndex < activePoints.length) { + const p = activePoints[this.dragIndex]; + const clampH = (hx, hy) => ({ x: Math.max(0, Math.min(this.coordWidth, hx)), y: Math.max(0, Math.min(this.coordHeight, hy)) }); + const ox = this.dragOffset?.x || 0, oy = this.dragOffset?.y || 0; + const prevH1x = p.h1x, prevH1y = p.h1y, prevH2x = p.h2x, prevH2y = p.h2y; + if (this.dragType === 'h1') { + const c = clampH(mouse.x - ox, mouse.y - oy); + p.h1x = c.x; p.h1y = c.y; + if (!e.altKey) { const dx = p.x - p.h1x, dy = p.y - p.h1y; const mc = clampH(p.x + dx, p.y + dy); p.h2x = mc.x; p.h2y = mc.y; } + } else { + const c = clampH(mouse.x - ox, mouse.y - oy); + p.h2x = c.x; p.h2y = c.y; + if (!e.altKey) { const dx = p.x - p.h2x, dy = p.y - p.h2y; const mc = clampH(p.x + dx, p.y + dy); p.h1x = mc.x; p.h1y = mc.y; } + } + if (p.h1x === prevH1x && p.h1y === prevH1y && p.h2x === prevH2x && p.h2y === prevH2y) return; + this.render(); + } + return; + } + + // Point drag + if (this.dragType === 'point' && this.dragIndex >= 0) { + const activePoints = this.splines[this.activeSplineIndex]?.points; + if (activePoints && this.dragIndex < activePoints.length) { + const p = activePoints[this.dragIndex]; + const ox = this.dragOffset?.x || 0, oy = this.dragOffset?.y || 0; + const target = this.clamp(mouse.x - ox, mouse.y - oy); + if (target.x === p.x && target.y === p.y) return; + const dx = target.x - p.x, dy = target.y - p.y; + if (this.interpolation === 'bezier' && p.h1x !== undefined) { + p.h1x += dx; p.h1y += dy; p.h2x += dx; p.h2y += dy; + } + p.x = target.x; p.y = target.y; + this.render(); + } + return; + } + + // Hover detection + if (!this.dragType) { + const ptIdx = this.findPointAt(mouse.x, mouse.y); + const activePoints = this.splines[this.activeSplineIndex]?.points; + + // Ctrl preview + if (e.ctrlKey && activePoints && activePoints.length >= 2) { + const closest = this.findClosestPoints(activePoints, clamped); + if (closest) { + const p1 = activePoints[closest.point1Index], p2 = activePoints[closest.point2Index]; + this.subdividePreview = { x: (p1.x + p2.x) / 2, y: (p1.y + p2.y) / 2 }; + this.canvas.style.cursor = 'copy'; this.render(); return; + } + } else if (this.subdividePreview) { this.subdividePreview = null; this.render(); } + + // Handle hover + const handleHit = this.findHandleAt(mouse.x, mouse.y); + if (handleHit) { this.canvas.style.cursor = 'crosshair'; if (ptIdx !== this.hoverIndex) { this.hoverIndex = ptIdx; this.render(); } return; } + + if (ptIdx !== this.hoverIndex) { + this.hoverIndex = ptIdx; + this.canvas.style.cursor = ptIdx >= 0 ? 'move' : 'default'; + this.render(); + } + + // Spline hover + if (ptIdx < 0 && this.splines.length > 1) { + const splineIdx = this.findSplineAt(mouse.x, mouse.y); + const newSplineHover = splineIdx >= 0 && splineIdx !== this.activeSplineIndex ? splineIdx : -1; + if (newSplineHover !== this.hoverSplineIndex) { + this.hoverSplineIndex = newSplineHover; + this.canvas.style.cursor = newSplineHover >= 0 ? 'pointer' : 'default'; + this.render(); + } + } else if (this.hoverSplineIndex >= 0) { this.hoverSplineIndex = -1; this.render(); } + } + } + + onMouseUp() { + if (this.dragType) { + this.endDrag(); + this.updatePath(); + } + } + + // ─── Path Building & Sampling ─── + + samplePoints(splineIndex, numSamples, samplingMethod) { + const spline = this.splines[splineIndex]; + if (!spline || !spline.points || spline.points.length < 1) return []; + if (spline.isSinglePoint || spline.points.length === 1) { + const point = spline.points[0]; + return Array(numSamples).fill().map(() => ({ x: point.x, y: point.y })); + } + if (numSamples < 2) { + const p = spline.points[0]; + return [{ x: p.x, y: p.y }]; + } + const pathD = Interpolation.buildPathD(spline.points, this.interpolation, this.tension); + if (!pathD) return []; + + this.sampler.setPath(pathD); + const pathLength = this.sampler.getTotalLength(); + const points = []; + + if (samplingMethod === "speed") { + if (pathLength < 0.001) return [{ x: spline.points[0].x, y: spline.points[0].y }]; + const controlPoints = spline.points; + // Pair each control point with its path position, then sort together + const cpWithPos = controlPoints.map(cp => { + let bestDist = Infinity, bestPos = 0; + for (let pos = 0; pos <= pathLength; pos += pathLength / 100) { + const pt = this.sampler.getPointAtLength(pos); + const dist = Math.sqrt((pt.x - cp.x) ** 2 + (pt.y - cp.y) ** 2); + if (dist < bestDist) { bestDist = dist; bestPos = pos; } + } + return { cp, pos: bestPos }; + }); + cpWithPos.sort((a, b) => a.pos - b.pos); + const pathPositions = cpWithPos.map(c => c.pos); + + const densities = []; + let totalWeight = 0; + for (let i = 0; i < cpWithPos.length - 1; i++) { + const segLength = pathPositions[i + 1] - pathPositions[i]; + const d = 1 / Math.max(segLength, 0.0001); + densities.push(d); + totalWeight += d; + } + const cumulativeWeights = []; + let cum = 0; + for (let i = 0; i < densities.length; i++) { + cum += densities[i] / totalWeight; + cumulativeWeights.push(cum); + } + const mapToPath = (t) => { + if (t === 0) return 0; if (t === 1) return pathLength; + let segIdx = cumulativeWeights.length - 1; + for (let i = 0; i < cumulativeWeights.length; i++) { if (t <= cumulativeWeights[i]) { segIdx = i; break; } } + const segStart = segIdx > 0 ? cumulativeWeights[segIdx - 1] : 0; + const segT = (t - segStart) / (cumulativeWeights[segIdx] - segStart); + return pathPositions[segIdx] + segT * (pathPositions[segIdx + 1] - pathPositions[segIdx]); + }; + for (let i = 0; i < numSamples; i++) { + const pt = this.sampler.getPointAtLength(mapToPath(i / (numSamples - 1))); + points.push({ x: pt.x, y: pt.y }); + } + return points; + } + + for (let i = 0; i < numSamples; i++) { + let point; + if (samplingMethod === "time") { + point = this.sampler.findPointAtX((this.coordWidth / (numSamples - 1)) * i, pathLength); + } else { + point = this.sampler.getPointAtLength((pathLength / (numSamples - 1)) * i); + } + points.push({ x: point.x, y: point.y }); + } + if (points.length > 0 && spline.points.length > 1) { + points[points.length - 1].y = spline.points[spline.points.length - 1].y; + } + return points; + } + + updatePath(allDirty = false) { + if (!this.splines || this.splines.length === 0) return; + + // Rebuild cache when spline count changes or full refresh requested + if (!this._sampledCache || this._sampledCache.length !== this.splines.length || allDirty) { + this._sampledCache = new Array(this.splines.length).fill(null); + } + + const method = this.samplingMethod; + const useControlPoints = method === "controlpoints"; + for (let i = 0; i < this.splines.length; i++) { + if (useControlPoints) { + this._sampledCache[i] = this.splines[i].points; + } else if (i === this.activeSplineIndex || !this._sampledCache[i]) { + this._sampledCache[i] = this.samplePoints(i, this.points_to_sample, method); + } + } + this.sampledCoords = this._sampledCache[this.activeSplineIndex] || []; + this.pointsStoreWidget.value = JSON.stringify(this.splines); + if (this.coordWidget) this.coordWidget.value = JSON.stringify(this._sampledCache); + this.render(); + } + + // ─── Rendering ─── + + _render() { + const ctx = this.ctx; + const h = this.coordHeight; + this.beginRender(); + + // Ruler lines + if (this.drawRuler && !this.bgImage) { + ctx.strokeStyle = 'rgba(128,128,128,0.3)'; ctx.lineWidth = 1; + for (let y = 64; y < h; y += 64) { + const cy = y * this.scaleY; + ctx.beginPath(); ctx.moveTo(0, cy); ctx.lineTo(this.width, cy); ctx.stroke(); + } + } + + const tc = (x, y) => this.toCanvas(x, y); + + // Draw all splines — use scale transform for curves, toCanvas for discrete elements + for (let si = 0; si < this.splines.length; si++) { + const spline = this.splines[si]; + const isActive = si === this.activeSplineIndex, isHover = si === this.hoverSplineIndex; + + if (spline.isSinglePoint || (spline.points && spline.points.length === 1)) { + const cp = tc(spline.points[0].x, spline.points[0].y), sz = isActive ? 8 : 6; + ctx.fillStyle = spline.color; ctx.fillRect(cp.x - sz, cp.y - sz, sz * 2, sz * 2); + ctx.strokeStyle = 'black'; ctx.lineWidth = 2; ctx.strokeRect(cp.x - sz, cp.y - sz, sz * 2, sz * 2); + continue; + } + if (spline.points.length < 2) continue; + + // Build path once, stroke twice (outline + color) to avoid recomputing interpolation + const pathD = Interpolation.buildPathD(spline.points, this.interpolation, this.tension); + const curvePath = new Path2D(pathD); + ctx.save(); ctx.scale(this.scaleX, this.scaleY); + ctx.lineWidth = (isActive ? 5 : isHover ? 4 : 3.5) / Math.min(this.scaleX, this.scaleY); ctx.strokeStyle = 'black'; + ctx.stroke(curvePath); + ctx.lineWidth = (isActive ? 3 : isHover ? 2 : 1.5) / Math.min(this.scaleX, this.scaleY); ctx.strokeStyle = spline.color; + ctx.stroke(curvePath); + ctx.restore(); + } + + const activeSpline = this.splines[this.activeSplineIndex]; + + // Handle lines + if (this.drawHandles) { + const activePoints = this.splines[this.activeSplineIndex]?.points; + if (activePoints && activePoints.length >= 2) { + ctx.strokeStyle = '#ff7f0e'; ctx.lineWidth = 1; ctx.beginPath(); + const c0 = tc(activePoints[0].x, activePoints[0].y); + ctx.moveTo(c0.x, c0.y); + for (let i = 1; i < activePoints.length; i++) { const ci = tc(activePoints[i].x, activePoints[i].y); ctx.lineTo(ci.x, ci.y); } + ctx.stroke(); + } + } + + // Control points for active spline + if (activeSpline && activeSpline.points && !activeSpline.isSinglePoint) { + const dotRadius = 12; + for (let i = 0; i < activeSpline.points.length; i++) { + const p = activeSpline.points[i]; + const cp = tc(p.x, p.y); + const isHovered = this.hoverIndex === i; + ctx.fillStyle = 'rgba(100, 100, 100, 0.3)'; + ctx.strokeStyle = isHovered ? '#ff7f0e' : '#1f77b4'; ctx.lineWidth = 2; + ctx.beginPath(); ctx.arc(cp.x, cp.y, dotRadius, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); + + // Chevron + const isEndpoint = i === 0 || i === activeSpline.points.length - 1; + const showChevron = this.showChevrons && ( + (this.samplingMethod === 'path' || this.samplingMethod === 'speed') + || (this.samplingMethod === 'time' && isEndpoint)); + + if (showChevron && activeSpline.points.length > 1) { + let angle = 0; + if (i > 0 && i < activeSpline.points.length - 1) { const prev = activeSpline.points[i-1], next = activeSpline.points[i+1]; angle = Math.atan2((next.y - prev.y) * this.scaleY, (next.x - prev.x) * this.scaleX); } + else if (i === 0) { const next = activeSpline.points[1]; angle = Math.atan2((next.y - p.y) * this.scaleY, (next.x - p.x) * this.scaleX); } + else { const prev = activeSpline.points[i-1]; angle = Math.atan2((p.y - prev.y) * this.scaleY, (p.x - prev.x) * this.scaleX); } + ctx.save(); + ctx.translate(cp.x, cp.y); + ctx.rotate(angle); + ctx.strokeStyle = isHovered ? '#fff' : 'rgba(255,255,255,0.7)'; + ctx.lineWidth = 2.5; ctx.lineCap = 'round'; ctx.lineJoin = 'round'; + ctx.beginPath(); + ctx.moveTo(-dotRadius * 0.2, -dotRadius * 0.35); + ctx.lineTo(dotRadius * 0.3, 0); + ctx.lineTo(-dotRadius * 0.2, dotRadius * 0.35); + ctx.stroke(); + ctx.lineCap = 'butt'; ctx.lineJoin = 'miter'; + ctx.restore(); + } + + if (isHovered) { + ctx.font = '11px monospace'; + const ax = String(Math.round(p.x)), ay = String(Math.round(p.y)); + const nx = (p.x / this.coordWidth).toFixed(3), ny = (p.y / this.coordHeight).toFixed(3); + const ml = Math.max(ax.length, nx.length), mr = Math.max(ay.length, ny.length); + const pre = `${i}: `, L1 = `[${ax.padStart(ml)}, ${ay.padStart(mr)}]`, L2 = `[${nx.padStart(ml)}, ${ny.padStart(mr)}]`; + const preW = ctx.measureText(pre).width, totW = preW + Math.max(ctx.measureText(L1).width, ctx.measureText(L2).width); + const pad = dotRadius + 5, lineH = 13; + let lx = cp.x + pad, ly = cp.y - lineH; + if (lx + totW > this.width) lx = cp.x - pad - totW; + if (ly - lineH < 0) ly = cp.y + pad; + for (const [style, fill] of [['#000', 3], ['#fff', 0]]) { + ctx.strokeStyle = style; ctx.lineWidth = fill; ctx.fillStyle = style; + const draw = fill ? 'strokeText' : 'fillText'; + ctx[draw](pre + L1, lx, ly); + ctx[draw](L2, lx + preW, ly + lineH); + } + } + } + } + + // Bezier handles + if (this.interpolation === 'bezier' && activeSpline && activeSpline.points && !activeSpline.isSinglePoint) { + const pts = activeSpline.points; + Interpolation.ensureBezierHandles(pts); + const hr = 5; + for (let i = 0; i < pts.length; i++) { + const p = pts[i]; + const cp = tc(p.x, p.y); + if (i > 0 && p.h1x !== undefined) { + const h1 = tc(p.h1x, p.h1y); + ctx.strokeStyle = 'rgba(255,255,255,0.4)'; ctx.lineWidth = 1; + ctx.beginPath(); ctx.moveTo(cp.x, cp.y); ctx.lineTo(h1.x, h1.y); ctx.stroke(); + ctx.fillStyle = (this.dragType === 'h1' && this.dragIndex === i) ? '#fff' : 'rgba(255,255,255,0.8)'; + ctx.strokeStyle = '#1f77b4'; ctx.lineWidth = 1.5; + ctx.beginPath(); ctx.arc(h1.x, h1.y, hr, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); + } + if (i < pts.length - 1 && p.h2x !== undefined) { + const h2 = tc(p.h2x, p.h2y); + ctx.strokeStyle = 'rgba(255,255,255,0.4)'; ctx.lineWidth = 1; + ctx.beginPath(); ctx.moveTo(cp.x, cp.y); ctx.lineTo(h2.x, h2.y); ctx.stroke(); + ctx.fillStyle = (this.dragType === 'h2' && this.dragIndex === i) ? '#fff' : 'rgba(255,255,255,0.8)'; + ctx.strokeStyle = '#ff7f0e'; ctx.lineWidth = 1.5; + ctx.beginPath(); ctx.arc(h2.x, h2.y, hr, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); + } + } + } + + // Sample points + if (this.drawSamplePoints && this.sampledCoords) { + ctx.fillStyle = 'red'; ctx.strokeStyle = 'black'; ctx.lineWidth = 1; + for (const pt of this.sampledCoords) { const cp = tc(pt.x, pt.y); ctx.beginPath(); ctx.arc(cp.x, cp.y, 5, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); } + } + + // Subdivide preview + if (this.subdividePreview) { + const sp = tc(this.subdividePreview.x, this.subdividePreview.y); + ctx.beginPath(); ctx.arc(sp.x, sp.y, 10, 0, Math.PI * 2); + ctx.strokeStyle = 'rgba(0,255,0,0.3)'; ctx.lineWidth = 4; ctx.stroke(); + ctx.beginPath(); ctx.arc(sp.x, sp.y, 8, 0, Math.PI * 2); + ctx.strokeStyle = '#0f0'; ctx.lineWidth = 2; ctx.stroke(); + } + + this.endRender(); + } + +} diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/web/js/fast_preview.js b/ComfyUI/custom_nodes/ComfyUI-KJNodes/web/js/fast_preview.js new file mode 100644 index 0000000000000000000000000000000000000000..15c99d6d7dfa7570e365390cfff3cb81dfbc8f2f --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-KJNodes/web/js/fast_preview.js @@ -0,0 +1,40 @@ +import { chainCallback } from './utility.js'; +const { app } = window.comfyAPI.app; +const { api } = window.comfyAPI.api; + +let execId = null; +api.addEventListener("executing", e => { execId = e.detail ?? null; }); + +app.registerExtension({ + name: 'KJNodes.FastPreview', + async beforeRegisterNodeDef(nodeType, nodeData) { + if (nodeData?.name !== 'FastPreview') return; + chainCallback(nodeType.prototype, "onNodeCreated", function () { + this.setSize([550, 550]); + const nodeRef = this; + + const show = blob => { + const img = new Image(); + img.onload = () => { nodeRef.imgs = [img]; nodeRef.setDirtyCanvas(true); }; + img.src = URL.createObjectURL(blob); + }; + + const metaHandler = e => { + const { blob, nodeId, displayNodeId } = e.detail; + if (String(displayNodeId || nodeId) === String(nodeRef.id)) show(blob); + }; + const plainHandler = e => { + if (api.serverSupportsFeature?.("supports_preview_metadata")) return; + if (String(execId) === String(nodeRef.id)) show(e.detail); + }; + + api.addEventListener("b_preview_with_metadata", metaHandler); + api.addEventListener("b_preview", plainHandler); + + chainCallback(nodeRef, "onRemoved", () => { + api.removeEventListener("b_preview_with_metadata", metaHandler); + api.removeEventListener("b_preview", plainHandler); + }); + }); + } +}); diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/web/js/fast_preview_batch.js b/ComfyUI/custom_nodes/ComfyUI-KJNodes/web/js/fast_preview_batch.js new file mode 100644 index 0000000000000000000000000000000000000000..f49e47cb3922b2ab04bd1488a846fea5a87cd229 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-KJNodes/web/js/fast_preview_batch.js @@ -0,0 +1,518 @@ +import { chainCallback, addMiddleClickPan, addWheelPassthrough } from './utility.js'; +const { app } = window.comfyAPI.app; +const { api } = window.comfyAPI.api; + +const STYLE_ID = 'kj-batch-preview-style'; +function ensureStyles() { + if (document.getElementById(STYLE_ID)) return; + const s = document.createElement('style'); + s.id = STYLE_ID; + s.textContent = ` +.kj-bp-root { width:100%; height:100%; position:relative; background:#1a1a1a; overflow:hidden; user-select:none; } +.kj-bp-grid { width:100%; height:100%; display:block; cursor:pointer; image-rendering:auto; } +.kj-bp-detail { position:absolute; inset:0; display:none; background:#0d0d0d; } +.kj-bp-detail.visible { display:flex; flex-direction:column; } +.kj-bp-detail-canvas { flex:1 1 auto; min-height:0; width:100%; display:block; } +.kj-bp-bar { display:flex; align-items:center; justify-content:space-between; padding:4px 8px; background:#222; color:#ddd; font:12px sans-serif; } +.kj-bp-btn { background:#333; color:#ddd; border:1px solid #444; padding:2px 10px; cursor:pointer; font:12px sans-serif; } +.kj-bp-btn:hover { background:#444; } +.kj-bp-btn:disabled { opacity:0.4; cursor:default; } +.kj-bp-scrubber { position:relative; flex:1 1 auto; height:22px; margin:0 8px; background:#2a2a2a; border:1px solid #444; cursor:ew-resize; user-select:none; touch-action:none; } +.kj-bp-scrubber-fill { position:absolute; top:0; bottom:0; left:0; width:0; background:#3a5a7a; pointer-events:none; } +.kj-bp-scrubber-text { position:absolute; inset:0; display:flex; align-items:center; justify-content:center; color:#ddd; font:11px sans-serif; pointer-events:none; text-shadow:0 1px 2px rgba(0,0,0,0.6); } +.kj-bp-status { position:absolute; inset:0; display:flex; align-items:center; justify-content:center; color:#888; font:12px sans-serif; pointer-events:none; } +`; + document.head.appendChild(s); +} + +function pickGridLayout(count, aspect, containerW, containerH) { + let best = { cols: 1, rows: count, cellW: 0, cellH: 0, area: 0 }; + for (let cols = 1; cols <= count; cols++) { + const rows = Math.ceil(count / cols); + const cellW = containerW / cols; + const cellH = containerH / rows; + const fitW = Math.min(cellW, cellH * aspect); + const fitH = fitW / aspect; + const area = fitW * fitH; + if (area > best.area) best = { cols, rows, cellW, cellH, area, fitW, fitH }; + } + return best; +} + +app.registerExtension({ + name: 'KJNodes.FastPreviewBatch', + async beforeRegisterNodeDef(nodeType, nodeData) { + if (nodeData?.name !== 'FastPreviewBatch') return; + + chainCallback(nodeType.prototype, 'onNodeCreated', function () { + ensureStyles(); + this.setSize([520, 520]); + + const root = document.createElement('div'); + root.className = 'kj-bp-root'; + + const status = document.createElement('div'); + status.className = 'kj-bp-status'; + status.textContent = 'No preview yet'; + root.appendChild(status); + + const gridCanvas = document.createElement('canvas'); + gridCanvas.className = 'kj-bp-grid'; + gridCanvas.style.display = 'none'; + root.appendChild(gridCanvas); + + const detail = document.createElement('div'); + detail.className = 'kj-bp-detail'; + const detailCanvas = document.createElement('canvas'); + detailCanvas.className = 'kj-bp-detail-canvas'; + const bar = document.createElement('div'); + bar.className = 'kj-bp-bar'; + const prevBtn = document.createElement('button'); + prevBtn.className = 'kj-bp-btn'; + prevBtn.textContent = '❮'; + const nextBtn = document.createElement('button'); + nextBtn.className = 'kj-bp-btn'; + nextBtn.textContent = '❯'; + const scrubber = document.createElement('div'); + scrubber.className = 'kj-bp-scrubber'; + const scrubberFill = document.createElement('div'); + scrubberFill.className = 'kj-bp-scrubber-fill'; + const scrubberText = document.createElement('span'); + scrubberText.className = 'kj-bp-scrubber-text'; + scrubber.appendChild(scrubberFill); + scrubber.appendChild(scrubberText); + const playBtn = document.createElement('button'); + playBtn.className = 'kj-bp-btn'; + playBtn.textContent = '▶'; + const closeBtn = document.createElement('button'); + closeBtn.className = 'kj-bp-btn'; + closeBtn.textContent = '✕'; + bar.appendChild(playBtn); + bar.appendChild(prevBtn); + bar.appendChild(scrubber); + bar.appendChild(nextBtn); + bar.appendChild(closeBtn); + detail.appendChild(detailCanvas); + detail.appendChild(bar); + root.appendChild(detail); + + this.addDOMWidget('fast_preview_batch', 'div', root, { serialize: false }); + + const removeMiddleClickPan = addMiddleClickPan(root); + addWheelPassthrough(root); + + const state = { + video: null, + strip: null, + stripCols: 1, + stripCellW: 0, + stripCellH: 0, + frameCount: 0, + fps: 30, + thumbW: 0, + thumbH: 0, + layout: null, + gridReady: false, + currentIndex: 0, + hoverIndex: -1, + }; + + const setStatus = (msg) => { + if (msg) { + status.textContent = msg; + status.style.display = 'flex'; + } else { + status.style.display = 'none'; + } + }; + + const cleanupVideo = () => { + if (state.video) { + try { state.video.pause(); } catch (_) { } + state.video.removeAttribute('src'); + try { state.video.load(); } catch (_) { } + if (state.video.parentNode) state.video.parentNode.removeChild(state.video); + state.video = null; + } + }; + + const seekToFrame = (i) => new Promise((resolve) => { + if (!state.video) return resolve(); + const t = (i + 0.5) / state.fps; + const onSeeked = () => { + state.video.removeEventListener('seeked', onSeeked); + // give the decoder a tick to ensure frame is drawable + requestAnimationFrame(() => resolve()); + }; + state.video.addEventListener('seeked', onSeeked); + try { + state.video.currentTime = t; + } catch (_) { + state.video.removeEventListener('seeked', onSeeked); + resolve(); + } + }); + + let drawGeneration = 0; + const updateScrubberUI = () => { + const t = state.frameCount > 1 ? state.currentIndex / (state.frameCount - 1) : 0; + scrubberFill.style.width = (t * 100) + '%'; + scrubberText.textContent = `frame ${state.currentIndex + 1} / ${state.frameCount}`; + prevBtn.disabled = state.currentIndex <= 0; + nextBtn.disabled = state.currentIndex >= state.frameCount - 1; + }; + const paintFrameToDetail = () => { + const v = state.video; + if (!v) return; + const w = detailCanvas.clientWidth; + const h = detailCanvas.clientHeight; + if (w <= 0 || h <= 0) return; + const dpr = window.devicePixelRatio || 1; + const targetW = Math.max(1, Math.floor(w * dpr)); + const targetH = Math.max(1, Math.floor(h * dpr)); + if (detailCanvas.width !== targetW) detailCanvas.width = targetW; + if (detailCanvas.height !== targetH) detailCanvas.height = targetH; + const ctx = detailCanvas.getContext('2d'); + ctx.fillStyle = '#0d0d0d'; + ctx.fillRect(0, 0, detailCanvas.width, detailCanvas.height); + const aspect = state.thumbW / state.thumbH; + let dw = detailCanvas.width; + let dh = dw / aspect; + if (dh > detailCanvas.height) { + dh = detailCanvas.height; + dw = dh * aspect; + } + const dx = (detailCanvas.width - dw) / 2; + const dy = (detailCanvas.height - dh) / 2; + ctx.drawImage(v, dx, dy, dw, dh); + }; + const drawDetail = async () => { + if (!state.video || !state.frameCount) return; + updateScrubberUI(); + const gen = ++drawGeneration; + await seekToFrame(state.currentIndex); + if (gen !== drawGeneration) return; // superseded by a newer scrub + paintFrameToDetail(); + }; + + let playing = false; + let playHandle = 0; + const updatePlayBtn = () => { playBtn.textContent = playing ? '⏸' : '▶'; }; + const stopPlayback = () => { + if (!playing) return; + playing = false; + playHandle++; + if (state.video) { try { state.video.pause(); } catch (_) { } } + updatePlayBtn(); + }; + const startPlayback = async () => { + if (!state.video || !state.frameCount || playing) return; + if (typeof state.video.requestVideoFrameCallback !== 'function') return; + if (state.currentIndex >= state.frameCount - 1) state.currentIndex = 0; + drawGeneration++; // invalidate any pending drawDetail + await seekToFrame(state.currentIndex); + paintFrameToDetail(); + updateScrubberUI(); + playing = true; + updatePlayBtn(); + state.video.playbackRate = 1; + const myHandle = ++playHandle; + try { await state.video.play(); } catch (_) { stopPlayback(); return; } + if (myHandle !== playHandle) return; + const onFrame = (_now, metadata) => { + if (!playing || myHandle !== playHandle) return; + const idx = Math.min(state.frameCount - 1, + Math.floor(metadata.mediaTime * state.fps + 1e-6)); + if (idx !== state.currentIndex) { + state.currentIndex = idx; + updateScrubberUI(); + } + paintFrameToDetail(); + if (idx >= state.frameCount - 1) { stopPlayback(); return; } + state.video.requestVideoFrameCallback(onFrame); + }; + state.video.requestVideoFrameCallback(onFrame); + }; + + const renderGrid = () => { + if (!state.frameCount || !state.strip) return; + const containerW = root.clientWidth; + const containerH = root.clientHeight; + if (containerW <= 0 || containerH <= 0) return; + const dpr = window.devicePixelRatio || 1; + const aspect = state.thumbW / state.thumbH; + const layout = pickGridLayout(state.frameCount, aspect, containerW, containerH); + state.layout = layout; + gridCanvas.width = Math.floor(containerW * dpr); + gridCanvas.height = Math.floor(containerH * dpr); + gridCanvas.style.width = containerW + 'px'; + gridCanvas.style.height = containerH + 'px'; + const ctx = gridCanvas.getContext('2d'); + ctx.fillStyle = '#1a1a1a'; + ctx.fillRect(0, 0, gridCanvas.width, gridCanvas.height); + + const cellW = layout.cellW * dpr; + const cellH = layout.cellH * dpr; + const fitW = layout.fitW * dpr; + const fitH = layout.fitH * dpr; + const total = state.frameCount; + const sw = state.stripCellW; + const sh = state.stripCellH; + const sCols = state.stripCols; + for (let i = 0; i < total; i++) { + const sx = (i % sCols) * sw; + const sy = Math.floor(i / sCols) * sh; + const dCol = i % layout.cols; + const dRow = Math.floor(i / layout.cols); + const dx = dCol * cellW + (cellW - fitW) / 2; + const dy = dRow * cellH + (cellH - fitH) / 2; + ctx.drawImage(state.strip, sx, sy, sw, sh, dx, dy, fitW, fitH); + } + if (state.hoverIndex >= 0 && state.hoverIndex < total) { + const i = state.hoverIndex; + const sx = (i % sCols) * sw; + const sy = Math.floor(i / sCols) * sh; + const dCol = i % layout.cols; + const dRow = Math.floor(i / layout.cols); + const cx = dCol * cellW + cellW / 2; + const cy = dRow * cellH + cellH / 2; + const scale = 1.15; + const zw = fitW * scale; + const zh = fitH * scale; + let dx = cx - zw / 2; + let dy = cy - zh / 2; + dx = Math.max(0, Math.min(gridCanvas.width - zw, dx)); + dy = Math.max(0, Math.min(gridCanvas.height - zh, dy)); + ctx.save(); + ctx.shadowColor = 'rgba(0,0,0,0.65)'; + ctx.shadowBlur = 14 * dpr; + ctx.drawImage(state.strip, sx, sy, sw, sh, dx, dy, zw, zh); + ctx.restore(); + ctx.strokeStyle = '#5a8ec4'; + ctx.lineWidth = Math.max(2, 2 * dpr); + ctx.strokeRect(dx + ctx.lineWidth / 2, dy + ctx.lineWidth / 2, zw - ctx.lineWidth, zh - ctx.lineWidth); + } + state.gridReady = true; + setStatus(''); + gridCanvas.style.display = 'block'; + }; + + const enterDetail = (idx) => { + state.currentIndex = idx; + detail.classList.add('visible'); + requestAnimationFrame(drawDetail); + }; + const exitDetail = () => { + stopPlayback(); + detail.classList.remove('visible'); + }; + + playBtn.addEventListener('click', () => { + if (playing) stopPlayback(); + else startPlayback(); + }); + + const cellIndexFromEvent = (e) => { + if (!state.gridReady || !state.layout) return -1; + const rect = gridCanvas.getBoundingClientRect(); + if (rect.width <= 0 || rect.height <= 0) return -1; + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + const cellW = rect.width / state.layout.cols; + const cellH = rect.height / state.layout.rows; + const col = Math.floor(x / cellW); + const row = Math.floor(y / cellH); + if (col < 0 || col >= state.layout.cols || row < 0 || row >= state.layout.rows) return -1; + const idx = row * state.layout.cols + col; + return (idx >= 0 && idx < state.frameCount) ? idx : -1; + }; + + gridCanvas.addEventListener('click', (e) => { + const idx = cellIndexFromEvent(e); + if (idx >= 0) enterDetail(idx); + }); + + gridCanvas.addEventListener('pointermove', (e) => { + const idx = cellIndexFromEvent(e); + if (idx !== state.hoverIndex) { + state.hoverIndex = idx; + renderGrid(); + } + }); + gridCanvas.addEventListener('pointerleave', () => { + if (state.hoverIndex !== -1) { + state.hoverIndex = -1; + renderGrid(); + } + }); + + prevBtn.addEventListener('click', () => { + if (state.currentIndex > 0) { + stopPlayback(); + state.currentIndex--; + drawDetail(); + } + }); + nextBtn.addEventListener('click', () => { + if (state.currentIndex < state.frameCount - 1) { + stopPlayback(); + state.currentIndex++; + drawDetail(); + } + }); + closeBtn.addEventListener('click', exitDetail); + + let scrubbing = false; + const scrubFromEvent = (e) => { + if (!state.frameCount) return; + const rect = scrubber.getBoundingClientRect(); + const t = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width)); + const idx = Math.round(t * (state.frameCount - 1)); + if (idx !== state.currentIndex) { + state.currentIndex = idx; + drawDetail(); + } + }; + scrubber.addEventListener('pointerdown', (e) => { + if (e.button !== 0) return; // let middle/right click bubble (canvas pan, etc.) + stopPlayback(); + scrubbing = true; + scrubber.setPointerCapture(e.pointerId); + scrubFromEvent(e); + e.preventDefault(); + }); + scrubber.addEventListener('pointermove', (e) => { + if (scrubbing) scrubFromEvent(e); + }); + const endScrub = (e) => { + if (!scrubbing) return; + scrubbing = false; + try { scrubber.releasePointerCapture(e.pointerId); } catch (_) { } + }; + scrubber.addEventListener('pointerup', endScrub); + scrubber.addEventListener('pointercancel', endScrub); + + const keyHandler = (e) => { + if (!detail.classList.contains('visible')) return; + if (e.key === 'Escape') { exitDetail(); e.stopPropagation(); } + else if (e.key === ' ') { + if (playing) stopPlayback(); else startPlayback(); + e.preventDefault(); e.stopPropagation(); + } + else if (e.key === 'ArrowLeft' && state.currentIndex > 0) { + stopPlayback(); state.currentIndex--; drawDetail(); e.stopPropagation(); + } else if (e.key === 'ArrowRight' && state.currentIndex < state.frameCount - 1) { + stopPlayback(); state.currentIndex++; drawDetail(); e.stopPropagation(); + } + }; + window.addEventListener('keydown', keyHandler); + + const ro = new ResizeObserver(() => { + if (detail.classList.contains('visible')) { + drawDetail(); + } else if (state.strip) { + renderGrid(); + } + }); + ro.observe(root); + + const buildViewUrl = (filename, subfolder, type) => { + const params = new URLSearchParams({ + filename: filename, + subfolder: subfolder || '', + type: type || 'temp', + t: String(Date.now()), + }); + return api.apiURL(`/view?${params.toString()}`); + }; + + const loadFromOutput = async (info) => { + stopPlayback(); + cleanupVideo(); + state.strip = null; + state.gridReady = false; + gridCanvas.style.display = 'none'; + setStatus('Loading preview…'); + + state.frameCount = info.frame_count; + state.fps = info.fps || 30; + state.thumbW = info.thumb_w; + state.thumbH = info.thumb_h; + state.stripCols = info.strip_cols || 1; + state.stripCellW = info.strip_cell_w || info.thumb_w; + state.stripCellH = info.strip_cell_h || info.thumb_h; + if (state.currentIndex >= state.frameCount || state.currentIndex < 0) { + state.currentIndex = 0; + } + + const v = document.createElement('video'); + v.muted = true; + v.playsInline = true; + v.preload = 'auto'; + v.style.position = 'absolute'; + v.style.left = '-99999px'; + v.style.width = '1px'; + v.style.height = '1px'; + document.body.appendChild(v); + state.video = v; + + // Video loads in parallel for the detail view; failure is non-fatal + // for the grid (strip handles that). Caught to avoid unhandled-rejection. + new Promise((resolve, reject) => { + v.addEventListener('loadeddata', resolve, { once: true }); + v.addEventListener('error', () => { + cleanupVideo(); + reject(new Error('video load failed')); + }, { once: true }); + v.src = buildViewUrl(info.filename, info.subfolder, info.type); + }).catch(() => { }); + + await new Promise((resolve, reject) => { + const img = new Image(); + img.onload = () => { state.strip = img; resolve(); }; + img.onerror = () => reject(new Error('strip load failed')); + img.src = buildViewUrl(info.strip_filename, info.subfolder, info.type); + }); + renderGrid(); + if (detail.classList.contains('visible')) { + // Refresh detail canvas with the new video; drawDetail awaits seek, + // which itself queues until the new video element finishes loading. + drawDetail(); + } + }; + + const tryLoad = (info, onMissing) => { + loadFromOutput(info).catch((err) => { + console.warn('FastPreviewBatch load failed', err); + if (onMissing) onMissing(); + else setStatus('Preview failed: ' + (err && err.message || err)); + }); + }; + + chainCallback(this, 'onExecuted', function (output) { + if (!output || !output.kj_batch_preview) return; + const info = output.kj_batch_preview[0]; + if (!info) return; + this.properties = this.properties || {}; + this.properties.kj_batch_preview = info; + tryLoad(info); + }); + + chainCallback(this, 'onConfigure', function () { + const info = this.properties && this.properties.kj_batch_preview; + if (!info) return; + tryLoad(info, () => { + setStatus('Preview unavailable (re-run to regenerate)'); + delete this.properties.kj_batch_preview; + }); + }); + + chainCallback(this, 'onRemoved', function () { + window.removeEventListener('keydown', keyHandler); + ro.disconnect(); + removeMiddleClickPan(); + cleanupVideo(); + }); + }); + }, +}); diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/web/js/fillconnect.js b/ComfyUI/custom_nodes/ComfyUI-KJNodes/web/js/fillconnect.js new file mode 100644 index 0000000000000000000000000000000000000000..1903e415eb10237b0dbae50440c9787fbf8d3f5a --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-KJNodes/web/js/fillconnect.js @@ -0,0 +1,225 @@ +const { app } = window.comfyAPI.app; +import { getSlotPos } from "./utility.js"; + +/** + * Check type compatibility. Returns a match tier: + * 2 = exact single-type match, 1 = multi-type or wildcard match, -1 = incompatible + */ +function typeMatchTier(outType, inType) { + if (!LiteGraph.isValidConnection(outType, inType)) return -1; + if (typeof outType === "string" && typeof inType === "string" + && !outType.includes(",") && !inType.includes(",") + && outType.toUpperCase() === inType.toUpperCase()) return 2; + return 1; +} + + +/** + * Order selected nodes for connection. + * Respects existing links between them (topological sort), + * then inserts unconnected nodes by spatial position (left-to-right, top-to-bottom). + */ +function orderNodes(nodes, graph) { + const selectedIds = new Set(nodes.map(n => n.id)); + const nodeById = new Map(nodes.map(n => [n.id, n])); + + const outEdges = new Map(); + const inDegree = new Map(); + for (const n of nodes) { + outEdges.set(n.id, new Set()); + inDegree.set(n.id, 0); + } + + for (const node of nodes) { + for (const out of (node.outputs || [])) { + for (const linkId of (out.links || [])) { + const link = graph.getLink(linkId); + if (link && selectedIds.has(link.target_id) && link.target_id !== node.id) { + if (!outEdges.get(node.id).has(link.target_id)) { + outEdges.get(node.id).add(link.target_id); + inDegree.set(link.target_id, inDegree.get(link.target_id) + 1); + } + } + } + } + } + + const bySpatial = (a, b) => a.pos[0] - b.pos[0] || a.pos[1] - b.pos[1]; + const queue = nodes.filter(n => inDegree.get(n.id) === 0); + queue.sort(bySpatial); + + const ordered = []; + while (queue.length > 0) { + const node = queue.shift(); + ordered.push(node); + for (const targetId of outEdges.get(node.id)) { + const deg = inDegree.get(targetId) - 1; + inDegree.set(targetId, deg); + if (deg === 0 && nodeById.has(targetId)) { + queue.push(nodeById.get(targetId)); + queue.sort(bySpatial); + } + } + } + + // Cycle fallback: append remaining by position + if (ordered.length < nodes.length) { + const inOrdered = new Set(ordered.map(n => n.id)); + const remaining = nodes.filter(n => !inOrdered.has(n.id)); + remaining.sort(bySpatial); + ordered.push(...remaining); + } + + return ordered; +} + +/** + * Collect all candidate connections and assign globally by closest 2D distance. + * Each input and output is used at most once per invocation. + */ +function planConnections(ordered) { + // Collect all candidates globally across all target nodes + const candidates = []; + + for (let b = 1; b < ordered.length; b++) { + const nodeB = ordered[b]; + if (!nodeB.inputs) continue; + + for (let inIdx = 0; inIdx < nodeB.inputs.length; inIdx++) { + const inp = nodeB.inputs[inIdx]; + if (inp.link != null) continue; + + const inPos = getSlotPos(nodeB, true, inIdx); + const inputCandidates = []; + let hasExact = false; + + const inName = (inp.name || inp.label || "").toLowerCase(); + + for (let a = b - 1; a >= 0; a--) { + const nodeA = ordered[a]; + if (!nodeA.outputs) continue; + + for (let outIdx = 0; outIdx < nodeA.outputs.length; outIdx++) { + const out = nodeA.outputs[outIdx]; + const tier = typeMatchTier(out.type, inp.type); + if (tier < 0) continue; + if (tier === 2) hasExact = true; + + const outName = (out.name || out.label || "").toLowerCase(); + const nameMatch = inName !== "" && inName === outName ? 1 : 0; + + const outPos = getSlotPos(nodeA, false, outIdx); + const dx = outPos[0] - inPos[0]; + const dy = outPos[1] - inPos[1]; + inputCandidates.push({ + targetNode: nodeB, + inIdx, + sourceNode: nodeA, + outIdx, + tier, + nameMatch, + dist: dx * dx + dy * dy, + }); + } + } + + for (const c of inputCandidates) { + c.hasExact = hasExact; + } + candidates.push(...inputCandidates); + } + } + + // Filter out non-exact matches for inputs that have an exact candidate (keep name matches) + const filtered = candidates.filter(c => c.tier === 2 || c.nameMatch || !c.hasExact); + + // Sort globally: name match first, then higher type tier, then closest pair + filtered.sort((x, y) => (y.nameMatch - x.nameMatch) || (y.tier - x.tier) || (x.dist - y.dist)); + + // Greedy global assignment — closest pairs first + const planned = []; + const usedInputs = new Set(); + const usedOutputs = new Set(); + + for (const c of filtered) { + const inKey = `${c.targetNode.id}:${c.inIdx}`; + if (usedInputs.has(inKey)) continue; + const outKey = `${c.sourceNode.id}:${c.outIdx}`; + if (usedOutputs.has(outKey)) continue; + + planned.push({ + sourceNode: c.sourceNode, + outIdx: c.outIdx, + targetNode: c.targetNode, + inIdx: c.inIdx, + }); + usedInputs.add(inKey); + usedOutputs.add(outKey); + } + + return planned; +} + +function connectPlanned(planned, graph) { + for (const p of planned) { + p.sourceNode.connect(p.outIdx, p.targetNode, p.inIdx); + } + graph.change(); +} + +function fillConnectSelected() { + if (!app.ui.settings.getSettingValue("KJNodes.fillConnectEnabled")) return; + + const canvas = app.canvas; + const graph = canvas.graph; + const nodes = Object.values(canvas.selected_nodes || {}); + if (nodes.length < 2) return; + + const ordered = orderNodes(nodes, graph); + const planned = planConnections(ordered); + + if (planned.length > 0) { + connectPlanned(planned, graph); + } + + if (app.extensionManager?.toast) { + app.extensionManager.toast.add({ + severity: planned.length > 0 ? "info" : "warn", + summary: planned.length > 0 + ? `Connected ${planned.length} link${planned.length > 1 ? "s" : ""}` + : "No compatible connections found", + life: 2000, + }); + } +} + +app.registerExtension({ + name: "KJNodes.FillConnect", + + settings: [ + { + id: "KJNodes.fillConnectEnabled", + name: "Enable Fill-Connect hotkey", + category: ["KJNodes", "Fill-Connect"], + tooltip: "Fill-connect selected nodes by compatible types", + type: "boolean", + defaultValue: true, + }, + ], + + commands: [ + { + id: "KJNodes.fillConnectSelected", + label: "Fill-Connect Selected Nodes", + function: fillConnectSelected, + }, + ], + + keybindings: [ + { + commandId: "KJNodes.fillConnectSelected", + combo: { key: "f" }, + targetElementId: "graph-canvas", + }, + ], +}); diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/web/js/fix_node.js b/ComfyUI/custom_nodes/ComfyUI-KJNodes/web/js/fix_node.js new file mode 100644 index 0000000000000000000000000000000000000000..aec5cbd9b621a9a2030a1d040e826b1cd0f9a071 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-KJNodes/web/js/fix_node.js @@ -0,0 +1,93 @@ +const { app } = window.comfyAPI.app; + +function snapshotOld(oldNode) { + const data = oldNode.serialize(); + const widgetValuesByName = {}; + for (const w of oldNode.widgets || []) widgetValuesByName[w.name] = w.value; + + const inputSnapshot = (oldNode.inputs || []).map(i => ({ + name: i.name, + widgetName: i.widget?.name ?? null, + linkId: i.link ?? null, + })); + const outputLinksByName = {}; + for (const o of oldNode.outputs || []) { + if (o.links?.length) outputLinksByName[o.name] = [...o.links]; + } + return { data, widgetValuesByName, inputSnapshot, outputLinksByName }; +} + +function applyCosmetics(newNode, old) { + if (old.pos) newNode.pos = [...old.pos]; + if (old.size) { + const min = newNode.computeSize?.() || [0, 0]; + newNode.size = [Math.max(old.size[0], min[0]), Math.max(old.size[1], min[1])]; + } + if (old.title) newNode.title = old.title; + if (old.color) newNode.color = old.color; + if (old.bgcolor) newNode.bgcolor = old.bgcolor; + if (old.flags) newNode.flags = { ...(newNode.flags || {}), ...old.flags }; + if (typeof old.mode === "number") newNode.mode = old.mode; + if (old.properties) newNode.properties = { ...(newNode.properties || {}), ...old.properties }; +} + +function fixNode(oldNode, comfyClass, { resetValues = false } = {}) { + const graph = app.canvas.graph; + const snap = snapshotOld(oldNode); + + const newNode = LiteGraph.createNode(comfyClass); + if (!newNode) { + console.error(`[KJNodes.FixNode] Unknown node type: ${comfyClass}`); + return null; + } + + try { + graph.add(newNode, false); + applyCosmetics(newNode, snap.data); + + // Restore widget values by name (live widgets, not widgets_values array). + if (!resetValues) { + for (const w of newNode.widgets || []) { + if (Object.prototype.hasOwnProperty.call(snap.widgetValuesByName, w.name)) { + try { w.value = snap.widgetValuesByName[w.name]; } catch {} + } + } + } + + // Reconnect inputs by name. + for (const inp of snap.inputSnapshot) { + if (inp.linkId == null) continue; + const link = app.graph.links[inp.linkId]; + if (!link) continue; + const src = app.graph.getNodeById(link.origin_id); + const newSlot = newNode.findInputSlot?.(inp.name); + if (!src || newSlot == null || newSlot < 0) continue; + try { src.connect(link.origin_slot, newNode, newSlot); } catch {} + } + + // Reconnect outputs by name. + (newNode.outputs || []).forEach((out, i) => { + const linkIds = snap.outputLinksByName[out.name]; + if (!linkIds) return; + for (const id of linkIds) { + const link = app.graph.links[id]; + if (!link) continue; + const tgt = app.graph.getNodeById(link.target_id); + if (!tgt) continue; + try { newNode.connect(i, tgt, link.target_slot); } catch {} + } + }); + + graph.remove(oldNode); + app.graph.afterChange(); + requestAnimationFrame(() => app.canvas.setDirty(true, true)); + return newNode; + } catch (err) { + console.error("[KJNodes.FixNode] Aborting, rolling back:", err); + try { graph.remove(newNode); } catch {} + return null; + } +} + +window.kjNodes = window.kjNodes || {}; +window.kjNodes.recreateNode = fixNode; diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/web/js/hdr_preview.js b/ComfyUI/custom_nodes/ComfyUI-KJNodes/web/js/hdr_preview.js new file mode 100644 index 0000000000000000000000000000000000000000..c7c454f58e4b2f2d82f6114a8cb7c04ff4b7a8b6 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-KJNodes/web/js/hdr_preview.js @@ -0,0 +1,574 @@ +import { chainCallback, addMiddleClickPan, addWheelPassthrough } from './utility.js'; +const { app } = window.comfyAPI.app; + +// Shared across all HDR Preview nodes so synced nodes can drive each other. +const hdrSyncGroup = new Set(); + +const VERTEX_SHADER = `#version 300 es +out vec2 v_texCoord; +void main() { + vec2 verts[3] = vec2[](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0)); + v_texCoord = verts[gl_VertexID] * 0.5 + 0.5; + v_texCoord.y = 1.0 - v_texCoord.y; + gl_Position = vec4(verts[gl_VertexID], 0.0, 1.0); +}`; + +// LogC3 constants ported from ComfyUI-LTXVideo/hdr.py +const FRAGMENT_SHADER = `#version 300 es +precision highp float; +in vec2 v_texCoord; +out vec4 fragColor; +uniform sampler2D u_image; +uniform float u_exposure; +uniform float u_saturation; +uniform float u_linearScale; +uniform int u_space; // 0 = logc3, 1 = linear + +const float LC_A = 5.555556; +const float LC_B = 0.052272; +const float LC_C = 0.247190; +const float LC_D = 0.385537; +const float LC_E = 5.367655; +const float LC_F = 0.092809; +const float LC_CUT = 0.010591; +const float LC_CUT_LOG = LC_E * LC_CUT + LC_F; // ~0.14966 + +vec3 logc3_decompress(vec3 logc) { + logc = clamp(logc, 0.0, 1.0); + vec3 lin_from_log = (pow(vec3(10.0), (logc - LC_D) / LC_C) - LC_B) / LC_A; + vec3 lin_from_lin = (logc - LC_F) / LC_E; + vec3 is_log = step(vec3(LC_CUT_LOG), logc); + return mix(lin_from_lin, lin_from_log, is_log); +} + +vec3 linear_to_srgb(vec3 x) { + vec3 cutoff = vec3(0.0031308); + vec3 low = 12.92 * x; + vec3 high = 1.055 * pow(max(x, cutoff), vec3(1.0 / 2.4)) - 0.055; + return clamp(mix(low, high, step(cutoff, x)), 0.0, 1.0); +} + +vec3 srgb_to_linear(vec3 x) { + vec3 cutoff = vec3(0.04045); + vec3 low = x / 12.92; + vec3 high = pow(max(x, vec3(0.0)) + 0.055, vec3(2.4)) / pow(vec3(1.055), vec3(2.4)); + return mix(low, high, step(cutoff, x)); +} + +void main() { + vec3 col = texture(u_image, v_texCoord).rgb; + vec3 hdr; + if (u_space == 0) { + hdr = logc3_decompress(col); + } else if (u_space == 1) { + hdr = col * u_linearScale; + } else { + hdr = srgb_to_linear(col); + } + hdr = max(hdr, vec3(0.0)); + vec3 exposed = hdr * exp2(u_exposure); + float luma = dot(exposed, vec3(0.2126, 0.7152, 0.0722)); + vec3 saturated = max(mix(vec3(luma), exposed, u_saturation), vec3(0.0)); + vec3 tm = (u_space == 2) ? clamp(saturated, 0.0, 1.0) : saturated / (1.0 + saturated); + fragColor = vec4(linear_to_srgb(tm), 1.0); +}`; + +function compileShader(gl, type, src) { + const sh = gl.createShader(type); + gl.shaderSource(sh, src); + gl.compileShader(sh); + if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) { + const log = gl.getShaderInfoLog(sh) || "shader compile failed"; + gl.deleteShader(sh); + throw new Error(log); + } + return sh; +} + +function createProgram(gl, vsSrc, fsSrc) { + const vs = compileShader(gl, gl.VERTEX_SHADER, vsSrc); + const fs = compileShader(gl, gl.FRAGMENT_SHADER, fsSrc); + const prog = gl.createProgram(); + gl.attachShader(prog, vs); + gl.attachShader(prog, fs); + gl.linkProgram(prog); + if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) { + const log = gl.getProgramInfoLog(prog) || "program link failed"; + gl.deleteProgram(prog); + throw new Error(log); + } + gl.deleteShader(vs); + gl.deleteShader(fs); + return prog; +} + +function loadImageBitmapFromView(filename, type) { + return fetch(`/view?filename=${encodeURIComponent(filename)}&type=${type}`) + .then(r => r.ok ? r.blob() : null) + .then(blob => blob ? createImageBitmap(blob, { colorSpaceConversion: "none", premultiplyAlpha: "none" }) : null) + .catch(() => null); +} + +app.registerExtension({ + name: "KJNodes.HDRPreview", + + async beforeRegisterNodeDef(nodeType, nodeData) { + if (nodeData?.name !== "HDRPreviewKJ") return; + + chainCallback(nodeType.prototype, "onNodeCreated", function () { + const node = this; + + const state = { + frames: [], + frameCount: 0, + width: 512, height: 512, + fps: 24, + inputSpace: "logc3", + linearScale: 1.0, + exposure: 0.0, + saturation: 1.0, + currentFrame: 0, + playing: false, + playTimer: null, + gl: null, program: null, texture: null, vao: null, + uniforms: {}, + uploadedFrame: -1, + needsRender: false, + execGen: 0, + }; + node._hdrState = state; + + let controlsHeight = 28; // measured from DOM after mount; this is the fallback + node._widgetHeight = controlsHeight; + + const container = document.createElement("div"); + container.style.cssText = + "position:relative;width:100%;background:#111;display:flex;flex-direction:column;align-items:center;overflow:hidden;"; + + const viewport = document.createElement("div"); + // flex-shrink:0 — legacy container can be shorter than this height; without it the viewport collapses to 0. + viewport.style.cssText = + "position:relative;width:100%;height:0;background:#000;overflow:hidden;flex-shrink:0;"; + + const canvas = document.createElement("canvas"); + canvas.style.cssText = + "display:block;width:100%;height:100%;"; + viewport.appendChild(canvas); + + const frameRow = document.createElement("div"); + frameRow.style.cssText = + "display:flex;align-items:center;gap:6px;padding:3px 6px;color:#ccc;font-size:11px;background:#1a1a1a;width:100%;box-sizing:border-box;flex:0 0 auto;"; + + const playBtn = document.createElement("button"); + playBtn.type = "button"; + playBtn.textContent = "▶"; + playBtn.style.cssText = + "background:#333;color:#ccc;border:1px solid #555;cursor:pointer;padding:1px 8px;font-size:11px;min-width:24px;"; + + const syncBtn = document.createElement("button"); + syncBtn.type = "button"; + syncBtn.textContent = "⛓"; + syncBtn.title = "Sync playback with other HDR Preview nodes that have sync enabled."; + syncBtn.style.cssText = + "background:#333;color:#888;border:1px solid #555;cursor:pointer;padding:1px 6px;font-size:11px;min-width:22px;"; + + const frameSlider = document.createElement("input"); + frameSlider.type = "range"; + frameSlider.min = "0"; + frameSlider.max = "0"; + frameSlider.value = "0"; + frameSlider.step = "1"; + frameSlider.style.cssText = "flex:1;min-width:40px;accent-color:#5af;"; + + const frameLabel = document.createElement("span"); + frameLabel.textContent = "0/0"; + frameLabel.style.cssText = "min-width:50px;text-align:right;font-variant-numeric:tabular-nums;"; + + frameRow.appendChild(playBtn); + frameRow.appendChild(syncBtn); + frameRow.appendChild(frameSlider); + frameRow.appendChild(frameLabel); + + container.appendChild(viewport); + container.appendChild(frameRow); + + const stopProp = (e) => e.stopPropagation(); + for (const el of [frameSlider, playBtn, syncBtn]) { + el.addEventListener("pointerdown", stopProp); + el.addEventListener("mousedown", stopProp); + } + + addMiddleClickPan(container); + addWheelPassthrough(canvas); + + function initGL() { + try { + const gl = canvas.getContext("webgl2", { antialias: false, premultipliedAlpha: false, alpha: false }); + if (!gl) { + console.error("[HDRPreviewKJ] WebGL2 not available"); + return false; + } + const program = createProgram(gl, VERTEX_SHADER, FRAGMENT_SHADER); + gl.useProgram(program); + + const uniforms = { + u_image: gl.getUniformLocation(program, "u_image"), + u_exposure: gl.getUniformLocation(program, "u_exposure"), + u_saturation: gl.getUniformLocation(program, "u_saturation"), + u_space: gl.getUniformLocation(program, "u_space"), + u_linearScale: gl.getUniformLocation(program, "u_linearScale"), + }; + gl.uniform1i(uniforms.u_image, 0); + + const texture = gl.createTexture(); + gl.activeTexture(gl.TEXTURE0); + gl.bindTexture(gl.TEXTURE_2D, texture); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + + const vao = gl.createVertexArray(); + gl.bindVertexArray(vao); + + state.gl = gl; + state.program = program; + state.texture = texture; + state.vao = vao; + state.uniforms = uniforms; + return true; + } catch (err) { + console.error("[HDRPreviewKJ] WebGL init failed:", err); + return false; + } + } + + function requestRender() { + if (state.needsRender) return; + state.needsRender = true; + requestAnimationFrame(() => { + state.needsRender = false; + render(); + }); + } + + function render() { + if (!state.gl || !state.frames.length) return; + const frame = state.frames[state.currentFrame]; + if (!frame) return; + + const gl = state.gl; + // Backing buffer sized to source resolution for quality; CSS handles display scaling + if (canvas.width !== state.width || canvas.height !== state.height) { + canvas.width = state.width; + canvas.height = state.height; + } + gl.viewport(0, 0, canvas.width, canvas.height); + gl.useProgram(state.program); + gl.bindVertexArray(state.vao); + gl.activeTexture(gl.TEXTURE0); + gl.bindTexture(gl.TEXTURE_2D, state.texture); + + if (state.uploadedFrame !== state.currentFrame) { + gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false); + gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB8, gl.RGB, gl.UNSIGNED_BYTE, frame); + state.uploadedFrame = state.currentFrame; + } + + gl.uniform1f(state.uniforms.u_exposure, state.exposure); + gl.uniform1f(state.uniforms.u_saturation, state.saturation); + const spaceIdx = state.inputSpace === "logc3" ? 0 : state.inputSpace === "srgb" ? 2 : 1; + gl.uniform1i(state.uniforms.u_space, spaceIdx); + gl.uniform1f(state.uniforms.u_linearScale, state.linearScale); + + gl.clearColor(0, 0, 0, 1); + gl.clear(gl.COLOR_BUFFER_BIT); + gl.drawArrays(gl.TRIANGLES, 0, 3); + } + + function stopPlayback() { + if (state.playTimer !== null) { + clearInterval(state.playTimer); + state.playTimer = null; + } + state.playing = false; + playBtn.textContent = "▶"; + } + + function updateFrameLabel() { + const total = Math.max(state.frameCount, 1); + frameLabel.textContent = `${state.currentFrame + 1}/${total}`; + } + + const exposureWidget = node.widgets?.find(w => w.name === "exposure"); + const saturationWidget = node.widgets?.find(w => w.name === "saturation"); + + function hookLiveWidget(widget, stateKey) { + if (!widget) return; + state[stateKey] = isFinite(widget.value) ? widget.value : state[stateKey]; + const original = widget.callback; + widget.callback = function (value) { + const num = isFinite(value) ? value : state[stateKey]; + state[stateKey] = num; + requestRender(); + return original?.apply(this, arguments); + }; + } + hookLiveWidget(exposureWidget, "exposure"); + hookLiveWidget(saturationWidget, "saturation"); + + // Sync handle: other nodes in the group call these to follow along. + let syncEnabled = false; + const syncHandle = { + setFrameFraction(fraction) { + if (state.frameCount <= 0) return; + const maxIdx = Math.max(state.frameCount - 1, 0); + const frame = Math.min(maxIdx, Math.max(0, Math.round(fraction * maxIdx))); + if (frame === state.currentFrame) return; + state.currentFrame = frame; + frameSlider.value = String(frame); + updateFrameLabel(); + requestRender(); + }, + showPlaying(play) { + state.playing = play; + playBtn.textContent = play ? "■" : "▶"; + if (!play && state.playTimer !== null) { + clearInterval(state.playTimer); + state.playTimer = null; + } + }, + }; + + function broadcastFraction() { + if (!syncEnabled || state.frameCount <= 1) return; + const fraction = state.currentFrame / (state.frameCount - 1); + for (const other of hdrSyncGroup) { + if (other !== syncHandle) other.setFrameFraction(fraction); + } + } + + function broadcastPlaying(play) { + if (!syncEnabled) return; + for (const other of hdrSyncGroup) { + if (other !== syncHandle) other.showPlaying(play); + } + } + + syncBtn.addEventListener("click", () => { + syncEnabled = !syncEnabled; + syncBtn.style.color = syncEnabled ? "#5fa" : "#888"; + syncBtn.style.borderColor = syncEnabled ? "#5fa" : "#555"; + if (syncEnabled) { + hdrSyncGroup.add(syncHandle); + } else { + hdrSyncGroup.delete(syncHandle); + } + }); + + frameSlider.addEventListener("input", () => { + state.currentFrame = parseInt(frameSlider.value, 10) || 0; + updateFrameLabel(); + requestRender(); + broadcastFraction(); + }); + frameSlider.addEventListener("pointerdown", () => { + stopPlayback(); + broadcastPlaying(false); + }); + function persistCurrentFrame() { + if (!node.properties?.hdrLastPreview) return; + node.properties.hdrLastPreview.current_frame = state.currentFrame; + node.graph?.change?.(); + try { app.extensionManager?.workflow?.activeWorkflow?.changeTracker?.checkState?.(); } catch {} + } + frameSlider.addEventListener("change", persistCurrentFrame); + + playBtn.addEventListener("click", () => { + if (state.playing) { + stopPlayback(); + broadcastPlaying(false); + persistCurrentFrame(); + return; + } + if (state.frameCount <= 1) return; + // Ensure no other synced node has an active timer — we become the sole driver. + if (syncEnabled) { + for (const other of hdrSyncGroup) { + if (other !== syncHandle) other.showPlaying(false); + } + } + const intervalMs = Math.max(16, 1000 / Math.max(state.fps, 1)); + state.playing = true; + playBtn.textContent = "■"; + state.playTimer = setInterval(() => { + state.currentFrame = (state.currentFrame + 1) % state.frameCount; + frameSlider.value = String(state.currentFrame); + updateFrameLabel(); + requestRender(); + broadcastFraction(); + }, intervalMs); + broadcastPlaying(true); + }); + + const domWidget = node.addDOMWidget("hdr_preview", "hdr_preview", container, { + serialize: false, + hideOnZoom: false, + margin: 0, + getMinHeight: () => node._widgetHeight, + getMaxHeight: () => node._widgetHeight, + getHeight: () => node._widgetHeight, + }); + node.resizable = true; + + // LiteGraph's computeSize uses a fixed per-widget height that ignores DOM widgets — sum manually. + const NATIVE_ROW_H = (LiteGraph?.NODE_WIDGET_HEIGHT ?? 20) + 4; + const TITLE_H = LiteGraph?.NODE_TITLE_HEIGHT ?? 30; + function computeNodeHeight() { + let nativeCount = 0; + for (const w of node.widgets || []) { + if (w === domWidget) continue; + if (w.hidden || w.type === "converted-widget") continue; + nativeCount++; + } + return TITLE_H + nativeCount * NATIVE_ROW_H + node._widgetHeight + 8; + } + + // Don't gate on state.frames — a fire during async bitmap load would wipe _widgetHeight and collapse the node. + const controlsObserver = new ResizeObserver(() => { + const measured = frameRow.offsetHeight; + if (measured <= 0 || measured === controlsHeight) return; + controlsHeight = measured; + const viewportPx = parseInt(viewport.style.height, 10) || 0; + node._widgetHeight = viewportPx + controlsHeight; + node.setSize([node.size[0], computeNodeHeight()]); + node.graph?.setDirtyCanvas(true, true); + }); + controlsObserver.observe(frameRow); + + let resizing = false; + function resizeToFit() { + if (resizing) return; + resizing = true; + try { + const srcW = state.width || 512, srcH = state.height || 512; + const availW = Math.max(100, node.size[0] - 30); + const ratio = srcH / srcW; + const displayH = Math.round(availW * ratio); + const totalH = displayH + controlsHeight; + viewport.style.width = availW + "px"; + viewport.style.height = displayH + "px"; + // min-height keeps children visible when ComfyUI sets the container height from a stale computeSize. + container.style.minHeight = totalH + "px"; + node._widgetHeight = totalH; + node.setSize([node.size[0], computeNodeHeight()]); + node.graph?.setDirtyCanvas(true, true); + } finally { + resizing = false; + } + } + + chainCallback(node, "onResize", function () { + if (state.frames.length) resizeToFit(); + }); + + async function applyPreviewData(data) { + const gen = ++state.execGen; + + stopPlayback(); + + for (const f of state.frames) { + try { f.close?.(); } catch {} + } + state.frames = []; + state.uploadedFrame = -1; + + state.frameCount = data.frame_count || 0; + state.width = data.width || 512; + state.height = data.height || 512; + state.fps = data.fps || 24; + state.inputSpace = data.input_space || "logc3"; + state.linearScale = data.linear_scale || 1.0; + const restoredFrame = Number.isInteger(data.current_frame) ? data.current_frame : 0; + state.currentFrame = Math.min(Math.max(0, restoredFrame), Math.max(0, state.frameCount - 1)); + + frameSlider.max = String(Math.max(0, state.frameCount - 1)); + frameSlider.value = String(state.currentFrame); + updateFrameLabel(); + + if (!state.gl) initGL(); + + resizeToFit(); + + const bitmaps = await Promise.all( + (data.frames || []).map(f => loadImageBitmapFromView(f.filename, f.type)) + ); + + if (gen !== state.execGen) { + for (const b of bitmaps) { + try { b?.close?.(); } catch {} + } + return; + } + + state.frames = bitmaps.filter(Boolean); + // Re-assert size in case the ResizeObserver fired mid-await with empty frames. + resizeToFit(); + requestRender(); + } + + chainCallback(node, "onExecuted", async function (message) { + const data = message?.hdr_preview_data?.[0]; + if (!data) return; + + node.properties = node.properties || {}; + node.properties.hdrLastPreview = { + frames: data.frames, + width: data.width, + height: data.height, + fps: data.fps, + input_space: data.input_space, + linear_scale: data.linear_scale, + frame_count: data.frame_count, + }; + // Autosave snapshots before execution, so post-execute property updates need an explicit dirty nudge. + node.graph?.change?.(); + try { + const ct = app.extensionManager?.workflow?.activeWorkflow?.changeTracker; + ct?.checkState?.(); + } catch {} + try { app.workflowManager?.activeWorkflow?.changeTracker?.checkState?.(); } catch {} + + await applyPreviewData(data); + }); + + chainCallback(node, "onConfigure", function () { + if (exposureWidget && isFinite(exposureWidget.value)) state.exposure = exposureWidget.value; + if (saturationWidget && isFinite(saturationWidget.value)) state.saturation = saturationWidget.value; + const saved = node.properties?.hdrLastPreview; + if (saved?.frames?.length) applyPreviewData(saved); + }); + + chainCallback(node, "onRemoved", function () { + stopPlayback(); + controlsObserver.disconnect(); + hdrSyncGroup.delete(syncHandle); + for (const f of state.frames) { + try { f.close?.(); } catch {} + } + state.frames = []; + if (state.gl) { + try { + state.gl.deleteProgram(state.program); + state.gl.deleteTexture(state.texture); + state.gl.deleteVertexArray(state.vao); + const lose = state.gl.getExtension("WEBGL_lose_context"); + if (lose) lose.loseContext(); + } catch {} + state.gl = null; + } + }); + }); + }, +}); diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/web/js/help_popup.js b/ComfyUI/custom_nodes/ComfyUI-KJNodes/web/js/help_popup.js new file mode 100644 index 0000000000000000000000000000000000000000..872ab81cec7498fd979785eb9a40d5ad539d451f --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-KJNodes/web/js/help_popup.js @@ -0,0 +1,471 @@ +const { app } = window.comfyAPI.app; + +// code based on mtb nodes by Mel Massadian https://github.com/melMass/comfy_mtb/ +export const loadScript = ( + FILE_URL, + async = true, + type = 'text/javascript', +) => { + return new Promise((resolve, reject) => { + try { + // Check if the script already exists + const existingScript = document.querySelector(`script[src="${FILE_URL}"]`) + if (existingScript) { + resolve({ status: true, message: 'Script already loaded' }) + return + } + + const scriptEle = document.createElement('script') + scriptEle.type = type + scriptEle.async = async + scriptEle.src = FILE_URL + + scriptEle.addEventListener('load', (ev) => { + resolve({ status: true }) + }) + + scriptEle.addEventListener('error', (ev) => { + reject({ + status: false, + message: `Failed to load the script ${FILE_URL}`, + }) + }) + + document.body.appendChild(scriptEle) + } catch (error) { + reject(error) + } + }) +} + +// Only load the marked/DOMPurify fallback when the native renderer is absent. +if (!app.extensionManager?.renderMarkdownToHtml) { + loadScript('kjweb_async/marked.min.js').catch((e) => { + console.error(e) + }) + loadScript('kjweb_async/purify.min.js').catch((e) => { + console.error(e) + }) +} + +const categories = ["KJNodes", "SUPIR", "VoiceCraft", "Marigold", "IC-Light", "WanVideoWrapper"]; +const nodeDescriptions = new Map(); + +function isHelpPopupEnabled() { + return app.ui.settings.getSettingValue("KJNodes.helpPopup") !== false; +} + +app.registerExtension({ + name: "KJNodes.HelpPopup", + async beforeRegisterNodeDef(nodeType, nodeData) { + if (!isHelpPopupEnabled()) return; + try { + categories.forEach(category => { + if (nodeData?.category?.startsWith(category)) { + if (nodeData.description) { + nodeDescriptions.set(nodeData.name, nodeData.description); + } + addDocumentation(nodeData, nodeType); + } + else return + }); + } catch (error) { + console.error("Error in registering KJNodes.HelpPopup", error); + } + }, + nodeCreated(node) { + if (!isHelpPopupEnabled()) return; + const description = nodeDescriptions.get(node.type) || nodeDescriptions.get(node.comfyClass); + if (!description) return; + node._kjHelpDescription = description; + }, + setup() { + if (!isHelpPopupEnabled()) return; + setupHelpObserver(); + }, +}); + +const create_documentation_stylesheet = () => { + const tag = 'kj-documentation-stylesheet' + + let styleTag = document.getElementById(tag) + + if (!styleTag) { + styleTag = document.createElement('style') + styleTag.type = 'text/css' + styleTag.id = tag + styleTag.innerHTML = ` + .kj-documentation-popup { + background: var(--comfy-menu-bg); + position: absolute; + color: var(--fg-color); + font: 12px monospace; + line-height: 1.5em; + padding: 10px; + border-radius: 10px; + border-style: solid; + border-width: medium; + border-color: var(--border-color); + z-index: 5; + overflow: hidden; + } + .content-wrapper { + overflow: auto; + max-height: 100%; + /* Scrollbar styling for Chrome */ + &::-webkit-scrollbar { + width: 6px; + } + &::-webkit-scrollbar-track { + background: var(--bg-color); + } + &::-webkit-scrollbar-thumb { + background-color: var(--fg-color); + border-radius: 6px; + border: 3px solid var(--bg-color); + } + + /* Scrollbar styling for Firefox */ + scrollbar-width: thin; + scrollbar-color: var(--fg-color) var(--bg-color); + a { + color: yellow; + } + a:visited { + color: orange; + } + a:hover { + color: red; + } + } + ` + document.head.appendChild(styleTag) + } + } + +/** + * Creates the documentation popup DOM and wires up resize/close interactions. + * Returns { docElement, contentWrapper }. + * @param {string} description - Markdown description text + * @param {AbortSignal} signal - Signal to clean up event listeners + * @param {() => void} onClose - Called when the close button is clicked + * @param {{ scaleResize: boolean }} opts - If scaleResize is true, resize deltas are divided by canvas scale + */ +function createDocPopup(description, signal, onClose, opts = {}) { + create_documentation_stylesheet() + + const docElement = document.createElement('div') + const contentWrapper = document.createElement('div') + docElement.appendChild(contentWrapper) + + contentWrapper.classList.add('content-wrapper') + docElement.classList.add('kj-documentation-popup') + // Try ComfyUI's built-in markdown renderer first (available after frontend PR #10700) + if (app.extensionManager?.renderMarkdownToHtml) { + contentWrapper.innerHTML = app.extensionManager.renderMarkdownToHtml(description) + } else if (typeof marked !== 'undefined' && typeof DOMPurify !== 'undefined') { + contentWrapper.innerHTML = DOMPurify.sanitize(marked.parse(description)) + } else { + // Fallback: convert markdown links to
    tags, auto-link bare URLs, preserve line breaks + const escaped = description + .replace(/&/g, '&').replace(//g, '>') + .replace(/\[([^\]]+)\]\((https?:\/\/[^)]+)\)/g, '$1') + .replace(/(^|[^"'])(https?:\/\/[^\s<]+)/g, '$1$2') + .replace(/\n/g, '
    ') + contentWrapper.innerHTML = escaped + } + + // resize handle + const resizeHandle = document.createElement('div') + resizeHandle.style.width = '0' + resizeHandle.style.height = '0' + resizeHandle.style.position = 'absolute' + resizeHandle.style.bottom = '0' + resizeHandle.style.right = '0' + resizeHandle.style.cursor = 'se-resize' + const borderColor = getComputedStyle(document.documentElement).getPropertyValue('--border-color').trim() + resizeHandle.style.borderTop = '10px solid transparent' + resizeHandle.style.borderLeft = '10px solid transparent' + resizeHandle.style.borderBottom = `10px solid ${borderColor}` + resizeHandle.style.borderRight = `10px solid ${borderColor}` + docElement.appendChild(resizeHandle) + + let isResizing = false + let startX, startY, startWidth, startHeight + resizeHandle.addEventListener('mousedown', (e) => { + e.preventDefault() + e.stopPropagation() + isResizing = true + startX = e.clientX + startY = e.clientY + startWidth = parseInt(document.defaultView.getComputedStyle(docElement).width, 10) + startHeight = parseInt(document.defaultView.getComputedStyle(docElement).height, 10) + }, { signal }) + + document.addEventListener('mousemove', (e) => { + if (!isResizing) return + const scaleFactor = opts.scaleResize ? app.canvas.ds.scale : 1 + const newWidth = startWidth + (e.clientX - startX) / scaleFactor + const newHeight = startHeight + (e.clientY - startY) / scaleFactor + docElement.style.width = `${newWidth}px` + docElement.style.height = `${newHeight}px` + }, { signal }) + + document.addEventListener('mouseup', () => { + isResizing = false + }, { signal }) + + // close button + const closeButton = document.createElement('div') + closeButton.textContent = '❌' + closeButton.style.position = 'absolute' + closeButton.style.top = '0' + closeButton.style.right = '0' + closeButton.style.cursor = 'pointer' + closeButton.style.padding = '5px' + closeButton.style.color = 'red' + closeButton.style.fontSize = '12px' + docElement.appendChild(closeButton) + + closeButton.addEventListener('mousedown', (e) => { + e.stopPropagation() + onClose() + }, { signal }) + + document.body.appendChild(docElement) + return { docElement, contentWrapper } +} + +// ─── Legacy canvas mode (onDrawForeground) ─── + +/** Add documentation widget to the selected node (legacy canvas rendering) */ +export const addDocumentation = ( + nodeData, + nodeType, + opts = { icon_size: 14, icon_margin: 4 },) => { + + opts = opts || {} + const iconSize = opts.icon_size ? opts.icon_size : 14 + const iconMargin = opts.icon_margin ? opts.icon_margin : 4 + let docElement = null + let contentWrapper = null + + if (!nodeData.description) { + return + } + + const drawFg = nodeType.prototype.onDrawForeground + nodeType.prototype.onDrawForeground = function (ctx) { + const r = drawFg ? drawFg.apply(this, arguments) : undefined + if (this.flags.collapsed) return r + + const x = this.size[0] - iconSize - iconMargin + + // create the popup + if (this.show_doc && docElement === null) { + const popup = createDocPopup( + nodeData.description, + this.docCtrl.signal, + () => { + this.show_doc = !this.show_doc + docElement.parentNode.removeChild(docElement) + docElement = null + contentWrapper = null + }, + { scaleResize: true } + ) + docElement = popup.docElement + contentWrapper = popup.contentWrapper + } + // close the popup + else if (!this.show_doc && docElement !== null) { + docElement.parentNode.removeChild(docElement) + docElement = null + } + // update position of the popup + if (this.show_doc && docElement !== null) { + const rect = ctx.canvas.getBoundingClientRect() + const scaleX = rect.width / ctx.canvas.width + const scaleY = rect.height / ctx.canvas.height + + const transform = new DOMMatrix() + .scaleSelf(scaleX, scaleY) + .multiplySelf(ctx.getTransform()) + .translateSelf(this.size[0] * scaleX * Math.max(1.0,window.devicePixelRatio) , 0) + .translateSelf(10, -32) + + const scale = new DOMMatrix() + .scaleSelf(transform.a, transform.d); + const bcr = app.canvas.canvas.getBoundingClientRect() + + const styleObject = { + transformOrigin: '0 0', + transform: scale, + left: `${transform.a + bcr.x + transform.e}px`, + top: `${transform.d + bcr.y + transform.f}px`, + }; + Object.assign(docElement.style, styleObject); + } + + ctx.save() + ctx.translate(x - 2, iconSize - 34) + ctx.scale(iconSize / 32, iconSize / 32) + ctx.strokeStyle = 'rgba(255,255,255,0.3)' + ctx.lineCap = 'round' + ctx.lineJoin = 'round' + ctx.lineWidth = 2.4 + ctx.font = 'bold 36px monospace' + ctx.fillStyle = 'orange'; + ctx.fillText('?', 0, 24) + ctx.restore() + return r + } + + // handle clicking of the icon + const mouseDown = nodeType.prototype.onMouseDown + nodeType.prototype.onMouseDown = function (e, localPos, canvas) { + const r = mouseDown ? mouseDown.apply(this, arguments) : undefined + const iconX = this.size[0] - iconSize - iconMargin + const iconY = iconSize - 34 + if ( + localPos[0] > iconX && + localPos[0] < iconX + iconSize && + localPos[1] > iconY && + localPos[1] < iconY + iconSize + ) { + if (this.show_doc === undefined) { + this.show_doc = true + } else { + this.show_doc = !this.show_doc + } + if (this.show_doc) { + this.docCtrl = new AbortController() + } else { + this.docCtrl.abort() + } + return true; + } + return r; + } + + const onRem = nodeType.prototype.onRemoved + nodeType.prototype.onRemoved = function () { + const r = onRem ? onRem.apply(this, []) : undefined + if (docElement) { + docElement.remove() + docElement = null + contentWrapper = null + } + return r + } +} + +// ─── Vue nodes mode (DOM injection via MutationObserver) ─── + +/** Per-node popup state, keyed by node ID */ +const popupState = new Map() + +function getNodeById(nodeId) { + return app.graph?.getNodeById(nodeId) +} + +function closeNodePopup(nodeId) { + const state = popupState.get(nodeId) + if (!state) return + if (state.docElement) { + state.docElement.remove() + } + if (state.abortCtrl) { + state.abortCtrl.abort() + } + if (state.animFrame) { + cancelAnimationFrame(state.animFrame) + } + popupState.delete(nodeId) +} + +function openNodePopup(nodeId, description) { + closeNodePopup(nodeId) + const state = popupState.get(nodeId) || {} + popupState.set(nodeId, state) + + state.abortCtrl = new AbortController() + const popup = createDocPopup( + description, + state.abortCtrl.signal, + () => closeNodePopup(nodeId), + { scaleResize: false } + ) + state.docElement = popup.docElement + + function updatePosition() { + if (!state.docElement || !state.docElement.parentNode) return + const nodeEl = document.querySelector(`[data-node-id="${nodeId}"]`) + if (nodeEl) { + const rect = nodeEl.getBoundingClientRect() + state.docElement.style.left = `${rect.right + 10}px` + state.docElement.style.top = `${rect.top}px` + } + state.animFrame = requestAnimationFrame(updatePosition) + } + state.animFrame = requestAnimationFrame(updatePosition) +} + +/** Try to inject a "?" button into a Vue node header */ +function tryInjectHelpButton(header) { + if (header.querySelector('.kj-help-btn')) return + + const nodeEl = header.closest('[data-node-id]') + if (!nodeEl) return + const nodeId = nodeEl.dataset.nodeId + const node = getNodeById(nodeId) + if (!node) return + + const description = node._kjHelpDescription + if (!description) return + + const flexContainer = header.querySelector(':scope > div') + if (!flexContainer) return + + const helpBtn = document.createElement('span') + helpBtn.className = 'kj-help-btn' + helpBtn.textContent = '?' + helpBtn.style.cssText = ` + color: orange; + font-weight: bold; + font-size: 14px; + cursor: pointer; + flex-shrink: 0; + padding: 0 4px; + line-height: 1; + user-select: none; + ` + helpBtn.title = 'Show help' + helpBtn.addEventListener('click', (e) => { + e.stopPropagation() + const state = popupState.get(nodeId) + if (state?.docElement) { + closeNodePopup(nodeId) + } else { + openNodePopup(nodeId, description) + } + }) + flexContainer.appendChild(helpBtn) +} + +/** Observe the DOM for Vue node headers appearing and inject help buttons */ +function setupHelpObserver() { + // Inject into any headers already in the DOM + document.querySelectorAll('.lg-node-header').forEach(tryInjectHelpButton) + + let pending = false + const observer = new MutationObserver(() => { + if (pending) return + pending = true + requestAnimationFrame(() => { + pending = false + document.querySelectorAll('.lg-node-header:not(:has(.kj-help-btn))').forEach(tryInjectHelpButton) + }) + }) + observer.observe(document.body, { childList: true, subtree: true }) +} diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/web/js/ideogram4_prompt_builder.js b/ComfyUI/custom_nodes/ComfyUI-KJNodes/web/js/ideogram4_prompt_builder.js new file mode 100644 index 0000000000000000000000000000000000000000..ff3df3e22b9befe08885c4f4f3a9c75535d26e64 --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-KJNodes/web/js/ideogram4_prompt_builder.js @@ -0,0 +1,2701 @@ +import { chainCallback, addMiddleClickPan, addWheelPassthrough, cursorForBboxMode, watchImageInputs, captureVideoFrame } from './utility.js'; +const { app } = window.comfyAPI.app; + +const HANDLE = 8; // hit radius (canvas px) for corners/edges +const MAX_ELEM_COLORS = 5; // Ideogram 4 per-element palette cap +const MAX_STYLE_COLORS = 16; // Ideogram 4 style palette cap +const DOCK_MINW = 300, DOCK_MINH = 240; // dock min size — kept in sync with the .kjideo-dock CSS below +let copiedBoxes = null; // internal clipboard for copy/paste of regions (array; shared across nodes) + +// Track the most recent generated image so it can be grabbed as a background. +let lastResultImage = null; +try { + // Keep the latest image of the prompt; by the time the prompt finishes this is the real output + // (the output node runs after the sampler), so an early image-preview node never wins. + app.api?.addEventListener?.("executed", (e) => { + const imgs = e?.detail?.output?.images; + if (Array.isArray(imgs) && imgs.length) lastResultImage = imgs[imgs.length - 1]; + }); + // Grab the full-res final once the prompt completes — independent of sampler previews, so it works + // even when sampling preview is disabled. + app.api?.addEventListener?.("execution_success", () => { + for (const n of livePreviewNodes) n._ideoGrabFinal?.(); + }); +} catch (e) {} +function resultViewUrl(img) { + const p = new URLSearchParams({ filename: img.filename || "", subfolder: img.subfolder || "", type: img.type || "output" }); + return "/view?" + p.toString(); +} + +// Nodes opted into "live background": feed them the sampling preview frames as they arrive. +const livePreviewNodes = new Set(); +let _lastPreviewBlob = null; +let _previewOffset = 0; // byte offset of the image inside the preview blob (0 = clean image) +function pushPreview(img) { for (const n of livePreviewNodes) n._ideoSetLiveBg?.(img); } +// Decode a blob to an via object URL (same path the final result uses); resolves null on failure. +function decodeToImg(blob) { + return new Promise((resolve) => { + const url = URL.createObjectURL(blob); + const img = new Image(); + img.onload = () => { resolve(img); URL.revokeObjectURL(url); }; // img retains its data after revoke + img.onerror = () => { resolve(null); URL.revokeObjectURL(url); }; + img.src = url; + }); +} +// Offset of an embedded JPEG (FF D8 FF) or PNG signature, or -1. Some servers frame the preview +// payload with header bytes the bundled frontend doesn't strip, so the image isn't at byte 0. +function imageStart(u) { + for (let i = 0; i + 3 < u.length; i++) { + if (u[i] === 0xff && u[i + 1] === 0xd8 && u[i + 2] === 0xff) return i; + if (u[i] === 0x89 && u[i + 1] === 0x50 && u[i + 2] === 0x4e && u[i + 3] === 0x47) return i; + } + return -1; +} +async function feedPreviewBlob(blob) { + // Both "b_preview" and "b_preview_with_metadata" can fire for the same frame (same Blob) — dedupe by identity. + if (!blob || blob === _lastPreviewBlob || !livePreviewNodes.size) return; + _lastPreviewBlob = blob; + let img = await decodeToImg(_previewOffset ? blob.slice(_previewOffset) : blob); + if (!img) { // wrong/stale offset — locate the embedded image and retry + const off = imageStart(new Uint8Array(await blob.arrayBuffer())); + _previewOffset = off > 0 ? off : 0; + if (off >= 0) img = await decodeToImg(off > 0 ? blob.slice(off) : blob); + } + if (img) pushPreview(img); +} +try { + app.api?.addEventListener?.("b_preview", (e) => feedPreviewBlob(e?.detail)); + app.api?.addEventListener?.("b_preview_with_metadata", (e) => feedPreviewBlob(e?.detail?.blob)); +} catch (e) {} + +// Named caption-JSON templates, each stored as its own file server-side via ComfyUI's userdata +// API (under the ComfyUI user dir) so they persist across browsers/machines and survive clears. +const TPL_DIR = "kjnodes/ideogram4/templates"; +const tplSafe = (s) => (s || "").replace(/[\/\\:*?"<>|]+/g, "_").trim(); // filesystem-safe name +const tplFile = (name) => `${TPL_DIR}/${name}.json`; +async function listTemplateNames() { + try { + const items = await app.api.listUserDataFullInfo(TPL_DIR); + return items.map((it) => it.path.split(/[\\/]/).pop() || "") + .filter((f) => /\.json$/i.test(f)) + .map((f) => f.replace(/\.json$/i, "")) + .filter(Boolean).sort((a, b) => a.localeCompare(b)); + } catch (e) { return []; } +} +async function loadTemplate(name) { + try { const r = await app.api.getUserData(tplFile(name)); if (r.status === 200) return await r.text(); } catch (e) {} + return null; +} +async function saveTemplate(name, caption) { + try { + await app.api.storeUserData(tplFile(name), caption, { overwrite: true, stringify: false, throwOnError: true }); + return true; + } catch (e) { window.alert("Couldn't save the template to the server."); return false; } +} +async function deleteTemplate(name) { + try { await app.api.deleteUserData(tplFile(name)); } catch (e) {} +} + +// Parse a #rrggbb hex into {r,g,b}, or null if malformed. +function hexRgb(hex) { + const h = (hex || "").replace("#", ""); + if (h.length < 6) return null; + return { r: parseInt(h.slice(0, 2), 16), g: parseInt(h.slice(2, 4), 16), b: parseInt(h.slice(4, 6), 16) }; +} +// Perceived luminance (0-255) of an {r,g,b}. +function luminance({ r, g, b }) { return 0.299 * r + 0.587 * g + 0.114 * b; } + +// The hex color lightened toward white if too dark, so it stays readable on the dark canvas. +function readableText(hex) { + const c = hexRgb(hex); + if (!c) return "#d4d4d4"; + let { r, g, b } = c; + const lum = luminance(c), MIN = 130; + if (lum < MIN) { + const t = (MIN - lum) / (255 - lum); + r = Math.round(r + (255 - r) * t); g = Math.round(g + (255 - g) * t); b = Math.round(b + (255 - b) * t); + } + return `rgb(${r},${g},${b})`; +} + +// Black or white, whichever contrasts better with the given hex background. +function textOn(hex) { + const c = hexRgb(hex); + if (!c) return "#000"; + return luminance(c) > 140 ? "#000" : "#fff"; +} + +// Parse a clipboard string into a normalized #rrggbb, or null if it isn't a color. +function parseColorString(s) { + if (!s) return null; + s = s.trim(); + let m = s.match(/^#?([0-9a-fA-F]{6})$/); + if (m) return "#" + m[1].toLowerCase(); + m = s.match(/^#?([0-9a-fA-F]{3})$/); + if (m) { const h = m[1]; return ("#" + h[0] + h[0] + h[1] + h[1] + h[2] + h[2]).toLowerCase(); } + m = s.match(/^rgba?\(\s*(\d+)\s*[, ]\s*(\d+)\s*[, ]\s*(\d+)/i); + if (m) { + const h2 = (n) => Math.max(0, Math.min(255, parseInt(n, 10))).toString(16).padStart(2, "0"); + return "#" + h2(m[1]) + h2(m[2]) + h2(m[3]); + } + return null; +} +// The palette swatch under the cursor + a setter, so Ctrl+V pastes a color onto it +// and Ctrl+C copies its hex. +let hoveredSwatch = null; +document.addEventListener("keydown", (e) => { + if (!hoveredSwatch) return; + const ctrl = e.ctrlKey || e.metaKey; + if (!ctrl) return; + const key = e.key.toLowerCase(); + if (key !== "v" && key !== "c") return; + const ae = document.activeElement; // don't steal copy/paste from a focused text field + if (ae && (ae.tagName === "TEXTAREA" || ae.tagName === "INPUT" || ae.isContentEditable)) return; + const target = hoveredSwatch; + if (!target.sw || !target.sw.isConnected) return; + e.preventDefault(); e.stopPropagation(); + if (key === "c") { + navigator.clipboard?.writeText?.(target.sw.dataset.hex || "").catch(() => {}); + } else { + navigator.clipboard?.readText?.().then((txt) => { + const c = parseColorString(txt); + if (c && target.sw && target.sw.isConnected) target.setColor(c); + }).catch(() => {}); + } +}, true); + +// The node whose canvas is under the cursor — lets H toggle box visibility on hover (no click needed). +let hoveredCanvasNode = null; +document.addEventListener("keydown", (e) => { + if (!hoveredCanvasNode) return; + if (e.ctrlKey || e.metaKey || e.altKey || (e.key !== "h" && e.key !== "H")) return; + const ae = document.activeElement; + if (ae && (ae.tagName === "TEXTAREA" || ae.tagName === "INPUT" || ae.isContentEditable)) return; + e.preventDefault(); e.stopPropagation(); + hoveredCanvasNode._toggleHideBoxes?.(); +}, true); +// Alt held over a canvas: preview the alt-click cycle target even without moving the mouse. +document.addEventListener("keydown", (e) => { if (hoveredCanvasNode && e.key === "Alt") hoveredCanvasNode._altRefresh?.(true); }, true); +document.addEventListener("keyup", (e) => { if (hoveredCanvasNode && e.key === "Alt") hoveredCanvasNode._altRefresh?.(false); }, true); + +// Pinned dock follows the node: Nodes 2.0 hosts it inside the node element (inherits its transform); legacy is body-fixed. +const pinnedDocks = new Set(); +const liveDocks = new Set(); // every node with a dock — swept so an orphaned dock can't linger +window.addEventListener("resize", () => wakeDocks()); // canvas may shift without pan/zoom +// Safety net: a dock whose node has left the graph (deleted, workflow cleared) is torn down even if onRemoved didn't fire. +function sweepOrphanDocks() { + for (const n of liveDocks) { + if (n.graph?.getNodeById?.(n.id) === n) continue; // still registered in its graph + try { n._dockRO?.disconnect(); } catch (e) {} + try { n._visObserver?.disconnect(); } catch (e) {} + n._dockEl?.remove(); + pinnedDocks.delete(n); liveDocks.delete(n); + } +} +// What the dock follows in the displayed graph, or null to hide: the node ("self", offset=dockGraph), +// or — opt-in via exposeToParent — the SubgraphNode wrapping its subgraph ("parent", offset=dockParent; v1 direct parent only). +function dockAnchor(n) { + const c = app.canvas; + if (!c || !c.graph) return null; + if (n.graph === c.graph) return { host: n, ctx: "self" }; + if (n.properties.exposeToParent && n.graph) { + const nodes = c.graph._nodes || c.graph.nodes || []; + const sn = nodes.find((m) => m && m.subgraph === n.graph); // SubgraphNode wrapping the node's subgraph + if (sn) return { host: sn, ctx: "parent" }; + } + return null; +} +// gr.x/gr.y = offset (graph units) from host.pos (host = the node, or its SubgraphNode at parent level); +// size (w/h) always lives in dockGraph. `rect` lets the tick loop share one canvas getBoundingClientRect. +function applyDockTransform(n, anchor, rect) { + const c = app.canvas, fl = n._dockEl; + if (!c || !fl) return; + anchor = anchor || dockAnchor(n); + if (!anchor) return; // off-screen; visibility handled in tickDocks + const host = anchor.host; + n._dockHost = host; n._dockCtx = anchor.ctx; // drag/snap handlers read these + const gr = anchor.ctx === "parent" + ? (n.properties.dockParent || (n.properties.dockParent = { x: 0, y: (host.size ? host.size[1] : 0) + 2 })) + : n.properties.dockGraph; + if (!gr) return; + let hostEl = null; + if (window.LiteGraph?.vueNodesMode && host.id != null) { // cache; re-query if the host changed or it's gone + if (n._dockHostId !== host.id || !n._dockHostEl || !n._dockHostEl.isConnected) { + n._dockHostEl = document.querySelector(`[data-node-id="${host.id}"]`); + n._dockHostId = host.id; n._dockSig = ""; + } + hostEl = n._dockHostEl; + } + if (hostEl) { // host inside the host element → inherits its transform + const title = window.LiteGraph?.NODE_TITLE_HEIGHT ?? 30; // element top is the title bar; host.pos sits below it + const sig = "v|" + host.id + "|" + gr.x + "|" + gr.y; + if (fl.parentElement !== hostEl) { + hostEl.appendChild(fl); + fl.style.position = "absolute"; fl.style.transform = ""; fl.style.transformOrigin = ""; fl.style.zIndex = ""; + n._dockSig = ""; + } + if (n._dockSig !== sig) { fl.style.left = gr.x + "px"; fl.style.top = (title + gr.y) + "px"; n._dockSig = sig; } + return; + } + // Legacy / fallback (no Vue element, e.g. just after a mode switch): body-fixed from host.pos + transform. + if (fl.parentElement !== document.body) { document.body.appendChild(fl); fl.style.left = ""; fl.style.top = ""; n._dockSig = ""; } // re-attach if orphaned + if (!host.pos) return; + const ds = c.ds, scale = ds.scale; + rect = rect || c.canvas.getBoundingClientRect(); + const baseLeft = rect.left + (host.pos[0] + gr.x + ds.offset[0]) * scale; + const baseTop = rect.top + (host.pos[1] + gr.y + ds.offset[1]) * scale; + const tf = `translate(${baseLeft}px,${baseTop}px) scale(${scale})`; + if (n._dockSig !== tf) { fl.style.position = "fixed"; fl.style.transformOrigin = "top left"; fl.style.transform = tf; n._dockSig = tf; } + const order = c.graph?.nodes?.indexOf(host); // sit at the node DOM-widget layer (not above everything) + if (order != null && order >= 0) fl.style.zIndex = String(order); +} +// Dock chrome follows the node: color theme (title → header, body → background; dark fallback when uncolored) +// and pinned state (node pinned → dock locked, no drag/resize). +function applyDockTheme(n) { + const fl = n._dockEl; if (!fl) return; + const pinned = !!(n.flags && n.flags.pinned); + const sig = (n.color || "") + "|" + (n.bgcolor || "") + "|" + pinned; + if (n._dockTheme === sig) return; + n._dockTheme = sig; + const set = (k, v) => (v ? fl.style.setProperty(k, v) : fl.style.removeProperty(k)); + set("--kj-dock-bg", n.bgcolor); + set("--kj-dock-head", n.color); + set("--kj-dock-border", n.color || n.bgcolor); + fl.classList.toggle("pinned", pinned); +} +// Event-driven loop: woken by canvas redraws (dirty-gated) + resize, self-stops when settled. +const DOCK_IDLE_STOP = 6; // stop after this many frames with no further wake +let _dockRAF = 0, _dockLastMode = null, _dockIdle = 0, _dockWakesInstalled = false; +function tickDocks() { + const c = app.canvas; + if (c && pinnedDocks.size) { + const vue = !!window.LiteGraph?.vueNodesMode; + if (vue !== _dockLastMode) { // legacy↔2.0 flip rebuilds the node DOM — force re-parent + re-place + _dockLastMode = vue; + for (const n of pinnedDocks) { n._dockSig = ""; n._dockHostEl = null; n._dockHostId = null; } + } + const rect = vue ? null : c.canvas.getBoundingClientRect(); // legacy: share one rect across all docks + for (const n of pinnedDocks) { // anchor = node (own graph on screen) or its SubgraphNode (exposed) + applyDockTheme(n); + if (n._exBtn) n._exBtn.style.display = (n.graph && !n.graph.isRootGraph) ? "" : "none"; // expose only matters inside a subgraph + const anchor = dockAnchor(n); + if (n._dockEl) n._dockEl.style.display = anchor ? "" : "none"; + if (anchor) applyDockTransform(n, anchor, rect); + } + } + if (pinnedDocks.size && ++_dockIdle < DOCK_IDLE_STOP) _dockRAF = requestAnimationFrame(tickDocks); + else _dockRAF = 0; // settled — stop until the next wake +} +function wakeDocks() { // a canvas change happened — run/continue the loop until it settles + _dockIdle = 0; + if (!_dockRAF && pinnedDocks.size) _dockRAF = requestAnimationFrame(tickDocks); +} +function installDockWakes() { // onDrawForeground only fires when dirty_canvas is set + if (_dockWakesInstalled) return; + const c = app.canvas; if (!c) return; + _dockWakesInstalled = true; + chainCallback(c, "onDrawForeground", () => { sweepOrphanDocks(); wakeDocks(); }); +} +function startDockLoop() { installDockWakes(); wakeDocks(); } + +// Pointer-capture drag: filters moves by pointerId, auto-removes its listeners on release, calls onEnd. +function dragPointer(e, target, onMove, onEnd) { + try { target.setPointerCapture(e.pointerId); } catch (er) {} + const move = (me) => { if (me.pointerId === e.pointerId) onMove(me); }; + const end = (ue) => { + if (ue.pointerId !== e.pointerId) return; + target.removeEventListener("pointermove", move); + target.removeEventListener("pointerup", end); + target.removeEventListener("pointercancel", end); + if (onEnd) onEnd(ue); + }; + target.addEventListener("pointermove", move); + target.addEventListener("pointerup", end); + target.addEventListener("pointercancel", end); +} + +// Outside-click dismissal for a popup: arm() after showing, disarm() after hiding. +function outsideDismiss(menu, onDismiss, anchor) { + let handler = null; + const disarm = () => { + if (!handler) return; + document.removeEventListener("pointerdown", handler, true); + document.removeEventListener("mousedown", handler, true); + handler = null; + }; + const arm = () => { + disarm(); + handler = (e) => { if (!menu.contains(e.target) && e.target !== anchor) onDismiss(); }; + setTimeout(() => { // defer so the opening click itself doesn't dismiss + if (!handler) return; + document.addEventListener("pointerdown", handler, true); + document.addEventListener("mousedown", handler, true); + }, 0); + }; + return { arm, disarm }; +} + +function injectStyle() { + if (document.getElementById("kjideo-style")) return; + const s = document.createElement("style"); + s.id = "kjideo-style"; + s.textContent = ` + .kjideo-wrap { display:flex; flex-direction:column; overflow:hidden; position:relative; pointer-events:auto; gap:4px; } + .kjideo-cv { flex:1 1 auto; min-height:60px; display:flex; align-items:center; justify-content:center; overflow:hidden; } + .kjideo-canvas { cursor:crosshair; display:block; flex:0 0 auto; background:#1a1a1a; border-radius:4px; outline:none; touch-action:none; } + .kjideo-bar { display:flex; align-items:center; gap:6px; font:11px sans-serif; color:#aaa; user-select:none; padding:0 2px; flex:0 0 auto; } + .kjideo-panel { display:flex; flex-direction:column; gap:5px; padding:6px; background:#262626; border-radius:4px; font:11px sans-serif; color:#bbb; flex:0 0 auto; overflow-y:auto; min-height:0; } + .kjideo-split { flex:0 0 auto; height:8px; cursor:ns-resize; position:relative; } + .kjideo-split::before { content:""; position:absolute; left:50%; top:50%; transform:translate(-50%,-50%); width:34px; height:3px; background:#555; border-radius:2px; } + .kjideo-split:hover::before { background:#46b4e6; } + .kjideo-row { display:flex; align-items:center; gap:6px; flex-wrap:wrap; } + .kjideo-btn { background:#333; border:1px solid #555; border-radius:4px; color:#bbb; font:11px sans-serif; cursor:pointer; padding:2px 8px; line-height:16px; white-space:nowrap; flex-shrink:0; } + .kjideo-btn:hover { border-color:#46b4e6; color:#fff; } + .kjideo-btn.active { border-color:#46b4e6; color:#46b4e6; background:#2a3a42; } + .kjideo-area { width:100%; box-sizing:border-box; background:#1d1d1d; border:1px solid #444; border-radius:4px; color:#ddd; font:13px monospace; padding:4px 6px; resize:none; min-height:36px; flex:1 1 auto; } + .kjideo-sw { width:20px; height:20px; border:1px solid #666; border-radius:3px; cursor:pointer; flex-shrink:0; position:relative; touch-action:none; transition:transform .18s ease, box-shadow .12s ease, opacity .12s ease; } + .kjideo-sw:hover { transform:scale(1.2); box-shadow:0 0 0 2px #46b4e6; z-index:3; } + .kjideo-sw.dragging { opacity:.4; box-shadow:0 0 0 2px #46b4e6; } + body.kjideo-dragging, body.kjideo-dragging * { cursor:move !important; } + .kjideo-sw input { position:absolute; opacity:0; width:0; height:0; pointer-events:none; } + .kjideo-inline { position:absolute; box-sizing:border-box; background:rgba(18,18,18,0.92); border:2px solid #46b4e6; border-radius:3px; color:#fff; font:13px monospace; padding:3px 4px; resize:none; outline:none; z-index:10; } + .kjideo-bbox { width:128px; box-sizing:border-box; background:#1d1d1d; border:1px solid #444; border-radius:4px; color:#bbb; font:11px monospace; padding:2px 5px; } + .kjideo-bbox:focus { border-color:#46b4e6; outline:none; color:#fff; } + .kjideo-menu { position:fixed; z-index:10000; background:#262626; border:1px solid #555; border-radius:6px; padding:4px; box-shadow:0 6px 20px rgba(0,0,0,0.55); font:12px sans-serif; color:#ddd; max-height:60vh; overflow-y:auto; min-width:210px; max-width:340px; } + .kjideo-mhdr { font:11px sans-serif; color:#888; padding:2px 6px 4px; user-select:none; } + .kjideo-lrow { display:flex; align-items:center; gap:6px; padding:3px 5px; border-radius:4px; cursor:move; user-select:none; touch-action:none; transition:transform .18s ease, box-shadow .12s ease, opacity .12s ease, background .12s; } + .kjideo-lrow:hover { background:#333; } + .kjideo-lrow.active { background:#2a3a42; box-shadow:inset 0 0 0 1px #46b4e6; } + .kjideo-lrow.dragging { opacity:.4; box-shadow:0 0 0 2px #46b4e6; background:#333; } + .kjideo-lsw { width:16px; height:16px; border-radius:3px; border:1px solid #666; flex:0 0 auto; } + .kjideo-lnum { font:bold 11px monospace; color:#888; flex:0 0 auto; width:18px; } + .kjideo-ltext { flex:1 1 auto; min-width:0; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; } + .kjideo-ltext.empty { color:#777; font-style:italic; } + .kjideo-lbtn { background:none; border:none; color:#999; cursor:pointer; font:13px sans-serif; line-height:1; padding:2px 5px; border-radius:3px; flex:0 0 auto; } + .kjideo-lbtn:hover { color:#fff; background:#444; } + .kjideo-lbtn.del:hover { color:#fff; background:#a33; } + .kjideo-lbtn.on { background:#3a3320; } + .kjideo-lock { filter:grayscale(1); opacity:0.4; } /* unlocked: faded grey */ + .kjideo-lock.on, .kjideo-lock:hover { filter:none; opacity:1; } /* locked / hover: full colour */ + .kjideo-lbtn:disabled { opacity:0.25; cursor:default; background:none; } + .kjideo-fs { position:fixed; inset:0; z-index:9000; background:rgba(0,0,0,0.72); display:flex; align-items:center; justify-content:center; } + .kjideo-fs-inner { position:relative; width:88vw; height:90vh; background:#1a1a1a; border:1px solid #444; border-radius:8px; box-shadow:0 12px 48px rgba(0,0,0,0.6); padding:12px; box-sizing:border-box; } + .kjideo-fs-inner .kjideo-wrap { height:100%; } + .kjideo-fs-close { position:absolute; top:14px; right:18px; z-index:5; padding:4px 12px; font-size:14px; } + .kjideo-dock { position:fixed; z-index:8500; pointer-events:auto; display:flex; flex-direction:column; background:var(--kj-dock-bg,#1a1a1a); border:1px solid var(--kj-dock-border,#555); border-radius:8px; box-shadow:0 8px 30px rgba(0,0,0,0.55); min-width:${DOCK_MINW}px; min-height:${DOCK_MINH}px; overflow:hidden; } + .kjideo-rsz { position:absolute; z-index:20; touch-action:none; } + .kjideo-rsz.n { top:0; left:11px; right:11px; height:6px; cursor:ns-resize; } + .kjideo-rsz.s { bottom:0; left:11px; right:11px; height:6px; cursor:ns-resize; } + .kjideo-rsz.e { right:0; top:11px; bottom:11px; width:6px; cursor:ew-resize; } + .kjideo-rsz.w { left:0; top:11px; bottom:11px; width:6px; cursor:ew-resize; } + .kjideo-rsz.ne { top:0; right:0; width:12px; height:12px; cursor:nesw-resize; } + .kjideo-rsz.nw { top:0; left:0; width:12px; height:12px; cursor:nwse-resize; } + .kjideo-rsz.se { bottom:0; right:0; width:12px; height:12px; cursor:nwse-resize; } + .kjideo-rsz.sw { bottom:0; left:0; width:12px; height:12px; cursor:nesw-resize; } + .kjideo-dock.minimized { min-height:0 !important; height:auto !important; } + .kjideo-dock.minimized .kjideo-dock-body { display:none; } + .kjideo-dock.minimized .kjideo-rsz { display:none; } + .kjideo-dock.pinned .kjideo-rsz { display:none; } /* node pinned → locked, like the node */ + .kjideo-dock.pinned .kjideo-dock-head { cursor:default; } + .kjideo-dock.snap-ready { box-shadow:0 -3px 0 0 #46b4e6, 0 8px 30px rgba(0,0,0,0.55); } /* upper border lit = release to snap */ + .kjideo-dock-head { display:flex; align-items:center; gap:6px; padding:4px 8px; background:var(--kj-dock-head,#262626); cursor:move; font:12px sans-serif; color:#ccc; user-select:none; border-bottom:1px solid rgba(0,0,0,0.25); flex:0 0 auto; } + .kjideo-dock-head .kjideo-btn { padding:1px 7px; } + .kjideo-dock-body { flex:1 1 auto; min-height:0; padding:8px; box-sizing:border-box; overflow:hidden; } + .kjideo-dock-body .kjideo-wrap { height:100%; } + .kjideo-bgmenu { padding:7px; display:flex; flex-direction:column; gap:7px; min-width:170px; } + .kjideo-bgrow { display:flex; align-items:center; gap:8px; } + .kjideo-bglbl { color:#888; font:11px sans-serif; flex:0 0 auto; min-width:62px; } + .kjideo-trow { padding:2px 4px; border-radius:4px; } + .kjideo-trow:hover { background:#333; } + `; + document.head.appendChild(s); +} + +app.registerExtension({ + name: "KJNodes.Ideogram4PromptBuilder", + + async beforeRegisterNodeDef(nodeType, nodeData) { + if (nodeData?.name !== "Ideogram4PromptBuilderKJ") return; + injectStyle(); + + chainCallback(nodeType.prototype, "onNodeCreated", function () { + const node = this; + const findW = (n) => node.widgets?.find((w) => w.name === n); + const elementsWidget = findW("elements_data"); + const stylePaletteWidget = findW("style_palette_data"); + const bgBrightnessWidget = findW("bg_brightness"); + const formatWidget = findW("output_format"); // "pretty" | "compact" (set via the toolbar) + const coordWidget = findW("coord_mode"); // "normalized" | "absolute" (set via the toolbar) + const orderWidget = findW("bbox_order"); // "yx" (Ideogram) | "xy" (Qwen/standard) (set via the toolbar) + if (bgBrightnessWidget && typeof bgBrightnessWidget.value !== "number") bgBrightnessWidget.value = 25; + const wWidget = findW("width"), hWidget = findW("height"); + // Hide the data widgets while keeping them serializable. + function hideDataWidgets() { + for (const w of [elementsWidget, stylePaletteWidget, bgBrightnessWidget, formatWidget, coordWidget, orderWidget]) { + if (!w) continue; + w.hidden = true; + w.computeSize = () => [0, -4]; + } + for (const name of ["elements_data", "style_palette_data", "bg_brightness", "output_format", "coord_mode", "bbox_order"]) { + const i = node.inputs?.findIndex((inp) => inp.name === name); + if (i != null && i !== -1) node.removeInput(i); + } + } + hideDataWidgets(); + + node._boxes = []; // {x,y,w,h normalized 0-1, type, text, desc, palette[]} + node._stylePalette = []; // global style color palette (hex[]) + node._activeIdx = -1; + node._drawing = false; + node._placing = false; // duplicate-placement mode: active box follows the cursor until clicked + node._dragMode = null; + node._dragStartN = null; // mouse-down point, normalized + node._boxAtStart = null; // active box snapshot at drag start + node._selection = new Set(); // selected box indices (multi-select); always contains _activeIdx + node._groupStart = null; // {idx: {x,y,w,h}} snapshot of all selected boxes at drag start + node._pendingCollapse = -1; // box to collapse selection to if a click (not drag) on a multi-selection + node._marquee = null; // {x0,y0,x,y} rubber-band selection rect (shift-drag), normalized + node._hoverTitle = null; // index of the title chip under the cursor + node._hoverBox = null; // index of the box under the cursor + node._focused = false; // editor (DOM) focused — gates the active-box highlight + node._selected = false; // node selected in the graph + node._bgImg = null; // optional reference image shown as the canvas background + node._bgManual = false; // bg set via "use last result" (not the image input) + node._lastImported = ""; // last import_json applied to the editor (avoid re-apply) + node._areaObservers = []; // (reserved) live ResizeObservers to disconnect on rebuild + + // ── DOM ── + const wrap = document.createElement("div"); + wrap.className = "kjideo-wrap"; + const bar = document.createElement("div"); + bar.className = "kjideo-bar"; + const hint = document.createElement("span"); + hint.style.flex = "1"; + const copyBtn = document.createElement("button"); + copyBtn.className = "kjideo-btn"; + copyBtn.textContent = "Copy"; + copyBtn.title = "Copy the current caption JSON to the clipboard"; + const importBtn = document.createElement("button"); + importBtn.className = "kjideo-btn"; + importBtn.textContent = "Paste"; + importBtn.title = "Parse a caption JSON (clipboard, else paste prompt) and populate the node"; + const clearBtn = document.createElement("button"); + clearBtn.className = "kjideo-btn"; + clearBtn.textContent = "Clear all"; + const tokenSpan = document.createElement("span"); + tokenSpan.style.cssText = "color:#888; white-space:nowrap;"; + tokenSpan.title = "Rough token estimate (~chars/4). Grey <256, green healthy, orange nearing, red ≥2048 (model cap — will error)"; + // Output-settings dropdown (formatting + bbox coordinate space/order) — writes the hidden widgets. + const outBtn = document.createElement("button"); + outBtn.className = "kjideo-btn"; outBtn.textContent = "Output ▾"; + outBtn.title = "Output JSON settings: compact/pretty, bbox coordinate space and axis order"; + stopProp(outBtn); + const outToggle = (get, set, label, title) => { // checkbox row bound to a hidden widget + const l = document.createElement("label"); + l.style.cssText = "display:flex;align-items:center;gap:4px;cursor:pointer;"; l.title = title; + const cb = document.createElement("input"); cb.type = "checkbox"; cb.checked = get(); + stopProp(cb); + cb.addEventListener("change", () => { set(cb.checked); updateTokens(); flushChange(); }); + l.appendChild(cb); l.appendChild(document.createTextNode(label)); + l._cb = cb; return l; + }; + const compactLbl = outToggle( + () => (formatWidget?.value) !== "pretty", + (on) => { if (formatWidget) formatWidget.value = on ? "compact" : "pretty"; }, + "compact JSON", "Compact output (the format Ideogram 4 expects). Uncheck for pretty/indented."); + const absLbl = outToggle( + () => (coordWidget?.value) === "absolute", + (on) => { if (coordWidget) coordWidget.value = on ? "absolute" : "normalized"; }, + "absolute pixels", "bbox in absolute pixels (scaled by width/height) instead of the 0-1000 grid. NON-STANDARD for Ideogram 4."); + const xyLbl = outToggle( + () => (orderWidget?.value) === "xy", + (on) => { if (orderWidget) orderWidget.value = on ? "xy" : "yx"; }, + "xy order (Qwen)", "bbox axis order [xmin,ymin,xmax,ymax] (x1,y1,x2,y2, as Qwen-VL uses) instead of Ideogram's [ymin,xmin,ymax,xmax]."); + const outMenu = document.createElement("div"); + outMenu.className = "kjideo-menu kjideo-bgmenu"; + outMenu.style.display = "none"; + for (const l of [compactLbl, absLbl, xyLbl]) { const r = document.createElement("div"); r.className = "kjideo-bgrow"; r.appendChild(l); outMenu.appendChild(r); } + document.body.appendChild(outMenu); + node._outMenu = outMenu; + const outDismiss = outsideDismiss(outMenu, () => closeOutMenu(), outBtn); + function closeOutMenu() { outMenu.style.display = "none"; outDismiss.disarm(); } + outBtn.addEventListener("click", () => { + if (outMenu.style.display !== "none") { closeOutMenu(); return; } + outMenu.style.display = ""; + const r = outBtn.getBoundingClientRect(); + outMenu.style.left = Math.max(4, Math.min(r.left, window.innerWidth - outMenu.offsetWidth - 4)) + "px"; + outMenu.style.top = Math.min(r.bottom + 4, window.innerHeight - outMenu.offsetHeight - 4) + "px"; + outDismiss.arm(); + }); + const grabBtn = document.createElement("button"); + grabBtn.className = "kjideo-btn"; + grabBtn.addEventListener("mousedown", (e) => e.stopPropagation()); + grabBtn.addEventListener("click", () => { (node._bgManual && node._bgImg) ? node._clearBg() : node._grabResultBg(); }); + function updateGrabBtn() { + const clear = node._bgManual && node._bgImg; + grabBtn.textContent = clear ? "Clear BG" : "Grab BG"; + grabBtn.title = clear ? "Remove the grabbed background" + : "Use the last generated image as the background"; + } + const liveLabel = document.createElement("label"); + liveLabel.style.cssText = "display:flex;align-items:center;gap:3px;flex:0 0 auto;cursor:pointer;"; + liveLabel.title = "Use the live sampling preview as the background while generating"; + const liveChk = document.createElement("input"); + liveChk.type = "checkbox"; + liveChk.checked = !!node.properties.liveBg; + liveChk.addEventListener("mousedown", (e) => e.stopPropagation()); + liveChk.addEventListener("change", () => { + node.properties.liveBg = liveChk.checked; + if (liveChk.checked) livePreviewNodes.add(node); else livePreviewNodes.delete(node); + }); + liveLabel.appendChild(liveChk); liveLabel.appendChild(document.createTextNode("Live")); + if (liveChk.checked) livePreviewNodes.add(node); + const bgSlider = document.createElement("input"); + bgSlider.type = "range"; bgSlider.min = "0"; bgSlider.max = "100"; bgSlider.step = "1"; + bgSlider.value = bgBrightnessWidget ? bgBrightnessWidget.value : 25; + bgSlider.title = "Background brightness (image or blank canvas)"; + bgSlider.style.cssText = "width:64px;flex:0 0 auto;"; + stopProp(bgSlider); + bgSlider.addEventListener("input", () => { if (bgBrightnessWidget) bgBrightnessWidget.value = parseInt(bgSlider.value, 10); drawCanvas(); }); + const guideSel = document.createElement("select"); + guideSel.className = "kjideo-btn"; guideSel.style.cssText = "flex:0 0 auto;"; + guideSel.title = "Composition guide overlay (editor view only)"; + for (const [val, label] of [["none", "no guide"], ["thirds", "thirds"], ["grid", "grid"], ["golden", "golden ratio"], ["spiral", "golden spiral"]]) { + const o = document.createElement("option"); o.value = val; o.textContent = label; guideSel.appendChild(o); + } + guideSel.value = node.properties.guide || "none"; + stopProp(guideSel); + guideSel.addEventListener("change", () => { node.properties.guide = guideSel.value; drawCanvas(); }); + // Group the background/guide controls into one popup to keep the toolbar tidy. + const bgBtn = document.createElement("button"); + bgBtn.className = "kjideo-btn"; bgBtn.textContent = "Background ▾"; + bgBtn.title = "Background & guides: live preview, grab/clear, brightness, composition guide"; + stopProp(bgBtn); + const GRID_INV = 130; // slider shows cell SIZE: position = GRID_INV - divisions (right = larger cells) + const gridSlider = document.createElement("input"); + gridSlider.type = "range"; gridSlider.min = "2"; gridSlider.max = "128"; gridSlider.step = "1"; + gridSlider.value = GRID_INV - (node.properties.gridSize || 10); + gridSlider.style.cssText = "width:90px;flex:0 0 auto;"; + gridSlider.title = "Grid cell size (drag right for larger cells); also the snap step"; + stopProp(gridSlider); + gridSlider.addEventListener("input", () => { node.properties.gridSize = GRID_INV - parseInt(gridSlider.value, 10); drawCanvas(); }); + const snapLabel = document.createElement("label"); + snapLabel.style.cssText = "display:flex;align-items:center;gap:4px;cursor:pointer;"; + const snapChk = document.createElement("input"); + snapChk.type = "checkbox"; snapChk.checked = !!node.properties.snap; + snapChk.addEventListener("change", () => { node.properties.snap = snapChk.checked; }); + snapLabel.appendChild(snapChk); snapLabel.appendChild(document.createTextNode("snap to grid")); + const guideColor = document.createElement("input"); + guideColor.type = "color"; guideColor.value = node.properties.guideColor || "#ffffff"; + guideColor.style.cssText = "width:32px;height:20px;flex:0 0 auto;padding:0;border:1px solid #555;background:none;"; + stopProp(guideColor); + guideColor.addEventListener("input", () => { node.properties.guideColor = guideColor.value; drawCanvas(); }); + const opacitySlider = document.createElement("input"); + opacitySlider.type = "range"; opacitySlider.min = "0"; opacitySlider.max = "100"; opacitySlider.step = "1"; + opacitySlider.value = node.properties.guideOpacity == null ? 100 : node.properties.guideOpacity; + opacitySlider.style.cssText = "width:90px;flex:0 0 auto;"; + opacitySlider.title = "Guide/grid line opacity"; + stopProp(opacitySlider); + opacitySlider.addEventListener("input", () => { node.properties.guideOpacity = parseInt(opacitySlider.value, 10); drawCanvas(); }); + const bgMenu = document.createElement("div"); + bgMenu.className = "kjideo-menu kjideo-bgmenu"; + bgMenu.style.display = "none"; + const bgRow = (labelText, el) => { + const r = document.createElement("div"); r.className = "kjideo-bgrow"; + if (labelText) { const l = document.createElement("span"); l.className = "kjideo-bglbl"; l.textContent = labelText; r.appendChild(l); } + r.appendChild(el); bgMenu.appendChild(r); + }; + bgRow("", liveLabel); bgRow("", grabBtn); bgRow("Brightness", bgSlider); + bgRow("Guide", guideSel); bgRow("Grid size", gridSlider); bgRow("", snapLabel); + bgRow("Line color", guideColor); bgRow("Line opacity", opacitySlider); + document.body.appendChild(bgMenu); + node._bgMenu = bgMenu; + const bgDismiss = outsideDismiss(bgMenu, () => closeBgMenu(), bgBtn); + function closeBgMenu() { bgMenu.style.display = "none"; bgDismiss.disarm(); } + bgBtn.addEventListener("click", () => { + if (bgMenu.style.display !== "none") { closeBgMenu(); return; } + bgMenu.style.display = ""; + const r = bgBtn.getBoundingClientRect(); + bgMenu.style.left = Math.max(4, Math.min(r.left, window.innerWidth - bgMenu.offsetWidth - 4)) + "px"; + bgMenu.style.top = Math.min(r.bottom + 4, window.innerHeight - bgMenu.offsetHeight - 4) + "px"; + bgDismiss.arm(); + }); + // ── Text style popup: show/hide, outline, font size, auto-placement ── + const txtBtn = document.createElement("button"); + txtBtn.className = "kjideo-btn"; txtBtn.textContent = "Text ▾"; + txtBtn.title = "Region text: show/hide, outline, font size, overlap-avoiding placement"; + stopProp(txtBtn); + const txtToggle = (prop, label, title) => { + const l = document.createElement("label"); + l.style.cssText = "display:flex;align-items:center;gap:4px;cursor:pointer;"; l.title = title || ""; + const cb = document.createElement("input"); cb.type = "checkbox"; cb.checked = node.properties[prop] !== false; + cb.addEventListener("change", () => { node.properties[prop] = cb.checked; drawCanvas(); }); + l.appendChild(cb); l.appendChild(document.createTextNode(label)); + l._cb = cb; return l; + }; + const showLbl = txtToggle("showBoxText", "show text", "Draw each region's text inside its box"); + const strokeLbl = txtToggle("textStroke", "outline", "Dark halo behind the text for legibility"); + const autoLbl = txtToggle("textAutoPlace", "auto-place", "Stagger overlapping labels to reduce overlap"); + const sizeSlider = document.createElement("input"); + sizeSlider.type = "range"; sizeSlider.min = "8"; sizeSlider.max = "22"; sizeSlider.step = "1"; + sizeSlider.value = node.properties.textSize || 12; + sizeSlider.style.cssText = "width:90px;flex:0 0 auto;"; + sizeSlider.title = "Region text font size"; + stopProp(sizeSlider); + sizeSlider.addEventListener("input", () => { node.properties.textSize = parseInt(sizeSlider.value, 10); drawCanvas(); }); + const boxOpacSlider = document.createElement("input"); + boxOpacSlider.type = "range"; boxOpacSlider.min = "0"; boxOpacSlider.max = "100"; boxOpacSlider.step = "1"; + boxOpacSlider.value = node.properties.boxOpacity == null ? 14 : node.properties.boxOpacity; + boxOpacSlider.style.cssText = "width:90px;flex:0 0 auto;"; + boxOpacSlider.title = "Box fill opacity"; + stopProp(boxOpacSlider); + boxOpacSlider.addEventListener("input", () => { node.properties.boxOpacity = parseInt(boxOpacSlider.value, 10); drawCanvas(); }); + const txtMenu = document.createElement("div"); + txtMenu.className = "kjideo-menu kjideo-bgmenu"; + txtMenu.style.display = "none"; + const txtRow = (labelText, el) => { + const r = document.createElement("div"); r.className = "kjideo-bgrow"; + if (labelText) { const l = document.createElement("span"); l.className = "kjideo-bglbl"; l.textContent = labelText; r.appendChild(l); } + r.appendChild(el); txtMenu.appendChild(r); + }; + txtRow("", showLbl); txtRow("Font size", sizeSlider); txtRow("Box opacity", boxOpacSlider); txtRow("", strokeLbl); txtRow("", autoLbl); + document.body.appendChild(txtMenu); + node._txtMenu = txtMenu; + const txtDismiss = outsideDismiss(txtMenu, () => closeTxtMenu(), txtBtn); + function closeTxtMenu() { txtMenu.style.display = "none"; txtDismiss.disarm(); } + txtBtn.addEventListener("click", () => { + if (txtMenu.style.display !== "none") { closeTxtMenu(); return; } + txtMenu.style.display = ""; + const r = txtBtn.getBoundingClientRect(); + txtMenu.style.left = Math.max(4, Math.min(r.left, window.innerWidth - txtMenu.offsetWidth - 4)) + "px"; + txtMenu.style.top = Math.min(r.bottom + 4, window.innerHeight - txtMenu.offsetHeight - 4) + "px"; + txtDismiss.arm(); + }); + // ── Templates popup: save/load named caption JSONs (server-side userdata) ── + const tplBtn = document.createElement("button"); + tplBtn.className = "kjideo-btn"; tplBtn.textContent = "Templates ▾"; + tplBtn.title = "Save / load the caption JSON as templates (stored on the server ComfyUI\\user\\default\\kjnodes\\ideogram4)"; + stopProp(tplBtn); + const tplMenu = document.createElement("div"); + tplMenu.className = "kjideo-menu kjideo-bgmenu"; + tplMenu.style.display = "none"; + document.body.appendChild(tplMenu); + node._tplMenu = tplMenu; + const tplDismiss = outsideDismiss(tplMenu, () => closeTplMenu(), tplBtn); + function closeTplMenu() { tplMenu.style.display = "none"; tplDismiss.disarm(); } + async function buildTplMenu() { + tplMenu.innerHTML = ""; + const saveRow = document.createElement("div"); saveRow.className = "kjideo-bgrow"; + const saveBtn = document.createElement("button"); saveBtn.className = "kjideo-btn"; saveBtn.textContent = "+ Save as…"; + saveBtn.addEventListener("click", async () => { + const name = tplSafe(window.prompt("Save template as:", "") || ""); + if (!name) return; + const existing = await listTemplateNames(); + if (existing.includes(name) && !window.confirm(`Overwrite template "${name}"?`)) return; + if (await saveTemplate(name, buildCaption())) buildTplMenu(); + }); + saveRow.appendChild(saveBtn); tplMenu.appendChild(saveRow); + const names = await listTemplateNames(); + if (!names.length) { + const empty = document.createElement("div"); empty.className = "kjideo-mhdr"; empty.textContent = "No templates saved."; + tplMenu.appendChild(empty); + } + for (const name of names) { + const row = document.createElement("div"); row.className = "kjideo-bgrow kjideo-trow"; + const txt = document.createElement("span"); + txt.className = "kjideo-ltext"; txt.style.cssText = "flex:1 1 auto;cursor:pointer;"; txt.textContent = name; txt.title = "Load " + name + " (replaces everything)"; + const ins = document.createElement("button"); ins.className = "kjideo-lbtn"; ins.textContent = "⊞"; ins.title = "Insert this template's boxes only into the current canvas"; + const upd = document.createElement("button"); upd.className = "kjideo-lbtn"; upd.textContent = "⤓"; upd.title = "Save current (overwrite)"; + const del = document.createElement("button"); del.className = "kjideo-lbtn del"; del.textContent = "✕"; del.title = "Delete template"; + row.append(txt, ins, upd, del); tplMenu.appendChild(row); + txt.addEventListener("click", async () => { + const cap = tryParseCaption(await loadTemplate(name)); + if (!cap) { window.alert("That template isn't a valid caption JSON."); return; } + loadCaption(cap); closeTplMenu(); + }); + ins.addEventListener("click", async (e) => { + e.stopPropagation(); + const cap = tryParseCaption(await loadTemplate(name)); + if (!cap) { window.alert("That template isn't a valid caption JSON."); return; } + insertCaptionBoxes(cap); closeTplMenu(); + }); + upd.addEventListener("click", async (e) => { + e.stopPropagation(); + if (await saveTemplate(name, buildCaption())) buildTplMenu(); + }); + del.addEventListener("click", async (e) => { + e.stopPropagation(); + if (!window.confirm(`Delete template "${name}"?`)) return; + await deleteTemplate(name); buildTplMenu(); + }); + } + } + tplBtn.addEventListener("click", async () => { + if (tplMenu.style.display !== "none") { closeTplMenu(); return; } + tplMenu.style.display = ""; + await buildTplMenu(); + const r = tplBtn.getBoundingClientRect(); + tplMenu.style.left = Math.max(4, Math.min(r.left, window.innerWidth - tplMenu.offsetWidth - 4)) + "px"; + tplMenu.style.top = Math.min(r.bottom + 4, window.innerHeight - tplMenu.offsetHeight - 4) + "px"; + tplDismiss.arm(); + }); + bar.appendChild(hint); bar.appendChild(tokenSpan); bar.appendChild(outBtn); bar.appendChild(bgBtn); bar.appendChild(txtBtn); bar.appendChild(copyBtn); bar.appendChild(importBtn); bar.appendChild(tplBtn); bar.appendChild(clearBtn); + updateGrabBtn(); + + // Persistent global style-palette row + const styleBar = document.createElement("div"); + styleBar.className = "kjideo-bar"; + const styleLbl = document.createElement("span"); + styleLbl.textContent = "Style colors:"; + styleBar.appendChild(styleLbl); + + const canvasEl = document.createElement("canvas"); + canvasEl.className = "kjideo-canvas"; + canvasEl.tabIndex = 0; // focusable, so it can receive key events + canvasEl.title = "Drag to draw · Ctrl-drag force-draw over a box · click to select · shift-drag marquee-select · " + + "shift-click toggle · drag a group to move all · alt-click overlap · dbl-click edit · right-click region list · " + + "Del remove (all selected) · Ctrl/Cmd+C/V/D copy/paste/duplicate · H hide boxes (view)"; + const ctx = canvasEl.getContext("2d"); + addWheelPassthrough(wrap); // middle-click pan is wired on the whole dock (fl) in undock() + + const panel = document.createElement("div"); + panel.className = "kjideo-panel"; + node._panelH = node._panelH || 150; // height of the prompt panel (set by the splitter) + panel.style.height = node._panelH + "px"; + + // Canvas letterbox-fits into a flex container (cvBox), so node resize never aspect-locks the height. + const cvBox = document.createElement("div"); + cvBox.className = "kjideo-cv"; + cvBox.appendChild(canvasEl); + + // Draggable separator between the canvas and the prompt panel — drag up for more prompt, down for more canvas. + const splitter = document.createElement("div"); + splitter.className = "kjideo-split"; + splitter.title = "Drag to resize the description area"; + splitter.addEventListener("pointerdown", (e) => { + if (e.button !== 0) return; + e.preventDefault(); e.stopPropagation(); + const scale = (node.properties.dockPinned && app.canvas) ? (app.canvas.ds.scale || 1) : 1; + const sy = e.clientY, h0 = panel.offsetHeight; + dragPointer(e, splitter, (me) => { + const dy = (me.clientY - sy) / scale; + const h = Math.max(60, Math.min(h0 - dy, wrap.clientHeight - 110)); // up → bigger panel; keep room for the canvas + node._panelH = Math.round(h); panel.style.height = node._panelH + "px"; // persisted via the onSerialize blob + fitCanvas(); + }, flushChange); + }); + + wrap.appendChild(bar); wrap.appendChild(styleBar); wrap.appendChild(cvBox); wrap.appendChild(splitter); wrap.appendChild(panel); + + // The editor is NOT a node widget — it lives in the floating dock (built in undock()), so the node + // is an ordinary widgets-only node that ComfyUI sizes/persists with zero custom height handling. + node.resizable = true; + + // The dock canvas measures 0 while hidden; re-letterbox it when the editor element re-enters view. + try { + node._visObserver = new IntersectionObserver((entries) => { + if (entries.some((en) => en.isIntersecting)) fitCanvas(); + }); + node._visObserver.observe(wrap); + } catch (e) {} + + // Letterbox-fit the canvas (keeping width/height aspect) into its flex container cvBox. + function fitCanvas() { + if (wrap.offsetParent === null) return; // hidden (collapsed tab / minimized) — measurements are 0 + const availW = cvBox.clientWidth, availH = cvBox.clientHeight; + if (availW < 4 || availH < 4) return; + const aspect = (wWidget?.value || 1) / (hWidget?.value || 1); + let cw = availW, ch = cw / aspect; + if (ch > availH) { ch = availH; cw = ch * aspect; } + canvasEl.style.width = Math.round(cw) + "px"; + canvasEl.style.height = Math.round(ch) + "px"; + drawCanvas(); + } + const fitFsCanvas = fitCanvas; // dock/fullscreen use the same fitter + function syncCanvasToDims() { fitCanvas(); } // aspect changed (width/height widgets) + function detachInto(container) { // move the editor element into a container (dock body / fullscreen) + node._wrapHome = wrap.parentNode; + container.appendChild(wrap); + node._fsInner = container; + } + function reattach() { // fullscreen exit → back into the floating dock + canvasEl.style.width = ""; canvasEl.style.height = ""; + const home = node._dockBody || node._wrapHome; + if (home) home.appendChild(wrap); + node._fsInner = node._dockBody || null; // stays detached, living in the dock + if (node.graph) node.graph.setDirtyCanvas(true, true); + requestAnimationFrame(fitCanvas); + } + + function onFsEsc(e) { if (e.key === "Escape") { e.preventDefault(); e.stopPropagation(); exitFs(); } } + function enterFs() { + if (node._fullscreen) return; + node._fullscreen = true; + const ov = document.createElement("div"); ov.className = "kjideo-fs"; + const inner = document.createElement("div"); inner.className = "kjideo-fs-inner"; + ov.appendChild(inner); + ov.addEventListener("mousedown", (e) => { if (e.target === ov) exitFs(); }); // backdrop closes + document.body.appendChild(ov); + node._fsOverlay = ov; + detachInto(inner); + const closeBtn = document.createElement("button"); // visible exit (⛶ in the dock header is hidden here) + closeBtn.className = "kjideo-btn kjideo-fs-close"; closeBtn.textContent = "✕"; closeBtn.title = "Close (Esc)"; + stopProp(closeBtn); closeBtn.addEventListener("click", exitFs); + ov.appendChild(closeBtn); // on the backdrop corner, clear of the editor + document.addEventListener("keydown", onFsEsc, true); + window.addEventListener("resize", fitFsCanvas); + requestAnimationFrame(fitFsCanvas); + } + function exitFs() { + if (!node._fullscreen) return; + node._fullscreen = false; + document.removeEventListener("keydown", onFsEsc, true); + window.removeEventListener("resize", fitFsCanvas); + node._fsOverlay?.remove(); node._fsOverlay = null; + reattach(); + } + + // ── floating / dockable editor panel ── + // Active-context position offset (set by applyDockTransform): dockParent (parent) or dockGraph (self). + // dockParent is created by applyDockTransform with the right default; fall back to dockGraph if absent. + function dockPos() { + return node._dockCtx === "parent" ? (node.properties.dockParent || node.properties.dockGraph) : node.properties.dockGraph; + } + // The node the dock is currently anchored to: the node itself, or the SubgraphNode at parent level. + function dockHostNode() { return node._dockHost || node; } + // Persist the panel geometry — screen-space rect when unpinned, graph-space size when pinned. + function saveDockGeom() { + const fl = node._dockEl; if (!fl) return; + if (node.properties.dockMin) return; // collapsed: don't persist the header-only size + // sub-min = transient (hidden / mid-reparent) → bogus, don't persist. (offsetParent is null for the fixed dock.) + if (fl.offsetWidth < DOCK_MINW || fl.offsetHeight < DOCK_MINH) return; + if (node.properties.dockPinned) { + node.properties.dockGraph = node.properties.dockGraph || { x: 0, y: 0 }; + node.properties.dockGraph.w = fl.offsetWidth; node.properties.dockGraph.h = fl.offsetHeight; + } else { + const r = fl.getBoundingClientRect(); + node.properties.dockRect = { x: Math.round(r.left), y: Math.round(r.top), w: Math.round(r.width), h: Math.round(r.height) }; + } + } + // Nudge ComfyUI's change tracker (it snapshots on mouseup, which our preventDefault'd drags suppress). + function flushChange() { try { window.dispatchEvent(new MouseEvent("mouseup")); } catch (e) {} } + function setPinned(on, pinBtn) { + const fl = node._dockEl, c = app.canvas; if (!fl || !c) return; + const rect = c.canvas.getBoundingClientRect(), ds = c.ds; + if (on) { // return to where it was pinned (the saved dockGraph) + let g = node.properties.dockGraph; + if (!g) { // never pinned before — derive from the current float position + const r = fl.getBoundingClientRect(); + const gx = (r.left - rect.left) / ds.scale - ds.offset[0], gy = (r.top - rect.top) / ds.scale - ds.offset[1]; + g = node.properties.dockGraph = { + x: gx - (node.pos?.[0] ?? 0), y: gy - (node.pos?.[1] ?? 0), + w: Math.round(fl.offsetWidth / ds.scale), h: Math.round(fl.offsetHeight / ds.scale), + }; + } + node.properties.dockPinned = true; + fl.style.left = ""; fl.style.top = ""; + fl.style.width = g.w + "px"; fl.style.height = g.h + "px"; + node._dockSig = ""; node._dockHostEl = null; node._dockHostId = null; // force a fresh place + re-parent + pinnedDocks.add(node); startDockLoop(); + applyDockTransform(node); // place this frame (avoid a flash at the old float spot) + } else { // freeze current visual rect as a body-fixed screen panel + const r = fl.getBoundingClientRect(); + node.properties.dockPinned = false; + pinnedDocks.delete(node); + node._dockSig = ""; node._dockHostEl = null; node._dockHostId = null; + document.body.appendChild(fl); // back out of the node element + fl.style.position = "fixed"; fl.style.transform = ""; fl.style.transformOrigin = ""; fl.style.zIndex = ""; + fl.style.left = Math.round(r.left) + "px"; fl.style.top = Math.round(r.top) + "px"; + fl.style.width = Math.round(r.width) + "px"; fl.style.height = Math.round(r.height) + "px"; + node.properties.dockRect = { x: Math.round(r.left), y: Math.round(r.top), w: Math.round(r.width), h: Math.round(r.height) }; + } + if (pinBtn) { pinBtn.classList.toggle("active", on); pinBtn.title = on ? "Unpin from canvas (float in screen)" : "Pin to canvas (move/zoom with the graph)"; } + c.setDirtyCanvas(true, true); requestAnimationFrame(fitFsCanvas); + flushChange(); // click's mouseup already fired before this handler — snapshot the new state + } + // Resize from any edge/corner; N/W edges shift the anchor. Units: graph px pinned (÷scale), else screen px. + function startDockResize(e, dir, fl) { + if (e.button !== 0 || node.flags?.pinned) return; // node pinned → not resizable, like the node + e.preventDefault(); e.stopPropagation(); + const pinned = !!node.properties.dockPinned; + const scale = (pinned && app.canvas) ? (app.canvas.ds.scale || 1) : 1; + const sx = e.clientX, sy = e.clientY, w0 = fl.offsetWidth, h0 = fl.offsetHeight; + const startPos = dockPos(); // active context: dockGraph (self) or dockParent + const gx0 = pinned ? startPos.x : (parseFloat(fl.style.left) || 0); + const gy0 = pinned ? startPos.y : (parseFloat(fl.style.top) || 0); + const MINW = DOCK_MINW, MINH = DOCK_MINH; + dragPointer(e, e.currentTarget, (me) => { + const dx = (me.clientX - sx) / scale, dy = (me.clientY - sy) / scale; + let w = w0, h = h0, gx = gx0, gy = gy0; + if (dir.indexOf("e") >= 0) w = w0 + dx; + if (dir.indexOf("s") >= 0) h = h0 + dy; + if (dir.indexOf("w") >= 0) { w = w0 - dx; gx = gx0 + dx; } + if (dir.indexOf("n") >= 0) { h = h0 - dy; gy = gy0 + dy; } + if (w < MINW) { if (dir.indexOf("w") >= 0) gx -= (MINW - w); w = MINW; } + if (h < MINH) { if (dir.indexOf("n") >= 0) gy -= (MINH - h); h = MINH; } + fl.style.width = Math.round(w) + "px"; fl.style.height = Math.round(h) + "px"; + if (pinned) { const p = dockPos(); p.x = gx; p.y = gy; node._dockSig = ""; applyDockTransform(node); } + else { fl.style.left = Math.round(gx) + "px"; fl.style.top = Math.round(gy) + "px"; } + fitFsCanvas(); + }, () => { saveDockGeom(); flushChange(); }); + } + function addDockResizeHandles(fl) { + for (const dir of ["n", "s", "e", "w", "ne", "nw", "se", "sw"]) { + const h = document.createElement("div"); + h.className = "kjideo-rsz " + dir; + h.addEventListener("pointerdown", (e) => startDockResize(e, dir, fl)); + fl.appendChild(h); + } + } + function undock() { + if (node._docked) return; + if (node._fullscreen) exitFs(); + node._docked = true; node.properties.docked = true; + const fl = document.createElement("div"); fl.className = "kjideo-dock"; + fl.dataset.captureWheel = "true"; // 2.0: let focused inputs in the dock scroll instead of zooming the graph + stopProp(fl); // hosted inside the node element — don't let dock interactions drag/zoom the node + addMiddleClickPan(fl); // middle-click pans the graph from anywhere in the dock (bubbles up from inner elements) + fl.addEventListener("mousedown", (e) => { if (e.button === 1) e.stopPropagation(); }); // ...but don't let it reach the node behind + const head = document.createElement("div"); head.className = "kjideo-dock-head"; + const title = document.createElement("span"); title.textContent = "Ideogram 4 editor"; title.style.flex = "1"; + const minBtn = document.createElement("button"); minBtn.className = "kjideo-btn"; + stopProp(minBtn); + const applyMin = (on) => { // collapse the editor to just the header bar + node.properties.dockMin = !!on; + fl.classList.toggle("minimized", !!on); + minBtn.textContent = on ? "▢" : "—"; + minBtn.title = on ? "Restore editor" : "Minimize editor"; + if (!on) requestAnimationFrame(fitFsCanvas); // re-letterbox the canvas after it's visible again + }; + minBtn.addEventListener("click", () => { applyMin(!node.properties.dockMin); flushChange(); }); + const fsBtn = document.createElement("button"); fsBtn.className = "kjideo-btn"; fsBtn.textContent = "⛶"; + fsBtn.title = "Open in a larger window (Esc to close)"; + stopProp(fsBtn); fsBtn.addEventListener("click", () => node._fullscreen ? exitFs() : enterFs()); + const pinBtn = document.createElement("button"); pinBtn.className = "kjideo-btn"; pinBtn.textContent = "📌"; + stopProp(pinBtn); pinBtn.addEventListener("click", () => setPinned(!node.properties.dockPinned, pinBtn)); + // Opt-in: also show this canvas at the parent (subgraph) level (tickDocks hides this button at root). + const exBtn = document.createElement("button"); exBtn.className = "kjideo-btn"; exBtn.textContent = "⤴"; + exBtn.style.display = (node.graph && !node.graph.isRootGraph) ? "" : "none"; + const syncExBtn = () => { + exBtn.classList.toggle("active", !!node.properties.exposeToParent); + exBtn.title = (node.properties.exposeToParent ? "Hide" : "Show") + " this canvas at the parent (subgraph) level"; + }; + stopProp(exBtn); + exBtn.addEventListener("click", () => { node.properties.exposeToParent = !node.properties.exposeToParent; syncExBtn(); node._dockSig = ""; wakeDocks(); flushChange(); }); + syncExBtn(); + node._syncExBtn = syncExBtn; node._exBtn = exBtn; // onConfigure refreshes state; tickDocks toggles visibility + head.append(title, fsBtn, minBtn, exBtn, pinBtn); + const body = document.createElement("div"); body.className = "kjideo-dock-body"; + fl.append(head, body); + addDockResizeHandles(fl); + document.body.appendChild(fl); + node._dockEl = fl; node._dockBody = body; node._dockSig = ""; node._dockHostEl = null; node._dockHostId = null; liveDocks.add(node); + applyDockTheme(node); // match the node's color theme right away + detachInto(body); + // initial geometry / mode + const pinned = !!node.properties.dockPinned; + if (pinned) { + const g = node.properties.dockGraph || { x: 0, y: 0, w: 540, h: 470 }; + fl.style.position = "absolute"; fl.style.width = g.w + "px"; fl.style.height = g.h + "px"; + pinnedDocks.add(node); startDockLoop(); + applyDockTransform(node); // parent + place this frame (avoid a flash at 0,0) + pinBtn.classList.add("active"); + } else { + const rc = node.properties.dockRect || { x: 90, y: 90, w: 540, h: 470 }; + fl.style.left = Math.max(0, Math.min(rc.x, window.innerWidth - 80)) + "px"; + fl.style.top = Math.max(0, Math.min(rc.y, window.innerHeight - 40)) + "px"; + fl.style.width = rc.w + "px"; fl.style.height = rc.h + "px"; + } + pinBtn.title = pinned ? "Unpin from canvas (float in screen)" : "Pin to canvas (move/zoom with the graph)"; + applyMin(!!node.properties.dockMin); // restore minimized state + // drag the panel by its header (graph-space when pinned, screen-space otherwise) + head.addEventListener("pointerdown", (e) => { + if (e.target === pinBtn || e.target === minBtn || e.target === fsBtn || e.button !== 0) return; + if (node.flags?.pinned) return; // node pinned → dock is locked too + e.preventDefault(); + const sx0 = e.clientX, sy0 = e.clientY; + const pinned = node.properties.dockPinned; + const scale = pinned ? (app.canvas.ds.scale || 1) : 1; + const dp0 = dockPos(); // active context: dockGraph (self) or dockParent + const gx0 = pinned ? dp0.x : 0, gy0 = pinned ? dp0.y : 0; + const ox = fl.offsetLeft, oy = fl.offsetTop; + let snapReady = false; + dragPointer(e, head, (me) => { + if (pinned) { // move the panel by writing its transform directly (no full redraw) + const gx = gx0 + (me.clientX - sx0) / scale, gy = gy0 + (me.clientY - sy0) / scale; + const h = dockHostNode(); // snap "home": flush under the anchor (node or SubgraphNode) + const snap = 26 / scale, underY = h.size[1] + 2; + snapReady = Math.abs(gx) < snap && Math.abs(gy - underY) < snap; // just highlight; snap on release + fl.classList.toggle("snap-ready", snapReady); + const p = dockPos(); p.x = gx; p.y = gy; // follow the cursor freely + applyDockTransform(node); + } else { + fl.style.left = Math.max(0, Math.min(ox + me.clientX - sx0, window.innerWidth - 60)) + "px"; + fl.style.top = Math.max(0, Math.min(oy + me.clientY - sy0, window.innerHeight - 30)) + "px"; + } + }, () => { + if (snapReady) { // commit the snap: flush under the anchor, matching its width + const h = dockHostNode(), w = Math.round(h.size[0]), p = dockPos(); + p.x = 0; p.y = h.size[1] + 2; + if (node.properties.dockGraph.w !== w) { node.properties.dockGraph.w = w; fl.style.width = w + "px"; } + node._dockSig = ""; applyDockTransform(node); + } + fl.classList.remove("snap-ready"); + saveDockGeom(); flushChange(); + }); + }); + // Re-letterbox the canvas and persist geometry on dock resize. The sub-min guard in + // saveDockGeom drops the bogus transient reads from Vue re-parenting. + try { node._dockRO = new ResizeObserver(() => { fitFsCanvas(); saveDockGeom(); }); node._dockRO.observe(fl); } catch (e) {} + requestAnimationFrame(fitFsCanvas); // letterbox the canvas once the dock has its size + } + // Pin the editor under the node. `fresh` (new node only) matches the node width; reloads honor the blob. + function ensureDocked(fresh) { + if (node._docked) return; + if (node.properties.dockPinned == null) node.properties.dockPinned = true; + const honor = !!node.properties.dockGraph && node._dockGeomRestored; // only trust blob-restored geometry + if (!honor) { + // old workflow: reuse its saved (aspect-locked) node height so the canvas isn't tiny + const sh = (!fresh && node._savedSize && node._savedSize[1] > 240) ? Math.round(node._savedSize[1]) : 470; + node.properties.dockGraph = { x: 0, y: 0, w: Math.round(node.size[0]), h: sh }; // match the node width + } + undock(); + if (!honor) requestAnimationFrame(() => { // no reliable geometry — place a default under the node, matching its width + const g = node.properties.dockGraph; + g.x = 0; g.y = node.size[1] + 2; g.w = Math.round(node.size[0]); + if (node._dockEl) node._dockEl.style.width = g.w + "px"; + node._dockSig = ""; applyDockTransform(node); saveDockGeom(); + }); + } + + // ── geometry helpers ── (logical CSS px = the displayed canvas size) + function logW() { return canvasEl.offsetWidth || 1; } + function logH() { return canvasEl.offsetHeight || 1; } + function toPx(b) { + const W = logW(), H = logH(); + return { x1: b.x * W, y1: b.y * H, x2: (b.x + b.w) * W, y2: (b.y + b.h) * H }; + } + function mouseN(e) { + const r = canvasEl.getBoundingClientRect(); + return { x: (e.clientX - r.left) / r.width, y: (e.clientY - r.top) / r.height }; + } + function clamp01(v) { return Math.max(0, Math.min(1, v)); } + // greedy word-wrap to maxW px (ctx.font must be set by caller) + function wrapLines(text, maxW) { + const lines = []; + for (const para of text.split("\n")) { + let line = ""; + for (const word of para.split(/\s+/)) { + if (!word) continue; + const test = line ? line + " " + word : word; + if (line && ctx.measureText(test).width > maxW) { lines.push(line); line = word; } + else line = test; + } + lines.push(line); + } + return lines; + } + function normalizeBox(b) { + // collapse negative size to positive top-left + w/h, clamp into canvas + let x = b.x, y = b.y, w = b.w, h = b.h; + if (w < 0) { x += w; w = -w; } + if (h < 0) { y += h; h = -h; } + x = clamp01(x); y = clamp01(y); + w = Math.min(w, 1 - x); h = Math.min(h, 1 - y); + return { ...b, x, y, w: Math.max(0, w), h: Math.max(0, h) }; + } + + // All boxes under the point, top-first to match draw order: the active box is + // drawn last (on top), then the rest by index low→high (index 0 = front). + function boxesAt(mN) { + const baseRx = HANDLE / logW(), baseRy = HANDLE / logH(); + const res = []; + for (let i = 0; i < node._boxes.length; i++) { + const b = node._boxes[i]; + if (b.locked) continue; // locked regions aren't grabbable on the canvas + const rx = Math.min(baseRx, b.w / 3), ry = Math.min(baseRy, b.h / 3); // shrink handles on small boxes so a central move zone remains + const mode = rectHitTestN(mN.x, mN.y, b.x, b.y, b.x + b.w, b.y + b.h, rx, ry); + if (mode) res.push({ index: i, mode }); + } + const ai = res.findIndex((c) => c.index === node._activeIdx); + if (ai > 0) res.unshift(res.splice(ai, 1)[0]); + return res; + } + // Hover / right-click: prefer a resize handle on the active box, else topmost. + function hitTest(mN) { + const cands = boxesAt(mN); + if (!cands.length) return null; + return cands.find((c) => c.index === node._activeIdx && c.mode !== "move") || cands[0]; + } + // Tag-chip rects (canvas px), placed to avoid overlapping each other: each + // box's tag tries top-left, top-right, bottom-right, bottom-left in turn. + function tagRects() { + ctx.font = "bold 11px monospace"; + const W = logW(), H = logH(), h = 14; + const placed = [], rects = []; + const hits = (a, b) => a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y; + for (let i = 0; i < node._boxes.length; i++) { + const b = node._boxes[i]; + const x1 = b.x * W, y1 = b.y * H, x2 = (b.x + b.w) * W, y2 = (b.y + b.h) * H; + const tag = String(i + 1).padStart(2, "0"); + const w = ctx.measureText(tag).width + 8; + // Box too small to hold the badge → place it just outside (above/below/beside) so it doesn't + // cover the box and the box stays grabbable; otherwise tuck it into a free corner inside. + const small = (x2 - x1) < w || (y2 - y1) < h; + const cands = small + ? [[x1, y1 - h - 1], [x1, y2 + 1], [x2 + 1, y1], [x1 - w - 1, y1]] + : [[x1, y1], [x2 - w, y1], [x2 - w, y2 - h], [x1, y2 - h]]; + let pick = cands[0]; + for (const [cx, cy] of cands) { + if (cx < 0 || cy < 0 || cx + w > W || cy + h > H) continue; // keep on-canvas + if (!placed.some((p) => hits({ x: cx, y: cy, w, h }, p))) { pick = [cx, cy]; break; } + } + const r = { x: pick[0], y: pick[1], w, h, tag }; + placed.push(r); rects[i] = r; + } + return rects; + } + function titleAt(mN) { + const px = mN.x * logW(), py = mN.y * logH(); + const rects = tagRects(); + for (let i = node._boxes.length - 1; i >= 0; i--) { + if (node._boxes[i].locked) continue; // locked badges aren't interactive + const r = rects[i]; + if (r && px >= r.x && px <= r.x + r.w && py >= r.y && py <= r.y + r.h) return i; + } + return null; + } + // In-box text placement: when auto-place is on, put each box's text in whichever vertical slot overlaps + // least with the text already placed for other boxes (greedy), so overlapping labels don't stack. + function textBlocks() { + if (node.properties.showBoxText === false) return []; + const { fs, lh } = txtFont(); + ctx.font = fs + "px monospace"; + const W = logW(), H = logH(), pad = 4, tagH = 14, auto = node.properties.textAutoPlace !== false; + const placed = [], blocks = []; + const overlap = (a, b) => Math.max(0, Math.min(a.x + a.w, b.x + b.w) - Math.max(a.x, b.x)) * + Math.max(0, Math.min(a.y + a.h, b.y + b.h) - Math.max(a.y, b.y)); + for (let i = 0; i < node._boxes.length; i++) { + const b = node._boxes[i]; + let body = b.desc || ""; + if (b.type === "text" && b.text) body = `"${b.text}"` + (body ? " — " + body : ""); + if (!body) { blocks[i] = null; continue; } + const x1 = b.x * W, y1 = b.y * H, x2 = (b.x + b.w) * W, y2 = (b.y + b.h) * H; + const lines = wrapLines(body, (x2 - x1) - pad * 2); // full width + let lw = 0; for (const ln of lines) lw = Math.max(lw, ctx.measureText(ln).width); + const bw = Math.min(x2 - x1, lw + pad * 2), bh = Math.min(y2 - y1, lines.length * lh + 4); + // candidates = several vertical slots (more when the box is taller); text spans the width, so only Y varies + const top = y1 + ((y2 - y1) > bh + tagH ? tagH : 0), bottom = y2 - bh; + const cands = []; + if (!auto || bottom <= top) cands.push([x1, top]); // off, or fills the box → fixed top + else { + const n = Math.min(6, 2 + Math.floor((bottom - top) / (lh * 2))); + for (let k = 0; k < n; k++) cands.push([x1, top + ((bottom - top) * k) / (n - 1)]); + } + let best = cands[0], bestScore = Infinity; + for (const [cx, cy] of cands) { + const rect = { x: cx, y: cy, w: bw, h: bh }; + let s = 0; for (const p of placed) s += overlap(rect, p); + if (s < bestScore) { bestScore = s; best = [cx, cy]; if (s === 0) break; } + } + const r = { x: best[0], y: best[1], w: bw, h: bh, lines, lh, fs, pad }; + placed.push(r); blocks[i] = r; + } + return blocks; + } + // Click selection: active box's resize handle wins (corner resize); then a + // title-chip click selects that box (drawn to front); Alt-click cycles the + // overlap stack; else the topmost box. + function pickForSelection(mN, cycle) { + const cands = boxesAt(mN); + if (!cands.length) return null; + const ah = cands.find((c) => c.index === node._activeIdx && c.mode !== "move"); + if (ah && !cycle) return ah; + const ti = titleAt(mN); + if (ti !== null && !cycle) return { index: ti, mode: "move" }; + if (cycle && cands.length > 1) { + // cycle over a stable order (boxesAt moves the active box to the front, which would otherwise ping-pong 2) + const ordered = [...cands].sort((a, b) => a.index - b.index); + const pos = ordered.findIndex((c) => c.index === node._activeIdx); + return ordered[(pos + 1) % ordered.length]; + } + return cands.find((c) => c.index === node._activeIdx && c.mode !== "move") || cands[0]; + } + // normalized variant of rectHitTest with separate x/y radii + function rectHitTestN(mx, my, x1, y1, x2, y2, rx, ry) { + const h = (cx, cy) => Math.abs(mx - cx) < rx && Math.abs(my - cy) < ry; + if (h(x1, y1)) return "resize-tl"; + if (h(x2, y1)) return "resize-tr"; + if (h(x1, y2)) return "resize-bl"; + if (h(x2, y2)) return "resize-br"; + if (mx >= x1 && mx <= x2 && Math.abs(my - y1) < ry) return "resize-t"; + if (mx >= x1 && mx <= x2 && Math.abs(my - y2) < ry) return "resize-b"; + if (my >= y1 && my <= y2 && Math.abs(mx - x1) < rx) return "resize-l"; + if (my >= y1 && my <= y2 && Math.abs(mx - x2) < rx) return "resize-r"; + if (mx >= x1 && mx <= x2 && my >= y1 && my <= y2) return "move"; + return null; + } + + function applyDrag(mode, start, dN) { + const { x, y, w, h } = start; + const dx = dN.x, dy = dN.y; + if (mode === "move") { + return { ...start, x: clamp01(Math.min(x + dx, 1 - w)), y: clamp01(Math.min(y + dy, 1 - h)) }; + } + if (mode === "draw") { + const ax = clamp01(x), ay = clamp01(y); // fixed corner (mousedown point) + const cx = clamp01(x + dx), cy = clamp01(y + dy); // moving corner (cursor), clamped to canvas + return { ...start, x: Math.min(ax, cx), y: Math.min(ay, cy), w: Math.abs(cx - ax), h: Math.abs(cy - ay) }; + } + // resize: move only the dragged edges (clamped to the canvas); the others stay anchored. + const suf = mode.slice(7); // "tl"|"tr"|"bl"|"br"|"t"|"b"|"l"|"r" + let l = x, t = y, r = x + w, b = y + h; + if (suf.includes("l")) l = clamp01(l + dx); + if (suf.includes("r")) r = clamp01(r + dx); + if (suf.includes("t")) t = clamp01(t + dy); + if (suf.includes("b")) b = clamp01(b + dy); + if (r < l) [l, r] = [r, l]; // crossing the opposite edge flips, no anchor drift + if (b < t) [t, b] = [b, t]; + return { ...start, x: l, y: t, w: r - l, h: b - t }; + } + + // ── grid / guides ── + function gridN() { return Math.max(2, Math.min(128, node.properties.gridSize || 10)); } + function txtFont() { const fs = Math.max(6, Math.min(40, node.properties.textSize || 12)); return { fs, lh: Math.round(fs * 1.2) }; } + // Whole number of cells per axis so they fill the canvas exactly (no split cells at the edges); + // near-square (the target size is rounded to fit). Shared by the grid guide and snap-to-grid. + function gridStep() { + const W = logW(), H = logH(), target = Math.min(W, H) / gridN(); + const nx = Math.max(1, Math.round(W / target)), ny = Math.max(1, Math.round(H / target)); + return { nx, ny, cw: W / nx, ch: H / ny, sx: 1 / nx, sy: 1 / ny }; + } + function guideStroke(a) { + const c = hexRgb(node.properties.guideColor || "#ffffff") || { r: 255, g: 255, b: 255 }; + const op = (node.properties.guideOpacity == null ? 100 : node.properties.guideOpacity) / 100; + return `rgba(${c.r},${c.g},${c.b},${(a * op).toFixed(3)})`; + } + // Snap a box's free edges to the (square) grid when snap-to-grid is on. + function snapBox(b, mode) { + if (!node.properties.snap) return b; + const { sx, sy } = gridStep(); + const sn = (v, s) => Math.round(v / s) * s; + const { x, y, w, h } = b; + if (mode === "move") { // snap position, preserve size + return { ...b, x: clamp01(Math.min(sn(x, sx), 1 - w)), y: clamp01(Math.min(sn(y, sy), 1 - h)) }; + } + const x2 = sn(x + w, sx), y2 = sn(y + h, sy); // draw/resize: snap the edges + return normalizeBox({ ...b, x: sn(x, sx), y: sn(y, sy), w: x2 - sn(x, sx), h: y2 - sn(y, sy) }); + } + + // ── drawing ── + // Golden spiral: largest φ-rectangle fitting the canvas, subdivided into squares with quarter arcs. + function goldenSpiral(W, H) { + const phi = 1.6180339887; + let w, h; + if (W >= H) { if (W / H >= phi) { h = H; w = H * phi; } else { w = W; h = W / phi; } } + else { if (H / W >= phi) { w = W; h = W * phi; } else { h = H; w = H / phi; } } + let x = (W - w) / 2, y = (H - h) / 2; + ctx.save(); + ctx.strokeStyle = guideStroke(0.25); ctx.lineWidth = 1; + ctx.strokeRect(x + 0.5, y + 0.5, w, h); + ctx.strokeStyle = guideStroke(0.6); ctx.lineWidth = 1.5; + let phase = w >= h ? 0 : 1; + for (let i = 0; i < 12 && w > 1 && h > 1; i++, phase = (phase + 1) % 4) { + const s = Math.min(w, h); + let cx, cy, a0, a1; + if (phase === 0) { cx = x + s; cy = y + s; a0 = Math.PI; a1 = Math.PI * 1.5; x += s; w -= s; } + else if (phase === 1) { cx = x; cy = y + s; a0 = Math.PI * 1.5; a1 = Math.PI * 2; y += s; h -= s; } + else if (phase === 2) { cx = x + w - s; cy = y; a0 = 0; a1 = Math.PI * 0.5; w -= s; } + else { cx = x + w; cy = y + h - s; a0 = Math.PI * 0.5; a1 = Math.PI; h -= s; } + ctx.beginPath(); ctx.arc(cx, cy, s, a0, a1); ctx.stroke(); + } + ctx.restore(); + } + // Composition guide overlay (rule of thirds / grid / golden ratio / spiral), drawn on the bg. + function drawGuide(W, H) { + const kind = node.properties.guide; + if (!kind || kind === "none") return; + if (kind === "spiral") { goldenSpiral(W, H); return; } + ctx.save(); + ctx.lineWidth = 1; + if (kind === "grid") { // whole cells across both axes (no split cells at edges) + ctx.strokeStyle = guideStroke(0.24); + const { nx, ny, cw, ch } = gridStep(); + for (let i = 1; i < nx; i++) { const X = Math.round(i * cw) + 0.5; ctx.beginPath(); ctx.moveTo(X, 0); ctx.lineTo(X, H); ctx.stroke(); } + for (let j = 1; j < ny; j++) { const Y = Math.round(j * ch) + 0.5; ctx.beginPath(); ctx.moveTo(0, Y); ctx.lineTo(W, Y); ctx.stroke(); } + ctx.restore(); return; + } + const v = [], h = []; // thirds / golden: per-axis fractions (correct at any aspect) + if (kind === "thirds") { v.push(1 / 3, 2 / 3); h.push(1 / 3, 2 / 3); } + else if (kind === "golden") { const g = 1 / 1.6180339887; v.push(1 - g, g); h.push(1 - g, g); } + ctx.strokeStyle = guideStroke(kind === "golden" ? 0.45 : 0.28); + for (const fx of v) { const px = Math.round(fx * W) + 0.5; ctx.beginPath(); ctx.moveTo(px, 0); ctx.lineTo(px, H); ctx.stroke(); } + for (const fy of h) { const py = Math.round(fy * H) + 0.5; ctx.beginPath(); ctx.moveTo(0, py); ctx.lineTo(W, py); ctx.stroke(); } + ctx.restore(); + } + let _rafPending = false; + function drawCanvas() { + if (_rafPending) return; + _rafPending = true; + requestAnimationFrame(() => { + _rafPending = false; + _draw(); + }); + } + function _draw() { + // Size the backing store to display × DPR and draw in logical px (crisp text/lines). + const W = logW(), H = logH(), d = window.devicePixelRatio || 1; + const bw = Math.round(W * d), bh = Math.round(H * d); + if (canvasEl.width !== bw || canvasEl.height !== bh) { canvasEl.width = bw; canvasEl.height = bh; } + ctx.setTransform(d, 0, 0, d, 0, 0); + ctx.clearRect(0, 0, W, H); + let bri = bgBrightnessWidget ? bgBrightnessWidget.value : 25; + if (typeof bri !== "number" || isNaN(bri)) bri = 25; // guard against unset widget value + if (node._bgImg) { // reference image, dimmed by brightness + ctx.drawImage(node._bgImg, 0, 0, W, H); + const dim = 1 - bri / 100; + if (dim > 0) { ctx.fillStyle = `rgba(0,0,0,${dim})`; ctx.fillRect(0, 0, W, H); } + } else { // blank canvas grey from brightness + const g = Math.round(bri / 100 * 128); + ctx.fillStyle = `rgb(${g},${g},${g})`; ctx.fillRect(0, 0, W, H); + } + drawGuide(W, H); // composition guide overlay + if (node._hideBoxes) return; // H: temporary background-only view + // selection highlight only when the editor is focused or the node is selected + const showSel = node._focused || node._selected; + const aIdx = showSel ? node._activeIdx : -1; + const selSet = new Set(showSel ? node._selection : []); + if (aIdx >= 0) selSet.add(aIdx); + // index 0 = front (drawn last); selected drawn above non-selected, active last of all + const nonSel = node._boxes.map((_, i) => i).filter((i) => !selSet.has(i)).reverse(); + const selOthers = node._boxes.map((_, i) => i).filter((i) => selSet.has(i) && i !== aIdx).reverse(); + const order = [...nonSel, ...selOthers]; + if (aIdx >= 0 && aIdx < node._boxes.length) order.push(aIdx); + const tagR = tagRects(); // collision-avoided tag positions + const textB = textBlocks(); // collision-avoided in-box text positions + for (const i of order) { + const b = node._boxes[i], active = i === aIdx, selected = selSet.has(i) && !b.locked; // locked never shows as selected + const pal = (b.palette || []).filter(Boolean); + const col = pal.length ? pal[0] : "#8c8c8c"; // box color = first palette color, else neutral grey + const { x1, y1, x2, y2 } = toPx(b); + const w = x2 - x1, h = y2 - y1; + const hovered = (i === node._hoverBox && !b.locked) || selected; // locked boxes don't hover; selected stay highlighted + if (selected) { // opaque backing so contents read clearly over boxes behind + ctx.fillStyle = "rgba(26,26,26,0.88)"; + ctx.fillRect(x1, y1, w, h); + } + const baseA = (node.properties.boxOpacity == null ? 14 : node.properties.boxOpacity) / 100; + const fillA = Math.min(1, hovered ? baseA + 0.1 : baseA); // box-color tint at the chosen opacity + ctx.fillStyle = col + Math.round(fillA * 255).toString(16).padStart(2, "0"); + ctx.fillRect(x1, y1, w, h); + if (b.locked) ctx.setLineDash([3, 3]); // locked: frozen on the canvas + else if (b.nobbox) ctx.setLineDash([6, 4]); // unplaced (no bbox in source) + const lw = selected ? 2 : (hovered ? 1.5 : 1); + ctx.strokeStyle = col; ctx.lineWidth = lw; + ctx.strokeRect(x1 + lw / 2, y1 + lw / 2, w - lw, h - lw); // inside the box so strip/badge align at y1 + ctx.setLineDash([]); + if (pal.length) { // palette shown as a strip along the top edge + const sw = w / pal.length, n = pal.length, sh = 7; + for (let p = 0; p < n; p++) { + const sx = x1 + Math.round(p * sw); + ctx.fillStyle = pal[p]; + ctx.fillRect(sx, y1, x1 + Math.round((p + 1) * sw) - sx, sh); + } + } + // in-box content (clipped to the box): prompt text (at its de-conflicted corner) + lock badge + ctx.save(); + ctx.beginPath(); ctx.rect(x1, y1, w, h); ctx.clip(); + const tb = textB[i]; + if (tb) { + const stroke = node.properties.textStroke !== false; + ctx.font = tb.fs + "px monospace"; + if (stroke) { ctx.lineWidth = 3; ctx.lineJoin = "round"; ctx.strokeStyle = "rgba(0,0,0,0.85)"; } // dark halo + ctx.fillStyle = readableText(col); // box color, lightened if too dark + let ty = tb.y + tb.fs; + for (const line of tb.lines) { + if (stroke) ctx.strokeText(line, tb.x + tb.pad, ty); + ctx.fillText(line, tb.x + tb.pad, ty); + ty += tb.lh; + } + } + ctx.restore(); + // tag chip on top, unclipped (sits outside boxes too small to hold it — its collision-avoided position) + const tr = tagR[i]; + ctx.font = "bold 11px monospace"; + ctx.fillStyle = col; // tag chip = box color + ctx.fillRect(tr.x, tr.y, tr.w, 14); + if (i === node._hoverTitle && !b.locked) { // hover highlight (never on locked) + ctx.fillStyle = "rgba(255,255,255,0.25)"; ctx.fillRect(tr.x, tr.y, tr.w, 14); + ctx.strokeStyle = "#fff"; ctx.lineWidth = 1; ctx.strokeRect(tr.x + 0.5, tr.y + 0.5, tr.w - 1, 13); + } + ctx.fillStyle = textOn(col); + ctx.fillText(tr.tag, tr.x + 4, tr.y + 11); + if (b.locked) { ctx.font = "11px sans-serif"; ctx.fillStyle = "#ddd"; ctx.fillText("🔒", tr.x + tr.w + 2, tr.y + 11); } // lock symbol next to the badge + if (selected) { // orange selection ring on top (above strip/tag): solid = primary, dashed = others + const olw = active ? 2 : 1; + ctx.strokeStyle = "#ff8c00"; ctx.lineWidth = olw; + if (!active) ctx.setLineDash([5, 3]); + ctx.strokeRect(x1 + olw / 2, y1 + olw / 2, w - olw, h - olw); + ctx.setLineDash([]); + } + } + const apIdx = node._altPreview ?? -1; // Alt-held: ring the box an alt-click would select next + if (apIdx >= 0 && apIdx < node._boxes.length) { + const { x1, y1, x2, y2 } = toPx(node._boxes[apIdx]); + ctx.strokeStyle = "#46b4e6"; ctx.lineWidth = 2; ctx.setLineDash([5, 3]); + ctx.strokeRect(x1 + 1, y1 + 1, (x2 - x1) - 2, (y2 - y1) - 2); + ctx.setLineDash([]); + } + if (node._marquee && node._marqueeActive) { // rubber-band selection rectangle + const r = marqueeRect(); + const mx = r.x * W, my = r.y * H, mw = r.w * W, mh = r.h * H; + ctx.fillStyle = "rgba(70,180,230,0.12)"; + ctx.fillRect(mx, my, mw, mh); + ctx.strokeStyle = "#46b4e6"; ctx.lineWidth = 1; ctx.setLineDash([4, 3]); + ctx.strokeRect(mx + 0.5, my + 0.5, mw - 1, mh - 1); + ctx.setLineDash([]); + } + } + + // ── serialization ── + // Wired AND the source node isn't muted(2)/bypassed(4) — those keep the link but emit nothing. + const importConnected = () => { + const link = node.graph?.links?.[node.inputs?.find((i) => i.name === "import_json")?.link]; + return !!link && ![2, 4].includes(node.graph.getNodeById(link.origin_id)?.mode); + }; + function serialize() { // saved/restored value: clean boxes + if (elementsWidget) elementsWidget.value = node._boxes.length ? JSON.stringify(node._boxes) : ""; + if (stylePaletteWidget) stylePaletteWidget.value = node._stylePalette.length ? JSON.stringify(node._stylePalette) : ""; + } + // Queue-time value (not the saved value): when a wired import should drive the output — "always" + // mode, or empty in "when empty" mode — return a unique marker so ComfyUI can't cache-skip the + // node. It then re-executes and pushes the import back via ui, refreshing the editor. The server + // treats a non-list elements_data as empty (and ignores it entirely in "always" mode), so the + // nonce never affects the output. + if (elementsWidget) { + elementsWidget.serializeValue = () => { + const always = findW("import_mode")?.value === "always"; + if (importConnected() && (always || !node._boxes.length)) { + return JSON.stringify({ _refresh: (node._serialSeq = (node._serialSeq || 0) + 1) }); + } + return node._boxes.length ? JSON.stringify(node._boxes) : ""; + }; + } + + function commit() { serialize(); renderPanel(); drawCanvas(); updateTokens(); flushChange(); } // flush so canvas edits persist without a defocus + // Live text edit: persist + repaint + token count, without rebuilding the panel. + function touch() { serialize(); drawCanvas(); updateTokens(); } + + function removeBox(i) { + node._boxes.splice(i, 1); + node._selection = new Set(); + if (node._boxes.length === 0) node._activeIdx = -1; + else if (i <= node._activeIdx) node._activeIdx = Math.max(0, node._activeIdx - 1); + if (node._activeIdx >= 0) node._selection.add(node._activeIdx); + } + // Delete every selected box (or the active one) and reset the selection. + function removeSelected() { + const idxs = [...node._selection].sort((a, b) => b - a).filter((i) => !node._boxes[i]?.locked); // keep locked + if (!idxs.length && node._activeIdx >= 0 && !node._boxes[node._activeIdx]?.locked) idxs.push(node._activeIdx); + if (!idxs.length) return; + for (const i of idxs) node._boxes.splice(i, 1); + node._selection = new Set(); + node._activeIdx = node._boxes.length ? Math.min(idxs[idxs.length - 1], node._boxes.length - 1) : -1; + if (node._activeIdx >= 0) node._selection.add(node._activeIdx); + } + // Replace the selection with a single box (used by clicks / programmatic selects). + function selectOnly(idx) { + node._activeIdx = idx; + node._selection = idx >= 0 ? new Set([idx]) : new Set(); + } + + // ── pointer interaction ── + canvasEl.addEventListener("pointerdown", (e) => { + if (node._placing) { // drop the duplicate being placed + if (e.button === 0) { placeFollower(mouseN(e)); finishPlacing(); } + else cancelPlacing(); + e.preventDefault(); e.stopPropagation(); + return; + } + if (e.button !== 0) return; + if (node._hideBoxes) return; // view-only while boxes are hidden (H) + canvasEl.focus(); // so Delete/Backspace targets this editor + // capture the pointer so move/up keep coming even when the cursor leaves the node + try { canvasEl.setPointerCapture(e.pointerId); } catch (e2) {} + node._hoverTitle = null; node._hoverBox = null; // clear hover highlight while interacting + const mN = mouseN(e); + // Ctrl/Cmd forces drawing a new box even when starting over an existing one. + const hit = (e.ctrlKey || e.metaKey) ? null : pickForSelection(mN, e.altKey); + // Touch double-tap opens the inline editor (dblclick may not fire on touch). + if (e.pointerType && e.pointerType !== "mouse") { + const last = node._lastTap, now = e.timeStamp; + if (hit && last && now - last.t < 350 && Math.abs(mN.x - last.x) < 0.03 && Math.abs(mN.y - last.y) < 0.03) { + node._lastTap = null; openInlineEditor(hit.index); + e.preventDefault(); e.stopPropagation(); return; + } + node._lastTap = { t: now, x: mN.x, y: mN.y }; + } + node._pendingCollapse = -1; + node._groupStart = null; + if (e.shiftKey) { // shift-drag = marquee select; shift-click = toggle + startMarquee(mN, hit ? hit.index : -1); + e.preventDefault(); e.stopPropagation(); + return; + } + if (hit) { + if (!node._selection.has(hit.index)) node._selection = new Set([hit.index]); // outside selection → pick it + else if (node._selection.size > 1) node._pendingCollapse = hit.index; // click (no drag) collapses + node._activeIdx = hit.index; + node._dragMode = hit.mode; + node._boxAtStart = { ...node._boxes[hit.index] }; + if (node._selection.size > 1) { // snapshot the whole group for group move/resize + node._groupStart = {}; + for (const i of node._selection) node._groupStart[i] = { ...node._boxes[i] }; + } + } else { + node._dragMode = "draw"; + const nb = { x: mN.x, y: mN.y, w: 0, h: 0, type: "obj", text: "", desc: "", palette: [] }; + node._boxes.push(nb); + node._activeIdx = node._boxes.length - 1; + node._selection = new Set([node._activeIdx]); + node._boxAtStart = { ...nb }; + } + node._drawing = true; + node._dragStartN = mN; + canvasEl.addEventListener("pointermove", onMove); + canvasEl.addEventListener("pointerup", onUp); + canvasEl.addEventListener("pointercancel", onUp); // touch can cancel instead of up + e.preventDefault(); e.stopPropagation(); + drawCanvas(); // panel rebuild/resize deferred to onUp so the canvas doesn't shift mid-drag + }); + + // Hover targets: with Alt, preview what an alt-click would select next (the box under the current one). + function hoverTargets(mN, alt, force) { + if (alt && !force) { const pick = pickForSelection(mN, true); return { ti: null, hb: null, ap: pick ? pick.index : -1, hit: null }; } + const ti = force ? null : titleAt(mN); + const hit = force ? null : hitTest(mN); + return { ti, hb: ti != null ? ti : (hit ? hit.index : null), ap: -1, hit }; + } + function applyHover(t) { + if (t.ti !== node._hoverTitle || t.hb !== node._hoverBox || t.ap !== (node._altPreview ?? -1)) { + node._hoverTitle = t.ti; node._hoverBox = t.hb; node._altPreview = t.ap; drawCanvas(); + } + } + canvasEl.addEventListener("pointermove", (e) => { + node._lastMouseN = mouseN(e); // track cursor for paste-under-cursor + if (node._placing) { placeFollower(node._lastMouseN); return; } + if (node._drawing || node._marquee || node._hideBoxes) return; + const mN = node._lastMouseN; + const force = e.ctrlKey || e.metaKey, alt = e.altKey; // Ctrl/Cmd = force-draw, Alt = cycle preview + const t = hoverTargets(mN, alt, force); + applyHover(t); + canvasEl.style.cursor = (alt && !force) ? (t.ap >= 0 ? "pointer" : "crosshair") + : (t.ti != null ? "pointer" : (t.hit ? (cursorForBboxMode(t.hit.mode) || "crosshair") : "crosshair")); + }); + // Alt pressed/released without moving the mouse — refresh the preview from the last cursor position. + node._altRefresh = (altDown) => { + if (node._drawing || node._marquee || node._placing || node._hideBoxes || !node._lastMouseN) return; + applyHover(hoverTargets(node._lastMouseN, altDown, false)); + }; + canvasEl.addEventListener("pointerleave", () => { + if (hoveredCanvasNode === node) hoveredCanvasNode = null; + if (node._hoverTitle !== null || node._hoverBox !== null || (node._altPreview ?? -1) >= 0) { + node._hoverTitle = null; node._hoverBox = null; node._altPreview = -1; drawCanvas(); + } + }); + canvasEl.addEventListener("pointerenter", () => { hoveredCanvasNode = node; }); + // H (while hovering the canvas): temporary background-only view (not serialized). + node._toggleHideBoxes = () => { node._hideBoxes = !node._hideBoxes; drawCanvas(); }; + + // ── inline description editing (double-click a region) ── + let inlineTa = null; + function closeInlineEditor() { + if (inlineTa) { inlineTa.remove(); inlineTa = null; } + } + function openInlineEditor(idx) { + closeInlineEditor(); + const b = node._boxes[idx]; + if (!b) return; + selectOnly(idx); + const dw = canvasEl.offsetWidth, dh = canvasEl.offsetHeight; // CSS display size + const ox = canvasEl.offsetLeft, oy = canvasEl.offsetTop; + const w = Math.min(dw, Math.max(70, b.w * dw)); + const h = Math.min(dh, Math.max(42, b.h * dh)); + // clamp so the editor stays inside the canvas (wrapper is overflow:hidden) + const left = Math.max(ox, Math.min(ox + b.x * dw, ox + dw - w)); + const top = Math.max(oy, Math.min(oy + b.y * dh, oy + dh - h)); + const ta = document.createElement("textarea"); + ta.className = "kjideo-inline"; + ta.value = b.desc || ""; + ta.style.left = left + "px"; + ta.style.top = top + "px"; + ta.style.width = w + "px"; + ta.style.height = h + "px"; + ta.style.borderColor = (b.palette || []).find(Boolean) || "#46b4e6"; // first palette color, else accent + stopProp(ta); + wrap.appendChild(ta); + inlineTa = ta; + ta.focus(); ta.select(); + const orig = b.desc || ""; + let cancelled = false; + ta.addEventListener("input", () => { b.desc = ta.value; drawCanvas(); updateTokens(); }); + ta.addEventListener("keydown", (e) => { + e.stopPropagation(); + if (e.key === "Escape") { cancelled = true; b.desc = orig; ta.blur(); } + else if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) ta.blur(); + }); + ta.addEventListener("blur", () => { + if (!cancelled) b.desc = ta.value; + closeInlineEditor(); + commit(); + }); + } + canvasEl.addEventListener("dblclick", (e) => { + if (node._hideBoxes) return; + e.preventDefault(); e.stopPropagation(); + const cands = boxesAt(mouseN(e)); // edit the active box if it's under the cursor, else topmost + const target = cands.find((c) => c.index === node._activeIdx) || cands[0]; + if (target) openInlineEditor(target.index); + }); + + // Snapshot the current selection (or the active box) for the clipboard. + function copySelection() { + const idxs = node._selection.size ? [...node._selection].sort((a, b) => a - b) + : (node._activeIdx >= 0 ? [node._activeIdx] : []); + return idxs.map((i) => JSON.parse(JSON.stringify(node._boxes[i]))); + } + // Paste the clipboard regions as a group, centered under the cursor, keeping their layout. + function pasteBoxes() { + if (!copiedBoxes || !copiedBoxes.length) return; + const clones = copiedBoxes.map((b) => { const c = JSON.parse(JSON.stringify(b)); delete c.locked; return c; }); // pasted = editable + let minx = Infinity, miny = Infinity, maxx = -Infinity, maxy = -Infinity; + for (const b of clones) { + minx = Math.min(minx, b.x); miny = Math.min(miny, b.y); + maxx = Math.max(maxx, b.x + b.w); maxy = Math.max(maxy, b.y + b.h); + } + const gw = maxx - minx, gh = maxy - miny; + const m = node._lastMouseN; + let tx = m ? m.x - gw / 2 : minx + 0.03; // target group top-left + let ty = m ? m.y - gh / 2 : miny + 0.03; + tx = Math.max(0, Math.min(tx, 1 - gw)); // clamp group into the canvas + ty = Math.max(0, Math.min(ty, 1 - gh)); + const dx = tx - minx, dy = ty - miny; + const start = node._boxes.length; + for (const b of clones) { + b.x = clamp01(b.x + dx); b.y = clamp01(b.y + dy); + delete b.nobbox; // pasted boxes are placed + node._boxes.push(b); + } + node._selection = new Set(); + for (let i = start; i < node._boxes.length; i++) node._selection.add(i); + node._activeIdx = node._boxes.length - 1; + commit(); fitCanvas(); + } + // Keyboard: Delete removes; Ctrl/Cmd C/V/D copy/paste/duplicate the active region. + // Canvas must be focused; stop the event so LiteGraph doesn't act on the node. + canvasEl.addEventListener("keydown", (e) => { + if (node._placing) { + if (e.key === "Escape") { e.preventDefault(); e.stopPropagation(); cancelPlacing(); } + return; + } + if (node._drawing || node._hideBoxes) return; // view-only while boxes are hidden (H) + const ctrl = e.ctrlKey || e.metaKey; + if ((e.key === "Delete" || e.key === "Backspace") && node._activeIdx >= 0) { + e.preventDefault(); e.stopPropagation(); + removeSelected(); commit(); fitCanvas(); // removes all selected (or the active one) + } else if (ctrl && e.key === "c" && node._activeIdx >= 0) { + e.preventDefault(); e.stopPropagation(); + copiedBoxes = copySelection(); + } else if (ctrl && e.key === "v" && copiedBoxes) { + e.preventDefault(); e.stopPropagation(); + pasteBoxes(); + } else if (ctrl && e.key === "d" && node._activeIdx >= 0) { + e.preventDefault(); e.stopPropagation(); + copiedBoxes = copySelection(); + pasteBoxes(); + } + }); + + function onMove(e) { + if (!node._drawing) return; + const mN = mouseN(e); + const dN = { x: mN.x - node._dragStartN.x, y: mN.y - node._dragStartN.y }; + if (Math.abs(dN.x) + Math.abs(dN.y) > 0.001) node._pendingCollapse = -1; // it's a drag, not a click + if (node._dragMode === "move" && node._groupStart) { + let dx = dN.x, dy = dN.y; // clamp delta so the whole group stays in bounds + for (const i in node._groupStart) { + const s = node._groupStart[i]; + dx = Math.min(Math.max(dx, -s.x), 1 - s.w - s.x); + dy = Math.min(Math.max(dy, -s.y), 1 - s.h - s.y); + } + if (node.properties.snap) { // snap the group's movement to whole grid cells + const { sx, sy } = gridStep(); + dx = Math.round(dx / sx) * sx; + dy = Math.round(dy / sy) * sy; + } + for (const i in node._groupStart) { + const s = node._groupStart[i]; + node._boxes[i] = { ...s, x: s.x + dx, y: s.y + dy }; + delete node._boxes[i].nobbox; + } + drawCanvas(); updateBboxLabel(); + return; + } + if (node._groupStart && node._dragMode.startsWith("resize")) { + // Scale every selected box by the primary's resize, about the handle's fixed edge. + const a = node._boxAtStart, na = applyDrag(node._dragMode, a, dN); + const suf = node._dragMode.slice(7); // "tl"|"tr"|"bl"|"br"|"t"|"b"|"l"|"r" + const scaleX = (suf.includes("l") || suf.includes("r")) && a.w > 0 ? Math.max(0.02, na.w / a.w) : 1; + const scaleY = (suf.includes("t") || suf.includes("b")) && a.h > 0 ? Math.max(0.02, na.h / a.h) : 1; + const ax = suf.includes("l") ? a.x + a.w : a.x; // fixed (anchor) edges + const ay = suf.includes("t") ? a.y + a.h : a.y; + for (const i in node._groupStart) { + const s = node._groupStart[i]; + node._boxes[i] = normalizeBox({ + ...s, x: ax + (s.x - ax) * scaleX, y: ay + (s.y - ay) * scaleY, + w: s.w * scaleX, h: s.h * scaleY, + }); + delete node._boxes[i].nobbox; + } + drawCanvas(); updateBboxLabel(); + return; + } + const nb = snapBox(applyDrag(node._dragMode, node._boxAtStart, dN), node._dragMode); + delete nb.nobbox; // moving/resizing places the element (gives it a bbox) + node._boxes[node._activeIdx] = nb; + drawCanvas(); updateBboxLabel(); + } + function onUp() { + if (!node._drawing) return; + node._drawing = false; + canvasEl.removeEventListener("pointermove", onMove); + canvasEl.removeEventListener("pointerup", onUp); + canvasEl.removeEventListener("pointercancel", onUp); + // a click (no drag) on empty space drops the placeholder box and deselects everything + const b = node._boxes[node._activeIdx]; + if (b && (b.w < 0.005 || b.h < 0.005) && node._dragMode === "draw") { + node._boxes.splice(node._activeIdx, 1); + selectOnly(-1); + } else if (node._pendingCollapse >= 0) { // click (no drag) on a group member → keep only it + selectOnly(node._pendingCollapse); + } + node._pendingCollapse = -1; node._groupStart = null; + commit(); + } + + // ── marquee (rubber-band) selection: shift-drag ── + function marqueeRect() { + const m = node._marquee; + return { x: Math.min(m.x0, m.x), y: Math.min(m.y0, m.y), + w: Math.abs(m.x - m.x0), h: Math.abs(m.y - m.y0) }; + } + function rectsOverlap(r, b) { + return r.x < b.x + b.w && r.x + r.w > b.x && r.y < b.y + b.h && r.y + r.h > b.y; + } + function startMarquee(mN, startHit) { + node._marquee = { x0: mN.x, y0: mN.y, x: mN.x, y: mN.y }; + node._marqueeBase = new Set(node._selection); // additive: marquee unions with what's selected + node._marqueeStartHit = startHit; // for the shift-click (no drag) toggle fallback + node._marqueeActive = false; + canvasEl.focus(); + canvasEl.addEventListener("pointermove", onMarqueeMove); + canvasEl.addEventListener("pointerup", onMarqueeUp); + canvasEl.addEventListener("pointercancel", onMarqueeUp); + drawCanvas(); + } + function onMarqueeMove(e) { + if (!node._marquee) return; + const mN = mouseN(e); + node._marquee.x = mN.x; node._marquee.y = mN.y; + if (Math.abs(mN.x - node._marquee.x0) + Math.abs(mN.y - node._marquee.y0) > 0.01) node._marqueeActive = true; + if (node._marqueeActive) { + const r = marqueeRect(); + const sel = new Set(node._marqueeBase); + node._boxes.forEach((b, i) => { if (!b.locked && rectsOverlap(r, b)) sel.add(i); }); // marquee skips locked + node._selection = sel; + if (node._activeIdx < 0 || !sel.has(node._activeIdx)) node._activeIdx = sel.size ? [...sel][0] : node._activeIdx; + } + drawCanvas(); + } + function onMarqueeUp() { + canvasEl.removeEventListener("pointermove", onMarqueeMove); + canvasEl.removeEventListener("pointerup", onMarqueeUp); + canvasEl.removeEventListener("pointercancel", onMarqueeUp); + if (!node._marqueeActive && node._marqueeStartHit >= 0) { // shift-click on a box → toggle it + const idx = node._marqueeStartHit; + if (node._selection.has(idx) && node._selection.size > 1) { + node._selection.delete(idx); + if (node._activeIdx === idx) node._activeIdx = node._selection.values().next().value; + } else { + node._selection.add(idx); node._activeIdx = idx; + } + } + node._marquee = null; node._marqueeActive = false; + if (node._activeIdx >= 0 && !node._selection.has(node._activeIdx)) { + node._activeIdx = node._selection.size ? [...node._selection][0] : -1; + } + commit(); + } + + // ── duplicate placement: the new box follows the cursor until clicked ── + function placeFollower(mN) { + const b = node._boxes[node._activeIdx]; + if (!b) return; + b.x = clamp01(Math.min(mN.x - b.w / 2, 1 - b.w)); + b.y = clamp01(Math.min(mN.y - b.h / 2, 1 - b.h)); + delete b.nobbox; + drawCanvas(); updateBboxLabel(); + } + function startPlacing(srcIdx) { + const src = node._boxes[srcIdx]; + if (!src) return; + const nb = JSON.parse(JSON.stringify(src)); + delete nb.nobbox; delete nb.locked; // a duplicate is editable, not born locked + node._boxes.push(nb); + selectOnly(node._boxes.length - 1); + node._placing = true; + canvasEl.focus(); + canvasEl.style.cursor = "move"; + serialize(); renderPanel(); drawCanvas(); updateTokens(); + } + function finishPlacing() { + if (!node._placing) return; + node._placing = false; + canvasEl.style.cursor = "crosshair"; + commit(); fitCanvas(); + } + function cancelPlacing() { + if (!node._placing) return; + node._placing = false; + canvasEl.style.cursor = "crosshair"; + removeBox(node._activeIdx); + commit(); fitCanvas(); + } + + // ── right-click "layers" menu: list / select / delete / duplicate / reorder regions ── + function closeLayersMenu() { + if (node._layerMenu) { node._layerMenu.remove(); node._layerMenu = null; } + node._layerDismiss?.disarm(); node._layerDismiss = null; + } + function rowLabel(b) { + if (b.type === "text") { + const t = b.text ? `"${b.text}"` : ""; + return b.desc ? (t ? t + " — " + b.desc : b.desc) : t; + } + return b.desc || ""; + } + function openLayersMenu(clientX, clientY) { + closeLayersMenu(); + const menu = document.createElement("div"); + menu.className = "kjideo-menu"; + const hdr = document.createElement("div"); + hdr.className = "kjideo-mhdr"; + hdr.textContent = "Regions — top = front · click select · drag reorder"; + // 01 at the top of the list = front-most (drawn on top); see _draw / boxesAt. + menu.appendChild(hdr); + const list = document.createElement("div"); + menu.appendChild(list); + node._layerMenu = menu; + + const renumber = () => Array.from(list.querySelectorAll(".kjideo-lrow")).forEach((row, k) => { + row.querySelector(".kjideo-lnum").textContent = String(k + 1).padStart(2, "0"); + }); + function buildRows() { + list.innerHTML = ""; + if (!node._boxes.length) { + const empty = document.createElement("div"); + empty.className = "kjideo-mhdr"; empty.textContent = "No regions yet."; + list.appendChild(empty); + return; + } + node._boxes.forEach((b, i) => { + const row = document.createElement("div"); + row.className = "kjideo-lrow" + (i === node._activeIdx ? " active" : ""); + row._box = b; + const sw = document.createElement("div"); + sw.className = "kjideo-lsw"; + sw.style.background = (b.palette || []).find(Boolean) || "#8c8c8c"; + const num = document.createElement("span"); + num.className = "kjideo-lnum"; num.textContent = String(i + 1).padStart(2, "0"); + const txt = document.createElement("span"); + const label = rowLabel(b); + txt.className = "kjideo-ltext" + (label ? "" : " empty"); + txt.textContent = label || (b.type === "text" ? "(text)" : "(empty)"); + txt.title = label; + const lock = document.createElement("button"); + lock.className = "kjideo-lbtn kjideo-lock" + (b.locked ? " on" : ""); + lock.textContent = b.locked ? "🔒" : "🔓"; + lock.title = b.locked ? "Unlock (allow moving/resizing)" : "Lock (freeze on the canvas)"; + const dup = document.createElement("button"); + dup.className = "kjideo-lbtn"; dup.textContent = "⧉"; + dup.title = "Duplicate, then click on the canvas to place"; + const del = document.createElement("button"); + del.className = "kjideo-lbtn del"; del.textContent = "✕"; + del.title = b.locked ? "Unlock to delete" : "Delete region"; + del.disabled = !!b.locked; + row.append(sw, num, txt, lock, dup, del); + list.appendChild(row); + + lock.addEventListener("click", (e) => { + e.stopPropagation(); + b.locked = !b.locked; + if (b.locked) { // a locked box drops out of the selection + const idx = node._boxes.indexOf(b); + node._selection.delete(idx); + if (node._activeIdx === idx) node._activeIdx = node._selection.size ? [...node._selection][0] : -1; + } + commit(); buildRows(); + }); + + row.addEventListener("click", () => { + if (row._dragged) { row._dragged = false; return; } + selectOnly(node._boxes.indexOf(b)); + commit(); + for (const r of list.querySelectorAll(".kjideo-lrow")) r.classList.toggle("active", r._box === b); + }); + dup.addEventListener("click", (e) => { + e.stopPropagation(); + const idx = node._boxes.indexOf(b); + closeLayersMenu(); + startPlacing(idx); + }); + del.addEventListener("click", (e) => { + e.stopPropagation(); + const idx = node._boxes.indexOf(b); + if (idx < 0) return; + removeBox(idx); commit(); fitCanvas(); + if (!node._boxes.length) { closeLayersMenu(); return; } + buildRows(); + }); + // drag-reorder (vertical FLIP, mirrors the palette swatch reorder) + row.addEventListener("pointerdown", (e) => { + if (e.button !== 0 || e.target === lock || e.target === dup || e.target === del) return; + e.preventDefault(); e.stopPropagation(); + const sx = e.clientX, sy = e.clientY; + let dragging = false; + const move = (me) => { + if (!dragging) { + if (Math.abs(me.clientX - sx) + Math.abs(me.clientY - sy) < 4) return; + dragging = true; row.classList.add("dragging"); document.body.classList.add("kjideo-dragging"); + } + for (const other of list.querySelectorAll(".kjideo-lrow")) { + if (other === row) continue; + const r = other.getBoundingClientRect(); + if (me.clientY >= r.top && me.clientY <= r.bottom) { + const ref = me.clientY > r.top + r.height / 2 ? other.nextSibling : other; + if (ref === row || ref === row.nextSibling) break; + const els = Array.from(list.querySelectorAll(".kjideo-lrow")); + const prev = els.map((el) => el.getBoundingClientRect().top); + list.insertBefore(row, ref); + els.forEach((el, k) => { // FLIP: slide to new positions + const dy = prev[k] - el.getBoundingClientRect().top; + if (!dy) return; + el.style.transition = "none"; + el.style.transform = `translateY(${dy}px)`; + el.getBoundingClientRect(); // flush + el.style.transition = ""; el.style.transform = ""; + }); + break; + } + } + }; + const up = () => { + document.removeEventListener("pointermove", move); + document.removeEventListener("pointerup", up); + document.removeEventListener("pointercancel", up); + document.body.classList.remove("kjideo-dragging"); + if (dragging) { + row.classList.remove("dragging"); + row._dragged = true; // suppress the trailing click + const active = node._boxes[node._activeIdx]; + const order = Array.from(list.querySelectorAll(".kjideo-lrow")).map((el) => el._box); + if (order.length === node._boxes.length) node._boxes = order; + selectOnly(active ? node._boxes.indexOf(active) : -1); // reorder invalidates multi-select indices + renumber(); + commit(); + } + }; + document.addEventListener("pointermove", move); + document.addEventListener("pointerup", up); + document.addEventListener("pointercancel", up); + }); + }); + } + buildRows(); + + document.body.appendChild(menu); + const r = menu.getBoundingClientRect(); // clamp into the viewport + let left = clientX, top = clientY; + if (left + r.width > window.innerWidth) left = window.innerWidth - r.width - 4; + if (top + r.height > window.innerHeight) top = window.innerHeight - r.height - 4; + menu.style.left = Math.max(4, left) + "px"; + menu.style.top = Math.max(4, top) + "px"; + + node._layerDismiss = outsideDismiss(menu, () => closeLayersMenu()); + node._layerDismiss.arm(); + } + + canvasEl.addEventListener("contextmenu", (e) => { + e.preventDefault(); e.stopPropagation(); + if (node._placing || node._hideBoxes) return; + closeInlineEditor(); + openLayersMenu(e.clientX, e.clientY); + }); + stopProp(clearBtn); + clearBtn.addEventListener("click", () => { + closeInlineEditor(); + node._boxes = []; node._activeIdx = -1; node._selection = new Set(); node._stylePalette = []; + node._lastImported = ""; + commit(); rebuildStylePalette(); fitCanvas(); // a wired import re-seeds on the next run (serializeValue cache-busts) + }); + + // ── build caption JSON (mirrors Python key order) ── + // pyJson: matches Python _dumps — indent=4, but scalar arrays stay on one line. + function pyJson(v, lvl = 0) { + if (v === null) return "null"; + if (typeof v === "number" || typeof v === "boolean") return String(v); + if (typeof v === "string") return JSON.stringify(v); + const pad = " ".repeat(lvl + 1), end = " ".repeat(lvl); + if (Array.isArray(v)) { + if (!v.length) return "[]"; + if (v.every((x) => x === null || typeof x !== "object")) // scalar array → inline + return "[" + v.map((x) => pyJson(x, lvl)).join(", ") + "]"; + return "[\n" + v.map((x) => pad + pyJson(x, lvl + 1)).join(",\n") + "\n" + end + "]"; + } + const keys = Object.keys(v); + if (!keys.length) return "{}"; + return "{\n" + keys.map((k) => pad + JSON.stringify(k) + ": " + pyJson(v[k], lvl + 1)).join(",\n") + "\n" + end + "}"; + } + function getW(name) { const w = findW(name); return w ? w.value : ""; } + function cleanPalette(arr) { return (arr || []).filter((c) => c).map((c) => c.toUpperCase()); } + function normBboxJS(b) { + const c = (v) => Math.max(0, Math.min(1000, Math.round(v * 1000))); + let ymin = c(b.y), xmin = c(b.x), ymax = c(b.y + b.h), xmax = c(b.x + b.w); + if (ymin > ymax) [ymin, ymax] = [ymax, ymin]; + if (xmin > xmax) [xmin, xmax] = [xmax, xmin]; + return [ymin, xmin, ymax, xmax]; + } + // Caption bbox: 0-1000 grid (default) or absolute pixels (scaled by width/height) — must match the + // Python output. normBboxJS stays 0-1000 for the editor's grid input field, so keep this separate. + function captionBboxJS(b) { + const abs = (coordWidget?.value) === "absolute", xy = (orderWidget?.value) === "xy"; + const sx = abs ? (wWidget?.value || 1000) : 1000, sy = abs ? (hWidget?.value || 1000) : 1000; + const cx = (v) => Math.max(0, Math.min(sx, Math.round(v * sx))); + const cy = (v) => Math.max(0, Math.min(sy, Math.round(v * sy))); + let ymin = cy(b.y), xmin = cx(b.x), ymax = cy(b.y + b.h), xmax = cx(b.x + b.w); + if (ymin > ymax) [ymin, ymax] = [ymax, ymin]; + if (xmin > xmax) [xmin, xmax] = [xmax, xmin]; + return xy ? [xmin, ymin, xmax, ymax] : [ymin, xmin, ymax, xmax]; + } + function buildCaption() { + const cap = {}; + if ((getW("high_level_description") || "").trim()) cap.high_level_description = getW("high_level_description"); + const styleW = findW("style"); + const kind = styleW ? styleW.value : "none"; + if (kind !== "none") { + const sd = { aesthetics: getW("aesthetics"), lighting: getW("lighting") }; + if (kind === "photo") { sd.photo = getW("style.photo") || ""; sd.medium = getW("medium"); } + else { sd.medium = getW("medium"); sd.art_style = getW("style.art_style") || ""; } + const pal = cleanPalette(node._stylePalette); + if (pal.length) sd.color_palette = pal; + cap.style_description = sd; + } + const elements = node._boxes.map((b) => { + const etype = b.type === "text" ? "text" : "obj"; + const el = { type: etype }; + if (!b.nobbox) el.bbox = captionBboxJS(b); // unplaced elements omit bbox + if (etype === "text") el.text = b.text || ""; + el.desc = b.desc || ""; + const pal = cleanPalette(b.palette).slice(0, MAX_ELEM_COLORS); + if (pal.length) el.color_palette = pal; + return el; + }); + cap.compositional_deconstruction = { background: getW("background"), elements }; + return (formatWidget?.value) === "pretty" ? pyJson(cap) : JSON.stringify(cap); // compact by default; matches the node output + } + // Rough token estimate (~chars/4); exact count needs the Qwen tokenizer. + function updateTokens() { + const n = Math.ceil(buildCaption().length / 4); + tokenSpan.textContent = "~" + n + " tok"; + // grey <256 (sparse) · green healthy · orange nearing · red ≥2048 (model hard cap) + tokenSpan.style.color = n >= 2048 ? "#e05555" : n >= 1792 ? "#e6a23c" : n >= 256 ? "#6cc06c" : "#888"; + } + async function doCopy() { + const txt = buildCaption(); + try { await navigator.clipboard.writeText(txt); copyBtn.textContent = "Copied"; setTimeout(() => (copyBtn.textContent = "Copy"), 900); } + catch (e) { window.prompt("Copy the caption JSON:", txt); } + } + stopProp(copyBtn); + copyBtn.addEventListener("click", doCopy); + + // ── import a caption JSON and populate the node ── + function setWidgetVal(name, val) { + const w = findW(name); + if (w) { w.value = val; w.callback?.(val); } + } + function bboxElemToBox(el, idx) { + if (!el || typeof el !== "object") return null; + const box = { type: el.type === "text" ? "text" : "obj", + text: el.text || "", desc: el.desc || "", + palette: Array.isArray(el.color_palette) ? el.color_palette.slice() : [] }; + const bb = el.bbox; + if (Array.isArray(bb) && bb.length === 4) { + const [ymin, xmin, ymax, xmax] = bb; + box.x = xmin / 1000; box.y = ymin / 1000; box.w = (xmax - xmin) / 1000; box.h = (ymax - ymin) / 1000; + } else { + // No bbox: "unplaced" element — small placeholder, flagged so export omits bbox. + const k = (idx || 0) % 6; + box.x = 0.03 + k * 0.035; box.y = 0.03 + k * 0.035; box.w = 0.22; box.h = 0.14; + box.nobbox = true; + } + return box; + } + function applyCaption(cap) { + const cd = (cap && cap.compositional_deconstruction) || {}; + const els = Array.isArray(cd.elements) ? cd.elements : []; + node._boxes = els.map((el, i) => bboxElemToBox(el, i)).filter(Boolean); + selectOnly(node._boxes.length ? 0 : -1); + setWidgetVal("high_level_description", cap.high_level_description || ""); + setWidgetVal("background", cd.background || ""); + const sd = cap.style_description || {}; + let kind = "none"; + if (typeof sd.photo === "string") kind = "photo"; + else if (typeof sd.art_style === "string") kind = "art_style"; + const styleW = findW("style"); + if (styleW) styleW.value = kind; // setter synchronously rebuilds sub-widgets + if (kind === "photo") setWidgetVal("style.photo", sd.photo || ""); + else if (kind === "art_style") setWidgetVal("style.art_style", sd.art_style || ""); + setWidgetVal("aesthetics", sd.aesthetics || ""); + setWidgetVal("lighting", sd.lighting || ""); + setWidgetVal("medium", sd.medium || ""); + node._stylePalette = Array.isArray(sd.color_palette) ? sd.color_palette.slice() : []; + } + // Lenient fixer: slice the outermost {...} (drops ``` fences / prose), then strip trailing + // commas before } or ] — the string alt matches first so quoted commas are left untouched. + function repairJson(s) { + const i = s.indexOf("{"), j = s.lastIndexOf("}"); + const t = (i !== -1 && j > i) ? s.slice(i, j + 1) : s; + return t.replace(/("(?:[^"\\]|\\.)*")|,(\s*[}\]])/g, (m, str, close) => str || close); + } + function tryParseCaption(t) { + if (!t) return null; + for (const cand of [t, repairJson(t)]) { + try { const o = JSON.parse(cand); if (o && typeof o === "object" && o.compositional_deconstruction) return o; } + catch (e) {} + } + return null; + } + // Apply a parsed caption to the editor and refresh everything. + function loadCaption(cap) { + closeInlineEditor(); + // Setting the multiline widgets + rebuilding the style sub-widget makes ComfyUI grow the node; + // an import shouldn't change its size, so restore it (now and after the deferred re-arrange). + const sz = node.size ? [node.size[0], node.size[1]] : null; + applyCaption(cap); + if (sz) { node.setSize(sz); requestAnimationFrame(() => node.setSize(sz)); } + syncCanvasToDims(); commit(); rebuildStylePalette(); fitCanvas(); + } + // Append a caption's regions to the current canvas (keeps existing boxes + caption fields). + function insertCaptionBoxes(cap) { + closeInlineEditor(); + const cd = (cap && cap.compositional_deconstruction) || {}; + const els = Array.isArray(cd.elements) ? cd.elements : []; + const added = els.map((el, i) => bboxElemToBox(el, i)).filter(Boolean); + if (!added.length) return; + const start = node._boxes.length; + node._boxes.push(...added); + node._selection = new Set(); // select the inserted regions + for (let i = start; i < node._boxes.length; i++) node._selection.add(i); + node._activeIdx = node._boxes.length - 1; + commit(); fitCanvas(); + } + async function doImport() { + let cap = null, txt = ""; + try { txt = (await navigator.clipboard.readText() || "").trim(); cap = tryParseCaption(txt); } catch (e) {} + if (!cap) { txt = (window.prompt("Paste Ideogram 4 caption JSON:", "") || "").trim(); cap = tryParseCaption(txt); } + if (!cap) { if (txt) alert("Not a valid Ideogram 4 caption JSON (needs 'compositional_deconstruction')."); return; } + loadCaption(cap); + } + stopProp(importBtn); + importBtn.addEventListener("click", doImport); + + // Populate the editor from a caption pushed back by execute() when import_json + // is connected (a connected socket can't be read in the frontend directly). + function applyImported(capStr) { + if (!capStr) return; + // "always" mode re-applies even an unchanged import so the editor snaps back to the + // authoritative JSON after edits; "when empty" keeps the guard so edits stick. + const always = findW("import_mode")?.value === "always"; + if (capStr === node._lastImported && !always) return; + const cap = tryParseCaption(capStr); + if (!cap) return; + node._lastImported = capStr; + loadCaption(cap); + } + chainCallback(node, "onExecuted", function (message) { + if (message?.caption) applyImported(message.caption[0]); + // Seed regions from the bboxes input only when the editor is empty, so user-drawn/edited + // regions are never overwritten. + if (message?.boxes && !node._boxes.length) { + const seeded = JSON.parse(message.boxes[0]); + if (Array.isArray(seeded) && seeded.length) { + node._boxes = seeded.filter((b) => b && typeof b.x === "number" && typeof b.w === "number"); + selectOnly(node._boxes.length ? 0 : -1); + commit(); fitCanvas(); + } + } + // Reflect resolved width/height (e.g. from connected inputs) in the canvas aspect. + // A connected background image governs the aspect itself, so skip then. + if (message?.dims && !node._bgImg) { + const [w, h] = message.dims; + if (wWidget && w) wWidget.value = w; + if (hWidget && h) hWidget.value = h; + syncCanvasToDims(); fitCanvas(); + } + }); + + // ── property panel ── + function stopProp(el) { + el.addEventListener("wheel", (e) => e.stopPropagation()); + // Let middle-click (button 1) bubble so it reaches the dock's pan handler; block left/right so + // dock controls don't drag/select the node behind them. + for (const ev of ["mousedown", "pointerdown"]) el.addEventListener(ev, (e) => { if (e.button !== 1) e.stopPropagation(); }); + } + // Color swatches: onEdit on change, onStruct on add/remove/reorder. Shared by both palettes. + // Pointer-based drag (HTML5 DnD is unreliable inside LiteGraph DOM widgets) with live reorder. + function buildSwatchRow(container, arr, max, onEdit, onStruct) { + arr.forEach((hex, i) => { + const sw = document.createElement("div"); + sw.className = "kjideo-sw"; + sw.style.background = hex; + sw.dataset.hex = hex; + sw.title = "Click edit · drag reorder · Ctrl+C/V copy/paste hex · right-click remove"; + const inp = document.createElement("input"); + inp.type = "color"; inp.value = hex; + sw.appendChild(inp); + container.appendChild(sw); + const setColor = (hex2) => { arr[i] = hex2; inp.value = hex2; sw.style.background = hex2; sw.dataset.hex = hex2; onEdit(); }; + inp.addEventListener("input", () => setColor(inp.value)); + sw.addEventListener("pointerenter", () => { hoveredSwatch = { sw, setColor }; }); + sw.addEventListener("pointerleave", () => { if (hoveredSwatch && hoveredSwatch.sw === sw) hoveredSwatch = null; }); + sw.addEventListener("wheel", (e) => e.stopPropagation()); + sw.addEventListener("contextmenu", (e) => { e.preventDefault(); e.stopPropagation(); arr.splice(i, 1); onStruct(); }); + sw.addEventListener("pointerdown", (e) => { + if (e.button !== 0) return; + e.preventDefault(); e.stopPropagation(); + try { sw.setPointerCapture(e.pointerId); } catch (e2) {} // capture so Nodes 2.0's WidgetDOM .stop can't swallow the drag + const sx = e.clientX, sy = e.clientY; + let dragging = false; + const move = (me) => { + if (!dragging) { + if (Math.abs(me.clientX - sx) + Math.abs(me.clientY - sy) < 4) return; + dragging = true; sw.classList.add("dragging"); document.body.classList.add("kjideo-dragging"); + } + for (const other of container.querySelectorAll(".kjideo-sw")) { + if (other === sw) continue; + const r = other.getBoundingClientRect(); + if (me.clientX >= r.left && me.clientX <= r.right && me.clientY >= r.top - 6 && me.clientY <= r.bottom + 6) { + const ref = me.clientX > r.left + r.width / 2 ? other.nextSibling : other; + if (ref === sw || ref === sw.nextSibling) break; // already there + const els = Array.from(container.querySelectorAll(".kjideo-sw")); + const prev = els.map((el) => el.getBoundingClientRect().left); + container.insertBefore(sw, ref); + els.forEach((el, k) => { // FLIP: slide to new positions + const dx = prev[k] - el.getBoundingClientRect().left; + if (!dx) return; + el.style.transition = "none"; + el.style.transform = `translateX(${dx}px)`; + el.getBoundingClientRect(); // flush + el.style.transition = ""; el.style.transform = ""; + }); + break; + } + } + }; + const up = () => { + sw.removeEventListener("pointermove", move); + sw.removeEventListener("pointerup", up); + sw.removeEventListener("pointercancel", up); + document.body.classList.remove("kjideo-dragging"); + if (dragging) { + sw.classList.remove("dragging"); + const order = Array.from(container.querySelectorAll(".kjideo-sw")).map((el) => el.dataset.hex); + if (order.length === arr.length) { arr.length = 0; arr.push(...order); } + onStruct(); + } else { + inp.click(); // no drag → treat as click, open the picker + } + }; + sw.addEventListener("pointermove", move); + sw.addEventListener("pointerup", up); + sw.addEventListener("pointercancel", up); + }); + }); + if (arr.length < max) { + const add = document.createElement("button"); + add.className = "kjideo-btn"; add.textContent = "+"; + add.title = "Add a color (uses the clipboard color if it is one)"; + stopProp(add); + add.addEventListener("click", async () => { + let col = "#ffffff"; + try { const c = parseColorString(await navigator.clipboard.readText()); if (c) col = c; } catch (e) {} + arr.push(col); onStruct(); + }); + container.appendChild(add); + } + } + + // Swatch color changed (no add/remove): persist + repaint. + function swatchEdit() { serialize(); drawCanvas(); } + + function rebuildStylePalette() { + while (styleBar.children.length > 1) styleBar.removeChild(styleBar.lastChild); + buildSwatchRow(styleBar, node._stylePalette, MAX_STYLE_COLORS, + swatchEdit, + () => { swatchEdit(); rebuildStylePalette(); fitCanvas(); }); + } + + // Textarea that flexes to fill the prompt panel (whose height is set by the splitter). + function makeArea(field, value, placeholder, onInput) { + const ta = document.createElement("textarea"); + ta.className = "kjideo-area"; + ta.placeholder = placeholder; + ta.value = value || ""; + stopProp(ta); + ta.addEventListener("input", onInput); + return ta; + } + let bboxPx = null, bboxGrid = null; // editable bbox fields: pixels and 0–1000 grid + function dims() { return [wWidget ? wWidget.value : 1024, hWidget ? hWidget.value : 1024]; } + function boxToPx(b) { // same order as the grid: ymin, xmin, ymax, xmax + const [W, H] = dims(); + return [Math.round(b.y * H), Math.round(b.x * W), Math.round((b.y + b.h) * H), Math.round((b.x + b.w) * W)]; + } + function setField(inp, b, fn) { // skip while the field is being edited + if (!inp || document.activeElement === inp) return; + inp.value = (!b || b.nobbox) ? "" : fn(b).join(", "); + } + function updateBboxLabel() { + const b = node._boxes[node._activeIdx]; + setField(bboxPx, b, boxToPx); + setField(bboxGrid, b, normBboxJS); + } + function parse4(inp) { + const nums = inp.value.split(/[,\s]+/).map(Number).filter((n) => !isNaN(n)); + return nums.length === 4 ? nums : null; + } + function commitPxEdit() { + const b = node._boxes[node._activeIdx]; if (!b || !bboxPx) return; + const nums = parse4(bboxPx); if (!nums) { updateBboxLabel(); return; } + const [W, H] = dims(); + let [ymin, xmin, ymax, xmax] = nums; // ymin, xmin, ymax, xmax (matches grid) + ymin = Math.max(0, Math.min(H, ymin)); ymax = Math.max(0, Math.min(H, ymax)); + xmin = Math.max(0, Math.min(W, xmin)); xmax = Math.max(0, Math.min(W, xmax)); + if (ymin > ymax) [ymin, ymax] = [ymax, ymin]; + if (xmin > xmax) [xmin, xmax] = [xmax, xmin]; + b.y = ymin / H; b.x = xmin / W; b.h = (ymax - ymin) / H; b.w = (xmax - xmin) / W; + delete b.nobbox; commit(); fitCanvas(); + } + function commitGridEdit() { + const b = node._boxes[node._activeIdx]; if (!b || !bboxGrid) return; + const nums = parse4(bboxGrid); if (!nums) { updateBboxLabel(); return; } + let [ymin, xmin, ymax, xmax] = nums.map((n) => Math.max(0, Math.min(1000, n))); + if (ymin > ymax) [ymin, ymax] = [ymax, ymin]; + if (xmin > xmax) [xmin, xmax] = [xmax, xmin]; + b.y = ymin / 1000; b.x = xmin / 1000; b.h = (ymax - ymin) / 1000; b.w = (xmax - xmin) / 1000; + delete b.nobbox; commit(); fitCanvas(); + } + function makeBboxField(placeholder, title, onCommit) { + const inp = document.createElement("input"); + inp.type = "text"; inp.className = "kjideo-bbox"; + inp.placeholder = placeholder; inp.title = title; + stopProp(inp); + inp.addEventListener("keydown", (e) => { + e.stopPropagation(); + if (e.key === "Enter") inp.blur(); + else if (e.key === "Escape") { updateBboxLabel(); inp.blur(); } + }); + inp.addEventListener("change", onCommit); + return inp; + } + function renderPanel() { + for (const ro of node._areaObservers) ro.disconnect(); + node._areaObservers = []; + panel.innerHTML = ""; + const b = node._boxes[node._activeIdx]; + if (!b) { + hint.textContent = ""; + const p = document.createElement("div"); + p.style.color = "#888"; + p.textContent = node._boxes.length ? "Click a region to edit it." : "No regions yet."; + panel.appendChild(p); + return; + } + const col = (b.palette || []).find(Boolean) || "#bbb"; + const selN = node._selection.size; + // Build with DOM + style.color (a CSS value) — never innerHTML — since col comes from + // box data that may be loaded from an untrusted template/import (avoids HTML injection). + hint.textContent = ""; + const tag = document.createElement("b"); + tag.style.color = col; tag.textContent = "region " + (node._activeIdx + 1); + hint.appendChild(tag); + if (selN > 1) { + const s = document.createElement("span"); + s.style.color = "#888"; s.textContent = ` (${selN} selected)`; + hint.appendChild(s); + } + + // type toggle + const typeRow = document.createElement("div"); + typeRow.className = "kjideo-row"; + const lbl = document.createElement("span"); lbl.textContent = "type:"; typeRow.appendChild(lbl); + for (const t of ["obj", "text"]) { + const btn = document.createElement("button"); + btn.className = "kjideo-btn" + (b.type === t ? " active" : ""); + btn.textContent = t; + stopProp(btn); + btn.addEventListener("click", () => { b.type = t; commit(); }); + typeRow.appendChild(btn); + } + const pxLbl = document.createElement("span"); + pxLbl.textContent = "px:"; pxLbl.style.cssText = "margin-left:auto; color:#888;"; + typeRow.appendChild(pxLbl); + bboxPx = makeBboxField("ymin, xmin, ymax, xmax", "Pixel bbox (of the node's width/height): ymin, xmin, ymax, xmax — editable", commitPxEdit); + typeRow.appendChild(bboxPx); + const gl = document.createElement("span"); + gl.textContent = "out:"; gl.style.color = "#888"; + typeRow.appendChild(gl); + bboxGrid = makeBboxField("ymin, xmin, ymax, xmax", "Exported bbox on the 0–1000 grid: ymin, xmin, ymax, xmax — editable", commitGridEdit); + typeRow.appendChild(bboxGrid); + updateBboxLabel(); + panel.appendChild(typeRow); + + // text (only for text type) + if (b.type === "text") { + panel.appendChild(makeArea("text", b.text, "text to render (verbatim)", + function () { b.text = this.value; touch(); })); + } + + panel.appendChild(makeArea("desc", b.desc, "description of this region", + function () { b.desc = this.value; touch(); })); + + // palette + const palRow = document.createElement("div"); + palRow.className = "kjideo-row"; + const pl = document.createElement("span"); pl.textContent = "colors:"; palRow.appendChild(pl); + b.palette = b.palette || []; + buildSwatchRow(palRow, b.palette, MAX_ELEM_COLORS, swatchEdit, commit); + panel.appendChild(palRow); + } + + // ── width/height widget callbacks ── + for (const w of [wWidget, hWidget]) { + if (!w) continue; + chainCallback(w, "callback", () => { syncCanvasToDims(); drawCanvas(); fitCanvas(); updateTokens(); }); // absolute coords depend on w/h + } + // Update the token estimate when the caption-level text widgets change. + for (const name of ["background", "high_level_description", "aesthetics", "lighting", "medium", "style"]) { + const w = findW(name); + if (w) chainCallback(w, "callback", () => updateTokens()); + } + + // Persist node resizes: the 2.0 resize drag preventDefaults the compat mouseup, so the change + // tracker (snapshots on window 'mouseup') misses the new size — nudge it once the resize settles. + let _sizeFlush = 0; + chainCallback(node, "onResize", function () { + clearTimeout(_sizeFlush); + _sizeFlush = setTimeout(flushChange, 150); + }); + + // Optional reference image as the canvas background (matches ImageTransformKJ). + function loadBg(src, keepAspect) { + const img = new Image(); + img.crossOrigin = "anonymous"; + img.onload = () => { + node._bgImg = img; + if (!keepAspect) { // live mode keeps the user's canvas aspect + const r16 = (v) => Math.max(16, Math.round(v / 16) * 16); // model needs multiples of 16 + if (wWidget) wWidget.value = r16(img.naturalWidth); // match canvas aspect to the image + if (hWidget) hWidget.value = r16(img.naturalHeight); + } + syncCanvasToDims(); drawCanvas(); fitCanvas(); updateGrabBtn(); + }; + img.src = src; + } + watchImageInputs(node, "image", (sources) => { + if (!sources.length) { if (!node._bgManual) { node._bgImg = null; drawCanvas(); updateGrabBtn(); } return; } + node._bgManual = false; // a connected image takes over + const s = sources[0]; + if (s.isVideo && s.videoEl) captureVideoFrame(s.videoEl, (cv) => loadBg(cv.toDataURL("image/webp", 0.9))); + else if (s.url) loadBg(s.url); + }); + // "Grab BG" button: use the last generated image as the background, or clear it. + node._grabResultBg = () => { + if (!lastResultImage) { alert("No sampling result yet — run a generation first."); return; } + node._bgManual = true; + loadBg(resultViewUrl(lastResultImage)); + }; + node._clearBg = () => { + node._bgManual = false; node._bgImg = null; + if (node._liveBmp?.close) { try { node._liveBmp.close(); } catch (e) {} node._liveBmp = null; } + drawCanvas(); updateGrabBtn(); + }; + // Feed a live sampling-preview frame as the background (no width/height change). + node._ideoSetLiveBg = (bmp) => { + if (node._liveBmp?.close && node._liveBmp !== bmp) { try { node._liveBmp.close(); } catch (e) {} } + node._liveBmp = bmp; node._bgImg = bmp; node._bgManual = true; + drawCanvas(); updateGrabBtn(); + }; + // After generation, replace the live preview with the full-res final result. + node._ideoGrabFinal = () => { + if (!lastResultImage) return; + if (node._liveBmp?.close) { try { node._liveBmp.close(); } catch (e) {} node._liveBmp = null; } + node._bgManual = true; + loadBg(resultViewUrl(lastResultImage), true); // keep aspect — live mode must never resize the canvas + }; + + // Active-box highlight only while the editor is focused or the node is selected. + wrap.addEventListener("focusin", () => { if (!node._focused) { node._focused = true; drawCanvas(); } }); + wrap.addEventListener("focusout", (e) => { + if (!wrap.contains(e.relatedTarget)) { node._focused = false; drawCanvas(); } + }); + chainCallback(node, "onSelected", function () { node._selected = true; drawCanvas(); }); + chainCallback(node, "onDeselected", function () { node._selected = false; finishPlacing(); closeLayersMenu(); drawCanvas(); }); + + chainCallback(node, "onRemoved", function () { + livePreviewNodes.delete(node); + if (hoveredCanvasNode === node) hoveredCanvasNode = null; + if (node._fullscreen) { // tear down the popup if open + document.removeEventListener("keydown", onFsEsc, true); + window.removeEventListener("resize", fitFsCanvas); + node._fsOverlay?.remove(); + } + pinnedDocks.delete(node); liveDocks.delete(node); + node._dockRO?.disconnect(); node._dockEl?.remove(); // tear down the floating dock if open + node._visObserver?.disconnect(); + if (node._liveBmp?.close) { try { node._liveBmp.close(); } catch (e) {} node._liveBmp = null; } // release GPU bitmap + closeBgMenu(); node._bgMenu?.remove(); + closeTxtMenu(); node._txtMenu?.remove(); + closeTplMenu(); node._tplMenu?.remove(); + closeOutMenu(); node._outMenu?.remove(); + closeInlineEditor(); + closeLayersMenu(); + for (const ro of node._areaObservers) ro.disconnect(); + node._areaObservers = []; + }); + + // ── restore on load ── + function _parseBoxes(s) { + try { + const p = JSON.parse(s); + if (Array.isArray(p) && p.some((b) => b && typeof b.x === "number" && typeof b.w === "number")) return p; + } catch (e) {} + return null; + } + // Persist editor data by name (robust to widget-order changes across versions). + chainCallback(node, "onSerialize", function (o) { + if (!o) return; + const p = node.properties; + o.ideo = { boxes: node._boxes, palette: node._stylePalette, importMode: findW("import_mode")?.value, + outputFormat: findW("output_format")?.value, coordMode: findW("coord_mode")?.value, bboxOrder: findW("bbox_order")?.value, + dock: { pinned: p.dockPinned, graph: p.dockGraph, rect: p.dockRect, panelH: node._panelH, min: p.dockMin, + exposeParent: p.exposeToParent, parent: p.dockParent } }; + }); + chainCallback(node, "onConfigure", function (o) { + const raw = o && Array.isArray(o.widgets_values) ? o.widgets_values : []; + // Restore dock geometry from the blob into node.properties BEFORE ensureDocked runs. + // Use the SAVED size (o.size) — old workflows' tall embedded-editor height — so the dock gets a + // sensible height. node.size here is already shrunk to content by configure's widget application. + node._savedSize = (o && Array.isArray(o.size) && o.size.length === 2) ? [o.size[0], o.size[1]] : (node.size ? [node.size[0], node.size[1]] : null); + const d = o && o.ideo && o.ideo.dock; + if (d) { + node._dockGeomRestored = true; // honor it; old workflows w/o the blob get a default + if (d.graph) node.properties.dockGraph = d.graph; + if (d.rect) node.properties.dockRect = d.rect; + if (d.pinned != null) node.properties.dockPinned = d.pinned; + if (d.min != null) node.properties.dockMin = d.min; + if (d.exposeParent != null) node.properties.exposeToParent = d.exposeParent; + if (d.parent) node.properties.dockParent = d.parent; + if (typeof d.panelH === "number") { node._panelH = d.panelH; panel.style.height = d.panelH + "px"; } + } + node._syncExBtn?.(); // refresh the expose-to-parent toggle after restore + // Recover regions: name-keyed blob → named widget → raw saved values (survives + // any widget reorder/remap across versions) → live widgets. + let boxes = (o && o.ideo && Array.isArray(o.ideo.boxes)) ? o.ideo.boxes : _parseBoxes(elementsWidget?.value || ""); + if (!boxes) { for (const v of raw) { const b = _parseBoxes(v); if (b) { boxes = b; break; } } } + if (!boxes) { for (const w of node.widgets || []) { const b = _parseBoxes(w?.value); if (b) { boxes = b; break; } } } + if (boxes) { + node._boxes = boxes.filter((b) => b && typeof b.x === "number"); + selectOnly(node._boxes.length ? 0 : -1); + } + const isPal = (p) => Array.isArray(p) && p.length && p.every((c) => typeof c === "string" && c[0] === "#"); + let pal = (o && o.ideo && isPal(o.ideo.palette)) ? o.ideo.palette : null; + if (!pal) { try { const p = JSON.parse(stylePaletteWidget?.value || ""); if (isPal(p)) pal = p; } catch (e) {} } + if (!pal) { for (const v of raw) { try { const p = JSON.parse(v); if (isPal(p)) { pal = p; break; } } catch (e) {} } } + if (pal) node._stylePalette = pal.slice(); + const imW = findW("import_mode"); // restore import_mode; coerce to a valid option so + if (imW) { // old workflows (saved before this widget) don't fail Combo validation + const im = o && o.ideo && o.ideo.importMode, opts = ["when empty", "always"]; + imW.value = opts.includes(im) ? im : (opts.includes(imW.value) ? imW.value : "when empty"); + } + if (formatWidget) { // restore output format from the blob; default compact unless explicitly pretty + formatWidget.value = (o && o.ideo && o.ideo.outputFormat) === "pretty" ? "pretty" : "compact"; + compactLbl._cb.checked = formatWidget.value === "compact"; + } + if (coordWidget) { // restore coord mode; default normalized unless explicitly absolute + coordWidget.value = (o && o.ideo && o.ideo.coordMode) === "absolute" ? "absolute" : "normalized"; + absLbl._cb.checked = coordWidget.value === "absolute"; + } + if (orderWidget) { // restore bbox axis order; default yx unless explicitly xy + orderWidget.value = (o && o.ideo && o.ideo.bboxOrder) === "xy" ? "xy" : "yx"; + xyLbl._cb.checked = orderWidget.value === "xy"; + } + node._configured = true; // mark as loaded so initial layout keeps the restored size + hideDataWidgets(); + serialize(); // realign widget values for Python + future saves + if (bgBrightnessWidget) { + if (typeof bgBrightnessWidget.value !== "number") bgBrightnessWidget.value = 25; // old workflows may restore "" + bgSlider.value = bgBrightnessWidget.value; + } + // node.properties is restored after onNodeCreated, so resync the toolbar controls to it. + liveChk.checked = !!node.properties.liveBg; + if (liveChk.checked) livePreviewNodes.add(node); else livePreviewNodes.delete(node); + guideSel.value = node.properties.guide || "none"; + gridSlider.value = GRID_INV - (node.properties.gridSize || 10); + snapChk.checked = !!node.properties.snap; + guideColor.value = node.properties.guideColor || "#ffffff"; + opacitySlider.value = node.properties.guideOpacity == null ? 100 : node.properties.guideOpacity; + showLbl._cb.checked = node.properties.showBoxText !== false; + strokeLbl._cb.checked = node.properties.textStroke !== false; + autoLbl._cb.checked = node.properties.textAutoPlace !== false; + sizeSlider.value = node.properties.textSize || 12; + boxOpacSlider.value = node.properties.boxOpacity == null ? 14 : node.properties.boxOpacity; + syncCanvasToDims(); + rebuildStylePalette(); + renderPanel(); + drawCanvas(); + updateTokens(); + // configure() shrinks the node to content height (multiline widgets) before this runs — for a + // current-version save, re-assert the saved widgets-only size. Skip for old (pre-dock) workflows: + // their saved height included the embedded editor, so let those shrink to content instead. + if (node._dockGeomRestored && o && Array.isArray(o.size) && o.size.length === 2) node.setSize([o.size[0], o.size[1]]); + requestAnimationFrame(() => ensureDocked(false)); // reload: build the dock honoring saved geometry + }); + + // Initial layout (deferred so size/last_y settle). NEVER set height here — it must stay whatever + // ComfyUI restored (touching it was the "height resets on reload/copy-paste" bug); only nudge a default width. + setTimeout(() => { + hideDataWidgets(); + if (!node._configured) node.setSize([Math.max(480, node.size[0]), node.size[1]]); // fresh: comfortable default width, keep natural height + else if (!node._dockGeomRestored) node.setSize([Math.max(380, node.size[0]), node.computeSize()[1]]); // old workflow: drop the embedded-editor height, shrink to content + else if (node.size[0] < 380) node.setSize([380, node.size[1]]); // current reload/paste: keep saved size, enforce min width + syncCanvasToDims(); + rebuildStylePalette(); + renderPanel(); + drawCanvas(); + updateTokens(); + if (!node._configured) ensureDocked(true); // fresh node: pop the dock under it, matching its width + }, 0); + }); + }, +}); diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/web/js/image_transform.js b/ComfyUI/custom_nodes/ComfyUI-KJNodes/web/js/image_transform.js new file mode 100644 index 0000000000000000000000000000000000000000..b104d901f02daff26c6e25c01e41af0ba161098b --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-KJNodes/web/js/image_transform.js @@ -0,0 +1,2058 @@ +import { chainCallback, addMiddleClickPan, addWheelPassthrough, captureVideoFrame, watchImageInputs, rectHitTest, cursorForBboxMode } from './utility.js'; +const { app } = window.comfyAPI.app; + +const BBOX_PALETTE = ["#46b4e6", "#e68246", "#82e646", "#e646b4", "#e6e646", "#46e6c8"]; +const MAX_CANVAS_W = 1024, MAX_CANVAS_H = 768; +const BBOX_COLORS = BBOX_PALETTE.map((hex) => { + const r = parseInt(hex.slice(1, 3), 16), g = parseInt(hex.slice(3, 5), 16), b = parseInt(hex.slice(5, 7), 16); + return { rgb: `rgb(${r}, ${g}, ${b})`, gridActive: `rgba(${r}, ${g}, ${b}, 0.35)`, + gridInactive: `rgba(${r}, ${g}, ${b}, 0.3)`, tint: `rgba(${r}, ${g}, ${b}, 0.2)` }; +}); + +function getBboxColor(index, active) { + const c = BBOX_COLORS[index % BBOX_COLORS.length]; + return { border: c.rgb, fill: c.rgb, handle: c.rgb, gridColor: active ? c.gridActive : c.gridInactive, tint: c.tint }; +} + +function clampToMaxCanvas(w, h) { + if (w > MAX_CANVAS_W || h > MAX_CANVAS_H) { + const s = Math.min(MAX_CANVAS_W / w, MAX_CANVAS_H / h); + return [Math.round(w * s), Math.round(h * s)]; + } + return [w, h]; +} + +function roundDown(val, divBy) { + return divBy > 1 ? val - (val % divBy) : val; +} + +app.registerExtension({ + name: "KJNodes.ImageTransformKJ", + + async beforeRegisterNodeDef(nodeType, nodeData) { + if (nodeData?.name !== "ImageTransformKJ") return; + + chainCallback(nodeType.prototype, "onNodeCreated", function () { + const node = this; + + const findW = (n) => this.widgets.find((w) => w.name === n); + const bboxWidget = findW("bboxes"), twWidget = findW("target_width"), thWidget = findW("target_height"); + const kpWidget = findW("keep_proportion"), divWidget = findW("divisible_by"); + const epWidget = findW("extra_padding"), icWidget = findW("invert_crop"); + bboxWidget.hidden = true; + + const _subWidgetCache = {}; + function _refreshSubWidgetCache() { + if (!node.widgets) return; + for (const w of node.widgets) { + if (w.name?.includes(".")) _subWidgetCache[w.name] = w; + } + } + function _getSubWidget(name) { + let w = _subWidgetCache[name]; + if (w && node.widgets?.includes(w)) return w; + // Cache miss — rescan + w = node.widgets?.find(ww => ww.name === name); + if (w) _subWidgetCache[name] = w; + return w || null; + } + + const _tmpCanvas = document.createElement("canvas"), _tmpCtx = _tmpCanvas.getContext("2d"); + const _mirrorCanvas = document.createElement("canvas"), _mirrorCtx = _mirrorCanvas.getContext("2d"); + const _rotCanvas = document.createElement("canvas"), _rotCtx = _rotCanvas.getContext("2d"); + let _rotCacheKey = ""; + function getRotatedPreview() { + if (!node._previewImg) return null; + const nw = node._previewImg.naturalWidth; + const nh = node._previewImg.naturalHeight; + const { w: effW, h: effH } = getEffectiveImageDims(true); + // Scale to reasonable preview size + let [rw, rh] = clampToMaxCanvas(effW, effH); + const key = `${node._previewImg.src}|${node._rotation}|${rw}|${rh}`; + if (_rotCacheKey === key) return _rotCanvas; + _rotCanvas.width = rw; + _rotCanvas.height = rh; + _rotCtx.clearRect(0, 0, rw, rh); + if (node._rotation === 0) { + _rotCtx.drawImage(node._previewImg, 0, 0, rw, rh); + } else { + const rad = Math.abs(rotRad()); + const rotW = Math.abs(nw * Math.cos(rad)) + Math.abs(nh * Math.sin(rad)); + const rotH = Math.abs(nw * Math.sin(rad)) + Math.abs(nh * Math.cos(rad)); + const fitScale = Math.min(rw / rotW, rh / rotH); + _rotCtx.translate(rw / 2, rh / 2); + _rotCtx.rotate(rotRad()); + _rotCtx.drawImage(node._previewImg, -(nw * fitScale) / 2, -(nh * fitScale) / 2, nw * fitScale, nh * fitScale); + _rotCtx.setTransform(1, 0, 0, 1, 0, 0); + } + _rotCacheKey = key; + return _rotCanvas; + } + + let _frameEp = { top: 0, bottom: 0, left: 0, right: 0, mode: "disabled", color: null, edgeMode: "clamp" }; + let _framePadXY = { x: 0.5, y: 0.5 }; + + const wrapper = document.createElement("div"); + wrapper.style.cssText = "display:flex;flex-direction:column;overflow:hidden;position:relative;pointer-events:auto;"; + const canvasEl = document.createElement("canvas"); + const canvasCtx = canvasEl.getContext("2d"); + canvasEl.style.cssText = "cursor:crosshair;display:block;width:100%;height:auto;"; + addWheelPassthrough(wrapper); + addMiddleClickPan(canvasEl); + + // Inject slider thumb styles once + if (!document.getElementById("kjcrop-slider-style")) { + const style = document.createElement("style"); + style.id = "kjcrop-slider-style"; + style.textContent = ` + .kjcrop-slider { -webkit-appearance: none; appearance: none; background: #444; border-radius: 4px; outline: none; } + .kjcrop-slider::-webkit-slider-thumb { -webkit-appearance: none; width: 8px; height: 18px; background: #46b4e6; border-radius: 4px; cursor: pointer; border: none; } + .kjcrop-slider::-moz-range-thumb { width: 8px; height: 18px; background: #46b4e6; border-radius: 4px; cursor: pointer; border: none; } + .kjcrop-btn { background: #333; border: 1px solid #555; border-radius: 4px; color: #bbb; font: 11px sans-serif; cursor: pointer; padding: 2px 8px; line-height: 16px; white-space: nowrap; flex-shrink: 0; transition: border-color 0.15s, color 0.15s, background 0.15s; text-align: center; } + .kjcrop-btn:hover { border-color: #46b4e6; color: #fff; background: #3a3a3a; } + .kjcrop-btn.active { border-color: #46b4e6; color: #46b4e6; background: #2a3a42; } + `; + document.head.appendChild(style); + } + // Grid size slider overlay (purely visual aid, not serialized) + const gridBar = document.createElement("div"); + gridBar.style.cssText = "display:flex;align-items:center;gap:6px;padding:3px 6px;margin-bottom:4px;font:11px sans-serif;color:#aaa;user-select:none;box-sizing:border-box;width:100%;flex:0 0 auto;"; + const gridLabel = document.createElement("span"); + gridLabel.textContent = "Grid: off"; + gridLabel.style.cssText = "min-width:62px;text-align:right;"; + const gridSlider = document.createElement("input"); + gridSlider.type = "range"; + gridSlider.min = "0"; + gridSlider.max = "256"; + gridSlider.step = "1"; + gridSlider.value = node.properties.gridSize ?? "0"; + gridSlider.className = "kjcrop-slider"; + gridSlider.style.cssText = "flex:1;height:8px;cursor:pointer;"; + function updateGridLabel() { + const v = parseInt(gridSlider.value); + gridLabel.textContent = v > 0 ? `Grid: ${v}px` : "Grid: off"; + } + updateGridLabel(); + let _gridStops = null; + function getGridStops() { + if (!node._previewImg) return null; + const tw = node._previewImg.naturalWidth; + const th = node._previewImg.naturalHeight; + const max = parseInt(gridSlider.max); + const set = new Set([0]); + for (let n = 1; n <= Math.max(tw, th); n++) { + const gw = tw / n, gh = th / n; + if (gw >= 2 && gw <= max) set.add(Math.round(gw * 10) / 10); + if (gh >= 2 && gh <= max) set.add(Math.round(gh * 10) / 10); + if (gw < 2 && gh < 2) break; + } + return [...set].sort((a, b) => a - b); + } + function invalidateGridStops() { _gridStops = null; } + gridSlider.addEventListener("input", () => { + let v = parseInt(gridSlider.value); + // Snap to divisors of the longer dimension for whole-cell grid + if (v > 0 && node._previewImg) { + if (!_gridStops) _gridStops = getGridStops(); + if (_gridStops) { + let best = 0, bestDist = Infinity; + for (const s of _gridStops) { + const d = Math.abs(s - v); + if (d < bestDist) { bestDist = d; best = s; } + } + v = best; + } + } + gridSlider.value = Math.round(v); + node.properties.gridSize = Math.round(v); + updateGridLabel(); + drawCanvas(); + }); + // Prevent drag events from propagating to the LiteGraph canvas, + // but only when not actively dragging a bbox handle + for (const evt of ["mousedown", "mousemove", "mouseup"]) { + gridSlider.addEventListener(evt, (e) => { + if (!node._drawing) e.stopPropagation(); + }); + } + // Toggle button to disable image preview (draw bboxes on blank canvas) + const previewBtn = document.createElement("button"); + previewBtn.title = "Auto: canvas sized to input image. Manual: canvas sized to target dimensions."; + previewBtn.className = "kjcrop-btn"; + previewBtn.style.width = "56px"; + if (node.properties.previewEnabled === undefined) node.properties.previewEnabled = true; + node._previewEnabled = node.properties.previewEnabled; + + function updatePreviewBtn() { + previewBtn.textContent = node._previewEnabled ? "Auto" : "Manual"; + previewBtn.classList.toggle("active", !node._previewEnabled); + } + updatePreviewBtn(); + + previewBtn.addEventListener("click", () => { + node._previewEnabled = !node._previewEnabled; + node.properties.previewEnabled = node._previewEnabled; + updatePreviewBtn(); + if (!node._previewEnabled) { + updateCanvasFromTargetDims(); + } else if (node._previewImg) { + // Restore canvas to image dimensions + const [cw, ch] = clampToMaxCanvas(node._previewImg.naturalWidth, node._previewImg.naturalHeight); + setCanvasSize(cw, ch); + } + drawCanvas(); + }); + previewBtn.addEventListener("mousedown", (e) => e.stopPropagation()); + + const colorSwatch = document.createElement("div"); + const colorInput = document.createElement("input"); + colorInput.type = "color"; + colorInput.value = node.properties.fillColor || "#000000"; + colorInput.style.cssText = "position:absolute;opacity:0;width:0;height:0;pointer-events:none;"; + colorSwatch.style.cssText = "width:18px;height:18px;border:1px solid #666;border-radius:3px;cursor:pointer;flex-shrink:0;background:" + (node.properties.fillColor || "#000000") + ";"; + colorSwatch.title = "Fill color (used by pad, invert crop, rotation)"; + colorSwatch.appendChild(colorInput); + colorSwatch.addEventListener("click", () => colorInput.click()); + colorSwatch.addEventListener("mousedown", (e) => e.stopPropagation()); + colorInput.addEventListener("input", () => { + node.properties.fillColor = colorInput.value; colorSwatch.style.background = colorInput.value; + updateBboxWidgets(); drawCanvas(); + }); + + function getEffectiveImageDims(expand = true) { + const w = node._previewImg.naturalWidth, h = node._previewImg.naturalHeight; + if (node._rotation === 0 || !expand) return { w, h }; + const rad = Math.abs(rotRad()); + const cosR = Math.cos(rad), sinR = Math.sin(rad); + return { w: Math.round(Math.abs(w * cosR) + Math.abs(h * sinR)), + h: Math.round(Math.abs(w * sinR) + Math.abs(h * cosR)) }; + } + + function renderReplicateRotated(destW, destH) { + const img = node._previewImg, nw = img.naturalWidth, nh = img.naturalHeight; + const pad = Math.max(nw, nh), pw = nw + pad * 2, ph = nh + pad * 2; + // Build replicate-padded image on _mirrorCanvas + _mirrorCanvas.width = pw; _mirrorCanvas.height = ph; + _mirrorCtx.drawImage(img, pad, pad, nw, nh); + // Edges (top, bottom, left, right) + _mirrorCtx.drawImage(img, 0, 0, nw, 1, pad, 0, nw, pad); + _mirrorCtx.drawImage(img, 0, nh - 1, nw, 1, pad, pad + nh, nw, pad); + _mirrorCtx.drawImage(img, 0, 0, 1, nh, 0, pad, pad, nh); + _mirrorCtx.drawImage(img, nw - 1, 0, 1, nh, pad + nw, pad, pad, nh); + // Corners (TL, TR, BL, BR) + _mirrorCtx.drawImage(img, 0, 0, 1, 1, 0, 0, pad, pad); + _mirrorCtx.drawImage(img, nw - 1, 0, 1, 1, pad + nw, 0, pad, pad); + _mirrorCtx.drawImage(img, 0, nh - 1, 1, 1, 0, pad + nh, pad, pad); + _mirrorCtx.drawImage(img, nw - 1, nh - 1, 1, 1, pad + nw, pad + nh, pad, pad); + // Rotate padded image, crop to original size, scale to dest + _tmpCanvas.width = destW; _tmpCanvas.height = destH; + _tmpCtx.save(); + _tmpCtx.scale(destW / nw, destH / nh); + _tmpCtx.translate(nw / 2, nh / 2); + _tmpCtx.rotate(rotRad()); + _tmpCtx.drawImage(_mirrorCanvas, -(pw / 2), -(ph / 2)); + _tmpCtx.restore(); + } + + // Resize node to fit the current canvas/image. If onlyGrow is true, + // the node will only get larger, never shrink (preserves user resize). + // If false, the node is set to the exact size needed. + function fitNodeToImage(onlyGrow) { + if (!node._previewEnabled) { + // Manual mode: only grow to fit current canvas, never change width + if (onlyGrow && canvasEl.width > 0 && canvasEl.height > 0) { + _resizing = true; + const displayedH = Math.round(canvasEl.height * ((node.size[0] - 30) / canvasEl.width)); + node._widgetHeight = displayedH + GRID_BAR_HEIGHT; + const aboveH = node.cropEditor?.last_y || 350; + const neededH = aboveH + node._widgetHeight + 20; + if (node.size[1] < neededH) node.setSize([node.size[0], neededH]); + _resizing = false; + } + return; + } + if (!node._previewImg) return; + _resizing = true; + const { w: effW, h: effH } = getEffectiveImageDims(true); + const ar = effH / effW; + const nodeW = onlyGrow ? node.size[0] : canvasEl.width + 30; + const canvasDisplayH = Math.round((nodeW - 30) * ar); + node._widgetHeight = canvasDisplayH + GRID_BAR_HEIGHT; + const aboveH = node.cropEditor?.last_y || 350; + // +20 accounts for DOM widget margin (10px top + 10px bottom) + const neededH = aboveH + node._widgetHeight + 20; + const newW = onlyGrow ? Math.max(node.size[0], nodeW) : nodeW; + const newH = onlyGrow ? Math.max(node.size[1], neededH) : neededH; + if (newW !== node.size[0] || newH !== node.size[1]) { + node.setSize([newW, newH]); + } + _resizing = false; + // Force reflow and redraw so the layout updates immediately + void canvasEl.offsetHeight; + if (node.graph) { + node.graph.setDirtyCanvas(true, true); + requestAnimationFrame(() => { + drawCanvas(); + if (node.graph) node.graph.setDirtyCanvas(true, true); + }); + } + } + + function resetRotation() { + if (node._rotation === 0) return; + node._rotation = 0; + node.properties.rotation = 0; + restoreCanvasToImage(); + fitNodeToImage(false); + updateBboxWidgets(); + drawCanvas(); + } + + const rotateBtn = document.createElement("button"); + rotateBtn.title = "Toggle rotation cross (drag to rotate image)"; + rotateBtn.className = "kjcrop-btn"; + if (node.properties.showRotationCross === undefined) node.properties.showRotationCross = false; + node._showRotationCross = node.properties.showRotationCross; + + function updateRotateBtn() { + rotateBtn.textContent = "Rotate"; + rotateBtn.classList.toggle("active", node._showRotationCross); + } + updateRotateBtn(); + + rotateBtn.addEventListener("click", () => { + node._showRotationCross = !node._showRotationCross; + node.properties.showRotationCross = node._showRotationCross; + updateRotateBtn(); + drawCanvas(); + }); + rotateBtn.addEventListener("mousedown", (e) => e.stopPropagation()); + rotateBtn.addEventListener("contextmenu", (e) => { + e.preventDefault(); + resetRotation(); + }); + + const gridColorInput = document.createElement("input"); + gridColorInput.type = "color"; + gridColorInput.value = node.properties.gridColor || "#ffffff"; + gridColorInput.style.cssText = "position:absolute;opacity:0;width:0;height:0;pointer-events:none;"; + const gridColorSwatch = document.createElement("div"); + gridColorSwatch.style.cssText = "width:18px;height:18px;border:1px solid #666;border-radius:3px;cursor:pointer;flex-shrink:0;background:" + (node.properties.gridColor || "#ffffff") + ";"; + gridColorSwatch.title = "Grid color"; + gridColorSwatch.appendChild(gridColorInput); + gridColorSwatch.addEventListener("click", () => gridColorInput.click()); + gridColorSwatch.addEventListener("mousedown", (e) => e.stopPropagation()); + let _gridColorRGB = null; + function getGridColorRGB() { + const gc = node.properties.gridColor || "#ffffff"; + if (!_gridColorRGB || _gridColorRGB.hex !== gc) { + _gridColorRGB = { hex: gc, r: parseInt(gc.slice(1, 3), 16), g: parseInt(gc.slice(3, 5), 16), b: parseInt(gc.slice(5, 7), 16) }; + } + return _gridColorRGB; + } + gridColorInput.addEventListener("input", () => { + node.properties.gridColor = gridColorInput.value; gridColorSwatch.style.background = gridColorInput.value; + _gridColorRGB = null; + drawCanvas(); + }); + + for (const el of [previewBtn, rotateBtn, colorSwatch, gridLabel, gridColorSwatch, gridSlider]) gridBar.appendChild(el); + wrapper.appendChild(gridBar); wrapper.appendChild(canvasEl); + + const GRID_BAR_HEIGHT = 46; + node._widgetHeight = 300 + GRID_BAR_HEIGHT; + + node.cropEditor = this.addDOMWidget("crop_preview", "CropPreviewWidget", wrapper, { + serialize: false, hideOnZoom: false, + getMinHeight: () => GRID_BAR_HEIGHT + 50, + }); + + this.resizable = true; + // Set default width wider than LiteGraph's default — deferred so onConfigure can set the flag first + setTimeout(() => { + if (!node._cropConfigured) node.setSize([450, node.size[1]]); + }, 0); + + Object.assign(node, { _previewImg: null, _bboxes: [], _activeIdx: -1, + _drawing: false, _dragMode: null, _dragStart: null, _bboxAtDragStart: null, + _rotation: node.properties.rotation || 0 }); + + for (const w of [twWidget, thWidget, kpWidget, divWidget, epWidget, icWidget]) { + if (!w) continue; + const origCb = w.callback; + w.callback = function (...args) { + if (origCb) origCb.apply(this, args); + updateCanvasFromTargetDims(); + drawCanvas(); + fitNodeToCanvas(); + fitNodeToImage(true); + if (w === kpWidget || w === icWidget || w === epWidget) { + // Restore values with increasing delays to catch ComfyUI's own default-setting + const restoreSaved = () => { + hookSubWidgets(); + _refreshSubWidgetCache(); + if (w === epWidget && node._savedEpValues && epWidget.value !== "disabled") { + for (const [n, v] of Object.entries(node._savedEpValues)) { + const sw = _getSubWidget(`extra_padding.${n}`); + if (sw) sw.value = v; + } + } else if (w === kpWidget && node._savedKpValues) { + for (const [n, v] of Object.entries(node._savedKpValues)) { + const sw = _getSubWidget(`keep_proportion.${n}`); + if (sw) sw.value = v; + } + } + drawCanvas(); + fitNodeToCanvas(); + fitNodeToImage(true); + }; + for (const delay of [50, 100, 200, 300, 500, 1000]) { + setTimeout(restoreSaved, delay); + } + } + }; + } + + const hookSubWidgets = () => { + _refreshSubWidgetCache(); + for (const subName of ["keep_proportion.edge_mode", "keep_proportion.pad_x", "keep_proportion.pad_y", "keep_proportion.width_mult", "keep_proportion.height_mult", "extra_padding.pad_top", "extra_padding.pad_bottom", "extra_padding.pad_left", "extra_padding.pad_right", "extra_padding.edge_mode", ]) { + const sw = _getSubWidget(subName); + if (sw && !sw._cropPreviewHooked) { + sw._cropPreviewHooked = true; + // Restore saved values from mode switch + const shortName = subName.split(".")[1]; + if (subName.startsWith("extra_padding.") && node._savedEpValues && shortName in node._savedEpValues) { + sw.value = node._savedEpValues[shortName]; + } else if (subName.startsWith("keep_proportion.") && node._savedKpValues && shortName in node._savedKpValues) { + sw.value = node._savedKpValues[shortName]; + } + // Hook callback — save values on change so they persist across mode switches + const swOrig = sw.callback; + sw.callback = function (...a) { + if (_suppressSubCallbacks) return; + if (subName.startsWith("extra_padding.")) node._savedEpValues[shortName] = sw.value; + else if (subName.startsWith("keep_proportion.")) node._savedKpValues[shortName] = sw.value; + if (swOrig) swOrig.apply(this, a); + drawCanvas(); + }; + } + } + }; + for (const delay of [50, 150, 300, 500]) { + setTimeout(hookSubWidgets, delay); + } + + let _lastWidgetSnapshot = ""; + const _snapshotNames = [ + "extra_padding", "extra_padding.pad_top", "extra_padding.pad_bottom", + "extra_padding.pad_left", "extra_padding.pad_right", + "extra_padding.edge_mode", + "keep_proportion.pad_x", "keep_proportion.pad_y", "keep_proportion.color", + "keep_proportion.edge_mode", "keep_proportion.width_mult", "keep_proportion.height_mult", + ]; + node._savedEpValues = node._savedEpValues || {}; + node._savedKpValues = node._savedKpValues || {}; + const origDrawFg = node.onDrawForeground; + node.onDrawForeground = function (...args) { + if (origDrawFg) origDrawFg.apply(this, args); + const snap = _snapshotNames.map(n => _getSubWidget(n)?.value ?? (n === "extra_padding" ? epWidget?.value : "") ?? "").join("|"); + if (snap !== _lastWidgetSnapshot) { + _lastWidgetSnapshot = snap; + drawCanvas(); + } + }; + + // Restore bboxes from widget + restoreBboxesFromWidget(); + + // Boxes are serialized in the canvas frame they were authored in (previewWidth/Height). + // Bring them into the current canvas frame so the in-memory coords stay canvas-relative. + function normalizeLoadedBboxes(boxes) { + const cw = canvasEl.width, ch = canvasEl.height; + if (!cw || !ch) return boxes; + return boxes.map((b) => { + const pw = b.previewWidth, ph = b.previewHeight; + if (!pw || !ph || (pw === cw && ph === ch)) { + const { previewWidth, previewHeight, ...rest } = b; + return rest; + } + const sx = cw / pw, sy = ch / ph; + return { startX: b.startX * sx, startY: b.startY * sy, endX: b.endX * sx, endY: b.endY * sy }; + }); + } + + function restoreBboxesFromWidget() { + if (bboxWidget.value) { + try { + const parsed = JSON.parse(bboxWidget.value); + // New format: { bboxes: [...], rotation: N } + if (parsed && !Array.isArray(parsed) && typeof parsed === "object") { + if (parsed.bboxes && Array.isArray(parsed.bboxes)) { + node._bboxes = normalizeLoadedBboxes(parsed.bboxes.filter((b) => b?.startX != null)); + node._activeIdx = node._bboxes.length > 0 ? 0 : -1; + } + if (parsed.rotation !== undefined) { + node._rotation = parsed.rotation; + node.properties.rotation = parsed.rotation; + } + if (parsed.fillColor) { + node.properties.fillColor = parsed.fillColor; + colorInput.value = parsed.fillColor; + colorSwatch.style.background = parsed.fillColor; + } + } + // Legacy format: [bbox, bbox, ...] + else if (Array.isArray(parsed) && parsed.length > 0) { + node._bboxes = normalizeLoadedBboxes(parsed.filter((b) => b?.startX != null)); + node._activeIdx = node._bboxes.length > 0 ? 0 : -1; + } + } catch (e) {} + } + } + + function normBbox(bbox) { + return { x1: Math.min(bbox.startX, bbox.endX), y1: Math.min(bbox.startY, bbox.endY), + x2: Math.max(bbox.startX, bbox.endX), y2: Math.max(bbox.startY, bbox.endY) }; + } + + function getCanvasMouse(e) { + const rect = canvasEl.getBoundingClientRect(); + return { x: (e.clientX - rect.left) * (canvasEl.width / rect.width), + y: (e.clientY - rect.top) * (canvasEl.height / rect.height) }; + } + + function rotRad() { return node._rotation * Math.PI / 180; } + function getDivBy() { return divWidget ? divWidget.value : 0; } + function getGridSize() { return parseInt(gridSlider.value) || 0; } + // Subtract extra padding from target dims for non-pad keep_proportion modes + function adjustTargetForPadding(rw, rh, tgtW, tgtH) { + const ep = getExtraPadding(); + if (hasExtraPad(ep) && !ep.isFirst) { + if (tgtW > 0) rw = Math.max(1, rw - ep.left - ep.right); + if (tgtH > 0) rh = Math.max(1, rh - ep.top - ep.bottom); + } + return { rw, rh }; + } + function getEffectiveTargetDims(fallbackW, fallbackH) { + const divBy = getDivBy(); + const { w: tgtW, h: tgtH } = getTgt(); + let { rw, rh } = adjustTargetForPadding(tgtW > 0 ? tgtW : fallbackW, tgtH > 0 ? tgtH : fallbackH, tgtW, tgtH); + return { rw: roundDown(rw, divBy), rh: roundDown(rh, divBy) }; + } + + function getFillColor() { return node.properties.fillColor || "#000000"; } + function getTgt() { return { w: twWidget ? twWidget.value : 0, h: thWidget ? thWidget.value : 0 }; } + function getKp() { return kpWidget ? kpWidget.value : "stretch"; } + function isPadMode(kp) { return kp === "pad_color" || kp === "pad_edge"; } + function hasExtraPad(ep) { return ep.top + ep.bottom + ep.left + ep.right > 0; } + + function hitTestRotationCross(mx, my) { + if (!node._showRotationCross) return false; + const cx = canvasEl.width / 2; + const cy = canvasEl.height / 2; + const dx = mx - cx; + const dy = my - cy; + const rad = rotRad(); + const cosR = Math.cos(rad); + const sinR = Math.sin(rad); + const perpH = -dx * sinR + dy * cosR; + const perpV = dx * cosR + dy * sinR; + const dist = Math.sqrt(dx * dx + dy * dy); + return dist < 15 || Math.abs(perpH) < 8 || Math.abs(perpV) < 8; + } + + function restoreCanvasToImage() { + if (!node._previewImg || !node._previewEnabled) return; + setCanvasSize(...clampToMaxCanvas(...Object.values(getEffectiveImageDims(true)))); + } + + function drawPreviewRegion(ctx, rx, ry, rw, rh, dx, dy, dw, dh) { + const rp = getRotatedPreview(); + const sx = _rotCanvas.width / canvasEl.width, sy = _rotCanvas.height / canvasEl.height; + ctx.drawImage(rp, rx * sx, ry * sy, rw * sx, rh * sy, dx, dy, dw, dh); + } + + function drawStretchOverlay(ctx, ar, rx, ry, rw, rh) { + const { pW, pH, pX, pY } = fitAspect(ar, rw, rh, rx, ry); + ctx.fillStyle = "rgba(0, 0, 0, 0.5)"; + ctx.fillRect(rx, ry, rw, rh); + if (node._previewEnabled && node._previewImg) drawPreviewRegion(ctx, rx, ry, rw, rh, pX, pY, pW, pH); + drawDashedBorder(ctx, pX, pY, pW, pH); + } + + // Compute content dimensions after keep_proportion resize, before extra padding + function computeContentDims(cropW, cropH) { + if (cropW <= 0 || cropH <= 0) return null; + const { w: tgtW, h: tgtH } = getTgt(); + let outW = cropW, outH = cropH; + const kp = getKp(); + // For pad-first non-pad modes, the source is padded before keep_proportion + const ep = getExtraPadding(); + if (ep.isFirst && hasExtraPad(ep) && !isPadMode(kp)) { + cropW += ep.left + ep.right; + cropH += ep.top + ep.bottom; + } + if (tgtW > 0 || tgtH > 0) { + let { rw, rh } = adjustTargetForPadding(tgtW > 0 ? tgtW : cropW, tgtH > 0 ? tgtH : cropH, tgtW, tgtH); + if (kp === "keep_long_edge") { + const ratio = Math.min(rw / cropW, rh / cropH); + rw = Math.round(cropW * ratio); + rh = Math.round(cropH * ratio); + } else if (kp === "keep_short_edge") { + const ratio = Math.max(rw / cropW, rh / cropH); + rw = Math.round(cropW * ratio); + rh = Math.round(cropH * ratio); + } else if (kp === "total_pixels") { + const totalPx = rw * rh; + const ar = cropW / cropH; + rh = Math.round(Math.sqrt(totalPx / ar)); + rw = Math.round(Math.sqrt(totalPx * ar)); + } + outW = rw; + outH = rh; + } + if (kp === "multiplier") { + const wm = _getSubWidget("keep_proportion.width_mult")?.value || 1.0; + const hm = _getSubWidget("keep_proportion.height_mult")?.value || 1.0; + outW = Math.round(cropW * wm); + outH = Math.round(cropH * hm); + } + return { w: outW, h: outH }; + } + + function computeOutputDims(cropW, cropH) { + const content = computeContentDims(cropW, cropH); + if (!content) return null; + let outW = content.w, outH = content.h; + // Add extra padding (skip for pad-first — handled on the source image) + const ep = getExtraPadding(); + if (!ep.isFirst) { + outW += ep.left + ep.right; + outH += ep.top + ep.bottom; + } + const divBy = getDivBy(); + outW = roundDown(outW, divBy); + outH = roundDown(outH, divBy); + return { w: outW, h: outH }; + } + + function getPadXY() { return _framePadXY; } + function getExtraPadding() { return _frameEp; } + + function _readPadXY() { + const px = _getSubWidget("keep_proportion.pad_x"), py = _getSubWidget("keep_proportion.pad_y"); + return { x: px ? px.value : 0.5, y: py ? py.value : 0.5 }; + } + + let _suppressSubCallbacks = false; + function setPadXY(x, y, defer) { + x = Math.max(0, Math.min(1, x)); + y = Math.max(0, Math.min(1, y)); + _framePadXY = { x, y }; + if (!defer) { + const px = _getSubWidget("keep_proportion.pad_x"), py = _getSubWidget("keep_proportion.pad_y"); + _suppressSubCallbacks = true; + if (px) px.value = x; + if (py) py.value = y; + _suppressSubCallbacks = false; + } + } + + function _readExtraPadding() { + const get = (name) => _getSubWidget(`extra_padding.${name}`)?.value || 0; + const mode = epWidget?.value || "disabled"; + // pad_color/pad_edge = pad first (before crop), pad_crop_color/pad_crop_edge = pad after crop + const isCrop = mode.startsWith("pad_crop_"); + const baseMode = isCrop ? mode.replace("pad_crop_", "pad_") : mode; + return { + top: get("pad_top"), bottom: get("pad_bottom"), + left: get("pad_left"), right: get("pad_right"), + mode: baseMode, + isFirst: !isCrop && mode !== "disabled", + edgeMode: _getSubWidget("extra_padding.edge_mode")?.value || "clamp", + }; + } + + // Draw extra padding fill around content area (cX,cY,cW,cH) within pad area (pX,pY,pW,pH) + // srcImg is the source image, srcX/Y/W/H define the source region + // Draw edge/color fill around content area. srcImg must be pre-rendered at its full dimensions. + function drawExtraPadFill(ctx, ep, pX, pY, pW, pH, cX, cY, cW, cH, srcImg) { + if (ep.mode === "pad_color") { + ctx.fillStyle = getFillColor(); + ctx.fillRect(pX, pY, pW, pH); + } else if (ep.mode === "pad_edge") { + const em = ep.edgeMode; + const sw = srcImg.width, sh = srcImg.height; + if (em === "clamp") { + const l = cX - pX, r = (pX + pW) - (cX + cW), t = cY - pY, b = (pY + pH) - (cY + cH); + if (t > 0) ctx.drawImage(srcImg, 0, 0, sw, 1, cX, pY, cW, t); + if (b > 0) ctx.drawImage(srcImg, 0, sh - 1, sw, 1, cX, cY + cH, cW, b); + if (l > 0) ctx.drawImage(srcImg, 0, 0, 1, sh, pX, cY, l, cH); + if (r > 0) ctx.drawImage(srcImg, sw - 1, 0, 1, sh, cX + cW, cY, r, cH); + if (l > 0 && t > 0) ctx.drawImage(srcImg, 0, 0, 1, 1, pX, pY, l, t); + if (r > 0 && t > 0) ctx.drawImage(srcImg, sw - 1, 0, 1, 1, cX + cW, pY, r, t); + if (l > 0 && b > 0) ctx.drawImage(srcImg, 0, sh - 1, 1, 1, pX, cY + cH, l, b); + if (r > 0 && b > 0) ctx.drawImage(srcImg, sw - 1, sh - 1, 1, 1, cX + cW, cY + cH, r, b); + } else if (em === "repeat" || em === "mirror") { + // Copy srcImg to _mirrorCanvas first — srcImg may be _tmpCanvas + const tileW = Math.max(1, Math.round(cW)); + const tileH = Math.max(1, Math.round(cH)); + _mirrorCanvas.width = tileW; + _mirrorCanvas.height = tileH; + _mirrorCtx.drawImage(srcImg, 0, 0, tileW, tileH); + let tileCanvas; + if (em === "mirror") { + _tmpCanvas.width = tileW * 2; _tmpCanvas.height = tileH * 2; + _tmpCtx.drawImage(_mirrorCanvas, 0, 0); + _tmpCtx.save(); _tmpCtx.translate(_tmpCanvas.width, 0); _tmpCtx.scale(-1, 1); _tmpCtx.drawImage(_mirrorCanvas, 0, 0); _tmpCtx.restore(); + _tmpCtx.save(); _tmpCtx.translate(0, _tmpCanvas.height); _tmpCtx.scale(1, -1); _tmpCtx.drawImage(_mirrorCanvas, 0, 0); _tmpCtx.restore(); + _tmpCtx.save(); _tmpCtx.translate(_tmpCanvas.width, _tmpCanvas.height); _tmpCtx.scale(-1, -1); _tmpCtx.drawImage(_mirrorCanvas, 0, 0); _tmpCtx.restore(); + tileCanvas = _tmpCanvas; + } else { + tileCanvas = _mirrorCanvas; + } + const pattern = ctx.createPattern(tileCanvas, "repeat"); + ctx.save(); + ctx.beginPath(); ctx.rect(pX, pY, pW, pH); ctx.clip(); + ctx.translate(cX, cY); + ctx.fillStyle = pattern; + ctx.fillRect(pX - cX, pY - cY, pW, pH); + ctx.restore(); + } + } + } + + function fitAspect(targetAR, cw, ch, cx = 0, cy = 0) { + if (targetAR > cw / ch) { const pH = cw / targetAR; return { pW: cw, pH, pX: cx, pY: cy + (ch - pH) / 2 }; } + const pW = ch * targetAR; return { pW, pH: ch, pX: cx + (cw - pW) / 2, pY: cy }; + } + + function drawDashedBorder(ctx, x, y, w, h, color = "rgba(255, 180, 60, 0.8)") { + ctx.setLineDash([4, 4]); ctx.strokeStyle = color; ctx.lineWidth = 1; + ctx.strokeRect(x, y, w, h); ctx.setLineDash([]); + } + + // Dim four sides around an inner rect within an outer rect + function dimSides(ctx, ox, oy, ow, oh, ix, iy, iw, ih, color = "rgba(255, 80, 80, 0.3)") { + ctx.fillStyle = color; + ctx.fillRect(ox, oy, ow, iy - oy); // top + ctx.fillRect(ox, iy + ih, ow, (oy + oh) - (iy + ih)); // bottom + ctx.fillRect(ox, iy, ix - ox, ih); // left + ctx.fillRect(ix + iw, iy, (ox + ow) - (ix + iw), ih); // right + } + + // Compute pad mode layout: fit target AR, compute content rect inside + function computePadLayout(rw, rh, cropW, cropH, fitW, fitH, fitX, fitY) { + const { pW, pH, pX, pY } = fitAspect(rw / rh, fitW, fitH, fitX, fitY); + const imgRatio = Math.min(rw / cropW, rh / cropH); + const cW = pW * (cropW * imgRatio / rw), cH = pH * (cropH * imgRatio / rh); + const { cX, cY } = computePadContentPos(pX, pY, pW, pH, cW, cH); + return { pW, pH, pX, pY, cW, cH, cX, cY }; + } + + // Render source to _tmpCanvas at given size. If srcImg + source rect provided, draws that region. + // Otherwise draws the rotated preview (with replicate rotation for pad_edge mode). + // replicate: force replicate-pad rotation (for keep_proportion pad_edge mode) + function renderToTmp(w, h, srcImg, sx, sy, sw, sh, replicate) { + const ep = getExtraPadding(); + if (!srcImg && node._rotation !== 0 && (replicate || (ep.mode === "pad_edge" && ep.isFirst))) { + renderReplicateRotated(w, h); return; + } + _tmpCanvas.width = w; _tmpCanvas.height = h; + if (srcImg) _tmpCtx.drawImage(srcImg, sx, sy, sw, sh, 0, 0, w, h); + else _tmpCtx.drawImage(getRotatedPreview(), 0, 0, _rotCanvas.width, _rotCanvas.height, 0, 0, w, h); + } + + function getEdgeEp() { + return { mode: "pad_edge", edgeMode: _getSubWidget("keep_proportion.edge_mode")?.value || "clamp" }; + } + + function computePadContentPos(pX, pY, pW, pH, cW, cH) { + const { x, y } = getPadXY(); + return { cX: pX + (pW - cW) * x, cY: pY + (pH - cH) * y }; + } + + function getOtherBboxEdges(axis) { + const edges = []; + for (let i = 0; i < node._bboxes.length; i++) { + if (i === node._activeIdx) continue; + const { x1, y1, x2, y2 } = normBbox(node._bboxes[i]); + edges.push(axis === "x" ? x1 : y1, axis === "x" ? x2 : y2); + } + return edges; + } + + function getEffectiveSnap() { + const g = getGridParams(); + if (g) return { x: g.effGridW, y: g.effGridH }; + const d = getDivBy(); + return d > 1 ? { x: d, y: d } : { x: 0, y: 0 }; + } + + function snapCoord(val, snapSize, scale, canvasDim, otherEdges) { + let best = val, bestDist = Infinity; + const consider = (c) => { const d = Math.abs(val - c); if (d < bestDist) { bestDist = d; best = c; } }; + consider(0); + consider(canvasDim); + if (snapSize > 1) consider(Math.round(val * scale / snapSize) * snapSize / scale); + if (otherEdges) for (const edge of otherEdges) consider(edge); + return best; + } + + // Snap the moving corner(s) of a bbox to grid / edges / other bboxes. + function snapBbox(bbox) { + if (!bbox) return bbox; + const snap = getEffectiveSnap(), scale = getCanvasScale(); + const xEdges = getOtherBboxEdges("x"), yEdges = getOtherBboxEdges("y"); + return { startX: snapCoord(bbox.startX, snap.x, scale.x, canvasEl.width, xEdges), + startY: snapCoord(bbox.startY, snap.y, scale.y, canvasEl.height, yEdges), + endX: snapCoord(bbox.endX, snap.x, scale.x, canvasEl.width, xEdges), + endY: snapCoord(bbox.endY, snap.y, scale.y, canvasEl.height, yEdges) }; + } + + function snapBboxPosition(bbox) { + if (!bbox) return bbox; + const snap = getEffectiveSnap(); + const scale = getCanvasScale(); + const xEdges = getOtherBboxEdges("x"); + const yEdges = getOtherBboxEdges("y"); + const { x1, y1 } = normBbox(bbox); + const w = Math.abs(bbox.endX - bbox.startX); + const h = Math.abs(bbox.endY - bbox.startY); + // Snap both the left/top and right/bottom edges, pick whichever is closer + const snappedX1 = snapCoord(x1, snap.x, scale.x, canvasEl.width, xEdges); + const snappedX2 = snapCoord(x1 + w, snap.x, scale.x, canvasEl.width, xEdges) - w; + const snappedX = Math.abs(snappedX1 - x1) <= Math.abs(snappedX2 - x1) ? snappedX1 : snappedX2; + const snappedY1 = snapCoord(y1, snap.y, scale.y, canvasEl.height, yEdges); + const snappedY2 = snapCoord(y1 + h, snap.y, scale.y, canvasEl.height, yEdges) - h; + const snappedY = Math.abs(snappedY1 - y1) <= Math.abs(snappedY2 - y1) ? snappedY1 : snappedY2; + return { startX: snappedX, startY: snappedY, endX: snappedX + w, endY: snappedY + h }; + } + + function constrainAspect(bbox, dragMode) { + if (!bbox) return bbox; + const { w: tw, h: th } = getTgt(); + // Target aspect in source-image pixels + const srcAR = (tw > 0 && th > 0) ? tw / th : 1; + // Convert to preview-pixel aspect by accounting for non-uniform scaling + const scale = getCanvasScale(); + const ar = srcAR * scale.y / scale.x; + + const { x1, y1, x2, y2 } = normBbox(bbox); + let bw = x2 - x1; + let bh = y2 - y1; + if (bw <= 0 || bh <= 0) return bbox; + + // Adjust height to match aspect, keep width + const hFromW = bw / ar; + const wFromH = bh * ar; + // Pick the smaller fit so we don't exceed the drawn extent + if (hFromW <= bh) { + bh = hFromW; + } else { + bw = wFromH; + } + + // Anchor based on drag mode + if (dragMode === "resize-tl") { + return { startX: x2 - bw, startY: y2 - bh, endX: x2, endY: y2 }; + } else if (dragMode === "resize-tr") { + return { startX: x1, startY: y2 - bh, endX: x1 + bw, endY: y2 }; + } else if (dragMode === "resize-bl") { + return { startX: x2 - bw, startY: y1, endX: x2, endY: y1 + bh }; + } else { + // resize-br or draw (anchor top-left) + return { startX: x1, startY: y1, endX: x1 + bw, endY: y1 + bh }; + } + } + + function removeBbox(index) { + node._bboxes.splice(index, 1); + if (node._bboxes.length === 0) node._activeIdx = -1; + else if (index <= node._activeIdx) node._activeIdx = Math.min(node._activeIdx - (index < node._activeIdx ? 1 : 0), node._bboxes.length - 1); + } + + function hitTestBboxes(mx, my) { + const order = node._activeIdx >= 0 ? [node._activeIdx] : []; + for (let i = 0; i < node._bboxes.length; i++) { if (i !== node._activeIdx) order.push(i); } + for (const idx of order) { + const { x1, y1, x2, y2 } = normBbox(node._bboxes[idx]); + const mode = rectHitTest(mx, my, x1, y1, x2, y2, 10); + if (mode) return { index: idx, mode }; + } + return null; + } + + canvasEl.addEventListener("mousedown", (e) => { + canvasEl.focus(); + if (e.button === 2) { + e.preventDefault(); + const m = getCanvasMouse(e); + // Right-click on rotation cross: reset rotation + if (node._rotation !== 0 && hitTestRotationCross(m.x, m.y)) { + resetRotation(); + return; + } + // Right-click: delete the bbox under cursor, or active bbox + const hit = hitTestBboxes(m.x, m.y); + if (hit) { + removeBbox(hit.index); + } else if (node._activeIdx >= 0) { + removeBbox(node._activeIdx); + } + updateBboxWidgets(); + drawCanvas(); + return; + } + if (e.button !== 0) return; + const m = getCanvasMouse(e); + + // Check for rotation cross drag (click near the center or any arm) + if (hitTestRotationCross(m.x, m.y)) { + node._dragMode = "rotate"; + node._rotateStart = node._rotation; + const rect0 = canvasEl.getBoundingClientRect(); + // Capture screen center once — immune to canvas resize + node._rotateCenterX = rect0.left + rect0.width / 2; + node._rotateCenterY = rect0.top + rect0.height / 2; + node._rotateScreenStartAngle = Math.atan2( + e.clientY - node._rotateCenterY, + e.clientX - node._rotateCenterX + ) * 180 / Math.PI; + node._drawing = true; + node._dragStart = m; + document.addEventListener("mousemove", onDragMove); + document.addEventListener("mouseup", onDragEnd); + e.preventDefault(); + e.stopPropagation(); + return; + } + + // Shift+drag: drag content position within padded area + if (e.shiftKey) { + const kpVal = getKp(); + const { w: tW2, h: tH2 } = getTgt(); + const ep = getExtraPadding(); + const startDrag = (mode, extra) => { + node._dragMode = mode; Object.assign(node, extra); + node._drawing = true; node._dragStart = m; + document.addEventListener("mousemove", onDragMove); + document.addEventListener("mouseup", onDragEnd); + e.preventDefault(); e.stopPropagation(); + }; + if ((tW2 > 0 || tH2 > 0) && isPadMode(kpVal)) { + startDrag("pad_drag", { _padDragStart: getPadXY(), _bboxAtDragStart: null }); return; + } else if (hasExtraPad(ep)) { + startDrag("extra_pad_drag", { _extraPadStart: { ...ep }, _bboxAtDragStart: null }); return; + } + } + + const hit = hitTestBboxes(m.x, m.y); + if (hit) { + // Select and interact with existing bbox + node._activeIdx = hit.index; + node._dragMode = hit.mode; + const bbox = node._bboxes[hit.index]; + const { x1, y1, x2, y2 } = normBbox(bbox); + node._bboxAtDragStart = { startX: x1, startY: y1, endX: x2, endY: y2 }; + } else { + // Draw new bbox + node._dragMode = "draw"; + const newBbox = { startX: m.x, startY: m.y, endX: m.x, endY: m.y }; + node._bboxes.push(newBbox); + node._activeIdx = node._bboxes.length - 1; + node._bboxAtDragStart = null; + } + node._drawing = true; + node._dragStart = m; + + document.addEventListener("mousemove", onDragMove); + document.addEventListener("mouseup", onDragEnd); + e.preventDefault(); + e.stopPropagation(); + }); + + // Hover cursor updates + canvasEl.addEventListener("mousemove", (e) => { + if (node._drawing) return; + const m = getCanvasMouse(e); + const hit = hitTestBboxes(m.x, m.y); + if (!hit) { + if (!node._showRotationCross) { + canvasEl.style.cursor = e.shiftKey ? "grab" : "crosshair"; + } else if (hitTestRotationCross(m.x, m.y)) { + canvasEl.style.cursor = "alias"; + } else if (e.shiftKey) { + const kpVal = getKp(); + const { w: tW, h: tH } = getTgt(); + const epHover = getExtraPadding(); + if (((tW > 0 || tH > 0) && isPadMode(kpVal)) || hasExtraPad(epHover)) { + canvasEl.style.cursor = "grab"; + } else { + canvasEl.style.cursor = "crosshair"; + } + } else { + canvasEl.style.cursor = "crosshair"; + } + } else { + canvasEl.style.cursor = cursorForBboxMode(hit.mode) || "crosshair"; + } + }); + + function onDragMove(e) { + if (!node._drawing) return; + if (node._dragMode === "rotate") { + // Screen-space atan2 with fixed center (captured at drag start) + const sdx = e.clientX - node._rotateCenterX; + const sdy = e.clientY - node._rotateCenterY; + const screenDist = Math.sqrt(sdx * sdx + sdy * sdy); + if (screenDist < 15) { + node._rotateStart = node._rotation; + node._rotateScreenStartAngle = Math.atan2(sdy, sdx) * 180 / Math.PI; + return; + } + const angleCur = Math.atan2(sdy, sdx) * 180 / Math.PI; + let angleDelta = angleCur - node._rotateScreenStartAngle; + // Normalize delta to -180..180 to handle atan2 wrap-around + while (angleDelta > 180) angleDelta -= 360; + while (angleDelta < -180) angleDelta += 360; + let newRot = node._rotateStart + angleDelta; + if (e.ctrlKey) { + newRot = Math.round(newRot / 15) * 15; + } else { + for (const snap of [0, 90, 180, 270, -90, -180, -270, 360]) { + if (Math.abs(newRot - snap) < 3) { newRot = snap; break; } + } + } + while (newRot > 180) newRot -= 360; + while (newRot < -180) newRot += 360; + node._rotation = newRot; + node.properties.rotation = newRot; + // Resize canvas to match rotated image dimensions + if (node._previewImg && node._previewEnabled) { + const { w: ew, h: eh } = getEffectiveImageDims(true); + const [rw, rh] = clampToMaxCanvas(ew, eh); + if (Math.abs(rw - canvasEl.width) > 3 || Math.abs(rh - canvasEl.height) > 3) { + setCanvasSize(rw, rh); + } + } + drawCanvas(); + return; + } + if (node._dragMode === "pad_drag") { + const m = getCanvasMouse(e); + const dx = m.x - node._dragStart.x; + const dy = m.y - node._dragStart.y; + // Compute the pad preview layout to find how many canvas pixels = full 0-1 range + const ep = getExtraPadding(); + const { w: imgW, h: imgH } = node._previewImg + ? getEffectiveImageDims(getKp() !== "pad_edge") + : { w: canvasEl.width, h: canvasEl.height }; + const { rw: baseTw, rh: baseTh } = getEffectiveTargetDims(imgW, imgH); + const totalW = baseTw + ep.left + ep.right; + const totalH = baseTh + ep.top + ep.bottom; + const { pW, pH } = fitAspect(totalW / totalH, canvasEl.width, canvasEl.height); + const imgRatio = Math.min(baseTw / imgW, baseTh / imgH); + const cW = pW * (imgW * imgRatio / totalW), cH = pH * (imgH * imgRatio / totalH); + const rangeX = pW - cW, rangeY = pH - cH; + const newX = rangeX > 0 ? node._padDragStart.x + dx / rangeX : 0.5; + const newY = rangeY > 0 ? node._padDragStart.y + dy / rangeY : 0.5; + setPadXY(newX, newY, true); + drawCanvas(); + return; + } + if (node._dragMode === "extra_pad_drag") { + const m = getCanvasMouse(e); + const dx = m.x - node._dragStart.x; + const dy = m.y - node._dragStart.y; + const s = node._extraPadStart; + const totalH = s.top + s.bottom, totalW = s.left + s.right; + const fullW = (node._previewImg ? node._previewImg.naturalWidth : canvasEl.width) + totalW; + const fullH = (node._previewImg ? node._previewImg.naturalHeight : canvasEl.height) + totalH; + const scale = (fullW / fullH > canvasEl.width / canvasEl.height) + ? fullW / canvasEl.width : fullH / canvasEl.height; + const pixDx = Math.round(dx * scale); + const pixDy = Math.round(dy * scale); + const newLeft = Math.max(0, Math.min(totalW, s.left + pixDx)); + const newTop = Math.max(0, Math.min(totalH, s.top + pixDy)); + for (const [name, val] of [["pad_left", newLeft], ["pad_right", totalW - newLeft], + ["pad_top", newTop], ["pad_bottom", totalH - newTop]]) { + const w = _getSubWidget(`extra_padding.${name}`); + if (w) w.value = val; + } + drawCanvas(); + return; + } + if (node._activeIdx < 0) return; + const m = getCanvasMouse(e); + const cw = canvasEl.width; + const ch = canvasEl.height; + let bbox = node._bboxes[node._activeIdx]; + + if (node._dragMode === "draw") { + bbox.endX = Math.max(0, Math.min(cw, m.x)); + bbox.endY = Math.max(0, Math.min(ch, m.y)); + } else if (node._dragMode === "move" && node._bboxAtDragStart) { + const dx = m.x - node._dragStart.x; + const dy = m.y - node._dragStart.y; + const bs = node._bboxAtDragStart; + const w = bs.endX - bs.startX; + const h = bs.endY - bs.startY; + const nx = Math.max(0, Math.min(cw - w, bs.startX + dx)); + const ny = Math.max(0, Math.min(ch - h, bs.startY + dy)); + bbox = { startX: nx, startY: ny, endX: nx + w, endY: ny + h }; + } else if (node._dragMode?.startsWith("resize") && node._bboxAtDragStart) { + const bs = node._bboxAtDragStart; + const dm = node._dragMode; + // Edge resize: only one axis moves + if (dm === "resize-t" || dm === "resize-b" || dm === "resize-l" || dm === "resize-r") { + const edgeCoord = dm === "resize-t" ? bs.startY : dm === "resize-b" ? bs.endY : dm === "resize-l" ? bs.startX : bs.endX; + const isVert = dm === "resize-t" || dm === "resize-b"; + const off = (isVert ? node._dragStart.y : node._dragStart.x) - edgeCoord; + const val = Math.max(0, Math.min(isVert ? ch : cw, (isVert ? m.y : m.x) - off)); + if (dm === "resize-t") bbox = { startX: bs.startX, startY: val, endX: bs.endX, endY: bs.endY }; + else if (dm === "resize-b") bbox = { startX: bs.startX, startY: bs.startY, endX: bs.endX, endY: val }; + else if (dm === "resize-l") bbox = { startX: val, startY: bs.startY, endX: bs.endX, endY: bs.endY }; + else bbox = { startX: bs.startX, startY: bs.startY, endX: val, endY: bs.endY }; + } else { + // Corner resize + let cornerX, cornerY; + if (dm === "resize-tl") { cornerX = bs.startX; cornerY = bs.startY; } + else if (dm === "resize-tr") { cornerX = bs.endX; cornerY = bs.startY; } + else if (dm === "resize-bl") { cornerX = bs.startX; cornerY = bs.endY; } + else { cornerX = bs.endX; cornerY = bs.endY; } + const offX = node._dragStart.x - cornerX; + const offY = node._dragStart.y - cornerY; + const cx = Math.max(0, Math.min(cw, m.x - offX)); + const cy = Math.max(0, Math.min(ch, m.y - offY)); + if (dm === "resize-tl") bbox = { startX: bs.endX, startY: bs.endY, endX: cx, endY: cy }; + else if (dm === "resize-tr") bbox = { startX: bs.startX, startY: bs.endY, endX: cx, endY: cy }; + else if (dm === "resize-bl") bbox = { startX: bs.endX, startY: bs.startY, endX: cx, endY: cy }; + else if (dm === "resize-br") bbox = { startX: bs.startX, startY: bs.startY, endX: cx, endY: cy }; + } + } + + // Alt: resize symmetrically — for draw, the click point is center; + // for resize, the center of the original bbox is the pivot + if (e.altKey && node._dragMode !== "move") { + let cx, cy; + if (node._dragMode === "draw") { + cx = node._dragStart.x; + cy = node._dragStart.y; + } else if (node._bboxAtDragStart) { + const bs = node._bboxAtDragStart; + cx = (bs.startX + bs.endX) / 2; + cy = (bs.startY + bs.endY) / 2; + } else { + cx = (bbox.startX + bbox.endX) / 2; + cy = (bbox.startY + bbox.endY) / 2; + } + const dx = Math.abs(bbox.endX - cx); + const dy = Math.abs(bbox.endY - cy); + bbox = { + startX: Math.max(0, cx - dx), + startY: Math.max(0, cy - dy), + endX: Math.min(cw, cx + dx), + endY: Math.min(ch, cy + dy), + }; + } + + // Shift: constrain to target aspect ratio (not applicable to edge resize) + const isEdgeResize = /^resize-[tblr]$/.test(node._dragMode); + if (e.shiftKey && node._dragMode !== "move" && !isEdgeResize) { + bbox = constrainAspect(bbox, node._dragMode); + } + + // Ctrl: snap to grid in source image space + if (e.ctrlKey) { + if (node._dragMode === "move") { + bbox = snapBboxPosition(bbox); + } else if (e.altKey) { + // Snap both edges, keep centered + const snap = getEffectiveSnap(); + const scale = getCanvasScale(); + const xEdges = getOtherBboxEdges("x"); + const yEdges = getOtherBboxEdges("y"); + const sx1 = snapCoord(bbox.startX, snap.x, scale.x, cw, xEdges); + const sx2 = snapCoord(bbox.endX, snap.x, scale.x, cw, xEdges); + const sy1 = snapCoord(bbox.startY, snap.y, scale.y, ch, yEdges); + const sy2 = snapCoord(bbox.endY, snap.y, scale.y, ch, yEdges); + bbox = { startX: sx1, startY: sy1, endX: sx2, endY: sy2 }; + } else if (isEdgeResize) { + // Only snap the edge being dragged + const snap = getEffectiveSnap(); + const scale = getCanvasScale(); + const xEdges = getOtherBboxEdges("x"), yEdges = getOtherBboxEdges("y"); + const dm = node._dragMode; + bbox = { ...bbox, + ...(dm === "resize-t" && { startY: snapCoord(bbox.startY, snap.y, scale.y, ch, yEdges) }), + ...(dm === "resize-b" && { endY: snapCoord(bbox.endY, snap.y, scale.y, ch, yEdges) }), + ...(dm === "resize-l" && { startX: snapCoord(bbox.startX, snap.x, scale.x, cw, xEdges) }), + ...(dm === "resize-r" && { endX: snapCoord(bbox.endX, snap.x, scale.x, cw, xEdges) }), + }; + } else { + bbox = snapBbox(bbox); + } + } + + node._bboxes[node._activeIdx] = bbox; + drawCanvas(); + e.preventDefault(); + } + + function onDragEnd(e) { + if (!node._drawing) return; + const wasRotate = node._dragMode === "rotate"; + const wasPadDrag = node._dragMode === "pad_drag"; + Object.assign(node, { _drawing: false, _dragMode: null, _dragStart: null, + _bboxAtDragStart: null, _padDragStart: null, _extraPadStart: null }); + // Flush deferred pad_x/pad_y widget values + if (wasPadDrag) setPadXY(_framePadXY.x, _framePadXY.y); + document.removeEventListener("mousemove", onDragMove); + document.removeEventListener("mouseup", onDragEnd); + + // Snap canvas to exact rotated dimensions on release (during drag we use a threshold to avoid jitter) + if (wasRotate) { + restoreCanvasToImage(); + fitNodeToImage(false); + } + + if (node._activeIdx >= 0) { + const bbox = node._bboxes[node._activeIdx]; + const { x1, y1, x2, y2 } = normBbox(bbox); + if (x2 - x1 < 3 && y2 - y1 < 3) { + // Too small — remove it + removeBbox(node._activeIdx); + } else { + node._bboxes[node._activeIdx] = { startX: x1, startY: y1, endX: x2, endY: y2 }; + } + } + updateBboxWidgets(); + drawCanvas(); + } + + canvasEl.addEventListener("contextmenu", (e) => { e.preventDefault(); e.stopPropagation(); }); + + canvasEl.tabIndex = 0; + canvasEl.style.outline = "none"; + canvasEl.addEventListener("keydown", (e) => { + if (node._activeIdx < 0) return; + const arrows = { ArrowLeft: [-1, 0], ArrowRight: [1, 0], ArrowUp: [0, -1], ArrowDown: [0, 1] }; + const dir = arrows[e.key]; + if (!dir) { + // Delete key removes active bbox + if (e.key === "Delete" || e.key === "Backspace") { + e.preventDefault(); + e.stopPropagation(); + removeBbox(node._activeIdx); + updateBboxWidgets(); + drawCanvas(); + } + // Tab cycles through bboxes + if (e.key === "Tab" && node._bboxes.length > 1) { + e.preventDefault(); + e.stopPropagation(); + node._activeIdx = (node._activeIdx + (e.shiftKey ? -1 : 1) + node._bboxes.length) % node._bboxes.length; + drawCanvas(); + } + return; + } + e.preventDefault(); + e.stopPropagation(); + + const snap = getEffectiveSnap(); + const step = e.ctrlKey ? (snap.x > 1 ? snap.x / getCanvasScale().x : 10) : 1; + const stepY = e.ctrlKey ? (snap.y > 1 ? snap.y / getCanvasScale().y : 10) : 1; + const dx = dir[0] * step; + const dy = dir[1] * stepY; + const bbox = node._bboxes[node._activeIdx]; + const { x1, y1, x2, y2 } = normBbox(bbox); + const w = x2 - x1, h = y2 - y1; + const nx = Math.max(0, Math.min(canvasEl.width - w, x1 + dx)); + const ny = Math.max(0, Math.min(canvasEl.height - h, y1 + dy)); + node._bboxes[node._activeIdx] = { startX: nx, startY: ny, endX: nx + w, endY: ny + h }; + updateBboxWidgets(); + drawCanvas(); + }); + + function updateBboxWidgets() { + const data = {}; + if (node._rotation !== 0) data.rotation = node._rotation; + data.fillColor = getFillColor(); + if (node._bboxes.length > 0) { + data.bboxes = node._bboxes.map((bbox) => { + const b = Object.assign({}, bbox); + if (node._previewImg) { + b.previewWidth = canvasEl.width; + b.previewHeight = canvasEl.height; + } + return b; + }); + } + bboxWidget.value = Object.keys(data).length > 0 ? JSON.stringify(data) : ""; + } + + let _resizing = false; + function fitNodeToCanvas() { + if (canvasEl.width <= 0 || canvasEl.height <= 0) return; + // If in a no-bbox mode with target dims, resize canvas to output AR + const kp = getKp(); + const { w: tgtW, h: tgtH } = getTgt(); + const hasBboxes = node._bboxes.length > 0; + if (!hasBboxes && node._previewImg && node._previewEnabled && node._rotation === 0 && (tgtW > 0 || tgtH > 0) && + (kp === "stretch" || isPadMode(kp))) { + const { w: imgW, h: imgH } = getEffectiveImageDims(kp !== "pad_edge"); + let rw = tgtW > 0 ? tgtW : imgW; + let rh = tgtH > 0 ? tgtH : imgH; + const outAR = rw / rh; + const newH = Math.round(canvasEl.width / outAR); + if (Math.abs(newH - canvasEl.height) > 2) { + canvasEl.height = newH; + canvasEl.style.aspectRatio = `${canvasEl.width} / ${newH}`; + _rotCacheKey = ""; + } + } else if (node._previewImg && node._previewEnabled) { + // Restore to image AR (including pad-first extra padding if active) + let { w: iw, h: ih } = getEffectiveImageDims(true); + const ep = getExtraPadding(); + if (ep.isFirst && (ep.top + ep.bottom + ep.left + ep.right) > 0) { + iw += ep.left + ep.right; + ih += ep.top + ep.bottom; + } + const imgAR = iw / ih; + const newH = Math.round(canvasEl.width / imgAR); + if (Math.abs(newH - canvasEl.height) > 2) { + rescaleBboxesTo(canvasEl.width, newH); + canvasEl.height = newH; + canvasEl.style.aspectRatio = `${canvasEl.width} / ${newH}`; + _rotCacheKey = ""; + } + } + // Update widget height but don't force node size — only grow if needed + const nodeW = node.size[0]; + const displayedW = nodeW - 30; + const displayedH = Math.round(canvasEl.height * (displayedW / canvasEl.width)); + node._widgetHeight = displayedH + GRID_BAR_HEIGHT; + } + + // Bbox coords are stored in current canvas-pixel space. Whenever the canvas + // buffer is resized (preview load, node resize, AR change) the boxes must be + // rescaled by the same ratio so they stay locked to the image content. + function rescaleBboxesTo(newW, newH) { + const oldW = canvasEl.width, oldH = canvasEl.height; + if (!oldW || !oldH || !newW || !newH || !node._bboxes.length) return; + if (oldW === newW && oldH === newH) return; + const sx = newW / oldW, sy = newH / oldH; + node._bboxes = node._bboxes.map((b) => ({ + startX: b.startX * sx, startY: b.startY * sy, + endX: b.endX * sx, endY: b.endY * sy, + })); + } + + function setCanvasSize(cw, ch) { + rescaleBboxesTo(cw, ch); + canvasEl.width = cw; + canvasEl.height = ch; + canvasEl.style.aspectRatio = `${cw} / ${ch}`; + node.properties.canvasSize = [cw, ch]; + node._widgetHeight = ch + GRID_BAR_HEIGHT; + if (node.graph) node.graph.setDirtyCanvas(true, true); + fitNodeToCanvas(); + } + + function updateCanvasFromTargetDims() { + if (node._previewImg && node._previewEnabled) return; + const divBy = getDivBy(); + const { w: twV, h: thV } = getTgt(); + let tw = roundDown(twV, divBy); + let th = roundDown(thV, divBy); + const [cw, ch] = clampToMaxCanvas(tw > 0 ? Math.max(tw, 200) : 400, th > 0 ? Math.max(th, 150) : 300); + if (cw !== canvasEl.width || ch !== canvasEl.height) { + setCanvasSize(cw, ch); + } + } + + function getCanvasScale() { + if (node._previewImg && node._previewEnabled) { + if (node._rotation !== 0) { + // When rotated, canvas is sized to rotated bounds — uniform scale + const { w: rotW } = getEffectiveImageDims(true); + const s = rotW / canvasEl.width; // same as rotH / canvasEl.height + return { x: s, y: s }; + } + let nw = node._previewImg.naturalWidth; + let nh = node._previewImg.naturalHeight; + // For "pad first" modes, bbox coords map to padded image dimensions + const ep = getExtraPadding(); + if (ep.isFirst && hasExtraPad(ep)) { + nw += ep.left + ep.right; + nh += ep.top + ep.bottom; + } + return { x: nw / canvasEl.width, y: nh / canvasEl.height }; + } + const { w: tw, h: th } = getTgt(); + return { x: tw > 0 ? tw / canvasEl.width : 1, y: th > 0 ? th / canvasEl.height : 1 }; + } + + function getOutputDims(bbox) { + if (!bbox) return null; + const { x: sX, y: sY } = getCanvasScale(); + const { x1, y1, x2, y2 } = normBbox(bbox); + const cropW = Math.round((x2 - x1) * sX); + const cropH = Math.round((y2 - y1) * sY); + const dims = computeOutputDims(cropW, cropH); + return dims ? { w: Math.max(1, dims.w), h: Math.max(1, dims.h) } : null; + } + + function getGridParams() { + const gridSize = getGridSize(); + if (gridSize <= 1) return null; + + let tw, th; + if (node._previewImg && node._previewEnabled) { + const dims = getEffectiveImageDims(true); + tw = dims.w; + th = dims.h; + } else { + tw = twWidget ? twWidget.value : canvasEl.width; + th = thWidget ? thWidget.value : canvasEl.height; + } + const scaleX = canvasEl.width / tw; + const scaleY = canvasEl.height / th; + if (gridSize * scaleX < 4 && gridSize * scaleY < 4) return null; + // Per-dimension effective grid: no partial cells, cells are near-square + const cellsX = Math.max(1, Math.round(tw / gridSize)); + const cellsY = Math.max(1, Math.round(th / gridSize)); + const effGridW = tw / cellsX; + const effGridH = th / cellsY; + return { gridSize, tw, th, scaleX, scaleY, effGridW, effGridH }; + } + + function drawGrid(ctx, alpha, clipBbox, color) { + const g = getGridParams(); + if (!g) return; + const { tw, th, scaleX, scaleY, effGridW, effGridH } = g; + + ctx.save(); + if (clipBbox) { + const { x1, y1, x2, y2 } = normBbox(clipBbox); + ctx.beginPath(); + ctx.rect(x1, y1, x2 - x1, y2 - y1); + ctx.clip(); + } + + const showPreview = node._previewImg && node._previewEnabled; + const defaultAlpha = showPreview ? 0.25 : 0.1; + if (color) { + ctx.strokeStyle = color; + } else { + const { r, g, b } = getGridColorRGB(); + ctx.strokeStyle = `rgba(${r}, ${g}, ${b}, ${alpha ?? defaultAlpha})`; + } + ctx.lineWidth = 1; + ctx.beginPath(); + for (let rx = effGridW; rx < tw - 0.5; rx += effGridW) { + const px = Math.round(rx * scaleX) + 0.5; + ctx.moveTo(px, 0); + ctx.lineTo(px, canvasEl.height); + } + for (let ry = effGridH; ry < th - 0.5; ry += effGridH) { + const py = Math.round(ry * scaleY) + 0.5; + ctx.moveTo(0, py); + ctx.lineTo(canvasEl.width, py); + } + ctx.stroke(); + ctx.restore(); + } + + function drawRotationCross(ctx) { + if (!node._showRotationCross) return; + const { width: cw, height: ch } = canvasEl; + const armLen = Math.sqrt(cw * cw + ch * ch) / 2; + const rad = rotRad(); + const isActive = node._rotation !== 0; + ctx.save(); + ctx.translate(cw / 2, ch / 2); + ctx.rotate(rad); + ctx.beginPath(); + ctx.moveTo(-armLen, 0); ctx.lineTo(armLen, 0); + ctx.moveTo(0, -armLen); ctx.lineTo(0, armLen); + // Black outline + Object.assign(ctx, { strokeStyle: "rgba(0, 0, 0, 0.6)", lineWidth: isActive ? 5 : 4 }); + ctx.stroke(); + // Orange fill + Object.assign(ctx, { strokeStyle: "rgba(255, 160, 30, 0.8)", lineWidth: isActive ? 3 : 2 }); + ctx.stroke(); + if (isActive) { + ctx.rotate(-rad); + const label = `${node._rotation.toFixed(1)}°`; + Object.assign(ctx, { font: "12px sans-serif", textAlign: "center" }); + const lw = ctx.measureText(label).width + 8; + ctx.fillStyle = "rgba(0, 0, 0, 0.6)"; + ctx.beginPath(); ctx.roundRect(-lw / 2, -34, lw, 16, 3); ctx.fill(); + ctx.fillStyle = "rgba(255, 160, 30, 1)"; + ctx.fillText(label, 0, -20); + } + ctx.restore(); + } + + function drawBbox(ctx, bbox, index, isActive) { + const { x1, y1, x2, y2 } = normBbox(bbox); + const w = x2 - x1, h = y2 - y1; + const color = getBboxColor(index, isActive); + const invertEnabled = icWidget ? icWidget.value === "enabled" : false; + if (isActive) { + if (invertEnabled) { + ctx.fillStyle = getFillColor(); ctx.fillRect(x1, y1, w, h); + } else { + ctx.fillStyle = "rgba(0, 0, 0, 0.5)"; ctx.beginPath(); + ctx.rect(0, 0, canvasEl.width, canvasEl.height); ctx.rect(x1, y1, w, h); ctx.fill("evenodd"); + } + } + drawGrid(ctx, null, bbox, color.gridColor); + if (!isActive) { ctx.fillStyle = color.tint; ctx.fillRect(x1, y1, w, h); } + + if (isActive && !invertEnabled) { + const { x: sX, y: sY } = getCanvasScale(); + const cropW = Math.round(w * sX); + const cropH = Math.round(h * sY); + const { w: tgtW, h: tgtH } = getTgt(); + const kp = getKp(); + + if (kp === "multiplier") { + const wmv = _getSubWidget("keep_proportion.width_mult")?.value || 1.0; + const hmv = _getSubWidget("keep_proportion.height_mult")?.value || 1.0; + if (wmv !== 1.0 || hmv !== 1.0) { + drawStretchOverlay(ctx, (cropW * wmv) / (cropH * hmv), x1, y1, w, h); + } + } + + if ((tgtW > 0 || tgtH > 0) && kp === "stretch") { + const { rw, rh } = getEffectiveTargetDims(cropW, cropH); + drawStretchOverlay(ctx, rw / rh, x1, y1, w, h); + } + + if ((tgtW > 0 || tgtH > 0) && (kp === "crop" || isPadMode(kp))) { + const { rw, rh } = getEffectiveTargetDims(cropW, cropH); + + if (kp === "crop") { + const ratio = Math.max(rw / cropW, rh / cropH); + const visW = rw / ratio, visH = rh / ratio; + const offX = (cropW - visW) / 2, offY = (cropH - visH) / 2; + const vx1 = x1 + offX / sX, vy1 = y1 + offY / sY; + const vx2 = x1 + (offX + visW) / sX, vy2 = y1 + (offY + visH) / sY; + dimSides(ctx, x1, y1, w, h, vx1, vy1, vx2 - vx1, vy2 - vy1); + drawDashedBorder(ctx, vx1, vy1, vx2 - vx1, vy2 - vy1, "rgba(255, 255, 100, 0.8)"); + } else if (isPadMode(kp)) { + const { pW, pH, pX, pY, cW, cH, cX, cY } = computePadLayout(rw, rh, cropW, cropH, w, h, x1, y1); + ctx.fillStyle = "rgba(0, 0, 0, 0.7)"; + ctx.fillRect(x1, y1, w, h); + if (kp === "pad_edge" && node._previewEnabled && node._previewImg) { + renderToTmp(Math.max(1, Math.round(cW)), Math.max(1, Math.round(cH)), + node._previewImg, x1 * sX, y1 * sY, w * sX, h * sY); + drawExtraPadFill(ctx, getEdgeEp(), pX, pY, pW, pH, cX, cY, cW, cH, _tmpCanvas); + } else if (kp === "pad_color") { + ctx.fillStyle = getFillColor(); + ctx.fillRect(pX, pY, pW, pH); + } + if (node._previewEnabled) { + drawPreviewRegion(ctx, x1, y1, w, h, cX, cY, cW, cH); + } + drawDashedBorder(ctx, cX, cY, cW, cH, "rgba(100, 200, 255, 0.8)"); + } + } + + const epBbox = getExtraPadding(); + const padModeHandledBbox = isPadMode(kp) && (tgtW > 0 || tgtH > 0); + if (hasExtraPad(epBbox) && !padModeHandledBbox && !invertEnabled && !epBbox.isFirst && node._previewImg) { + const epContent = computeContentDims(cropW, cropH); + const epCW = epContent ? epContent.w : cropW, epCH = epContent ? epContent.h : cropH; + const totalW2 = epCW + epBbox.left + epBbox.right, totalH2 = epCH + epBbox.top + epBbox.bottom; + const { pW: pW2, pH: pH2, pX: pX2, pY: pY2 } = fitAspect(totalW2 / totalH2, w, h, x1, y1); + ctx.fillStyle = "rgba(0, 0, 0, 0.7)"; ctx.fillRect(x1, y1, w, h); + const cW2 = pW2 * (epCW / totalW2), cH2 = pH2 * (epCH / totalH2); + const cX2 = pX2 + pW2 * (epBbox.left / totalW2), cY2 = pY2 + pH2 * (epBbox.top / totalH2); + // Draw crop fitted within content area (letterbox, don't stretch) + const cropAR = cropW / cropH; + const { pW: iW2, pH: iH2, pX: iX2, pY: iY2 } = fitAspect(cropAR, cW2, cH2, cX2, cY2); + const riW2 = Math.max(1, Math.round(iW2)), riH2 = Math.max(1, Math.round(iH2)); + renderToTmp(riW2, riH2, node._previewImg, x1 * sX, y1 * sY, w * sX, h * sY); + drawExtraPadFill(ctx, epBbox, pX2, pY2, pW2, pH2, cX2, cY2, cW2, cH2, _tmpCanvas); + drawPreviewRegion(ctx, x1, y1, w, h, iX2, iY2, iW2, iH2); + drawDashedBorder(ctx, cX2, cY2, cW2, cH2, "rgba(100, 200, 255, 0.8)"); + } + + const { w: outW, h: outH } = getOutputDims(bbox) || { w: cropW, h: cropH }; + const boxLabel = node._bboxes.length > 1 ? `[${index + 1}] ` : ""; + const label = (tgtW > 0 || tgtH > 0) + ? `${boxLabel}${cropW}×${cropH} → ${outW}×${outH}` + : `${boxLabel}${cropW} × ${cropH}`; + ctx.font = "12px sans-serif"; + const lw = ctx.measureText(label).width + 12; + const lh = 20; + const ly = y1 > lh + 4 ? y1 - lh - 2 : y1 + 4; + ctx.fillStyle = "rgba(0, 0, 0, 0.7)"; + ctx.fillRect(x1, ly, lw, lh); + ctx.fillStyle = "#fff"; + ctx.textAlign = "left"; + ctx.fillText(label, x1 + 6, ly + 14); + } else if (!isActive) { + // Inactive: show index label centered in bbox + const boxLabel = `${index + 1}`; + ctx.font = "bold 20px sans-serif"; + ctx.fillStyle = color.border; + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + ctx.fillText(boxLabel, x1 + w / 2, y1 + h / 2); + ctx.textBaseline = "alphabetic"; + } + + if (isActive) { + const cx = (x1 + x2) / 2, cy = (y1 + y2) / 2; + Object.assign(ctx, { strokeStyle: color.border, lineWidth: 1 }); + ctx.beginPath(); + ctx.moveTo(cx - 5, cy); ctx.lineTo(cx + 5, cy); + ctx.moveTo(cx, cy - 5); ctx.lineTo(cx, cy + 5); + ctx.stroke(); + } + if (!isActive) { ctx.strokeStyle = "rgba(0, 0, 0, 0.6)"; ctx.lineWidth = 4; ctx.strokeRect(x1, y1, w, h); } + ctx.strokeStyle = color.border; ctx.lineWidth = 2; ctx.strokeRect(x1, y1, w, h); + + ctx.fillStyle = color.handle; + const hs = isActive ? 5 : 4, s2 = hs * 2; + const cx1 = Math.max(hs, Math.min(canvasEl.width - hs, x1)); + const cx2 = Math.max(hs, Math.min(canvasEl.width - hs, x2)); + const cy1 = Math.max(hs, Math.min(canvasEl.height - hs, y1)); + const cy2 = Math.max(hs, Math.min(canvasEl.height - hs, y2)); + for (const [hx, hy] of [[cx1, cy1], [cx2, cy1], [cx1, cy2], [cx2, cy2]]) + ctx.fillRect(hx - hs, hy - hs, s2, s2); + } + + function drawCanvas() { + const ctx = canvasCtx; + ctx.clearRect(0, 0, canvasEl.width, canvasEl.height); + _frameEp = _readExtraPadding(); + if (node._dragMode !== "pad_drag") _framePadXY = _readPadXY(); + const _kpVal = getKp(), { w: _tgtW, h: _tgtH } = getTgt(); + const hasBboxes = node._bboxes.length > 0; + const showPreview = node._previewImg && node._previewEnabled; + const _skipFullImage = !hasBboxes && showPreview && isPadMode(_kpVal) && (_tgtW > 0 || _tgtH > 0); + + const _epBase = getExtraPadding(); + const _isFirst = _epBase.isFirst, _hasFirstPad = _isFirst && hasExtraPad(_epBase); + const maybeResize = (cw, ch) => { + if (cw !== canvasEl.width || ch !== canvasEl.height) { setCanvasSize(cw, ch); ctx.clearRect(0, 0, cw, ch); } + }; + if (showPreview) { + if (_hasFirstPad) { + const { w: eiw, h: eih } = getEffectiveImageDims(_epBase.mode !== "pad_edge"); + maybeResize(...clampToMaxCanvas(eiw + _epBase.left + _epBase.right, eih + _epBase.top + _epBase.bottom)); + } else if (node._previewImg) { + maybeResize(...clampToMaxCanvas(...Object.values(getEffectiveImageDims(true)))); + } + } + + if (showPreview && !_skipFullImage) { + if (_hasFirstPad) { + const { w: epImgW, h: epImgH } = getEffectiveImageDims(_epBase.mode !== "pad_edge"); + const totalW = epImgW + _epBase.left + _epBase.right, totalH = epImgH + _epBase.top + _epBase.bottom; + const cX = canvasEl.width * (_epBase.left / totalW), cY = canvasEl.height * (_epBase.top / totalH); + const cW = canvasEl.width * (epImgW / totalW), cH = canvasEl.height * (epImgH / totalH); + const rcW = Math.max(1, Math.round(cW)), rcH = Math.max(1, Math.round(cH)); + // Render content (with replicate-rotate for pad_edge + rotation) + renderToTmp(rcW, rcH); + drawExtraPadFill(ctx, _epBase, 0, 0, canvasEl.width, canvasEl.height, cX, cY, cW, cH, _tmpCanvas); + drawPreviewRegion(ctx, 0, 0, canvasEl.width, canvasEl.height, cX, cY, cW, cH); + } else { + if (node._rotation !== 0) { ctx.fillStyle = getFillColor(); ctx.fillRect(0, 0, canvasEl.width, canvasEl.height); } + const { pW, pH, pX, pY } = fitAspect(_rotCanvas.width / _rotCanvas.height, canvasEl.width, canvasEl.height); + ctx.drawImage(getRotatedPreview(), 0, 0, _rotCanvas.width, _rotCanvas.height, pX, pY, pW, pH); + } + } else if (!showPreview) { + ctx.fillStyle = "#383838"; ctx.fillRect(0, 0, canvasEl.width, canvasEl.height); + const divBy = getDivBy(), { w: lw0, h: lh0 } = getTgt(); + Object.assign(ctx, { fillStyle: "#888", font: "14px sans-serif", textAlign: "center" }); + ctx.fillText(`${roundDown(lw0, divBy) || canvasEl.width} × ${roundDown(lh0, divBy) || canvasEl.height}`, + canvasEl.width / 2, canvasEl.height / 2); + } + + // Manual mode: visualize extra padding on the placeholder + if (!hasBboxes && !showPreview) { + const _ep = getExtraPadding(); + if (hasExtraPad(_ep)) { + const cW0 = canvasEl.width, cH0 = canvasEl.height; + const { w: tw0, h: th0 } = getTgt(); + const rawW = tw0 > 0 ? tw0 : cW0; + const rawH = th0 > 0 ? th0 : cH0; + const content = computeContentDims(rawW, rawH); + const contentW = content ? content.w : rawW; + const contentH = content ? content.h : rawH; + const totalW = contentW + _ep.left + _ep.right; + const totalH = contentH + _ep.top + _ep.bottom; + const { pW, pH, pX, pY } = fitAspect(totalW / totalH, cW0, cH0); + const cW = pW * (contentW / totalW), cH = pH * (contentH / totalH); + const cX = pX + pW * (_ep.left / totalW), cY = pY + pH * (_ep.top / totalH); + ctx.fillStyle = "rgba(0, 0, 0, 0.5)"; + ctx.fillRect(0, 0, cW0, cH0); + if (_ep.mode === "pad_color" || _ep.mode === "pad_crop_color") { + ctx.fillStyle = getFillColor(); + } else { + ctx.fillStyle = "#555"; + } + ctx.fillRect(pX, pY, pW, pH); + ctx.fillStyle = "#383838"; + ctx.fillRect(cX, cY, cW, cH); + drawDashedBorder(ctx, cX, cY, cW, cH, "rgba(100, 200, 255, 0.8)"); + // Redraw dimension text on top + const divBy = getDivBy(), { w: lw0, h: lh0 } = getTgt(); + Object.assign(ctx, { fillStyle: "#888", font: "14px sans-serif", textAlign: "center" }); + ctx.fillText(`${roundDown(lw0, divBy) || cW0} × ${roundDown(lh0, divBy) || cH0}`, cX + cW / 2, cY + cH / 2); + } + } + + // When no bboxes are drawn, show pad/crop preview for the full image (auto mode only) + if (!hasBboxes && node._previewEnabled) { + const cW0 = canvasEl.width, cH0 = canvasEl.height; + if (node._previewImg) { + const { w: tgtW, h: tgtH } = getTgt(); + const kp = getKp(); + // Multiplier mode: compute effective target from crop dims × multiplier + if (kp === "multiplier") { + const wmv = _getSubWidget("keep_proportion.width_mult")?.value || 1.0; + const hmv = _getSubWidget("keep_proportion.height_mult")?.value || 1.0; + if (wmv !== 1.0 || hmv !== 1.0) { + const nw = node._previewImg.naturalWidth; + const nh = node._previewImg.naturalHeight; + drawStretchOverlay(ctx, (nw * wmv) / (nh * hmv), 0, 0, cW0, cH0); + } + } + if ((tgtW > 0 || tgtH > 0) && (kp === "stretch" || isPadMode(kp) || kp === "crop")) { + const { w: imgW, h: imgH } = getEffectiveImageDims(kp !== "pad_edge"); + const { rw, rh } = getEffectiveTargetDims(imgW, imgH); + + if (kp === "stretch") { + drawStretchOverlay(ctx, rw / rh, 0, 0, cW0, cH0); + } else if (isPadMode(kp)) { + const epPad = getExtraPadding(); + const hasEp = hasExtraPad(epPad); + // Full output = target. Content = target - extra padding. + const fullTgtW = tgtW > 0 ? tgtW : imgW; + const fullTgtH = tgtH > 0 ? tgtH : imgH; + const contentTgtW = hasEp ? Math.max(1, fullTgtW - epPad.left - epPad.right) : fullTgtW; + const contentTgtH = hasEp ? Math.max(1, fullTgtH - epPad.top - epPad.bottom) : fullTgtH; + // Fit the full output to the canvas + const { pW: outerW, pH: outerH, pX: outerX, pY: outerY } = fitAspect(fullTgtW / fullTgtH, cW0, cH0); + // Content area within the output (offset by padding) + const contentW = outerW * (contentTgtW / fullTgtW); + const contentH = outerH * (contentTgtH / fullTgtH); + const contentX = outerX + outerW * (epPad.left / fullTgtW); + const contentY = outerY + outerH * (epPad.top / fullTgtH); + // Image is sized to fit the content area, but positioned across the full output via pad_x/pad_y + const imgRatio = Math.min(contentTgtW / imgW, contentTgtH / imgH); + const scaledW = imgW * imgRatio, scaledH = imgH * imgRatio; + const pW = outerW, pH = outerH, pX = outerX, pY = outerY; + const cW = outerW * (scaledW / fullTgtW), cH = outerH * (scaledH / fullTgtH); + const { cX, cY } = computePadContentPos(outerX, outerY, outerW, outerH, cW, cH); + ctx.clearRect(0, 0, cW0, cH0); + // Fill outer area (extra padding region) + if (hasEp) { + ctx.fillStyle = getFillColor(); + ctx.fillRect(outerX, outerY, outerW, outerH); + } + if (kp === "pad_edge") { + renderToTmp(Math.max(1, Math.round(cW)), Math.max(1, Math.round(cH)), + null, 0, 0, 0, 0, true); + ctx.drawImage(_tmpCanvas, 0, 0, _tmpCanvas.width, _tmpCanvas.height, cX, cY, cW, cH); + drawExtraPadFill(ctx, getEdgeEp(), pX, pY, pW, pH, cX, cY, cW, cH, _tmpCanvas); + } else { + ctx.fillStyle = getFillColor(); + ctx.fillRect(pX, pY, pW, pH); + drawPreviewRegion(ctx, 0, 0, cW0, cH0, cX, cY, cW, cH); + } + drawDashedBorder(ctx, cX, cY, cW, cH, "rgba(100, 200, 255, 0.8)"); + if (hasEp) drawDashedBorder(ctx, contentX, contentY, contentW, contentH, "rgba(255, 200, 100, 0.6)"); + } else if (kp === "crop") { + const ratio = Math.max(rw / imgW, rh / imgH); + const visW = rw / ratio, visH = rh / ratio; + const offX = (imgW - visW) / 2, offY = (imgH - visH) / 2; + const sc = Math.min(cW0 / imgW, cH0 / imgH); + const ox = (cW0 - imgW * sc) / 2, oy = (cH0 - imgH * sc) / 2; + const vx1 = ox + offX * sc, vy1 = oy + offY * sc; + const vw = visW * sc, vh = visH * sc; + dimSides(ctx, 0, 0, cW0, cH0, vx1, vy1, vw, vh); + drawDashedBorder(ctx, vx1, vy1, vw, vh, "rgba(255, 255, 100, 0.8)"); + } + } + } + const _ep = getExtraPadding(); + if (hasExtraPad(_ep) && node._previewImg && !_isFirst && !(isPadMode(_kpVal) && (_tgtW > 0 || _tgtH > 0))) { + const { w: rawImgW, h: rawImgH } = getEffectiveImageDims(_ep.mode !== "pad_edge"); + const content = computeContentDims(rawImgW, rawImgH); + const epImgW = content ? content.w : rawImgW; + const epImgH = content ? content.h : rawImgH; + const totalW = epImgW + _ep.left + _ep.right, totalH = epImgH + _ep.top + _ep.bottom; + const { pW, pH, pX, pY } = fitAspect(totalW / totalH, cW0, cH0); + ctx.clearRect(0, 0, cW0, cH0); + const cW = pW * (epImgW / totalW), cH = pH * (epImgH / totalH); + const cX = pX + pW * (_ep.left / totalW), cY = pY + pH * (_ep.top / totalH); + // Draw padding fill, then image in content area (same approach as keep_proportion pad mode) + if (_ep.mode === "pad_color") { + ctx.fillStyle = getFillColor(); + ctx.fillRect(pX, pY, pW, pH); + drawPreviewRegion(ctx, 0, 0, cW0, cH0, cX, cY, cW, cH); + } else { + // pad_edge: render content for edge fill source + const rcW2 = Math.max(1, Math.round(cW)), rcH2 = Math.max(1, Math.round(cH)); + renderToTmp(rcW2, rcH2, null, 0, 0, 0, 0, _ep.isFirst); + drawExtraPadFill(ctx, _ep, pX, pY, pW, pH, cX, cY, cW, cH, _tmpCanvas); + drawPreviewRegion(ctx, 0, 0, cW0, cH0, cX, cY, cW, cH); + } + drawDashedBorder(ctx, cX, cY, cW, cH, "rgba(100, 200, 255, 0.8)"); + } + + drawGrid(ctx); + drawRotationCross(ctx); + if (!node._drawing) updateOutputSlotLabels(); + return; + } + + if (node._activeIdx >= 0 && node._activeIdx < node._bboxes.length) { + drawBbox(ctx, node._bboxes[node._activeIdx], node._activeIdx, true); + } + for (let i = 0; i < node._bboxes.length; i++) { + if (i === node._activeIdx) continue; + drawBbox(ctx, node._bboxes[i], i, false); + } + + drawGrid(ctx); + drawRotationCross(ctx); + if (!node._drawing) updateOutputSlotLabels(); + } + + let _lastSlotLabelKey = ""; + function updateOutputSlotLabels() { + const activeBbox = (node._activeIdx >= 0 && node._activeIdx < node._bboxes.length) + ? node._bboxes[node._activeIdx] : null; + let dims = activeBbox ? getOutputDims(activeBbox) : null; + if (!dims) { + const { w: tW, h: tH } = getTgt(); + const imgW = node._previewImg ? node._previewImg.naturalWidth + : (tW > 0 ? tW : canvasEl.width); + const imgH = node._previewImg ? node._previewImg.naturalHeight + : (tH > 0 ? tH : canvasEl.height); + const computed = computeOutputDims(imgW, imgH); + if (computed && computed.w > 0 && computed.h > 0) { + dims = computed; + } + } + // Skip expensive DOM/canvas updates if nothing changed + const key = `${dims?.w || 0},${dims?.h || 0},${node._bboxes.length}`; + if (key === _lastSlotLabelKey) return; + _lastSlotLabelKey = key; + if (node.outputs) { + const hasBboxes = node._bboxes.length > 0; + for (let i = 0; i < node.outputs.length; i++) { + const out = node.outputs[i]; + if (!out._origName) { + if (out.name?.includes("width") || out.label?.includes("width")) out._origName = "width"; + else if (out.name?.includes("height") || out.label?.includes("height")) out._origName = "height"; + else if (out.name === "bbox") out._origName = "bbox"; + else if (out.name === "bbox_mask") out._origName = "bbox_mask"; + } + if (out._origName === "width") { + out.label = dims ? `${dims.w} width` : "width"; + } else if (out._origName === "height") { + out.label = dims ? `${dims.h} height` : "height"; + } + // Identify output/output_mask slots + if (!out._origName) { + if (out.name === "cropped" || out.name === "output") out._origName = "output"; + else if (out.name === "cropped_mask" || out.name === "output_mask") out._origName = "output_mask"; + } + // Toggle list icon on output/output_mask + if (out._origName === "output" || out._origName === "output_mask") { + out.shape = hasBboxes && node._bboxes.length > 1 ? 6 : undefined; + } + // Visually hide bbox/bbox_mask slots when no bboxes, show with list icon when multiple + if (out._origName === "bbox" || out._origName === "bbox_mask") { + if (!hasBboxes) { + out.label = " "; + out.color_off = "transparent"; + out.color_on = "transparent"; + out.shape = 6; + out.dir = undefined; + } else { + out.label = out._origName; + out.color_off = undefined; + out.color_on = undefined; + out.shape = node._bboxes.length > 1 ? 6 : undefined; + } + } + } + node.setDirtyCanvas(true, true); + // Vue nodes mode: hide/show bbox slots via DOM since canvas properties don't apply + if (LiteGraph.vueNodesMode && node.id != null) { + const nodeEl = document.querySelector(`[data-node-id="${node.id}"]`); + if (nodeEl) { + const outputSlots = nodeEl.querySelectorAll(".lg-slot--output"); + for (let i = 0; i < node.outputs.length; i++) { + const out = node.outputs[i]; + if ((out._origName === "bbox" || out._origName === "bbox_mask") && outputSlots[i]) { + outputSlots[i].style.visibility = hasBboxes ? "" : "hidden"; + // Update label text directly since Vue may not observe raw property changes + if (hasBboxes) { + const labelEl = outputSlots[i].querySelector("span"); + if (labelEl) labelEl.textContent = out._origName; + } + } + } + } + } + } + } + + function loadPreviewImage(src, preserveBbox) { + const img = new Image(); + img.crossOrigin = "anonymous"; + img.onload = () => { + node._previewImg = img; + _rotCacheKey = ""; + invalidateGridStops(); + restoreCanvasToImage(); + if (node._previewEnabled && !preserveBbox) { + node._bboxes = []; + node._activeIdx = -1; + } + updateBboxWidgets(); + drawCanvas(); + fitNodeToImage(preserveBbox); + }; + img.onerror = () => { + console.warn("ImageTransformKJ: failed to load preview:", src); + }; + img.src = src; + } + + function loadPreviewFromTempFile(filename) { + if (!filename) return; + loadPreviewImage(`/view?filename=${encodeURIComponent(filename)}&type=temp`, true); + } + + function captureVideoFrameToPreview(videoEl, preserveBbox) { + captureVideoFrame(videoEl, (canvas) => { + loadPreviewImage(canvas.toDataURL("image/webp", 0.9), preserveBbox); + }); + } + + function handleSourceChange(sources, preserveBbox) { + if (sources.length === 0) { + // Disconnected + node._previewImg = null; + delete node.properties.previewFile; + if (node._previewEnabled) { + node._bboxes = []; + node._activeIdx = -1; + updateBboxWidgets(); + } + updateCanvasFromTargetDims(); + drawCanvas(); + return; + } + const src = sources[0]; + if (src.isVideo && src.videoEl) { + captureVideoFrameToPreview(src.videoEl, preserveBbox); + } else if (src.url) { + loadPreviewImage(src.url, preserveBbox); + } + } + + let _configuring = false, _sourceResolved = false; + watchImageInputs(node, "image", (sources) => { + _sourceResolved = true; + handleSourceChange(sources, _configuring); + }); + + chainCallback(this, "onResize", function () { + if (_resizing) return; + const availW = Math.max(100, node.size[0] - 30); + _resizing = true; + if (node._previewEnabled) { + // Auto mode: resize canvas pixel buffer to match node width + const ar = canvasEl.height / canvasEl.width; + const newW = Math.round(availW); + const newH = Math.round(availW * ar); + if (newW !== canvasEl.width || newH !== canvasEl.height) { + rescaleBboxesTo(newW, newH); + canvasEl.width = newW; + canvasEl.height = newH; + canvasEl.style.aspectRatio = `${newW} / ${newH}`; + node.properties.canvasSize = [newW, newH]; + } + node._widgetHeight = newH + GRID_BAR_HEIGHT; + } else { + // Manual mode: keep pixel buffer fixed, CSS handles display scaling + const displayedH = Math.round(canvasEl.height * (availW / canvasEl.width)); + node._widgetHeight = displayedH + GRID_BAR_HEIGHT; + } + drawCanvas(); + _resizing = false; + }); + + chainCallback(this, "onConfigure", function () { + node._cropConfigured = true; + if (node.properties.gridSize !== undefined) { + gridSlider.value = node.properties.gridSize; + updateGridLabel(); + } + if (node.properties.previewEnabled !== undefined) { + node._previewEnabled = node.properties.previewEnabled; + updatePreviewBtn(); + } + restoreBboxesFromWidget(); + if (node.properties.rotation !== undefined) node._rotation = node.properties.rotation; + if (node.properties.fillColor) { + colorInput.value = node.properties.fillColor; + colorSwatch.style.background = node.properties.fillColor; + } + if (node.properties.gridColor) { + gridColorInput.value = node.properties.gridColor; + gridColorSwatch.style.background = node.properties.gridColor; + } + + if (node.properties.canvasSize) { + setCanvasSize(node.properties.canvasSize[0], node.properties.canvasSize[1]); + } + + _configuring = true; + _sourceResolved = false; + setTimeout(() => { + if (node.properties.gridSize !== undefined) { + gridSlider.value = node.properties.gridSize; + updateGridLabel(); + } + _configuring = false; + if (!_sourceResolved) { + if (node.properties.previewFile) { + loadPreviewFromTempFile(node.properties.previewFile); + } else { + updateCanvasFromTargetDims(); + drawCanvas(); + } + } + }, 200); + }); + + chainCallback(this, "onExecuted", function (message) { + const filename = message?.preview_filename?.[0]; + if (filename) { + node.properties.previewFile = filename; + loadPreviewFromTempFile(filename); + } + }); + + // Initial draw + setCanvasSize(400, 300); + drawCanvas(); + }); + }, +}); diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/web/js/jsnodes.js b/ComfyUI/custom_nodes/ComfyUI-KJNodes/web/js/jsnodes.js new file mode 100644 index 0000000000000000000000000000000000000000..a38556a37824c7ce6b70c31c26f6e14fffafba4e --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-KJNodes/web/js/jsnodes.js @@ -0,0 +1,365 @@ +const { app } = window.comfyAPI.app; +const { applyTextReplacements } = window.comfyAPI.utils; + +// ─── Dynamic input slot management for *Multi nodes ─── +// Adds an "Update inputs" button that rebuilds prefix_1..prefix_N slots to match the count widget. +// Also wired to the count callback so API-format reload (bare callback) restores the slots; the +// canvas arg from interactive edits is the tell, so scrubbing the count doesn't reflow the node. +function setupDynamicInputs(node, { type, prefix, countWidget = "inputcount", slotOptions } = {}) { + const rebuild = () => { + if (!node.inputs) node.inputs = []; + const countW = node.widgets?.find(w => w.name === countWidget); + if (!countW) return; + const target = countW.value; + const current = node.inputs.filter(i => i.name?.startsWith(prefix)).length; + if (target === current) return; + if (target < current) { + for (let i = 0; i < current - target; i++) node.removeInput(node.inputs.length - 1); + } else { + for (let i = current + 1; i <= target; i++) node.addInput(`${prefix}${i}`, type, slotOptions); + } + }; + node.addWidget("button", "Update inputs", null, rebuild); + const countW = node.widgets?.find(w => w.name === countWidget); + if (countW) { + const origCb = countW.callback; // guard: may be nullish + countW.callback = function (value, canvas) { + const r = origCb ? origCb.apply(this, arguments) : undefined; + if (!canvas) rebuild(); // bare = API reload; skip interactive scrub + return r; + }; + } + return rebuild; +} + +app.registerExtension({ + name: "KJNodes.jsnodes", + async beforeRegisterNodeDef(nodeType, nodeData, app) { + if(!nodeData?.category?.startsWith("KJNodes")) { + return; + } + switch (nodeData.name) { + case "ConditioningMultiCombine": + nodeType.prototype.onNodeCreated = function () { + this.inputs_offset = nodeData.name.includes("selective")?1:0 + setupDynamicInputs(this, { type: "CONDITIONING", prefix: "conditioning_" }); + } + break; + case "ImageBatchMulti": + case "ImageAddMulti": + case "CrossFadeImagesMulti": + case "TransitionImagesMulti": + nodeType.prototype.onNodeCreated = function () { + setupDynamicInputs(this, { type: "IMAGE", prefix: "image_", slotOptions: {shape: 7} }); + } + break; + case "ImageConcatMulti": + // Dynamic slots accept MASK too; name-prefix counting handles the mixed types. + nodeType.prototype.onNodeCreated = function () { + setupDynamicInputs(this, { type: "IMAGE,MASK", prefix: "image_", slotOptions: {shape: 7} }); + } + break; + case "MaskBatchMulti": + nodeType.prototype.onNodeCreated = function () { + setupDynamicInputs(this, { type: "MASK", prefix: "mask_" }); + } + break; + + case "FluxBlockLoraSelect": + case "HunyuanVideoBlockLoraSelect": + case "Wan21BlockLoraSelect": + case "LTX2BlockLoraSelect": + nodeType.prototype.onNodeCreated = function () { + this.addWidget("button", "Set all", null, () => { + const userInput = prompt("Enter the values to set for widgets (e.g., s0,1,2-7=2.0, d0,1,2-7=2.0, or 1.0):", ""); + if (userInput) { + const regex = /([sd])?(\d+(?:,\d+|-?\d+)*?)?=(\d+(\.\d+)?)/; + const match = userInput.match(regex); + if (match) { + const type = match[1]; + const indicesPart = match[2]; + const value = parseFloat(match[3]); + + let targetWidgets = []; + if (type === 's') { + targetWidgets = this.widgets.filter(widget => widget.name.includes("single")); + } else if (type === 'd') { + targetWidgets = this.widgets.filter(widget => widget.name.includes("double")); + } else { + targetWidgets = this.widgets; // No type specified, all widgets + } + + if (indicesPart) { + const indices = indicesPart.split(',').flatMap(part => { + if (part.includes('-')) { + const [start, end] = part.split('-').map(Number); + return Array.from({ length: end - start + 1 }, (_, i) => start + i); + } + return Number(part); + }); + + for (const index of indices) { + if (index < targetWidgets.length) { + targetWidgets[index].value = value; + } + } + } else { + // No indices provided, set value for all target widgets + for (const widget of targetWidgets) { + widget.value = value; + } + } + } else if (!isNaN(parseFloat(userInput))) { + // Single value provided, set it for all widgets + const value = parseFloat(userInput); + for (const widget of this.widgets) { + widget.value = value; + } + } else { + alert("Invalid input format. Please use the format s0,1,2-7=2.0, d0,1,2-7=2.0, or 1.0"); + } + } else { + alert("Invalid input. Please enter a value."); + } + }); + }; + break; + + case "GetMaskSizeAndCount": + const onGetMaskSizeConnectInput = nodeType.prototype.onConnectInput; + nodeType.prototype.onConnectInput = function (targetSlot, type, output, originNode, originSlot) { + const v = onGetMaskSizeConnectInput? onGetMaskSizeConnectInput.apply(this, arguments): undefined + this.outputs[1]["label"] = "width" + this.outputs[2]["label"] = "height" + this.outputs[3]["label"] = "count" + return v; + } + const onGetMaskSizeExecuted = nodeType.prototype.onAfterExecuteNode; + nodeType.prototype.onExecuted = function(message) { + const r = onGetMaskSizeExecuted? onGetMaskSizeExecuted.apply(this,arguments): undefined + let values = message["text"].toString().split('x').map(Number); + this.outputs[1]["label"] = values[1] + " width" + this.outputs[2]["label"] = values[2] + " height" + this.outputs[3]["label"] = values[0] + " count" + return r + } + break; + + case "GetImageSizeAndCount": + const onGetImageSizeConnectInput = nodeType.prototype.onConnectInput; + nodeType.prototype.onConnectInput = function (targetSlot, type, output, originNode, originSlot) { + console.log(this) + const v = onGetImageSizeConnectInput? onGetImageSizeConnectInput.apply(this, arguments): undefined + //console.log(this) + this.outputs[1]["label"] = "width" + this.outputs[2]["label"] = "height" + this.outputs[3]["label"] = "count" + return v; + } + //const onGetImageSizeExecuted = nodeType.prototype.onExecuted; + const onGetImageSizeExecuted = nodeType.prototype.onAfterExecuteNode; + nodeType.prototype.onExecuted = function(message) { + console.log(this) + const r = onGetImageSizeExecuted? onGetImageSizeExecuted.apply(this,arguments): undefined + let values = message["text"].toString().split('x').map(Number); + console.log(values) + this.outputs[1]["label"] = values[1] + " width" + this.outputs[2]["label"] = values[2] + " height" + this.outputs[3]["label"] = values[0] + " count" + return r + } + break; + + case "GetLatentSizeAndCount": + const onGetLatentConnectInput = nodeType.prototype.onConnectInput; + nodeType.prototype.onConnectInput = function (targetSlot, type, output, originNode, originSlot) { + console.log(this) + const v = onGetLatentConnectInput? onGetLatentConnectInput.apply(this, arguments): undefined + //console.log(this) + this.outputs[1]["label"] = "batch_size" + this.outputs[2]["label"] = "channels" + this.outputs[3]["label"] = "frames" + this.outputs[4]["label"] = "height" + this.outputs[5]["label"] = "width" + return v; + } + //const onGetImageSizeExecuted = nodeType.prototype.onExecuted; + const onGetLatentSizeExecuted = nodeType.prototype.onAfterExecuteNode; + nodeType.prototype.onExecuted = function(message) { + console.log(this) + const r = onGetLatentSizeExecuted? onGetLatentSizeExecuted.apply(this,arguments): undefined + let values = message["text"].toString().split('x').map(Number); + console.log(values) + this.outputs[1]["label"] = values[0] + " batch" + this.outputs[2]["label"] = values[1] + " channels" + this.outputs[3]["label"] = values[2] + " frames" + this.outputs[4]["label"] = values[3] + " height" + this.outputs[5]["label"] = values[4] + " width" + return r + } + break; + + case "PreviewAnimation": + const onPreviewAnimationConnectInput = nodeType.prototype.onConnectInput; + nodeType.prototype.onConnectInput = function (targetSlot, type, output, originNode, originSlot) { + const v = onPreviewAnimationConnectInput? onPreviewAnimationConnectInput.apply(this, arguments): undefined + this.title = "Preview Animation" + return v; + } + const onPreviewAnimationExecuted = nodeType.prototype.onAfterExecuteNode; + nodeType.prototype.onExecuted = function(message) { + const r = onPreviewAnimationExecuted? onPreviewAnimationExecuted.apply(this,arguments): undefined + let values = message["text"].toString(); + this.title = "Preview Animation " + values + return r + } + break; + + case "VRAM_Debug": + const onVRAM_DebugConnectInput = nodeType.prototype.onConnectInput; + nodeType.prototype.onConnectInput = function (targetSlot, type, output, originNode, originSlot) { + const v = onVRAM_DebugConnectInput? onVRAM_DebugConnectInput.apply(this, arguments): undefined + this.outputs[3]["label"] = "freemem_before" + this.outputs[4]["label"] = "freemem_after" + return v; + } + const onVRAM_DebugExecuted = nodeType.prototype.onAfterExecuteNode; + nodeType.prototype.onExecuted = function(message) { + const r = onVRAM_DebugExecuted? onVRAM_DebugExecuted.apply(this,arguments): undefined + let values = message["text"].toString().split('x'); + this.outputs[3]["label"] = values[0] + " freemem_before" + this.outputs[4]["label"] = values[1] + " freemem_after" + return r + } + break; + + case "JoinStringMulti": + const originalOnNodeCreated = nodeType.prototype.onNodeCreated || function() {}; + nodeType.prototype.onNodeCreated = function () { + originalOnNodeCreated.apply(this, arguments); + setupDynamicInputs(this, { type: "STRING", prefix: "string_", slotOptions: {shape: 7} }); + } + break; + case "SoundReactive": + nodeType.prototype.onNodeCreated = function () { + let audioContext; + let microphoneStream; + let animationFrameId; + let analyser; + let dataArray; + let startRangeHz; + let endRangeHz; + let smoothingFactor = 0.5; + let smoothedSoundLevel = 0; + + // Function to update the widget value in real-time + const updateWidgetValueInRealTime = () => { + // Ensure analyser and dataArray are defined before using them + if (analyser && dataArray) { + analyser.getByteFrequencyData(dataArray); + + const startRangeHzWidget = this.widgets.find(w => w.name === "start_range_hz"); + if (startRangeHzWidget) startRangeHz = startRangeHzWidget.value; + const endRangeHzWidget = this.widgets.find(w => w.name === "end_range_hz"); + if (endRangeHzWidget) endRangeHz = endRangeHzWidget.value; + const smoothingFactorWidget = this.widgets.find(w => w.name === "smoothing_factor"); + if (smoothingFactorWidget) smoothingFactor = smoothingFactorWidget.value; + + // Calculate frequency bin width (frequency resolution) + const frequencyBinWidth = audioContext.sampleRate / analyser.fftSize; + // Convert the widget values from Hz to indices + const startRangeIndex = Math.floor(startRangeHz / frequencyBinWidth); + const endRangeIndex = Math.floor(endRangeHz / frequencyBinWidth); + + // Function to calculate the average value for a frequency range + const calculateAverage = (start, end) => { + const sum = dataArray.slice(start, end).reduce((acc, val) => acc + val, 0); + const average = sum / (end - start); + + // Apply exponential moving average smoothing + smoothedSoundLevel = (average * (1 - smoothingFactor)) + (smoothedSoundLevel * smoothingFactor); + return smoothedSoundLevel; + }; + // Calculate the average levels for each frequency range + const soundLevel = calculateAverage(startRangeIndex, endRangeIndex); + + // Update the widget values + + const lowLevelWidget = this.widgets.find(w => w.name === "sound_level"); + if (lowLevelWidget) lowLevelWidget.value = soundLevel; + + animationFrameId = requestAnimationFrame(updateWidgetValueInRealTime); + } + }; + + // Function to start capturing audio from the microphone + const startMicrophoneCapture = () => { + // Only create the audio context and analyser once + if (!audioContext) { + audioContext = new (window.AudioContext || window.webkitAudioContext)(); + // Access the sample rate of the audio context + console.log(`Sample rate: ${audioContext.sampleRate}Hz`); + analyser = audioContext.createAnalyser(); + analyser.fftSize = 2048; + dataArray = new Uint8Array(analyser.frequencyBinCount); + // Get the range values from widgets (assumed to be in Hz) + const lowRangeWidget = this.widgets.find(w => w.name === "low_range_hz"); + if (lowRangeWidget) startRangeHz = lowRangeWidget.value; + + const midRangeWidget = this.widgets.find(w => w.name === "mid_range_hz"); + if (midRangeWidget) endRangeHz = midRangeWidget.value; + } + + navigator.mediaDevices.getUserMedia({ audio: true }).then(stream => { + microphoneStream = stream; + const microphone = audioContext.createMediaStreamSource(stream); + microphone.connect(analyser); + updateWidgetValueInRealTime(); + }).catch(error => { + console.error('Access to microphone was denied or an error occurred:', error); + }); + }; + + // Function to stop capturing audio from the microphone + const stopMicrophoneCapture = () => { + if (animationFrameId) { + cancelAnimationFrame(animationFrameId); + } + if (microphoneStream) { + microphoneStream.getTracks().forEach(track => track.stop()); + } + if (audioContext) { + audioContext.close(); + // Reset audioContext to ensure it can be created again when starting + audioContext = null; + } + }; + + // Add start button + this.addWidget("button", "Start mic capture", null, startMicrophoneCapture); + + // Add stop button + this.addWidget("button", "Stop mic capture", null, stopMicrophoneCapture); + }; + break; + case "SaveImageKJ": + case "SaveImageWithAlpha": + case "SaveStringKJ": + case "DecodeAndSaveVideo": + case "ModelSaveKJ": + case "LoraExtractKJ": + const onNodeCreated = nodeType.prototype.onNodeCreated; + nodeType.prototype.onNodeCreated = function() { + const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : void 0; + const widget = this.widgets.find((w) => w.name === "filename_prefix"); + widget.serializeValue = () => { + return applyTextReplacements(app, widget.value); + }; + return r; + }; + break; + + } + + }, +}); \ No newline at end of file diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/web/js/node_insert.js b/ComfyUI/custom_nodes/ComfyUI-KJNodes/web/js/node_insert.js new file mode 100644 index 0000000000000000000000000000000000000000..468c20265aff1612db6c59339d9db476c50847be --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-KJNodes/web/js/node_insert.js @@ -0,0 +1,449 @@ +const { app } = window.comfyAPI.app; +import { getSlotPos, clientToCanvas, getNodeAtPoint, typesCompatible, chainCallback } from "./utility.js"; + +// Max age of the last keydown for it to count as the command's trigger. +// Beyond this, treat the command as menu-fired (no release tracking). +const KEYDOWN_MAX_AGE_MS = 100; + +const state = { + pointerDown: false, + hasMoved: false, + insertKeyDown: false, + activationKey: null, + draggedNode: null, + startPos: null, + insertTargetLink: null, + insertSlots: null, + insertOriginNode: null, + insertDestNode: null, + lastScanTime: 0, + animating: false, +}; + +let lastKeyDown = null; +let lastKeyDownTime = 0; + +function bezierAt(p0, p1, p2, p3, t) { + const u = 1 - t; + const uu = u * u, uuu = uu * u; + const tt = t * t, ttt = tt * t; + return [ + uuu * p0[0] + 3 * uu * t * p1[0] + 3 * u * tt * p2[0] + ttt * p3[0], + uuu * p0[1] + 3 * uu * t * p1[1] + 3 * u * tt * p2[1] + ttt * p3[1], + ]; +} + +// Horizontal control-point offset for the link bezier; must match what +// LiteGraph draws so the hit-test region matches the visible curve. +const bezierOffsetX = (from, to) => Math.max(Math.abs(to[0] - from[0]) * 0.5, 50); + +function findLinkUnderNode(graph, draggedNode) { + const bounds = draggedNode.getBounding?.(); + const nodeX = bounds ? bounds[0] : draggedNode.pos[0]; + const nodeY = bounds ? bounds[1] : draggedNode.pos[1]; + const nodeW = bounds ? bounds[2] : (draggedNode.size[0] || 100); + const nodeH = bounds ? bounds[3] : (draggedNode.size[1] || 60); + const nodeCx = nodeX + nodeW / 2; + const nodeCy = nodeY + nodeH / 2; + + let bestLink = null; + let bestDist = Infinity; + + const links = graph.links; + if (!links) return null; + + for (const link of links.values()) { + if (!link) continue; + if (link.origin_id === draggedNode.id || link.target_id === draggedNode.id) continue; + + const originNode = graph.getNodeById(link.origin_id); + const targetNode = graph.getNodeById(link.target_id); + if (!originNode || !targetNode) continue; + + // Reject on node bounds before resolving slot positions — the latter + // hits the DOM in Vue mode and is the expensive step. + const oW = originNode.size?.[0] || 100, oH = originNode.size?.[1] || 60; + const tW = targetNode.size?.[0] || 100, tH = targetNode.size?.[1] || 60; + const cMinX = Math.min(originNode.pos[0], targetNode.pos[0]); + const cMaxX = Math.max(originNode.pos[0] + oW, targetNode.pos[0] + tW); + const cMinY = Math.min(originNode.pos[1], targetNode.pos[1]) - 50; + const cMaxY = Math.max(originNode.pos[1] + oH, targetNode.pos[1] + tH) + 50; + if (cMaxX < nodeX || cMinX > nodeX + nodeW || cMaxY < nodeY || cMinY > nodeY + nodeH) continue; + + const outPos = getSlotPos(originNode, false, link.origin_slot); + const inPos = getSlotPos(targetNode, true, link.target_slot); + + const lMinX = Math.min(outPos[0], inPos[0]); + const lMaxX = Math.max(outPos[0], inPos[0]); + const lMinY = Math.min(outPos[1], inPos[1]) - 50; // bezier can sag + const lMaxY = Math.max(outPos[1], inPos[1]) + 50; + if (lMaxX < nodeX || lMinX > nodeX + nodeW || lMaxY < nodeY || lMinY > nodeY + nodeH) continue; + + const offsetX = bezierOffsetX(outPos, inPos); + const p0 = outPos; + const p1 = [outPos[0] + offsetX, outPos[1]]; + const p2 = [inPos[0] - offsetX, inPos[1]]; + const p3 = inPos; + + for (let i = 0; i <= 20; i++) { + const pt = bezierAt(p0, p1, p2, p3, i / 20); + if (pt[0] >= nodeX && pt[0] <= nodeX + nodeW && pt[1] >= nodeY && pt[1] <= nodeY + nodeH) { + const d = Math.hypot(pt[0] - nodeCx, pt[1] - nodeCy); + if (d < bestDist) { + bestDist = d; + bestLink = link; + } + break; + } + } + } + return bestLink; +} + +function findInsertSlots(node, linkType) { + const inputSlot = node.inputs?.findIndex(i => i.link == null && typesCompatible(linkType, i.type)) ?? -1; + const outputSlot = node.outputs?.findIndex(o => typesCompatible(linkType, o.type)) ?? -1; + if (inputSlot === -1 || outputSlot === -1) return null; + return { inputSlot, outputSlot }; +} + +function executeNodeInsert(canvas, node, link) { + const graph = canvas.graph || app.graph; + + const originNode = graph.getNodeById(link.origin_id); + const targetNode = graph.getNodeById(link.target_id); + if (!originNode || !targetNode) return; + + const slots = findInsertSlots(node, link.type); + if (!slots) return; + + const originSlot = link.origin_slot; + const targetSlot = link.target_slot; + + targetNode.disconnectInput(targetSlot); + originNode.connect(originSlot, node, slots.inputSlot); + node.connect(slots.outputSlot, targetNode, targetSlot); + + graph.change(); + canvas.setDirty(true, true); +} + +function setInsertTarget(link, graph, slots = null) { + if (link === state.insertTargetLink) return; + // `_dragging` is LiteGraph's own runtime flag for hiding a link mid-rewire. + // Not serialized, so save/copy/undo stay safe. + if (state.insertTargetLink) delete state.insertTargetLink._dragging; + if (link) { + link._dragging = true; + state.insertSlots = slots ?? findInsertSlots(state.draggedNode, link.type); + state.insertOriginNode = graph.getNodeById(link.origin_id); + state.insertDestNode = graph.getNodeById(link.target_id); + } else { + state.insertSlots = null; + state.insertOriginNode = null; + state.insertDestNode = null; + } + state.insertTargetLink = link; +} + +function getTypeColor(lgCanvas, slotType) { + if (slotType && LGraphCanvas.link_type_colors && LGraphCanvas.link_type_colors[slotType]) { + return LGraphCanvas.link_type_colors[slotType]; + } + return lgCanvas.default_link_color || "#AAA"; +} + +function drawGhostLink(ctx, from, to, color, alpha, drawTime) { + const offsetX = bezierOffsetX(from, to); + + ctx.beginPath(); + ctx.moveTo(from[0], from[1]); + ctx.bezierCurveTo( + from[0] + offsetX, from[1], + to[0] - offsetX, to[1], + to[0], to[1], + ); + + ctx.strokeStyle = color; + ctx.globalAlpha = alpha; + ctx.lineWidth = 2.5; + ctx.setLineDash([8, 4]); + ctx.lineDashOffset = -(drawTime ?? performance.now()) / 50; + + ctx.stroke(); + ctx.setLineDash([]); + ctx.globalAlpha = 1; +} + +function drawSlotHighlight(ctx, pos, color, alpha) { + ctx.beginPath(); + ctx.arc(pos[0], pos[1], 6, 0, Math.PI * 2); + ctx.globalAlpha = alpha; + ctx.fillStyle = color; + ctx.fill(); + ctx.globalAlpha = 1; +} + +function drawGhostSegment(ctx, from, to, color, now) { + drawGhostLink(ctx, from, to, color, 0.8, now); + drawSlotHighlight(ctx, from, color, 0.6); + drawSlotHighlight(ctx, to, color, 0.6); +} + +function drawInsertPreview(ctx, lgCanvas) { + const { insertTargetLink: link, insertSlots: slots, insertOriginNode, insertDestNode, draggedNode } = state; + if (!link || !slots || !insertOriginNode || !insertDestNode || !draggedNode) return; + + const now = performance.now(); + const color = getTypeColor(lgCanvas, link.type); + + drawGhostSegment(ctx, + getSlotPos(insertOriginNode, false, link.origin_slot), + getSlotPos(draggedNode, true, slots.inputSlot), + color, now); + drawGhostSegment(ctx, + getSlotPos(draggedNode, false, slots.outputSlot), + getSlotPos(insertDestNode, true, link.target_slot), + color, now); +} + +function startAnimLoop(lgCanvas) { + if (state.animating) return; + state.animating = true; + + function tick() { + const link = state.insertTargetLink; + if (!link) { + state.animating = false; + lgCanvas.setDirty(true, true); + return; + } + // If the link was removed externally (e.g. undo) mid-preview, our + // `_dragging` flag has nothing to land on — drop the preview. + const graph = lgCanvas.graph || app.graph; + const stored = graph?.links?.get(link.id); + if (stored !== link) { + setInsertTarget(null); + state.animating = false; + lgCanvas.setDirty(true, true); + return; + } + // Steady-state: only the dash offset changes — front canvas only. + lgCanvas.setDirty(true, false); + requestAnimationFrame(tick); + } + requestAnimationFrame(tick); +} + +function clearState() { + setInsertTarget(null); + state.pointerDown = false; + state.hasMoved = false; + state.draggedNode = null; + state.startPos = null; +} + +app.registerExtension({ + name: "KJNodes.NodeInsert", + + settings: [ + { + id: "KJNodes.nodeInsertMode", + name: "Node insert activation", + category: ["KJNodes", "Node Insert", "Activation mode"], + tooltip: "Always: dragging a compatible node onto a link previews insertion. Hotkey: only while the hotkey (default: D) is held. Disabled: feature off.", + type: "combo", + defaultValue: "hotkey", + options: ["always", "hotkey", "disabled"], + }, + ], + + commands: [ + { + id: "KJNodes.NodeInsertMode", + label: "Node insert mode (hold to activate)", + active: () => state.insertKeyDown, + function: () => { + state.insertKeyDown = true; + // Snapshot the key that triggered this so any rebind works. + state.activationKey = (performance.now() - lastKeyDownTime < KEYDOWN_MAX_AGE_MS) + ? lastKeyDown + : null; + if (state.draggedNode) { + const graph = app.canvas?.graph || app.graph; + if (graph) { + const link = findLinkUnderNode(graph, state.draggedNode); + if (link && findInsertSlots(state.draggedNode, link.type)) { + setInsertTarget(link, graph); + startAnimLoop(app.canvas); + } + } + } + }, + }, + ], + + keybindings: [ + { + commandId: "KJNodes.NodeInsertMode", + // Default only — release detection captures the actually-triggering + // key at command-fire time, so user rebinds work. + combo: { key: "d" }, + targetElementId: "graph-canvas", + }, + ], + + async setup() { + const lgCanvas = app.canvas; + const canvasEl = lgCanvas.canvas; + + // Capture phase so `lastKeyDown` is set before ComfyUI fires the command. + document.addEventListener("keydown", (e) => { + if (e.repeat) return; + lastKeyDown = e.key?.toLowerCase() ?? null; + lastKeyDownTime = performance.now(); + }, true); + + document.addEventListener("keyup", (e) => { + const key = e.key?.toLowerCase() ?? null; + // Released key can't be a future trigger — invalidate so a stale + // keydown can't be picked up by a later menu activation. + if (key && key === lastKeyDown) lastKeyDownTime = 0; + + if (!state.insertKeyDown) return; + // No activation key (menu-fired) → blur/pointercancel will clean up. + if (!state.activationKey || key !== state.activationKey) return; + state.insertKeyDown = false; + state.activationKey = null; + if (state.insertTargetLink) { + setInsertTarget(null); + lgCanvas.setDirty(false, true); + } + }); + + // Releases outside the window never reach our keyup/pointerup listeners, + // so without these the `_dragging` flag and hotkey state can stick. + const dropTransient = () => { + state.insertKeyDown = false; + state.activationKey = null; + clearState(); + lgCanvas.setDirty(false, true); + }; + window.addEventListener("blur", dropTransient); + document.addEventListener("visibilitychange", () => { + if (document.hidden) dropTransient(); + }); + document.addEventListener("pointercancel", dropTransient, true); + + // Scope the Vue-mode node lookup to the graph canvas's container so + // stray `[data-node-id]` elements elsewhere can't activate this flow. + const graphRoot = canvasEl.parentElement; + document.addEventListener("pointerdown", (e) => { + if (e.button !== 0) return; + const onCanvas = e.target === canvasEl; + const vueNodeEl = graphRoot?.contains(e.target) + ? e.target?.closest?.("[data-node-id]") + : null; + if (!onCanvas && !vueNodeEl) return; + + state.pointerDown = true; + state.hasMoved = false; + + const graph = lgCanvas.graph || app.graph; + if (graph) { + let node = null; + if (vueNodeEl) { + const nodeId = parseInt(vueNodeEl.getAttribute("data-node-id")); + if (Number.isFinite(nodeId)) node = graph.getNodeById(nodeId); + } else { + const [cx, cy] = clientToCanvas(lgCanvas, e.clientX, e.clientY); + node = getNodeAtPoint(graph, cx, cy); + } + if (node && (node.inputs || node.outputs)) { + state.draggedNode = node; + state.startPos = node.pos ? [node.pos[0], node.pos[1]] : null; + } + } + }, true); + + document.addEventListener("pointermove", () => { + if (!state.pointerDown) return; + if (lgCanvas.connecting_links?.length) return; + if (lgCanvas.resizing_node) return; + if (lgCanvas.node_widget) return; + + const mode = app.ui.settings.getSettingValue("KJNodes.nodeInsertMode") ?? "always"; + if (mode === "disabled") return; + + state.hasMoved = true; + + const active = mode === "always" || state.insertKeyDown; + if (!active) { + setInsertTarget(null); + return; + } + + if (!state.draggedNode) return; + + // Multi-node drags move several nodes together — inserting one of + // them into a link is almost never what the user wants. + const selected = lgCanvas.selected_nodes; + if (selected) { + const count = selected instanceof Map ? selected.size : Object.keys(selected).length; + if (count > 1) { + setInsertTarget(null); + return; + } + } + + // Vue mode: pointermove fires on the document during widget drags + // (sliders etc.) too — require the node itself to have moved. + const np = state.draggedNode.pos; + if (!np || !state.startPos + || (np[0] === state.startPos[0] && np[1] === state.startPos[1])) { + setInsertTarget(null); + return; + } + + const graph = lgCanvas.graph || app.graph; + if (!graph) return; + + const now = performance.now(); + const nodeCount = graph._nodes?.length ?? 0; + const throttle = nodeCount > 200 ? 50 : nodeCount > 100 ? 32 : 16; + if (now - state.lastScanTime < throttle) return; + state.lastScanTime = now; + + const link = findLinkUnderNode(graph, state.draggedNode); + const slots = link ? findInsertSlots(state.draggedNode, link.type) : null; + const wasIdle = !state.insertTargetLink; + + if (link && slots) { + setInsertTarget(link, graph, slots); + if (wasIdle) startAnimLoop(lgCanvas); + } else { + setInsertTarget(null); + } + }, true); + + document.addEventListener("pointerup", (e) => { + if (e.button !== 0) return; + if (!state.pointerDown) return; + + if (state.insertTargetLink && state.draggedNode && state.hasMoved + && !state.draggedNode.flags?.pinned) { + delete state.insertTargetLink._dragging; + executeNodeInsert(lgCanvas, state.draggedNode, state.insertTargetLink); + } + + clearState(); + }, true); + + // onDrawForeground: ghost paints over the existing (hidden) link. + chainCallback(lgCanvas, "onDrawForeground", function (ctx) { + if (state.insertTargetLink) { + drawInsertPreview(ctx, lgCanvas); + } + }); + }, +}); diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/web/js/nodeswap.js b/ComfyUI/custom_nodes/ComfyUI-KJNodes/web/js/nodeswap.js new file mode 100644 index 0000000000000000000000000000000000000000..d588fed5321cb4caf3fbb12da342732abe58c93e --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-KJNodes/web/js/nodeswap.js @@ -0,0 +1,434 @@ +const { app } = window.comfyAPI.app; +import { typesCompatible, clientToCanvas, getNodeAtPoint } from "./utility.js"; + +let swapTargetNode = null; +let swapDraggedNode = null; +let swapDragStartPos = null; +let swapAnimating = false; +let swapHasMoved = false; +let swapKeyDown = false; + +/** Find the topmost node whose bounding box overlaps with draggedNode */ +function getOverlappingNode(graph, draggedNode) { + const ax = draggedNode.pos[0]; + const ay = draggedNode.pos[1]; + const aw = draggedNode.size[0] || 100; + const ah = draggedNode.size[1] || 60; + + for (let i = graph._nodes.length - 1; i >= 0; i--) { + const n = graph._nodes[i]; + if (n === draggedNode) continue; + const bx = n.pos[0]; + const by = n.pos[1]; + if (ax < bx + (n.size[0] || 100) && ax + aw > bx && + ay < by + (n.size[1] || 60) && ay + ah > by) { + return n; + } + } + return null; +} + + +function startHighlightAnim(lgCanvas) { + if (swapAnimating) return; + swapAnimating = true; + + function tick() { + if (!swapTargetNode) { + swapAnimating = false; + return; + } + lgCanvas.setDirty(false, true); + requestAnimationFrame(tick); + } + requestAnimationFrame(tick); +} + +function clearSwapState() { + swapTargetNode = null; + swapDraggedNode = null; + swapDragStartPos = null; + swapHasMoved = false; +} + +function snapshotConnections(graph, node) { + const inputs = []; + for (let i = 0; i < (node.inputs?.length ?? 0); i++) { + const inp = node.inputs[i]; + if (inp.link == null) continue; + const link = graph.getLink(inp.link); + if (!link) continue; + inputs.push({ + slotIndex: i, + type: inp.type, + name: inp.name, + originNodeId: link.origin_id, + originSlot: link.origin_slot, + }); + } + + const outputs = []; + for (let o = 0; o < (node.outputs?.length ?? 0); o++) { + const out = node.outputs[o]; + if (!out.links?.length) continue; + const targets = out.links + .map((linkId) => graph.getLink(linkId)) + .filter(Boolean) + .map((link) => ({ targetNodeId: link.target_id, targetSlot: link.target_slot })); + if (targets.length > 0) { + outputs.push({ slotIndex: o, type: out.type, name: out.name, targets }); + } + } + + return { inputs, outputs }; +} + +function disconnectAll(node) { + if (node.inputs) { + for (let i = node.inputs.length - 1; i >= 0; i--) { + if (node.inputs[i].link != null) node.disconnectInput(i); + } + } + if (node.outputs) { + for (let o = node.outputs.length - 1; o >= 0; o--) { + if (node.outputs[o].links?.length) node.disconnectOutput(o); + } + } +} + +// Map each snapshot entry → a new-node slot. Name matches win first, then sameIndex, +// then first compatible. Each slot is claimed at most once so fallbacks can't stomp matches. +function resolveAssignments(snapshots, slots, isFree) { + const assignments = new Map(); + if (!slots || !slots.length) return assignments; + const used = new Set(); + + for (const snap of snapshots) { + if (!snap.name) continue; + const nameLower = String(snap.name).toLowerCase(); + for (let s = 0; s < slots.length; s++) { + if (used.has(s) || !isFree(s)) continue; + const slotName = slots[s].name; + if (slotName && String(slotName).toLowerCase() === nameLower && + typesCompatible(snap.type, slots[s].type)) { + assignments.set(snap, s); + used.add(s); + break; + } + } + } + + for (const snap of snapshots) { + if (assignments.has(snap)) continue; + const i = snap.slotIndex; + if (i < slots.length && !used.has(i) && isFree(i) && + typesCompatible(snap.type, slots[i].type)) { + assignments.set(snap, i); + used.add(i); + continue; + } + for (let s = 0; s < slots.length; s++) { + if (used.has(s) || !isFree(s)) continue; + if (typesCompatible(snap.type, slots[s].type)) { + assignments.set(snap, s); + used.add(s); + break; + } + } + } + + return assignments; +} + +// Reconnect external connections from a snapshot onto newNode. +function reconnectExternal(graph, snapshot, newNode, otherNodeId) { + const inputAssignments = resolveAssignments( + snapshot.inputs, newNode.inputs || [], + (s) => newNode.inputs[s].link == null, + ); + for (const inp of snapshot.inputs) { + if (inp.originNodeId === otherNodeId) continue; + if (!inputAssignments.has(inp)) continue; + const originNode = graph.getNodeById(inp.originNodeId); + if (!originNode) continue; + originNode.connect(inp.originSlot, newNode, inputAssignments.get(inp)); + } + + const outputAssignments = resolveAssignments( + snapshot.outputs, newNode.outputs || [], + () => true, + ); + for (const out of snapshot.outputs) { + if (!outputAssignments.has(out)) continue; + const bestSlot = outputAssignments.get(out); + for (const tgt of out.targets) { + if (tgt.targetNodeId === otherNodeId) continue; + const targetNode = graph.getNodeById(tgt.targetNodeId); + if (!targetNode) continue; + newNode.connect(bestSlot, targetNode, tgt.targetSlot); + } + } +} + +// Reconnect links that were between the two swapped nodes in one direction. +// A→B becomes B→A, if slot types are compatible. +function reconnectInternalOneDirection(snap, fromNodeId, srcNode, dstNode) { + for (const out of snap.outputs) { + for (const tgt of out.targets) { + if (tgt.targetNodeId !== fromNodeId) continue; + if (!srcNode.outputs || out.slotIndex >= srcNode.outputs.length) continue; + if (!dstNode.inputs || tgt.targetSlot >= dstNode.inputs.length) continue; + if (!typesCompatible(srcNode.outputs[out.slotIndex].type, dstNode.inputs[tgt.targetSlot].type)) continue; + srcNode.connect(out.slotIndex, dstNode, tgt.targetSlot); + } + } +} + +function getNodeElement(nodeId) { + return document.querySelector(`[data-node-id="${nodeId}"]`); +} + +function animateSwapVue(nodeA, toA, nodeB, toB, duration) { + const elA = getNodeElement(nodeA.id); + const elB = getNodeElement(nodeB.id); + const transition = `transform ${duration}ms cubic-bezier(0.65, 0, 0.35, 1)`; + + if (elA) elA.style.transition = transition; + if (elB) elB.style.transition = transition; + + nodeA.pos = [toA[0], toA[1]]; + nodeB.pos = [toB[0], toB[1]]; + app.canvas.setDirty(true, true); + + setTimeout(() => { + if (elA) elA.style.transition = ""; + if (elB) elB.style.transition = ""; + }, duration + 16); +} + +function animateSwapCanvas(nodeA, fromA, toA, nodeB, fromB, toB, duration) { + const start = performance.now(); + + function ease(t) { + return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; + } + + function frame(now) { + const t = Math.min((now - start) / duration, 1); + const e = ease(t); + nodeA.pos = [ + fromA[0] + (toA[0] - fromA[0]) * e, + fromA[1] + (toA[1] - fromA[1]) * e, + ]; + nodeB.pos = [ + fromB[0] + (toB[0] - fromB[0]) * e, + fromB[1] + (toB[1] - fromB[1]) * e, + ]; + app.canvas.setDirty(true, true); + if (t < 1) requestAnimationFrame(frame); + } + + requestAnimationFrame(frame); +} + +function executeNodeSwap(canvas, nodeA, nodeB, originalPosA) { + const graph = canvas.graph || app.graph; + + const snapA = snapshotConnections(graph, nodeA); + const snapB = snapshotConnections(graph, nodeB); + + const posA = originalPosA || [nodeA.pos[0], nodeA.pos[1]]; + const posB = [nodeB.pos[0], nodeB.pos[1]]; + + disconnectAll(nodeA); + disconnectAll(nodeB); + + reconnectExternal(graph, snapA, nodeB, nodeA.id); + reconnectExternal(graph, snapB, nodeA, nodeB.id); + + reconnectInternalOneDirection(snapA, nodeB.id, nodeB, nodeA); + reconnectInternalOneDirection(snapB, nodeA.id, nodeA, nodeB); + + if (LiteGraph.vueNodesMode) { + animateSwapVue(nodeA, posB, nodeB, posA, 200); + } else { + animateSwapCanvas(nodeA, [nodeA.pos[0], nodeA.pos[1]], posB, + nodeB, [nodeB.pos[0], nodeB.pos[1]], posA, 200); + } + + graph.change(); + canvas.setDirty(true, true); +} + +app.registerExtension({ + name: "KJNodes.NodeSwap", + + settings: [ + { + id: "KJNodes.nodeSwapEnabled", + name: "Enable node swap on drag", + category: ["KJNodes", "Node Swap", "Enable"], + tooltip: "Hold swap key (default: S, rebindable in Keybindings) and drag a node onto another to swap their positions and reconnect links", + type: "boolean", + defaultValue: true, + }, + ], + commands: [ + { + id: "KJNodes.ToggleNodeSwap", + label: "Toggle node swap on drag", + active: () => app.ui.settings.getSettingValue("KJNodes.nodeSwapEnabled"), + function: () => { + const cur = app.ui.settings.getSettingValue("KJNodes.nodeSwapEnabled"); + app.ui.settings.setSettingValue("KJNodes.nodeSwapEnabled", !cur); + }, + }, + { + id: "KJNodes.NodeSwapMode", + label: "Node swap mode (hold to activate)", + active: () => swapKeyDown, + function: () => { + swapKeyDown = true; + // If already dragging a node, check for overlap immediately + // (handles case where node is already on top of another when key is pressed) + if (swapDraggedNode) { + const graph = app.canvas?.graph || app.graph; + if (graph) { + swapTargetNode = getOverlappingNode(graph, swapDraggedNode); + if (swapTargetNode) startHighlightAnim(app.canvas); + } + } + }, + }, + ], + keybindings: [ + { + commandId: "KJNodes.NodeSwapMode", + combo: { key: "s" }, + targetElementId: "graph-canvas", + }, + ], + + async setup() { + await new Promise((resolve) => { + function check() { + if (app.canvas) return resolve(); + requestAnimationFrame(check); + } + check(); + }); + + const lgCanvas = app.canvas; + const canvasEl = lgCanvas.canvas; + + // The command system handles keydown (sets swapKeyDown = true). + // Any keyup clears it since the user must hold the key. + document.addEventListener("keyup", () => { + if (swapKeyDown) { + swapKeyDown = false; + if (swapTargetNode) { + swapTargetNode = null; + lgCanvas.setDirty(false, true); + } + } + }); + + // Pointermove/pointerup handlers — only attached while pointer is down. + function onPointerMove() { + swapHasMoved = true; + + if (!swapKeyDown) { + if (swapTargetNode) { + swapTargetNode = null; + lgCanvas.setDirty(false, true); + } + return; + } + + if (!swapDraggedNode) return; + + const graph = lgCanvas.graph || app.graph; + if (!graph) return; + + const prev = swapTargetNode; + swapTargetNode = getOverlappingNode(graph, swapDraggedNode); + + if (swapTargetNode && !prev) { + startHighlightAnim(lgCanvas); + } + } + + function onPointerUp(e) { + if (e.button !== 0) return; + + document.removeEventListener("pointermove", onPointerMove); + document.removeEventListener("pointerup", onPointerUp, true); + + const targetNode = swapTargetNode; + const draggedNode = swapDraggedNode; + const startPos = swapDragStartPos; + const hasMoved = swapHasMoved; + clearSwapState(); + + if (!hasMoved || !targetNode || !draggedNode || !swapKeyDown) return; + + if (!targetNode.flags?.pinned && !draggedNode.flags?.pinned) { + executeNodeSwap(lgCanvas, draggedNode, targetNode, startPos); + } + } + + // Record clicked node and its original position. + // Listen on document to catch events from both the canvas and Vue node overlays. + const graphContainer = canvasEl.closest(".graph-canvas-container") || canvasEl.parentElement; + document.addEventListener("pointerdown", (e) => { + if (e.button !== 0) return; + if (!app.ui.settings.getSettingValue("KJNodes.nodeSwapEnabled")) return; + if (!graphContainer?.contains(e.target)) return; + + swapHasMoved = false; + + const graph = lgCanvas.graph || app.graph; + if (!graph) return; + + const [cx, cy] = clientToCanvas(lgCanvas, e.clientX, e.clientY); + const node = getNodeAtPoint(graph, cx, cy); + + if (node && (node.inputs || node.outputs)) { + swapDraggedNode = node; + swapDragStartPos = [node.pos[0], node.pos[1]]; + document.addEventListener("pointermove", onPointerMove); + document.addEventListener("pointerup", onPointerUp, true); + } + }, true); + + // Draw pulsing glow outline on the swap target node + const origOnDrawBg = lgCanvas.onDrawBackground; + lgCanvas.onDrawBackground = function (ctx, ...args) { + origOnDrawBg?.call(this, ctx, ...args); + + const target = swapTargetNode; + if (!target) return; + + const [x, y] = target.pos; + const w = target.size[0] || 100; + const h = target.size[1] || 60; + const titleH = LiteGraph.NODE_TITLE_HEIGHT || 20; + const pulse = 0.5 + 0.3 * Math.sin(Date.now() / 200); + const isPinned = target.flags?.pinned; + const color = isPinned ? "255, 80, 80" : "100, 200, 255"; + const pad = 5; + + ctx.save(); + ctx.strokeStyle = `rgba(${color}, ${pulse})`; + ctx.lineWidth = 3; + ctx.shadowColor = `rgba(${color}, 0.8)`; + ctx.shadowBlur = 15; + ctx.beginPath(); + ctx.roundRect(x - pad, y - titleH - pad, w + pad * 2, h + titleH + pad * 2, 8); + ctx.stroke(); + ctx.fillStyle = `rgba(${color}, ${isPinned ? 0.08 : 0.05})`; + ctx.fill(); + ctx.restore(); + }; + }, +}); diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/web/js/performance.js b/ComfyUI/custom_nodes/ComfyUI-KJNodes/web/js/performance.js new file mode 100644 index 0000000000000000000000000000000000000000..4420dce237fab545326b298cd175b27bd85e67bd --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-KJNodes/web/js/performance.js @@ -0,0 +1,286 @@ +const { app } = window.comfyAPI.app; + +const _savedRoundRadius = typeof LiteGraph !== "undefined" ? LiteGraph.ROUND_RADIUS : 8; + +function getSetting(id, fallback) { + return app.ui.settings.getSettingValue(id) ?? fallback; +} + +function toggleSetting(id) { + const cur = app.ui.settings.getSettingValue(id); + app.ui.settings.setSettingValue(id, !cur); +} + +app.registerExtension({ + name: "KJNodes.Performance", + + commands: [ + { + id: "KJNodes.perf.toggleSingleCanvasPan", + label: "Single-canvas mode during pan", + menubarLabel: "Single-canvas mode during pan", + function: () => toggleSetting("KJNodes.perf.singleCanvasPan"), + active: () => getSetting("KJNodes.perf.singleCanvasPan", false), + }, + { + id: "KJNodes.perf.toggleDisableShadows", + label: "Disable node shadows", + menubarLabel: "Disable node shadows", + function: () => toggleSetting("KJNodes.perf.disableShadows"), + active: () => getSetting("KJNodes.perf.disableShadows", false), + }, + { + id: "KJNodes.perf.toggleDisableConnectionBorders", + label: "Disable connection borders", + menubarLabel: "Disable connection borders", + function: () => toggleSetting("KJNodes.perf.disableConnectionBorders"), + active: () => getSetting("KJNodes.perf.disableConnectionBorders", false), + }, + { + id: "KJNodes.perf.toggleDisableRoundedCorners", + label: "Disable rounded corners", + menubarLabel: "Disable rounded corners", + function: () => toggleSetting("KJNodes.perf.disableRoundedCorners"), + active: () => getSetting("KJNodes.perf.disableRoundedCorners", false), + }, + { + id: "KJNodes.perf.toggleThrottleRenderInfo", + label: "Throttle info overlay", + menubarLabel: "Throttle info overlay", + function: () => toggleSetting("KJNodes.perf.throttleRenderInfo"), + active: () => getSetting("KJNodes.perf.throttleRenderInfo", false), + }, + ], + + menuCommands: [ + { + path: ["KJNodes", "Performance"], + commands: [ + "KJNodes.perf.toggleSingleCanvasPan", + "KJNodes.perf.toggleDisableShadows", + "KJNodes.perf.toggleDisableConnectionBorders", + "KJNodes.perf.toggleDisableRoundedCorners", + "KJNodes.perf.toggleThrottleRenderInfo", + ], + }, + ], + + settings: [ + { + id: "KJNodes.perf.singleCanvasPan", + name: "Single-canvas mode during pan", + category: ["KJNodes", "Performance", "Single-canvas mode during pan"], + tooltip: "Eliminates the expensive bgcanvas-to-canvas copy during panning. Significant improvement without hardware acceleration.", + type: "boolean", + defaultValue: false, + onChange: (value) => { + const canvas = app.canvas; + if (!canvas) return; + if (value) { + installSingleCanvasPan(canvas); + } else { + uninstallSingleCanvasPan(canvas); + } + }, + }, + { + id: "KJNodes.perf.disableShadows", + name: "Disable node shadows", + category: ["KJNodes", "Performance", "Disable node shadows"], + tooltip: "Disable shadow rendering on nodes for better performance without hardware acceleration.", + type: "boolean", + defaultValue: false, + onChange: (value) => { + if (app.canvas) { + app.canvas.render_shadows = !value; + app.canvas.setDirty(true, true); + } + }, + }, + { + id: "KJNodes.perf.disableConnectionBorders", + name: "Disable connection borders", + category: ["KJNodes", "Performance", "Disable connection borders"], + tooltip: "Disable the outer border stroke on connection lines. Each link draws two strokes instead of one when enabled.", + type: "boolean", + defaultValue: false, + onChange: (value) => { + if (app.canvas) { + app.canvas.render_connections_border = !value; + app.canvas.setDirty(true, true); + } + }, + }, + { + id: "KJNodes.perf.disableRoundedCorners", + name: "Disable rounded corners", + category: ["KJNodes", "Performance", "Disable rounded corners"], + tooltip: "Use square corners on nodes instead of rounded. Avoids roundRect calls in software rendering.", + type: "boolean", + defaultValue: false, + onChange: (value) => { + if (typeof LiteGraph !== "undefined") { + LiteGraph.ROUND_RADIUS = value ? 0 : _savedRoundRadius; + app.canvas?.setDirty(true, true); + } + }, + }, + { + id: "KJNodes.perf.throttleRenderInfo", + name: "Throttle info overlay", + category: ["KJNodes", "Performance", "Throttle info overlay"], + tooltip: "Cache the FPS/info text overlay and only re-render it a few times per second. Saves ~20ms per frame in software rendering.", + type: "boolean", + defaultValue: false, + onChange: (value) => { + const canvas = app.canvas; + if (!canvas) return; + if (value) { + installThrottleRenderInfo(canvas); + } else { + uninstallThrottleRenderInfo(canvas); + } + }, + }, + ], + + setup() { + const canvas = app.canvas; + if (!canvas) return; + + if (getSetting("KJNodes.perf.singleCanvasPan", false)) { + installSingleCanvasPan(canvas); + } + if (getSetting("KJNodes.perf.disableShadows", false)) { + canvas.render_shadows = false; + } + if (getSetting("KJNodes.perf.disableConnectionBorders", false)) { + canvas.render_connections_border = false; + } + if (getSetting("KJNodes.perf.disableRoundedCorners", false)) { + LiteGraph.ROUND_RADIUS = 0; + } + if (getSetting("KJNodes.perf.throttleRenderInfo", false)) { + installThrottleRenderInfo(canvas); + } + }, +}); + +// ── Single-canvas mode during pan ─────────────────────────────────────────── + +let _origDraw = null; +let _panInstalledOn = null; + +function installSingleCanvasPan(canvas) { + if (_origDraw && _panInstalledOn === canvas) return; + if (_origDraw && _panInstalledOn !== canvas) { + // Canvas was replaced — discard stale reference + _origDraw = null; + } + _panInstalledOn = canvas; + _origDraw = canvas.draw; + let panning = false; + let savedBgCanvas = null; + let savedBgCtx = null; + + canvas.draw = function (force_canvas, force_bgcanvas) { + if (this.dragging_canvas) { + if (!panning) { + // Flush any pending bg redraw before aliasing to avoid a stale-frame flash + if (this.dirty_bgcanvas) { + _origDraw.call(this, false, true); + } + savedBgCanvas = this.bgcanvas; + savedBgCtx = this.bgctx; + panning = true; + this.bgcanvas = this.canvas; + this.bgctx = this.ctx; + } + } else if (panning) { + panning = false; + this.bgcanvas = savedBgCanvas; + this.bgctx = savedBgCtx; + this.dirty_bgcanvas = true; + this.dirty_canvas = true; + } + _origDraw.call(this, force_canvas, force_bgcanvas); + }; +} + +function uninstallSingleCanvasPan(canvas) { + if (!_origDraw) return; + if (_panInstalledOn === canvas) { + canvas.draw = _origDraw; + } + _origDraw = null; + _panInstalledOn = null; + canvas.dirty_bgcanvas = true; + canvas.dirty_canvas = true; +} + +// ── Throttle renderInfo ───────────────────────────────────────────────────── + +let _origRenderInfo = null; +let _infoCanvas = null; +let _infoInstalledOn = null; + +function installThrottleRenderInfo(canvas) { + if (_origRenderInfo && _infoInstalledOn === canvas) return; + if (_origRenderInfo && _infoInstalledOn !== canvas) { + _origRenderInfo = null; + _infoCanvas = null; + } + _infoInstalledOn = canvas; + _origRenderInfo = canvas.renderInfo; + _infoCanvas = document.createElement("canvas"); + let infoCtx = _infoCanvas.getContext("2d"); + let lastInfoTime = 0; + let cachedDpr = 0; + let _infoDrawX = 10; + let _infoDrawY = 0; + + canvas.renderInfo = function (ctx, x, y) { + const dpr = window.devicePixelRatio || 1; + + // Resize offscreen canvas if DPR changed + if (dpr !== cachedDpr) { + cachedDpr = dpr; + _infoCanvas.width = Math.ceil(200 * dpr); + _infoCanvas.height = Math.ceil(100 * dpr); + infoCtx = _infoCanvas.getContext("2d"); + infoCtx.scale(dpr, dpr); + lastInfoTime = 0; + } + + const now = performance.now(); + if (now - lastInfoTime > 250) { + lastInfoTime = now; + infoCtx.clearRect(0, 0, 200, 100); + // Render with the same x/y the original would use, but offset + // into offscreen canvas coordinates + const origX = x || 10; + const lineHeight = 13; + const lineCount = (this.graph ? 5 : 1) + (this.info_text ? 1 : 0); + const origY = y || (this.canvas.height / dpr - (lineCount + 1) * lineHeight); + // Store the computed position for blitting + _infoDrawX = origX; + _infoDrawY = origY; + // Draw into offscreen at (1, 1), text flows from there + _origRenderInfo.call(this, infoCtx, 1, 1); + } + + // Blit cached text at the original position + ctx.drawImage(_infoCanvas, _infoDrawX - 1, _infoDrawY - 1, 200, 100); + }; +} + +function uninstallThrottleRenderInfo(canvas) { + if (!_origRenderInfo) return; + if (_infoInstalledOn === canvas) { + canvas.renderInfo = _origRenderInfo; + } + _origRenderInfo = null; + _infoCanvas = null; + _infoInstalledOn = null; + canvas.setDirty(true, true); +} diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/web/js/play_sound.js b/ComfyUI/custom_nodes/ComfyUI-KJNodes/web/js/play_sound.js new file mode 100644 index 0000000000000000000000000000000000000000..cda30caea56623e650836b77a5e532f47c3cae8c --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-KJNodes/web/js/play_sound.js @@ -0,0 +1,94 @@ +const { app } = window.comfyAPI.app; +const { api } = window.comfyAPI.api; + +app.registerExtension({ + name: "KJNodes.PlaySound", + async beforeRegisterNodeDef(nodeType, nodeData) { + if (nodeData.name !== "PlaySoundKJ") return; + + const onExecuted = nodeType.prototype.onExecuted; + nodeType.prototype.onExecuted = function (output) { + onExecuted?.call(this, output); + + const audios = output?.audio; + if (!audios?.length) return; + + const modeWidget = this.widgets?.find(w => w.name === "mode"); + const volumeWidget = this.widgets?.find(w => w.name === "volume"); + const durationWidget = this.widgets?.find(w => w.name === "duration"); + const mode = modeWidget?.value ?? "always"; + const volume = volumeWidget?.value ?? 0.5; + const duration = durationWidget?.value ?? 0; + + // on_change: skip if audio content hasn't changed + if (mode === "on_change") { + const audioHash = output?.audio_hash?.[0]; + if (audioHash != null && this._kjLastAudioHash === audioHash) return; + this._kjLastAudioHash = audioHash; + } + + // Clean up previous state + if (this._kjStatusListener) { + api.removeEventListener("status", this._kjStatusListener); + this._kjStatusListener = null; + } + clearTimeout(this._kjQueueDebounce); + this._kjPendingAudio = null; + + if (this._kjPlayingAudio) { + this._kjPlayingAudio.pause(); + this._kjPlayingAudio = null; + } + if (this._kjPlayTimer != null) { + clearTimeout(this._kjPlayTimer); + this._kjPlayTimer = null; + } + + const startPlayback = () => { + const { filename, subfolder, type } = audios[0]; + const params = new URLSearchParams({ + filename: filename ?? "", + subfolder: subfolder ?? "", + type: type ?? "temp", + }); + const url = api.apiURL(`/view?${params.toString()}`); + const audio = new Audio(url); + audio.volume = Math.max(0, Math.min(1, volume)); + audio.play().catch(() => {}); + this._kjPlayingAudio = audio; + if (duration > 0) { + this._kjPlayTimer = setTimeout(() => { + audio.pause(); + this._kjPlayingAudio = null; + this._kjPlayTimer = null; + }, duration * 1000); + } + }; + + if (mode === "on_empty_queue") { + this._kjPendingAudio = startPlayback; + this._kjStatusListener = ({ detail }) => { + const remaining = detail?.exec_info?.queue_remaining ?? 0; + if (remaining === 0) { + // Debounce: confirm queue is truly empty + // (status can briefly show 0 between dispatches) + clearTimeout(this._kjQueueDebounce); + this._kjQueueDebounce = setTimeout(() => { + if (this._kjPendingAudio) { + this._kjPendingAudio(); + this._kjPendingAudio = null; + } + api.removeEventListener("status", this._kjStatusListener); + this._kjStatusListener = null; + }, 1000); + } else { + clearTimeout(this._kjQueueDebounce); + } + }; + api.addEventListener("status", this._kjStatusListener); + } else { + startPlayback(); + } + }; + }, +}); diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/web/js/preview_override/preview_override.css b/ComfyUI/custom_nodes/ComfyUI-KJNodes/web/js/preview_override/preview_override.css new file mode 100644 index 0000000000000000000000000000000000000000..c5197d80df27cbc767d677b78cc260e1522f8e0a --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-KJNodes/web/js/preview_override/preview_override.css @@ -0,0 +1,194 @@ +/* Palette matched to ComfyUI-MemoryVisualization. */ + +.kj-pov-root { + width: 100%; + height: 100%; + position: relative; + background: #181818; + color: #b0b0b0; + font: 12px monospace; + display: flex; + flex-direction: column; + overflow: hidden; + user-select: none; +} + +.kj-pov-image-area { + flex: 1 1 auto; + min-height: 80px; + position: relative; + display: flex; + align-items: center; + justify-content: center; + background: #0a0a0a; + overflow: hidden; +} + +/* Absolute-fill so latent-resolution previews scale up rather than staying centred-tiny. */ +.kj-pov-img { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + object-fit: contain; + image-rendering: pixelated; + display: block; +} + +.kj-pov-placeholder { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + color: #707070; + font: 12px monospace; + pointer-events: none; +} + +.kj-pov-scrub { + position: absolute; + left: 0; + right: 0; + bottom: 0; + height: 6px; + background: rgba(0, 0, 0, 0.5); + cursor: pointer; + z-index: 5; + transition: height 80ms ease; +} +.kj-pov-scrub:hover { height: 10px; } +.kj-pov-scrub-fill { + height: 100%; + width: 0%; + background: #e67e22; + pointer-events: none; +} +.kj-pov-scrub.kj-pov-paused .kj-pov-scrub-fill { + background: #707070; +} + +.kj-pov-panel-grip { + flex: 0 0 5px; + background: #2a2a2a; + cursor: ns-resize; + position: relative; + z-index: 4; + user-select: none; +} +.kj-pov-panel-grip::after { + content: ''; + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + width: 26px; + height: 2px; + background: #555; + border-radius: 1px; +} +.kj-pov-panel-grip:hover { background: #3a3a3a; } +.kj-pov-panel-grip:hover::after { background: #888; } + +.kj-pov-panel { + flex: 0 0 auto; + height: 140px; + min-height: 60px; + background: #181818; + font: 11px monospace; + color: #b0b0b0; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.kj-pov-panel-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 4px 10px; + gap: 8px; + background: #202020; + border-bottom: 1px solid #2a2a2a; +} +.kj-pov-header-left { + display: flex; + align-items: center; + gap: 8px; +} +.kj-pov-panel-title { color: #b0b0b0; font-size: 11px; } +.kj-pov-panel-summary { color: #707070; font-size: 10px; } + +.kj-pov-toggle { + background: #2a2a2a; + color: #888; + border: 1px solid #333; + border-radius: 3px; + padding: 1px 7px; + font: 10px monospace; + cursor: pointer; + user-select: none; +} +.kj-pov-toggle:hover { background: #333; color: #b0b0b0; } +.kj-pov-toggle.kj-pov-on { + background: #3a2a1a; + color: #e67e22; + border-color: #c66e1a; +} + +.kj-pov-graphs-grid { + display: flex; + align-items: stretch; + padding: 6px 6px 7px; + gap: 6px; + flex: 1 1 auto; + min-height: 0; + box-sizing: border-box; +} + +.kj-pov-graph-cell { + flex: 1 1 0; + min-width: 0; + display: flex; + flex-direction: column; + background: #0e0e0e; + border: 1px solid #1c1c1c; + border-radius: 3px; + overflow: hidden; +} + +.kj-pov-graph-head { + flex: 0 0 auto; + display: flex; + justify-content: space-between; + align-items: baseline; + padding: 3px 6px 2px; + background: #161616; + border-bottom: 1px solid #1c1c1c; + font-size: 10px; + line-height: 1.1; + gap: 4px; +} + +.kj-pov-graph-label { + color: #707070; + letter-spacing: 0.05em; + flex: 0 0 auto; +} + +.kj-pov-graph-value { + color: #b0b0b0; + font-size: 10px; + text-align: right; + flex: 1 1 auto; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.kj-pov-graph-canvas { + flex: 1 1 auto; + display: block; + width: 100%; + min-height: 0; +} diff --git a/ComfyUI/custom_nodes/ComfyUI-KJNodes/web/js/preview_override/preview_override.js b/ComfyUI/custom_nodes/ComfyUI-KJNodes/web/js/preview_override/preview_override.js new file mode 100644 index 0000000000000000000000000000000000000000..cc1cf1a8e23e0e38bc0dc2e5dd4d6f64d7ab50cd --- /dev/null +++ b/ComfyUI/custom_nodes/ComfyUI-KJNodes/web/js/preview_override/preview_override.js @@ -0,0 +1,948 @@ +import { chainCallback, addMiddleClickPan, addWheelPassthrough } from '../utility.js'; +const { app } = window.comfyAPI.app; +const { api } = window.comfyAPI.api; + +const STYLE_ID = "kj-pov-stylesheet"; +const _cssUrl = new URL("./preview_override.css", import.meta.url).href; +function ensureStyles() { + if (document.getElementById(STYLE_ID)) return; + const link = document.createElement("link"); + link.id = STYLE_ID; + link.rel = "stylesheet"; + link.href = _cssUrl; + document.head.appendChild(link); +} + +// Walks subgraph chain for IDs like "12:7:5". Mirrors getNodeByExecutionId (not exported). +function findNodeByQualifiedId(rootGraph, qid) { + if (!rootGraph || !qid) return null; + const parts = String(qid).split(":"); + let graph = rootGraph; + for (let i = 0; i < parts.length - 1; i++) { + const parentId = parseInt(parts[i], 10); + if (!Number.isFinite(parentId)) return null; + const parentNode = graph?.getNodeById?.(parentId); + if (!parentNode?.subgraph) return null; + graph = parentNode.subgraph; + } + const leafId = parseInt(parts[parts.length - 1], 10); + if (!Number.isFinite(leafId)) return null; + return graph?.getNodeById?.(leafId) || null; +} + +api.addEventListener("kj_preview_override", (e) => { + const data = e.detail; + if (!data || data.node_id == null) return; + const node = findNodeByQualifiedId(app.graph, data.node_id); + if (node?._kjPreviewHandler) node._kjPreviewHandler(data); +}); + +const GRAPH_PAD_X = 4; + +function fmt(n, d) { + return Number.isFinite(n) ? n.toFixed(d) : "—"; +} + +function el(tag, className, parent) { + const e = document.createElement(tag); + if (className) e.className = className; + if (parent) parent.appendChild(e); + return e; +} + +function b64ToBlob(b64, mime) { + const bin = atob(b64); + const arr = new Uint8Array(bin.length); + for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i); + return new Blob([arr], { type: mime }); +} + +function syncCanvasDPR(canvas) { + const dpr = window.devicePixelRatio || 1; + const cssW = canvas.clientWidth || canvas.width; + const cssH = canvas.clientHeight || canvas.height; + if (canvas.width !== Math.round(cssW * dpr) || canvas.height !== Math.round(cssH * dpr)) { + canvas.width = Math.round(cssW * dpr); + canvas.height = Math.round(cssH * dpr); + } + const ctx = canvas.getContext("2d"); + ctx.setTransform(dpr, 0, 0, dpr, 0, 0); + return { ctx, W: cssW, H: cssH }; +} + +function drawGridlines(ctx, W, H, padX, padY) { + ctx.strokeStyle = "#1e1e1e"; + ctx.lineWidth = 1; + for (let g = 1; g < 4; g++) { + const y = Math.round(padY + (g / 4) * (H - 2 * padY)) + 0.5; + ctx.beginPath(); + ctx.moveTo(padX, y); + ctx.lineTo(W - padX, y); + ctx.stroke(); + } +} + +// σ + Δ overlaid; each series normalised to its own max for comparable shapes. +function drawSigmaDeltaGraph(canvas, sigmas, deltas, step, totalSteps, hoverStep, dbCurve, lockedStep) { + const { ctx, W, H } = syncCanvasDPR(canvas); + const padX = GRAPH_PAD_X, padY = 3; + const iW = W - 2 * padX, iH = H - 2 * padY; + ctx.clearRect(0, 0, W, H); + drawGridlines(ctx, W, H, padX, padY); + + const n = sigmas?.length || 0; + const xSteps = Math.max(totalSteps || n, n, deltas?.length || 0); + const xAt = i => padX + (i / Math.max(1, xSteps - 1)) * iW; + + let sYAt = null; + if (n > 1) { + let sMax = -Infinity, sMin = Infinity; + for (const s of sigmas) { if (s > sMax) sMax = s; if (s < sMin) sMin = s; } + if (sMin > 0) sMin = 0; + const sRange = Math.max(sMax - sMin, 1e-6); + sYAt = v => padY + (1 - (v - sMin) / sRange) * iH; + + ctx.strokeStyle = "rgba(208, 208, 208, 0.55)"; + ctx.lineWidth = 1; + ctx.setLineDash([3, 3]); + ctx.beginPath(); + for (let i = 0; i < n; i++) { + const px = padX + (i / (n - 1)) * iW; + const py = sYAt(sigmas[i]); + if (i === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py); + } + ctx.stroke(); + ctx.setLineDash([]); + + const i = Math.max(0, Math.min(n - 1, step)); + const mx = padX + (i / Math.max(1, n - 1)) * iW; + const my = sYAt(sigmas[i]); + ctx.fillStyle = "#d0d0d0"; + ctx.beginPath(); + ctx.arc(mx, my, 2.5, 0, Math.PI * 2); + ctx.fill(); + } + + let dYAt = null; + if (deltas && deltas.length >= 1) { + let dMax = -Infinity; + for (const v of deltas) if (Number.isFinite(v) && v > dMax) dMax = v; + const dRange = Math.max(dMax, 1e-6); + dYAt = v => padY + (1 - v / dRange) * iH; + + // delta[i] is plotted at boundary (i+1); flat-extend delta[0] back to boundary 0. + const lastB = deltas.length; + ctx.beginPath(); + ctx.moveTo(xAt(0), H - padY); + ctx.lineTo(xAt(0), dYAt(deltas[0])); + for (let i = 0; i < deltas.length; i++) ctx.lineTo(xAt(i + 1), dYAt(deltas[i])); + ctx.lineTo(xAt(lastB), H - padY); + ctx.closePath(); + ctx.fillStyle = "rgba(230, 126, 34, 0.15)"; + ctx.fill(); + + ctx.strokeStyle = "#e67e22"; + ctx.lineWidth = 1.3; + if (deltas.length === 1) { + ctx.fillStyle = "#e67e22"; + ctx.beginPath(); + ctx.arc(xAt(1), dYAt(deltas[0]), 2, 0, Math.PI * 2); + ctx.fill(); + } else { + ctx.beginPath(); + for (let i = 0; i < deltas.length; i++) { + const px = xAt(i + 1), py = dYAt(deltas[i]); + if (i === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py); + } + ctx.stroke(); + } + } + + // SamplerDetailBoost curve. `null` entries = outside the gate (drawn as faint baseline), + // finite entries = active (drawn as the bright cyan line, auto-scaled to its own peak). + if (Array.isArray(dbCurve) && dbCurve.length > 1) { + let dbMaxAbs = 0; + for (const v of dbCurve) if (Number.isFinite(v) && Math.abs(v) > dbMaxAbs) dbMaxAbs = Math.abs(v); + const dbXAt = i => padX + (i / Math.max(1, dbCurve.length - 1)) * iW; + const baselineY = H - padY - 0.5; + const isActive = v => Number.isFinite(v); + + ctx.strokeStyle = "rgba(120, 200, 220, 0.25)"; + ctx.lineWidth = 1; + ctx.beginPath(); + let inactiveOpen = false; + for (let i = 0; i < dbCurve.length; i++) { + if (!isActive(dbCurve[i])) { + if (!inactiveOpen) { ctx.moveTo(dbXAt(i), baselineY); inactiveOpen = true; } + else ctx.lineTo(dbXAt(i), baselineY); + } else { + inactiveOpen = false; + } + } + ctx.stroke(); + + if (dbMaxAbs > 1e-9) { + const dbYAt = v => padY + (1 - Math.abs(v) / dbMaxAbs) * iH; + ctx.strokeStyle = "rgba(120, 200, 220, 0.85)"; + ctx.fillStyle = "rgba(120, 200, 220, 0.85)"; + ctx.lineWidth = 1.2; + ctx.beginPath(); + let segStart = -1; + for (let i = 0; i <= dbCurve.length; i++) { + const v = i < dbCurve.length ? dbCurve[i] : null; + if (isActive(v)) { + if (segStart < 0) { segStart = i; ctx.moveTo(dbXAt(i), dbYAt(v)); } + else ctx.lineTo(dbXAt(i), dbYAt(v)); + } else if (segStart >= 0) { + if (i - segStart === 1) { + const v0 = dbCurve[segStart]; + ctx.fillRect(dbXAt(segStart) - 1, dbYAt(v0) - 1, 2, 2); + } + segStart = -1; + } + } + ctx.stroke(); + } + } + + if (lockedStep != null && lockedStep >= 0 && lockedStep < xSteps) { + const lx = xAt(lockedStep) + 0.5; + ctx.strokeStyle = "rgba(245, 200, 60, 0.9)"; + ctx.lineWidth = 1.2; + ctx.setLineDash([4, 2]); + ctx.beginPath(); + ctx.moveTo(lx, padY); + ctx.lineTo(lx, H - padY); + ctx.stroke(); + ctx.setLineDash([]); + } + + if (hoverStep != null && hoverStep >= 0 && hoverStep < xSteps) { + const hx = xAt(hoverStep) + 0.5; + ctx.strokeStyle = "rgba(208, 208, 208, 0.5)"; + ctx.lineWidth = 1; + ctx.beginPath(); + ctx.moveTo(hx, padY); + ctx.lineTo(hx, H - padY); + ctx.stroke(); + if (sYAt && hoverStep < n) { + ctx.fillStyle = "#d0d0d0"; + ctx.beginPath(); + ctx.arc(hx - 0.5, sYAt(sigmas[hoverStep]), 2.5, 0, Math.PI * 2); + ctx.fill(); + } + // delta[k-1] is plotted at boundary k. + if (dYAt && hoverStep >= 1 && (hoverStep - 1) < deltas.length) { + ctx.fillStyle = "#e67e22"; + ctx.beginPath(); + ctx.arc(hx - 0.5, dYAt(deltas[hoverStep - 1]), 2.5, 0, Math.PI * 2); + ctx.fill(); + } + } +} + +// totalSteps fixes the x-axis so the line grows left-to-right, not stretching to fill. +function drawLineGraph(canvas, values, totalSteps) { + const { ctx, W, H } = syncCanvasDPR(canvas); + const padX = GRAPH_PAD_X, padY = 3; + const iW = W - 2 * padX, iH = H - 2 * padY; + ctx.clearRect(0, 0, W, H); + drawGridlines(ctx, W, H, padX, padY); + if (!values || values.length < 1) return; + + let vMax = -Infinity, vMin = Infinity; + for (const v of values) { if (v > vMax) vMax = v; if (v < vMin) vMin = v; } + if (vMin > 0) vMin = 0; + const vRange = Math.max(vMax - vMin, 1e-6); + + const xSteps = Math.max(totalSteps || values.length, values.length); + + ctx.beginPath(); + ctx.moveTo(padX, H - padY); + for (let i = 0; i < values.length; i++) { + const px = padX + (i / Math.max(1, xSteps - 1)) * iW; + const py = padY + (1 - (values[i] - vMin) / vRange) * iH; + ctx.lineTo(px, py); + } + ctx.lineTo(padX + ((values.length - 1) / Math.max(1, xSteps - 1)) * iW, H - padY); + ctx.closePath(); + ctx.fillStyle = "rgba(230, 126, 34, 0.15)"; + ctx.fill(); + + ctx.strokeStyle = "#e67e22"; + ctx.lineWidth = 1.3; + ctx.beginPath(); + for (let i = 0; i < values.length; i++) { + const px = padX + (i / Math.max(1, xSteps - 1)) * iW; + const py = padY + (1 - (values[i] - vMin) / vRange) * iH; + if (i === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py); + } + ctx.stroke(); +} + +app.registerExtension({ + name: "KJNodes.ModelPreviewOverride", + async beforeRegisterNodeDef(nodeType, nodeData) { + if (nodeData?.name !== "ModelPreviewOverrideKJ") return; + + chainCallback(nodeType.prototype, "onNodeCreated", function () { + ensureStyles(); + const node = this; + + const root = el("div", "kj-pov-root"); + + const imageArea = el("div", "kj-pov-image-area", root); + // Double-buffered: decode() on the visible-to-be element so the bitmap is reused. + const imgA = el("img", "kj-pov-img", imageArea); + const imgB = el("img", "kj-pov-img", imageArea); + imgA.draggable = false; + imgB.draggable = false; + imgB.style.opacity = "0"; + let visibleImg = imgA; + let pendingImg = imgB; + const img = imgA; // alias used by class-toggle code; applied to both buffers + // WebP path: ImageDecoder → VideoFrame[] → canvas, driven by a global timer. + const videoCanvas = el("canvas", "kj-pov-img", imageArea); + videoCanvas.style.opacity = "0"; + const videoCtx = videoCanvas.getContext("2d"); + // MP4 path: double-buffered